
Если администрировать Linux-сервера достаточно долго, рано или поздно сталкиваешься с сетевой фильтрацией. Где-то нужно закрыть лишние порты, где-то ограничить доступ между сегментами сети, а где-то настроить NAT.
На практике это почти всегда приводит к iptables: таблицы, цепочки, правила — и со временем конфигурация начинает напоминать археологический слой. Правила копируются, дополняются, теряют актуальность, и через пару лет уже сложно понять, почему конкретное правило вообще существует.
В этой статье разберёмся, как появился nftables, чем он отличается от привычного iptables, как устроена его архитектура и как на практике использовать его для настройки firewall на Linux-сервере.
Историческая справка: от ipfwadm к nftables

Подсистема сетевой фильтрации в Linux появилась не сразу в том виде, к которому все привыкли. В ранних версиях ядра 2.0 использовался инструмент ipfwadm, который позволял управлять простыми правилами фильтрации пакетов. По современным меркам его возможности были весьма скромными: базовая фильтрация по адресам и портам без каких-либо сложных механизмов обработки соединений.
С выходом ядра Linux 2.2 на смену ему пришёл ipchains. Он стал заметным шагом вперёд: появилась более понятная структура цепочек, правила стали гибче, а сама система фильтрации начала лучше вписываться в сетевую архитектуру ядра. Тем не менее, довольно быстро стало понятно, что и этого подхода недостаточно.
Следующим этапом развития стал iptables, появившийся вместе с подсистемой netfilter в ядре Linux 2.4. Именно iptables на долгие годы стал стандартным инструментом для настройки firewall в Linux. Он добавил поддержку stateful-фильтрации, NAT, различные таблицы обработки пакетов и модульную архитектуру, позволяющую расширять возможности системы.
Однако архитектура iptables разрабатывалась в начале 2000-х, когда сетевые нагрузки и требования к инфраструктуре были совсем другими. С ростом объёмов трафика, появлением контейнеризации и усложнением сетевых топологий конфигурации iptables начали разрастаться до сотен и тысяч правил. Поддерживать такие наборы становилось всё сложнее, а производительность при больших списках правил заметно снижалась.
Чтобы решить эти проблемы, в ядре Linux появилась новая система — nftables. Она стала следующим поколением интерфейса к подсистеме netfilter и предложила более универсальную модель описания правил, которая одновременно упрощает конфигурацию и повышает эффективность обработки сетевого трафика.
Почему iptables уступил место nftables
Со временем у iptables начали проявляться архитектурные ограничения. Основная проблема заключалась в модели обработки правил: пакет проверяется линейно, последовательно сравниваясь с каждым правилом цепочки. При небольшом количестве правил это незаметно, но цепочки легко разрастаются до сотен и тысяч записей (NAT, ACL, сервисные исключения), и обработка каждого пакета превращается в последовательный перебор, что влияет на производительность.
Дополнительную сложность создавал исторически сложившийся набор отдельных утилит — iptables, ip6tables, arptables и ebtables. Хотя они использовали одну подсистему netfilter, конфигурации оставались разрозненными, а изменения крупных наборов правил требовали пересборки цепочек в userspace и повторной загрузки в ядро, что могло приводить к кратковременной рассинхронизации правил.
nftables появился как переработанная архитектура фильтрации. Он предоставляет единый интерфейс управления для IPv4, ARP и bridge-трафика, а правила компилируются во внутреннее представление из выражений и операций, что снижает количество проверок при обработке пакетов. Вместо длинных цепочек правил используются структуры sets и maps, позволяющие проверять большие наборы адресов или портов внутри одного правила.
Первые версии nftables (ветка 0.x), появившиеся в середине 2010-х, фактически закладывали новую архитектуру netfilter: единый ruleset, выражения вместо линейных правил и атомарные обновления конфигурации. С выходом ветки 1.0.x система стала зрелой для production-использования: стабилизировался синтаксис, появились интервальные sets, таймауты для элементов, улучшенные maps и более предсказуемые атомарные обновления правил. Современная ветка 1.1.x (актуальная версия — 1.1.6) уже сосредоточена на развитии сетевых возможностей и удобстве эксплуатации: улучшена диагностика и трассировка пакетов (nft monitor trace), расширена работа с routing-информацией (FIB), добавлена поддержка новых типов сетевого трафика и туннелей, а также оптимизирована работа с большими наборами правил
Линейная модель обработки iptables
Если обозначить:
— количество правил в цепочке
— среднее время проверки одного правила
То время обработки одного пакета можно оценить как:

В худшем случае пакет проверяется против всех правил цепочки. С точки зрения алгоритмов такая модель имеет сложность:

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

где — число пакетов в секунду. При

Фактически ядро выполняет миллиард сравнений правил в секунду. Даже если каждая проверка занимает доли микросекунды, суммарная нагрузка начинает заметно влиять на CPU.
Подход nftables: множества и ассоциативные структуры
С точки зрения алгоритмов проверка принадлежности множеству обычно реализуется через хеш-таблицы или другие индексированные структуры. Средняя сложность поиска в такой структуре:

Тогда время обработки пакета можно представить как:

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

Даже для большого набора элементов логарифмический рост остаётся относительно небольшим.

То есть вместо десятков тысяч проверок выполняется примерно 16 операций поиска.
Если обобщить различие между подходами:
Механизм |
Алгоритмическая сложность |
|---|---|
iptables |
|
nftables sets |
|
nftables interval sets |
Основные сущности nftables: tables, chains, rules, sets
table — это верхний уровень логической группировки правил. Внутри таблицы хранятся цепочки, а сами таблицы обычно создаются для определённого семейства протоколов. Семейство определяет, с каким типом трафика будет работать ruleset и на каком уровне сетевого стека он применяется.
На практике используются несколько основных вариантов:
ip— только IPv4-трафикip6— только IPv6inet— объединённое семейство для IPv4 и IPv6bridge— фильтрация на уровне L2 (bridge-интерфейсы)netdev— ранняя обработка пакетов на ingress (ещё до netfilter pipeline)
Наиболее часто используемый вариант — это inet. Он позволяет писать единый ruleset сразу для IPv4 и IPv6, избавляя от дублирования правил.
Например, создадим таблицу для IPv4:
nft add table ip filter
Посмотреть существующие таблицы можно так:
nft list tables
На практике часто используются таблицы вроде filter, nat или mangle, но в nftables это просто соглашение об именовании — таблицу можно назвать как угодно.
chain — это набор правил, через который проходит пакет. Именно цепочки определяют, в какой момент обработки трафика будут применяться правила.
Создадим базовую цепочку для входящего трафика:
nft add chain ip filter input { type filter hook input priority 0 \; policy drop \; }
Здесь происходит сразу несколько вещей:
hook input— цепочка привязывается к точке обработки входящего трафикаtype filter— указываем тип цепочкиpolicy drop— политика по умолчанию (если ни одно правило не сработало)
rule — это конкретное условие и действие. Именно правила определяют, что делать с пакетом: разрешить, запретить, изменить или передать дальше.
Простой пример: разрешим SSH:
nft add rule ip filter input tcp dport 22 accept
Теперь пакет с TCP-портом 22 будет принят. Добавим правило для уже установленных соединений:
nft add rule ip filter input ct state established,related accept
Это классический паттерн для stateful firewall — разрешаем трафик, относящийся к уже существующим соединениям.
set — это одна из самых полезных возможностей nftables. Он позволяет хранить набор значений, которые можно проверять в одном правиле. Это сильно уменьшает количество правил и делает конфигурацию гораздо компактнее.
Создадим набор доверенных IP:
nft add set ip filter trusted_ips { type ipv4_addr \; }
Добавим адреса:
nft add element ip filter trusted_ips { 192.168.1.10, 192.168.1.20 }
Теперь можно написать правило:
nft add rule ip filter input ip saddr @trusted_ips accept
В iptables для такого сценария пришлось бы писать несколько отдельных правил.
Atomic updates и транзакционность
В iptables изменение правил — это по сути последовательность операций: добавили правило, удалили правило, вставили куда-то в середину. В момент применения ruleset система может находиться в промежуточном состоянии, особенно если изменения выполняются скриптом или системой автоматизации. На практике это выливается в неприятные эффекты: кратковременные «дыры» в firewall, race condition при параллельных обновлениях.
В nftables этот класс проблем решён на уровне архитектуры. Все изменения описываются как единая транзакция: ruleset формируется в userspace, после чего целиком отправляется в ядро и применяется атомарно. Либо применяется всё, либо не применяется ничего — промежуточных состояний просто не существует.
Чтобы это стало более наглядно, представим простой сценарий: у нас есть базовый firewall с policy drop, и мы хотим обновить список разрешённых портов — например, добавить 443 и убрать 80.
В iptables это обычно выглядит так:
iptables -D INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT
Между этими двумя командами есть окно, в котором:
порт 80 уже закрыт
порт 443 ещё не открыт
Если в этот момент приходит трафик — он попадёт под DROP. А если порядок команд поменять, можно получить обратную ситуацию: оба порта на короткое время окажутся открытыми.
В nftables тот же сценарий делается через транзакцию:
nft -f - <<EOF flush ruleset table inet filter { chain input { type filter hook input priority 0; policy drop; tcp dport 443 accept } } EOF
Здесь новый ruleset сначала полностью формируется в userspace, и только потом одним атомарным применением заменяет старый. В системе никогда не будет состояния «между 80 и 443» — либо действует старая конфигурация, либо уже новая.
Debugging и observability
Базовый инструмент здесь — nft monitor trace, который в реальном времени показывает путь пакета через цепочки и правила. Достаточно запустить:
nft monitor trace
И затем сгенерировать трафик (например, curl или nc) — в выводе будет видно, какие именно правила матчились и какое решение было принято. Для анализа текущего состояния конфигурации используется nft list ruleset, а если нужно работать точечно (например, удалить конкретное правило), полезен вариант с handle’ами:
nft -a list ruleset
Этот вызов показывает уникальные идентификаторы правил
Практический benchmark: сравниваем производительность iptables и nftables
Чтобы получить воспроизводимые и интерпретируемые цифры, проведём небольшой benchmark. Его задача — смоделировать поведение типичного L3/L4 firewall, выполняющего набор базовых операций:
обработку состояний соединений (conntrack)
разрешение уже установленных соединений (ESTABLISHED, RELATED)
фильтрацию по сервисным портам (80, 443, 22)
политику по умолчанию — DROP
В тесте рассматриваются три сценария:
iptables с линейным набором правил — классическая модель netfilter, где пакет последовательно проходит через всю цепочку
nftables с линейными правилами — функционально аналогичная конфигурация, но через современный API
nftables с использованием sets — более эффективный подход, где проверка IP-адресов выполняется через структуры данных с близкой к
сложностью
Benchmark фиксирует следующие характеристики:
время применения ruleset — насколько быстро firewall загружает правила
throughput — пропускная способность при генерации трафика
pps (packets per second) — нагрузка на пакетную обработку
CPU usage — средняя загрузка процессора во время теста
SoftIRQ (NET_RX) — интенсивность обработки пакетов в сетевом стеке ядра
состояние conntrack — текущее количество отслеживаемых соединений
latency — влияние firewall на RTT (через ping)
Для генерации нагрузки будет использован iperf3, при этом тест включает фазу прогрева (warmup), чтобы исключить влияние холодных кешей и начальной инициализации.
fw-bench.sh
#!/usr/bin/env bash set -euo pipefail RULES=${RULES:-10000} TEST_TIME=${TEST_TIME:-20} SERVER_IP=${SERVER_IP:-127.0.0.1} IFACE=${IFACE:-eth0} TMP_DIR=/tmp/fw-bench mkdir -p $TMP_DIR echo "Firewall benchmark" echo "Rules count: $RULES" echo "Test duration: $TEST_TIME sec" echo "Interface: $IFACE" ######################################## # helpers ######################################## cleanup() { echo "[*] Cleaning firewall rules" iptables -F || true iptables -X || true nft flush ruleset || true } generate_ips() { for i in $(seq 1 $RULES); do echo "10.$((i/255)).$((i%255)).$((RANDOM%255))" done } ######################################## # CPU ######################################## get_cpu_stat() { local cpu=${1:-cpu} awk -v cpu="$cpu" '$1 == cpu {print $2+$3+$4+$5+$6+$7+$8, $5}' /proc/stat } calc_cpu_usage() { local start_total=$1 local start_idle=$2 local end_total=$3 local end_idle=$4 local total_diff=$((end_total - start_total)) local idle_diff=$((end_idle - start_idle)) echo $(( (100 * (total_diff - idle_diff)) / total_diff )) } ######################################## # NET ######################################## get_net_stat() { cat /proc/net/dev | grep "$IFACE" | awk '{print $2, $10}' } ######################################## # SOFTIRQ ######################################## get_softirq() { grep NET_RX /proc/softirqs | awk '{sum=0; for(i=2;i<=NF;i++) sum+=$i; print sum}' } ######################################## # conntrack ######################################## print_conntrack() { echo "Conntrack count: $(cat /proc/sys/net/netfilter/nf_conntrack_count)" echo "Conntrack max: $(cat /proc/sys/net/netfilter/nf_conntrack_max)" } ######################################## # traffic test ######################################## run_iperf() { echo "[*] Starting iperf3 server" taskset -c 0 iperf3 -s -D sleep 2 echo "[*] Warmup..." taskset -c 1 iperf3 -c $SERVER_IP -t 5 > /dev/null echo "[*] Collecting baseline stats" read cpu_start_total cpu_start_idle < <(get_cpu_stat) read rx_start tx_start < <(get_net_stat) softirq_start=$(get_softirq) echo "[*] Running benchmark..." taskset -c 1 iperf3 -c $SERVER_IP -t $TEST_TIME echo "[*] Collecting end stats" read cpu_end_total cpu_end_idle < <(get_cpu_stat) read rx_end tx_end < <(get_net_stat) softirq_end=$(get_softirq) ######################################## # results ######################################## CPU_USAGE=$(calc_cpu_usage $cpu_start_total $cpu_start_idle $cpu_end_total $cpu_end_idle) RX_PPS=$(( (rx_end - rx_start) / TEST_TIME )) TX_PPS=$(( (tx_end - tx_start) / TEST_TIME )) SOFTIRQ_RATE=$(( (softirq_end - softirq_start) / TEST_TIME )) echo "" echo "========== RESULTS ==========" echo "CPU usage: ${CPU_USAGE}%" echo "RX pps: $RX_PPS" echo "TX pps: $TX_PPS" echo "SoftIRQ NET_RX/s: $SOFTIRQ_RATE" print_conntrack echo "" echo "[*] Latency test" ping -c 20 $SERVER_IP | tail -n 1 echo "=============================" pkill iperf3 || true } ######################################## # iptables linear ######################################## iptables_test() { echo "[*] Running iptables linear test" cleanup RULEFILE=$TMP_DIR/iptables.rules { echo "*filter" echo ":INPUT DROP [0:0]" echo ":FORWARD DROP [0:0]" echo ":OUTPUT ACCEPT [0:0]" echo "-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" echo "-A INPUT -p tcp --dport 22 -j ACCEPT" echo "-A INPUT -p tcp --dport 80 -j ACCEPT" echo "-A INPUT -p tcp --dport 443 -j ACCEPT" echo "-A INPUT -m limit --limit 50/sec --limit-burst 100 -j ACCEPT" for ip in $(generate_ips); do echo "-A INPUT -s $ip -j DROP" done echo "COMMIT" } > $RULEFILE START=$(date +%s%N) iptables-restore < $RULEFILE END=$(date +%s%N) echo "iptables load time: $(( (END-START)/1000000 )) ms" run_iperf } ######################################## # nft linear ######################################## nft_linear_test() { echo "[*] Running nftables linear test" cleanup RULEFILE=$TMP_DIR/nft-linear.rules { echo "table inet fw {" echo "chain input {" echo "type filter hook input priority 0;" echo "policy drop;" echo "ct state established,related accept" echo "tcp dport {22,80,443} accept" echo "limit rate 50/second accept" for ip in $(generate_ips); do echo "ip saddr $ip drop" done echo "}" echo "}" } > $RULEFILE START=$(date +%s%N) nft -f $RULEFILE END=$(date +%s%N) echo "nft linear load time: $(( (END-START)/1000000 )) ms" run_iperf } ######################################## # nft set ######################################## nft_set_test() { echo "[*] Running nftables set test" cleanup SETFILE=$TMP_DIR/ipset.txt generate_ips > $SETFILE RULEFILE=$TMP_DIR/nft-set.rules { echo "table inet fw {" echo "set blacklist {" echo "type ipv4_addr;" echo "flags interval;" echo "elements = {" awk '{printf "%s,", $0}' $SETFILE echo "}" echo "}" echo "chain input {" echo "type filter hook input priority 0;" echo "policy drop;" echo "ct state established,related accept" echo "tcp dport {22,80,443} accept" echo "limit rate 50/second accept" echo "ip saddr @blacklist drop" echo "}" echo "}" } > $RULEFILE START=$(date +%s%N) nft -f $RULEFILE END=$(date +%s%N) echo "nft set load time: $(( (END-START)/1000000 )) ms" run_iperf } ######################################## # menu ######################################## case "${1:-}" in iptables) iptables_test ;; nft-linear) nft_linear_test ;; nft-set) nft_set_test ;; *) echo "Usage:" echo "$0 iptables" echo "$0 nft-linear" echo "$0 nft-set" ;; esac
В скрипте можно указать кол-во загружаемых правил, IP адрес сервера и сетевой интерфейс
Как запускать:
iptables
./fw-bench.sh iptables
nftables linear
./fw-bench.sh nft-linear
nftables sets
./fw-bench.sh nft-set
Сам тестовый стенд достаточно типовой: сервер с 4 CPU, 16 ГБ оперативной памяти и сетевой картой на 1 Гбит/с. Сухие прогоны вышли такие:
Кому интересно тут можно глянуть сухие результаты скрипта
Rules count: 10000 Test duration: 20 sec Interface: enp4s1 [*] Running iptables linear test [*] Cleaning firewall rules iptables load time: 34 ms [*] Starting iperf3 server [*] Warmup... [*] Collecting baseline stats [*] Running benchmark... Connecting to host *, port 5201 [ 5] local * port 54346 connected to * port 5201 [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-1.00 sec 4.19 GBytes 35.9 Gbits/sec 0 1.44 MBytes [ 5] 1.00-2.00 sec 4.51 GBytes 38.7 Gbits/sec 0 1.62 MBytes [ 5] 2.00-3.00 sec 4.52 GBytes 38.8 Gbits/sec 0 1.62 MBytes [ 5] 3.00-4.00 sec 4.53 GBytes 38.9 Gbits/sec 0 1.62 MBytes [ 5] 4.00-5.00 sec 4.40 GBytes 37.8 Gbits/sec 0 1.94 MBytes [ 5] 5.00-6.00 sec 4.44 GBytes 38.2 Gbits/sec 0 2.12 MBytes [ 5] 6.00-7.00 sec 4.46 GBytes 38.3 Gbits/sec 0 2.12 MBytes [ 5] 7.00-8.00 sec 4.53 GBytes 38.9 Gbits/sec 0 2.12 MBytes [ 5] 8.00-9.00 sec 4.54 GBytes 39.0 Gbits/sec 0 2.12 MBytes [ 5] 9.00-10.00 sec 4.54 GBytes 39.0 Gbits/sec 0 2.12 MBytes [ 5] 10.00-11.00 sec 4.51 GBytes 38.8 Gbits/sec 0 3.25 MBytes [ 5] 11.00-12.00 sec 4.53 GBytes 39.0 Gbits/sec 0 3.25 MBytes [ 5] 12.00-13.00 sec 4.56 GBytes 39.2 Gbits/sec 0 3.25 MBytes [ 5] 13.00-14.00 sec 4.55 GBytes 39.1 Gbits/sec 0 3.25 MBytes [ 5] 14.00-15.00 sec 4.56 GBytes 39.2 Gbits/sec 0 3.25 MBytes [ 5] 15.00-16.00 sec 4.55 GBytes 39.1 Gbits/sec 0 3.25 MBytes [ 5] 16.00-17.00 sec 4.46 GBytes 38.3 Gbits/sec 0 3.25 MBytes [ 5] 17.00-18.00 sec 4.43 GBytes 38.1 Gbits/sec 0 3.25 MBytes [ 5] 18.00-19.00 sec 4.40 GBytes 37.8 Gbits/sec 0 3.25 MBytes [ 5] 19.00-20.00 sec 4.39 GBytes 37.7 Gbits/sec 0 3.25 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-20.00 sec 89.6 GBytes 38.5 Gbits/sec 0 sender [ 5] 0.00-20.00 sec 89.6 GBytes 38.5 Gbits/sec receiver iperf Done. [*] Collecting end stats ========== RESULTS ========== CPU usage: 39% RX pps: 116034 TX pps: 27045 SoftIRQ NET_RX/s: 164018 Conntrack count: 81 Conntrack max: 262144 [*] Latency test rtt min/avg/max/mdev = 0.047/0.065/0.084/0.008 ms ============================= Rules count: 10000 Test duration: 20 sec Interface: enp4s1 [*] Running nftables linear test [*] Cleaning firewall rules nft linear load time: 142 ms [*] Starting iperf3 server [*] Warmup... [*] Collecting baseline stats [*] Running benchmark... Connecting to host *, port 5201 [ 5] local * port 48978 connected to * port 5201 [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-1.00 sec 4.31 GBytes 37.0 Gbits/sec 0 1.12 MBytes [ 5] 1.00-2.00 sec 4.30 GBytes 36.9 Gbits/sec 0 1.12 MBytes [ 5] 2.00-3.00 sec 4.33 GBytes 37.2 Gbits/sec 0 1.25 MBytes [ 5] 3.00-4.00 sec 4.31 GBytes 37.1 Gbits/sec 0 1.25 MBytes [ 5] 4.00-5.00 sec 4.31 GBytes 37.0 Gbits/sec 0 1.25 MBytes [ 5] 5.00-6.00 sec 4.31 GBytes 37.0 Gbits/sec 0 1.25 MBytes [ 5] 6.00-7.00 sec 4.28 GBytes 36.8 Gbits/sec 0 1.25 MBytes [ 5] 7.00-8.00 sec 4.23 GBytes 36.4 Gbits/sec 0 1.25 MBytes [ 5] 8.00-9.00 sec 4.28 GBytes 36.8 Gbits/sec 0 1.25 MBytes [ 5] 9.00-10.00 sec 4.28 GBytes 36.8 Gbits/sec 0 1.94 MBytes [ 5] 10.00-11.00 sec 4.28 GBytes 36.8 Gbits/sec 0 1.94 MBytes [ 5] 11.00-12.00 sec 4.28 GBytes 36.8 Gbits/sec 0 1.94 MBytes [ 5] 12.00-13.00 sec 4.21 GBytes 36.1 Gbits/sec 0 4.37 MBytes [ 5] 13.00-14.00 sec 4.29 GBytes 36.8 Gbits/sec 0 4.37 MBytes [ 5] 14.00-15.00 sec 4.30 GBytes 36.9 Gbits/sec 0 4.37 MBytes [ 5] 15.00-16.00 sec 4.30 GBytes 36.9 Gbits/sec 0 4.37 MBytes [ 5] 16.00-17.00 sec 4.32 GBytes 37.1 Gbits/sec 0 4.37 MBytes [ 5] 17.00-18.00 sec 4.33 GBytes 37.2 Gbits/sec 0 4.37 MBytes [ 5] 18.00-19.00 sec 4.34 GBytes 37.2 Gbits/sec 0 4.37 MBytes [ 5] 19.00-20.00 sec 4.34 GBytes 37.3 Gbits/sec 0 4.37 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-20.00 sec 85.9 GBytes 36.9 Gbits/sec 0 sender [ 5] 0.00-20.00 sec 85.9 GBytes 36.9 Gbits/sec receiver iperf Done. [*] Collecting end stats ========== RESULTS ========== CPU usage: 39% RX pps: 16219 TX pps: 4847 SoftIRQ NET_RX/s: 157796 Conntrack count: 114 Conntrack max: 262144 [*] Latency test rtt min/avg/max/mdev = 0.047/0.061/0.076/0.006 ms ============================= Rules count: 10000 Test duration: 20 sec Interface: enp4s1 [*] Running nftables set test [*] Cleaning firewall rules nft set load time: 75 ms [*] Starting iperf3 server [*] Warmup... [*] Collecting baseline stats [*] Running benchmark... Connecting to host *, port 5201 [ 5] local * port 39392 connected to * port 5201 [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-1.00 sec 4.44 GBytes 38.1 Gbits/sec 0 1.62 MBytes [ 5] 1.00-2.00 sec 4.43 GBytes 38.1 Gbits/sec 0 3.12 MBytes [ 5] 2.00-3.00 sec 4.30 GBytes 36.9 Gbits/sec 1 3.12 MBytes [ 5] 3.00-4.00 sec 4.33 GBytes 37.2 Gbits/sec 0 3.12 MBytes [ 5] 4.00-5.00 sec 4.41 GBytes 37.9 Gbits/sec 0 3.12 MBytes [ 5] 5.00-6.00 sec 4.44 GBytes 38.1 Gbits/sec 0 3.12 MBytes [ 5] 6.00-7.00 sec 4.42 GBytes 38.0 Gbits/sec 1 3.12 MBytes [ 5] 7.00-8.00 sec 4.45 GBytes 38.2 Gbits/sec 0 3.12 MBytes [ 5] 8.00-9.00 sec 4.43 GBytes 38.0 Gbits/sec 0 3.12 MBytes [ 5] 9.00-10.00 sec 4.42 GBytes 38.0 Gbits/sec 0 3.12 MBytes [ 5] 10.00-11.00 sec 4.42 GBytes 38.0 Gbits/sec 0 3.12 MBytes [ 5] 11.00-12.00 sec 4.42 GBytes 37.9 Gbits/sec 0 3.12 MBytes [ 5] 12.00-13.00 sec 4.43 GBytes 38.1 Gbits/sec 0 3.12 MBytes [ 5] 13.00-14.00 sec 4.43 GBytes 38.1 Gbits/sec 0 3.12 MBytes [ 5] 14.00-15.00 sec 4.43 GBytes 38.0 Gbits/sec 0 3.12 MBytes [ 5] 15.00-16.00 sec 4.43 GBytes 38.1 Gbits/sec 0 3.12 MBytes [ 5] 16.00-17.00 sec 4.41 GBytes 37.9 Gbits/sec 0 3.12 MBytes [ 5] 17.00-18.00 sec 4.42 GBytes 37.9 Gbits/sec 0 3.12 MBytes [ 5] 18.00-19.00 sec 4.42 GBytes 37.9 Gbits/sec 0 3.12 MBytes [ 5] 19.00-20.00 sec 4.45 GBytes 38.3 Gbits/sec 0 3.12 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-20.00 sec 88.3 GBytes 37.9 Gbits/sec 2 sender [ 5] 0.00-20.00 sec 88.3 GBytes 37.9 Gbits/sec receiver iperf Done. [*] Collecting end stats ========== RESULTS ========== CPU usage: 40% RX pps: 138907 TX pps: 134979 SoftIRQ NET_RX/s: 161285 Conntrack count: 139 Conntrack max: 262144 [*] Latency test rtt min/avg/max/mdev = 0.038/0.076/0.112/0.016 ms ============================= Rules count: 40000 Test duration: 20 sec Interface: enp4s1 [*] Running iptables linear test [*] Cleaning firewall rules iptables load time: 115 ms [*] Starting iperf3 server [*] Warmup... [*] Collecting baseline stats [*] Running benchmark... Connecting to host *, port 5201 [ 5] local * port 35038 connected to * port 5201 [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-1.00 sec 4.57 GBytes 39.3 Gbits/sec 0 1.75 MBytes [ 5] 1.00-2.00 sec 4.60 GBytes 39.5 Gbits/sec 0 1.94 MBytes [ 5] 2.00-3.00 sec 4.57 GBytes 39.2 Gbits/sec 0 1.94 MBytes [ 5] 3.00-4.00 sec 4.56 GBytes 39.1 Gbits/sec 0 1.94 MBytes [ 5] 4.00-5.00 sec 4.53 GBytes 38.9 Gbits/sec 0 2.12 MBytes [ 5] 5.00-6.00 sec 4.51 GBytes 38.8 Gbits/sec 0 2.12 MBytes [ 5] 6.00-7.00 sec 4.54 GBytes 39.0 Gbits/sec 0 3.25 MBytes [ 5] 7.00-8.00 sec 4.56 GBytes 39.2 Gbits/sec 0 3.25 MBytes [ 5] 8.00-9.00 sec 4.54 GBytes 39.0 Gbits/sec 0 3.25 MBytes [ 5] 9.00-10.00 sec 4.54 GBytes 39.0 Gbits/sec 0 3.25 MBytes [ 5] 10.00-11.00 sec 4.52 GBytes 38.8 Gbits/sec 0 3.25 MBytes [ 5] 11.00-12.00 sec 4.49 GBytes 38.6 Gbits/sec 0 3.25 MBytes [ 5] 12.00-13.00 sec 4.43 GBytes 38.1 Gbits/sec 0 3.25 MBytes [ 5] 13.00-14.00 sec 4.49 GBytes 38.6 Gbits/sec 0 3.25 MBytes [ 5] 14.00-15.00 sec 4.47 GBytes 38.4 Gbits/sec 0 3.25 MBytes [ 5] 15.00-16.00 sec 4.30 GBytes 36.9 Gbits/sec 0 3.25 MBytes [ 5] 16.00-17.00 sec 4.43 GBytes 38.0 Gbits/sec 0 3.25 MBytes [ 5] 17.00-18.00 sec 4.34 GBytes 37.3 Gbits/sec 2 3.25 MBytes [ 5] 18.00-19.00 sec 4.54 GBytes 39.0 Gbits/sec 0 3.25 MBytes [ 5] 19.00-20.00 sec 4.55 GBytes 39.0 Gbits/sec 0 3.25 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-20.00 sec 90.1 GBytes 38.7 Gbits/sec 2 sender [ 5] 0.00-20.00 sec 90.1 GBytes 38.7 Gbits/sec receiver iperf Done. [*] Collecting end stats ========== RESULTS ========== CPU usage: 40% RX pps: 15457 TX pps: 22775 SoftIRQ NET_RX/s: 162880 Conntrack count: 120 Conntrack max: 262144 [*] Latency test rtt min/avg/max/mdev = 0.047/0.073/0.088/0.010 ms ============================= Rules count: 40000 Test duration: 20 sec Interface: enp4s1 [*] Running nftables linear test [*] Cleaning firewall rules nft linear load time: 619 ms [*] Starting iperf3 server [*] Warmup... [*] Collecting baseline stats [*] Running benchmark... Connecting to host *, port 5201 [ 5] local * port 45710 connected to * port 5201 [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-1.00 sec 4.47 GBytes 38.4 Gbits/sec 0 1.12 MBytes [ 5] 1.00-2.00 sec 4.53 GBytes 38.9 Gbits/sec 0 1.31 MBytes [ 5] 2.00-3.00 sec 4.39 GBytes 37.7 Gbits/sec 0 1.56 MBytes [ 5] 3.00-4.00 sec 4.41 GBytes 37.9 Gbits/sec 0 1.87 MBytes [ 5] 4.00-5.00 sec 4.36 GBytes 37.4 Gbits/sec 0 2.06 MBytes [ 5] 5.00-6.00 sec 4.45 GBytes 38.2 Gbits/sec 0 2.06 MBytes [ 5] 6.00-7.00 sec 4.42 GBytes 38.0 Gbits/sec 0 3.19 MBytes [ 5] 7.00-8.00 sec 4.41 GBytes 37.8 Gbits/sec 0 3.19 MBytes [ 5] 8.00-9.00 sec 4.41 GBytes 37.9 Gbits/sec 0 3.19 MBytes [ 5] 9.00-10.00 sec 4.44 GBytes 38.1 Gbits/sec 0 3.19 MBytes [ 5] 10.00-11.00 sec 4.43 GBytes 38.0 Gbits/sec 0 3.19 MBytes [ 5] 11.00-12.00 sec 4.44 GBytes 38.2 Gbits/sec 0 3.19 MBytes [ 5] 12.00-13.00 sec 4.43 GBytes 38.0 Gbits/sec 0 3.19 MBytes [ 5] 13.00-14.00 sec 4.40 GBytes 37.8 Gbits/sec 0 3.19 MBytes [ 5] 14.00-15.00 sec 4.40 GBytes 37.8 Gbits/sec 0 3.19 MBytes [ 5] 15.00-16.00 sec 4.38 GBytes 37.6 Gbits/sec 0 3.19 MBytes [ 5] 16.00-17.00 sec 4.39 GBytes 37.7 Gbits/sec 0 3.19 MBytes [ 5] 17.00-18.00 sec 4.41 GBytes 37.9 Gbits/sec 0 3.19 MBytes [ 5] 18.00-19.00 sec 4.42 GBytes 38.0 Gbits/sec 0 3.19 MBytes [ 5] 19.00-20.00 sec 4.41 GBytes 37.8 Gbits/sec 0 3.19 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-20.00 sec 88.4 GBytes 38.0 Gbits/sec 0 sender [ 5] 0.00-20.00 sec 88.4 GBytes 38.0 Gbits/sec receiver iperf Done. [*] Collecting end stats ========== RESULTS ========== CPU usage: 39% RX pps: 13776 TX pps: 1583 SoftIRQ NET_RX/s: 162343 Conntrack count: 66 Conntrack max: 262144 [*] Latency test rtt min/avg/max/mdev = 0.033/0.067/0.097/0.015 ms ============================= Rules count: 40000 Test duration: 20 sec Interface: enp4s1 [*] Running nftables set test [*] Cleaning firewall rules nft set load time: 326 ms [*] Starting iperf3 server [*] Warmup... [*] Collecting baseline stats [*] Running benchmark... Connecting to host *, port 5201 [ 5] local * port 57260 connected to * port 5201 [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-1.00 sec 4.54 GBytes 39.0 Gbits/sec 0 1.12 MBytes [ 5] 1.00-2.00 sec 4.53 GBytes 38.9 Gbits/sec 0 1.12 MBytes [ 5] 2.00-3.00 sec 4.53 GBytes 38.9 Gbits/sec 0 1.25 MBytes [ 5] 3.00-4.00 sec 4.54 GBytes 39.0 Gbits/sec 0 1.25 MBytes [ 5] 4.00-5.00 sec 4.43 GBytes 38.0 Gbits/sec 0 3.12 MBytes [ 5] 5.00-6.00 sec 4.50 GBytes 38.7 Gbits/sec 0 3.12 MBytes [ 5] 6.00-7.00 sec 4.46 GBytes 38.3 Gbits/sec 0 3.12 MBytes [ 5] 7.00-8.00 sec 4.44 GBytes 38.2 Gbits/sec 0 3.12 MBytes [ 5] 8.00-9.00 sec 4.49 GBytes 38.6 Gbits/sec 0 3.12 MBytes [ 5] 9.00-10.00 sec 4.54 GBytes 39.0 Gbits/sec 0 3.12 MBytes [ 5] 10.00-11.00 sec 4.48 GBytes 38.5 Gbits/sec 0 3.12 MBytes [ 5] 11.00-12.00 sec 4.51 GBytes 38.8 Gbits/sec 0 3.12 MBytes [ 5] 12.00-13.00 sec 4.48 GBytes 38.5 Gbits/sec 0 3.12 MBytes [ 5] 13.00-14.00 sec 4.50 GBytes 38.7 Gbits/sec 0 3.12 MBytes [ 5] 14.00-15.00 sec 4.51 GBytes 38.8 Gbits/sec 0 3.12 MBytes [ 5] 15.00-16.00 sec 4.47 GBytes 38.4 Gbits/sec 0 3.12 MBytes [ 5] 16.00-17.00 sec 4.48 GBytes 38.5 Gbits/sec 0 3.12 MBytes [ 5] 17.00-18.00 sec 4.50 GBytes 38.6 Gbits/sec 0 3.12 MBytes [ 5] 18.00-19.00 sec 4.51 GBytes 38.7 Gbits/sec 0 3.12 MBytes [ 5] 19.00-20.00 sec 4.48 GBytes 38.5 Gbits/sec 0 3.12 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-20.00 sec 89.9 GBytes 38.6 Gbits/sec 0 sender [ 5] 0.00-20.00 sec 89.9 GBytes 38.6 Gbits/sec receiver iperf Done. [*] Collecting end stats ========== RESULTS ========== CPU usage: 40% RX pps: 53826 TX pps: 49308 SoftIRQ NET_RX/s: 164820 Conntrack count: 104 Conntrack max: 262144 [*] Latency test rtt min/avg/max/mdev = 0.035/0.073/0.108/0.014 ms
Персентиль — это значение, ниже которого лежит заданная доля наблюдений. Например, p50 — медиана, p99 — задержка, которую не превышают 99 % запросов. В отличие от среднего значения, персентили позволяют увидеть поведение системы в хвостах распределения — а именно там обычно и прячутся все сюрпризы в сетевых и распределённых системах.
Важно: в этих прогонах трафик шёл не через loopback, а через основной сетевой интерфейс (
enp4s1)
Если перейти от цифр к интерпретации, картина стала заметно интереснее и, честно говоря, ближе к ожиданиям.
По throughput видно три сценария. iptables стабильно держит ~38.5 Gbit/s на 10k и ~38.7 Gbit/s на 40k правил — практически без деградации. nftables с sets показывает аналогичный результат (~37.9–38.6 Gbit/s) и ведёт себя предсказуемо при росте ruleset’а. А вот nftables в линейном режиме на 10k правил даёт ~36.9 Gbit/s — заметно ниже. При этом загрузка CPU остаётся на уровне ~39–40%, то есть bottleneck всё так же находится в обработке правил, а не в вычислительных ресурсах
Чтобы понять, как именно ведёт себя система во времени, посмотрим на throughput по секундам:

На графике видно, что nftables linear стабильно работает на уровне ~36–37 Gbit/s — это устойчивый режим, без резких провалов. В то же время iptables и nftables с sets держат почти ровную линию около 38 Gbit/s.
Если посмотреть на распределение throughput по времени, это подтверждается персентилями. Для 10k правил iptables показывает p50 ≈ 38.5 Gbit/s и p99 ≈ 39.2 Gbit/s — разброс минимальный, система работает стабильно. nftables linear в этом же сценарии даёт p50 ≈ 36.9 Gbit/s и p99 ≈ 37.3 Gbit/s, то есть это не случайная деградация, а устойчивый уровень. nftables с sets, напротив, практически повторяет iptables: p50 ≈ 38.0 Gbit/s и p99 ≈ 38.3 Gbit/s.
При увеличении числа правил до 40k ситуация немного выравнивается:

nftables linear выходит на ~38.0 Gbit/s. Это хорошо видно и по персентилям: p50 ≈ 38.0 Gbit/s и p99 ≈ 38.9 Gbit/s. Однако появляется чуть больший разброс значений — система достигает той же производительности, но менее стабильно. Это указывает на чувствительность к структуре chains и особенностям прохождения pipeline.
Для сравнения распределений:
iptables — узкое распределение и предсказуемое поведение
nftables linear — более широкий разброс (особенно заметен на 40k)
nftables sets — высокая производительность и стабильность
На этом фоне iptables ведёт себя максимально предсказуемо. Даже при 40k правил p50 ≈ 38.8 Gbit/s и p99 ≈ 39.5 Gbit/s — практически без изменения профиля.
Наиболее сбалансированный результат по-прежнему показывает nftables с sets. Здесь видно не только высокий throughput (~38+ Gbit/s), но и узкое распределение: при 40k правил p50 ≈ 38.6 Gbit/s и p99 ≈ 39.0 Gbit/s. Это означает, что система не просто быстрая, но и предсказуемая под нагрузкой.
Чтобы отдельно посмотреть на хвосты распределения задержек, вынесем p99 latency в виде графика:

Здесь видно, что различия между сценариями остаются минимальными: даже в худших случаях значения укладываются примерно в диапазон 0.07–0.11 ms. То есть даже в худших случаях задержки остаются в пределах ~0.1 ms. Это подтверждает, что firewall в данном сценарии почти не влияет на latency, но существенно влияет на throughput и pps.
Важно подчеркнуть, что это синтетический и довольно упрощённый тестовый сценарий: задействован только один хук и одна политика. Ниже приведу исследования команды Kubernetes, где на kube-proxy была смоделирована более приближённая к реальности нагрузка.
Отдельно отмечу, что я не являюсь специалистом в области нагрузочного тестирования, поэтому часть нюансов могла остаться за кадром — например: профиль трафика, прохождение более тяжелых хуков или особенности генерации нагрузки
Kubernetes тоже движется в сторону nftables
Разработчики Kubernetes опубликовали исследование, в котором сравнивали работу backend-ов kube-proxy на базе iptables, nftables и ipvs. В больших кластерах проблема масштабируемости становится особенно заметной: количество правил в iptables растёт пропорционально числу сервисов и их endpoint-ов. В результате время обработки первого пакета нового соединения увеличивается по мере роста ruleset.

В кластерах с 5000 – 10000 сервисов средняя задержка (p50) nftables была сопоставима с лучшим случаем (p01) для iptables, а в кластере с 30 000 сервисов даже худший случай nftables (p99) оказался быстрее лучшего случая iptables. Однако большинство подобных исследований проводится в контексте service routing.
Поддержка nftables сначала появилась как alpha-функция в версии 1.29, после чего постепенно развивалась. Начиная с Kubernetes 1.33 этот режим стал использоваться по умолчанию.
С архитектурной точки зрения он фактически становится современной заменой как iptables, так и ipvs-режима. В документации Kubernetes прямо отмечается, что nftables обеспечивает более высокую производительность и лучшую масштабируемость при работе с виртуальными адресами IP сервисов. В частности режим ipvs уже переведен в статус deprecated с версии 1.35 и будет исключен из API.
Параллельно меняется и ландшафт сетевых плагинов. Например, Calico уже может работать с nftables-датаплейном. Переключение выполняется через конфигурацию оператора, где параметр linuxDataplane устанавливается в значение Nftables вместо Iptables
kind: Installation metadata: name: default spec: linuxDataplane: Nftables calicoNetwork: ipPools: - name: default-ipv4-ippool blockSize: 26 cidr: 192.168.0.0/16 encapsulation: VXLANCrossSubnet natOutgoing: Enabled nodeSelector: all() --- apiVersion: kubeproxy.config.k8s.io/v1alpha1 kind: KubeProxyConfiguration mode: nftables
На практике это подтверждается и в реальных средах: в одном из небольших рабочих кластеров, я уже выполнил переход на nftables (примерно месяца 3 назад), и деградаций по сетевому взаимодействию не наблюдается. Текущие размеры кластера составляют 333 Pod и 197 Service:
kubectl get pods -A --no-headers | wc -l 333 kubectl get svc -A --no-headers | wc -l 197
В итоге получается довольно показательная картина. Переход, который мы только что рассматриваем на уровне Linux firewall, постепенно происходит и внутри Kubernetes. nftables становится базовым механизмом программирования сетевых правил — сначала на уровне операционной системы, а затем и в инфраструктуре контейнерных платформ.
Заключение
nftables — это не просто новая утилита, а переосмысление подхода к фильтрации трафика в Linux. Он убирает ключевые ограничения iptables: линейный проход по правилам, разрозненные инструменты и неатомарные изменения конфигурации.
На практике выбор выглядит так:
iptables — понятен, предсказуем и до сих пор нормально работает в небольших и стабильных конфигурациях
nftables (linear) — даёт современный API, но без использования sets выигрыша почти нет
nftables (sets/maps) — раскрывает архитектуру полностью: компактные ruleset’ы, стабильная производительность и масштабируемость
Если конфигурация небольшая и редко меняется — iptables всё ещё допустим. Если ruleset растёт, появляется автоматизация или высокая нагрузка — переход на nftables становится практически необходимым.