# теория · шаг 1 из 6
Что бэкенд перестаёт видеть
Коротко
Между клиентом и приложением появился посредник, и приложение теперь видит его,
а не клиента. Потерянное возвращают четырьмя строками proxy_set_header.
- По умолчанию бэкенд получает
Host: app:8000- адрес изproxy_pass, а не имя сайта. IP клиента и схему он не видит вовсе. - Хоп-хоп-заголовки (
Connection,Upgrade) nginx до бэкенда не доносит. proxy_set_headerнаследуется всё или ничего: одна своя строка вlocationотменяет весь набор сверху. Симптом -Hostмолча вернулся кapp:8000.- Пустое значение убирает заголовок. У
Hostне убирает: он возвращается к умолчанию.
Если четыре строки набора знакомы и правило «всё или ничего» тоже - листай до «Теперь сам».
Сначала ответь сам
server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
location /api/ {
proxy_set_header X-Api-Key "secret";
proxy_pass http://app:8000/;
}
}
Заголовки объявлены на уровне server, в location добавлен ещё один. Какой
Host увидит приложение на запросе к /api/cart?
Стенд отвечает app:8000. Три строки сверху не действуют, потому что в блоке
появилась своя. Отладка такого выглядит абсурдно: «мы же передаём Host, вот он,
строкой выше».
На что это похоже
Курьерская служба забирает у тебя посылку и везёт получателю. В накладной на двери получателя будет написан адрес склада, с которого приехала машина, - это и правда тот адрес, откуда посылку привезли.
Получатель, которому нужно ответить отправителю, смотрит в накладную и пишет на склад. Ошибки тут нет: посредник честно назвал себя. Просто про исходного отправителя в накладной не сказано ничего, и, если это важно, отправителя вписывают отдельной строкой.
Механизм: что меняется на границе
Заголовки клиента nginx передаёт почти все: User-Agent, Accept, Cookie,
Authorization. Отбрасывает он хоп-хоп-заголовки - те, что относятся к
одному участку пути, а не ко всему маршруту: Connection, Upgrade,
Keep-Alive, Transfer-Encoding. Их отсутствие ниже по цепочке правильно и
станет темой отдельной главы про WebSocket. Замер на nginx 1.31.5: к бэкенду
он ходит по HTTP/1.1 и Connection не шлёт вовсе. Адрес 172.24.0.5 на схеме -
адрес самого nginx в сети Docker: его приложение и видит вместо клиента.
Правило
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
$host - имя, которое запросил клиент: в нижнем регистре и без порта. Стенд на
запросе с Host: HOST.local:8443 даёт $host = host.local, $http_host =
HOST.local:8443, $proxy_host = app:8000. Нужен порт - бери $http_host,
но помни, что это сырая строка от клиента.
$scheme - схема запроса к nginx, http или https. $proxy_add_x_forwarded_for
берёт пришедший заголовок и дописывает к нему адрес клиента, открывшего
соединение ($remote_addr). Пришло 203.0.113.9 - уйдёт
203.0.113.9, 172.24.0.1.
Разбор: что видит бэкенд до и после
Бэкендом на стенде служит nc -l -p 8001 - программа, которая печатает
пришедший запрос целиком. Два блока: /raw/ проксирует без единой строки
proxy_set_header, /full/ - с набором из четырёх. Клиент в обоих случаях
один и тот же:
curl -s -H 'X-Forwarded-For: 203.0.113.9' -H 'Cookie: a=1' -H 'Accept-Encoding: gzip' http://shop.local/raw/cart
GET /raw/cart HTTP/1.1
Host: 127.0.0.1:8001
User-Agent: curl/8.22.0
Accept: */*
X-Forwarded-For: 203.0.113.9
Cookie: a=1
Accept-Encoding: gzip
curl -s -H 'X-Forwarded-For: 203.0.113.9' -H 'Cookie: a=1' -H 'Accept-Encoding: gzip' http://shop.local/full/cart
GET /full/cart HTTP/1.1
Host: shop.local
X-Real-IP: 127.0.0.1
X-Forwarded-For: 203.0.113.9, 127.0.0.1
X-Forwarded-Proto: http
User-Agent: curl/8.22.0
Accept: */*
Cookie: a=1
Accept-Encoding: gzip
В первом дампе Host - адрес из proxy_pass, на стенде 127.0.0.1:8001
(в Docker это было бы app:8000).
Обрати внимание на первый дамп: X-Forwarded-For: 203.0.113.9 доехал до бэкенда
без всякой настройки - его прислал клиент, а nginx передал как обычный
заголовок. Это и есть причина, по которой заголовку нельзя верить: любой может
написать в нём что угодно, и без специальных мер это доедет до приложения.
Разбор: наследование всё или ничего
Правило то же, что у add_header из второго раздела. Замер по четырём блокам,
у каждого свой набор строк в location при трёх объявленных на уровне server:
| в location | что получил бэкенд |
|---|---|
| ничего | Host: shop.local, X-Real-IP на месте |
proxy_set_header X-Api-Key secret |
Host - адрес из proxy_pass, X-Real-IP пропал |
proxy_set_header Accept-Encoding "" |
Accept-Encoding убран, набор сверху пропал |
proxy_set_header Host "" |
Host - адрес из proxy_pass, набор сверху пропал |
Третья строка показывает второй приём: пустое значение убирает заголовок.
Так снимают Accept-Encoding, когда от бэкенда нужен несжатый ответ.
Четвёртая показывает исключение: Host убрать нельзя. Пустое значение
возвращает его к умолчанию, то есть к $proxy_host, - запрос без Host в
HTTP/1.1 недопустим, и nginx его не отправит.
Лечится всё это одинаково: общий набор держат отдельным файлом и подключают в
каждый проксирующий блок. Файл кладут рядом с nginx.conf, а не в conf.d:
всё из conf.d/*.conf nginx подключает в http сам, и набор оказался бы там
вторым экземпляром.
include /etc/nginx/proxy_headers.conf;
proxy_set_header X-Api-Key "secret";
proxy_pass http://app:8000;
Что ломается без этого
Host: app:8000 уезжает в места, где его никто не ждёт. Приложение строит по
нему абсолютные ссылки, и человек получает письмо со ссылкой на сброс пароля
вида http://app:8000/reset?token=.... Проверка источника формы не проходит,
потому что домен в заголовке не совпал с настроенным. Мультидоменное приложение
показывает не тот сайт: имени, по которому выбирать, у него нет.
Потерянная схема даёт цикл: сайт живёт на https, фреймворк видит http и
отправляет клиента на https, оттуда снова приходит запрос со схемой http.
Браузер сдаётся после десятка кругов и пишет ERR_TOO_MANY_REDIRECTS.
Зачем это в работе
Четыре строки набора - первое, что пишут в любом проксирующем блоке, и первое,
что проверяют, когда приложение ведёт себя странно. Увидеть, что nginx на самом
деле отправил, можно тем же приёмом, что в дампах выше: временно направь
проблемный блок на proxy_pass http://127.0.0.1:8001;, запусти рядом
nc -l -p 8001 и отправь запрос. Приём с add_header X-Debug-Host $host тут не
годится: он печатает значение переменной, а не заголовок, который ушёл, - и
поломку наследования не покажет.
Теперь сам
В блоке server объявлены все четыре строки набора. В location /upload/
добавили proxy_request_buffering off; - и загрузка перестала работать, а
приложение пишет, что не узнаёт домен. Виноват ли добавленный off?
Нет. proxy_request_buffering - другая директива, наследование
proxy_set_header она не трогает. Ищи в этом же блоке добавленный
proxy_set_header - скорее всего, рядом появилась строка вроде
proxy_set_header Content-Type ..., и она увела весь набор. Проверяется
дампом заголовков на бэкенде.
Главное
За прокси приложение теряет три вещи: имя сайта в Host, адрес клиента и
схему. Возвращают их четыре строки: Host $host, X-Real-IP $remote_addr,
X-Forwarded-For $proxy_add_x_forwarded_for, X-Forwarded-Proto $scheme.
proxy_set_header наследуется всё или ничего, поэтому набор держат отдельным
файлом и подключают через include в каждый блок. Пустое значение убирает
заголовок, кроме Host - он возвращается к адресу из proxy_pass. И помни, что
X-Forwarded-For от клиента доезжает до бэкенда сам по себе: доверять ему без
realip нельзя.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий