В 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.15
table 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 и т.д. Благодаря этому, сделанные в какой-либо таблице изменения не влияют на нумерацию в других таблицах.
Порядок обработки правил
Если таблицы и цепочки мы добавляем сами — как файервол поймёт, в каком порядке применять правила? Очень просто: он обрабатывает пакеты с учётом семейства таблиц и хуков цепочек. Вот как на этой картинке:
Таблицы могут быть одного из 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 [family] table [{ flags flags; }]
{delete | list | flush} table [family] table
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 [family] table chain [{ type type hook hook [device device] priority priority; [policy policy ;] }]
{delete | list | flush} chain [family] table chain
list chains [family]
delete chain [family] table handle handle
rename chain [family] table chain newname
Цепочки бывают базовые (base) и обычные (regular). Базовая цепочка получает пакеты из хука, с которым она связана. А обычная цепочка — это просто контейнер для группировки правил. Чтобы сработали её правила, нужно выполнить на неё явный переход.
Пример в начале статьи содержит обычные цепочки input_wan и input_lan, а также базовые цепочки input, forward и postrouting.
Для базовой цепочки кроме хука и приоритета нужно указать тип:
- filter — стандартный тип, может применяться в любом семействе для любого хука
- nat — используется для NAT. В цепочке обрабатывается только первый пакет соединения, все остальные отправляются «по натоптанной дорожке» через conntrack
- route — применяется в хуке output для маркировки пакетов
Также можно указать 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 [family] table chain [handle handle | index index] statement… [comment comment]
replace rule [family] table 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 [family] table 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 [family] table set
list sets [family]
delete set [family] table handle handle
{add | delete} element [family] table set{ element[, ...] }
Множества бывают двух типов — анонимные и именованные. Анонимные — пишутся в фигурных скобках прямо в строке с правилом:
# 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 [family] table map { type type | typeof expression [flags flags ;] [elements = { element[, ...] } ;] [size size ;] [policy policy ;] }
{delete | list | flush} map [family] table 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. Это, скорее, шпаргалка по мотивам документации. Для более подробного изучения темы можно почитать следующие материалы:
Автор: Олег Сухонос