1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 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?

/etc/nginx/conf.d/forward-proxy.confhttp
server {
    listen 3128;
    tunnel_pass $host:$port;
}
↳  выводnginx -t
nginx: [emerg] unknown "port" variable

Переменной $port в nginx нет. Дописать её неоткуда, а tunnel_pass $host; загрузится и упадёт уже в работе: no port in upstream "api.pay.example" и 500 клиенту. В CONNECT порт есть, но $host его не содержит.

На что это похоже

CONNECT - это просьба к телефонистке: «соедините меня с таким-то номером». Телефонистка проверяет, положено ли тебе звонить наружу, набирает номер, говорит «соединила» и втыкает провод в гнездо. Дальше она разговор не слышит: внутри TLS, и ключей у неё нет.

Отсюда и вся её работа: решить, звонить ли, набрать номер и записать в журнал, кто с кем соединялся. Содержимое разговора не её дело - и это не ограничение модуля, а свойство схемы.

Механизм: что происходит при CONNECT

CONNECT api.pay.example:443 allow, auth_basic, map resolver: имя в адрес 200 Connection established байты в обе стороны, TLS внутри обычный http не понимает
До ответа 200 работает вся http-обвязка, после - только перекладывание байтов

Правило

tunnel_pass пишется без аргументов: адрес назначения берётся из самого запроса. Имя оттуда надо разрешать в работе, поэтому resolver обязателен.

/etc/nginx/conf.d/forward-proxy.confhttp
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, и там она названа точно:

↳  вывод/var/log/nginx/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 (тринадцатый раздел):

/etc/nginx/conf.d/forward-proxy.confhttp
map $host $allowed_target {
    default                0;
    api.pay.example        1;
    .githubusercontent.com 1;
}
/etc/nginx/conf.d/forward-proxy.confhttp server
if ($allowed_target = 0) {
    return 403;
}

На стенде разрешённая цель даёт 200, запрещённая - 403 прямо на CONNECT, то есть клиент не успевает открыть соединение. Проверять $host здесь можно именно потому, что в CONNECT он равен имени назначения - в отличие от обычного запроса, где это имя твоего же сайта.

Что ломается без этого

Забытый resolver. nginx -t доволен, прокси поднимается, и каждый CONNECT возвращает 502. В error_log при этом написано прямым текстом:

↳  вывод/var/log/nginx/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. Пароль в приложении прописан верный. Где смотреть?

/etc/nginx/conf.d/forward-proxy.confhttp
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 есть только в коммерческой версии. Цель видит адрес прокси, а не клиента.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.