Трёхстороннее рукопожатие TCP
Три пакета, которыми начинается любое TCP-соединение. Разбираем, зачем нужен каждый, откуда берутся номера последовательности и как всё это связано с таймаутами, которые вы видите в логах HTTP-клиента.
Почти каждый разработчик может назвать три пакета: SYN, SYN-ACK, ACK. Гораздо реже удаётся ответить на вопрос, почему пакетов именно три, а не два и не четыре. Этот разбор отвечает на него, а заодно объясняет несколько вещей, которые из-за непонимания рукопожатия выглядят загадочно: отказы под нагрузкой, зависающие на минуту подключения и странные настройки ядра, которые советуют в интернете.
Зачем вообще рукопожатие
TCP обещает две вещи: данные дойдут и дойдут в том порядке, в котором их отправили. Чтобы это обеспечить, каждому байту присваивается номер, а получатель подтверждает, какие номера он принял.
Значит, до передачи данных стороны должны договориться, с какого номера начинается нумерация. Причём обе: TCP-соединение двунаправленное, и у каждого направления своя независимая нумерация.
Отсюда и берётся число три. Нужно передать два начальных номера и подтвердить оба:
- клиент сообщает свой начальный номер;
- сервер подтверждает его и сообщает свой;
- клиент подтверждает номер сервера.
Второй и третий шаги совмещают подтверждение и объявление в одном пакете — поэтому рукопожатие трёхстороннее, а не четырёхстороннее. Разрыв соединения, где такого совмещения обычно нет, как раз занимает четыре пакета.
Начальные номера и почему они случайные
Начальный номер последовательности (ISN, initial sequence number) — не ноль. Если бы каждое соединение начиналось с нуля, возникли бы две проблемы.
Первая — старые пакеты. Сегмент из предыдущего соединения между той же парой адресов и портов может задержаться в сети и прийти уже после того, как соединение закрылось и открылось заново. С нулевым ISN такой сегмент попал бы в допустимое окно и был бы принят как часть нового потока.
Вторая — подделка. Зная, что нумерация начинается с нуля, кто угодно собрал бы пакет с правильным номером и вклинился в чужое соединение, не видя трафика.
Поэтому RFC 6528 предписывает вычислять ISN как сумму монотонно растущего таймера и криптографической хеш-функции от четвёрки «адрес источника, порт источника, адрес назначения, порт назначения» и секретного ключа хоста. Номер остаётся непредсказуемым извне, но для одной и той же пары сокетов растёт со временем — это и защищает от старых пакетов.
Запустите tcpdump -n -i any 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0 and port 443' и откройте любой сайт. В выводе будут видны Flags [S], Flags [S.] и Flags [.] — те самые три пакета, с относительными номерами последовательности.
Две очереди на сервере
Между приходом SYN и вызовом accept() в вашем коде соединение живёт в ядре. Там его ждут две очереди, и путать их — распространённая ошибка.
| Очередь | Что в ней | Чем ограничена |
|---|---|---|
| SYN queue полуоткрытые |
Соединения в состоянии SYN_RECEIVED: сервер ответил SYN-ACK и ждёт третий пакет | net.ipv4.tcp_max_syn_backlog |
| Accept queue установленные |
Рукопожатие завершилось, соединение ждёт, пока приложение вызовет accept() |
аргумент backlog в listen(), сверху ограничен net.core.somaxconn |
Отсюда два разных вида отказа. Если переполнена accept queue — ваше приложение не успевает разбирать соединения: оно занято работой, заблокировано на вводе-выводе или просто однопоточное. Увеличение backlog здесь лечит симптом, а не причину.
Если переполнена SYN queue — до завершения рукопожатия не доходит значительная часть попыток. Это либо потери в сети, либо SYN-флуд.
SYN-флуд и cookies
Асимметрия рукопожатия очевидна: отправить SYN стоит одного пакета, а ответить на него — выделить запись в SYN queue и держать её, пока не придёт третий пакет или не истечёт таймер. Атакующему дёшево, защищающемуся дорого.
Классическая атака — поток SYN-пакетов с подменённых адресов. Третий пакет не придёт никогда, записи копятся, очередь переполняется, настоящие клиенты получают отказ.
Ответ на это — SYN cookies. Когда очередь переполняется, сервер перестаёт хранить состояние: он кодирует всё необходимое прямо в ISN, который отправляет в SYN-ACK. Это хеш от четвёрки адресов, грубой временной метки и секрета плюс закодированный MSS. Если клиент настоящий, он вернёт ack = ISN + 1, и сервер восстановит параметры из самого номера, ничего не храня.
В ISN помещается немного: MSS кодируется тремя битами из таблицы, а опции масштабирования окна, SACK и timestamps при включённых cookies теряются. Поэтому cookies — аварийный режим, а не постоянная настройка: соединения, установленные через них, работают медленнее.
Откуда берётся минутное ожидание
Теперь то, ради чего это чаще всего гуглят. Если SYN не получил ответа, клиент повторяет его с экспоненциальной задержкой: 1 с, 2 с, 4 с, 8 с, 16 с, 32 с. В Linux число попыток задаётся net.ipv4.tcp_syn_retries и по умолчанию равно шести.
Сумма интервалов — 63 секунды, плюс ожидание последнего ответа. Отсюда знаменитые «примерно 127 секунд» до того, как connect() вернёт ETIMEDOUT.
Практический вывод: если ваш HTTP-клиент не задаёт собственный таймаут на установку соединения, он унаследует этот. Запрос, который «должен отвечать за 200 мс», при недоступном узле будет висеть две минуты, держать поток и съедать место в пуле.
Важно и то, что это тишина, а не отказ. Если бы на том конце был живой хост с закрытым портом, пришёл бы RST и connect() упал бы с ECONNREFUSED мгновенно. Долгое ожидание означает, что пакеты уходят в никуда: фильтрующий брандмауэр, выключенная машина или ошибка маршрутизации.
Коротко
- Пакетов три, потому что начальных номеров два и каждый нужно подтвердить.
- ISN непредсказуем, чтобы отсечь задержавшиеся пакеты прошлых соединений и затруднить подделку.
- У сервера две очереди; их переполнение означает разные проблемы и лечится по-разному.
- SYN cookies спасают от флуда ценой потери опций соединения — это аварийный режим.
- Всегда задавайте таймаут на установку соединения. Иначе он равен примерно двум минутам.
Источники
- RFC 9293 — Transmission Control Protocol, разделы 3.5 и 3.10
- RFC 6528 — Defending against Sequence Number Attacks
- RFC 4987 — TCP SYN Flooding Attacks and Common Mitigations
- Документация ядра Linux:
Documentation/networking/ip-sysctl.rst
Нашли ошибку или неточность? Напишите на redakciya@example.invalid — фактические ошибки мы правим в течение недели.