Раздел 1 · Сети — глава 1.1

Пакеты: что внутри и как TCP ведёт разговор

Про TCP обычно рассказывают схемой с тремя стрелочками и словами «SYN, SYN-ACK, ACK». Этого хватает ровно до первого вопроса «а что происходит дальше» и до первого инцидента, где соединение устанавливается, но данные не идут. Разберём по-настоящему: из чего физически состоит пакет, как стороны договариваются о порядке байтов, почему соединение может висеть в TIME_WAIT и куда девается ответ на большой запрос.

§1Матрёшка: что физически летит по проводу

Каждый уровень сетевого стека решает свою задачу и для этого дописывает свой заголовок в начало того, что ему передали сверху. Ничего больше в инкапсуляции нет: это последовательное оборачивание в конверты.

приложение отдало: [ HTTP-запрос, 200 байт ] TCP добавил свой заголовок [TCP hdr 20B][ 200 байт ] порты, seq/ack, флаги, окно └─ теперь это сегмент IP добавил свой [IP hdr 20B][TCP hdr][ 200 байт ] адреса, TTL, protocol=6 └─ теперь это пакет (датаграмма) Ethernet обернул сверху [Eth 14B][IP hdr][TCP hdr][ 200 байт ][FCS 4B] MAC-адреса, ethertype └─ теперь это кадр, и вот он идёт в провод итого на 200 байт полезных данных — 54 байта служебных

На приёмной стороне всё разворачивается в обратном порядке: сетевая карта проверяет контрольную сумму кадра и смотрит MAC назначения, ядро по полю ethertype понимает, что внутри IP, по полю protocol в IP-заголовке — что внутри TCP, по номеру порта в TCP-заголовке находит нужный сокет и кладёт данные в его буфер. Приложение делает read() и получает свои 200 байт, ничего не зная о путешествии.

Разница между «сегментом», «пакетом» и «кадром» — не педантизм, а способ точно указать, где ты смотришь. Тебя спросят «на каком уровне проблема», и правильный ответ звучит как «кадры до узла доходят, ARP резолвится, но TCP-хендшейк не завершается» — то есть второй уровень жив, третий жив, четвёртый рвётся.

§2Что реально стоит знать про поля заголовков

Наизусть перечислять все поля не нужно, но несколько из них — рабочие инструменты, которыми пользуешься при разборе инцидентов.

Ethernet — второй уровень

Три вещи: MAC назначения, MAC источника, ethertype (0x0800 — внутри IPv4, 0x0806 — ARP, 0x86DD — IPv6). MAC-адреса меняются на каждом переходе через маршрутизатор, IP-адреса — нет. Это, пожалуй, самое важное утверждение во всей главе 1.2, и к нему мы вернёмся в следующей главе.

IP — третий уровень

ПолеЗачем оно тебе
TTLУменьшается на единицу каждым маршрутизатором. Дошёл до нуля — пакет отбрасывается, отправителю летит ICMP Time Exceeded. На этом целиком построен traceroute. По значению TTL в ответе можно прикинуть, сколько было переходов: пришло 57 при типичном старте 64 — значит, семь хопов.
ProtocolЧто внутри: 6 — TCP, 17 — UDP, 1 — ICMP, 47 — GRE, 50 — ESP (IPsec).
Flags: DFDon't Fragment. Запрещает маршрутизаторам резать пакет. Ставится почти всегда, и именно из-за него ломается связь при неправильном MTU (§8).
Total LengthРазмер всего пакета. Расхождение между ним и тем, что реально пришло, — признак усечения при захвате.
адресаSrc и dst. Не меняются на маршруте — кроме случаев NAT, где их намеренно переписывают.

TCP — четвёртый уровень

Здесь важнее всего понять смысл, а не расположение полей.

  • Порты источника и назначения. Вместе с IP-адресами образуют «четвёрку», которая однозначно определяет соединение: (src IP, src port, dst IP, dst port). Два разных соединения на один и тот же сервер отличаются исходящим портом клиента.
  • Sequence number — номер первого байта в этом сегменте. Нумеруются именно байты, а не пакеты.
  • Acknowledgment number — номер следующего байта, который сторона ожидает получить. Не «последний полученный», а «следующий нужный» — на единицу больше. Из-за этой единицы путаются постоянно.
  • Флаги. SYN — установка соединения и синхронизация номеров. ACK — поле подтверждения значимо (стоит во всех пакетах после первого). FIN — «я закончил передавать». RST — «сброс, соединения нет» (именно его ты получаешь как connection refused). PSH — «не буферизуй, отдай приложению немедленно».
  • Window — сколько байт отправитель этого сегмента готов принять прямо сейчас. Механизм управления потоком, §6.
  • Options — MSS, масштабирование окна, SACK, временные метки. Договариваются в SYN-пакетах.

§3Три рукопожатия: зачем именно три

Вопрос «почему три, а не два» задают почти всегда. Ответ: потому что синхронизировать надо оба направления, а каждая синхронизация — это «я сообщил свой начальный номер» плюс «ты подтвердил, что его получил». Итого четыре действия, но два средних складываются в один пакет.

клиент сервер │ │ │──── SYN, seq=1000, MSS=1460, WS=7, SACK-perm ────────→ │ мой номер = 1000 │ │ │←─── SYN+ACK, seq=5000, ack=1001, MSS=1460, WS=7 ────── │ твой принял, │ │ мой номер = 5000 │──── ACK, seq=1001, ack=5001 ─────────────────────────→ │ твой принял │ │ │══════════ соединение установлено, можно данные ═══════ │

Начальные номера (ISN) выбираются не с нуля, а случайно. Причины две: чтобы задержавшиеся пакеты от старого соединения с той же четвёркой не были приняты за данные нового, и чтобы усложнить подделку пакетов посторонним, который не видит трафик.

Заодно в SYN-пакетах согласуются опции, о которых потом придётся вспоминать при разборе проблем:

  • MSS — максимальный размер полезной нагрузки в одном сегменте. Считается от MTU: 1500 − 20 (IP) − 20 (TCP) = 1460. Если включены временные метки, они съедают ещё 12 байт опций, и по проводу идёт 1448 байт данных.
  • Window scale — множитель для поля окна. Само поле шириной 16 бит, то есть максимум 65535 байт, чего катастрофически мало для современных скоростей. Коэффициент масштабирования расширяет предел до гигабайта. Договаривается только в SYN — если промежуточный файрвол вырезал опцию, соединение установится, но упрётся в 64 КБ в полёте и будет работать медленно на длинных дистанциях.
  • SACK — избирательное подтверждение, §5.

Спросят

«Что происходит, если сервер не слушает порт?» — Ядро в ответ на SYN отправляет RST, клиент немедленно получает ECONNREFUSED. Быстро и однозначно. А вот если пакет молча дропает файрвол, ответа нет вообще: клиент повторяет SYN несколько раз с растущими интервалами и в итоге отваливается по таймауту — секунды и десятки секунд. Отличать эти два случая критически важно: мгновенный отказ означает «дошли, но никто не слушает», долгое зависание — «не дошли или ответ не вернулся». Первое — проблема приложения, второе — проблема сети или правил фильтрации.

§4Обмен данными: seq и ack на пальцах

Продолжим тот же диалог. Клиент отправляет 100 байт запроса.

клиент сервер │──── seq=1001, ack=5001, len=100 ────────────────────→ │ байты 1001–1100 │ │ │←─── ack=1101 ──────────────────────────────────────── │ жду 1101-й, │ │ значит 100 байт │ │ дошли │←─── seq=5001, ack=1101, len=3000 (3 сегмента) ─────── │ ответ │──── ack=8001 ────────────────────────────────────────→│ всё принял

Три свойства этого механизма стоит понимать отчётливо.

Подтверждения кумулятивные. ack=8001 означает «все байты до 8000 включительно у меня есть». Не нужно подтверждать каждый сегмент отдельно — одно подтверждение закрывает всё, что пришло подряд.

Подтверждения ездят автостопом. Отдельные ACK-пакеты нужны, только когда ответных данных нет. Если данные идут в обе стороны, поле ack просто заполняется в очередном сегменте с данными — это называется piggybacking и экономит половину пакетов.

Границы сегментов не значат ничего. TCP — это поток байтов, не сообщений. Отправленные одним write() 3000 байт приедут тремя сегментами, и приложение может прочитать их одним read() или пятью — как ляжет. Именно поэтому любой протокол поверх TCP обязан сам обозначать границы сообщений: длиной в заголовке, разделителем, чем угодно. Ошибка «прочитал ровно столько, сколько отправили» — классика багов в самописных протоколах.

§5Что происходит при потере

TCP не имеет никакого способа узнать, что пакет потерялся, кроме отсутствия подтверждения. Отсюда два механизма восстановления — медленный и быстрый.

Таймаут (RTO). Отправитель ждёт подтверждения. Не дождался за расчётное время — повторяет отправку. Время рассчитывается динамически из измеренного времени кругового пути и его дисперсии, а при повторных неудачах удваивается (экспоненциальная выдержка). Механизм надёжный, но медленный: первый повтор через доли секунды, десятый — уже через десятки секунд.

Быстрый повтор. Если потерялся один сегмент из середины потока, следующие всё равно доезжают. Получатель на каждый пришедший «не тот» сегмент отправляет повторное подтверждение с одним и тем же номером — «я всё ещё жду вот этот байт». Отправитель, увидев три одинаковых подтверждения подряд, не ждёт таймаута, а отправляет потерянный сегмент немедленно. Это спасает от лишних секунд задержки.

отправлено: [1000][2000][3000][4000][5000] ✕ потерялся получено: [1000] ... [3000][4000][5000] получатель шлёт: ack=2000, ack=2000, ack=2000, ack=2000 ↑ три дубля подряд → отправитель шлёт 2000 сразу, не дожидаясь RTO

Проблема в том, что кумулятивное подтверждение не умеет сказать «мне не хватает только вот этого куска, остальное есть». Это чинит опция SACK: получатель прикладывает список успешно принятых диапазонов, и отправитель повторяет ровно дырку, а не всё начиная с неё. Без SACK потеря одного сегмента при большом окне приводит к переотправке всего окна — заметная разница в производительности на плохих каналах.

§6Окно: почему TCP не отправляет всё сразу

Здесь живут два разных механизма, которые постоянно смешивают. Их надо развести.

Управление потоком (flow control) защищает получателя. Он в каждом пакете сообщает размер своего свободного буфера — это и есть поле Window. Отправитель не имеет права держать в полёте больше неподтверждённых байтов, чем сказал получатель. Если приложение на приёмной стороне читает медленнее, чем приходит, буфер заполняется, окно уменьшается, в пределе — до нуля («zero window»), и передача встаёт до тех пор, пока получатель не разгребёт.

Управление перегрузкой (congestion control) защищает сеть между ними. Про её состояние никто ничего не сообщает, поэтому отправитель ведёт собственную оценку — окно перегрузки (cwnd) — и подбирает его эмпирически:

  • Медленный старт. Соединение начинает с крошечного окна (около десяти сегментов) и удваивает его каждый круговой путь. Название обманчиво: рост экспоненциальный, «медленный» — только начальная точка.
  • Избежание перегрузки. После достижения порога рост становится линейным — по одному сегменту за круг.
  • Реакция на потерю. Потеря трактуется как признак перегрузки, и окно резко сокращается. Насколько резко — зависит от алгоритма: CUBIC (по умолчанию в Linux) режет умеренно и восстанавливается по кубической кривой, старый Reno делил пополам.

Реально в полёте может быть min(окно получателя, окно перегрузки). Отсюда важное практическое следствие: на длинных каналах скорость ограничена не шириной канала, а произведением полосы на задержку. Если между Петербургом и Франкфуртом круговая задержка 40 мс, а окно 64 КБ, то предел — примерно 65536 / 0.04 ≈ 1,6 МБ/с, сколько бы гигабит ни было в канале. Ровно за этим и нужно масштабирование окна из §3.

В бою

Медленная передача между дата-центрами — сначала смотри не на «канал забит», а на окно и потери: ss -tin покажет по каждому сокету текущее cwnd, rtt, счётчик retrans и активный алгоритм. Если retrans растёт, а cwnd держится маленьким — теряются пакеты, и канал тут ни при чём. Если retrans нулевой, а скорость упирается в потолок — не хватает буферов, смотри net.ipv4.tcp_rmem/tcp_wmem.

§7Закрытие и загадочный TIME_WAIT

Соединение закрывается независимо в каждую сторону — оно полнодуплексное, и одна сторона может закончить передавать, продолжая принимать. Отсюда четыре пакета вместо трёх.

клиент сервер │──── FIN, seq=1101 ──────────────────────────────→ │ я закончил │←─── ACK, ack=1102 ─────────────────────────────── │ принял │ (сервер ещё может слать данные) │←─── FIN, seq=8001 ─────────────────────────────── │ я тоже закончил │──── ACK, ack=8002 ──────────────────────────────→ │ принял │ │ TIME_WAIT CLOSED (2×MSL, в Linux 60 секунд)

Сторона, которая закрыла первой, попадает в состояние TIME_WAIT и висит в нём фиксированные 60 секунд (в Linux это константа, не настраивается через sysctl). Зачем — две причины, и обе неочевидны:

  1. Последний ACK может потеряться. Тогда сервер повторит свой FIN, и кто-то должен быть жив, чтобы ответить. Если бы клиент закрылся сразу, повторный FIN нарвался бы на RST, и сервер завершился бы с ошибкой.
  2. Заблудившиеся пакеты старого соединения. Если немедленно открыть новое соединение с той же четвёркой, задержавшийся в сети пакет от предыдущего может приехать и быть принят за данные нового. Шестьдесят секунд — гарантия, что все старые пакеты в сети уже протухли.

Практическая проблема одна: TIME_WAIT висит на инициаторе закрытия. Если это клиент, который в цикле открывает и закрывает соединения (типичный HTTP-клиент без keep-alive), у него накапливаются десятки тысяч сокетов в TIME_WAIT и кончаются исходящие порты — их всего около 28 тысяч в дефолтном диапазоне. Симптом: cannot assign requested address.

$ ss -tan state time-wait | wc -l
28431                              ← вот и вся история

$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768 60999      # 28231 порта

Что делать — в порядке правильности:

  • Использовать keep-alive и пулы соединений. Единственное настоящее решение: не открывать новое TCP-соединение на каждый запрос. Убирает проблему целиком.
  • Расширить диапазон портовnet.ipv4.ip_local_port_range = 1024 65535. Даёт двукратный запас, не решает причину.
  • net.ipv4.tcp_tw_reuse = 1 — разрешает переиспользовать сокет в TIME_WAIT для нового исходящего соединения, если временны́е метки говорят, что он старше секунды. Относительно безопасно и помогает именно клиентам.
  • tcp_tw_recycle — не использовать. Раньше советовали во всех гайдах, ломал соединения от клиентов за NAT, из свежих ядер удалён вовсе. Если увидишь его в чужом чеклисте — это маркер, что чеклист устарел лет на десять.

Заодно стоит помнить про CLOSE_WAIT — состояние по другую сторону. В него попадает сторона, которая получила FIN, но сама ещё не закрыла сокет. Растущее число CLOSE_WAIT — это всегда баг в приложении: оно не вызывает close() на дескрипторе. Никаким sysctl это не лечится, и на собесе умение отличить «много TIME_WAIT» (обычно нормально, вопрос нагрузки) от «много CLOSE_WAIT» (утечка дескрипторов в коде) сразу выделяет тебя из общей массы.

§8MTU: инцидент, который выглядит как мистика

Это, наверное, самая полезная история во всей главе — особенно если работаешь с Kubernetes или VPN.

MTU — максимальный размер кадра, который можно отправить в конкретный сетевой интерфейс. В обычном Ethernet — 1500 байт. Если пакет больше, его либо режут на фрагменты, либо отбрасывают.

Современный TCP почти всегда ставит флаг DF («не фрагментировать») и рассчитывает на Path MTU Discovery: если по пути встречается канал с меньшим MTU, маршрутизатор обязан отбросить пакет и отправить обратно ICMP-сообщение «Fragmentation Needed» с указанием допустимого размера. Отправитель это сообщение получает, уменьшает размер сегментов и живёт дальше.

Ломается это тогда, когда кто-то по дороге блокирует ICMP — по мотивам «ICMP это же пинг, а пинг это небезопасно». Тогда картина получается издевательская:

MTU 1500 MTU 1450 (туннель) MTU 1500 клиент ─────── роутер ──────────╫────────── роутер ────── сервер ╫ SYN (60 байт) ──────────→╫──────────→ проходит ✓ ACK (60 байт) ←──────────╫←────────── проходит ✓ GET / (200 байт) ──────────→╫──────────→ проходит ✓ ответ 1500 байт ←──────────╫ ✕ БОЛЬШЕ MTU, DF стоит → дроп ╫ ICMP "нужна фрагментация" ← ✕ зарублен файрволом итог: соединение установлено, мелкое ходит, большое — исчезает

Симптомы, по которым это узнаётся мгновенно: ping работает, telnet на порт подключается, короткие запросы отвечают, а вот скачивание файла, большой JSON или git clone — виснет намертво и отваливается по таймауту. SSH подключается и здоровается, но падает при выводе большой простыни текста.

Проверяется одной командой — пингом с запретом фрагментации и подбором размера. Учитывай, что размер в ping задаётся для полезной нагрузки ICMP, а сверху идут ещё 28 байт заголовков:

$ ping -M do -s 1472 10.0.5.7      # 1472 + 8 (ICMP) + 20 (IP) = 1500
PING 10.0.5.7 1472 bytes of data
ping: local error: message too long, mtu=1450

$ ping -M do -s 1422 10.0.5.7      # 1422 + 28 = 1450
64 bytes from 10.0.5.7: icmp_seq=1 ttl=63 time=0.6 ms   ← вот он, реальный MTU

$ tracepath 10.0.5.7               # покажет, на каком хопе MTU падает

Почему это твоя тема. Любой туннель добавляет свои заголовки внутрь тех же 1500 байт: VXLAN съедает 50 байт (отсюда классический MTU 1450 у оверлейных сетей Kubernetes), WireGuard — 60 (типовое значение 1420), IPsec — от 50 до 70 в зависимости от режима, GRE — 24. Стоит где-то в цепочке ошибиться с MTU интерфейса или CNI — и получаешь ровно ту самую мистику: «поды пингуются, мелкие запросы ходят, а этот один сервис таймаутит на больших ответах».

В бою

Если исправить MTU по всей цепочке невозможно (чужой туннель, чужой файрвол), есть костыль на уровне пограничного узла — MSS clamping: маршрутизатор на лету переписывает опцию MSS в проходящих SYN-пакетах, заставляя стороны сразу договориться о меньшем размере сегментов. В iptables это -j TCPMSS --clamp-mss-to-pmtu в цепочке FORWARD. Именно так живут почти все VPN-концентраторы.

§9Задержки, которые появляются из ниоткуда

Две встроенные оптимизации TCP, которые по отдельности разумны, а вместе дают загадочные паузы ровно по 40 миллисекунд.

Алгоритм Нейгла. Чтобы не гонять по сети пакеты с одним байтом полезной нагрузки и 40 байтами заголовков, отправитель придерживает мелкие порции данных, пока не получит подтверждение на предыдущие. Экономит трафик, добавляет задержку.

Отложенное подтверждение. Получатель не отвечает ACK немедленно — ждёт до 40 мс в надежде, что появятся встречные данные и подтверждение уедет вместе с ними. Экономит пакеты, добавляет задержку.

Теперь совместим: приложение делает два маленьких write() подряд. Первый уходит. Второй Нейгл придерживает — ждёт ACK на первый. А получатель ACK не отправляет — ждёт данных, чтобы приклеить его к ним. Обе стороны ждут друг друга, пока не сработает таймер отложенного подтверждения. Ровно 40 миллисекунд на пустом месте, и это отлично видно в графиках задержек как узкая полка.

Лечится опцией сокета TCP_NODELAY, которая выключает Нейгла. В большинстве современных библиотек и рантаймов она включена по умолчанию именно поэтому. Второй способ — не делать несколько мелких write(), а собирать сообщение в один буфер и отправлять целиком.

Спросят

«Приложение отвечает за 40 мс вместо одной, что смотреть?» — Число 40 в контексте TCP почти всегда означает встречу Нейгла с отложенным подтверждением. Проверяется по tcpdump: если между сегментом с данными и следующим за ним ровно ~40 мс тишины, диагноз подтверждён. Ответ — TCP_NODELAY либо склейка мелких записей.

§10tcpdump: как снимать и как читать

Вся эта глава становится практическим навыком ровно тогда, когда ты умеешь посмотреть на настоящие пакеты. tcpdump — тот инструмент, который отделяет «я думаю, что запрос не доходит» от «я вижу, что он не доходит». И на собеседовании умение прочитать вывод ценится выше, чем знание полей заголовка наизусть.

Как снимать, чтобы не выстрелить себе в ногу

$ tcpdump -i any -nn -vv port 443 and host 10.0.5.7
              │   │   │
              │   │   └─ подробнее: флаги, TTL, размеры, опции
              │   └─ НЕ резолвить имена и номера портов
              └─ слушать все интерфейсы сразу

Ключ -nn обязателен почти всегда, и вот почему: без него tcpdump будет делать обратный резолв каждого адреса. Это, во-первых, замедляет вывод, во-вторых — само создаёт сетевой трафик, который попадает в твой же захват, а если DNS недоступен, каждая строка будет ждать таймаута. При разборе сетевой проблемы это последнее, что нужно.

Ещё три ключа, которые стоит знать:

-c 100              # остановиться после 100 пакетов
-s 0                # снимать пакет целиком, не обрезая (в свежих версиях по умолчанию)
-w dump.pcap        # писать в файл — потом открыть в Wireshark
-r dump.pcap        # читать из файла, фильтры применимы и здесь
-A                  # показать содержимое как текст (для HTTP)
-X                  # hex + текст
-ttt                # время как разница с предыдущим пакетом — для поиска пауз

Грабли

На нагруженном проде tcpdump без фильтра — плохая идея: он поднимает интерфейс в неразборчивый режим, ест процессор и способен заполнить диск гигабайтами за минуты. Правило: всегда фильтр, всегда -c или -w с ротацией. Если пишешь в файл надолго — -W 5 -C 100 ограничит пятью файлами по 100 МБ. И помни, что дамп содержит данные пользователей: незашифрованный трафик в файле — это персональные данные, к нему надо относиться соответственно.

Фильтры, которых хватает на 95% случаев

host 10.0.5.7                      # всё, что с этим адресом
src host 10.0.5.7                  # только от него
dst port 5432                      # только на этот порт
port 443 and host 10.0.5.7         # сочетание условий
net 10.244.0.0/16                  # целая подсеть — удобно для сети подов
icmp                               # для разбора MTU и недоступности
arp                                # для разбора переезда VIP
not port 22                        # исключить свою же ssh-сессию — почти всегда нужно

# только пакеты с установкой соединения — видно, кто к кому стучится
'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'

# только сбросы — быстрый способ найти, кто рвёт соединения
'tcp[tcpflags] & tcp-rst != 0'

Как читать строку вывода

Каждая строка — один пакет. Формат постоянный, и разобрать его надо один раз.

14:02:11.483921 IP 10.244.3.17.41022 > 10.0.5.7.5432: Flags [S], seq 1829301, win 64240, options [mss 1460,sackOK,TS val 91 ecr 0,wscale 7], length 0 │ │ │ │ │ │ │ │ │ │ │ источник:порт получатель:порт флаги номер окно согласуемые опции размер │ │ данных │ какой протокол время с точностью до микросекунд

Флаги в квадратных скобках — самое информативное поле:

ОбозначениеФлагЧто означает
[S]SYNПопытка установить соединение
[S.]SYN+ACKСервер согласен. Точка в записи означает ACK
[.]ACKПросто подтверждение, данных нет
[P.]PSH+ACKПакет с данными — обычный рабочий трафик
[F.]FIN+ACKШтатное закрытие своей половины соединения
[R] или [R.]RSTСброс. Либо никто не слушает порт, либо кто-то принудительно оборвал

Четыре картины, которые надо узнавать мгновенно

### 1. Здоровое соединение — всё как в §3
14:02:11.483 IP 10.244.3.17.41022 > 10.0.5.7.5432: Flags [S],  seq 1829301
14:02:11.484 IP 10.0.5.7.5432 > 10.244.3.17.41022: Flags [S.], seq 77123, ack 1829302
14:02:11.484 IP 10.244.3.17.41022 > 10.0.5.7.5432: Flags [.],  ack 77124
→ три пакета за миллисекунду, дальше пойдут данные. Норма.

### 2. Никто не слушает порт
14:05:22.100 IP 10.244.3.17.41100 > 10.0.5.7.5432: Flags [S], seq 4410
14:05:22.101 IP 10.0.5.7.5432 > 10.244.3.17.41100: Flags [R.], seq 1, ack 4411
→ мгновенный RST в ответ на SYN. Сеть в порядке, приложение не запущено
  или слушает другой адрес. connection refused у клиента.

### 3. Пакеты уходят в никуда
14:07:03.200 IP 10.244.3.17.41200 > 10.0.5.7.5432: Flags [S], seq 9120
14:07:04.210 IP 10.244.3.17.41200 > 10.0.5.7.5432: Flags [S], seq 9120
14:07:06.230 IP 10.244.3.17.41200 > 10.0.5.7.5432: Flags [S], seq 9120
14:07:10.270 IP 10.244.3.17.41200 > 10.0.5.7.5432: Flags [S], seq 9120
→ повторы SYN с удвоением паузы (1, 2, 4 секунды), ответа нет вообще.
  Дропает файрвол, либо ответ не находит обратной дороги.
  Тот же seq во всех — это ПОВТОРЫ одного пакета, а не новые попытки.

### 4. Потери и повторные передачи в живом соединении
14:09:41.100 IP A > B: Flags [P.], seq 1461:2921, length 1460
14:09:41.140 IP B > A: Flags [.], ack 1461
14:09:41.140 IP B > A: Flags [.], ack 1461
14:09:41.141 IP B > A: Flags [.], ack 1461
→ три одинаковых ack подряд — это дублирующие подтверждения из §5.
  Сейчас отправитель повторит потерянный сегмент, не дожидаясь таймаута.
  Единичные — норма, постоянные — реальные потери в канале.

Где именно снимать

Это половина метода, и её часто упускают. Дамп с одной стороны отвечает только на вопрос «что я отправил и что мне пришло». Чтобы понять, где теряется, снимать надо с обеих сторон одновременно и сравнивать:

  • SYN есть в дампе отправителя, но нет в дампе получателя → теряется по пути туда. Смотреть файрволы, маршруты, сетевые политики.
  • SYN есть у обоих, SYN-ACK есть в дампе получателя, но не доходит до отправителя → теряется обратный путь. Классика асимметричной маршрутизации или NAT, который не знает про это соединение.
  • Оба видят обмен, но соединение всё равно рвётся → смотреть RST и кто его послал.

В Kubernetes к этому добавляется вопрос, в каком пространстве имён снимать. Дамп на узле покажет трафик уже после трансляций kube-proxy; чтобы увидеть то, что реально видит приложение, надо зайти в сетевое пространство пода:

# на узле: снять внутри netns конкретного пода
$ PID=$(crictl inspect --output go-template --template '{{.info.pid}}' <id>)
$ nsenter -t $PID -n tcpdump -i any -nn port 8080

# либо через эфемерный контейнер, если на узел не попасть
$ kubectl debug -it мой-под --image=nicolaka/netshoot --target=app

Образ netshoot стоит запомнить: в нём собраны tcpdump, dig, ss, curl, mtr и всё остальное, чего никогда нет в нормальном минимальном образе приложения.

Спросят

«Как понять, доходит ли трафик до сервиса?» — Ответ через метод, а не через команду: «Снимаю дамп с обеих сторон одновременно с фильтром по хосту и порту и обязательно с -nn, чтобы обратный резолв не создавал лишнего трафика. Дальше смотрю на первый пакет: если SYN виден только у отправителя — теряется путь туда; если ответный SYN-ACK ушёл, но до клиента не дошёл — проблема на обратном пути, обычно асимметричная маршрутизация или NAT; если пришёл RST — сеть в порядке, порт никто не слушает; если SYN повторяется с удвоением паузы и тишина в ответ — дропает файрвол. В кластере снимаю внутри netns пода, потому что на узле трафик уже прошёл трансляции.»

§11Как это звучит в ответе

«Пакет — это матрёшка: приложение отдаёт данные, TCP добавляет заголовок с портами, номерами последовательности и подтверждения, окном и флагами; IP добавляет адреса, TTL и признак протокола; Ethernet — MAC-адреса. По проводу идёт кадр, на приёме всё разбирается в обратном порядке и попадает в нужный сокет по номеру порта.

Соединение начинается с трёх пакетов, потому что синхронизировать надо оба направления: каждая сторона сообщает свой случайный начальный номер и подтверждает чужой, средние два шага складываются в один пакет. Заодно там же согласуются MSS, масштабирование окна и SACK — и если файрвол по дороге вырезал опции, соединение установится, но будет работать медленно.

Дальше нумеруются байты, а не пакеты. Подтверждение — это номер следующего ожидаемого байта, и оно кумулятивное. Потеря обнаруживается либо по таймауту с экспоненциальным ростом, либо по трём одинаковым подтверждениям подряд — тогда сегмент повторяется сразу. Скорость ограничена минимумом из окна получателя (это защита получателя от переполнения буфера) и окна перегрузки (это оценка состояния сети, которую отправитель подбирает сам).

Закрытие — четыре пакета, потому что каждая сторона закрывает своё направление независимо. Инициатор закрытия остаётся в TIME_WAIT на 60 секунд, чтобы дожать потерянный последний ACK и не перепутать заблудившиеся пакеты с данными нового соединения. Много TIME_WAIT на клиенте — признак отсутствия keep-alive; много CLOSE_WAIT — баг в приложении, оно не закрывает сокеты.

И самая частая в практике проблема — MTU. Если по пути MTU меньше, а ICMP заблокирован, Path MTU Discovery не работает: соединение устанавливается, мелкие запросы проходят, большие ответы молча исчезают. Проверяется ping -M do -s, лечится правильным MTU или MSS clamping. В Kubernetes с оверлейными сетями это встречается регулярно.»