# теория · шаг 1 из 6
Рукопожатие: что происходит до первого запроса
Коротко
Прежде чем уйдёт хоть байт запроса, стороны договариваются о шифровании. Договор занимает круг по сети - путь пакета туда и обратно (в старой версии два) и определяет всё дальнейшее: какой сертификат покажут, по какому протоколу пойдут дальше, можно ли переиспользовать результат.
- Первым говорит клиент:
ClientHelloс именем сайта открытым текстом - это и есть SNI. - По этому имени nginx выбирает
server-блок и его сертификат, ещё до всякого HTTP. - В TLS 1.3 сертификат уходит зашифрованным, в 1.2 - открытым.
- TLS 1.3 укладывается в один круг, 1.2 требует двух: замер на стенде даёт 163 против 210 миллисекунд.
- Заодно в рукопожатии выбирается протокол поверх TLS - так согласуется HTTP/2.
Если ClientHello, SNI и разница кругов у 1.2 и 1.3 знакомы - листай до «Теперь сам».
Сначала ответь сам
На одном адресе живут три сайта с тремя разными сертификатами. Клиент открывает
https://shop.local. Соединение зашифровано - значит nginx не может прочитать
заголовок Host, пока не расшифрует запрос. А расшифровать он может, только
предъявив сертификат.
Как он выбирает, чей сертификат показывать?
По имени, которое клиент называет до шифрования. В самом первом сообщении
ClientHello есть поле SNI, и в нём открытым текстом написано shop.local.
Курица и яйцо разошлись: имя сайта передаётся дважды - сначала в открытую для
выбора сертификата, потом внутри шифрованного запроса как Host.
Из этого растёт всё поведение из раздела про выбор сервера: и ssl_reject_handshake
для чужих имён, и правило «сертификат выбирается раньше, чем Host».
На что это похоже
Разговор через переводчика, которого ещё надо выбрать.
Ты подходишь к стойке и вслух, на понятном всем языке, говоришь: мне нужен испанский. Эту фразу слышат все вокруг - иначе тебя некому было бы направить. Дальше выходит переводчик, показывает удостоверение, уводит тебя в кабинку, и уже там начинается разговор по делу, которого не слышит никто.
Имя языка - это SNI: его нельзя сказать шёпотом, иначе не поймут, кого звать. Удостоверение - сертификат. Кабинка - шифрованный канал. А сам разговор - HTTP, который до этого момента ещё не начинался.
Механизм: два сценария рукопожатия
Правило
Сертификат выбирается по SNI, а не по Host. К моменту, когда nginx увидит
Host, сертификат уже предъявлен и канал уже зашифрован.
Разбор: что реально летит по проводу
Живой обмен с nginx 1.31.5, версия TLS 1.3:
>>> Handshake, ClientHello
<<< Handshake, ServerHello
<<< ChangeCipherSpec
<<< Handshake, EncryptedExtensions
<<< Handshake, Certificate
Флаг -msg велит openssl печатать сами сообщения рукопожатия; >>> - то, что
отправил клиент, <<< - то, что пришло от сервера. Сообщений от сервера четыре, но идут они одной пачкой, в ответ на одно
сообщение клиента - вот почему круг всего один. И Certificate стоит после
EncryptedExtensions, то есть уже внутри шифрованной части: подслушивающий
увидит, что кто-то соединился с этим адресом, но не увидит, чей сертификат ему
показали.
Тот же стенд, версия принудительно понижена до 1.2:
>>> Handshake, ClientHello
<<< Handshake, ServerHello
<<< Handshake, Certificate ← открытым текстом
<<< Handshake, ServerKeyExchange
<<< Handshake, ServerHelloDone
>>> Handshake, ClientKeyExchange
>>> ChangeCipherSpec
Здесь клиент вынужден заговорить второй раз до того, как канал заработает, - это и есть второй круг. Плюс сертификат виден любому, кто смотрит на трафик.
Разница в миллисекундах, замеренная на стенде с задержкой 50 мс в одну сторону:
| Что | Готов к отправке запроса | Ответ получен |
|---|---|---|
| HTTP без шифрования | 51 мс | 102 мс |
| HTTPS, TLS 1.3 | 112 мс | 163 мс |
| HTTPS, TLS 1.2 | 160 мс | 210 мс |
Задержка на стенде - 50 мс в одну сторону, то есть круг туда-обратно 100 мс. «Готов к отправке» у HTTP - 51 мс: один круг на TCP-рукопожатие и сразу запрос. У TLS 1.3 добавляется круг на шифрование (112), у 1.2 - два (160). Сорок семь миллисекунд разницы между версиями - это ровно один лишний круг.
Разбор: где наивное правило подводит
SNI виден всем. Шифруется содержимое, но не имя сайта: оно обязано быть открытым, иначе сервер не выберет сертификат. Провайдер, корпоративный шлюз и любой, кто видит трафик, знает, к какому домену ты подключился, - хотя и не знает, что именно ты там запросил. Технология, скрывающая и его, существует (Encrypted Client Hello), но требует поддержки с обеих сторон и в стандартную сборку nginx пока не входит.
Старый клиент без SNI не исчез. Если имя не названо, nginx покажет сертификат блока по умолчанию для этой пары «адрес и порт». Для одного сайта это незаметно, для десяти - ошибка сертификата у того, кто пришёл на девять остальных.
Рукопожатие - это ещё и выбор протокола. В том же ClientHello клиент
перечисляет, на чём готов говорить дальше: h2, http/1.1. Сервер выбирает
один. Поэтому HTTP/2 не требует отдельного порта и не «включается» отдельным
запросом - он согласуется здесь, за один круг вместе с шифрованием. Подробности -
в разделе про HTTP/2.
Не всякое соединение платит за рукопожатие. Клиент, который уже говорил с этим сервером, может предъявить билет прошлой сессии (выданный сервером при прошлом рукопожатии ярлык «мы уже договаривались») и пропустить часть обмена, а установленное соединение переиспользуется по keep-alive - и тогда рукопожатия нет вовсе. Полную цену платит только первое соединение.
Что ломается без этого
Не зная порядка, легко искать причину не там. Ошибка сертификата у клиента - это
не про HTTP: до HTTP дело не дошло, и в access_log такой запрос не появится
вовсе. Строку надо искать в error_log, а сам запрос - у клиента.
Вторая беда - удивление от медленного первого захода. Страница «открывается секунду, а потом всё летает» - это три круга на первое соединение и ноль на последующие.
Зачем это в работе
Посмотреть, чем закончилось рукопожатие, можно одной командой:
openssl s_client -connect shop.local:443 -servername shop.local </dev/null 2>/dev/null | grep -E 'Protocol|Cipher|Verify return'
Три строки отвечают на три вопроса: какая версия согласована, каким шифром идёт
канал и сошлась ли цепочка доверия. Флаг -servername обязателен - без него
openssl не пошлёт SNI, и ты увидишь сертификат чужого блока, а не своего.
Теперь сам
Мониторинг ходит на https://shop.local/healthz каждые пять секунд и жалуется на
медленные ответы: 180 миллисекунд при бэкенде, отвечающем за десять. В логе
nginx $request_time около 0,01. Где эти 170 миллисекунд?
В рукопожатии, и в лог nginx они не попадают: $request_time считается с первого
байта запроса, а рукопожатие происходит раньше. Мониторинг открывает новое
соединение на каждую проверку и каждый раз платит за TCP и TLS. Лечится на
стороне мониторинга - переиспользовать соединение; а если проверок много,
заодно стоит посмотреть на версию TLS: понижение до 1.2 добавляет к каждой
проверке лишний круг.
Главное
До HTTP идёт рукопожатие: клиент шлёт ClientHello с именем сайта открытым
текстом (SNI), по нему nginx выбирает server-блок и сертификат. TLS 1.3
укладывается в один круг и отдаёт сертификат уже зашифрованным; 1.2 требует двух
кругов и показывает сертификат открыто - на стенде это 163 против 210 мс. В том
же обмене согласуется протокол поверх TLS, поэтому HTTP/2 не нужен отдельный
порт. Ошибка сертификата в access_log не попадает: до HTTP дело не дошло.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий