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 587;
    ssl_preread on;
    proxy_pass mail-relay:587;
}

На порту 587 почта начинается открытым текстом: сервер здоровается строкой 220, и только после команды STARTTLS стороны переходят на шифрование. Клиент подключается и ждёт приветствия сервера. 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 получит пустую строку. По $ssl_preread_protocol можно проверить максимальную версию, предложенную клиентом, даже без SNI. Согласованную версию эта переменная не показывает: её ограничивает завершающий TLS бэкенд.

Разбор: 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 на портах 25 и 587, IMAP на 143, POP3 на 110, FTP, MySQL - клиент там ждёт приветствия. Замер: клиент подключился и молчит.

Порт решает, а не название протокола. На SMTPS (465), IMAPS (993) и POP3S (995) шифрование начинается сразу, клиент первым шлёт ClientHello, и ssl_preread там работает как на 443-м.

конфиг сколько ждал клиент дошло до бэкенда
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 не попадёт никуда. Для проверки предложенной версии можно добавить карту по $ssl_preread_protocol или использовать одну карту с составным ключом из версии и SNI. Запрет старых согласованных версий настрой на TLS-бэкенде.

Главное

ssl_preread позволяет выбрать назначение по открытому ClientHello без завершения TLS. Проверь, кто первым отправляет данные в обслуживаемом протоколе: ожидание клиента не подходит протоколу, где первым должен говорить сервер. При диагностике сравни время соединения и наличие адреса бэкенда в логе.

Комментарии

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

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

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