# теория · шаг 1 из 5
Forward proxy и модуль tunnel
Коротко
Обратный прокси стоит перед твоими серверами, forward proxy - перед твоими
клиентами: приложение из закрытого контура ходит наружу через него. Клиент шлёт
CONNECT host:port, прокси открывает TCP и перекладывает байты, не
расшифровывая TLS. С 1.31.0 это умеет ngx_http_tunnel_module: директива
tunnel_pass без аргументов плюс обязательный resolver. Вокруг работает
обычная http-обвязка - allow, auth_basic, map, лимиты, логи, - но отвечает
она клиенту прокси по-своему: не 401, а 407.
Нужен только рабочий конфиг - листай до «Правило». Разборы про коды ответа прокси и про белый список назначений.
Конфиг, который кочует по статьям про новый модуль. Что скажет nginx -t?
server {
listen 3128;
tunnel_pass $host:$port;
}
nginx: [emerg] unknown "port" variable
Переменной $port в nginx нет. Дописать её неоткуда, а tunnel_pass $host;
загрузится и упадёт уже в работе: no port in upstream "api.pay.example" и
500 клиенту. В CONNECT порт есть, но $host его не содержит.
На что это похоже
CONNECT - это просьба к телефонистке: «соедините меня с таким-то номером».
Телефонистка проверяет, положено ли тебе звонить наружу, набирает номер,
говорит «соединила» и втыкает провод в гнездо. Дальше она разговор не слышит:
внутри TLS, и ключей у неё нет.
Отсюда и вся её работа: решить, звонить ли, набрать номер и записать в журнал, кто с кем соединялся. Содержимое разговора не её дело - и это не ограничение модуля, а свойство схемы.
Механизм: что происходит при CONNECT
Правило
tunnel_pass пишется без аргументов: адрес назначения берётся из самого
запроса. Имя оттуда надо разрешать в работе, поэтому resolver обязателен.
server {
listen 10.0.0.5:3128;
resolver 10.0.0.2 ipv6=off;
tunnel_pass;
tunnel_connect_timeout 5s;
tunnel_read_timeout 60s;
satisfy any;
allow 10.0.0.0/8;
deny all;
auth_basic "Corporate proxy";
auth_basic_user_file /etc/nginx/.htpasswd;
auth_delay 2s;
access_log /var/log/nginx/proxy.log;
}
Явная форма tunnel_pass $host:$request_port; даёт ровно тот же результат -
$request_port в запросе CONNECT равен порту назначения. Она нужна, только
если адрес хочется подменить; в обычном случае пиши директиву без аргументов.
Рядом есть tunnel_buffer_size, tunnel_send_timeout, tunnel_next_upstream
и tunnel_bind (с какого адреса выходить наружу).
Разбор: четыре ответа прокси
Стенд с конфигом выше, curl -x. Коды сняты с провода, не из документации:
| что сделал клиент | код | заголовок в ответе |
|---|---|---|
CONNECT без пароля |
407 | Proxy-Authenticate: Basic realm="Corporate proxy" |
CONNECT с неверным паролем |
407 | тот же, задержка auth_delay 2 с |
обычный GET через прокси |
405 | - |
CONNECT на закрытый порт |
502 | - |
407 вместо привычного 401 - это и есть то, что добавили в 1.31.0 вместе с
модулем: auth_basic научился отвечать клиенту прокси на его языке. Браузер
и curl понимают только этот код, на 401 они пароль не подставят.
405 объясняет частое недоумение «прокси не работает с http-сайтами». Обычный
запрос с абсолютным адресом (GET http://site/ HTTP/1.1) туннелем не
обслуживается: модуль отвечает на CONNECT и только на него. Для http-целей
нужен отдельный location с обычным proxy_pass.
Причина 502 видна только в error_log, и там она названа точно:
connect() failed (111: Connection refused) while connecting to upstream,
request: "CONNECT ngx:9999 HTTP/1.1", upstream: "172.25.0.3:9999"
nosuchhost.invalid could not be resolved (3: Host not found),
request: "CONNECT nosuchhost.invalid:443 HTTP/1.1"
Разбор: белый список назначений
tunnel_allow_upstream есть только в коммерческой сборке - в открытой на неё
unknown directive. Белый список собирается тем же map, а if здесь уместен,
потому что внутри стоит return (тринадцатый раздел):
map $host $allowed_target {
default 0;
api.pay.example 1;
.githubusercontent.com 1;
}
if ($allowed_target = 0) {
return 403;
}
На стенде разрешённая цель даёт 200, запрещённая - 403 прямо на CONNECT,
то есть клиент не успевает открыть соединение. Проверять $host здесь можно
именно потому, что в CONNECT он равен имени назначения - в отличие от
обычного запроса, где это имя твоего же сайта.
Что ломается без этого
Забытый resolver. nginx -t доволен, прокси поднимается, и каждый
CONNECT возвращает 502. В error_log при этом написано прямым текстом:
no resolver defined to resolve api.pay.example,
request: "CONNECT api.pay.example:443 HTTP/1.1"
Это тот же класс дефекта, что server_name без ssl_preread из прошлой главы:
конфиг проверку проходит, ломается в работе.
Прокси, открытый наружу. Сканеры находят такой за часы, и дальше через твой
адрес идёт чужой трафик - от перебора паролей до рассылок. Минимум: слушать
внутренний адрес (listen 10.0.0.5:3128, а не listen 3128), список сетей,
пароль и белый список назначений. Три из четырёх пунктов в конфиге выше стоят
не для красоты.
Ожидание, что цель увидит клиента. Не увидит: соединение к назначению
открывает nginx, и адрес источника - его. Замер это подтверждает - сервис за
туннелем видит remote=172.25.0.3, адрес прокси. Туннель не подставляет ни
X-Forwarded-For (заголовков внутри TLS он не видит), ни PROXY protocol.
Зачем это в работе
Три случая, ради которых это заводят. Выпустить приложение из закрытого контура
строго к нескольким внешним API - и доказать аудиту, что больше никуда.
Собрать журнал того, куда ходят твои сервисы: в access.log прокси остаётся
"CONNECT api.pay.example:443 HTTP/1.1" 200, адрес назначения в
$upstream_addr и объём трафика в $bytes_sent. Терминировать исходящие
соединения на одном адресе, который проще внести в чужой белый список.
Ничего из этого не нужно - модуль не нужен тоже: forward proxy это ещё один сервис, который придётся охранять и чинить.
Теперь сам
Конфиг подняли, nginx -t зелёный, из контейнера приложения
curl -x http://10.0.0.5:3128 https://api.pay.example/ возвращает 407.
Пароль в приложении прописан верный. Где смотреть?
server {
listen 10.0.0.5:3128;
resolver 10.0.0.2 ipv6=off;
tunnel_pass;
auth_basic "Corporate proxy";
auth_basic_user_file /etc/nginx/.htpasswd;
}
Ответ: 407 означает, что пароль до проверки не дошёл или не подошёл, и
error_log скажет что именно - user "app": password mismatch против no user
в файле. Частая причина - приложение подставляет пароль только на 401:
библиотеки различают обычную и прокси-авторизацию, и учётные данные надо класть
в настройку прокси, а не в настройку запроса. Проверяется одной командой:
curl -x http://app:пароль@10.0.0.5:3128 ... должен дать 200.
Главное
Forward proxy пропускает через себя исходящий трафик: клиент шлёт
CONNECT host:port, прокси открывает TCP и перекладывает байты, TLS не
расшифровывая. В 1.31.0 появился ngx_http_tunnel_module: tunnel_pass без
аргументов ($port не существует, $host не содержит порта) плюс
обязательный resolver - без него зелёный nginx -t и 502 на каждом
запросе. Вокруг работает обычная http-обвязка, но авторизация отвечает 407 с
Proxy-Authenticate, а обычный GET через такой сервер получает 405. Белый
список назначений - map $host плюс return 403; tunnel_allow_upstream есть
только в коммерческой версии. Цель видит адрес прокси, а не клиента.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий