{ "index": 278, "slug": "editorial-2020-04-mechanism-reverse-proxy", "title": "Reverse proxy под капотом: два соединения, заголовки и таймауты", "excerpt": "Приложение видит не внешний запрос, а новый upstream-hop. Разбираем, как proxy меняет Host, forwarded-заголовки и время ожидания, и как проверить границу без догадок.", "contentHtml": "

Проблема часто выглядит как ошибка приложения. Пользователь открыл HTTPS, а код строит ссылку с http. В журнале все посетители имеют один адрес. После добавления proxy часть запросов заканчивается 502 или 504. Цена ошибки — не только один неудачный ответ. Неверная схема ломает редиректы и cookie. Ошибка в адресе клиента ломает лимиты, аудит и расследование. Слишком большой таймаут дольше держит соединения и маскирует зависший upstream.

\n

Тезис статьи простой: reverse proxy не является прозрачным проводом. Он принимает внешний HTTP-запрос, завершает один hop и создаёт новый запрос к upstream. На новой границе меняются peer address, часть заголовков, момент подключения и правила ожидания. Поэтому приложение нужно настраивать не на «оригинальный запрос вообще», а на явный договор: какие поля proxy передаёт, откуда они пришли, чему приложение доверяет и какой сигнал подтверждает каждый вывод.

\n

Механизм: у одного запроса есть два hop-а

\n

Клиент устанавливает соединение с proxy. Proxy выбирает server и location, проверяет маршрут, может завершить TLS, изменить URI, добавить заголовки и буферизовать тело. Затем proxy устанавливает отдельное соединение с upstream. Для приложения непосредственным соседом становится proxy, а не браузер. Это нормально. Ошибка возникает, когда код принимает внутренний peer или клиентский header за внешний факт без проверки границы.

\n

Схема работает так же. Если TLS завершается на Nginx, внешний hop может быть HTTPS, а соединение Nginx с приложением — HTTP. Значение $scheme на этом Nginx описывает именно вход в него. Если TLS завершился раньше, оно может не совпасть со схемой, которую видел клиент. В таком случае нужен один доверенный источник исходной схемы. Нельзя позволять приложению выбирать между несколькими заголовками по ситуации.

\n

То же относится к адресу. $remote_addr на Nginx обозначает непосредственную удалённую сторону входного соединения. Если перед Nginx стоит балансировщик, это может быть адрес балансировщика. X-Forwarded-For может содержать цепочку, которую сформировал предыдущий proxy или прислал клиент. Само имя заголовка не делает значение достоверным.

\n
Симптом → причина → проверка → действие
СимптомПричина или гипотезаПроверкаДействие
Приложение строит ссылку с httpTLS завершился на proxy, а приложение не получило согласованный признак схемыСверить место TLS termination, $scheme и поле, которое читает приложениеОставить один доверенный forwarded-сигнал и закрыть прямой обход приложения
Все пользователи имеют один адресПриложение видит peer proxy или неверно разбирает forwarded-цепочкуНарисовать hop-ы и проверить доверенные источники до включения realipЗадать список доверенных proxy; не брать первый элемент заголовка вслепую
502 появился после изменения маршрутаНеверный proxy_pass, URI, порт или недоступный upstreamПроверить итоговый location, адрес upstream и его statusИсправить маршрут; не увеличивать proxy_read_timeout
504 появляется только на одном endpointProxy не получил следующий байт ответа в пределах read timeoutСопоставить request_time, upstream-времена и безопасную запись приложенияНайти паузу в upstream или изменить лимит только для этого типа endpoint
Потоковый ответ обрывается при долгой паузеproxy_read_timeout ограничивает паузу между чтениями, а не всю передачуИзмерить интервалы между частями ответаВыделить отдельную policy для streaming; не переносить общий лимит на другие маршруты
\n

Таблица отделяет наблюдение от диагноза. Status 504 говорит о результате ожидания proxy, но не называет базу, GC, внешний API или код обработчика. Status 502 говорит о проблеме на пути к корректному upstream-ответу, но не выбирает между маршрутом, портом и самим процессом. Один сигнал даёт ветку проверки, а не готовое объяснение.

\n

Учебная конфигурация границы

\n

Ниже приведён минимальный пример для изолированного стенда. Имена, адрес 127.0.0.1 и значения таймаутов учебные. Пример показывает места договора, а не готовую production-конфигурацию.

\n
upstream app_backend {\n    server 127.0.0.1:3000;\n}\n\nserver {\n    listen 8080;\n    server_name _;\n\n    location / {\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        proxy_set_header Connection "";\n\n        proxy_connect_timeout 3s;\n        proxy_send_timeout 10s;\n        proxy_read_timeout 15s;\n        proxy_pass http://app_backend;\n    }\n}
\n

proxy_set_header Host $host задаёт значение явно. В этом примере X-Real-IP $remote_addr передаёт приложению адрес непосредственного peer текущего Nginx; он не становится адресом клиента сам по себе. X-Forwarded-For расширяет цепочку, а не доказывает, что каждый её элемент заслуживает доверия. X-Forwarded-Proto $scheme описывает схему входного hop-а именно этого Nginx. Если перед ним есть другой TLS-терминатор, контракт должен учитывать его. Нельзя копировать строку https только потому, что внешняя страница открывается по HTTPS.

\n

Таймауты отвечают на разные паузы. proxy_connect_timeout относится к установлению соединения с upstream. proxy_send_timeout относится к последовательным операциям записи запроса. proxy_read_timeout относится к паузе между последовательными операциями чтения ответа. Поэтому длительность ответа и read timeout — разные величины. Streaming может длиться дольше лимита, если upstream регулярно отправляет данные. Ответ без следующего байта может оборваться раньше, чем команда ожидает.

\n
\"Схема
Header становится полезным сигналом только вместе с источником и правилом доверия. Строка без границы не доказывает ни схему, ни адрес клиента.
\n

Как связать два hop-а в журнале

\n

Для первого разбора достаточно access-log с URI, итоговым status, $request_time, адресом upstream, его status и временами подключения, получения заголовков и ответа. Такой формат отвечает на узкий вопрос: proxy не подключился к upstream или подключился, но не дождался заголовков. Не нужно записывать cookie, authorization, тело запроса или полный query string. Лишние данные не делают гипотезу точнее.

\n
log_format proxy_boundary "$request_method $uri status=$status request=$request_time upstream=$upstream_addr upstream_status=$upstream_status\n                         connect=$upstream_connect_time header=$upstream_header_time response=$upstream_response_time";\n\naccess_log /path/to/proxy-boundary.log proxy_boundary;
\n

Синтетическая строка ниже показывает, как читать поля. Она учебная и не получена от реального Nginx.

\n
GET /health status=504 request=15.001 upstream=<backend> upstream_status=-\nconnect=0.001 header=- response=15.001
\n

Строка поддерживает гипотезу: соединение установилось быстро, но этот Nginx не получил заголовки ответа до заданного proxy_read_timeout 15s. Она не доказывает причину внутри приложения. Следующий шаг — проверить конкретный endpoint и безопасный токен в логе приложения. Если записи нет, это неизвестное, а не повод сочинять её содержание.

\n

Порядок проверки

\n
  1. Записать один симптом, один безопасный URL и цену ошибки. Не объединять неверную схему, адрес и 504 в одну причину.
  2. Нарисовать реальные hop-ы: клиент, TLS termination, Nginx, возможный предыдущий proxy и upstream. Отдельно указать, кто имеет право выставлять forwarded-поля.
  3. Проверить итоговый location, proxy_pass и все proxy_set_header. Смотреть нужно собранную конфигурацию, а не только фрагмент include-файла.
  4. Разделить connect, send и read timeout. Для выбранного endpoint указать, какую паузу ограничивает каждое значение.
  5. Выполнить один учебный запрос к маршруту без пользовательских данных. Сверить status и ответные заголовки с access-log и записью приложения по безопасному токену.
  6. Изменить одну подтверждённую границу и повторить тот же запрос. Если результат не изменился, сохранить отрицательный вывод и перейти к следующей строке таблицы.
  7. Зафиксировать откат и новый сигнал наблюдения. Увеличение таймаута без ожидаемого изменения в журнале не считается объяснённым исправлением.
\n

Ограничения и отрицательный путь

\n

Эта модель не делает forwarded-заголовок безопасным. Доверие задаёт топология и конфигурация. В Nginx realip-модуле адрес клиента меняется только для источников, которые явно разрешены через set_real_ip_from. Если прямой запрос может обойти доверенный proxy, приложение должно считать такие поля недоверенными или закрыть этот путь на сети.

\n

Модель также не выбирает таймауты за команду. Значение зависит от типа endpoint, бюджета клиента, лимита приложения, поведения upstream и числа одновременных соединений. Длинный timeout может уменьшить число быстрых ошибок, но увеличить очередь и расход ресурсов. Retry добавляет ещё одну попытку и может повторить неидемпотентное действие. Его нельзя добавлять как универсальное средство против 504.

\n

Отрицательный путь обязателен. Если после явного Host приложение всё ещё строит неверную ссылку, проверяют поле, которое читает код, и предыдущий TLS-терминатор. Если после изменения read timeout status не изменился, проверяют маршрут, доступность upstream и клиентский timeout. Если адрес остаётся адресом proxy, проверяют доверенную цепочку и прямой доступ. «Перезапустили — стало нормально» не подтверждает ни одну из этих гипотез.

\n

Проверяемый критерий готовности

\n

Граница готова, если команда может повторить один безопасный запрос и получить доказательства по каждому hop-у: внешний status и схема, итоговый набор переданных headers, запись proxy с request/upstream-временами и согласованная запись приложения. Для адреса известен список доверенных proxy. Для каждого timeout названа конкретная пауза. Отрицательный тест с недоверенным прямым header не меняет схему, права или адрес клиента. Если хотя бы одно условие не проверено, конфигурация ещё не готова к переносу в рабочий контур.

\n

Все конфигурации, команды и значения времени в статье учебные. Nginx, curl, staging и production здесь не запускались. Перед применением нужно проверить версию Nginx, реальную топологию, владельца конфигурации, политику журналирования и возможность безопасного отката.

\n

Проверяемые источники

" }