Если администрировать 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

Если обозначить:

  • N — количество правил в цепочке

  • T_m ​ — среднее время проверки одного правила

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

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

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

где P — число пакетов в секунду. При P=10^6 и N = 1000

Фактически ядро выполняет миллиард сравнений правил в секунду. Даже если каждая проверка занимает доли микросекунды, суммарная нагрузка начинает заметно влиять на CPU.

Подход nftables: множества и ассоциативные структуры

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

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

где:

  • Trule — проверка самого правила

  • Tlookup​ — поиск элемента в наборе

Важно, что время поиска практически не зависит от размера набора.

Интервальные множества

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

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

То есть вместо десятков тысяч проверок выполняется примерно 16 операций поиска.

Если обобщить различие между подходами:

Механизм

Алгоритмическая сложность

iptables

O(N)

nftables sets

O(1)

nftables interval sets

O(logN)

Основные сущности nftables: tables, chains, rules, sets

table — это верхний уровень логической группировки правил. Внутри таблицы хранятся цепочки, а сами таблицы обычно создаются для определённого семейства протоколов. Семейство определяет, с каким типом трафика будет работать ruleset и на каком уровне сетевого стека он применяется.

На практике используются несколько основных вариантов:

  • ip — только IPv4-трафик

  • ip6 — только IPv6

  • inet — объединённое семейство для IPv4 и IPv6

  • bridge — фильтрация на уровне 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, где пакет последовательно проходит через всю цепочку O(n)

  • nftables с линейными правилами — функционально аналогичная конфигурация, но через современный API

  • nftables с использованием sets — более эффективный подход, где проверка IP-адресов выполняется через структуры данных с близкой к O(1) сложностью

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 становится практически необходимым.

Комментарии (16)