# теория · шаг 2 из 5
ssl_preread, pass и маршрутизация без URI
Коротко
ssl_preread on читает ClientHello - первое сообщение клиента, которое ещё
не зашифровано, - и достаёт оттуда имя (SNI), список протоколов (ALPN) и
версию TLS. По ним map или server_name выбирает бэкенд, а соединение уходит
дальше нетронутым. Цена: nginx ждёт этих байтов, поэтому протокол, где
первым говорит сервер, под ssl_preread висит до preread_timeout и до
бэкенда не доходит вовсе. Директива pass передаёт соединение другому сокету
того же nginx, сохраняя адрес клиента.
Знаешь про SNI-маршрутизацию - листай до «Что ломается без этого»: там разбор про протоколы, где первым говорит сервер.
На 443-м порту стоит stream с ssl_preread, и туда же завели почтовый релей.
Почта работает. Что увидит почтовый клиент?
server {
listen 465;
ssl_preread on;
proxy_pass mail-relay:465;
}
Клиент подключается и ждёт приветствия сервера. nginx подключается и ждёт
ClientHello от клиента. Ждут оба, тридцать секунд, после чего nginx закрывает
соединение, ни разу не обратившись к бэкенду. В логе при этом 200.
На что это похоже
Диспетчер на входе слышит первое слово посетителя - «в бухгалтерию» - и по нему открывает нужную дверь. Он не читает документы, он слышит одно слово.
Продолжение аналогии и объясняет ловушку: если посетитель молчит и ждёт, пока поздороваются с ним, диспетчер будет стоять напротив ровно столько, сколько ему велено ждать, и потом закроет дверь перед человеком, который никуда не спешил.
Механизм: что видно до шифрования
Правило
ssl_preread on заполняет три переменные и ничего не расшифровывает. Дальше
выбор бэкенда делается одним из двух способов: картой из тринадцатого раздела
или виртуальным сервером с server_name (с 1.25.5). Соединение уходит бэкенду
как есть, включая сам ClientHello.
map $ssl_preread_server_name $upstream {
default web-backend;
vpn.shop.local vpn-gateway;
}
server {
listen 443;
ssl_preread on;
proxy_pass $upstream;
}
Разбор: три соединения и три набора переменных
Прогон по одному и тому же блоку, лог со всеми тремя переменными:
ups=vpn-gateway sni=vpn.shop.local alpn=h2 proto=TLSv1.3
ups=web-backend sni= alpn= proto=TLSv1.3
ups=web-backend sni=web.shop.local alpn=http/1.1,h2 proto=TLSv1.2
Первая строка - обычный клиент с именем, карта отработала. Вторая - клиент,
подключившийся по адресу без SNI: имя пустое, сработал default, а версия
TLS определилась. Третья показывает, что ALPN приезжает списком в том
порядке, в каком его прислал клиент, а не в каком тебе удобно.
Отсюда два практических вывода. default в такой карте обязателен, иначе
proxy_pass получит пустую строку. А отсекать старый TLS по
$ssl_preread_protocol можно даже там, где имени нет.
Разбор: pass против проксирования на самого себя
Задача, ради которой директива и появилась: на 443 стоит stream, часть
соединений надо отдать своему же сайту. Два способа, один блок http,
печатающий $remote_addr:
server { listen 7005; pass 127.0.0.1:8443; }
server { listen 7015; proxy_pass 127.0.0.1:8443; }
через pass: https-server host=web.shop.local remote=172.25.0.4
через proxy_pass: https-server host=web.shop.local remote=127.0.0.1
pass передаёт соединение другому слушающему сокету этого же nginx, и адрес
клиента остаётся настоящим. proxy_pass открывает новое соединение, поэтому
сайт видит клиентом сам сервер - и в логах, и в limit_req, и в allow.
Раньше это лечили связкой proxy_protocol on плюс set_real_ip_from на
принимающей стороне; с 1.25.5 лечить нечего.
Что ломается без этого
Протокол, где первым говорит сервер. SMTP, IMAP, POP3, FTP, MySQL - клиент там ждёт приветствия. Замер: клиент подключился и молчит.
| конфиг | сколько ждал клиент | дошло до бэкенда |
|---|---|---|
ssl_preread on (умолчание 30 с) |
30,0 с, потом обрыв | нет |
ssl_preread on; preread_timeout 2s; |
2,0 с, потом обрыв | нет |
В логе у обеих сессий $status 200 и $upstream_addr равен прочерку.
То есть по коду в логе поломка неотличима от нормальной работы, а по прочерку -
отличима сразу. Лечится тем, что ssl_preread включают только на портах, где
первым говорит клиент; для остальных - отдельный блок без него.
server_name без ssl_preread. Конфиг проходит проверку без единого
предупреждения, а имя брать неоткуда - весь трафик уходит в первый блок:
server { listen 7070; server_name a.example; proxy_pass back-a:6001; }
server { listen 7070; server_name b.example; proxy_pass back-b:6002; }
На стенде оба соединения попали в back-a. Виртуальный сервер в stream живёт
только вместе с ssl_preread on или listen ... ssl - без них он выглядит
работающим и не работает.
Не-TLS трафик на порту с ssl_preread. Он не отбрасывается: nginx понимает,
что это не ClientHello, и пропускает соединение дальше с пустыми переменными.
Ждать тридцать секунд он будет только от того, кто молчит.
Зачем это в работе
Один внешний адрес и один 443-й порт на всё: сайт, VPN-шлюз, почтовый релей клиентов. Сертификаты остаются у сервисов - ты не терминируешь чужой TLS и не хранишь чужие ключи. Второй частый случай - контур, из которого наружу торчит единственный хост: по SNI разводишь несколько внутренних сервисов, не заводя им отдельных адресов.
allow и deny работают и здесь, с теми же правилами наследования, что в
двенадцатом разделе. Отличие в симптоме: вместо 403 клиент получает
оборванное соединение, а 403 остаётся в твоём логе. Если бэкенду нужен
настоящий адрес клиента - proxy_protocol on, и бэкенд получит перед данными
строку PROXY TCP4 172.25.0.4 172.25.0.3 57926 7006 - протокол, версия IP,
адрес клиента, адрес прокси, порт клиента, порт прокси.
Теперь сам
На 443 надо развести три сервиса по имени, а клиентов со старым TLS не пускать дальше nginx вовсе. Чего не хватает в заготовке?
map $ssl_preread_server_name $upstream {
vpn.shop.local vpn-gateway;
smtp.shop.local mail-relay;
web.shop.local web-backend;
}
server {
listen 443;
proxy_pass $upstream;
}
Ответ: нет ssl_preread on; - без неё переменная всегда пуста, и proxy_pass
получит пустое значение на любом соединении. Нет default в карте - клиент по
адресу без SNI не попадёт никуда. Отсечь старый TLS одной картой не выйдет:
нужна вторая по $ssl_preread_protocol, отдающая, например, web-backend
только для TLSv1.2 и TLSv1.3, - или return в отдельном блоке.
Главное
ssl_preread on читает открытую часть ClientHello и даёт
$ssl_preread_server_name, $ssl_preread_alpn_protocols и
$ssl_preread_protocol; расшифровки не происходит. Выбор бэкенда - картой или
виртуальным сервером по server_name (1.25.5), причём server_name без
ssl_preread молча отправляет всё в первый блок. Главная ловушка: nginx ждёт
первого сообщения клиента, поэтому на протоколе, где первым говорит сервер,
соединение умирает по preread_timeout (умолчание 30 секунд), не дойдя до
бэкенда, а в логе остаётся 200 с прочерком вместо адреса. pass (1.25.5)
передаёт соединение другому сокету того же nginx и сохраняет адрес клиента,
чего проксирование на 127.0.0.1 не делает.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий