В 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. Это, скорее, шпаргалка по мотивам документации. Для более подробного изучения темы можно почитать следующие материалы:
Комментарии (44)
Sap_ru
04.10.2021 13:51+2А как с этим работет докер и его костыли?
Mnemonik
04.10.2021 14:41+1извиняюсь что повторяюсь, но с iptables-nft докер прекрасно и без проблем работает (и kubernetes, вернее flannel и calico тоже).
fail2ban работает, да я думаю вообще всё работает. я перечислил только то, что я точно пробовал.
iptables-nft консольная утилита полностью повторяющая синтаксис iptables, но работающая с интерфейсом nft в ядре. то есть ей можно колбасить привычные правила и делать всё как обычно, под капотом это всё будет nftables (и даже отображаться в виде длиннющих правил если использовать утилиту nft)
а так в целом есть какие-то проекты типа использования nft вместо iptables для докера - но они в настолько зачаточном состоянии уже годы, что не стоит надеяться что ими можно будет пользоваться. мне кажется все кто хочет просто ставят iptables-nft и пользуются (я реально пытался запустить докер с поддержкой nft, и это где-то в 2020-м было даже хуже чем концепт)
Sap_ru
04.10.2021 14:49-2Вот тут-то и проблема. Правила iptables-nft не видны через nftables и наоборот. В результате, та дичь, которую докер динамически добавляет через iptables-nft, не может нормально интрегрироваться в системе, где используется iptables-nft, и приходится всю систему переводить на "старый добрый" iptables в лице ipatables-nft.
Т.е. если у вас в системе есть несколько изолированных виртуальный сетевых интерфейсов, VPN'ы с различными правилами доступа к интерфейсам и серисам и т.п. Т.е. как раз тот случай, когда требуется сложное и гибкое конфигурирование фильтров и хочется использовать NFT, то наличие докера и его производных с их ублюдочными и кривым автоконфигурированием сетевых интерфейсов через iptables ломает абсолютно и всё.
Mnemonik
04.10.2021 15:23+4звучит странно.
и nft и iptables-nft это интерфейсы к nftables в ядре. они обе редактируют одни и те же правила и очевидно взаимозаменяемы.
iptables -L -n -v
в моей системе показывает тоже самое что и
nft list table ip filter
я вообще в своём ядре забанил модули которые отвечали за "классический" iptables, и старый iptables невозможно использовать в принципе, ни через cli ни через api. и всё нормально работает.
Sap_ru
04.10.2021 15:35А докер работает? Свои правила нормально добавляет/удаляет?
Mnemonik
04.10.2021 15:54+3конечно.
вот кусок из iptables -L -n -v
Chain DOCKER (3 references) pkts bytes target prot opt in out source destination 8193 452K ACCEPT tcp -- !docker_gwbridge docker_gwbridge 0.0.0.0/0 172.18.0.9 tcp dpt:443 18438 891K ACCEPT tcp -- !docker_gwbridge docker_gwbridge 0.0.0.0/0 172.18.0.9 tcp dpt:80 15691 854K ACCEPT tcp -- !docker_gwbridge docker_gwbridge 0.0.0.0/0 172.18.0.13 tcp dpt:51413 5327K 672M ACCEPT udp -- !docker_gwbridge docker_gwbridge 0.0.0.0/0 172.18.0.13 udp dpt:51413 Chain DOCKER-ISOLATION-STAGE-1 (1 references) pkts bytes target prot opt in out source destination 6762 457K DOCKER-ISOLATION-STAGE-2 all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0 0 0 DOCKER-ISOLATION-STAGE-2 all -- br-pvythdd8ctos !br-pvythdd8ctos 0.0.0.0/0 0.0.0.0/0 19M 27G DOCKER-ISOLATION-STAGE-2 all -- docker_gwbridge !docker_gwbridge 0.0.0.0/0 0.0.0.0/0 40M 36G RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 Chain DOCKER-USER (1 references) pkts bytes target prot opt in out source destination 172M 133G RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 Chain DOCKER-ISOLATION-STAGE-2 (3 references) pkts bytes target prot opt in out source destination 2 386 DROP all -- * docker0 0.0.0.0/0 0.0.0.0/0 3 937 DROP all -- * br-pvythdd8ctos 0.0.0.0/0 0.0.0.0/0 0 0 DROP all -- * docker_gwbridge 0.0.0.0/0 0.0.0.0/0 19M 27G RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
вот кусок из nft list table ip filter
chain DOCKER { iifname != "docker_gwbridge" oifname "docker_gwbridge" meta l4proto tcp ip daddr 172.18.0.9 tcp dport 443 counter packets 8193 bytes 452260 accept iifname != "docker_gwbridge" oifname "docker_gwbridge" meta l4proto tcp ip daddr 172.18.0.9 tcp dport 80 counter packets 18441 bytes 891064 accept iifname != "docker_gwbridge" oifname "docker_gwbridge" meta l4proto tcp ip daddr 172.18.0.13 tcp dport 51413 counter packets 15691 bytes 854236 accept iifname != "docker_gwbridge" oifname "docker_gwbridge" meta l4proto udp ip daddr 172.18.0.13 udp dport 51413 counter packets 5327475 bytes 672496088 accept } chain DOCKER-ISOLATION-STAGE-1 { iifname "docker0" oifname != "docker0" counter packets 6762 bytes 456593 jump DOCKER-ISOLATION-STAGE-2 iifname "br-pvythdd8ctos" oifname != "br-pvythdd8ctos" counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-2 iifname "docker_gwbridge" oifname != "docker_gwbridge" counter packets 19095322 bytes 26769977257 jump DOCKER-ISOLATION-STAGE-2 counter packets 40298535 bytes 36112505838 return } chain DOCKER-USER { counter packets 171779895 bytes 133264417545 return } chain DOCKER-ISOLATION-STAGE-2 { oifname "docker0" counter packets 2 bytes 386 drop oifname "br-pvythdd8ctos" counter packets 3 bytes 937 drop oifname "docker_gwbridge" counter packets 0 bytes 0 drop counter packets 19102079 bytes 26770432527 return }
можно заметить что даже счётчики пакетов примерно совпадают (всё же там была секунда паузы между первой и второй командой). то есть это не дубликаты, это одни и те же правила в ядре, просто выведенные с помощью двух разных утилит
Grommy
05.10.2021 08:48Можно попросить подсказать, как в ядре банили модули? Просто пересобрали, или? Перехожу как раз на nft и хочется не оставлять лазеек для пути назад :)
Mnemonik
05.10.2021 12:42+1Да, я пересобирал на сервере где делал изначальные тестовые прогоны. Ну там вообще не только для этого ядро кастомное, так что было ок. Потом убедился что достаточно поменять iptables c iptables-legacy на iptables-nft через /etc/alternatives, и старые модули не подгружаются со временем от использования любого софта который у нас в продакшене, и дальше на серверах которые уже ставились с nft не парился. Я думаю можно в /etc/modprobe.d/blacklist.conf перечислить основные модули iptables-legacy:
bpfilter
iptable_filter
iptable_nat
iptable_mangle
и этого будет достаточно чтобы те кто попытаются дёрнуть за старый интерфейс обломились.
suricat404
04.10.2021 17:37+2Довольно похоже на pf.
A_J
05.10.2021 17:44Да... Много лет потребовалось, чтобы сделать что-то похожее на PF. Но вот с читабельностью так и не получилось.
arheops
04.10.2021 19:47-1Так вроде бы в Debian работает firewalld и дальше сам разбирается, не?
А так всегда возникает вопрос «зачем?»Tangeman
04.10.2021 23:27+4firewalld достаточен для простых вещей, типа открыть порт для чего-то, но для ряда случаев этого очень мало и приходится копать глубже.
vsb
05.10.2021 10:08В firewalld есть iptables-подобный язык для более сложных правил. Его должно хватать для большинства ситуаций.
selivanov_pavel
06.10.2021 20:34А зачем, если есть настоящий iptables/nftables, в который эти правила всё равно транслируются?
vsb
07.10.2021 09:21Если у вас все правила "сложные", то незачем. А если у вас большинство задач типовые, но возникла необходимость что-то сложное сделать, то разумней использовать firewalld, с которым простые задачи решаются просто. Ну на мой взгляд так.
Germanjon
05.10.2021 07:30+5Тот неловкий момент, когда администраторы Facebook почитали данный мануал и пошли применять полученные знания на практике
1A1A1
05.10.2021 08:54+7Это точно для людей? Для человека удобнее именно табличный вид, а не парсить json Или у программеров это уже на уровне лимбической системы парстся?
morijndael
05.10.2021 10:07+1А что сложного в чтении JSON? Вот таблички, у них внутри цепочки, условия проверяются сверху вниз
По сути базовые правила nft 1-в-1 как iptables, только по человечески сгруппированные и без мусорных дефисов
1A1A1
05.10.2021 10:46+8Вы сильно лукавите, что это просто. Это возможно, но отнюдь не просто, иначе зачем нужны бы были все эти плагины и оболочки над json? Элементарно, сравнить две строчки проще чем два "облака". Ну и странно пенять на мусорные дефисы при обилии скобок.
vanishacmp
10.10.2021 21:58+1Особенно радует впихивание ; в командной строке с последующим его экранированием \;
scruff
05.10.2021 11:44-1Вообще это не дело - иметь такой зоопарк файрволлов. ip,nfttables, pf, firewalld, возможно что-то еще. Притом каждый файрволл абсолютно несовместим по синтаксу с другим. Из всех перечисленных, более менее по user-friendly (но ущербнее по функционалу) это firewalld, но эта зараза напрочь ломает правила iptables - в итоге ни тот ни другой вместе нормально не пашут. Поэтому логичнее всего надо было оставить только один - iptables. Давеча был сильно разочарован в iptables на CentOS - оказалось там нет такой элементарной возможности как сохранить правила после перезагрузки. Карл, 21 век! WTF? И мне нужно извращаться в target это прописывать, где собственно ничерта и не заработало. Благо хоть в крон получилось засунуть что-то вроде /sbin/iptables-restore < somerules и отрабатывать это каждые 5 минут. Костыль, но хоть как-то пашет. Притом что в дебиане и убунте есть штатная утилита сохранения правил без всяких таргетов, скриптов и кронов. Мало того что есть куча разношёрстных файрволов. Так еще один и тот-же файрвол на разных ОС - тоже отличается. Бардак!
ssgu85
05.10.2021 12:18В iptables на CentOS всё так-же работает
service iptables save
, оно-же/usr/libexec/iptables/iptables.init save
. Правда нужно доставить пакетiptables-services
. После этого появляется/etc/sysconfig/iptables-config
cIPTABLES_SAVE_ON_STOP
,IPTABLES_SAVE_ON_RESTART
.scruff
05.10.2021 13:05-1До боли знакомый ключик - не пашет, точно могу сказать. Возможно трабл в том, что я ставил openvpn из готового скрипта, а как он там настроен "из коробки" только писателям того скрипта известно. В любом случае такая мелочь как
iptables-persistent
в любом linux-дистре должен быть в коробке, а это утилитки охх как не хватает мне в моей конфе.
vesper-bot
05.10.2021 17:59В смысле напрочь ломает правила iptables? Если вы о тех, которые сами в обход firewalld настраивали — то да, хотя любой файрволл считает, что он тут один командует, и под себя создает все цепочки фильтров и тому подобные структуры уровня ядра. А так у него есть firewalld add rule с функционалом практически эквивалентным iptables, но в рамках firewalld'овских цепочек, т.е. пихается сильно ниже по ветвлению, чем правило iptables -A INPUT.
Zhurikello
05.10.2021 16:34-1Ужас, то ли дело бсдёвый ipfw. Оно может хорошо и функционально на каких то защитных шлюзах, но на обычных серверах, тем более на рабочем десктопе, ну нафик.
Ziptar
06.10.2021 06:54+1Ну вот и зачем ломать то, что и так прекрасно работает? Иптейблс прекрасен, прост и понятен, а это треш какой-то
selivanov_pavel
06.10.2021 20:36+2iptables, arptables, ebtables, ipset - делались каждый по отдельности. А тут собрали всё вместе, порефакторили и причесали.
Мне тоже лень переучиваться, но надо.
Ryav
13.10.2021 05:28А я ни то, ни другое не учил, как теперь выбирать? :)
vesper-bot
13.10.2021 09:22+2Ничего не знаешь — учи наиболее популярное из того, что не deprecated. Здесь nftables.
selivanov_pavel
13.10.2021 13:55Учитывая, что nftables ещё долго и много где будут использоваться через эмуляцию iptables - пока что придётся выбирать оба.
Mnemonik
Хорошая инструкция, тоже заморочался два года назад, когда вышал убунта 18.04, где можно было использовать nft вместо iptables.
Два года парился, ломал пальцы и мозги (всё-таки вся эта борода ближе к тому как правила использует компьютер, чем человек), пока в итоге не дочитался до того, что nftables и iptables это проект одной и той же команды и конечно же они сделали iptables-nft, который имеет тот же самый один в один синтаксис что и iptables, но использует kernel интерфейс nft. то есть просто привычная утилита которая не враппер над nftables, а просто имплементирует работу с kernel nft другим синтаксисом (всем привычным).
iptables-nft являются частью iptables, и допустим в убунте их можно просто включить с помощью alternatives.
посмотреть что используется можно сделав iptables -v, например это выглядит так:
root@c2:~# iptables -v
iptables v1.8.4 (nf_tables): no command specified
и это значит что используется iptables-nft
Я реально два года пользовался nft но так и не смог к нему привыкнуть (а я сторонник всего нового) и через два года вернулся на iptables синтаксис, но безусловно пользуясь nft (потому что это крутой новый интефейс для фильтрации пакетов)
P6i
А как iptables-nft реализует сеты, словари, которых нет в классическом iptables?