Глава 4

Серверное ПО и проксирование

Разбираемся кто принимает HTTP-запрос на стороне сервера, как устроены реверс-прокси и чем оборачивается проксирование для информации о клиенте.

📖 ~20 мин Основы

Типы серверного ПО

Мы разобрали HTTP, заголовки и TLS. Но кто на стороне сервера всё это обрабатывает? Между клиентом и приложением стоит серверное ПО. По роли оно делится на три типа.

📁
Веб-сервер
отдаёт файлы с диска
Клиент
nginx
💾 файл
Принимает запрос и отдаёт готовый файл с диска. HTML, картинка, CSS, JS. Никакой бизнес-логики — только путь + Host → файл.
ApachenginxlighttpdIIS
🔀
Реверс-прокси
передаёт запрос бэкенду
Клиент
прокси
бэкенд
Стоит между клиентом и приложением. Принимает запрос, передаёт дальше. Клиент не знает о бэкенде ничего — он общается только с прокси.
nginxEnvoyTraefikSquid
⚖️
Балансировщик
распределяет по бэкендам
запрос 1
LB
backend-1
запрос 2
LB
backend-2
запрос 3
LB
backend-3
Частный случай реверс-прокси. Выбирает один из нескольких бэкендов. Алгоритмы: round-robin, least connections, ip-hash.
HAProxyF5AWS ALB
ℹ️
Реверс-прокси vs прямой прокси Прямой (forward) прокси стоит на стороне клиента — помогает клиенту выйти в интернет (корпоративный прокси, VPN). Реверс-прокси стоит на стороне сервера — защищает бэкенд. Клиент даже не знает что реверс-прокси существует.

А что такое бэкенд?

Бэкенд — это серверное приложение которое содержит бизнес-логику. Если веб-сервер просто отдаёт файлы с диска, а реверс-прокси просто передаёт запросы — то бэкенд думает: проверяет права, лезет в базу данных, считает что-то, формирует ответ.

Один продукт — несколько ролей

В реальности один продукт часто совмещает несколько ролей. nginx — классический пример: в одном конфиге он одновременно выполняет все три роли в зависимости от того что делает с запросом.

🔧 nginx — один процесс, три роли
Веб-сервер
Запрос на /static/ → отдаёт файл с диска
location /static/ { root /var/www; }
Реверс-прокси
Запрос на /api/ → передаёт бэкенду
location /api/ { proxy_pass http://backend:8080; }
Балансировщик
Распределяет между несколькими инстансами
upstream backend { server 10.0.0.2; server 10.0.0.3; }

Роль определяется не продуктом, а тем что он делает с конкретным запросом. HAProxy — в первую очередь балансировщик, но умеет и проксировать. Apache — в первую очередь веб-сервер, но умеет и проксировать через модули.

CDN — реверс-прокси ближе к пользователю

CDN (Content Delivery Network) — географически распределённая сеть серверов, каждый из которых работает как кеширующий реверс-прокси. Принцип тот же что у обычного реверс-прокси — принять запрос клиента и отдать ответ — но серверы CDN расположены в разных городах и странах, ближе к пользователям.

⚠ Без CDN
👤
Пользователь
🇷🇺 Москва
🖥️
Сервер
🇳🇱 Амстердам
✓ С CDN
👤
Пользователь
🇷🇺 Москва
🌍
CDN-узел
🇷🇺 Москва
✓ ответ из кеша
🖥️
Origin-сервер
🇳🇱 Амстердам

CDN-узел хранит копии ответов. Если копии нет или она устарела — CDN сам запрашивает данные у основного сервера (origin), сохраняет и отдаёт клиенту. Клиент не знает что общается с CDN — для него это просто сервер.

Основное применение — статика: картинки, CSS, JavaScript, шрифты, видео. Эти файлы одинаковы для всех пользователей и редко меняются — идеальный случай для кеширования.

С точки зрения архитектуры CDN — ещё один слой в цепочке между клиентом и бэкендом:

Архитектура с CDN
Клиент
  │
  ▼
CDN-узел      ← кеширующий реверс-прокси, географически близко к пользователю
  │  если кеш есть → отвечает сразу
  │  если нет     → идёт к origin
  ▼
HAProxy / nginx / Бэкенд   ← основная инфраструктура
ℹ️
Как CDN решает что кешировать Теми же HTTP-заголовками — Cache-Control, Expires, Vary. CDN читает директивы которые ставит бэкенд и подчиняется им — точно так же как прокси. Подробно разберём в главе 6 (Кеширование).

Потеря информации о клиенте

Когда запрос проходит через реверс-прокси, происходят две важные вещи которые нужно понимать.

Проблема 1: потеря IP клиента

Бэкенд видит TCP-соединение не от клиента, а от прокси. В $remote_addr на бэкенде будет IP прокси — не IP реального пользователя.

Как теряется IP при прохождении через прокси
🌐
Клиент
TCP от 185.220.101.42
Реальный IP пользователя
⚖️
HAProxy 10.0.0.1
принял от 185.220.101.42
отправил от 10.0.0.1
IP клиента заменился на IP HAProxy
🔀
nginx 10.0.0.5
принял от 10.0.0.1
отправил от 10.0.0.5
Снова замена — теперь виден только nginx
🖥️
Бэкенд
$remote_addr = 10.0.0.5
Бэкенд видит nginx, а не реального клиента

Почему это проблема:

Решение — специальные заголовки проксирования. Каждый прокси добавляет заголовок с оригинальным IP, и следующее звено может его прочитать.

Проблема 2: потеря протокола

Клиент подключается по HTTPS. Реверс-прокси терминирует TLS и передаёт бэкенду по HTTP. Бэкенд видит HTTP-соединение и не знает что клиент использовал HTTPS.

Это ломает редиректы: бэкенд формирует ссылку http://site.ru/page вместо https://site.ru/page. Решение — заголовок X-Forwarded-Proto.

Заголовки проксирования

Заголовки проксирования — механизм передачи информации о реальном клиенте через цепочку посредников. Без них бэкенд слеп: не знает кто на самом деле сделал запрос.

X-Forwarded-For
185.220.101.42, 10.0.0.1
широко используется
Цепочка IP-адресов через которые прошёл запрос. Каждый прокси дописывает IP предыдущего звена в конец. Самый левый IP — оригинальный клиент. Понимают все фреймворки, логгеры и WAF.
X-Real-IP
185.220.101.42
нестандартный
Просто IP клиента — одно значение, без цепочки. Проще парсить. Нестандартный заголовок (нет в RFC), поэтому понимают не все фреймворки. Обычно передают оба: X-Real-IP для простоты, X-Forwarded-For для совместимости.
X-Forwarded-Proto
https
широко используется
Протокол по которому клиент подключился к реверс-прокси — http или https. Нужен потому что соединение прокси → бэкенд идёт по HTTP, и бэкенд без этого заголовка не знает что снаружи было HTTPS. Без него бэкенд формирует редиректы на http:// — пользователь получает ошибки mixed content.
X-Forwarded-Host
example.com
нестандартный
Оригинальный Host из запроса клиента. Обычно реверс-прокси передаёт Host как есть через proxy_set_header Host $host и отдельный X-Forwarded-Host не нужен. Но если прокси подменяет Host при маршрутизации — оригинальное значение сохраняется здесь.
Forwarded
for=185.220.101.42;proto=https;host=example.com
RFC 7239
Стандартизованная замена всем X-Forwarded-* заголовкам — один заголовок вместо четырёх. На практике X-Forwarded-For настолько укоренился что Forwarded используется редко.

Как X-Forwarded-For накапливается в цепочке

Пример: Клиент → HAProxy → nginx → Бэкенд
1
Клиент → HAProxy
# Клиент не добавляет заголовок
X-Forwarded-For: —

# HAProxy добавляет IP клиента:
X-Forwarded-For: 185.220.101.42
2
HAProxy → nginx
# nginx дописывает IP HAProxy в конец:
X-Forwarded-For: 185.220.101.42, 10.0.0.1
3
nginx → Бэкенд
# Бэкенд читает первый IP — это реальный клиент:
X-Forwarded-For: 185.220.101.42, 10.0.0.1

✓ первый IP = 185.220.101.42 → реальный клиент
Левый — всегда оригинальный клиент. Правее — промежуточные прокси.
⚠️
X-Forwarded-For можно подделать Клиент может сам добавить X-Forwarded-For: 1.2.3.4 до отправки запроса. Нельзя слепо доверять первому IP из цепочки. Нужен механизм доверия — set_real_ip_from.

Механизм set_real_ip_from

Как отличить реальный IP клиента от подделки в заголовке? Директива set_real_ip_from в nginx решает это через список доверенных прокси.

  1. Перечисляем IP-адреса прокси которым доверяем (наши собственные)
  2. nginx смотрит откуда физически пришёл TCP-пакет ($remote_addr)
  3. Если это доверенный прокси — берёт IP из X-Forwarded-For
  4. Если это кто-то посторонний — игнорирует X-Forwarded-For
Конфиг nginx + алгоритм разбора
set_real_ip_from 10.0.0.1; # IP HAProxy — доверяем ему
real_ip_header X-Forwarded-For; # из какого заголовка брать IP
real_ip_recursive on; # перебирать цепочку справа налево
Входящий заголовок: X-Forwarded-For: 185.220.101.42, 10.0.0.1
🔍
Читаем цепочку справа налево
Берём 10.0.0.1 — есть ли в set_real_ip_from?
Да — это наш доверенный прокси (HAProxy). Пропускаем, берём следующий IP слева.
🔍
Берём 185.220.101.42 — есть ли в set_real_ip_from?
Нет — значит это и есть реальный клиент. Останавливаемся.
$remote_addr = 185.220.101.42

Защита от подделки

Что если злоумышленник сам вставит поддельный IP в заголовок?

⚠ Попытка подделки
# Злоумышленник (99.88.77.66) отправляет:
X-Forwarded-For: 1.2.3.4

# HAProxy дописывает реальный IP:
X-Forwarded-For: 1.2.3.4, 99.88.77.66
✓ nginx с real_ip_recursive on
# Читает справа налево:
99.88.77.66 — в set_real_ip_from? Нет
→ стоп, это реальный клиент

$remote_addr = 99.88.77.66
# Подделка не сработала ✓

Механизм работает потому что HAProxy (наш доверенный прокси) всегда дописывает реальный IP в конец цепочки. Злоумышленник может подделать начало — но не конец.

Злоумышленник подключается напрямую к nginx

Сценарий
# Злоумышленник (99.88.77.66) идёт напрямую в nginx, минуя HAProxy:
X-Forwarded-For: 1.2.3.4

# nginx проверяет: $remote_addr = 99.88.77.66
# 99.88.77.66 — в set_real_ip_from? Нет.
# → X-Forwarded-For игнорируется полностью.

$remote_addr = 99.88.77.66  # реальный IP злоумышленника

Полная картина

Соберём всё вместе. Вот как выглядит реальная цепочка с заголовками на каждом уровне.

Путь запроса: Клиент → HAProxy → nginx → Бэкенд
Клиент
Пользователь
185.220.101.42
GET /api/users HTTP/1.1
Host: example.com
HAProxy
Балансировщик
10.0.0.1
Добавляет заголовки с информацией о клиенте:
X-Forwarded-For: 185.220.101.42
X-Forwarded-Proto: https
nginx
Реверс-прокси
10.0.0.5
set_real_ip_from → восстанавливает IP. Дописывает заголовки:
$remote_addr 185.220.101.42 (после set_real_ip_from)
X-Forwarded-For: 185.220.101.42, 10.0.0.1
X-Real-IP: 185.220.101.42
Бэкенд
Бизнес-логика
10.1.11.35
Получает полную картину о клиенте:
X-Real-IP: 185.220.101.42
X-Forwarded-For: 185.220.101.42, 10.0.0.1
X-Forwarded-Proto: https

На каждом уровне — своя роль. Каждый делает своё дело и передаёт дальше. Заголовки проксирования — клей который не даёт информации потеряться по дороге.

Итог

Заголовок Что передаёт
X-Forwarded-For Цепочка IP: клиент, прокси-1, прокси-2...
X-Real-IP Один IP клиента — проще парсить
X-Forwarded-Proto Протокол клиента (http / https)
X-Forwarded-Host Оригинальный Host из запроса клиента