nftables


Предыдущая | Следующая

Аналог iptables

Нет документации.

В Debian теперь нет iptables. Во всяком случае, по умолчанию.

Узнал я об этом, когда на Debian 11 ввёл команду iptables и получил “command not found”. Сильно удивился и стал читать документацию. Оказалось, теперь нужно использовать nftables.

Хорошие новости: одна утилита nft заменяет четыре прежних — iptables, ip6tables, ebtables и arptables.

Плохие новости: документация (man nft) содержит больше 3 тысяч строк.

Чтобы вам не пришлось всё это читать, я написал небольшое руководство по переходу с iptables на nftables. Точнее, краткое практическое пособие по основам nftables. Без углубления в теорию и сложные места. С примерами.

 

Предисловие (TL;DR)


Для облегчения перехода можно конвертировать правила iptables в nftables с помощью утилит iptables-translate, iptables-restore-translate, iptables-nft-restore и т.п. Утилиты находятся в пакете iptables, который нужно установить дополнительно.

После чего возьмём какую-нибудь команду и пропустим её через iptables-translate. Например, из такой команды:
 

iptables -A INPUT -i eth0 -p tcp --dport 80 -j DROP


получится вот такая:
 

nft add rule ip filter INPUT iifname "eth0" tcp dport 80 counter drop


Казалось бы, всё очень просто, и переход на nftables не доставит никаких проблем. Запускаем преобразованную команду, и … она не работает!!!

А вот почему она не работает — об этом вы узнаете в следующей серии дальше.
 

Первое правило nftables — никаких правил


В nftables нет обязательных предопределённых таблиц, как в iptables. Вы сами создаёте нужные вам таблицы. И называете их, как хотите.

Вероятно, самое заметное отличие nftables от iptables — наличие иерархической структуры: правила группируются в цепочки, цепочки группируются в таблицы. Внешне это всё слегка напоминает JSON. И неудивительно, что экспорт в JSON имеется (команда nft -j list ruleset).

Конечно, в iptables тоже есть таблицы и цепочки, но они не выделяются настолько явно. Посмотрите, как выглядит файл конфигурации nftables:
 

flush ruleset
define wan_if = eth0
define lan_if = eth1
define admin_ip = 203.0.113.15table ip filter {
  set blocked_services {
    type inet_service
    elements = { 22, 23 }
  }
  chain input_wan {
    ip saddr $admin_ip tcp dport ssh accept
    tcp dport @blocked_services drop
  }
  chain input_lan {
    icmp type echo-request limit rate 5/second accept
    ip protocol . th dport vmap { tcp . 22 : accept, udp . 53 : accept, \
                                  tcp . 53 : accept, udp . 67 : accept}
  }
  chain input { 
    type filter hook input priority 0; policy drop;
    ct state vmap { established : accept, related : accept, invalid : drop }
    iifname vmap { lo : accept, $wan_if : jump input_wan, \
                   $lan_if : jump input_lan }
  }
  chain forward { 
    type filter hook forward priority 0; policy drop;
    ct state vmap { established : accept, related : accept, invalid : drop }
    iif $lan_if accept
  }
  chain postrouting { 
    type nat hook postrouting priority srcnat; policy accept;
    masquerade comment "Masquerading rule example"
  }
}


Действующие правила показываются в таком же формате. Чтобы их увидеть, используется команда nft list ruleset. И эта же команда позволяет сохранить правила в файл:
 

# echo "flush ruleset" > /etc/nftables.conf
# nft -s list ruleset >> /etc/nftables.conf


Впоследствии правила можно загрузить:
 

# nft -f /etc/nftables.conf

 

Внимание! Загружаемые из файла правила добавляются к уже работающим, а не заменяют их полностью. Чтобы начать «с чистого листа», первой строкой файла вписывают команду полной очистки (flush ruleset).


Можно также хранить правила в разных файлах, собирая их вместе с помощью include. И как вы заметили — можно использовать define.
 

Синтаксис командной строки


Конечно, вводить многострочные конструкции в командной строке неудобно. Поэтому для управления файерволом используется обычный синтаксис примерно такого вида:
 

nft <команда> <объект> <путь к объекту> <параметры>


Команда — add, insert, delete, replace, rename, list, flush…
Объект — table, chain, rule, set, ruleset…
Путь к объекту зависит от типа. Например, у таблицы это <семейство> <название>.
У правила — гораздо длиннее: <семейство таблицы> <название таблицы> <название цепочки>. А иногда ещё добавляются handle или index (будут описаны дальше).
Параметры зависят от типа объекта. Для правила это условие отбора пакетов и действие, применяемое к отобранным пакетам.

Допустим, нужно заблокировать доступ к ssh и telnet. Для этого используем такую команду:
 

# nft insert rule inet filter input iif eth0 tcp dport { ssh, telnet } drop


Как видите, она состоит из простых, понятных частей:
вставить (insert) правило (rule) в семейство таблиц inet, таблицу filter, цепочку input;
запретить (drop) прохождение пакетов, вошедших через интерфейс (iif) eth0, имеющих тип протокола tcp и направляющихся к сервисам ssh или telnet
 

Примечание: номера портов для сервисов берутся из файла /etc/services


Теперь, чтобы удалить это правило, на него как-то нужно сослаться. Для этого существуют хэндлы (handle), которые можно увидеть, добавив опцию «a» в команду просмотра правил. Кстати, необязательно смотреть все правила (nft -a list ruleset). Можно глянуть только нужную таблицу или цепочку:
 

# nft -a list chain ip filter input


Результат:
 

chain input { # handle 1
  type filter hook input priority 0; policy drop;
  iif "eth0" tcp dport { 22, 23 } drop # handle 4
  ct state vmap { established : accept, related : accept, \ 
                  invalid : drop }  # handle 2
  iifname vmap { lo : accept, $wan_if : jump input_wan, \ 
                 $lan_if : jump input_lan }  # handle 3
}


Соответственно, удаление правила выглядит так:
 

# nft delete rule inet filter input handle 3


Учтите: в каждой таблице своя нумерация хэндлов, не зависящая от других таблиц. Если сейчас добавить ещё одну таблицу — у неё будут свои handle 1, handle 2 и т.д. Благодаря этому, сделанные в какой-либо таблице изменения не влияют на нумерацию в других таблицах.
 

Порядок обработки правил


Если таблицы и цепочки мы добавляем сами — как файервол поймёт, в каком порядке применять правила? Очень просто: он обрабатывает пакеты с учётом семейства таблиц и хуков цепочек. Вот как на этой картинке:

image

Таблицы могут быть одного из 6-ти семейств (families):
ip — для обработки пакетов IPv4
ip6 — IPv6
inet — обрабатывает сразу и IPv4 и IPv6 (чтобы не дублировать одинаковые правила)
arp — пакеты протокола ARP
bridge — пакеты, проходящие через мост
netdev — для обработки «сырых» данных, поступающих из сетевого интерфейса (или передающихся в него)

Цепочки получают на вход пакеты из хуков (цветные прямоугольники на картинке). Для ip/ip6/inet предусмотрены хуки prerouting, input, forward, output и postrouting.

У цепочки есть приоритет. Чем он ниже (может быть отрицательным), тем раньше обрабатывается цепочка. Обратите внимание на хук prerouting в зелёной части картинки — там это видно.

Чтобы не запоминать числа, для указания приоритета можно использовать зарезервированные слова. Самые используемые – dstnat (приоритет = -100), filter (0), srcnat (100).

Теперь рассмотрим основные части nftables подробнее.
 

Таблицы (tables)


Синтаксис:
{add | create} table [familytable [{ flags flags; }]
{delete | list | flush} table [familytable
list tables [family]
delete table [family] handle handle

Поскольку таблиц изначально нет, их нужно создать до того, как создавать цепочки и правила.
Именно поэтому не сработало правило после iptables-translate — для него не нашлось таблицы и цепочки.

По умолчанию (если не указана family) считается, что таблица относится к семейству ip.
У таблицы может быть единственный флаг — dormant, который позволяет временно отключить таблицу (вместе во всем её содержимым):
 

# nft add table filter {flags dormant \;}


Включить обратно:
 

# nft add table filter

 

Примечание: если команда вводится в командной строке — нужно ставить бэкслэш перед точкой с запятой.


 

Цепочки (chains)


Синтаксис:
{add | create} chain [familytable chain [{ type type hook hook [device device] priority priority; [policy policy ;] }]
{delete | list | flush} chain [familytable chain
list chains [family]
delete chain [familytable handle handle
rename chain [familytable chain newname

Цепочки бывают базовые (base) и обычные (regular). Базовая цепочка получает пакеты из хука, с которым она связана. А обычная цепочка — это просто контейнер для группировки правил. Чтобы сработали её правила, нужно выполнить на неё явный переход.

Пример в начале статьи содержит обычные цепочки input_wan и input_lan, а также базовые цепочки input, forward и postrouting.

Для базовой цепочки кроме хука и приоритета нужно указать тип:


Также можно указать policy (действие по умолчанию). Т.е., что делать с пакетами, добравшимися до конца цепочки — drop или accept. Если не указано — подразумевается accept.

Пример добавления цепочки:
 

# nft add chain inet filter output { type filter hook output priority 0 \; 
policy accept \; }


Переход на обычную цепочку может выполняться одной из двух команд — jump или goto. Отличие состоит в поведении после возврата из обычной цепочки. После jump продолжается обработка пакетов по всей цепочке, после goto сразу срабатывает действие по умолчанию.

Пример:
 

table ip filter {
  chain input { # handle 1
    type filter hook input priority 0; policy accept;
    ip saddr 1.1.1.1 ip daddr 2.2.2.2 tcp sport 111 \
                                      tcp dport 222 jump other-chain # handle 3
    ip saddr 1.1.1.1 ip daddr 2.2.2.2 tcp sport 111 \
                                      tcp dport 222 accept # handle 4
  }
  chain other-chain { # handle 2
    counter packets 8 bytes 2020 # handle 5
  }
}


Пакет, для которого в handle 3 сработало условие, пойдёт на обработку в цепочку other-chain, а после возврата из неё — продолжит обрабатываться в правиле handle 4.

Если вместо jump будет использовано goto — после возврата из other-chain сработает действие по умолчанию (в этом примере — policy accept).

Из вызванной цепочки можно выйти досрочно с помощью действия return. При этом вызывающая цепочка продолжит выполняться со следующего правила (аналогично jump). Использование return в базовой цепочке вызывает срабатывание действия по умолчанию.
 

Правила (rules)


Синтаксис:
{add | insert} rule [familytable chain [handle handle | index index] statement… [comment comment]
replace rule [familytable chain handle handle statement … [comment comment]
delete rule [family] table chain handle handle

Правила можно добавлять и вставлять не только по хэндлу, но и по индексу («вставить перед 5-м правилом»). Правило, на которое ссылается index, должно существовать (то есть, в пустую цепочку вставить по индексу не получится).

Правила можно комментировать:
 

# nft insert rule inet filter output index 3 tcp dport 2300-2400 drop 
comment \"Block games ports\"


Заодно здесь показано, как можно использовать интервалы. Для адресов они тоже работают: 192.168.50.15-192.168.50.82. Их также можно применять в множествах, словарях и т.п. (с флагом interval для именованных).
 

Множества (sets)


Синтаксис:
add set [familytable set { type type | typeof expression; [flags flags ;] [timeout timeout ;] [gc-interval gc-interval ;] [elements = { element[, ...] } ;] [size size ;] [policy policy ;] [auto-merge ;] }
{delete | list | flush} set [familytable set
list sets [family]
delete set [familytable handle handle
{add | delete} element [familytable setelement[, ...] }

Множества бывают двух типов — анонимные и именованные. Анонимные — пишутся в фигурных скобках прямо в строке с правилом:
 

# nft add rule filter input ip saddr { 10.0.0.0/8, 192.168.0.0/16 } drop


Такое множество можно изменить, только изменив правило целиком.

А вот именованные множества можно менять независимо от правил:
 

# nft add set inet filter blocked_services { type inet_service \; }
# nft add element inet filter blocked_services { ssh, telnet }
# nft insert rule inet filter input iif eth0 tcp dport @blocked_services drop
# nft delete element inet filter blocked_services { 22 }


Чтобы в правиле сослаться на множество, нужно указать его имя с префиксом "@".

Возможные типы элементов у множеств: ipv4_addr, ipv6_addr, ether_addr, inet_proto, inet_service, mark, ifname.

Элементы можно добавлять сразу при объявлении множества:
 

# nft add set ip filter two_addresses {type ipv4_addr \; flags timeout \; 
elements={192.168.1.1 timeout 10s, 192.168.1.2 timeout 30s} \;}


Также здесь можно увидеть, как указать собственный таймаут для каждого элемента.

Если множество сохранено в файле, для объявления элементов можно использовать define:
 

define CDN_EDGE = {
  192.168.1.1,
  192.168.1.2,
  192.168.1.3,
  10.0.0.0/8
}
define CDN_MONITORS = {
  192.168.1.10,
  192.168.1.20
}
define CDN = {
  $CDN_EDGE,
  $CDN_MONITORS
}
tcp dport { http, https } ip saddr $CDN accept


Флаги во множествах бывают такие: constant, dynamic, interval, timeout. Можно указывать несколько флагов через запятую.

Если указать timeout — элемент будет находиться во множестве заданное время, после чего автоматически удалится.

Флаг dynamic используется, если элементы формируются на основе информации из проходящих пакетов (packet path).

Можете поэкспериментировать и посмотреть, как оно работает. Для этого удобно использовать ICMP и пинговать целевой компьютер с соседней машины. Допустим, возьмём вот такую комбинацию:
 

# nft add chain inet filter ping_chain {type filter hook input priority 0\;}
# nft add set inet filter ping_set { type ipv4_addr\; flags dynamic , timeout\; 
timeout 30s\;}
# nft add rule inet filter ping_chain icmp type echo-request add @ping_set 
{ ip saddr limit rate over 5/minute } drop


Здесь при приходе первого же пакета icmp в множество ping_set будет добавлен элемент, описанный в фигурных скобках. А когда у элемента сработает условие «rate over 5/minute» (превышена скорость 5 пакетов в минуту) — выполнится описанное в правиле действие (drop). В развёрнутом виде это выглядит так:
 

set ping_set { 
  type ipv4_addr
  size 65535
  flags dynamic,timeout
  timeout 30s
  elements = { 192.168.16.1 limit rate over 5/minute timeout 30s expires 25s664ms }
}
chain ping_chain {
  type filter hook input priority filter; policy accept;
  icmp type echo-request add @ping_set { ip saddr limit rate over 5/minute } drop
}


На первый взгляд кажется, что пройдёт 5 пингов, и всё остановится. Затем на 30-й секунде элемент удалится, и опять всё пойдёт по новой. Однако, алгоритм ограничения скорости (здесь используется «token bucket») работает по-другому.

Получается вот такое пингование (смотрите на icmp_seq):
 

PING 192.168.16.201 (192.168.16.201) 56(84) bytes of data.
64 bytes from 192.168.16.201: icmp_seq=1 ttl=64 time=0.568 ms
64 bytes from 192.168.16.201: icmp_seq=2 ttl=64 time=0.328 ms
64 bytes from 192.168.16.201: icmp_seq=3 ttl=64 time=0.367 ms
64 bytes from 192.168.16.201: icmp_seq=4 ttl=64 time=0.456 ms
64 bytes from 192.168.16.201: icmp_seq=5 ttl=64 time=0.319 ms
64 bytes from 192.168.16.201: icmp_seq=13 ttl=64 time=0.369 ms
64 bytes from 192.168.16.201: icmp_seq=25 ttl=64 time=0.339 ms


На 30-й секунде элемент удаляется, и цикл повторяется — 31,32,33,34,35,43,55.

То есть, первые 5 пингов проскакивают без задержки, затем срабатывает ограничение rate over 5/minute и пакеты начинают отбрасываться. Но через 12 секунд (1 минута / 5 = 12с) первый прошедший пакет удалится из виртуальной «корзины с токенами» и освободит место для прохода следующего пакета. И через 12 секунд — ещё один.

Разумеется, блокировка ICMP мало кому интересна. Обычно это используется для защиты ssh. Прямо в документации есть такой пример:
 

# nft add set ip filter blackhole  { type ipv4_addr\; flags dynamic\; timeout 1m\; 
size 65536\; }
# nft add set ip filter flood  { type ipv4_addr\; flags dynamic\; timeout 10s\; 
size 128000\; }
# nft add rule ip filter input meta iifname \"internal\" accept
# nft add rule ip filter input ip saddr @blackhole counter drop
# nft add rule ip filter input tcp flags syn tcp dport ssh \
add @flood { ip saddr limit rate over 10/second } \
add @blackhole { ip saddr } drop


Здесь сделан интересный «финт ушами», который заключается в том, что сделано два множества с разными таймаутами. Первое детектирует превышение скорости пакетов, а действием у него назначено добавление элемента во второе множество, которое и используется непосредственно для блокировки.

Хотя, на мой взгляд, вместо add для blackhole лучше использовать update. Разница в том, что update при каждом вызове перезапускает таймаут элемента. Таким образом, блокировка будет действовать непрерывно, пока первое множество будет детектировать флуд и обновлять таймауты второго множества. А в примере из документации блокировка каждую минуту ненадолго снимается.
 

Словари (maps)


Синтаксис:
add map [familytable map { type type | typeof expression [flags flags ;] [elements = { element[, ...] } ;] [size size ;] [policy policy ;] }
{delete | list | flush} map [familytable map
list maps [family]

Словари похожи на множества, только хранят пары ключ-значение. Бывают анонимными и именованными. Анонимный:
 

# nft add rule ip nat prerouting dnat to tcp dport map { 80: 192.168.1.100, 
443 : 192.168.1.101 }


Именованный:
 

# nft add map nat port_to_ip  { type inet_service: ipv4_addr\; }
# nft add element nat port_to_ip { 80 : 192.168.1.100, 443 : 192.168.1.101 }


Для использования в правилах именованные словари нужно предварять префиксом «@»:
 

# nft add rule ip nat postrouting snat to tcp dport map @port_to_ip


И, разумеется, элементы именованных словарей можно добавлять и удалять.
 

Словари действий (verdict maps)


Это вариант словарей, где в качестве значения используется действие (verdict). Действие может быть таким: accept, drop, queue, continue, return, jump, goto.

Пример правила c анонимным verdict map:
 

# nft add rule inet filter input ip protocol vmap { tcp : jump tcp_chain , 
udp : jump udp_chain , icmp : drop }


Пример с именованным:
 

# nft add map filter my_vmap { type ipv4_addr : verdict \; }
# nft add element filter my_vmap { 192.168.0.10 : drop, 192.168.0.11 : accept }
# nft add rule filter input ip saddr vmap @my_vmap


Обратите внимание: в правиле перед ключевым словом vmap нужно указать, что будет использоваться в качестве ключа (здесь — ip saddr). Этот ключ должен иметь тип значения, совпадающий с указанным в определении словаря (в этом примере — type ipv4_addr). Для ipv4_addr в качестве ключей могут быть ip saddr, ip daddr, arp saddr ip, ct original ip daddr и пр. Все возможные варианты описаны вот здесь.
 

Условия отбора пакетов

 

▍ Конкатенации (сoncatenations)


Позволяют использовать несколько условий одновременно (логическое И):
 

# nft add rule ip filter input ip saddr . ip daddr . ip protocol 
{ 1.1.1.1 . 2.2.2.2 . tcp, 1.1.1.1 . 3.3.3.3 . udp} accept


Правило сработает, если ip saddr == 1.1.1.1 И ip daddr == 2.2.2.2 И ip protocol == tcp

Конкатенации можно применять в словарях:
 

# nft add rule ip nat prerouting dnat to ip saddr . tcp dport map 
{ 1.1.1.1 . 80 : 192.168.1.100, 2.2.2.2 . 443 : 192.168.1.101 }


И в verdict maps:
 

# nft add map filter whitelist { type ipv4_addr . inet_service : verdict \; }
# nft add rule filter input ip saddr . tcp dport vmap @whitelist
# nft add element filter whitelist { 1.2.3.4 . 22 : accept}


 

▍ Payload expressions (отбор пакетов на основе содержимого)


Это те условия, которые отбирают пакеты на основе информации, содержащейся в самих пакетах. Например, порт назначения, адрес источника, тип протокола и т.п.

Условий очень много, поэтому я приведу только их список. Впрочем, во многих случаях их назначение понятно из названия. Если нет — всегда можно посмотреть в документации.

ether {daddr | saddr | type}
vlan {id | dei | pcp | type}
arp {htype | ptype | hlen | plen | operation | saddr { ip | ether } | daddr { ip | ether }

ip {version | hdrlength | dscp | ecn | length | id | frag-off | ttl | protocol | checksum | saddr | daddr }
icmp {type | code | checksum | id | sequence | gateway | mtu}
igmp {type | mrt | checksum | group}

ip6 {version | dscp | ecn | flowlabel | length | nexthdr | hoplimit | saddr | daddr}
icmpv6 {type | code | checksum | parameter-problem | packet-too-big | id | sequence | max-delay}
tcp {sport | dport | sequence | ackseq | doff | reserved | flags | window | checksum | urgptr}

udp {sport | dport | length | checksum}
udplite {sport | dport | checksum}
sctp {sport | dport | vtag | checksum}

sctp chunk CHUNK [ FIELD ]
dccp {sport | dport | type}
ah {nexthdr | hdrlength | reserved | spi | sequence}  # Authentication header
esp {spi | sequence}  # Encrypted security payload header
comp {nexthdr | flags | cpi}  # IPComp header
 

▍ RAW payload expression (отбор на основе «сырых» данных)


Это условие, которое выбирает из пакета указанное количество бит, начиная с заданного смещения. Бывает полезным, если нужно сопоставить данные, для которых ещё нет готового шаблона. Т.к. у пакетов разных протоколов по заданному смещению находятся разные данные, сначала нужно отобрать подходящие пакеты (в примере ниже — с помощью meta l4proto).

Синтаксис выглядит так:
 

@base,offset,length


Например, выберем пакеты протоколов TCP и UDP, идущие на заданные порты:
 

# nft add rule filter input meta l4proto {tcp, udp} @th,16,16 { 53, 80 }


впрочем, для этих протоколов есть готовые шаблоны, так что писать можно проще:
 

# nft add rule filter input meta l4proto { tcp, udp } th dport { 53, 80 }  accept


Поскольку TCP и UDP – протоколы транспортного уровня, в качестве base здесь используется заголовок транспортного уровня (transport header => th).

Для протоколов сетевого уровня (например, IPv4 и IPv6) используются заголовки сетевого уровня (network header => nh).

А Ethernet, PPP и PPPoE – это канальный уровень. Для них применяется ll (т.к. link layer).
 

▍ Метаусловия (meta expression)


Метаусловия позволяют фильтровать пакеты на основе метаданных. То есть, на основе таких данных, которые не содержатся в самом пакете, но каким-либо образом с ним связаны — порт, через который вошёл пакет; номер процессора, обрабатывающего пакет; UID исходного сокета и прочее. Метаусловия бывают двух типов. У одних ключевое слово meta обязательно, у других — нет:
 

meta {length | nfproto | l4proto | protocol | priority}[meta] {mark | iif | iifname | iiftype | oif | oifname | oiftype | skuid | skgid |
 nftrace | rtclassid | ibrname | obrname | pkttype | cpu | iifgroup | oifgroup | 
cgroup | random | ipsec | iifkind | oifkind | time | hour | day }


 

▍ Conntrack (connection tracking system)


Система conntrack хранит множество метаданных, по которым можно отбирать пакеты. Соответствующее условие выглядит таким образом:
 

ct { l3proto | protocol | expiration | state | original saddr | original daddr | 
original proto-src | original proto-dst | reply saddr | reply daddr | 
reply proto-src | reply proto-dst | status | mark | id }


Вероятно, наиболее используемое условие при работе с conntrack — ct state. Которое может иметь значения new, established, related, invalid, untracked.

Остальные возможности conntrack используются гораздо реже. Даже не буду их описывать. А вот несколько примеров c conntrack лишними не будут.

Разрешить не более 10 соединений с портом tcp/22 (ssh):
 

table inet connlimit_demo {
  chain ssh_in { 
    type filter hook input priority filter; policy drop;
    tcp dport 22 ct count 10 accept
  }
}



Счётчик открытых соединений HTTPS:
 

table inet filter {
  set https {
    type ipv4_addr;
    flags dynamic;
    size 65536;
    timeout 60m;
  }
  chain input {
    type filter hook input priority filter;
    ct state new tcp dport 443 update @https { ip saddr counter }
  }
}



Следующее правило разрешает только 20 соединений с каждого адреса. Для каждого адреса IPv4 во множестве my_connlimit будет создан элемент со счётчиком. Когда счётчик достигнет нуля — элемент удалится, поэтому флаг timeout здесь не нужен.
 

table ip my_filter_table {
  set my_connlimit {
    type ipv4_addr
    size 65535
    flags dynamic
  }
  chain my_output_chain {
    type filter hook output priority filter; policy accept;
    ct state new add @my_connlimit { ip daddr ct count over 20 } counter
  }
}


При описании множеств уже был пример, как ограничивать скорость пакетов. Это можно делать и с помощью conntrack:
 

# nft add rule my_table my_chain tcp dport 22 ct state new 
add @my_set { ip saddr limit rate 10/second } accept


Пустить пакеты в обход conntrack:
 

# nft add table my_table
# nft add chain my_table prerouting { type filter hook prerouting 
priority -300 \; }
# nft add rule my_table prerouting tcp dport { 80, 443 } notrack


 

Учёт и ограничения


 

▍ Квоты (quotas)


Считают проходящий трафик и срабатывают, когда достигнуто (over) или не достигнуто (until) указанное значение. Пример анонимной квоты:
 

table inet anon_quota_demo {
  chain IN {
    type filter hook input priority filter; policy drop;
    udp dport 5060 quota until 100 mbytes accept
  }
}


В этом примере на UDP порт 5060 можно будет передать только 100 МБ данных

Пример именованных квот:
 

table inet quota_demo {
  quota q_until_sip { until 100 mbytes }
  quota q_over_http { over  500 mbytes }  chain IN { 
    type filter hook input priority filter; policy drop;
    udp dport 5060 quota name "q_until_sip" accept
    tcp dport 80 quota name "q_over_http" drop
    tcp dport { 80, 443 } accept
  }
}


Здесь на порт SIP (udp/5060) пройдёт не больше 100 МБ, на http — не больше 500, на https — без ограничений, всё остальное блокируется. Обратите внимание на два варианта использования квот — until + accept и over + drop.

Именованные квоты (в отличие от анонимных) можно сбрасывать:
 

# nft reset quota inet quota_demo q_until_sip


Или все квоты файервола:
 

# nft reset quotas


 

▍ Лимиты (limits)


Используются для ограничения скорости в пакетах или байтах за единицу времени.

Пример:
 

table inet limit_demo {
  limit lim_400ppm { rate 400/minute }
  limit lim_1kbps  { rate over 1024 bytes/second burst 512 bytes }
  chain IN { 
    type filter hook input priority filter; policy drop;
    meta l4proto icmp limit name "lim_400ppm" accept
    tcp dport 25 limit name "lim_1kbps" accept
  }
}


Здесь для ICMP установлен лимит 400 пакетов в минуту, для SMTP (TCP порт 25) — 1 кбайт/с.
При этом первые 512 байт на SMTP проскакивают без ограничения скорости (burst).
Весь остальной трафик блокируется политикой по умолчанию.

Можно уместить ограничение в одном правиле:
 

# nft add rule filter input icmp type echo-request limit rate over 10/second drop


Здесь отбрасываются пакеты, которые не влезают в лимит 10 пакетов в секунду.

Аналогично и с объёмом трафика:
 

# nft add rule filter input limit rate over 10 mbytes/second drop


Если не использовать over – правила применятся к тем пакетам, которые влезают в ограничение. Например:
 

# nft add rule filter input limit rate 10 mbytes/second accept


В этом правиле будет принят трафик, влезающий в 10 МБ/с. Всё, что превысит этот лимит – пойдёт на обработку в следующие правила или в политику по умолчанию.

Разумеется, burst здесь тоже возможен:
 

# nft add rule filter input limit rate 10 mbytes/second burst 9000 kbytes accept


Используя хук ingress в семействе netdev можно ограничить трафик на самом входе в систему. Например, уменьшим поступление широковещательного трафика:
 

# nft add rule netdev filter ingress pkttype broadcast limit rate 
over 10/second drop


 

▍ Счётчики (counters)


Счётчики учитывают одновременно количество пакетов и байт. Анонимный счётчик:
 

# nft insert rule inet filter input ip protocol tcp counter


Посмотреть результаты можно с помощью list:
 

# nft list chain inet filter input


Результат:
 

table inet filter {
  chain input {
    type filter hook input priority filter; policy accept;
    ip protocol tcp counter packets 331 bytes 21560
    …


Такие счётчики можно просто добавлять к любому правилу с помощью слова counter:
 

# nft add rule inet filter input tcp dport 22 counter accept


Именованные счётчики:
 

table inet named_counter_demo {
  counter cnt_http {
  }
  counter cnt_smtp {
  }
  chain IN {
    type filter hook input priority filter; policy drop;
    tcp dport   25 counter name cnt_smtp
    tcp dport   80 counter name cnt_http
    tcp dport  443 counter name cnt_http
  }
}


Посмотреть результаты по всему файерволу, таблице или одному правилу:
 

# nft list counters
# nft list counters table inet named_counter_demo
# nft list counter inet named_counter_demo cnt_http


Сбросить счётчики – такой же синтаксис, только вместо list – reset.
 

Разная мелочёвка, примеры


 

▍ Маскарадинг (Masquerading)


 

# echo "1" >/proc/sys/net/ipv4/ip_forward
# nft add table ip nat
# nft add chain ip nat postrouting { type nat hook postrouting priority 100 \; }
# nft add rule nat postrouting masquerade


В развёрнутом виде:
 

table ip nat {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    masquerade
  }
}


 

▍ Source NAT, Destination NAT


 

# nft add table nat
# nft add chain nat postrouting { type nat hook postrouting priority snat \; }
# nft add rule nat postrouting ip saddr 192.168.1.0/24 oif eth0 snat to 1.2.3.4


Это правило направит трафик с сети 192.168.1.0/24 на интерфейс eth0. Выходящие с интерфейса пакеты получат исходящий адрес 1.2.3.4
 

# nft add table nat
# nft add chain nat prerouting { type nat hook prerouting priority dnat \; }
# nft add rule nat prerouting iif eth0 tcp dport { 80, 443 } dnat to 192.168.1.120


Это правило перенаправит входящий трафик для портов 80 и 443 на хост 192.168.1.120
 

▍ Редирект (redirect)


Перенаправление входящего трафика на другой порт этого же хоста
 

# nft add table nat
# nft add chain nat prerouting { type nat hook prerouting priority dstnat \; }
# nft add rule nat prerouting tcp dport 80 redirect to 8080


Исходящий трафик также можно редиректить:
 

# nft add rule nat output tcp dport 53 redirect to 10053


 

▍ Логгирование


Пишет информацию о пакетах в системный лог (/var/log/syslog). Примеры:
 

# nft add rule inet filter input tcp dport 22 ct state new \
log flags all prefix \"New SSH connection: \" accept
# nft add rule inet filter input meta pkttype broadcast \
log prefix \"Broadcast \"
# nft add rule inet filter input ether daddr 01:00:0c:cc:cc:cc \
log level info prefix \"Cisco Discovery Protocol \"


 

▍ Балансировка нагрузки (load balancing)


Обычный round-robin (равномерное распределение):
 

# nft add rule nat prerouting dnat to numgen inc mod 2 map { \
               0 : 192.168.10.100, \
               1 : 192.168.20.200 }


Распределение с разными весами:
 

# nft add rule nat prerouting dnat to numgen inc mod 10 map { \
               0-7 : 192.168.10.100, \
               8-9 : 192.168.20.200 }


Переход на цепочку, со случайным распределением и разными весами:
 

# nft add rule nat prerouting numgen random mod 100 vmap \
{ 0-69 : jump chain1, 70-99 : jump chain2 }


 

Итого


На мой взгляд, nftables получился удобнее, чем iptables. У него простой понятный синтаксис без многочисленных опций с дефисами. Иерархическая структура конфига. Свобода в формировании таблиц и цепочек.

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