Сетевую часть Linux обычно «настраивают», но редко понимают. Добавляют iptables-правило, включают NAT, правят sysctl — и если трафик пошёл, считается, что задача решена. Проблемы начинаются ровно в тот момент, когда он не идёт, а поведение системы перестаёт быть очевидным. В Linux нет магии. Есть IP-пакет, его заголовки и строго определённый путь внутри ядра: маршрутизация, netfilter, conntrack, NAT, TCP/UDP стек. Если не понимать этот путь целиком, firewall выглядит как чёрный ящик, NAT — как случайный набор правил, а Kubernetes CNI — как нечто «особенное», существующее отдельно от обычной сети.
Эта статья — попытка собрать единую ментальную модель Linux-сетей:
Ethernet-кадр, почему ядро сначала работает с L2, и как по MAC-адресам и EtherType выбирается дальнейшая обработка
Что такое IP-пакет и какие решения ядро принимает, глядя на его заголовки
Как именно происходит маршрутизация и где в этот процесс встраивается firewall
Что на самом деле делает conntrack и почему он влияет на производительность
Как работает NAT
Где в этой цепочке находятся TCP и UDP
И почему Kubernetes CNI — это не магия, а обычный Linux networking, разложенный по namespace’ам и интерфейсам
Здесь не будет «рецептов на все случаи жизни» и копипасты правил. Цель статьи — не научить быстро настраивать сеть, а объяснить, почему она ведёт себя именно так. С этим пониманием большинство сетевых проблем перестают быть сложными и начинают быть просто инженерными.
Ethernet как точка входа

Ethernet — это первый протокольный уровень сетевого стека, с которого для Linux начинается осмысленная сетевая обработка. Ниже находятся физический уровень и драйверы устройств, но до появления IP-пакета ядро оперирует именно Ethernet-кадром, принятым на конкретном интерфейсе. На этом этапе ядро ещё не думает о маршрутизации, соединениях или политиках безопасности уровня L3/L4 — оно решает, что это за кадр и стоит ли вообще поднимать его выше по стеку. Ethernet задаёт границы локального сегмента, модель доставки по MAC-адресам и определяет, какой протокол будет обрабатывать полезную нагрузку дальше. Для Linux Ethernet — это точка демультиплексирования и первичной фильтрации: кадр либо отбрасывается, либо преобразуется в объект сетевого стека, с которым уже можно работать на уровне IP и выше.
Кадр как базовая единица приёма
Ядро получает данные от драйвера сетевой карты в виде Ethernet-кадра. В нём есть MAC-адрес назначения, по которому решается, предназначен ли кадр этому хосту, broadcast’у, multicast’у или должен быть проигнорирован. Если кадр не проходит базовые проверки, до IP он просто не доходит.
EtherType как указатель «что внутри»
Поле EtherType определяет, как интерпретировать полезную нагрузку: IPv4, IPv6, ARP, VLAN и т.д. Именно здесь происходит первое ветвление логики — какой стек протоколов будет задействован дальше. Без корректного EtherType IP-пакета для ядра не существует.
L2 до L3: где заканчивается Ethernet

На уровне Ethernet нет маршрутизации между сетями — есть только доставка внутри одного сегмента. Все bridge’и, VLAN’ы и MAC-таблицы работают здесь. Только после успешной обработки на L2 кадр может быть «распакован» до IP-пакета и передан в L3-логику.
IP как фундамент

IP — это минимальный, предельно простой протокол доставки пакетов между узлами. Он не гарантирует ни доставку, ни порядок, ни целостность данных — его задача ограничивается маршрутизацией пакета от источника к получателю. IP по своей природе stateless: каждый пакет обрабатывается независимо, без знания о предыдущих или последующих. Понятия «соединение» или «сеанс» для него не существуют и появляются либо на транспортном уровне, либо как внешняя надстройка в ядре. В контексте Linux именно IP-пакет является базовой единицей, с которой работает ядро: все последующие решения — маршрутизация, firewall, NAT, conntrack — принимаются на основе содержимого его заголовков. Ядро оперирует метаданными пакета, а не абстрактными «сервисами» или «приложениями», и любое более высокоуровневое поведение является результатом дополнительной логики поверх этого простого механизма.
Заголовоки IP-пакета

Для ядра Linux IP-пакет — это структура данных с чётко определёнными полями. Source и destination address используются при маршрутизации и policy routing, поле TTL защищает сеть от зацикливания маршрутов, а поле protocol указывает, какой протокол инкапсулирован в полезной нагрузке IP — TCP, UDP, ICMP или другой. При этом ICMP не является «уровнем выше», а относится к тому же сетевому уровню, что и IP, и логически обрабатывается внутри IP-стека. Отдельно стоит fragmentation: IP допускает фрагментацию пакетов, и ядро обязано либо собрать фрагменты обратно, либо отбросить их, если сборка невозможна. На практике фрагментация — частый источник проблем, потому что firewall и NAT работают с отдельными фрагментами, а не с «целым» пакетом, как это часто себе представляют.
MTU и фрагментация

MTU определяет максимальный размер IP-пакета, который может быть передан по каналу без фрагментации. Если пакет превышает MTU, он либо фрагментируется, либо отбрасывается — в зависимости от флагов пакета и работы Path MTU Discovery. В Linux это особенно критично для VPN и overlay-сетей, где реальный MTU ниже, чем кажется при взгляде на физический интерфейс. Неправильно подобранный MTU приводит к «плавающим» проблемам: соединение успешно устанавливается, но данные не передаются, TCP уходит в ретрансляции, а отладка неожиданно упирается в sysctl-параметры и правила firewall, которые на первый взгляд к проблеме не относятся.
Понимание того, где и почему происходит фрагментация, позволяет сразу исключить целый класс таких ситуаций. На практике фрагментация выглядит следующим образом. Предположим, хост формирует IP-пакет размером 2000 байт (включая IP-заголовок), а MTU на выходящем интерфейсе составляет 1500 байт. Такой пакет не может быть передан целиком и разбивается на два фрагмента: первый размером 1500 байт (20 байт IP-заголовок и 1480 байт полезной нагрузки) и второй размером 520 байт (20 байт IP-заголовок и оставшиеся 500 байт данных). Эти фрагменты передаются независимо и собираются обратно только на конечном хосте. Потеря любого из них означает потерю всего исходного пакета: для TCP это приводит к ретрансляциям и деградации производительности, для UDP — к «тихой» потере данных.
В overlay-сетях ситуация становится ещё хуже. Пакет может быть корректным с точки зрения MTU на хосте и иметь установленный флаг DF, но при ин��апсуляции внутри туннеля превысить реальный MTU канала. В этом случае он не фрагментируется, а отбрасывается на промежуточном узле. Стандартный MTU для Ethernet-интерфейса составляет 1500 байт, однако при использовании туннелей и инкапсуляции трафика — таких как IPIP, VXLAN или WireGuard — эффективный MTU уменьшается на размер добавляемых заголовков. В результате типичные значения MTU составляют: около 1480 для IPIP, порядка 1450 для VXLAN и примерно 1420 для WireGuard. Эти значения не являются магическими — они напрямую следуют из структуры инкапсуляции и должны учитываться при проектировании сетей.
Посмотреть MTU для всех интерфейсов можно через команду ниже:
ip a | grep mtu
ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000Как Linux маршрутизирует пакеты
Маршрутизация в Linux — это не «поиск подходящего маршрута», а детерминированный процесс с жёстко заданным порядком шагов. Ядро принимает решение о дальнейшей судьбе пакета, опираясь на IP-заголовок, таблицы маршрутизации и правила policy routing. Именно на этом этапе определяется, будет ли пакет обработан локально, отправлен на другой интерфейс или отброшен ещё до попытки передачи, независимо от NAT и firewall.
Таблица маршрутизации и принцип её работы
Таблица маршрутизации в Linux — это конкретный набор записей, каждая из которых описывает, что делать с пакетом, если его адрес назначения попадает в определённый диапазон. Записи в таблице не обрабатываются по порядку. Всегда применяется правило наиболее длинного префикса (longest prefix match): ядро выбирает маршрут с самым точным совпадением сети назначения.
Каждая запись маршрута содержит несколько ключевых параметров. Префикс назначения (destination) определяет, для каких адресов применяется маршрут — это может быть как сеть (192.168.1.0/24), так и конкретный хост (10.0.0.5/32). Параметр via указывает следующий хоп; если он отсутствует, сеть считается напрямую подключённой. Поле dev определяет интерфейс, через который пакет будет отправлен. Метрика используется только при равной длине префикса и служит для выбора предпочтительного маршрута. Дополнительно маршрут имеет scope (link, global, host) и тип, который напрямую влияет на дальнейшую обработку пакета. Посмотреть таблицу маршрутизации можно командами:
ip route show table main (Просмотр основной таблицы которая создается всегда)
route -n || ip route show (Linux)
route print (Windows)
ip route get <ip> (Linux)Последняя особенно полезна — она показывает, какой именно маршрут будет выбран ядром для конкретного IP, а не просто содержимое таблицы.
Типы маршрутов и их влияние на пакет
Не все маршруты означают передачу пакета дальше. В Linux существуют разные типы маршрутов, и они принципиально по-разному влияют на судьбу трафика. Обычный маршрут (unicast) приводит к отправке пакета на следующий хоп или интерфейс. Маршрут типа local означает, что адрес назначения принадлежит системе, и пакет будет доставлен в локальный сетевой стек. Типы blackhole, unreachable и prohibit приводят к отбрасыванию пакета ещё на этапе маршрутизации: в первом случае — молча, во втором — с ICMP Destination Unreachable, в третьем — с ICMP Administratively Prohibited. Такие маршруты активно используются в policy routing, VPN-сценариях и контейнерных сетях.
Несколько таблиц маршрутизации и policy routing
Важно понимать, что в Linux используется не одна таблица маршрутизации. Помимо основной таблицы main, существуют таблицы local, default и произвольное количество пользовательских таблиц. Выбор таблицы маршрутизации выполняется механизмом policy routing, описанным в правилах ip rule.
Алгоритм работы упрощённо выглядит так: ядро последовательно применяет правила ip rule, которые могут учитывать source-адрес, fwmark, uid процесса, TOS и другие параметры. Каждое правило указывает, в какой таблице искать маршрут. После выбора таблицы поиск маршрута выполняется по правилу наиболее длинного префикса. Если маршрут найден — он используется. Если нет — ядро переходит к следующему правилу. Это означает, что наличие маршрута в main не гарантирует его использования, если пакет был сопоставлен с другой таблицей на более раннем этапе.
Входящий и исходящий трафик — разные пути
Linux принципиально различает пакеты, пришедшие с сетевого интерфейса, и пакеты, сгенерированные локальными процессами. Входящий пакет сначала принимается с интерфейса, после чего для него выполняется маршрутизация: либо пакет предназначен локальной системе и передаётся в цепочку INPUT, либо должен быть переслан дальше и проходит через FORWARD. Исходящий пакет, напротив, изначально существует как сокет, привязанный к процессу, и маршрутизируется без этапа приёма с интерфейса, сразу попадая в OUTPUT. Из-за этого одно и то же правило iptables может срабатывать для транзитного трафика и полностью игнорироваться для локального, что часто становится неожиданностью при отладке.
Принятие маршрута и маршрут по умолчанию
После выбора таблицы маршрутизации ядро принимает решение о маршруте строго по правилу наиболее длинного префикса. Маршрут по умолчанию (0.0.0.0/0 или ::/0) — это маршрут с нулевой длиной префикса и используется только тогда, когда не найдено ни одного более специфичного маршрута. В системе может существовать несколько default-маршрутов, например, на разных интерфейсах. В этом случае выбор между ними определяется метрикой или правилами policy routing.
Если в системе существуют маршруты 10.0.0.0/8 и 10.1.0.0/16, пакет к адресу 10.1.2.3 всегда будет отправлен по маршруту 10.1.0.0/16, независимо от порядка добавления маршрутов или их метрик. Это прямое следствие правила longest prefix match.
Forwarding и локальная доставка
После того как маршрут выбран, ядро определяет, должен ли пакет быть доставлен локальному процессу или переслан на другой интерфейс. Если IP-адрес назначения принадлежит системе, пакет попадает в локальный стек и передаётся соответствующему сокету. Если адрес не локальный, пакет может быть переслан дальше только при включённом forwarding.
sysctl net.ipv4.ip_forwardИ ответ должен быть:
net.ipv4.ip_forward = 1Наличие маршрута не означает, что пакет будет переслан. Маршрут отвечает только на вопрос «куда идти», а forwarding — на вопрос «разрешено ли идти вообще». Если флаг net.ipv4.ip_forward равен 0, пакет в другой интерфейс не пойдёт и будет просто отброшен, даже если таблица маршрутизации полностью корректна. Этот флаг критичен для всех CNI (Container Network Interface) и kube-proxy, поскольку в контейнерных средах пакеты практически всегда перемещаются между сетевыми интерфейсами.
Netfilter: где firewall реально встраивается
Netfilter — это не firewall и не NAT, а фреймворк внутри ядра Linux, который позволяет перехватывать пакет в строго определённых точках его жизненного цикла. iptables и nftables — всего лишь пользовательские интерфейсы к этим хукам. Ключевая ошибка при работе с firewall — воспринимать правила как «набор условий», а не как конвейер, через который пакет проходит в фиксированном порядке. Если правило стоит не в том месте — оно не сработает никогда, независимо от своей корректности.
Netfilter hooks и их порядок
В Linux существует пять основных хуков netfilter:
PREROUTING — пакет только что пришёл с интерфейса, маршрутизация ещё не выполнена
INPUT — пакет предназначен локальной системе
FORWARD — пакет транзитный, будет переслан дальше
OUTPUT — пакет сгенерирован локальным процессом
POSTROUTING — маршрут уже выбран, пакет готов к отправке

Просмотр всех цепочек iptables и на каких хуках они висят:
iptables -L -n -v && iptables-save
Показывает текущие правила фильтрации и счётчики пакетов, что позволяет понять, через какие цепочки реально проходит трафик.
Для nftables:
nft -a list rulesetДефолтный вывод будет примерно таким:
table inet filter {
chain input {
type filter hook input priority filter; policy accept;
}
chain forward {
type filter hook forward priority filter; policy accept;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
Читать это конечно намного удобнее чем вывод iptables
Где происходит маршрутизация относительно netfilter
Маршрутизация выполняется между PREROUTING и INPUT/FORWARD для входящего трафика и до POSTROUTING для исходящего.
Это означает:
DNAT (Destination Network Address Translation) должен выполняться в PREROUTING
SNAT (Source Network Address Translation) — в POSTROUTING
Фильтрация INPUT/FORWARD работает уже после выбора маршрута
DNAT для входящего трафика:
iptables -t nat -A PREROUTING -d 203.0.113.10 -p tcp --dport 80 -j DNAT --to-destination 10.0.0.10:80Это правило изменяет адрес назначения до маршрутизации, чтобы ядро приняло решение, исходя уже из нового IP.
Почему порядок хуков критичен
Netfilter не «пробует правила, пока не получится». Пакет проходит хуки один раз и строго по порядку. Если, например, попытаться сделать DNAT в POSTROUTING или фильтровать входящий пакет в OUTPUT, правило будет синтаксически корректным, но логически бесполезным. Типичная ошибка:
iptables -A INPUT -d 203.0.113.10 -j DNAT --to 10.0.0.10Правило не сработает, потому что INPUT — это уже локальная доставка, а DNAT должен происходить до выбора маршрута. Отдельного внимания заслуживает локальный трафик. Пакеты, сгенерированные процессами на хосте, никогда не проходят PREROUTING. Это ломает ожидания при тестировании NAT и firewall «с той же машины». DNAT для локального трафика:
iptables -t nat -A OUTPUT -d 203.0.113.10 -j DNAT --to-destination 10.0.0.10Это правило необходимо, если локальный процесс обращается к сервису по внешнему IP, но должен попасть на внутренний адрес.
nftables: современный netfilter без наследия iptables
nftables — это не «новый iptables» и не просто другой синтаксис. Это полностью переработанная подсистема работы с netfilter, в которой исправлены архитектурные ограничения iptables. iptables исторически вырос как набор отдельных утилит и таблиц, связанных между собой неявным порядком обработки. nftables, напротив, изначально проектировался как единый язык описания правил и виртуальная машина внутри ядра, исполняющая эти правила детерминированно и атомарно. В nftables пользователь описывает не последовательность команд, а правила обработки пакетов, которые загружаются в ядро целиком. Внутри ядра эти правила компилируются в байткод и выполняются встроенной VM. Это означает, что ядро не «проходит по списку правил», как в iptables, а выполняет оптимизированный набор инструкций.
Ключевые концепции nftables:
table — логический контейнер правил (по семейству протоколов: ip, ip6, inet)
chain — точка привязки к netfilter hook
rule — условие и действие
set / map — структуры данных для эффективного матчингa
Не во всех дистрибутивах nftables установлен по умолчанию и приходится делать:
sudo apt update && sudo apt install nftables
sudo systemctl enable nftables
sudo systemctl start nftablesЯвная привязка к netfilter hooks
В nftables каждая цепочка явно привязывается к хуку и имеет приоритет выполнения. В отличие от iptables, где порядок скрыт за таблицами (nat, filter, mangle), здесь он описан явно и читаемо.
nft add chain inet filter input {
type filter hook input priority 0;
policy drop;
}Это правило буквально говорит ядру: тип цепочки — filter, точка в жизненном цикле — input , приоритет — 0, политика по умолчанию — drop.
Чем nftables концептуально лучше iptables
Атомарные обновления. В iptables правила применяются по одному. В момент обновления firewall может находиться в неконсистентном состоянии. В nftables весь ruleset загружается и применяется атомарно.
nft -f ruleset.nftЕсли в конфигурации ошибка — не применится ничего.
Производительность и масштабируемость. iptables линейно перебирает правила. Чем больше правил — тем хуже производительность. nftables использует sets и maps, что позволяет обрабатывать тысячи правил за O(1).
nft add set inet filter blacklist { type ipv4_addr; }
nft add rule inet filter input ip saddr @blacklist dropЭто заменяет сотни iptables-правил одним доступом к структуре данных.
Единый синтаксис для IPv4 и IPv6. iptables требует отдельные инструменты (iptables и ip6tables). nftables работает с семейством inet.
nft add table inet filterОдно правило — два протокола. Хотя ipv6 встречается сейчас очень редко - практически никогда, то этот пункт можно притянуть за плюс.
Удобная отладка. nftables позволяет трассировать прохождение пакета через ruleset.
nft monitor traceЭто один из самых мощных инструментов отладки netfilter, которого фактически не было в iptables.
iptables — это исторически сложившийся интерфейс к netfilter. nftables — это его правильная архитектурная реализация. Если iptables — это набор костылей, накопленных за 20 лет, то nftables — это попытка наконец описать firewall как программируемую систему, а не как список правил. Именно поэтому все современные системы и дистрибутивы движутся в сторону nftables, даже если внешне это не всегда заметно.
Conntrack — скрытый центр управления трафиком

Conntrack (connection tracking) — это подсистема ядра Linux, которая отслеживает состояние сетевых потоков. Важно сразу расставить акценты: чаще всего от conntrack страдает не TCP, а UDP. TCP — протокол stateful, с чётким жизненным циклом, корректно завершается через FIN/RST и относительно предсказуем для conntrack. Основные проблемы переполнения таблицы почти всегда приходят от UDP и ICMP-трафика. Conntrack работает не с «соединениями» прикладного уровня, а с потоками пакетов, объединённых по 5-tuple: source IP, destination IP, source port, destination port и протокол. Именно conntrack делает возможными stateful firewall, NAT и большинство сетевых сценариев в Linux. Firewall (iptables/nftables) не вычисляет состояние сам — он лишь читает то, что уже определило ядро через conntrack. Conntrack логически обрабатывается раньше правил firewall.
Как работает conntrack
При первом пакете нового потока ядро создаёт запись в таблице conntrack и помечает её состоянием NEW. Далее поток переходит в ESTABLISHED, а вспомогательные — в RELATED. Пакеты, которые не укладываются в ожидаемую модель, помечаются как INVALID и обычно отбрасываются firewall’ом.
Conntrack и NAT
NAT без conntrack невозможен. Ядро должно помнить соответствие внешних и внутренних адресов — эта информация хранится в таблице conntrack. Трансляция выполняется для первого пакета, после чего все последующие пакеты идут по сохранённому состоянию. Если запись conntrack удаляется, соединение рвётся независимо от правил firewall. NAT по определению stateful.
Таблица conntrack и её ограничения
Conntrack использует ограниченную таблицу в памяти ядра. При переполнении новые потоки перестают отслеживаться, что выглядит как массовые сетевые сбои и «рандомные» таймауты. Дефолтный размер таблицы в большинстве дистрибутивов — 32768 записей. Увеличение лимита — это не бесплатная операция: каждая запись — это память и работа ядра. Размер таблицы должен соответствовать профилю нагрузки, а не дефолтам дистрибутива.
Посмотреть текущую загрузку таблицы:
cat /proc/sys/net/netfilter/nf_conntrack_count
или
conntrack -L && conntrack -L | wc -lМаксимальный размер таблицы:
cat /proc/sys/net/netfilter/nf_conntrack_max
или
sudo sysctl -a | grep nf_conntrack_maxТекущие таймауты и параметры:
sudo sysctl -a | grep conntrack
Если таблица будет переполнена, то появится сообщение:
nf_conntrack: table full, dropping packetПочему чаще всего ломается именно UDP
Хотя UDP не имеет соединений на уровне протокола, для conntrack он всё равно stateful. Для каждого UDP-потока создаётся запись в таблице с таймаутом. Дефолтный таймаут для UDP — 30 секунд. Это означает, что каждый UDP-запрос держит слот в таблице минимум 30 секунд, даже если это одиночный пакет. При большом количестве запросов (DNS, метрики, syslog, NTP, RPC) таблица conntrack начинает заполняться лавинообразно. Именно поэтому UDP-сервисы первыми падают при проблемах с conntrack, а не TCP.
TCP и conntrack: где реальные проблемы
TCP для conntrack предсказуем. Он stateful, имеет строгую модель состояний (SYN_SENT, ESTABLISHED, FIN_WAIT, TIME_WAIT и т.д.) и после FIN корректно завершается. Проблемы с TCP начинаются не из-за самого протокола, а из-за таймаутов conntrack, которые живут отдельно от TCP-стека. Соединение может быть давно закрыто с точки зрения приложения, но всё ещё занимать слот в таблице conntrack.
Наиболее чувствительные параметры:
nf_conntrack_tcp_timeout_established— по умолчанию измеряется днями. Опасен для большого числа долгоживущих, но неактивных соединений.nf_conntrack_tcp_timeout_time_wait— именно TIME_WAIT часто забивает таблицу при большом числе коротких TCP-соединений.nf_conntrack_tcp_timeout_close_wait,nf_conntrack_tcp_timeout_fin_wait— влияют на «зависшие» соединения при некорректном завершении.
Типовой сценарий деградации:
сервис принимает много коротких TCP-соединений;
соединения корректно закрываются;
conntrack продолжает хранить их состояние;
таблица заполняется;
новые соединения начинают отбрасываться или не отслеживаются.
На этом этапе маршрутизация и firewall формально исправны — проблема исключительно в conntrack.
Отключение conntrack там, где он не нужен
Не весь трафик требует stateful-обработки. Для highload UDP и ICMP-сервисов conntrack часто можно и нужно отключать полностью. Это резко снижает нагрузку на таблицу и ускоряет обработку пакетов. Типичные примеры:
DNS-серверы (пример: частые запросы к kube-proxy по udp)
telemetry / metrics
trusted-сети без NAT и stateful-firewall
Conntrack и TCP/UDP живут параллельными жизнями. Если это не учитывать, система может ломаться при полностью корректных маршрутах и правилах firewall. Оптимизация conntrack — это не тюнинг, а обязательная часть проектирования сетей под нагрузкой, особенно в Kubernetes.
Отключить conntrack можно вот так:
iptables -t raw -I PREROUTING -j NOTRACK
iptables -t raw -I OUTPUT -j NOTRACK
или выборочно
iptables -t raw -A PREROUTING -i <interface_name> -j NOTRACK
iptables -t raw -A OUTPUT -o <interface_name> -j NOTRACKNAT

Что такое NAT на самом деле
NAT — это translation, а не tunneling. Network Address Translation занимается простой задачей: он переписывает поля в заголовках пакетов (IP-адреса и, как правило, порты). Никаких новых пакетов, encapsulation или виртуальных интерфейсов он не создает. В отличие от VPN или GRE, NAT не «оборачивает» трафик, а лишь модифицирует его на лету. Например, пакет 10.0.0.10:54321 → 8.8.8.8:53 на выходе может превратиться в 203.0.113.5:40001 → 8.8.8.8:53
NAT «один раз на соединение»
Это ключевой момент, который многие не понимают. Для connection-oriented протоколов (TCP и, в практическом смысле, UDP при включённом conntrack) NAT применяется в момент появления нового соединения. В этот момент ядро создаёт запись в conntrack, где фиксируется трансляция «внутренний адрес:порт ↔ внешний адрес:порт» и сопутствующее состояние. Все последующие пакеты, относящиеся к этому потоку, больше не проходят полную цепочку NAT-правил — они просто сопоставляются с уже существующей записью в conntrack и транслируются согласно ей. Поэтому изменение или даже полное удаление NAT-правил не влияет на уже установленные соединения: они продолжают работать до тех пор, пока соединение корректно не завершится (TCP FIN/RST) или пока не истечёт таймаут записи в conntrack. Это поведение часто вводит в заблуждение при отладке.
Практический пример: при настройке роутера можно запустить непрерывный ping, после чего удалить или отключить NAT. Пока соответствующая запись существует в conntrack, ICMP-эхо-запросы и ответы будут продолжать проходить. Как только запись удаляется (по таймауту или вручную), пакеты перестают транслироваться — NAT больше не применяется, и ping сразу обрывается. Важно понимать, что это не «магия NAT», а следствие stateful-обработки пакетов: первичное решение принимается один раз, а дальше используется закэшированное состояние.
Почему NAT не шифрует
NAT вообще не имеет отношения к криптографии. Он не скрывает содержимое пакетов, не защищает от MITM и не делает трафик «безопасным». Любой, кто видит трафик после NAT, видит его в открытом виде (если это не TLS/IPsec поверх). Миф о «NAT как безопасности» возник из-за побочного эффекта: входящие соединения без DNAT обычно не проходят. Но это не защита, а просто отсутствие состояния в conntrack.
Виды NAT
DNAT (Destination NAT)
Используется, когда нужно изменить адрес назначения (destination address) в IP-заголовке. Классический пример — проброс портов: 203.0.113.5:80 → 10.0.0.10:8080. DNAT применяется в цепочке PREROUTING (или OUTPUT для локально сгенерированного трафика) до выбора маршрута, потому что ядру нужно сначала понять реальный адрес назначения, чтобы корректно рассчитать маршрут дальнейшей доставки пакета.
SNAT (Source NAT)
Меняет адрес источника (source address) в IP-заголовке. Типичный сценарий — выход внутренней сети в интернет: 10.0.0.0/24 → 203.0.113.5. SNAT применяется в POSTROUTING, когда маршрут уже выбран и ядро точно знает, через какой интерфейс пакет будет отправлен. В отличие от MASQUERADE, SNAT обычно используют с явно заданным, статическим внешним IP-адресом.
MASQUERADE
Это частный случай SNAT, предназначенный для интерфейсов с динамическим IP-адресом, например при подключении к провайдеру без статического адреса. В отличие от обычного SNAT, внешний адрес не задаётся явно — ядро каждый раз подставляет текущий IP, назначенный на выходной интерфейс.
Это упрощает конфигурацию, но имеет цену. При смене IP-адреса интерфейса ядро вынуждено удалить все связанные conntrack-записи, так как продолжить трансляцию уже невозможно. В результате обрываются все активные соединения. Поэтому на серверах со статическим внешним адресом использование MASQUERADE — плохая практика: оно не даёт преимуществ по сравнению с SNAT, но добавляет лишние сбросы состояний и осложняет отладку.
Hairpin NAT (loopback NAT)
Нужен в ситуации, когда клиент из внутренней сети обращается к сервису по внешнему IP-адресу, который через DNAT снова указывает во внутреннюю сеть. Без hairpin NAT ответный трафик пойдёт в обход ожидаемого пути, и соединение развалится из-за несовпадения адресов и состояний.
Типичный пример: клиент в LAN открывает example.com, который резолвится во внешний адрес балансера или роутера, а сам сервис физически находится в той же внутренней сети. Hairpin NAT заставляет и прямой, и ответный трафик проходить через один и тот же NAT-путь, сохраняя корректное состояние соединения.
Типовые ошибки
NAT + policy routing
Частая проблема: SNAT меняет source IP, а policy routing (ip rule) матчит по оригинальному адресу. В результате пакеты после NAT уходят не в ту таблицу маршрутизации.
NAT + local traffic
DNAT для трафика, инициированного с самого хоста, не работает так же, как для внешнего. Пакеты не проходят PREROUTING. Для этого нужен DNAT в цепочке OUTPUT или явное использование iptables -t nat -A OUTPUT. Это классическая ловушка при настройке локальных прокси, ingress’ов и тестировании «с той же машины».
NAT в Kubernetes
kube-proxy (iptables или nftables режим) активно использует DNAT и SNAT. Основные грабли: неожиданный SNAT на egress, потеря реального source IP, проблемы с asymmetric routing при CNI, а также огромные таблицы правил. Многие считают, что «Kubernetes сам разрулит», но на деле NAT внутри кластера сильно усложняет отладку и требует четкого понимания, где именно переписываются адреса — на ноде или CNI.
TCP и UDP стек в Linux
TCP и UDP (L4 OSI) принято противопоставлять как «надежный» и «простой». Это верно на уровне спецификаций, но в реальной системе оба протокола перестают быть абстракцией и превращаются в набор структур, очередей и таймеров внутри ядра.
UDP: простота, которая заканчивается на RFC
UDP не знает соединений. Каждый пакет сам по себе: пришёл — обработался, не пришёл — никто не заметил. Нет handshake, нет подтверждений, нет повторной отправки. За счёт этого UDP выглядит быстрым и лёгким.
Но как только пакет попадает в Linux, иллюзия stateless заканчивается. Почти всегда он проходит через conntrack — даже если приложение об этом не подозревает. NAT, firewall, rate limit — всё это требует помнить, кто, куда и зачем шлёт пакеты. В итоге вокруг UDP вырастает полноценное состояние: записи в таблицах, таймеры, сборка мусора.
Проблема в том, что сам UDP никак не защищает систему от перегрузки. Если трафик приходит быстрее, чем ядро или приложение способны его переварить, пакеты просто начинают пропадать. Без сигналов, без ошибок, без повторов. На практике это выглядит как «рандом»: DNS иногда не отвечает, метрики пропадают, стриминг сыпется под нагрузкой. Формально всё работает, фактически — нет.
TCP: состояние как плата за предсказуемость
TCP начинается не с данных, а с договорённости. Трёхстороннее рукопожатие — это не формальность, а момент, когда ядро выделяет память, инициализирует управляющие структуры, ставит таймеры и берёт на себя ответственность за будущее соединение. Уже на этапе SYN сервер резервирует запись в half-open очереди и тратит ресурсы. Пока handshake не завершён, соединение ещё не готово, но цена за него уже уплачена.
Установка соединения: как проходит рукопожатие

SYN. Клиент отправляет SYN с начальным sequence number (ISN) и набором TCP-опций (MSS, window scaling, SACK permitted, timestamps). Сервер, получив SYN, создаёт заготовку соединения и отвечает.
SYN-ACK. Сервер подтверждает ISN клиента и сообщает свой ISN. На этом этапе соединение ещё не fully established: данные передавать нельзя, но состояние уже хранится в ядре.
ACK. Клиент подтверждает ISN сервера. Только после этого соединение переходит в ESTABLISHED и может передавать данные.
Если ACK не приходит, срабатывают таймауты и ретрансляции SYN-ACK. По их исчерпании запись очищается. Именно здесь SYN flood наиболее болезненен: атакующий заставляет сервер держать тысячи полусоединений.
Согласованность и надёжность
TCP обеспечивает согласованность потока за счёт sequence numbers и ACK. Каждый байт имеет номер, а принимающая сторона подтверждает, какие данные получены и в каком порядке. Потерянные сегменты выявляются по дыркам в последовательности и переотправляются.
Для оптимизации используются:
Cumulative ACK — подтверждается последний непрерывно полученный байт.
SACK — явно указываются полученные диапазоны, что ускоряет восстановление при потерях.
Retransmission timers — рассчитываются динамически на основе RTT. Ошибка в RTT или агрессивные таймауты напрямую бьют по latency и throughput.
В результате приложение получает упорядоченный байтовый поток без дубликатов и потерь, ценой дополнительной логики и состояния в ядре.
Управление потоком и перегрузкой
Дальше TCP делает то, за что его ценят — управляет потоком и перегрузкой.
Flow control: window size ограничивает объём данных «в полёте» без подтверждений. Это защита получателя от переполнения буферов.
Congestion control: алгоритмы (cubic, bbr и др.) ограничивают скорость отправки исходя из состояния сети, а не получателя.
Linux динамически подбирает размеры окон и темп передачи, но строго в рамках sysctl-лимитов. Неправильные значения net.core.rmem_max, wmem_max или забитые socket buffers приводят к искусственным бутылочным горлышкам, которые выглядят как «медленная сеть», хотя проблема локальная.
Разрыв соединения: как TCP закрывается
Закрытие соединения — это тоже протокол, а не мгновенное событие.
Одна сторона отправляет FIN (FINISH) — «я больше не буду слать данные».
Вторая сторона подтверждает FIN (ACK), но может продолжать передачу в обратном направлении.
Когда вторая сторона тоже заканчивает, она отправляет свой FIN.
Первый участник подтверждает его и переходит в TIME_WAIT.
TIME_WAIT нужен, чтобы:
гарантировать доставку последнего ACK;
дать сети время «вымыть» старые сегменты, которые могут прилететь с задержкой.
TIME_WAIT и таймауты
TIME_WAIT обычно длится 2×MSL (часто ~60 секунд). В этот период соединение уже мёртвое с точки зрения приложения, но живое с точки зрения ядра. При большом числе коротких соединений это быстро упирается в лимит file descriptors. Симптом: «не можем открыть новое соединение», при том что сервисы и сеть формально здоровы.
SYN cookies
Когда система перегружается, в игру вступают SYN cookies. Это аварийный режим, при котором ядро перестаёт хранить состояние на этапе handshake, кодируя его в sequence number. Сервер восстанавливает состояние только если клиент корректно завершил рукопожатие.
Это защищает от SYN flood, но имеет цену:
часть TCP-опций отключается
снижается эффективность передачи
усложняется отладка
Включённые SYN cookies — почти всегда индикатор того, что система уже работает на грани и лечит симптомы, а не причину.
Где здесь Kubernetes и CNI — кульминация
В этот момент важно убрать маркетинговую шелуху и сказать прямо: CNI — это не «оверлей» и не какая-то магия Kubernetes, а тонкий слой над обычным Linux networking. Сам по себе CNI — это всего лишь контракт (спецификация), по которому Kubernetes вызывает бинарник плагина и передаёт ему параметры, а всё остальное делается стандартными механизмами ядра. Фактически Kubernetes лишь инициирует вызов CNI-плагина, после чего полностью выходит из игры — дальше начинается чистый Linux. Принципиально важно понимать, что CNI ≠ overlay. Overlay — это всего лишь один из возможных способов доставки пакетов между узлами (VXLAN, IPIP, WIREGUARD), который может использоваться плагином, но не является сутью CNI. Независимо от того, используется overlay или нет, CNI всегда оперирует одними и теми же базовыми примитивами Linux: сетевыми namespace’ами, интерфейсами, маршрутами и правилами фильтрации и трансляции. Типичный набор действий CNI-плагина сводится к созданию veth-пары, переносу одного конца в namespace pod’а, назначению IP-адреса и настройке маршрутов и NAT — ровно тем же операциям, которые администраторы выполняли задолго до появления Kubernetes. Когда pod стартует, для него создаётся отдельный network namespace. CNI-плагин создаёт veth-пару: один интерфейс остаётся в root namespace ноды, второй переносится внутрь namespace pod’а и становится его eth0. Внутри pod’а на этот интерфейс назначается IP-адрес и прописывается default route, как правило указывающий на виртуальный шлюз, который может существовать только логически. С точки зрения ядра это выглядит абсолютно тривиально: обычный интерфейс, обычная таблица маршрутизации, никакой «кластерной» специфики.
Как примерно задается маршрутизация между подами:
ip netns add <pod-ns>
ip link add veth-host type veth peer name veth-pod
ip link set veth-pod netns <pod-ns>
ip addr add 10.244.1.5/32 dev veth-pod
ip route add default via 10.244.1.1
iptables -t nat -A POSTROUTING ...Когда приложение в pod’е отправляет пакет, он проходит через eth0, попадает во veth и «выпадает» во второй конец пары уже в root namespace ноды. Дальше пакет обрабатывается стандартным routing decision ноды: выбирается маршрут, определяется выходной интерфейс, и на этом этапе могут применяться правила NAT. Типичный пример — SNAT в цепочке POSTROUTING для трафика из pod-сети во внешний мир. Все состояния соединений при этом фиксируются в conntrack, и именно поэтому переполнение conntrack-таблицы напрямую приводит к сетевым проблемам pod’ов: дропам пакетов, таймаутам и нестабильному поведению сервисов. Это не проблема Kubernetes и не «глюк CNI» — это прямое следствие работы netfilter в ядре Linux. Если используется overlay, например VXLAN, то поверх этой модели добавляется дополнительный транспортный слой: пакет инкапсулируется и доставляется на другую ноду, где проходит обратный путь. Однако это не меняет базовую архитектуру: внутри каждой ноды всё так же остаются namespace’ы, veth-пары, маршруты и правила фильтрации. Overlay здесь — всего лишь способ доставки пакета между узлами, а не фундамент CNI. Из этого напрямую следует, что Calico, Flannel и Cilium — это не «разные сети», а разные реализации одной и той же модели. Они по-разному выбирают транспорт, по-разному программируют правила и по-разному автоматизируют настройку, но базовый набор примитивов всегда одинаков: network namespace, veth, routing, NAT и отслеживание состояний. Поэтому CNI невозможно нормально дебажить без понимания Linux networking. Если не ясно, как ядро принимает решение о маршрутизации, чем PREROUTING отличается от POSTROUTING, где именно применяется SNAT и какую роль играет conntrack, то отладка неизбежно сведётся к гаданию по логам плагина и YAML-манифестам Kubernetes. В реальности CNI — это просто Linux networking, и разбирать его нужно именно на этом уровне, если цель — не угадывать, а понимать, что реально происходит в кластере.
