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

# теория · шаг 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, и туда же завели почтовый релей. Почта работает. Что увидит почтовый клиент?

/etc/nginx/nginx.confstream
server {
    listen 465;
    ssl_preread on;
    proxy_pass mail-relay:465;
}

Клиент подключается и ждёт приветствия сервера. nginx подключается и ждёт ClientHello от клиента. Ждут оба, тридцать секунд, после чего nginx закрывает соединение, ни разу не обратившись к бэкенду. В логе при этом 200.

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

Диспетчер на входе слышит первое слово посетителя - «в бухгалтерию» - и по нему открывает нужную дверь. Он не читает документы, он слышит одно слово.

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

Механизм: что видно до шифрования

клиент ClientHello, открытым текстом SNI: имя сервера ALPN: h2, http/1.1 версия TLS выбор бэкенда дальше идёт шифрованный поток, nginx его не трогает
Читается только первое сообщение; ключей у nginx нет и не появляется

Правило

ssl_preread on заполняет три переменные и ничего не расшифровывает. Дальше выбор бэкенда делается одним из двух способов: картой из тринадцатого раздела или виртуальным сервером с server_name (с 1.25.5). Соединение уходит бэкенду как есть, включая сам ClientHello.

/etc/nginx/nginx.confstream
map $ssl_preread_server_name $upstream {
    default          web-backend;
    vpn.shop.local   vpn-gateway;
}

server {
    listen 443;
    ssl_preread on;
    proxy_pass $upstream;
}

Разбор: три соединения и три набора переменных

Прогон по одному и тому же блоку, лог со всеми тремя переменными:

↳  вывод/var/log/nginx/stream.log
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:

/etc/nginx/nginx.confstream
server { listen 7005; pass 127.0.0.1:8443; }
server { listen 7015; proxy_pass 127.0.0.1:8443; }
↳  выводответ http-блока
через 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. Конфиг проходит проверку без единого предупреждения, а имя брать неоткуда - весь трафик уходит в первый блок:

/etc/nginx/nginx.confstream
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 вовсе. Чего не хватает в заготовке?

/etc/nginx/nginx.confstream
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 не делает.

Комментарии

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

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

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