В этой инструкции соберём «живой» набор правил nftables для stateful NAT: сделаем dnat/snat, настроим проброс портов на сервисы (в том числе в Docker), а затем научимся быстро проверять, куда реально ходит трафик: через conntrack, счётчики counter и точечную трассировку nftrace. Фокус — на практической эксплуатации, когда нужно не только «чтобы работало», но и диагностировать проблему в проде за минуты.
Что важно понимать про nftables NAT и stateful-фильтрацию
nftables удобнее держать в голове как два слоя, которые решают разные задачи:
- NAT (переписывание адресов/портов): выполняется в цепочках NAT с хуками
preroutingиpostrouting(иногда ещёoutputдля локального DNAT). - Firewall (фильтрация): обычно таблица
filterс цепочкамиinput/forward/output.
Stateful-поведение обеспечивает подсистема conntrack: она создаёт запись о соединении и позволяет писать правила в стиле «разрешить новые соединения на нужный порт, а дальше пускать established/related». Это ключ к тому, чтобы port forwarding не превращался в «дырку во всё».
Большинство проблем с NAT/port forwarding в итоге упираются в три вещи: не тот интерфейс/маршрут, нет разрешающего правила в
forward, или ответный трафик ломается из‑за отсутствующего/неподходящего SNAT.
Схема стенда (типовой кейс)
Будем считать, что у вас один сервер (например, VDS) с публичным IP и Docker:
- Публичный интерфейс:
eth0, внешний IP:203.0.113.10. - Docker bridge:
docker0, подсеть контейнеров:172.17.0.0/16. - Нужно пробросить порт
443на контейнер172.17.0.2:8443. - Плюс — классический SNAT/MASQUERADE для исходящих из контейнеров.
Все адреса в примерах — из документационных диапазонов; подставьте свои реальные.
Если вы параллельно разбираетесь, как Docker «подмешивает» правила и как это не сломать строгим policy drop, полезно держать под рукой отдельный материал: Docker и firewall: как согласовать iptables/nftables и не сломать published ports.
Когда нужен предсказуемый сетевой периметр и стабильная маршрутизация, удобнее отрабатывать такие конфигурации на отдельном сервере: на VDS вы быстро воспроизведёте схему с публичным IP, Docker и строгими policy.
Проверяем базу: маршрутизация, IP forwarding, rp_filter
Включаем маршрутизацию
Для DNAT на контейнер через forward нужен IPv4 forwarding:
sysctl -w net.ipv4.ip_forward=1
Чтобы сделать это постоянно (пример для /etc/sysctl.d/99-forwarding.conf):
printf '%s\n' 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-forwarding.conf
sysctl --system
rp_filter и «SYN приходит, ответа нет»
Если у вас несколько интерфейсов/маршрутов, policy routing или асимметрия, жёсткий rp_filter может дропать «неожиданные» ответы. Для типового сервера с одним uplink обычно всё ок, но при симптомах «SYN приходит, SYN-ACK не уходит» — проверьте значения:
sysctl net.ipv4.conf.all.rp_filter
sysctl net.ipv4.conf.eth0.rp_filter
Часто достаточно перевести в режим 2 (loose), если это уместно в вашей сети и вы понимаете последствия:
sysctl -w net.ipv4.conf.all.rp_filter=2
sysctl -w net.ipv4.conf.eth0.rp_filter=2
Минимальный каркас nftables: NAT отдельно, фильтрация отдельно
Практичнее всего держать NAT и фильтрацию раздельно: table ip nat и table inet filter. Так проще отлаживать, а правила фильтрации при необходимости можно расширить на IPv6 без дублирования.
Перед экспериментами полезно сохранить текущие правила:
nft list ruleset > /root/nftables.ruleset.backup
Конфиг: NAT + firewall со stateful-логикой
Ниже — рабочий «скелет» для /etc/nftables.conf. Он делает DNAT 443 на контейнер 172.17.0.2:8443, включает маскарадинг исходящих из docker-сети и держит строгие политики с разрешением только нужного.
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iifname "lo" accept
ct state established,related accept
ct state invalid drop
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
tcp dport 22 accept
counter comment "INPUT default drop counter"
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
ct state invalid drop
iifname "eth0" oifname "docker0" tcp dport 8443 ct state new accept
iifname "docker0" oifname "eth0" ct state new accept
counter comment "FORWARD default drop counter"
}
chain output {
type filter hook output priority 0; policy accept;
}
}
table ip nat {
chain prerouting {
type nat hook prerouting priority -100; policy accept;
iifname "eth0" tcp dport 443 dnat to 172.17.0.2:8443
}
chain postrouting {
type nat hook postrouting priority 100; policy accept;
oifname "eth0" ip saddr 172.17.0.0/16 masquerade
}
}
Ключевые моменты:
- DNAT делается в
ip nat/prerouting: всё, что прилетает наeth0:443, переписывается на172.17.0.2:8443. - SNAT делается в
ip nat/postrouting: весь исходящий из docker-сети трафик при выходе наeth0маскарадуется. - В
inet filter/forwardмы явно разрешаем форвардинг к контейнеру и исходящие из контейнеров соединения наружу, остальное дропаем. - В
inet filter/inputоткрываем только нужное на сам хост (например, SSH).
Применение конфигурации:
nft -f /etc/nftables.conf
Если вы собираете такую схему на чистом сервере с публичным IP, удобнее всего повторять конфигурацию на отдельной машине: VDS подойдёт для тестов NAT, Docker и строгих policy без сюрпризов.

Где и почему чаще всего ошибаются с port forwarding
1) Разрешают 443 во forward вместо 8443
После dnat пакет «внутри» уже направлен на 172.17.0.2:8443. Поэтому во forward корректнее матчить конечный порт (8443), как в примере. Если разрешить только dport 443, правило не сработает: счётчики будут стоять на нуле, а клиент увидит таймаут.
2) Делают DNAT, но забывают SNAT там, где он реально нужен
Если целевой сервис находится в сети, которая не знает, как вернуть ответ на клиента (или возвращает через другой шлюз), без snat/masquerade получится «односторонняя» связь. Для docker-сетей на одном хосте чаще всего достаточно маскарадинга подсети контейнеров на внешний интерфейс.
3) Включают строгий policy drop во forward без исключений
Когда вы делаете строгий firewall, цепочка forward почти всегда требует отдельной настройки. Даже если NAT идеальный, пакет может быть переписан и тут же отброшен политикой drop.
Счётчики counter: быстрый способ понять, «попадаем ли в правило»
Счётчики counter — первое, что стоит добавить в отладочный цикл. Их можно вешать на конкретное правило (DNAT/allow) или ставить «на хвост» цепочки, чтобы видеть, что падает на дефолтном дропе.
Посмотреть правила со статистикой и handle:
nft -a list table inet filter
nft -a list table ip nat
Практический приём: повесьте counter на правило DNAT и на разрешающее правило во forward. Тогда сразу видно, где именно «обрывается» путь: до NAT, после NAT или уже на выходе/обратном трафике.
Сброс счётчиков перед тестом
Чтобы результаты были чистыми:
nft reset counters
Если пробрасываете наружу HTTPS, не откладывайте нормальный TLS: с валидным сертификатом проще и безопаснее поддерживать прод. Сертификат можно выпустить через SSL-сертификаты под ваш домен.
conntrack: смотрим реальное состояние stateful NAT
В stateful NAT «магия» лежит в conntrack-записях. Если DNAT/SNAT отработали, вы увидите соответствующие трансляции в таблице соединений: оригинальное направление и направление ответа (reply).
Просмотр conntrack
Утилита обычно называется conntrack (пакет conntrack-tools). Базовые примеры:
conntrack -L | head
conntrack -L -p tcp | grep -E 'dport=8443|dport=443' | head
Для «живого» просмотра событий (удобно параллельно с тестом через curl):
conntrack -E -o timestamp,extended
На что смотреть:
- Состояния:
NEW,ESTABLISHED,ASSURED. - Original и reply: должно быть видно, что внешний
203.0.113.10:443соответствует внутреннему172.17.0.2:8443.
Типовой симптом: счётчики DNAT растут, а соединения «не живут»
Для TCP это чаще всего означает одно из трёх:
- пакет дропнули фильтром (обычно
forward); - сервис в контейнере недоступен/не слушает порт (нет ответа на SYN);
- ответ уходит не тем маршрутом и теряется (маршрутизация,
rp_filter, внешний балансировщик).
nftrace в nftables: точечная трассировка пакета по правилам
Когда непонятно, в какое правило попал пакет (особенно при большом ruleset и include-файлах), помогает meta nftrace. Вы включаете трассировку только для интересующего трафика, а затем читаете сообщения ядра с «маршрутом» пакета по цепочкам/правилам.
Включаем трассировку для конкретного потока
Например, трассировать входящие TCP на 443 с внешнего интерфейса. Обратите внимание: это правило добавится в input, то есть поможет понять судьбу пакетов, адресованных самому хосту. Для проброса на контейнер зачастую полезнее ставить trace в forward, но начать можно с самого очевидного.
nft add rule inet filter input iifname "eth0" tcp dport 443 meta nftrace set 1 counter comment "trace https"
Дальше смотрим сообщения ядра. Универсально:
dmesg -T | grep -i nft | tail -n 50
Если у вас systemd-journald:
journalctl -k -n 200 | grep -i nft
После отладки правило с nftrace лучше удалить, чтобы не создавать лишний шум:
nft -a list chain inet filter input
Находим handle и удаляем:
nft delete rule inet filter input handle 123
Trace полезнее counters, когда у вас пересекающиеся условия, несколько таблиц или Docker/оркестратор подмешивает свои правила. Он показывает «путь» пакета по цепочкам.
Если хотите углубиться в «изоляцию контейнеров и кто куда может ходить» (особенно в мультиарендных средах), посмотрите: изоляция контейнеров на практике: gVisor и Firecracker. Это не про NAT напрямую, но часто всплывает рядом, когда вы делаете сетевую сегментацию.

Docker и nftables: что учитывать, чтобы не воевать с автомагией
Docker традиционно управляет правилами для публикации портов и NAT. На системах с nftables это часто выглядит как набор правил, добавленных через совместимость (iptables-nft), и как дополнительные цепочки/таблицы в ruleset.
Вариант 1: публиковать порты через Docker, а nftables держать «строгим, но совместимым»
Если вы делаете docker run -p 443:8443 (или через compose публикуете порт), Docker сам создаст DNAT и сопутствующие правила. Ваша задача — не сломать это строгим policy drop во forward. В таком сценарии обычно безопаснее разрешать нужное во forward по интерфейсам и состояниям (ct state), а не пытаться полностью заменить Docker-NAT ручными правилами.
Вариант 2: отключить управление firewall у Docker и писать всё самим
Путь для тех, кому нужна полная предсказуемость ruleset. Но он требует дисциплины: вы обязаны сами обеспечить и NAT, и открытие портов, и межконтейнерные политики. Смешивать два «источника правды» (Docker и ручные правила) без контроля — частая причина фантомных проблем.
Как понять, кто добавил правила
Смотрите полный ruleset и ищите цепочки/таблицы, связанные с Docker:
nft list ruleset
Если видите много сущностей, которые вы не создавали, зафиксируйте подход: либо интегрируетесь (оставляете Docker управлять публикацией), либо централизуете (Docker не трогает firewall, и вы всё описываете в nftables).
Проверка port forwarding: короткий чек-лист
- Сервис жив? С хоста проверьте доступность контейнера: запрос на
172.17.0.2:8443(илиncна порт). - DNAT срабатывает? Счётчик на правиле DNAT растёт.
- Forward разрешён? Счётчик на allow-правиле в
forwardрастёт, а default-drop — не растёт (или растёт только на «левом» трафике). - Conntrack видит соединение? Есть запись TCP с ожидаемыми original/reply направлениями.
- Ответы уходят тем же путём? При сомнениях — проверяйте маршруты и
rp_filter. - Trace показывает путь пакета, если всё равно непонятно, где его «съели».
Несколько практичных улучшений для продакшена
Ограничение источников для проброса
Если проброс нужен только от определённых IP/подсетей, добавьте условие в DNAT и/или forward. Пример (разрешить DNAT только из одной подсети):
nft add rule ip nat prerouting iifname "eth0" ip saddr 198.51.100.0/24 tcp dport 443 dnat to 172.17.0.2:8443
Логирование именно дропа (и только по делу)
Вместо сплошного логирования всего, логируйте только то, что неожиданно падает. Например, перед финальным дропом в forward можно добавить лог с ограничением частоты:
nft add rule inet filter forward limit rate 10/second burst 20 packets log prefix "nft fwd drop: " flags all counter
Сервисные порты на самом хосте и на контейнерах — разделяйте
Чётко решите, что открывается в input (сам хост), а что — в forward (маршрутизируемый трафик к контейнерам/внутренним сетям). Это снижает риск «случайно открыли админку наружу» и упрощает аудит.
Итоги
nftables для NAT становится действительно удобным, когда вы строите систему вокруг наблюдаемости: добавляете counter к ключевым правилам, проверяете state через conntrack и включаете точечный nftrace, когда нужно понять, где именно пакет теряется. В сочетании со строгим stateful firewall (ct state) это даёт предсказуемый и безопасный port forwarding как для обычных сервисов, так и для Docker-окружений.
Если нужен следующий шаг — можно разобрать усложнённый вариант: несколько публичных IP (статический SNAT), разные контейнерные сети, hairpin NAT и аккуратные правила для published ports без конфликта с Docker.


