Акция Панель управления ispmanager для VDS — первый месяц бесплатно
до 31.07.2026 Подробнее
Выберите продукт

nftables: stateful NAT, проброс портов и Docker — счётчики, conntrack и trace на практике

Разберём nftables NAT в реальной эксплуатации: stateful DNAT/SNAT и проброс портов до контейнеров Docker. Пошагово соберём правила, включим counters, посмотрим conntrack и научимся отлаживать firewall через nftrace и логи ядра без лишнего шума.
nftables: stateful NAT, проброс портов и Docker — счётчики, conntrack и trace на практике

В этой инструкции соберём «живой» набор правил 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 без сюрпризов.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Схема проброса порта через DNAT на контейнер и обратного трафика через SNAT

Где и почему чаще всего ошибаются с 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-сертификаты под ваш домен.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой 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 напрямую, но часто всплывает рядом, когда вы делаете сетевую сегментацию.

Пример вывода nftables trace в логах ядра для отладки прохождения пакета

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: короткий чек-лист

  1. Сервис жив? С хоста проверьте доступность контейнера: запрос на 172.17.0.2:8443 (или nc на порт).
  2. DNAT срабатывает? Счётчик на правиле DNAT растёт.
  3. Forward разрешён? Счётчик на allow-правиле в forward растёт, а default-drop — не растёт (или растёт только на «левом» трафике).
  4. Conntrack видит соединение? Есть запись TCP с ожидаемыми original/reply направлениями.
  5. Ответы уходят тем же путём? При сомнениях — проверяйте маршруты и rp_filter.
  6. 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.

Поделиться статьей

Вам будет интересно

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину

Ошибка mount: wrong fs type, bad option, bad superblock в Debian/Ubuntu может означать и простую опечатку в имени раздела, и пробл ...
Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление

Если XFS-раздел внезапно стал доступен только для чтения, а сервер ушёл в emergency mode, главное — не спешить. Разберём безопасны ...
Debian/Ubuntu: как исправить Failed to fetch при apt update OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Failed to fetch при apt update

Ошибка Failed to fetch при apt update в Debian и Ubuntu обычно связана не с самим APT, а с DNS, сетью, зеркалом, прокси, временем ...