447 lines
44 KiB
Markdown
447 lines
44 KiB
Markdown
# D5, часть 2. TCP глубоко (урок)
|
||
|
||
Это не проверка, а урок: сначала разбираем механизм, потом решаешь задачи. Разбор идёт от
|
||
байтов заголовка к состояниям соединения, затем к механизмам надёжности и скорости, и в конце
|
||
к соседним протоколам (UDP, DNS, DHCP, NAT), которые решают то, что TCP не решает.
|
||
|
||
## 1. Заголовок TCP: 20 байт, поле за полем
|
||
|
||
```
|
||
0 1 2 3
|
||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||
| Source Port | Destination Port |
|
||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||
| Sequence Number |
|
||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||
| Acknowledgment Number |
|
||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||
|Offset| Rsvd|C E U A P R S F| Window |
|
||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||
| Checksum | Urgent Pointer |
|
||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||
```
|
||
|
||
Фиксированная часть — ровно **20 байт**, это минимум заголовка TCP-сегмента (тот же минимум
|
||
20 байт, что и у IP-заголовка, вложенного на уровень ниже). Поля:
|
||
|
||
- **Source Port / Destination Port** — по 16 бит каждый (порты 0–65535).
|
||
- **Sequence Number** — 32 бита, номер первого байта данных в этом сегменте.
|
||
- **Acknowledgment Number** — 32 бита, номер следующего ожидаемого байта от собеседника.
|
||
- **Data Offset** — 4 бита, длина заголовка в 32-битных словах (максимум 15×4 = 60 байт,
|
||
значит опции — максимум 60 − 20 = 40 байт).
|
||
- **Флаги** — по одному биту: **SYN** (установка соединения), **ACK** (подтверждение), **FIN**
|
||
(корректное закрытие направления), **RST** (аварийный сброс), **PSH** (передать данные
|
||
приложению немедленно, не буферизуя), **URG** (часть данных помечена срочной через Urgent
|
||
Pointer); плюс **ECE**/**CWR** — сигнализация перегрузки сети (ECN, RFC 3168), делит
|
||
6 «резервных» бит исходного RFC 793 на 4 действительно резервных и 2 флаговых.
|
||
- **Window** — 16 бит, объявляемый размер приёмного окна (flow control, раздел 6).
|
||
- **Checksum** — 16 бит, контрольная сумма заголовка+данных+псевдозаголовка IP.
|
||
- **Urgent Pointer** — 16 бит, действителен только при выставленном URG.
|
||
- **Options** — переменная длина, до 40 байт; сюда входит **MSS** (Maximum Segment Size) —
|
||
опция, которой стороны обмениваются только в SYN-сегментах, сообщая максимальный размер
|
||
сегмента, который готовы принять.
|
||
|
||
**Факты для карточек**
|
||
- base | Минимальный размер заголовка TCP? — 20 байт
|
||
- base | Сколько бит занимает порт в заголовке TCP? — 16 бит (диапазон 0–65535)
|
||
- core | Максимальный размер опций TCP-заголовка и почему именно столько? — 40 байт, потому что Data Offset (4 бита) кодирует длину заголовка в 32-битных словах максимум 15×4=60 байт, минус 20 байт фиксированной части
|
||
- core | В каких сегментах передаётся опция MSS? — только в сегментах с флагом SYN, при установке соединения
|
||
- deep | Какие два флага TCP появились позже исходного RFC 793 и для чего? — ECE и CWR (RFC 3168), сигнализация перегрузки сети (ECN) вместо/вместе с потерей пакета
|
||
|
||
Почему дальше: поля Sequence/ACK Number и флаг SYN используются в первую очередь при
|
||
установке соединения — разберём этот обмен по шагам.
|
||
|
||
## 2. Установка соединения: three-way handshake
|
||
|
||
1. Клиент → серверу: сегмент с флагом **SYN** и собственным начальным порядковым номером
|
||
(**ISN**, Initial Sequence Number).
|
||
2. Сервер → клиенту: сегмент с флагами **SYN+ACK** — подтверждает ISN клиента (Ack = ISN_клиента + 1)
|
||
и присылает собственный ISN.
|
||
3. Клиент → серверу: сегмент с флагом **ACK**, подтверждающим ISN сервера.
|
||
|
||
Три шага, а не два, нужны потому, что соединение TCP полнодуплексное — у каждого направления
|
||
свой независимый ISN, и каждая сторона должна не только сообщить свой ISN, но и получить
|
||
подтверждение, что собеседник его получил. Два шага (SYN → SYN-ACK) недостаточно: сервер не
|
||
может быть уверен, что его SYN-ACK дошёл до клиента, пока не получит финальный ACK. После
|
||
рукопожатия обе стороны знают начальные номера друг друга и могут независимо отслеживать
|
||
доставку и порядок байт в каждом направлении.
|
||
|
||
**Ловушки**
|
||
- Файрвол блокирует ответный ACK клиента → сервер зависает в состоянии `SYN_RECEIVED`
|
||
(полуоткрытое соединение) → видно как одинокий `SYN` без завершающего `ACK` в tcpdump и
|
||
запись в `ss -tan`.
|
||
|
||
**Факты для карточек**
|
||
- base | Сколько сегментов в three-way handshake? — 3 (SYN, SYN+ACK, ACK)
|
||
- core | Почему для установки TCP-соединения недостаточно двух сегментов? — соединение полнодуплексное, серверу нужно подтверждение, что его SYN-ACK (и его ISN) реально дошёл до клиента
|
||
- core | Что означает ISN и синхронизируется ли он в одном экземпляре на оба направления? — начальный порядковый номер; нет, у каждого направления свой собственный ISN
|
||
|
||
Почему дальше: раз соединение открывается тремя сегментами и переходит через промежуточные
|
||
состояния (`SYN_SENT`, `SYN_RECEIVED`), логично разобрать полный набор состояний TCP как
|
||
конечный автомат.
|
||
|
||
## 3. Состояние-машина TCP
|
||
|
||
- **CLOSED** — соединения нет.
|
||
- **LISTEN** — сервер ждёт входящих SYN.
|
||
- **SYN_SENT** — клиент отправил SYN, ждёт SYN-ACK.
|
||
- **SYN_RECEIVED** — сервер получил SYN, отправил SYN-ACK, ждёт финальный ACK.
|
||
- **ESTABLISHED** — соединение открыто, идёт обмен данными.
|
||
- **FIN_WAIT_1** — эта сторона отправила FIN, ждёт ACK на него.
|
||
- **FIN_WAIT_2** — FIN подтверждён, эта сторона ждёт FIN от собеседника.
|
||
- **CLOSE_WAIT** — получен FIN от собеседника, эта сторона ещё может досылать данные.
|
||
- **LAST_ACK** — эта сторона отправила свой FIN (после CLOSE_WAIT), ждёт последний ACK.
|
||
- **TIME_WAIT** — сторона, отправившая финальный ACK, ждёт 2×MSL перед освобождением сокета.
|
||
- **CLOSING** — оба конца отправили FIN почти одновременно, редкий путь одновременного закрытия.
|
||
|
||
Автомат асимметричен по конструкции: клиент и сервер проходят разные пути (`SYN_SENT` только
|
||
у инициатора, `SYN_RECEIVED`/`LISTEN` только у принимающей стороны), потому что роли в
|
||
рукопожатии разные. `ss -tan` показывает текущее состояние сокета в столбце State — это прямое
|
||
отражение позиции в этом автомате, а не абстракция.
|
||
|
||
**Факты для карточек**
|
||
- base | В каком состоянии сервер ждёт входящие подключения? — LISTEN
|
||
- core | Чем отличаются пути клиента и сервера в конечном автомате TCP? — клиент проходит SYN_SENT, сервер — LISTEN и SYN_RECEIVED; роли в рукопожатии асимметричны
|
||
- core | Какой командой в Linux видно текущее состояние TCP-сокета? — `ss -tan` (столбец State)
|
||
|
||
Почему дальше: часть состояний (`FIN_WAIT_*`, `CLOSE_WAIT`, `LAST_ACK`, `TIME_WAIT`) относится
|
||
к закрытию соединения — разберём эту последовательность отдельно, она сложнее открытия.
|
||
|
||
## 4. Закрытие в четыре шага и TIME_WAIT
|
||
|
||
1. Сторона A, завершившая передачу, шлёт **FIN**.
|
||
2. Сторона B подтверждает его **ACK** (A уходит в `FIN_WAIT_2`, B — в `CLOSE_WAIT`).
|
||
3. Когда сторона B тоже готова закрыться, она шлёт свой **FIN**.
|
||
4. Сторона A подтверждает финальным **ACK** (A уходит в `TIME_WAIT`, B — в `CLOSED` сразу
|
||
после получения этого ACK).
|
||
|
||
Четыре сегмента, а не два, — потому что закрытие каждого направления независимо
|
||
(**полузакрытие**, half-close): получение FIN от B означает только «B больше не пришлёт
|
||
данных», но A может продолжать досылать данные в обратном направлении, прежде чем закрыть
|
||
свою половину. FIN идёт в одну сторону за раз именно поэтому — это закрытие конкретного
|
||
направления потока, а не всего соединения разом.
|
||
|
||
Сторона, отправившая последний ACK (то есть первой инициировавшая закрытие), уходит в
|
||
**TIME_WAIT** и ждёт там **2×MSL** (Maximum Segment Life) перед освобождением сокета. Зачем:
|
||
эта сторона должна поймать задержавшиеся в сети дубликаты старых сегментов (если порт
|
||
освободить сразу и тут же переиспользовать для нового соединения, устаревший сегмент может
|
||
быть по ошибке принят как часть новой сессии) и быть готовой повторно отправить последний ACK,
|
||
если он потерялся и партнёр повторяет свой FIN. RFC 793 определяет номинальный MSL = 2 минуты
|
||
(отсюда 2×MSL = 4 минуты), но в Linux TIME_WAIT реализован как фиксированный таймаут **60
|
||
секунд** (константа `TCP_TIMEWAIT_LEN` в ядре) независимо от настраиваемого MSL. Куча
|
||
накопившихся `TIME_WAIT`-сокетов на активном сервере — видимая проблема (`ss -tan state
|
||
time-wait`), решается через `SO_REUSEADDR` или снижением частоты пересоздания соединений.
|
||
|
||
**Ловушки**
|
||
- Считать TIME_WAIT «багом» и убирать его целиком (агрессивные настройки reuse) → сервер
|
||
начинает принимать дубликаты старых сегментов как часть новых соединений → редкие, трудно
|
||
воспроизводимые повреждения данных на высоконагруженных коротких соединениях.
|
||
|
||
**Факты для карточек**
|
||
- base | Сколько сегментов нужно для полного закрытия TCP-соединения? — 4 (FIN, ACK, FIN, ACK)
|
||
- base | Формула длительности TIME_WAIT? — 2×MSL
|
||
- core | Почему закрытие TCP асимметрично («полузакрытие»), а не мгновенное закрытие по первому FIN? — соединение дуплексное, получение FIN означает только «собеседник больше не пришлёт данные», но сама сторона может ещё дописывать данные в обратном направлении
|
||
- deep | Сколько секунд реально длится TIME_WAIT в Linux и совпадает ли это с 2×MSL по RFC 793? — 60 секунд, фиксированная константа ядра; не совпадает с номинальными 4 минутами (2×2 мин) по RFC 793
|
||
|
||
Почему дальше: FIN — это вежливое закрытие. Разберём флаг, который сигнализирует не закрытие
|
||
по согласию, а ошибку или невозможность продолжить, — RST.
|
||
|
||
## 5. RST: аварийный сброс, а не закрытие
|
||
|
||
**RST** сигнализирует ошибку или невозможность продолжить соединение — в отличие от вежливого
|
||
FIN, тишины не будет. Типичный случай: попытка подключиться к закрытому порту получает в ответ
|
||
именно RST, а не молчание — так клиент сразу узнаёт, что порт не слушает, вместо ожидания
|
||
таймаута. Если приложение получает RST там, где ожидало нормальное закрытие через FIN, это
|
||
обычно означает, что сокет на другой стороне был закрыт грубо (например, процесс убит) —
|
||
классический симптом «connection reset by peer» в логах, подтверждается флагом RST в
|
||
последнем сегменте дампа tcpdump.
|
||
|
||
**Факты для карточек**
|
||
- base | Что получает клиент в ответ на попытку подключиться к закрытому порту? — RST
|
||
- core | Чем RST принципиально отличается от FIN по смыслу? — RST — аварийный немедленный сброс (ошибка/невозможность продолжить), FIN — согласованное закрытие направления
|
||
|
||
Почему дальше: SYN, FIN, RST — это управление соединением. Отдельный вопрос — как TCP поверх
|
||
этого управления гарантирует, что данные точно дойдут и в правильном порядке.
|
||
|
||
## 6. Надёжность: ACK, окно, flow control против congestion control
|
||
|
||
TCP строит надёжный упорядоченный байтовый поток поверх ненадёжной доставки IP:
|
||
|
||
- Каждый байт данных нумеруется порядковым номером (Sequence Number).
|
||
- Получатель подтверждает принятые данные **кумулятивным ACK** — Ack Number означает «я
|
||
получил всё непрерывно вплоть до этого байта», а не «я получил именно этот сегмент».
|
||
- Если подтверждение не пришло за таймаут (RTO, раздел 7) — отправитель ретранслирует.
|
||
- Сегменты, пришедшие не по порядку, получатель буферизует и переупорядочивает перед тем, как
|
||
отдать данные приложению.
|
||
- **Окно скольжения** (sliding window) — отправитель держит в полёте сразу много
|
||
неподтверждённых байт, не дожидаясь ACK на каждый сегмент отдельно; иначе пришлось бы ждать
|
||
полный RTT на каждый пакет.
|
||
|
||
TCP регулирует скорость передачи **двумя разными** механизмами, которые легко перепутать:
|
||
|
||
- **Flow control** (управление получателем, поле Window) — сколько байт получатель готов
|
||
принять прямо сейчас. Защищает получателя от переполнения его приёмного буфера: если
|
||
приложение читает данные медленно, окно сужается, вплоть до нуля («TCP Zero Window» в
|
||
tcpdump — передача полностью останавливается).
|
||
- **Congestion control** (управление сетью, `cwnd` — congestion window) — сколько отправитель
|
||
может слать, не перегружая сеть между узлами, о состоянии которой напрямую ничего не
|
||
известно. Работает по схеме **AIMD** (Additive Increase, Multiplicative Decrease):
|
||
- **Slow start** — `cwnd` стартует с малого значения (исторически 1 MSS, современный RFC
|
||
6928 разрешает стартовое окно до 10 MSS) и удваивается каждый RTT, пока не достигнет
|
||
порога `ssthresh` или не случится потеря.
|
||
- **Congestion avoidance** — после `ssthresh` рост становится линейным: `cwnd` растёт
|
||
примерно на 1 MSS за RTT (аддитивное увеличение).
|
||
- **Fast retransmit** — 3 повторных (дублирующих) ACK на один и тот же номер сегмента
|
||
трактуются как сигнал потери без ожидания полного таймаута RTO — ретрансмиссия начинается
|
||
немедленно.
|
||
- При потере: `ssthresh = cwnd / 2`, `cwnd` тоже уменьшается (мультипликативное уменьшение).
|
||
Потеря вдвое режет `cwnd`, а не сбрасывает в ноль, потому что потеря одного сегмента —
|
||
сигнал «сеть перегружена сейчас», а не «сеть недоступна»; резкое падение до минимума
|
||
впустую потратило бы уже проверенную пропускную способность. Полный сброс `cwnd` к
|
||
минимуму происходит отдельно — при таймауте RTO (более серьёзный сигнал, чем
|
||
дублирующие ACK).
|
||
|
||
Действующий по умолчанию в Linux алгоритм congestion control — **CUBIC** (с ядра 2.6.19),
|
||
более сложная функция роста `cwnd` от времени, чем классический AIMD Reno, но сама идея
|
||
«расти, пока не потеряли, резко сократиться при потере» сохраняется.
|
||
|
||
**Ловушки**
|
||
- Перепутать flow control (Window) с congestion control (`cwnd`) → неверный ответ на вопрос
|
||
«почему передача остановилась»: Zero Window — проблема медленного читателя на приёмнике,
|
||
просевший `cwnd` — проблема сети между узлами, у них разная диагностика и разное решение.
|
||
|
||
**Факты для карточек**
|
||
- base | Чем измеряется flow control в заголовке TCP? — полем Window
|
||
- core | Чем отличается flow control от congestion control по цели? — flow control защищает получателя от переполнения буфера, congestion control защищает сеть от перегрузки
|
||
- core | Во сколько раз падает cwnd при обнаруженной потере? — вдвое (ssthresh = cwnd/2)
|
||
- core | Что такое fast retransmit? — ретрансмиссия по 3 дублирующим ACK без ожидания полного таймаута RTO
|
||
- deep | Какой алгоритм congestion control используется в Linux по умолчанию? — CUBIC
|
||
|
||
Почему дальше: и ретрансмиссия по таймауту, и fast retransmit опираются на измеренное время
|
||
кругового пути — разберём, как считается сам таймаут RTO.
|
||
|
||
## 7. Таймеры ретрансмиссии: RTO и RTTVAR
|
||
|
||
Таймаут ретрансмиссии (RTO) не фиксированное число — он адаптируется под измеренный round-trip
|
||
time (RTT) конкретного соединения. По алгоритму Джекобсона/Карелса (RFC 6298):
|
||
|
||
- `SRTT` (сглаженный RTT) обновляется как экспоненциальное скользящее среднее с коэффициентом
|
||
`α = 1/8` от нового измерения.
|
||
- `RTTVAR` (вариация RTT) обновляется как скользящее среднее отклонения `|SRTT − RTT_sample|`
|
||
с коэффициентом `β = 1/4`.
|
||
- `RTO = SRTT + 4 × RTTVAR`.
|
||
|
||
Множитель 4 у `RTTVAR` — запас на случай, если сеть внезапно станет менее стабильной (большой
|
||
разброс задержек), чтобы не срабатывать ложно на обычный джиттер, но и не ждать избыточно
|
||
долго при реальной потере. Фиксированный RTO (без адаптации под RTT) не работает — RTT
|
||
локальной сети и RTT через несколько континентов различаются на порядки, единое число либо
|
||
слишком долго ждёт в быстрой сети, либо слишком рано ретранслирует в медленной.
|
||
|
||
**Факты для карточек**
|
||
- core | Формула RTO по Джекобсону/Карелсу? — SRTT + 4×RTTVAR
|
||
- deep | Какие коэффициенты сглаживания используются для SRTT и RTTVAR? — α=1/8 для SRTT, β=1/4 для RTTVAR
|
||
|
||
Почему дальше: RTO касается решения «когда переслать заново». Отдельный, более локальный
|
||
таймер решает более мелкий вопрос — отправлять ли данные прямо сейчас маленьким куском или
|
||
подождать и накопить.
|
||
|
||
## 8. Nagle и TCP_NODELAY
|
||
|
||
Алгоритм Нейгла по умолчанию задерживает отправку маленьких сегментов, пока не придёт ACK на
|
||
предыдущие неподтверждённые данные или не накопится достаточно данных для полного сегмента —
|
||
цель в том, чтобы не засорять сеть множеством мелких пакетов (несколько байт полезной нагрузки
|
||
на 20 байт TCP-заголовка — плохое соотношение). Флаг сокета **`TCP_NODELAY`** отключает эту
|
||
задержку — данные уходят сразу, как только приложение вызвало `write`/`send`.
|
||
|
||
Nagle плохо сочетается с **delayed ACK** (получатель тоже не спешит слать ACK, ждёт немного —
|
||
вдруг появятся данные для отправки в обратную сторону, тогда ACK можно приклеить к ним) —
|
||
классический сценарий из статьи Кларка 1982 года даёт задержки порядка сотен миллисекунд:
|
||
отправитель ждёт ACK, чтобы послать следующий маленький кусок, получатель ждёт данные, чтобы
|
||
не слать ACK отдельно — оба ждут друг друга. Для интерактивных протоколов с мелкими,
|
||
чувствительными к задержке сообщениями (например, построчный ввод в интерактивном сеансе)
|
||
`TCP_NODELAY` — стандартная практика.
|
||
|
||
**Факты для карточек**
|
||
- base | Что делает флаг TCP_NODELAY? — отключает алгоритм Нейгла, данные отправляются сразу без задержки на накопление
|
||
- core | Почему Nagle + delayed ACK вместе дают заметные задержки? — обе стороны ждут друг друга: отправитель — ACK перед следующей мелкой отправкой, получатель — данные для отправки в обратную сторону, чтобы не слать ACK отдельно
|
||
|
||
Почему дальше: TCP — не единственный транспортный протокол. Разберём его прямую
|
||
противоположность по философии — UDP, у которого почти ничего из разобранного выше просто нет.
|
||
|
||
## 9. UDP: 8 байт, без гарантий
|
||
|
||
Заголовок UDP — всего **8 байт**: порт источника, порт назначения, длина, контрольная сумма —
|
||
и всё; никаких порядковых номеров, подтверждений или окна, как у TCP. Нет установки
|
||
соединения, нет гарантии доставки, порядка или отсутствия дублей — датаграмма либо доходит,
|
||
либо нет, молча. Такая простота осознанная: приложениям, которым важнее низкая задержка и
|
||
минимум накладных расходов, чем гарантия каждого байта, не нужен вес состояния соединения и
|
||
ретрансмиссий.
|
||
|
||
Где уместен UDP:
|
||
- **DNS** — короткий запрос-ответ, переспросить целиком дешевле, чем ждать TCP-ретрансмиссию.
|
||
- **RTP** (голос/видео реального времени) — устаревший потерянный кадр всё равно бесполезен,
|
||
ждать его повторной доставки хуже, чем пропустить.
|
||
- **QUIC** — строит собственную надёжность и порядок поверх UDP на прикладном уровне, обходя
|
||
то, что классический TCP жёстко зашивает в ядро (head-of-line blocking на уровне сегментов).
|
||
|
||
**Факты для карточек**
|
||
- base | Размер заголовка UDP? — 8 байт
|
||
- base | Какие поля есть в заголовке UDP? — порт источника, порт назначения, длина, контрольная сумма
|
||
- core | Почему DNS исторически использует UDP, а не TCP? — типичный запрос-ответ короткий и умещается в одну датаграмму, устанавливать TCP-соединение ради одного маленького обмена избыточно медленно
|
||
|
||
Почему дальше: TCP и UDP используют один и тот же числовой идентификатор приложения на узле —
|
||
порт. Разберём, как устроено адресное пространство портов.
|
||
|
||
## 10. Порты: три диапазона
|
||
|
||
- **0–1023** — Well-known / System Ports: закреплены за стандартными службами (22 SSH, 53
|
||
DNS, 80 HTTP, 443 HTTPS) — клиент заранее знает, куда стучаться.
|
||
- **1024–49151** — Registered Ports: регистрируются IANA за конкретными приложениями, но без
|
||
такой строгой резервации, как первый диапазон.
|
||
- **49152–65535** — Dynamic/Private Ports (ephemeral): ОС временно выделяет их клиентским
|
||
сокетам на время соединения, не привязывая ни к какой конкретной службе.
|
||
|
||
Два процесса не могут одновременно слушать один и тот же порт на одном интерфейсе — второй
|
||
вызов `bind` завершится ошибкой «Address already in use»; частая причина, по которой сервис
|
||
не поднимается после аварийного перезапуска — старый процесс ещё держит порт (либо сокет
|
||
всё ещё в `TIME_WAIT`).
|
||
|
||
**Факты для карточек**
|
||
- base | Диапазон well-known портов? — 0–1023
|
||
- base | В каком диапазоне ОС обычно выделяет эфемерные порты клиентским соединениям? — 49152–65535
|
||
- core | Что вернёт второй `bind` на уже занятый порт? — ошибку «Address already in use»
|
||
|
||
Почему дальше: порт определяет приложение на узле, но не решает проблему нехватки IPv4-адресов
|
||
для самих узлов — этим занимается NAT.
|
||
|
||
## 11. NAT: таблица трансляций и почему ломается P2P
|
||
|
||
Маршрутизатор на исходящем пакете подменяет внутренний адрес источника на свой внешний и
|
||
запоминает в **таблице трансляций** соответствие «внутренний IP:порт — внешний IP:порт»; на
|
||
входящий ответный пакет ищет по этой таблице нужного внутреннего получателя и подменяет адрес
|
||
обратно. NAT возник как практическое решение нехватки публичных IPv4-адресов: 32-битного
|
||
пространства не хватает на все устройства мира, а один внешний адрес может обслуживать целую
|
||
локальную сеть, различая внутренние узлы по номеру порта (**PAT**, Port Address Translation).
|
||
|
||
Почему ломается P2P: узел за NAT не имеет собственного публичного адреса и не может принимать
|
||
входящие соединения без явной настройки (проброс портов, `-p` в Docker — тот же принцип) —
|
||
входящий пакет от нового, незнакомого узла просто не с чем сопоставить в таблице трансляций,
|
||
её запись создаётся только исходящим трафиком. Диагностируется тем, что снаружи виден только
|
||
внешний адрес роутера, а не внутренний узел.
|
||
|
||
**Факты для карточек**
|
||
- base | Что делает NAT с исходящим пакетом? — подменяет внутренний IP:порт источника на внешний IP:порт, запоминая соответствие в таблице трансляций
|
||
- core | Почему NAT ломает входящие P2P-соединения без проброса портов? — запись в таблице трансляций создаётся только исходящим трафиком, входящему от незнакомого узла не с чем сопоставиться
|
||
|
||
Почему дальше: NAT решает адресацию узлов, но перед этим узел нужно ещё найти по имени —
|
||
разберём DNS.
|
||
|
||
## 12. DNS: порт 53, рекурсия, типы записей, TTL
|
||
|
||
Клиент отправляет запрос резолверу через **UDP на порт 53** (типичный ответ умещается в один
|
||
пакет, соединение не нужно); резолвер либо отвечает из кэша, либо рекурсивно опрашивает
|
||
корневые, затем доменные (TLD), затем авторитативные серверы, пока не получит финальный ответ.
|
||
Если ответ не помещается в стандартный размер UDP-датаграммы (передача зоны, большие
|
||
DNSSEC-записи), DNS переключается на **TCP/53**, где нет ограничения на размер одного пакета.
|
||
|
||
Основные типы записей: **A** (имя → IPv4-адрес), **AAAA** (имя → IPv6-адрес), **MX** (почтовый
|
||
сервер домена, с приоритетом). Каждая запись несёт **TTL** — время в секундах, на которое
|
||
резолверам разрешено кэшировать запись без повторного запроса к авторитативному серверу;
|
||
меньший TTL — быстрее распространяются изменения (например, при смене IP сервиса), но больше
|
||
нагрузка на DNS-инфраструктуру повторными запросами.
|
||
|
||
**Факты для карточек**
|
||
- base | Порт DNS по умолчанию и протокол? — 53, UDP (переключение на TCP/53 для больших ответов)
|
||
- base | Что хранит запись типа A? — соответствие имени домена IPv4-адресу
|
||
- core | Зачем у DNS-записи есть TTL? — ограничивает время кэширования резолверами; компромисс между скоростью распространения изменений и нагрузкой повторными запросами
|
||
|
||
Почему дальше: DNS резолвит имя в адрес, но сам адрес узлу тоже нужно откуда-то получить при
|
||
подключении к сети — этим занимается DHCP.
|
||
|
||
## 13. DHCP: порты 67/68, схема DORA
|
||
|
||
DHCP-сервер слушает **UDP-порт 67**, клиент — **UDP-порт 68**. Обмен идёт по схеме **DORA**:
|
||
**D**iscover (узел широковещательно ищет сервер) → **O**ffer (сервер предлагает адрес) →
|
||
**R**equest (узел подтверждает выбор) → **A**ck (сервер закрепляет адрес на ограниченный срок
|
||
аренды — lease, который нужно периодически продлевать). Автоматизация нужна потому, что вручную
|
||
прописывать уникальный IP на каждое устройство в сети из сотен узлов неуправляемо и чревато
|
||
конфликтами адресов при ошибке администратора.
|
||
|
||
**Факты для карточек**
|
||
- base | Порты DHCP-сервера и клиента? — сервер 67/UDP, клиент 68/UDP
|
||
- core | Из каких четырёх шагов состоит DORA? — Discover, Offer, Request, Ack
|
||
|
||
Почему дальше: всё разобранное выше — это то, что реально видно в байтах на проводе; разберём
|
||
инструмент, которым эти байты читают напрямую.
|
||
|
||
## 14. tcpdump и Wireshark: как читать дамп
|
||
|
||
`tcpdump` захватывает пакеты на интерфейсе и печатает их построчно (или пишет в файл для
|
||
Wireshark). Базовые приёмы:
|
||
|
||
- Фильтр по порту: `tcpdump tcp port 80` — только TCP-трафик на порту 80 в любую сторону.
|
||
- Сохранение в файл для последующего анализа в Wireshark: `tcpdump -w capture.pcap`.
|
||
- Чтение handshake в выводе: строка с флагом `[S]` (SYN) от клиента, `[S.]` (SYN-ACK) от
|
||
сервера, `[.]` (ACK) от клиента — три строки подряд с растущими seq/ack номерами это и есть
|
||
three-way handshake, ровно как в разделе 2.
|
||
- Закрытие видно как пара `[F.]` (FIN+ACK) с обеих сторон, каждый подтверждён отдельным `[.]`.
|
||
- Одинокий `[S]` без ответа — недоступный порт или заблокированный файрволом ACK (раздел 2,
|
||
ловушка `SYN_RECEIVED`).
|
||
- Флаг `[R]` — RST, аварийный сброс (раздел 5).
|
||
|
||
**Факты для карточек**
|
||
- base | Команда для захвата TCP-трафика на 80 порту? — `tcpdump tcp port 80`
|
||
- base | Флаг tcpdump для сохранения дампа в файл? — `-w`
|
||
- core | Как в выводе tcpdump выглядит three-way handshake? — три строки подряд: `[S]` от клиента, `[S.]` от сервера, `[.]` от клиента
|
||
|
||
Ссылки на задачи этого дня: `tasks/04_ipv4` — разбор IPv4-заголовка и контрольной суммы
|
||
(нижний уровень относительно TCP, инкапсулирующий его); задачи чтения дампов — применение
|
||
раздела 14 на реальных `.pcap`.
|
||
|
||
<details>
|
||
<summary>Проверь себя</summary>
|
||
|
||
1. В tcpdump видно: `[S]` от клиента, `[S.]` от сервера, дальше тишина — ACK от клиента не
|
||
приходит. В каком состоянии завис сервер и почему?
|
||
<details><summary>Ответ</summary>`SYN_RECEIVED` — сервер получил SYN, отправил SYN-ACK, но
|
||
финальный ACK не дошёл (например, заблокирован файрволом), поэтому рукопожатие не
|
||
завершилось и сервер ждёт третий сегмент.</details>
|
||
|
||
2. Почему TIME_WAIT длится именно 2×MSL, а не произвольное короткое время вроде 1 секунды?
|
||
<details><summary>Ответ</summary>Сторона, закрывшая соединение последней, должна успеть
|
||
поймать задержавшиеся в сети дубликаты старых сегментов (которым отводится время жизни
|
||
MSL на путь туда и обратно — отсюда удвоение) и быть готовой повторно отправить последний
|
||
ACK, если его потеря заставит партнёра повторить FIN. Слишком короткий таймаут рискует
|
||
освободить порт раньше, чем дубликат добежит и будет ошибочно принят новым
|
||
соединением.</details>
|
||
|
||
3. Клиент шлёт данные маленькими кусками через `write()` без `TCP_NODELAY`, сервер использует
|
||
delayed ACK. Почему передача может ощутимо тормозить, хотя пропускной способности канала
|
||
достаточно?
|
||
<details><summary>Ответ</summary>Алгоритм Нейгла на клиенте задерживает отправку
|
||
следующего маленького куска, пока не придёт ACK на предыдущий; сервер с delayed ACK не
|
||
спешит слать этот ACK отдельно, ожидая данных для отправки в обратную сторону — обе
|
||
стороны ждут друг друга, и задержка растёт не от нехватки полосы, а от этого
|
||
взаимного ожидания.</details>
|
||
|
||
4. При передаче по сети с потерями каждые несколько RTT срабатывает fast retransmit. Что
|
||
происходит с `cwnd` при этом и почему не сбрасывается до минимального значения, как при
|
||
таймауте RTO?
|
||
<details><summary>Ответ</summary>`ssthresh` и `cwnd` уменьшаются вдвое (`cwnd/2`), а не до
|
||
минимума — 3 дублирующих ACK означают, что сеть в целом жива и часть сегментов всё же
|
||
доходит, это более мягкий сигнал перегрузки, чем полное отсутствие ответа при таймауте
|
||
RTO, поэтому реакция мягче.</details>
|
||
|
||
</details>
|
||
|
||
## Материалы
|
||
|
||
- RFC 793, Transmission Control Protocol — https://datatracker.ietf.org/doc/html/rfc793
|
||
- RFC 6298, Computing TCP's Retransmission Timer — https://datatracker.ietf.org/doc/html/rfc6298
|
||
- RFC 3168, The Addition of Explicit Congestion Notification (ECN) to IP — https://datatracker.ietf.org/doc/html/rfc3168
|
||
- RFC 768, User Datagram Protocol (UDP) — https://datatracker.ietf.org/doc/html/rfc768
|
||
- RFC 1035, Domain Names — Implementation and Specification (DNS) — https://datatracker.ietf.org/doc/html/rfc1035
|
||
- RFC 2131, Dynamic Host Configuration Protocol (DHCP) — https://datatracker.ietf.org/doc/html/rfc2131
|
||
- man7.org, `tcp(7)` — https://man7.org/linux/man-pages/man7/tcp.7.html
|