Learning Platform
Глоссарий Troubleshooting
Урок 09.02 · 22 мин
Начальный
TCPHandshakeWiresharkNetworking

TCP 3-way handshake — SYN, SYN-ACK, ACK и sequence numbers

Каждый раз, когда твой браузер открывает любой сайт, твой Python-скрипт подключается к Postgres, или Git клонирует репозиторий — происходит TCP 3-way handshake. Тысячи раз в день на каждой машине. Это самая известная картинка в сетях, и понимание её необходимо для:

  1. Анализа сетевых проблем (почему сервер не отвечает?).
  2. Понимания, почему первый запрос всегда медленнее последующих.
  3. Оптимизации производительности (TCP Fast Open, connection pooling).
  4. Защиты от атак (SYN flood).
  5. Чтения вывода Wireshark/tcpdump.

В этом уроке разберём handshake в деталях: что значит каждый пакет, какие данные обмениваются, как это видно в захваченном трафике, и какие фокусы операционных систем тут происходят.


Зачем вообще нужен handshake?

UDP отправляет данные сразу, без всякого «здравствуй». TCP сначала договаривается. Зачем?

Цели handshake:

  1. Подтвердить, что обе стороны существуют и слушают. Если бы не было handshake, отправитель не знал бы, доступен ли сервер. Через 3 шага handshake обе стороны точно знают друг о друге.
  2. Согласовать initial sequence numbers (ISN). TCP нумерует байты для надёжности — каждая сторона должна сообщить свой стартовый номер.
  3. Обменяться параметрами соединения. Максимальный размер сегмента (MSS), окно (window scaling), поддержка SACK, timestamps — всё это согласуется в опциях SYN и SYN-ACK.
  4. Установить state на обеих сторонах. Каждая сторона создаёт TCB (Transmission Control Block) — структуру, в которой ядро хранит state соединения.
  5. Защититься от старых пакетов. Случайный ISN не даёт «осколкам» от старого соединения, на тех же портах, испортить новое.

Три пакета handshake

Handshake состоит из ровно трёх пакетов: SYN, SYN-ACK, ACK. Названия — это флаги в TCP-заголовке (один или два бита установлены).

TCP 3-way handshake -- пакет за пакетом
Client
Server
1. SYN (seq=x)2. SYN-ACK (seq=y, ack=x+1)3. ACK (seq=x+1, ack=y+1)Data (seq=x+1, len=100)

Шаг за шагом:

Шаг 1: SYN (client -> server). Клиент шлёт TCP-сегмент с флагом SYN (Synchronize). В этом сегменте нет данных, только заголовок. Главные поля:

  • Source Port — клиентский ephemeral порт (например, 54321).
  • Destination Port — порт сервера (например, 443 для HTTPS).
  • Sequence Number = x — initial sequence number (ISN) клиента. Случайное число в диапазоне 0..2^32-1.
  • Flags: SYN.
  • Опции: MSS, Window Scaling, SACK Permitted, Timestamps.

После отправки SYN клиент переходит в состояние SYN_SENT и ждёт ответа.

Шаг 2: SYN-ACK (server -> client). Сервер получил SYN, добавляет соединение в очередь принятых SYN (это место, где можно перегружать сервер SYN-flood’ом). В ответ отправляет сегмент с обоими флагами SYN и ACK:

  • Sequence Number = y — ISN сервера (тоже случайный).
  • Acknowledgment Number = x + 1 — «я ожидаю следующий байт x+1». Цифра «+1» — потому что SYN считается за 1 байт, даже если payload пустой.
  • Flags: SYN, ACK.
  • Опции сервера в ответ на клиентские.

Сервер переходит в SYN_RCVD.

Шаг 3: ACK (client -> server). Клиент получил SYN-ACK. Он шлёт обратно подтверждение:

  • Sequence Number = x + 1 — мой следующий байт.
  • Acknowledgment Number = y + 1 — жду следующий байт сервера y+1.
  • Flags: ACK.

После отправки ACK клиент переходит в ESTABLISHED. Когда сервер получает этот ACK — он тоже в ESTABLISHED. Всё, соединение установлено. Можно слать данные.

Бонус: В современном TCP (TCP Fast Open, RFC 7413) клиент может приложить данные сразу к третьему пакету (или даже к первому, если установлены cookies). Но классически — handshake завершается до начала передачи данных.


Initial Sequence Numbers — почему они случайные

В очень старых TCP-стеках ISN был предсказуемым (например, нарастающим со скоростью 250 000 в секунду). Это позволяло атакующему предсказать ISN и подделать TCP-сегмент, который сервер бы принял как часть существующего соединения. Эта атака — TCP sequence prediction attack — была реальной угрозой в 90-е.

Современные ОС используют криптографически случайный ISN. Это две функции:

  1. Защита от sequence prediction. Угадать случайное 32-битное число невозможно за разумное время.
  2. Защита от старых пакетов того же соединения. Если ты только что закрыл соединение на (порт_клиента, порт_сервера) и сразу открыл новое — старые пакеты, висящие в сети, могут случайно попасть на новое соединение. Случайный ISN делает их невалидными (sequence numbers не совпадут с ожидаемыми).
# Захвати handshake в Wireshark или tcpdump:
sudo tcpdump -i any -n -nn -S 'tcp port 443' &
curl -s https://example.com > /dev/null

# Ты увидишь что-то вроде:
# Flags [S], seq 1234567890           <- SYN
# Flags [S.], seq 4321098765, ack 1234567891    <- SYN-ACK
# Flags [.], seq 1234567891, ack 4321098766     <- ACK
# Flags [P.], seq 1234567891:1234568000         <- первые данные (например, TLS ClientHello)

# Опция -S заставляет tcpdump показывать абсолютные seq numbers, не относительные

TCP-заголовок и его поля

Чтобы понимать handshake до конца, нужно знать структуру TCP-заголовка:

Структура TCP-заголовка -- 20 байт минимум
Src Port (2)Source port -- откуда. Клиент: ephemeral (49152-65535). Сервер: well-known или registered
Dst Port (2)Destination port -- куда. 80 (HTTP), 443 (HTTPS), 22 (SSH), 5432 (Postgres), etc
Sequence Number (4)Номер первого байта в этом сегменте. В SYN-сегменте это ISN. Дальше каждый байт получает свой sequence number
Acknowledgment Number (4)Номер следующего байта, который ждёт отправитель этого сегмента. Если ack=100, значит 'я получил все байты до 99 включительно, следующий ожидаю 100'
Flags (6 bits)Контрольные биты: URG, ACK, PSH, RST, SYN, FIN. SYN -- синхронизация (start), FIN -- нормальное закрытие, RST -- аварийное закрытие, ACK -- есть валидный ack number
Window Size (2)Receive window -- сколько байт ещё может принять. Это и есть flow control. С Window Scaling может быть много больше 65535
Checksum (2)Контрольная сумма всего сегмента + псевдо-IP-заголовок. Защищает от случайных искажений
Urgent (2)Указатель на 'срочные' данные. На практике почти не используется в современном коде
Options (0-40 bytes)Опции: MSS, Window Scale, SACK Permitted, Timestamps. Согласуются в SYN/SYN-ACK

В handshake обмениваются опциями:

  • MSS (Maximum Segment Size) — обычно 1460 байт на Ethernet (1500 MTU - 20 IP - 20 TCP). Клиент и сервер выбирают min обоих MSS.
  • Window Scale — позволяет окну быть больше 65535 (множитель). Без этого на быстрых сетях невозможно держать «трубу» в полнометражности.
  • SACK Permitted — Selective ACK, позволяет более эффективные retransmits.
  • Timestamps — позволяют точно измерить RTT.

Анализ handshake в Wireshark

Когда захватываешь трафик в Wireshark и видишь начало соединения, оно выглядит так:

No. Time     Source         Destination    Protocol Info
1   0.0000   192.168.1.10  93.184.216.34  TCP      54321 > 443 [SYN] Seq=0 Win=65535 MSS=1460 SACK_PERM WS=128
2   0.0234   93.184.216.34 192.168.1.10   TCP      443 > 54321 [SYN, ACK] Seq=0 Ack=1 Win=29200 MSS=1460 SACK_PERM WS=128
3   0.0235   192.168.1.10  93.184.216.34  TCP      54321 > 443 [ACK] Seq=1 Ack=1 Win=4194304
4   0.0240   192.168.1.10  93.184.216.34  TLSv1.3  Client Hello

Wireshark показывает sequence numbers как относительные (от ISN) по умолчанию для удобства — Seq=0 не значит, что в проводе реальный 0, это «первый байт относительно стартового». Чтобы видеть абсолютные значения, в Wireshark отключи Edit -> Preferences -> Protocols -> TCP -> «Relative sequence numbers».

Ключевые вещи в логе:

  • Time — относительное время. Между пакетами 1 и 2 — это RTT до сервера. Здесь 23 мс.
  • [SYN] — первый пакет handshake.
  • [SYN, ACK] — второй.
  • [ACK] — третий.
  • Win — заявленный receive window.
  • MSS, SACK_PERM, WS — TCP-опции, согласуемые при handshake.

После handshake идут реальные данные. В этом примере четвёртый пакет — TLS Client Hello (потому что мы открываем HTTPS). Без TLS это был бы сразу HTTP-запрос.

Жизненный цикл HTTP-соединения: от handshake до keep-alive

Состояния TCP во время handshake

ОС хранит state каждого соединения как state-машину. Во время handshake состояния:

State machine клиента и сервера при handshake
CLOSEDНачальное состояние. Соединение не существует. Приложение готовится к connect() или listen()
send SYN
SYN_SENTКлиент отправил SYN, ждёт SYN-ACK. Если ACK не приходит за RTO, шлёт SYN снова
recv SYN-ACK, send ACK
ESTABLISHEDПолное соединение. Данные могут передаваться в обе стороны
LISTENСерверное состояние. Сокет готов принимать соединения. listen()-сокет
recv SYN, send SYN-ACK
SYN_RCVDСервер отправил SYN-ACK, ждёт ACK от клиента. Соединение в half-open состоянии. Уязвимость для SYN flood
recv ACK
ESTABLISHEDaccept() вернёт это соединение приложению

Можно увидеть состояния всех соединений:

# Linux:
ss -tan | head
# State        Recv-Q Send-Q Local Address:Port    Peer Address:Port
# LISTEN       0      128         0.0.0.0:22         0.0.0.0:*
# ESTABLISHED  0      0    192.168.1.10:54321  93.184.216.34:443
# TIME_WAIT    0      0    192.168.1.10:54322   140.82.114.4:443

# macOS:
netstat -an -p tcp | head

SYN flood атака — почему важно понимать handshake

Когда сервер получает SYN, он создаёт запись в backlog-очереди (TCP_SYN_BACKLOG). Эта очередь имеет ограниченный размер. Если очередь полная — новые SYN отбрасываются, легитимные клиенты не могут подключиться.

SYN flood: атакующий шлёт тысячи SYN с поддельных IP-адресов. Сервер на каждый отвечает SYN-ACK и переходит в SYN_RCVD, ждёт ACK. Но третий пакет никогда не приходит (поддельный IP не отвечает). Очередь забивается, реальные клиенты не могут подключиться.

Защита: SYN cookies. Сервер не хранит state для половинно-открытых соединений. Вместо этого, в ISN сервера зашит криптографически подписанный куки от параметров соединения. Когда придёт реальный ACK, сервер по куки восстановит state. Тогда нет очереди, которую можно забить.

# Linux включает SYN cookies автоматически под нагрузкой:
sysctl net.ipv4.tcp_syncookies
# net.ipv4.tcp_syncookies = 1

# Размер backlog можно настроить:
sysctl net.ipv4.tcp_max_syn_backlog
# net.ipv4.tcp_max_syn_backlog = 1024

Сколько времени занимает handshake?

Handshake — это ровно 1 RTT (round-trip time). Конкретные цифры:

  • Localhost: ~0.1 мс
  • Внутри ЦОД: 0.5-2 мс
  • Внутри страны (RTT клиент-сервер): 10-50 мс
  • Между континентами: 100-300 мс
  • Спутник: 500-1000 мс

Эти миллисекунды — это «налог» на установление TCP-соединения. Для одного запроса не страшно, но если нужно делать много коротких запросов (микросервисы, шкаф API-вызовов), эти RTT суммируются. Поэтому в production:

  1. Connection pooling. Переиспользуй TCP-соединения вместо открытия/закрытия каждый раз.
  2. HTTP keep-alive. Одно TCP-соединение для нескольких HTTP-запросов.
  3. HTTP/2 multiplexing. Несколько одновременных запросов в одном TCP-соединении.
  4. TCP Fast Open. Данные в первом пакете handshake (требует cookies от прошлой сессии).
  5. HTTP/3 (QUIC). 0-RTT resumption для повторных соединений.
Docker DNS и service discovery внутри overlay-сети
# Замерь handshake-время на конкретный хост:
curl -w '@-' -o /dev/null -s https://www.example.com <<'EOF'
DNS lookup:    %{time_namelookup}s
TCP handshake: %{time_connect}s
TLS handshake: %{time_appconnect}s
First byte:    %{time_starttransfer}s
Total:         %{time_total}s
EOF
# time_connect минус time_namelookup -- это и есть TCP handshake

Попробуй сам

# 1. Захвати handshake простого HTTP-запроса:
sudo tcpdump -i any -n 'tcp port 80' -c 6 &
curl http://example.com > /dev/null
# Ты увидишь SYN, SYN-ACK, ACK, потом GET-запрос и ответ

# 2. Замерь RTT через TCP handshake к разным серверам:
for host in example.com github.com www.baidu.com www.alibaba.com; do
  printf "%s: " "$host"
  curl -o /dev/null -s -w "TCP connect=%{time_connect}s\n" -m 5 "https://$host"
done
# Видно разницу: близкие -- миллисекунды, далёкие -- сотни мс

# 3. Посмотри TCP options, которыми обменивались стороны:
sudo tcpdump -i any -n -v 'tcp port 443' -c 4 &
curl -s https://example.com > /dev/null
# В подробном выводе видно MSS, SACK, WS, Timestamps

# 4. Посмотри активные TCP-соединения:
ss -tan | head -20
# или
netstat -an | grep ESTABLISHED | head -20

# 5. (Linux) Симулируй медленную сеть и посмотри, как handshake тормозит:
sudo tc qdisc add dev lo root netem delay 300ms
time curl -o /dev/null -s http://127.0.0.1:80
# С 300 мс задержки handshake займёт ~600 мс (1 RTT туда + RTT обратно с SYN, SYN-ACK, ACK)
sudo tc qdisc del dev lo root

Проверка знанийKnowledge check
Junior спрашивает: 'Зачем нужен 3-way handshake? Почему не 2-way (SYN, SYN-ACK)? И почему initial sequence numbers случайные, а не нулевые? Кажется, что это всё усложнение ради усложнения'
ОтветAnswer
Каждая часть 3-way handshake имеет конкретное обоснование. Почему 3 пакета, а не 2: Если бы было 2 шага (SYN, SYN-ACK), у нас была бы проблема: клиент знает, что сервер получил SYN (увидел SYN-ACK), но сервер не знает, что клиент получил SYN-ACK. Без подтверждения от клиента сервер не может быть уверен, что соединение установлено симметрично. Это критично для защиты от 'старых' пакетов. Представь: клиент послал SYN, потом перезагрузился; пакет где-то задержался в сети и дошёл до сервера через 10 минут. Сервер пошлёт SYN-ACK и подумает, что соединение есть, но клиент уже не существует. Третий пакет (ACK от клиента) -- это явное подтверждение, что обе стороны 'живы' и согласны. Без него возможны 'призрачные' соединения на стороне сервера. Технически это решение классической задачи 'Two Generals' Problem' -- нельзя точно синхронизировать два узла за конечное число сообщений. 3-way handshake -- компромисс: 'достаточно надёжно' за минимум RTT. Почему ISN случайные: (1) Защита от sequence prediction attack. Если бы ISN был нулевым или предсказуемым, атакующий мог бы подделать TCP-сегмент: 'я клиент Alice с известным ISN, у меня data', и сервер принял бы его как часть существующего соединения Alice. Со случайным ISN атакующий не знает sequence numbers и не может вставить валидные сегменты. (2) Защита от 'осколков' старых соединений. Когда (client_ip, client_port, server_ip, server_port) переиспользуется (а ephemeral ports переиспользуются часто), пакеты от старого соединения могут случайно прилететь в новое. Если ISN случайные, sequence numbers старых пакетов не попадут в окно нового соединения, и они будут отброшены. (3) Согласование. Каждая сторона нумерует свой поток отдельно. Клиент со своего ISN, сервер со своего. Симметрия упрощает дизайн протокола -- нет 'главного' и 'подчинённого' направления. Можно ли сделать handshake быстрее? Да, частично: TCP Fast Open (RFC 7413) -- после первой сессии клиент получает cookie. На последующих handshake'ах он включает cookie + данные прямо в SYN. Сервер валидирует cookie и сразу начинает обработку. Это экономит 1 RTT для повторных соединений. QUIC -- переосмыслил всё. 0-RTT resumption: первый пакет содержит и handshake-данные, и данные приложения. Это сейчас стандарт для HTTP/3. Поэтому 3-way handshake это не 'усложнение ради усложнения' -- это минимальный надёжный способ установить двустороннее соединение в untrusted сети с возможной задержкой и переупорядочиванием пакетов. Простой как только можно быть.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Какой порядок пакетов в TCP 3-way handshake и что означают флаги?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5