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

Сети: IP, ARP и NAT

Три темы, которые кажутся школьными, пока не начинаешь объяснять их вслух. Ключ ко всем трём — одна история: что именно происходит с пакетом от момента, когда приложение вызвало send(), до момента, когда электрический сигнал ушёл в провод. Расскажем её один раз подробно, и дальше ARP и NAT встанут на свои места сами.

§1IP-адрес и маска: что они на самом деле разделяют

IP-адрес — это 32 бита, которые для удобства пишут четырьмя байтами. Маска делит эти биты на две части: какая сеть и какой хост в этой сети.

адрес 192.168.10.37 → 11000000.10101000.00001010.00100101 маска /24 → 11111111.11111111.11111111.00000000 └──────── сеть ─────────┘ └─ хост ─┘ сеть 192.168.10.0 все нули в хостовой части первый хост 192.168.10.1 последний хост 192.168.10.254 широковещание 192.168.10.255 все единицы в хостовой части

Число после слэша — это просто количество единичных бит в маске. /24 — 24 бита сети и 8 бит хоста, то есть 256 адресов, из которых два служебных. /16 — 65536 адресов. Полезный ориентир: каждый шаг маски вниз удваивает размер сети, /25 — 128 адресов, /26 — 64, /30 — четыре (два используемых, классика для линка между маршрутизаторами), /32 — ровно один адрес.

Смысл всего этого ровно один: по маске хост определяет, находится ли получатель с ним в одном сегменте. Это и есть единственное решение, которое принимает отправитель, и из него вытекает вся дальнейшая механика.

Заодно стоит помнить приватные диапазоны, потому что они всплывают в каждом разговоре про NAT: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. Эти адреса не маршрутизируются в интернете — любой пакет с таким адресом назначения провайдер отбросит. Отдельно есть 100.64.0.0/10 — диапазон для CGNAT, который провайдеры используют между своим оборудованием и абонентами.

§2Решение о маршруте: два случая, и всё

Когда ядру нужно отправить пакет, оно смотрит в таблицу маршрутизации и выбирает самый специфичный подходящий маршрут — то есть с самой длинной маской (longest prefix match). Дальше возможны ровно два исхода.

$ ip route
default via 192.168.10.1 dev eth0                       # 0.0.0.0/0 — всё остальное
10.244.0.0/16 via 192.168.10.20 dev eth0                # сеть подов через другой узел
192.168.10.0/24 dev eth0 proto kernel scope link src 192.168.10.37
                                                         ↑ моя сеть, напрямую

Случай первый: получатель в моей сети (маршрут со scope link, без via). Пакет надо отдать напрямую. Для этого нужен MAC-адрес получателя — и вот здесь вступает ARP.

Случай второй: получателя нет в моей сети (маршрут с via). Пакет надо отдать маршрутизатору, чтобы он передал его дальше. IP-адрес назначения в пакете при этом остаётся адресом конечного получателя — но MAC в кадре будет MAC-адресом маршрутизатора. Значит, нужен MAC маршрутизатора — и снова ARP.

Вот это и есть та самая мысль, которую хотят услышать на собесе: IP-адреса в пакете отвечают за то, куда он идёт в конечном счёте; MAC-адреса в кадре — только за то, кому его передать на следующем шаге. IP от начала до конца остаётся тем же (кроме NAT), а MAC переписывается на каждом маршрутизаторе.

§3ARP: как узнать MAC по IP

ARP решает единственную задачу — узнать канальный адрес того, кому надо передать кадр прямо сейчас. Работает предельно просто: широковещательный вопрос всем, персональный ответ от одного.

хост A (192.168.10.37) хочет отправить на 192.168.10.50 A → всем: ARP Request «У кого 192.168.10.50? Скажите 192.168.10.37» кадр с MAC назначения ff:ff:ff:ff:ff:ff — broadcast, его получают все в сегменте B → A: ARP Reply «192.168.10.50 — это 00:1a:2b:3c:4d:5e» обычный unicast-кадр, только для A A запоминает пару в ARP-кеше и отправляет наконец свой IP-пакет

Пока идёт ARP-запрос, исходный пакет ждёт в очереди. Если ответа нет, ядро повторяет запрос несколько раз и в итоге возвращает приложению ошибку EHOSTUNREACH — «нет маршрута до хоста». Именно поэтому ping до соседа в той же сети при отключённом кабеле выдаёт «Destination Host Unreachable», а до адреса за маршрутизатором — просто молчит и таймаутит: в первом случае ошибка локальная и известна сразу, во втором пакет ушёл и просто не вернулся.

$ ip neigh                       # современная замена arp -n
192.168.10.1   dev eth0 lladdr 00:0c:29:aa:bb:cc REACHABLE
192.168.10.50  dev eth0 lladdr 00:1a:2b:3c:4d:5e STALE
192.168.10.99  dev eth0  FAILED   ← никто не ответил

Состояния записей стоит понимать: REACHABLE — недавно подтверждена, STALE — устарела, но будет использована и проверена при следующей отправке, FAILED — ответа не было. Записи живут порядка минут и обновляются лениво.

Из природы ARP следуют два свойства, важных на практике. Первое: ARP работает только в пределах широковещательного домена — за маршрутизатором он не ходит, потому что маршрутизаторы не пересылают broadcast. Второе: ARP не имеет никакой аутентификации. Любой в сегменте может ответить на чужой запрос и увести трафик на себя (ARP spoofing). В доверенной сети это не проблема, в общей — основа классической атаки посередине; защищаются на коммутаторе (Dynamic ARP Inspection) или шифрованием.

§4Путь пакета целиком, от сокета до провода

Соберём всё вместе. Клиент 192.168.1.10 открывает соединение с сервером 10.20.30.40, между ними два маршрутизатора.

1. приложение: connect() → ядро выбирает свободный исходящий порт, скажем 48122 2. ядро смотрит ip route: 10.20.30.40 не в 192.168.1.0/24 → via 192.168.1.1 3. ядро ищет MAC для 192.168.1.1 в ARP-кеше; нет — шлёт ARP Request 4. собирается кадр и уходит в провод: ┌ Eth: src AA:AA (клиент) dst BB:BB (шлюз) ┐ │ IP: src 192.168.1.10 dst 10.20.30.40 TTL=64 │ │ TCP: src 48122 dst 443 SYN │ └────────────────────────────────────────────────────┘ 5. коммутатор смотрит только на MAC назначения и отдаёт кадр в нужный порт 6. шлюз: снимает Eth, TTL 64→63, смотрит свою таблицу, узнаёт MAC следующего маршрутизатора, собирает НОВЫЙ кадр: ┌ Eth: src BB:BB (шлюз) dst CC:CC (роутер-2) ← поменялись оба │ IP: src 192.168.1.10 dst 10.20.30.40 TTL=63 ← НЕ поменялись │ TCP: src 48122 dst 443 SYN ← не трогали вообще 7. ...и так на каждом хопе: MAC переписываются, IP живут, TTL убывает 8. последний маршрутизатор в сети сервера делает ARP на 10.20.30.40 и отдаёт кадр непосредственно серверу 9. сервер: по dst-порту 443 находит слушающий сокет → SYN приложению

Если по дороге стоит NAT — а он стоит почти всегда — то на нём поменяется и IP источника. Это единственное исключение из правила «IP не меняются», и оно как раз следующий раздел.

Спросят

«Что меняется в пакете при прохождении маршрутизатора, а что — при прохождении коммутатора?» — Маршрутизатор переписывает оба MAC-адреса (источник — свой, назначение — следующего хопа), уменьшает TTL и пересчитывает контрольную сумму IP-заголовка; IP-адреса не трогает. Коммутатор не меняет ничего — он работает на втором уровне, смотрит только MAC назначения и отдаёт кадр в порт по своей таблице. Если коммутатор не знает нужный MAC, он рассылает кадр во все порты, кроме входящего.

§5Где ARP всплывает в реальной работе: failover виртуального IP

Отдельно вынесено, потому что это ровно тот случай, где абстрактное знание про ARP превращается в понимание боевого инцидента — и потому что VIP с HAProxy встречается почти у всех.

Схема стандартная: два узла, между ними плавающий виртуальный адрес, которым управляет keepalived (VRRP). Активный узел держит VIP на своём интерфейсе и обслуживает трафик, резервный ждёт.

Теперь представь, что активный узел умер и VIP переехал на резервный. IP-адрес тот же, а вот MAC-адрес за ним — другой, потому что это физически другая сетевая карта. И все, кто общался с этим VIP, имеют в своих ARP-кешах старую пару «VIP → MAC мёртвого узла». Они продолжат честно отправлять кадры на MAC, которого больше нет, — и трафик будет уходить в никуда, пока записи не протухнут. Это минуты полного простоя при формально успешном файловере.

Решается gratuitous ARP — «беспричинным» ARP-объявлением. Новый владелец адреса сразу после захвата VIP рассылает широковещательно ARP-пакет вида «192.168.1.100 — это я, вот мой MAC», хотя никто не спрашивал. Все, кто его слышит, обновляют записи в кеше немедленно. Keepalived делает это автоматически (и обычно шлёт несколько пакетов подряд, на случай потери), но полезно знать, что происходит.

# поймать момент переезда VIP
$ tcpdump -i eth0 -n arp
ARP, Reply 192.168.1.100 is-at 00:1a:2b:cc:dd:ee, length 28
ARP, Reply 192.168.1.100 is-at 00:1a:2b:cc:dd:ee, length 28
ARP, Reply 192.168.1.100 is-at 00:1a:2b:cc:dd:ee, length 28
                                    ↑ gratuitous ARP: новый узел объявляет о себе

# отправить вручную, если что-то залипло
$ arping -U -I eth0 -c 3 192.168.1.100

Грабли

Если VIP переехал, а клиенты продолжают ходить на мёртвый узел, причин обычно две. Либо gratuitous ARP не отправляется или блокируется — некоторые коммутаторы и облачные сети фильтруют такие пакеты, и тогда VRRP в облаке просто не работает, там нужны API-вызовы к провайдеру для переноса адреса. Либо между клиентами и VIP стоит маршрутизатор, и обновлять кеш нужно ему, а не клиентам, — а он может держать записи дольше и игнорировать беспричинные объявления по соображениям безопасности.

§6NAT изнутри: подмена адресов и таблица трансляций

NAT появился как затычка от нехватки IPv4-адресов и остался навсегда, попутно став ещё и способом изоляции сетей. Идея простая: маршрутизатор на границе переписывает адреса в проходящих пакетах и запоминает, что и на что поменял, чтобы обратный трафик вернуть правильному хозяину.

SNAT / masquerade — подмена источника, для исходящих

Хост в приватной сети хочет в интернет. Его адрес 192.168.1.10 в интернете не маршрутизируется — ответ просто не найдёт дорогу назад. Поэтому шлюз подменяет адрес источника на свой публичный, а чтобы различать десятки хостов за одним адресом, заодно подменяет и порт источника.

до NAT (внутри): 192.168.1.10:48122 → 93.184.216.34:443 ↓ SNAT на шлюзе после (в интернет): 203.0.113.5:61001 → 93.184.216.34:443 таблица трансляций на шлюзе: 203.0.113.5:61001 ←→ 192.168.1.10:48122 → 93.184.216.34:443 ответ приходит на 203.0.113.5:61001, шлюз находит строку в таблице и переписывает обратно: dst → 192.168.1.10:48122

Строго говоря, подмену вместе с портом называют PAT или NAPT, а в мире Linux — masquerade, если внешний адрес берётся автоматически с интерфейса (нужно для динамических адресов), и SNAT, если адрес прописан явно (быстрее, потому что не надо каждый раз читать адрес интерфейса).

DNAT — подмена назначения, для входящих

Обратная задача: снаружи стучатся на публичный адрес, а обслужить должен внутренний сервер. Шлюз переписывает адрес назначения. Это и есть «проброс порта».

# весь исходящий трафик из 192.168.1.0/24 наружу — под адресом шлюза
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE

# входящие на 443 отдать внутреннему веб-серверу
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 \
         -j DNAT --to-destination 192.168.1.20:443

Почему цепочки именно такие: DNAT делается в PREROUTING, то есть до принятия решения о маршрутизации, — иначе ядро отправило бы пакет не туда, ведь маршрут выбирается по адресу назначения. SNAT делается в POSTROUTING, то есть после выбора маршрута и исходящего интерфейса, — иначе было бы неизвестно, чей адрес подставлять. Это любимый уточняющий вопрос, и логика в нём абсолютно прозрачная.

§7Conntrack: где живёт вся правда

Ключевая деталь, которую упускают: правила NAT применяются только к первому пакету соединения. Дальше работает conntrack — подсистема ядра, отслеживающая состояние соединений. Она запоминает трансляцию, и все последующие пакеты обрабатываются по записи в таблице, минуя правила. Это и делает NAT быстрым.

Та же подсистема обеспечивает работу stateful-файрвола — правила вида «разрешить установленные соединения» опираются именно на неё:

iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

У таблицы conntrack есть предел, и его переполнение — один из самых противных инцидентов, потому что симптомы выглядят как «сеть барахлит», а не как «кончился ресурс».

$ sysctl net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_max = 262144

$ cat /proc/sys/net/netfilter/nf_conntrack_count
261998                     ← почти упёрлись

$ dmesg | tail
nf_conntrack: table full, dropping packet          ← вот он, диагноз
nf_conntrack: table full, dropping packet

Когда таблица заполнена, ядро молча отбрасывает пакеты новых соединений. Со стороны это выглядит как случайные таймауты у части клиентов при работающем сервере — и ищут это обычно долго, потому что в логах приложения ничего нет. Первое, что стоит проверять при жалобах «иногда не коннектится», — счётчик conntrack и dmesg.

Лечится увеличением nf_conntrack_max (учитывая, что каждая запись занимает несколько сотен байт памяти) и сокращением таймаутов, особенно неприлично долгого дефолта для установленных TCP-соединений — пять суток:

net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 86400   # сутки вместо 5
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30

§8Что NAT ломает

Полезно проговорить осознанно, потому что половина «странных» проблем в инфраструктуре — отсюда.

Входящие соединения не работают в принципе. Пока хост за NAT сам не инициировал соединение, записи в таблице нет, и пришедший снаружи пакет некуда транслировать. Это и защита, и главное ограничение. Отсюда растут STUN/TURN для видеозвонков, техника hole punching и весь UPnP.

Теряется настоящий IP клиента. Сервер видит адрес NAT-шлюза, а не пользователя. Для веба это решается заголовком X-Forwarded-For (седьмой уровень), для TCP вообще — proxy protocol, который умеют HAProxy и nginx.

Порты кончаются. Комбинаций «внешний адрес + порт» конечное число — около 64 тысяч на один внешний IP. Если много хостов ломится к одному и тому же сервису (скажем, тысяча подов в одну базу), диапазон исчерпывается, и новые соединения не устанавливаются. Симптом почти неотличим от предыдущего пункта, а лечится добавлением адресов в пул SNAT или, опять же, пулами соединений вместо постоянного переоткрытия.

Hairpin. Хост внутри сети обращается к своему же сервису по внешнему адресу. Пакет уходит на шлюз, тот делает DNAT и отправляет обратно внутрь — и сервер отвечает напрямую клиенту, минуя шлюз, потому что они в одной сети. Клиент получает ответ с адреса, с которым он не устанавливал соединение, и отбрасывает его. Лечится дополнительным SNAT на шлюзе (hairpin NAT) или, что проще и правильнее, split-horizon DNS — чтобы внутри имя резолвилось во внутренний адрес.

Спросят

«Почему из офиса сайт открывается, а изнутри дата-центра по тому же адресу — нет?» — Скорее всего, hairpin NAT. Расскажи механику: пакет уходит на шлюз, там DNAT, ответ идёт напрямую в обход шлюза, адрес источника в ответе не совпадает с тем, к которому подключались, клиент его отбрасывает. И назови оба решения — hairpin NAT на шлюзе или внутренняя DNS-зона.

§9Как это выглядит в Kubernetes

Всё вышеописанное в кластере используется буквально, и понимание этого сильно облегчает отладку.

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

Трафик на Service — это DNAT с ClusterIP на IP пода, который ставит kube-proxy, плюс запись в conntrack, фиксирующая соединение за выбранным подом. Про то, как именно выбирается под, — в главе 3.1.

NodePort и externalTrafficPolicy — самый практически важный сюжет. По умолчанию (Cluster) пакет, прилетевший на NodePort любого узла, может быть переброшен на под, живущий на другом узле. Чтобы ответ вернулся тем же путём, узел делает ещё и SNAT — и приложение видит адрес узла вместо адреса клиента. Настоящий IP теряется.

Если поставить externalTrafficPolicy: Local, узел отдаёт трафик только подам на себе, SNAT не нужен, исходный IP клиента сохраняется. Цена: если на узле нет подов этого сервиса, трафик туда просто отбрасывается, — поэтому внешний балансировщик обязан опрашивать health check и отправлять только на узлы с подами. Плюс нагрузка распределяется по узлам, а не по подам, и при неравномерном размещении получается перекос.

Cluster (по умолчанию)Local
IP клиентатеряется (SNAT)сохраняется
Лишний хопвозможеннет
Узлы без подовпринимают и пересылаютотбрасывают трафик
Равномерностьпо подампо узлам

§10Инструменты: чем и что смотреть

Знать теорию и уметь разобрать поломку — разные навыки, и на собеседовании второй проверяют почти всегда, задавая вопрос вида «а как бы ты это диагностировал». Здесь — набор, которым закрывается почти всё, с объяснением, зачем каждый инструмент нужен и что именно в его выводе смотреть.

ss — сокеты и состояния

Первое, что смотрят при любой проблеме с подключением. Заменил собой netstat, работает заметно быстрее на машинах с тысячами соединений.

# кто вообще слушает на этой машине
$ ss -tulpn
Netid State   Local Address:Port    Process
tcp   LISTEN  127.0.0.1:5432        users:(("postgres",pid=1203))
tcp   LISTEN  0.0.0.0:8080          users:(("java",pid=4410))
                ↑ вот эта разница критична: 127.0.0.1 — только локально,
                  0.0.0.0 — со всех интерфейсов

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

# соединения к конкретному хосту с состояниями
$ ss -tan dst 10.0.5.7
State       Recv-Q Send-Q   Local Address:Port   Peer Address:Port
ESTAB       0      0        10.244.3.17:41022    10.0.5.7:5432
TIME-WAIT   0      0        10.244.3.17:40988    10.0.5.7:5432

# посчитать состояния — быстрый способ увидеть аномалию
$ ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
  28431 TIME-WAIT      ← кончаются исходящие порты, нет keep-alive
   1204 ESTAB
    417 CLOSE-WAIT     ← приложение не закрывает сокеты, это баг в коде

# подробности по TCP: окно, RTT, повторы — для разбора медленной передачи
$ ss -tin dst 10.0.5.7
cubic wscale:7,7 rto:204 rtt:3.5/1.2 cwnd:10 retrans:0/47 ...
                            ↑ задержка      ↑ было 47 повторных передач

Про разницу между большим числом TIME-WAIT и большим числом CLOSE-WAIT подробно в главе 1.1 — это очень разные диагнозы, и умение их различать сразу выделяет.

ip — адреса, маршруты, соседи

$ ip -br a                     # коротко: интерфейсы, статус, адреса
$ ip route get 10.0.5.7        # КАК ИМЕННО пойдёт пакет к этому адресу
10.0.5.7 via 192.168.1.1 dev eth0 src 192.168.1.20
         ↑ через шлюз      ↑ выйдет отсюда  ↑ с этим адресом источника

$ ip neigh                     # ARP-кеш (см. §3)
$ ip -s link show eth0         # счётчики ошибок и дропов на интерфейсе

ip route get недооценивают, а это самый прямой способ ответить на вопрос «куда вообще пойдёт этот пакет»: ядро само посчитает и покажет выбранный маршрут, интерфейс и адрес источника. При разборе проблем с несколькими интерфейсами, VPN или policy-based routing — первое, что стоит спросить.

traceroute и mtr — где по пути

traceroute работает на механизме TTL из главы 1.1: отправляет пакеты с TTL=1, 2, 3 и так далее, и каждый маршрутизатор, обнуливший TTL, присылает ICMP-сообщение со своим адресом. Так и получается список переходов.

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

$ mtr -rwc 100 10.0.5.7            # 100 пакетов, отчётом
HOST                    Loss%   Snt   Avg  Best  Wrst
1. 192.168.1.1           0.0%   100   0.5   0.3   1.2
2. 10.10.0.1             0.0%   100   2.1   1.8   9.4
3. 172.16.4.9           12.0%  100   4.5   3.9  88.1  ← вот здесь теряется
4. 10.0.5.7              0.0%   100   5.0   4.6  12.3

mtr лучше traceroute тем, что шлёт пакеты непрерывно и показывает статистику потерь по каждому переходу. Читать её надо аккуратно: потери на промежуточном хопе при нулевых потерях на конечном означают, что этот маршрутизатор просто не любит отвечать, а не что трафик страдает. Реальная проблема — когда потери начинаются на каком-то хопе и сохраняются до конца.

curl — проверка того, что выше четвёртого уровня

$ curl -v https://shop.example.com/health
* Connected to shop.example.com (203.0.113.10) port 443   # TCP есть
* TLSv1.3 (OUT), TLS handshake                            # TLS пошёл
* Server certificate: CN=shop.example.com                  # сертификат тот
> GET /health HTTP/2                                       # запрос ушёл
< HTTP/2 200                                               # ответ получен

# проверить сервер в обход DNS — отделить «сломан DNS» от «сломан сервер»
$ curl -v --resolve shop.example.com:443:203.0.113.10 https://shop.example.com/

# где именно тратится время
$ curl -w 'dns %{time_namelookup}s  tcp %{time_connect}s  tls %{time_appconnect}s  всего %{time_total}s\n' \
       -o /dev/null -s https://shop.example.com
dns 0.004s  tcp 0.021s  tls 0.089s  всего 0.310s

Последняя команда — очень недооценённый приём. Она за один вызов показывает, на что уходит время: на резолв, на установку соединения, на TLS или на сам ответ приложения. Это сразу направляет расследование в нужную сторону вместо гадания.

Что ещё

ИнструментКогда
tcpdumpКогда надо увидеть настоящие пакеты. Подробно — глава 1.1, §10
digВсё про DNS. Подробно — глава 1.3, §10
nc -zv host portБыстрая проверка «открыт ли порт» без установки лишнего
openssl s_clientРазбор проблем с сертификатами и TLS
conntrack -LПосмотреть таблицу трансляций живьём: кто с кем и во что транслируется
iptables -L -v -nСчётчики по правилам — видно, срабатывает ли правило вообще
netstat -sСводные счётчики стека: переполнения очередей, отброшенные пакеты, повторы

§11Разбор типовых случаев

Здесь собрано то, что действительно случается, — с последовательностью действий. На собеседовании обычно и просят проговорить именно последовательность, а не назвать причину сразу.

«Сервис недоступен»

Самая общая формулировка, и метод один — идти снизу вверх по слоям, отсекая половину пространства поиска на каждом шаге.

1. имя резолвится? dig +short имя нет → DNS, дальше не идём 2. адрес отвечает? ping адрес (учитывая, что ICMP могут блокировать) 3. порт открыт? nc -zv адрес порт refused → приложение не слушает / слушает 127.0.0.1 → ss -tulpn на той стороне таймаут → файрвол, маршруты, политики → tcpdump с обеих сторон 4. TLS в порядке? openssl s_client -connect адрес:порт -servername имя 5. приложение отвечает? curl -v 6. где время? curl -w с таймингами

Мгновенный отказ против таймаута

Различие, которое стоит проговаривать в первую очередь, потому что оно делит все сетевые проблемы пополам.

Мгновенный connection refused означает, что пакет дошёл, ядро на той стороне увидело, что порт никто не слушает, и ответило сбросом. Сеть, маршруты и файрволы в порядке — проблема в приложении. Проверять: запущено ли оно, на каком адресе слушает, тот ли порт.

Долгий таймаут означает, что ответа не пришло вовсе. Тут виноват путь: дропает файрвол или сетевая политика, нет маршрута, ответ уходит не туда. Проверять парным дампом.

Работает не у всех / плавающие отказы

Слово «иногда» почти всегда означает исчерпание какого-то ресурса или неоднородность путей. Проверять по порядку:

  • dmesg | grep -i conntrack — не переполнилась ли таблица трансляций. При переполнении пакеты новых соединений молча отбрасываются, и в логах приложения нет ничего.
  • ss -tan state time-wait | wc -l — не кончаются ли исходящие порты.
  • netstat -s | grep -i -E 'listen|overflow' — не переполняется ли очередь принятых соединений на сервере.
  • Не за балансировщиком ли часть бэкендов мертва: тогда «иногда» — это просто попадание на сломанный экземпляр.

Соединение есть, а большие ответы не приходят

Диагноз почти наверняка MTU, метод — ping -M do -s с подбором размера, подробности в главе 1.1. Отдельно проверь, не блокируется ли ICMP: без него механизм определения MTU по пути не работает, и вместо аккуратного уменьшения сегментов получаешь чёрную дыру.

После переключения трафик всё ещё идёт на старый сервер

Два кандидата, и различаются они уровнем:

  • Если переключали виртуальный IP — устаревший ARP-кеш у клиентов или у маршрутизатора. Проверять tcpdump -i eth0 arp на предмет gratuitous ARP от нового владельца, лечить arping -U. Механика — в §5.
  • Если меняли DNS-запись — не вытек TTL, либо приложение кеширует резолв само. Проверять dig с разных резолверов и смотреть оставшийся TTL.

В Kubernetes: под не достучится до сервиса

1. $ kubectl get endpointslice -l kubernetes.io/service-name=myapp
      пусто → селектор не совпал ИЛИ поды не Ready. Это причина №1.

2. $ kubectl exec -it другой-под -- nc -zv 10.244.3.17 8080
      напрямую по IP пода: живо ли приложение вообще

3. $ kubectl exec -it под -- nslookup myapp.prod.svc.cluster.local
      резолвится ли имя — жив ли CoreDNS

4. $ kubectl get networkpolicy -n prod
      не запрещает ли политика. Помни: одна политика на namespace
      превращает модель в «запрещено всё, кроме описанного»

5. и только потом — дамп внутри netns пода

В бою

Держи под рукой отладочный под: kubectl run netshoot --rm -it --image=nicolaka/netshoot -- bash. Внутри есть dig, tcpdump, ss, curl, mtr, ipvsadm — всё, чего нет в нормальных минимальных образах приложений. Проверять связность из него честнее, чем с узла: под живёт в той же сети подов и под теми же сетевыми политиками, что и настоящее приложение.

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

«Когда хост отправляет пакет, он по маске сравнивает адрес получателя со своим и решает одно: получатель в моём сегменте или за маршрутизатором. В первом случае кадр отправляется прямо получателю, во втором — шлюзу. И там, и там нужен MAC-адрес следующего шага — за этим и нужен ARP: широковещательный вопрос "у кого такой IP", unicast-ответ, запись в кеш на несколько минут.

Дальше по пути IP-адреса в пакете не меняются — они говорят, куда доставить в итоге. А MAC-адреса переписываются на каждом маршрутизаторе, потому что они отвечают только за передачу на следующий хоп. Заодно на каждом хопе уменьшается TTL, на чём и работает traceroute.

ARP в работе всплывает при файловере виртуального IP: адрес переехал, а MAC за ним другой, и у всех в кешах старая запись — поэтому новый владелец рассылает gratuitous ARP, чтобы кеши обновились немедленно. Без этого файловер формально проходит, а трафик минуты уходит в никуда.

NAT — это подмена адресов на границе с запоминанием трансляции. SNAT/masquerade меняет источник для исходящих и делается в POSTROUTING, после выбора маршрута. DNAT меняет назначение для входящих и делается в PREROUTING, до выбора маршрута, иначе пакет уйдёт не туда. Правила применяются только к первому пакету, дальше всё держит conntrack — и вот у него есть лимит: при переполнении ядро молча дропает новые соединения и пишет в dmesg "table full", а выглядит это как случайные таймауты. В Kubernetes всё то же самое буквально: выход подов наружу — это masquerade на узле, Service — это DNAT, а externalTrafficPolicy: Local нужен ровно затем, чтобы убрать лишний SNAT и сохранить IP клиента.»