Skip to content

Latest commit

 

History

History
1519 lines (1057 loc) · 132 KB

nftables.md

File metadata and controls

1519 lines (1057 loc) · 132 KB

nftables

Источник: nftables

nftables — компонент подсистемы Linux Netfilter, предоставляющий самый современный каркас для определения функциональности классификации и фильтрации пакетов. Как таковой, он — преемник компонента ядра xtables и связанных с ним пользовательских утилит iptables / ip6tables / arptables / ebtables.

Эта документация использует термин nftables, когда речь идёт о компоненте ядра Linux, и nft, когда речь идёт о пользовательской утилите. Обратите внимание: пакет Ubuntu, предоставляющий пользовательскую утилиту, тоже называется nftables, и эта документация будет называть его «пакет nftables Ubuntu».

Эта страница постепенно строит пример конфигурационного файла nftables, подходящего для Linux-хоста, который предоставляет сетевые сервисы (такие как SSH и HTTP) по IPv4 и IPv6 в локальную сеть (LAN).

Преимущества

Использование nftables даёт несколько преимуществ по сравнению со старыми альтернативами:

  • Выражения, формирующие правила классификации пакетов, компилируются в пользовательском пространстве в байткод и исполняются ядром на специально созданной виртуальной машине; это даёт куда большую гибкость.

  • Высокая производительность достигается с помощью maps и конкатенаций: вместо линейной обработки правил (O(n)) можно добиться постоянного времени (O(1)).

  • Синтаксис пользовательской утилиты nft декларативный, а не процедурный, как требовалось для ip/ip6/arp/ebtables, что упрощает управление конфигурацией файрвола.

  • Таблицы и цепочки не предопределены, и структура позволяет регистрировать их произвольное количество: это облегчает независимое управление правилами несколькими приложениями.

  • Переадресацию пакетов можно ускорить с помощью функциональности flowtables, которая также интегрируется с отдельным оборудованием.

  • Можно определять общие правила для IPv4 и IPv6, в отличие от старых iptables и ip6tables.

  • Трассировку вычисления правил для конкретных пакетов можно легко включить.

Совместимость

В целом, большинство правил, которые можно определить с помощью iptables, ip6tables, arptables и ebtables, можно определить и с помощью nftables, но не наоборот. Используйте только один из двух подходов для управления правилами файрвола. Иначе взаимодействие между разными правилами или службами, которые их устанавливают, может быть непредсказуемым и привести либо к небезопасным конфигурациям, либо к блокировке трафика, который должен быть разрешён. Более того, некоторые приложения — например, системы оркестрации контейнеров или VPN-утилиты — могут настраивать правила файрвола сами, что приводит к неожиданным взаимодействиям правил.

Всё ещё есть определённые правила xtables, которые нельзя определить через nftables — это задокументировано на вики-странице nftables о совместимости возможностей. Пробелы сокращались в последних релизах nft и ядра Linux, так что старые версии Ubuntu могут иметь более ограниченную поддержку. Кроме того, следует учитывать, что часть функциональности, доступной через Netlink-интерфейс nftables, может ещё не поддерживаться пользовательской утилитой nft (например, поддержка правил, вызывающих программы eBPF).

Начиная с Ubuntu 16.04 LTS (Xenial Xerus), пакет iptables предоставляет версии инструментов iptables, ip6tables, arptables и ebtables, работающие с API nftables и дающие совместимый интерфейс с legacy-реализацией. Бэкенд nftables, используемый утилитами iptables-nft / ip6tables-nft / arptables-nft / ebtables-nft, является стандартным начиная с Ubuntu 20.10 Groovy Gorilla. Они управляются через систему alternatives, и текущую конфигурацию можно посмотреть следующими командами:

update-alternatives --display iptables
update-alternatives --display ip6tables
update-alternatives --display arptables
update-alternatives --display ebtables

Утилиты iptables-nft / ip6tables-nft / arptables-nft / ebtables-nft предполагают, что никакое другое приложение не управляет правилами nftables нативно, и потому их не следует комбинировать с другими подходами к управлению правилами файрвола Netfilter.

ufw работает, вызывая legacy-утилиты iptables и ip6tables. Поэтому его не следует использовать одновременно с нативными правилами файрвола nftables.

Использование

Правила nftables настраиваются пользовательской утилитой nft, которую предоставляет пакет nftables Ubuntu. Связь с Netfilter выполняется через сокеты AF_NETLINK, что позволяет приложениям использовать и этот низкоуровневый интерфейс. Эта документация описывает только использование утилиты nft с фокусом на формат конфигурационного файла.

Начиная с Ubuntu 15.04 (Vivid Vervet), пакет nftables предоставляет unit-файл systemd-службы, по умолчанию отключённый. Если его включить, служба автоматически загрузит конфигурацию nftables из файла /etc/nftables.conf (в пакете nftables есть макет этого файла, не выполняющий никакой фильтрации). Включить её и загрузить конфигурацию можно следующими командами:

sudo systemctl enable nftables.service
sudo systemctl start nftables.service

Использование из командной строки

Утилита nft может принимать одну или несколько команд как аргументы для управления любыми поддерживаемыми объектами (таблицами, правилами, наборами и т.д.). Например, следующая команда выведет все правила файрвола:

sudo nft list ruleset

Все операции выполняются атомарно: обработка пакета увидит либо правила файрвола, определённые до вызова утилиты, либо правила со всеми запрошенными изменениями. Следующая команда создаст две таблицы, обрабатывающие и IPv4-, и IPv6-пакеты:

sudo nft "add table inet foo; add table inet bar"

Изменения в наборах правил nftables, выполненные через утилиту, эфемерны и теряются при перезагрузке (точнее — при уничтожении связанного с ними сетевого пространства имён). Постоянства можно добиться через упомянутый unit-файл systemd или похожие механизмы.

Как альтернативный способ вызова, утилите nft можно передать имя файла аргументом с флагом -f. Файл может содержать как команды, так и определения объектов в декларативном синтаксисе (подразумевается, что они создаются). Как и в случае командной строки, все операции выполняются атомарно. Стандартный файл /etc/nftables.conf содержит команду удаления всех настроенных правил (flush ruleset) и декларативное определение таблицы с именем filter, обрабатывающей и IPv4-, и IPv6-пакеты в трёх пустых цепочках:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority filter;
    }
    chain forward {
        type filter hook forward priority filter;
    }
    chain output {
        type filter hook output priority filter;
    }
}

Опция -f позволяет использовать утилиту nft как интерпретатор, что демонстрирует shebang-строка выше. Поскольку файл по умолчанию помечен как исполняемый, правила можно атомарно перезагрузить, просто выполнив файл:

sudo /etc/nftables.conf

Либо unit-файл systemd поддерживает команду reload для той же цели:

sudo systemctl reload nftables.service

Формат конфигурационного файла

Конфигурационный файл ориентирован на строки. Несколько команд можно объединить в одной строке, разделив их точкой с запятой (;). Комментарии добавляются знаком решётки (#) и продолжаются до конца строки. Команды можно разбивать на несколько строк, экранируя конец строки обратным слешем (\); строка, содержащая только комментарий, не считается строкой продолжения (она пропускается), а пустая строка считается строкой продолжения, фактически завершающей правило. Пробелы (и, соответственно, отступы) не имеют значения.

Хотя декларативный синтаксис использует фигурные скобки ({ и }) для определения блоков с описанием объектов, строкоориентированная обработка всё же действует, и её нужно учитывать (например, открывающая скобка ({) должна быть в той же строке, что и тип и имя объекта). Следующий пример закладывает основу конфигурационного файла хостового файрвола, которая будет расширяться на протяжении всей документации:

#!/usr/sbin/nft -f

# Это пустое определение нужно, чтобы команда flush сработала, если
# таблица ещё не определена.
table inet host-firewall; flush table inet host-firewall

# Обратите внимание: команда flush не уничтожает таблицу и объекты в ней,
# а лишь очищает правила во всех цепочках. Используйте следующее, если
# определения объектов нужно изменить, цепочки полностью уничтожить или
# наборы/maps очистить.
#destroy table inet host-firewall

table inet host-firewall {
    chain firewall-input {
        # Обрабатывать пакеты, адресованные этому хосту.
        type filter hook input priority filter;
        # Использовать политику запрета по умолчанию для пакетов.
        policy drop;
    }
}

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

  • flush ruleset очистит всю конфигурацию nftables, включая все таблицы, цепочки и правила, наборы или maps, объекты с состоянием и их содержимое, а также flowtables. Это уместно для единого центрального определения правил файрвола, но может привести к неожиданным результатам, если элементы в наборах или maps управляются извне или если таблицами управляет какое-либо другое приложение.

  • destroy table удалит таблицу и все связанные с ней объекты, включая цепочки и правила, наборы или maps, объекты с состоянием и flowtables. Это особенно полезно, когда настройки, связанные с объектами, могут меняться между версиями конфигурационного файла (например, приоритет базовой цепочки), или когда элементы в наборах или maps нужно удалить и пересоздать.

  • flush table очистит правила в таблицах, но не удалит цепочки, наборы и maps или их элементы, объекты с состоянием и flowtables. Это уместно, когда элементы в наборах или maps управляются извне или когда состояние объектов с состоянием не должно сбрасываться.

Директива include

Файлы можно включать с помощью директивы include. Они интерпретируются в контексте, в котором директива используется. Например, следующие подсвеченные строки позволяют drop-in-файлам добавлять правила в определённую цепочку firewall-input из /etc/nftables/input-rules.d/, а любым другим таблицам — определяться в файлах из /etc/nftables/tables.d/ (если используются подстановочные знаки, файлы могут и не существовать):

#!/usr/sbin/nft -f

# Это пустое определение нужно, чтобы команда flush сработала, если
# таблица ещё не определена.
table inet host-firewall; flush table inet host-firewall

# ...

table inet host-firewall {
    chain firewall-input {
        # Process packets destined for this host.
        type filter hook input priority filter;
        policy drop;

        # Drop-in files can add rules here.
        include "/etc/nftables/input-rules.d/*.conf"
    }
}

include "/etc/nftables/tables.d/*.conf"

Символьные переменные

Символьные переменные повышают сопровождаемость правил файрвола, связывая имена с произвольными выражениями, которые затем можно переиспользовать по всей конфигурации. Связав имя IF_LOOPBACK с именем интерфейса lo (стандартный loopback-интерфейс Linux), можно определить правило, ссылающееся на него, как показывают подсвеченные строки ниже:

#!/usr/sbin/nft -f

define IF_LOOPBACK = lo

# ...

table inet host-firewall {
    chain firewall-input {
        type filter hook input priority filter;
        policy drop;

        # Allow traffic on the loopback interface(s).
        meta iif $IF_LOOPBACK accept

        include "/etc/nftables/input-rules.d/*.conf"
    }
}

include "/etc/nftables/tables.d/*.conf"

Если позже появится новый loopback-интерфейс, можно воспользоваться синтаксисом наборов и изменить только символьную переменную:

define IF_LOOPBACK = { lo, lo1 }

Область действия символьных переменных — файл, интерпретируемый утилитой nft (и все включённые файлы), но с ограничением блоком, в котором переменная определена, и всеми вложенными блоками — для уменьшения конфликтов. Символьная переменная интерпретируется только в пользовательском пространстве. Любой другой конфигурационный файл, переданный в nft, не сможет на неё сослаться. Аналогично, получение набора правил, установленного в nftables (например, командой nft list ruleset), реконструирует правила, но без ссылок на символьные переменные.

Отладка

nftables предоставляет несколько средств отладки правил файрвола:

  • Использование выражения log, которое можно связать с любым правилом; оно приведёт к журналированию информации о пакете — либо в журнал ядра (читается через dmesg), либо в приложение пользовательского пространства. Подробно это описано в документации nftables и man-странице в разделе LOG STATEMENT.

  • Установка флага nftrace на пакете, что позволяет трассировать все правила, которым пакет соответствует, во всех цепочках и всех таблицах, с определением выполненных действий. Подробно описано в документации nftables.

Выражение log

Подсвеченные строки в следующем примере демонстрируют использование выражения log для отправки всех пакетов, приходящих на loopback-интерфейс, в журнал ядра перед их принятием:

#!/usr/sbin/nft -f

define IF_LOOPBACK = lo

# ...

table inet host-firewall {
    chain firewall-input {
        type filter hook input priority filter;
        policy drop;

        # Allow traffic on the loopback interface(s).
        meta iif $IF_LOOPBACK \
            # Log the packets...
            log prefix "loopback packet: " \
            # ...and accept them.
            accept

        include "/etc/nftables/input-rules.d/*.conf"
    }
}

include "/etc/nftables/tables.d/*.conf"

Проверка dmesg покажет сообщения вроде следующих (при условии, что пакеты действительно идут через loopback-интерфейс):

[694077.575927] loopback packet: IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.53 LEN=73 TOS=0x00 PREC=0x00 TTL=64 ID=24453 DF PROTO=UDP SPT=37969 DPT=53 LEN=53
Трассировка правил

Флаг nftrace включает трассировку прохождения пакета через правила nftables по цепочкам и таблицам — с момента установки флага до завершения обработки пакета или снятия флага. Эта функциональность позволяет вести сложную отладку правил файрвола nftables. Информация о пакете вместе со ссылками на пройденные правила отправляется приложению пользовательского пространства через netlink-интерфейс. Для получения этой информации используется команда nft monitor trace.

Выражение meta nftrace set 1 можно скомбинировать с выражением-условием для установки флага, а meta nftrace set 0 его снимает. Если нужно идентифицировать все пройденные правила, флаг стоит устанавливать как можно раньше. Подсвеченные строки в следующем примере создают две цепочки, подключённые к хукам prerouting и output, работающие настолько рано, насколько возможно (даже раньше других цепочек, зарегистрированных с приоритетом raw):

#!/usr/sbin/nft -f

define IF_LOOPBACK = lo

# ...

table inet host-firewall {
    chain trace-inbound {
        # Process after reassembly and conntrack lookup, but before other
        # potential raw chains.
        type filter hook prerouting priority raw - 10; policy accept;

        meta l4proto udp meta nftrace set 1
    }

    chain trace-outbound {
        # Process after conntrack lookup, but before other potential raw chains.
        type filter hook output priority raw - 10; policy accept;

        meta l4proto udp meta nftrace set 1
    }

    chain firewall-input {
        type filter hook input priority filter;
        policy drop;

        # Allow traffic on the loopback interface(s).
        meta iif $IF_LOOPBACK accept

        include "/etc/nftables/input-rules.d/*.conf"
    }
}

include "/etc/nftables/tables.d/*.conf"

Эти два правила совпадут только с UDP-датаграммами, независимо от того, переносит их IPv4 или IPv6 (meta l4proto udp), и затем включат трассировку правил для этих пакетов (meta nftrace set 1). Запуск команды nft monitor trace выдаст сообщения вроде:

trace id 78653943 inet host-firewall trace-inbound packet: iif "lo" @ll,0,112 0x800 ip saddr 127.0.0.53 ip daddr 127.0.0.1 ip dscp cs0 ip ecn not-ect ip ttl 1 ip id 64669 ip protocol udp ip length 168 udp sport 53 udp dport 36520 udp length 148 @th,64,96 0x2e4881800001000100000004
trace id 78653943 inet host-firewall trace-inbound rule meta l4proto udp meta nftrace set 1 (verdict continue)
trace id 78653943 inet host-firewall trace-inbound policy accept
trace id 78653943 inet host-firewall firewall-input packet: iif "lo" @ll,0,112 0x800 ip saddr 127.0.0.53 ip daddr 127.0.0.1 ip dscp cs0 ip ecn not-ect ip ttl 1 ip id 64669 ip protocol udp ip length 168 udp sport 53 udp dport 36520 udp length 148 @th,64,96 0x2e4881800001000100000004
trace id 78653943 inet host-firewall firewall-input rule ct state established,related accept (verdict accept)

trace id будет одинаковым для одного пакета в разных таблицах и цепочках, что позволяет сопоставлять разные строки вывода. Всякий раз, когда пакет начинает обрабатываться цепочкой, выводится строка packet с информацией о содержимом пакета.

Следует отметить, что уведомления трассировки, получаемые утилитой nft monitor trace, содержат только идентификаторы таблиц, цепочек и правил. nft monitor trace читает все правила при первом запуске. Имена таблиц и цепочек и фактическое содержимое правил реконструируются из этого первоначального чтения для каждого журналируемого пакета. Это значит, что если правила изменены после запуска nft monitor trace, вывод будет неполным или неточным (поскольку идентификаторы правил (handles), в частности, могут переиспользоваться), так что напечатанное правило может не быть тем правилом, которому пакет соответствовал.

Инспекция байткода

Байткод, интерпретируемый ядром Linux, можно наблюдать с помощью аргумента --debug=netlink у nft. Это работает и для команд, изменяющих правила (например, добавление нового правила), и для команд, получающих правила. Байткод печатается вместе с handle правила (идентификатором правила). Полезна и опция --handle — напечатать handles, связанные с каждым правилом.

Например, листинг цепочки trace-inbound, созданной выше:

sudo nft --handle --debug=netlink list chain inet host-firewall trace-inbound

даст следующий вывод:

inet host-firewall trace-inbound 13
  [ meta load l4proto => reg 1 ]
  [ cmp eq reg 1 0x00000011 ]
  [ immediate reg 1 0x00000001 ]
  [ meta set nftrace with reg 1 ]

table inet host-firewall {
    chain trace-inbound { # handle 1
        type filter hook prerouting priority raw - 10; policy accept;
        meta l4proto udp meta nftrace set 1 # handle 13
    }
}

Интеграция с Netfilter

Компонент nftables интегрирован в существующую подсистему Netfilter и использует те же хуки, обработку с состоянием для отслеживания соединений или преобразования сетевых адресов (NAT) и функциональность для очередей пакетов и обработки в пользовательском пространстве, что и подсистема xtables.

Высокоуровневое понимание каркаса Netfilter важно для управления правилами файрвола. Этот раздел даёт необходимую информацию и ссылки на дополнительную документацию.

Поток пакетов

Пакет начинает обрабатываться сетевой подсистемой Linux (и, как следствие, Netfilter) одним из трёх путей:

  • он принят драйвером сетевого интерфейса (физического сетевого контроллера (NIC) или виртуального);

  • он сгенерирован процессом приложения в системе (через сокет);

  • он сгенерирован ядром.

Netfilter интегрирован в более широкую сетевую подсистему Linux. Обработка пакета проходит через несколько точек принятия решений, потенциально изменяющих пакет, таких как:

  • реассемблирование фрагментов;

  • отслеживание соединений;

  • решения маршрутизации;

  • NAT источника и назначения (включая трансляцию портов).

Netfilter предоставляет хуки, позволяющие компонентам Netfilter обрабатывать пакет на разных стадиях. Они используются и nftables, и xtables для исполнения пользовательских правил. В частности, имена предопределённых цепочек в legacy-утилитах iptables / ip6tables / ebtables / arptables происходят от имён хуков Netfilter:

  • ingress (доступен только для nftables)

  • prerouting (для bridge и IP)

  • input (для ARP, bridge и IP)

  • forward (для bridge и IP)

  • postrouting (для bridge и IP)

  • output (для ARP, bridge и IP)

  • egress (доступен только для nftables)

Пакеты не пройдут через все точки хуков — в зависимости от решений, принятых в ходе обработки. Это наглядно показано на диаграмме вики-страницы nftables о хуках Netfilter. В частности, использование мостов (bridge) даёт другой поток пакетов, частично пересекающийся с потоком немостовых пакетов.

Следует учитывать, что часть стандартной обработки пакета выполняется в некоторых точках хуков (реассемблирование фрагментов, поиск в conntrack, NAT), а другая — между точками хуков (решение маршрутизации). В каждой точке хука порядок операций определяется приоритетом. Например, вот некоторые стандартные операции, выполняемые на IP-хуке prerouting:

Приоритет Netfilter Операция
-400 реассемблирование фрагментов
-200 поиск и связывание в conntrack
-100 NAT назначения

Список функций, включая цепочки nftables и стандартную обработку Netfilter, которые были зарегистрированы, можно вывести следующей командой (обратите внимание: числовые значения — это приоритеты, перечисленные в десятичном формате):

sudo nft list hooks

Если зарегистрировать правила с приоритетом ниже -400 (например, -500), они исполнятся до реассемблирования фрагментов IP-датаграмм. Поэтому правила могут увидеть фрагменты IP-датаграмм, у которых заголовок транспорта может быть недоступен, потому что это не первый фрагмент. С другой стороны, правила, зарегистрированные с приоритетом выше -400 (например, -300), не смогут принимать решения на основе информации о фрагментации (пакет будет выглядеть так, будто вся IP-датаграмма получена целиком).

Сами значения приоритетов не несут внутреннего смысла, кроме того факта, что некоторые стандартные операции выполняются на общеизвестных значениях приоритета. Например, при отсутствии другого контекста регистрация правил с приоритетом 1000 ничем не отличается от приоритета 1500. Документация nftables перечисляет общеизвестные значения приоритетов.

Пакет перестаёт обрабатываться сетевой подсистемой Linux — а значит, хуки Netfilter больше не вызываются — когда выполняется одно из условий:

  • пакет отброшен — либо правилом файрвола, либо другим условием стандартной обработки (например, маршрутом blackhole);

  • пакет передан процессу приложения в системе (через сокет);

  • пакет обработан ядром (например, ICMP echo request — пинг);

  • пакет отправлен в сетевой интерфейс (физический NIC или виртуальный).

Следует учитывать, что конкретный пакет может проходить через хуки Netfilter несколько раз, в таких условиях, как:

  • пакет отправлен в виртуальный интерфейс, который закольцовывает пакет обратно в то же ядро Linux (например, интерфейсы veth), хотя списки хуков не будут полностью пересекаться;

  • используется Virtual Routing and Forwarding (VRF) — пакет пройдёт L3-хук prerouting дважды: один раз с входным интерфейсом, установленным в L3-интерфейс, и один раз — с входным интерфейсом, установленным в VRF-интерфейс;

  • пакет обработан и повторно внедрён ядром в сетевой стек (например, после шифрования/расшифрования IPsec и инкапсуляции/декапсуляции ESP в туннельном режиме — хотя пакет, строго говоря, другой, часть состояния через эту операцию сохраняется, например метка Netfilter).

Хуки Netfilter и, соответственно, правила nftables управляются независимо в каждом сетевом пространстве имён. Поэтому в каждом сетевом пространстве имён настраиваются свои правила файрвола, что облегчает функциональность вроде контейнеров. Это также значит, что если два конца пары veth связаны с разными пространствами имён, они будут обрабатываться независимыми правилами файрвола.

Структура

nftables организует объекты для управления файрволом в иерархию. Основная используемая терминология:

  • Наборы правил (Rulesets): это все объекты, определённые в nftables; команда nft list ruleset выведет всё, что определено в nftables (в конкретном сетевом пространстве имён), а nft flush ruleset уничтожит все объекты: таблицы, наборы, maps и т.д. Это включает элементы, определённые в наборах и maps, или содержимое объектов с состоянием (например, значения счётчиков). Поэтому команда вроде следующей фактически ничего не делает (хотя состояние может измениться между моментом чтения и моментом перезаписи): (echo "nft flush rulset"; nft list ruleset) | nft -f -.

  • Таблицы (Tables): в отличие от xtables, в nftables можно определить любое число таблиц. Это коллекции цепочек, наборов, maps и объектов с состоянием (например, счётчиков). Имя таблицы не несёт внутреннего смысла и может назначаться системными администраторами или приложениями по желанию. Таблицы связаны с семейством адресов, диктующим ограничения на цепочки и определяющим, с какими хуками Netfilter цепочки будут связаны. Семейства адресов задокументированы в man-странице. Вот они:

    • ip: для IPv4-пакетов — то, чем управляла бы legacy-утилита iptables.

    • ip6: для IPv6-пакетов — то, чем управляла бы legacy-утилита ip6tables.

    • inet: для IPv4- и IPv6-пакетов — упрощает управление согласованными правилами для обоих сетевых протоколов.

    • arp: для ARP-пакетов IPv4 — то, чем управляла бы legacy-утилита arptables.

    • bridge: для Ethernet-пакетов, проходящих через мосты, — то, чем управляла бы legacy-утилита ebtables.

    • netdev: для очень ранней (на ingress) или очень поздней (на egress) обработки пакетов. Полезно для эффективной фильтрации или балансировки нагрузки, но налагает ограничения: поддержка только хуков ingress и egress и требование строгой привязки цепочек к единственному сетевому интерфейсу. Заметьте: начиная с Linux 5.10 семейство inet тоже поддерживает хук ingress без ограничения на единственный сетевой интерфейс, что во многом снижает полезность семейства netdev; использование одной таблицы в семействе inet также облегчило бы совместное использование наборов и maps с цепочками, зарегистрированными на других хуках.

  • Цепочки (Chains): контейнеры для правил файрвола; аналогично xtables, различаются базовые и обычные цепочки. В отличие от xtables, базовые цепочки не предопределены, и их можно создать сколько нужно, включая несколько цепочек на одних хуках (с одинаковым или разным приоритетом).

    • базовые цепочки имеют type, policy и регистрируются на точке hook Netfilter с определённым priority. Они также могут иметь дополнительные атрибуты, описанные в man-странице. Их правила вычисляются всякий раз, когда обработка пакета проходит через указанный хук Netfilter.

    • обычные цепочки просто вызываются правилами из других цепочек и могут рассматриваться как подпрограммы. Они полезны для упрощения сопровождения правил или оптимизации их обработки (например, с использованием verdict maps). Правила в обычной цепочке не вычисляются при обработке пакета, пока не будут вызваны — напрямую или косвенно — из базовой цепочки.

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

  • type: определяет условия, при которых пакет обрабатывается цепочкой, и доступные хуки. Некоторые выражения доступны только в определённых типах цепочек. Возможные значения:

    • filter: общий тип, применимый ко всем семействам адресов и всем хукам. Используется для типичных действий файрвола, а также произвольных модификаций пакетов.

    • nat: эквивалент цепочек, определённых в legacy-таблице nat утилит iptables / ip6tables. Только первый пакет соединения обрабатывается цепочками этого типа. Действия NAT (snat, dnat, masquerade, redirect) могут выполняться только в этих цепочках.

    • route: не имеет эквивалента в xtables; позволяет интегрировать правила nftables с policy routing. Может использоваться только с локально сгенерированными пакетами (из процессов или ядра), единственный доступный хук — output. Согласно диаграмме потока Netfilter, решение маршрутизации для локально сгенерированных пакетов выполняется до всех хуков. Однако если правила в цепочке типа route изменяют части пакета или его метаданные (например, метку Netfilter), используемые в решениях policy routing, будет выполнен ещё один поиск маршрута. Пакеты, принятые из сетевого интерфейса, не требуют этого специального типа цепочки, так как есть несколько хуков, которые могут подготовить пакет до прохождения решений маршрутизации.

  • hook: точка обработки, в которой вычисляются правила, как описано в разделе «Поток пакетов». Следует учитывать, что не все хуки доступны для всех семейств адресов и типов цепочек. Ограничения перечислены в разделе Chains man-страницы.

  • priority: определяет порядок, в котором цепочки и другие стандартные операции Netfilter выполняются в конкретной точке хука, как описано в разделе «Поток пакетов». Может задаваться как символьное имя (например, filter, raw, mangle), знаковое целое (например, 0, -300) или значение относительно символьного имени (например, raw - 10). Обратите внимание: символьные имена могут соответствовать разным целым значениям в зависимости от семейства адресов (filter — это 0 для inet, ip, ip6, arp и netdev, но -200 для bridge).

  • policy: определяет вердикт, связываемый с пакетом, если при обработке ни одно из совпавших правил не выдало вердикт. Должно быть accept (по умолчанию) или drop.

Следует учитывать, как описано в разделе «Поток пакетов», что пакет перестаёт обрабатываться сетевой подсистемой, когда он либо отброшен, либо прошёл весь поток обработки и либо отправлен в интерфейс, либо обработан приложением или ядром. Поэтому вердикт drop для пакета окончателен, а accept — нет: достаточно, чтобы одна цепочка в одной таблице выполнила drop — и пакет отброшен; но пакет должен быть accept-нут всеми цепочками во всех таблицах, чтобы продолжить путь (то есть вердикт accept завершает обработку только в конкретной базовой цепочке и не влияет на обработку в других базовых цепочках, через которые пакет пройдёт далее).

Состав правил

Правила состоят из выражений и инструкций (statements), оба опциональны. Выражения используются для сопоставления пакетов, а инструкции определяют, какие действия выполнить. Правило без инструкций валидно и может использоваться в целях отладки — оно будет отмечено трассировкой правил для всех совпавших пакетов. Например, следующее подсвеченное правило совпадёт с локально сгенерированными IPv4 UDP-пакетами, не выполняя никаких действий (обратите внимание на использование выражения ip protocol udp вместо meta l4proto udp: оно совпадёт только с IPv4-пакетами):

#!/usr/sbin/nft -f

destroy table inet test-firewall
table inet test-firewall {
    chain test-outbound {
        type filter hook output priority filter; policy accept;

        ip protocol udp
    }
}

Выражения внутри правила комбинируются логическим И при вычислении: все они должны сработать, чтобы инструкции правила выполнились. Комбинирование выражений логическим ИЛИ требует использования нескольких правил, наборов, maps или интервалов. В следующем примере первое правило совпадёт и с IPv4-, и с IPv6-пакетами, если транспортный протокол — UDP (meta l4proto udp) и порт назначения — 53 (udp dport 53). Второе правило совпадёт с пакетами, если сетевой протокол — IPv4 (подразумевается), транспортный протокол — UDP (подразумевается) и выполняется одно из:

  • IPv4-адрес назначения — 10.1.1.1, а порт назначения — 53

  • IPv4-адрес назначения — 10.2.2.2, а порт назначения — 80 или 443

Синтаксис ip daddr . udp dport объяснён в разделе «Конкатенации».

#!/usr/sbin/nft -f

destroy table inet test-firewall
table inet test-firewall {
    chain test-outbound {
        type filter hook output priority filter; policy accept;

        # Transport protocol is UDP and destination port is 53.
        meta l4proto udp udp dport 53

        # Network protocol is IPv4, transport protocol is UDP and the
        # combination of IPv4 destination address and UDP destination port
        # is one of the following:
        ip daddr . udp dport {
            10.1.1.1 . 53,
            10.2.2.2 . 80,
            10.2.2.2 . 443
        }
    }
}

Правило может содержать ноль или более инструкций. Есть два типа инструкций: терминальные и нетерминальные. Терминальные инструкции безусловно завершают вычисление правила и могут также завершить вычисление цепочки или полностью остановить обработку пакета. Нетерминальные инструкции приводят к действиям, которые либо не завершают вычисление правила, либо делают это условно. Единственное ограничение: правило может иметь максимум одну терминальную инструкцию, и она должна быть последней. Большинство вердикт-инструкций — терминальные, но есть и терминальные инструкции без вердикта (например, reject, который отбрасывает пакет и генерирует ответ ICMP или TCP reset).

Вердикт-инструкции

Вердикт-инструкции влияют на поток управления вычисления правил; большинство из них (кроме continue) — терминальные. Инструкция continue подразумевается, если никакая другая терминальная инструкция не связана с правилом. Вот список вердикт-инструкций:

  • accept: завершает обработку пакета в текущей базовой цепочке, позволяя пакету продолжить путь внутри Netfilter и сетевой подсистемы Linux. Другие базовые цепочки, зарегистрированные на текущем хуке с более высоким числовым приоритетом, всё равно вычислят пакет и могут его отбросить. Использование этой инструкции в обычной цепочке, вызванной — напрямую или косвенно — из базовой, останавливает обработку всех последующих правил как в текущей цепочке, так и в цепочках выше по стеку вызовов.

  • drop: завершает обработку пакета внутри сетевой подсистемы Linux без дальнейших действий. Эта инструкция — основа реализации файрвола. Никакие дальнейшие базовые цепочки не вызываются.

  • queue: завершает обработку пакета в текущей базовой цепочке и передаёт пакет в пользовательское пространство для дальнейшей обработки. Пользовательское пространство должно выдать вердикт accept или drop. Это объяснено в документации nftables по очередям.

  • continue: подразумеваемое действие, если никакая другая терминальная инструкция не выдана: вычисление правил продолжается со следующим правилом текущей цепочки.

  • jump: продолжает обработку в новой обычной цепочке; после завершения обработка возвращается в текущую цепочку, если только в одной из вызванных цепочек не выдана завершающая обработку инструкция (например, accept, drop, queue или reject). С точки зрения процедурного программирования это похоже на вызов подпрограммы (псевдокод: call subprocedure()).

  • goto: продолжает обработку в новой обычной цепочке; после завершения обработка не возвращается в текущую цепочку, а в цепочку выше по стеку вызовов (если текущая цепочка базовая, вместо этого выполняется действие policy). С точки зрения процедурного программирования это похоже на вызов и возврат результата подпрограммы (псевдокод: return subprocedure()).

Следующий пример расширяет предыдущее определение файрвола каркасом двух новых функций в подсвеченных строках, демонстрируя возможности управления потоком:

  • Установка метки Netfilter для входящих пакетов, обозначающей происхождение пакета, чтобы последующие правила могли принимать решения на основе этого критерия. Мы называем это «realm» (областью), но не следует путать с iproute2 realms. Например, правила ниже устанавливают метку в значение 1 (через символьную переменную MARK_REALM_LOCAL), если пакет принят на одном из loopback-интерфейсов. Вводятся две новые цепочки: early-inbound (базовая) и mark-inbound-determine (обычная).

    • Когда обработка пакета следует за ним через входной VRF-интерфейс (meta iifkind "vrf"), обработка пакета в этой цепочке завершается через return. Инструкция return, поскольку она находится в базовой цепочке, эквивалентна вызову policy цепочки (accept в данном случае).

    • Если пакет приходит в эту цепочку с ненулевой меткой (meta mark != 0) — условие, которое может возникать при использовании функциональности вроде расширения GBP у VXLAN, — пакет отбрасывается полностью.

    • Обычная цепочка mark-inbound-determine вызывается через jump mark-inbound-determine; это позволяет последующим правилам в цепочке early-inbound быть вычисленными.

    • В цепочке mark-inbound-determine, если пакет принят на одном из интерфейсов, определённых символьной переменной IF_LOOPBACK (meta iif $IF_LOOPBACK), выполняются две инструкции:

      • метка пакета устанавливается в значение MARK_REALM_LOCAL, определённое как 1 (meta mark set $MARK_REALM_LOCAL) — нетерминальная инструкция;

      • обработка в цепочке mark-inbound-determine завершается инструкцией return, и пакет продолжает обработку в вызывающей цепочке (early-inbound).

  • В базовой цепочке firewall-input обработка multicast-пакетов делегируется обычной цепочке firewall-input-multicast. Это позволяет инкапсулировать multicast-логику в отдельную цепочку, улучшая сопровождаемость. Базовая конфигурация принимает IPv4 IGMP-пакеты, поскольку они нужны для стандартной multicast-готовой топологии сети с multicast-запросчиками (и, возможно, bridge multicast snooping, необходимым для эффективной multicast-переадресации уровня 2). Эквивалентное для IPv6 правило MLD вводится в разделе «Наборы». Отдельно Multicast DNS (mDNS) пакеты, используемые также протоколом DNS Service Discovery (DNS-SD), разрешаются отдельным правилом (udp dport 5353 accept). mDNS обычно используется в LAN для ad-hoc обнаружения сервисов — например, сетевых принтеров и сетевых шар. В Ubuntu такими протоколами пользуются приложения вроде Avahi (общее обнаружение сервисов) и cups-browsed (обнаружение сетевых принтеров).

    • Инструкция goto гарантирует, что последующие правила в firewall-input не вычисляются, даже если вызванная цепочка выполнит return или некоторые пакеты не совпадут ни с одним правилом; вместо этого в этих случаях применится policy (drop).

    • Инструкция accept в цепочке firewall-input-multicast необходима, чтобы пропустить пакеты. Как только достигнуто одно из условий (например, ip protocol igmp), обработка завершается, и дальнейшие правила в firewall-input-multicast или firewall-input не вычисляются.

#!/usr/sbin/nft -f

define IF_LOOPBACK = lo

define MARK_REALM_LOCAL = 1

# ...

table inet host-firewall {
    chain early-inbound {
        type filter hook prerouting priority raw; policy accept;

        # When VRF interfaces are in use, packets go through the prerouting hook
        # twice, once with the VRF interface set as input and another time with
        # actual interface set as input.
        meta iifkind "vrf" return

        # Do not allow inbound packets that have an externally-determined packet
        # mark (this is possible, for example, by using VXLAN with the GBP
        # extension).
        meta mark != 0 drop
        jump mark-inbound-determine
    }

    chain mark-inbound-determine {
        # Set the realm to LOCAL for packets received on the loopback interface.
        meta iif $IF_LOOPBACK meta mark set $MARK_REALM_LOCAL return
    }

    chain firewall-input {
        # Process packets destined for this host.
        type filter hook input priority filter;
        policy drop;

        # Allow traffic on the loopback interface(s).
        meta iif $IF_LOOPBACK accept

        # Process multicast packets. Upon returning, do not evaluate any more
        # rules and apply the policy verdict (drop).
        meta pkttype multicast goto firewall-input-multicast

        # Drop-in files can add rules here.
        include "/etc/nftables/input-rules.d/*.conf"
    }

    chain firewall-input-multicast {
        # Allow any IPv4 IGMP.
        ip protocol igmp accept

        # Allow inbound Multicast DNS packets.
        udp dport 5353 accept

        # If no prior action was taken, this will return to the calling chain
        # (firewall-input).
    }
}

include "/etc/nftables/tables.d/*.conf"

Другие инструкции

nftables поддерживает большое число инструкций. Они задокументированы в разделе Statements man-страницы. Хотя этот документ не претендует на полноту, вот некоторые из более употребимых:

  • reject: отбрасывает пакет, но также генерирует соответствующий ответ ICMP или TCP reset. Например, правило udp dport 389 reject with icmpx admin-prohibited совпадёт с пакетами, адресованными на порт LDAP (udp dport 389), и сгенерирует соответствующий сетевому протоколу ICMP-ответ admin-prohibited (тип 3 код 13 для IPv4 и тип 1 код 1 для IPv6).

  • log: описано в разделе «Выражение log».

  • meta: позволяет изменять мета-информацию, отслеживаемую Netfilter для конкретного пакета, например meta mark set 42 для установки метки Netfilter в константу 42 или meta nftrace set 1 для включения трассировки правил.

  • nat: позволяют выполнять трансляцию сетевых адресов источника и назначения (NAT) (включая поддержку динамической трансляции портов транспортного протокола и обработку ICMP-пакетов с состоянием).

  • counter: поддержка подсчёта пакетов и байтов, совпавших с правилами.

  • payload: позволяют изменять произвольное содержимое пакетов: например, ip dscp set 46 устанавливает поле DSCP IPv4 в 46 (EF — Expedited Forwarding).

  • set: позволяет динамически добавлять элементы в наборы и maps. Объяснено в соответствующих разделах.

  • map: позволяет искать элементы в map по произвольному ключу и возвращать связанное значение для использования как аргумента другой инструкции. Это пример нетерминальной инструкции, которая может условно завершить обработку правила, если ни один элемент map не совпадает с входным ключом. Подробно объяснено в разделе «Maps».

  • vmap: позволяет динамически определять вердикт правила на основе произвольного ключа; подробно объяснено в разделе «Verdict maps».

Выражения

Выразительная сила nftables происходит из реализации основанной на байткоде виртуальной машины для вычисления выражений. У выражения есть связанный тип данных, определяющий, как операции вычисляются на выражении и как его можно комбинировать с другими выражениями или использовать как аргументы инструкции. Например, выражение meta mark имеет 32-битный целый тип данных. Типы перечислены в разделе Data Types man-страницы. Обратите внимание: типы данных — особенность пользовательской утилиты nft; байткод, интерпретируемый ядром Linux, оперирует исключительно сырыми байтами.

Тип данных выражения можно посмотреть командой nft describe, например:

nft describe udp dport

Выражения обычно следуют конвенции «класс, затем атрибут» (например, udp dport, ip protocol или meta mark). Они задокументированы в man-странице в разделах Primary Expressions и Payload Expressions.

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

  • eq или ==: подразумеваемое сравнение (udp dport 53 эквивалентно udp dport == 53). Может сравнивать произвольное выражение с константой или искать выражение в наборе (udp dport == { 80, 443 } совпадает, если порт назначения — 80 или 443).

  • ne или !=: совпадает, если произвольное выражение не равно константе (например, udp dport != 53) или отсутствует в наборе (udp dport != { 80, 443 } совпадает, если назначение — ни 80, ни 443).

  • lt / <, gt / >, le / <= и ge / >=: совпадают, если сравнение произвольного выражения — меньше, больше, меньше-или-равно и больше-или-равно соответственно — с константой (например, udp dport < 1024 совпадает с привилегированными UDP-портами).

Выражения также можно комбинировать с бинарными операторами:

  • and / &: побитовое И

  • or / |: побитовое ИЛИ

  • xor / ^: побитовое исключающее ИЛИ

  • lshift / <<: побитовый сдвиг влево

  • rshift / >>: побитовый сдвиг вправо

Правая сторона бинарных операторов должна быть константным выражением. Например, следующее выражение совпадёт с IPv4-пакетами, у которых второй по значимости байт адреса назначения меньше 16:

(ip daddr >> 16) & 0xFF < 0x10

То же условие можно записать как:

ip daddr & 0x00F00000 == 0

Или, менее читаемо:

ip daddr & 0x00F00000 0

Операторы равенства и неравенства можно также использовать с интервалами, совпадая, если значение выражения находится (или, соответственно, не находится) в замкнутом интервале. Следующее выражение совпадает с IPv4-пакетами, у которых адрес назначения имеет форму A.B.C.D, где B между 10 и 20 (включительно):

(ip daddr >> 16) & 0xFF == 10-20

Адреса IPv4 и IPv6 также поддерживают префиксную нотацию: следующее совпадает, если IPv4-адрес назначения — не один из приватных адресов RFC1918:

ip daddr != { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }

Не все операции поддерживаются всеми типами данных. Например, IPv6-адреса не поддерживают битовые сдвиги, а операторы and, or и xor требуют полных IPv6-адресов на правой стороне — как и операторы сравнения.

Комбинировать операторы выражений с инструкциями, поддерживающими выражения, тоже возможно. Например, следующее выражение устанавливает метку Netfilter в 16 наименее значимых бит IPv4-адреса источника, скомбинированных с установленным битом 16 — но только если IPv4-адрес источника в префиксе 10.0.0.0/16:

ip saddr 10.0.0.0/16 meta mark set (ip saddr & 0xFFFF) | 0x10000

Битовые маски поддерживают специфические операции, упрощающие управление, особенно через символьные имена, связанные с отдельными битами:

  • Без оператора реляционное выражение совпадает, если любой из указанных бит установлен. Выражение tcp flags syn,ack совпадает, если у пакета установлен хотя бы один из битов SYN или ACK. Это эквивалентно tcp flags & (syn|ack) != 0.

  • Оператор / можно использовать для указания маски в дополнение к набору значений, которые должны быть настроены. Выражение tcp flags syn / syn,ack совпадает, если из битов SYN и ACK установлен только SYN (остальные биты не важны). Это эквивалентно tcp flags & (syn|ack) == syn.

  • Операторы равенства (eq / ==) и неравенства (ne / !=) сравнивают точное значение битовой маски. Выражение tcp flags == syn,ack совпадает тогда и только тогда, когда установлены оба бита SYN и ACK, а все остальные биты очищены. Это эквивалентно tcp flags == (syn|ack).

Соединение этих концепций позволяет создать каркас использования метки Netfilter как битового поля, облегчающий родовые правила файрвола. Поскольку метка пакета может определяться внешними источниками (например, VXLAN с расширением GBP) и копируется при декапсуляции пакетов (например, IPsec), нужна специальная обработка: один бит (флаг) используется для определения, можно ли доверять метке пакета как проверенной локально.

Следующая конфигурация nftables содержит два изменения по сравнению с предыдущим примером:

  • Расширение цепочки early-inbound с двумя обычными цепочками, которые она вызывает (mark-inbound-determine и mark-inbound-external-validate). Конвенция формата метки Netfilter объяснена в комментарии вверху файла, с символьными переменными, определёнными для упрощения операций с битовыми полями.

  • Два новых правила в цепочке firewall-input, использующие выражение битовой маски ct state:

    • пропускать пакеты, отмеченные conntrack-модулем как established или related (ct state established,related accept);

    • отбрасывать пакеты, отмеченные conntrack-модулем как invalid (ct state invalid drop).

#!/usr/sbin/nft -f

define IF_LOOPBACK = lo

# The packet mark is interpreted as follows (big endian):
#    3                   2                   1                   0
#  1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# |V| Unused                                          | Realm (6) |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
#
# V - validated flag (1 - packet mark was validated locally; 0 - it wasn't)
# Realm - class of hosts the packet originated from (64 possible values)

define MARK_MASK_REALM  = 0x0000003f

define MARK_REALM_UNKNOWN   = 0  # Other provenance of packet
define MARK_REALM_LOCAL     = 1  # Packet from local host
define MARK_REALM_VIRT      = 2  # Packet from local VMs / containers
define MARK_REALM_LAN       = 3  # Packet from internal network

define MARK_FLAG_VALIDATED  = 0x80000000

# ...

table inet host-firewall {
    chain early-inbound {
        type filter hook prerouting priority raw; policy accept;

        # When VRF interfaces are in use, packets go through the prerouting hook
        # twice, once with the VRF interface set as input and another time with
        # actual interface set as input.
        meta iifkind "vrf" return

        # If the mark was previously set with the validated flag set (e.g.
        # decapsulated packet), reset it. This also resets the mark for remote
        # packets that automatically set the mark and attempt to forge the
        # validated flag (e.g. VXLAN with the GBP extension).
        (meta mark & $MARK_FLAG_VALIDATED) != 0 meta mark set 0
        meta mark != 0 jump mark-inbound-external-validate
        meta mark == 0 jump mark-inbound-determine
        meta mark set (meta mark | $MARK_FLAG_VALIDATED)
    }

    chain mark-inbound-external-validate {
        # Do not allow externally-determined marks to have the realm set to
        # LOCAL or VIRT.
        meta mark & $MARK_MASK_REALM == {
            $MARK_REALM_LOCAL,
            $MARK_REALM_LAN,
        } drop
    }

    chain mark-inbound-determine {
        # Set the realm to LOCAL for packets received on the loopback interface.
        meta iif $IF_LOOPBACK meta mark set $MARK_REALM_LOCAL return

        # Set the realm to VIRT for packets received on bridge interfaces.
        meta iifkind "bridge" meta mark set $MARK_REALM_VIRT return

        # Set the realm to LAN for link-local and private addresses.
        ip saddr {
            169.254.0.0/16,
            10.0.0.0/8,
            172.16.0.0/12,
            192.168.0.0/16,
        } meta mark set $MARK_REALM_LAN return
        ip6 saddr {
            fe80::/64,
            fc00::/7,
        } meta mark set $MARK_REALM_LAN return
    }

    chain firewall-input {
        # Process packets destined for this host.
        type filter hook input priority filter;
        policy drop;

        # Use conntrack state to allow packets belonging to already established
        # flows, while dropping packets which conntrack considers invalid.
        ct state established,related accept
        ct state invalid drop

        # Allow traffic on the loopback interface(s).
        meta iif $IF_LOOPBACK accept

        # Process multicast packets. Upon returning, do not evaluate any more
        # rules and apply the policy verdict (drop).
        meta pkttype multicast goto firewall-input-multicast

        # Drop-in files can add rules here.
        include "/etc/nftables/input-rules.d/*.conf"
    }

    chain firewall-input-multicast {
        # Allow any IPv4 IGMP.
        ip protocol igmp accept

        # Allow inbound Multicast DNS packets.
        udp dport 5353 accept

        # If no prior action was taken, this will return to the calling chain
        # (firewall-input).
    }
}

include "/etc/nftables/tables.d/*.conf"

Конкатенации

Конкатенации позволяют комбинировать выражения в составные выражения со сложным типом, используя оператор .. Они особенно мощны в сочетании с наборами и maps, позволяя определять ключи на основе нескольких атрибутов пакета. Например, следующее выражение комбинирует три разных поля — метку Netfilter, транспортный протокол (meta l4proto совпадает независимо от инкапсулирующего сетевого протокола, IPv4 или IPv6) и порт назначения транспортного протокола (th dport совпадает независимо от транспортного протокола — TCP, UDP или SCTP):

meta mark . meta l4proto . th dport

Использование бинарных операторов для извлечения информации на основе установленной ранее конвенции метки Netfilter вместе с анонимными наборами для указания альтернативных значений и интервалов даёт мощные выражения сопоставления:

(meta mark & $MARK_MASK_REALM) . meta l4proto . th dport {
    # Web service allowed from anywhere
    0-63                . tcp   . 80,
    # SSH allowed from local machine and local VMs
    $MARK_REALM_LOCAL   . tcp   . 22,
    $MARK_REALM_VIRT    . tcp   . 22,
    # SIP signalling allowed from LAN over any transport
    $MARK_REALM_LAN     . sctp  . 5060-5061,
    $MARK_REALM_LAN     . tcp   . 5060-5061,
    $MARK_REALM_LAN     . udp   . 5060-5061,
} accept

Наборы (Sets)

Наборы — родовая структура данных в nftables, выступающая контейнером значений с поддержкой эффективных операций поиска, добавления и удаления. Они похожи на функциональность ipset, доступную в xtables, но поддерживают произвольные типы благодаря использованию конкатенаций. Реализация использует хеш-таблицы и красно-чёрные деревья. Наборы бывают двух типов:

  • Именованные наборы: определены в таблицах и со связанным именем; они позволяют и внешним приложениям, и правилам nftables управлять элементами.

  • Анонимные наборы: определены инлайн в правилах; они позволяют выражать логический оператор OR. Выражение tcp dport { 80, 443 } совпадает, если TCP-порт назначения — 80 или 443.

Именованные наборы, как и другие объекты (таблицы или цепочки), можно определять многократно с аддитивным эффектом. Это позволяет добавлять элементы наборов в нескольких местах — например, с помощью директив include с подстановочными знаками для drop-in-файлов. В отличие от анонимных наборов, в определение можно добавить различные опции конфигурации, управляющие поведением наборов. Все они задокументированы в разделе Sets man-страницы, но вот некоторые из более полезных:

  • type или typeof: необходимы для именованного набора и определяют формат элементов. type требует использования имён типов данных, возможно с конкатенациями, а typeof получает выражение, из которого выводится тип элементов. Конфигурация typeof особенно полезна для выражений, у которых связан только целочисленный тип переменной длины и которые нельзя выразить через type (например, typeof meta cgroup нельзя выразить через type).

  • flags interval: позволяет использовать интервалы в элементах. Анонимный набор с интервалами тоже фактически активирует этот флаг.

  • flags dynamic: позволяет добавлять элементы из правил, используя инструкцию set.

  • flags timeout: позволяет автоматически удалять элементы по прошествии интервала с момента (последнего) добавления элемента в набор.

  • timeout: выражение, определяющее интервал по умолчанию, после которого элемент будет удалён из набора. Например: timeout 5m для 5-минутного интервала.

  • size: определяет максимальное число элементов, которое может вместить набор.

Следующие строки расширяют пример конфигурации файрвола:

  • Именованным набором (input-services) для сервисов, разрешённых на локальный хост. Они определены на основе метки Netfilter (только биты realm), транспортного протокола и порта назначения транспортного протокола. Директива include облегчает определение дополнительных сервисов в drop-in-файлах.

  • Правилом, ссылающимся на новый именованный набор в базовой цепочке firewall-input.

  • Новым правилом в обычной цепочке firewall-input-multicast, которое пропускает IPv6-пакеты Multicast Listener Discovery (MLD) и Neighbour Discovery (ND) ICMPv6 через анонимный набор. Они обычно необходимы для корректной работы IPv6 в локальных сетях.

#!/usr/sbin/nft -f

define IF_LOOPBACK = lo

# ...

table inet host-firewall {
    set input-services {
        type mark . inet_proto . inet_service
        flags interval
        elements = {
            # Web service allowed from anywhere
            0-63                . tcp   . 80,
            # SSH allowed from local machine and local VMs
            $MARK_REALM_LOCAL   . tcp   . 22,
            $MARK_REALM_VIRT    . tcp   . 22,
            # SIP signalling allowed from LAN over any transport
            $MARK_REALM_LAN     . sctp  . 5060-5061,
            $MARK_REALM_LAN     . tcp   . 5060-5061,
            $MARK_REALM_LAN     . udp   . 5060-5061,
        }
    }
    include "/etc/nftables/input-services.d/*.conf"

    # ... (цепочки early-inbound, mark-inbound-*, firewall-input — как ранее)

    chain firewall-input {
        # Process packets destined for this host.
        type filter hook input priority filter;
        policy drop;

        ct state established,related accept
        ct state invalid drop

        # Allow traffic on the loopback interface(s).
        meta iif $IF_LOOPBACK accept

        # Process multicast packets. Upon returning, do not evaluate any more
        # rules and apply the policy verdict (drop).
        meta pkttype multicast goto firewall-input-multicast

        # Allow services based on the origin realm, the transport protocol and
        # the destination port.
        (meta mark & $MARK_MASK_REALM) . meta l4proto . th dport @input-services accept

        # Drop-in files can add rules here.
        include "/etc/nftables/input-rules.d/*.conf"
    }

    chain firewall-input-multicast {
        # Allow any IPv4 IGMP.
        ip protocol igmp accept

        # Allow IPv6 MLD (for multicast group management) and neighbour
        # discovery (note that unicast packets would not be handled here).
        icmpv6 type {
            mld-listener-query,
            mld-listener-report,
            mld-listener-reduction,
            mld2-listener-report,
            nd-router-advert,
            nd-neighbor-solicit,
            nd-neighbor-advert,
        } accept

        # Allow inbound Multicast DNS packets.
        udp dport 5353 accept

        # If no prior action was taken, this will return to the calling chain
        # (firewall-input).
    }
}

include "/etc/nftables/tables.d/*.conf"

Недостаток конфигурации с drop-in-файлами в том, что каждый файл должен заново определять набор с точно такими же настройками:

#!/usr/sbin/nft -f

set input-services {
    type mark . inet_proto . inet_service
    flags interval
    elements = {
        # LDAP server access allowed from local machine and LAN
        $MARK_REALM_LOCAL   . tcp   . 389,
        $MARK_REALM_LAN     . tcp   . 389,
    }
}

Управление элементами в правилах

Элементы именованного набора можно динамически добавлять из правил с помощью инструкций add или update. Их использование требует наличия флага dynamic в определении набора. Главное различие между инструкциями: add условно завершает правило раньше, если элемент уже существует в наборе, а update обновит любую мета-информацию, связанную с элементом (например, сброс таймаута или модификацию связанных объектов с состоянием).

Для демонстрации этой функциональности следующий пример реализует тривиальный rate limit для новых соединений: принимаются только те новые соединения, для которых конкатенацию адреса источника, транспортного протокола и порта назначения можно добавить в набор (то есть если элемент ранее не существовал). Элементы истекают через 2 минуты, так что это фактически применяет лимит одного соединения каждые 2 минуты. Обратите внимание на ограничение размера набора — необходимое, чтобы объём используемой памяти был ограничен. Это сделано только для иллюстрации инструкции add; лимиты — предпочтительное средство реализации rate limiting, они объяснены в разделе «Объекты с состоянием».

destroy table ip limits
table ip limits {
    set connections {
        type ipv4_addr . inet_proto . inet_service
        flags dynamic, timeout
        timeout 2m
        size 65536
    }
    chain limits-inbound {
        # This must execute after conntrack lookup (priority -200).
        type filter hook prerouting priority filter; policy drop;

        # Only apply limits to packets that establish new flows.
        ct state != new accept

        # Accept packets that can be added to the set.
        add @connections { ip saddr . meta l4proto . th dport } accept

        # Anything that reaches here is dropped by policy.
    }
}

Аналогично, инструкция update может использоваться для добавления элементов в набор, но не завершится ошибкой, если элемент уже существует. В следующем примере таймаут сбрасывается, что позволяет набору отслеживать любой префикс IPv4 /24, инициировавший новый поток за последние 10 минут.

destroy table ip flow-track
table ip flow-track {
    set connections {
        type ipv4_addr
        flags dynamic, timeout
        timeout 10m
        size 65536
    }
    chain track-inbound {
        # This must execute after conntrack lookup (priority -200).
        type filter hook prerouting priority filter; policy accept;

        # Only bother with packets that establish new flows.
        ct state != new accept

        # Add /24 prefix to the connections set.
        ct state new update @connections { ip saddr & 255.255.255.0 }
    }
}

Управление элементами из пользовательского пространства

Элементами именованных наборов можно инспектировать и управлять и из пользовательского пространства. Это реализовано поверх стандартной связи AF_NETLINK для nftables, причём утилита nft предлагает удобные команды, описанные в разделах Sets и Elements man-страницы. Вот примеры команд для ранее определённого набора connections из таблицы flow-track семейства ip.

Листинг всех элементов набора:

sudo nft list set ip flow-track connections

Удаление всех элементов того же набора:

sudo nft flush set ip flow-track connections

Сброс любых объектов с состоянием, связанных с элементами набора (например, счётчиков или лимитов, объяснённых в разделе «Объекты с состоянием»):

sudo nft reset set ip flow-track connections

Получение элементов из набора:

sudo nft get element ip flow-track connections '{ 127.0.0.0 }'

Добавление элемента в набор (похожая команда create завершится успешно, только если элемент не существует):

sudo nft add element ip flow-track connections '{ 10.0.0.0 }'

Удаление элемента из набора (похожая команда destroy завершится успешно, даже если элемента не существует):

sudo nft delete element ip flow-track connections '{ 10.0.0.0 }'

Maps

Maps — структуры данных nftables, связывающие ключи со значениями, форма ассоциативных массивов или словарей. Maps похожи на наборы: фактически наборы реализованы как maps, где элементы — ключи без связанных значений. Поэтому использование maps очень похоже на наборы, включая:

  • анонимные maps и именованные maps;

  • похожие опции конфигурации для именованных maps;

  • возможность управлять элементами из других приложений или правил (используются те же инструкции и команды — см. раздел «Наборы»).

Maps поддерживают операцию поиска — инструкцию map, которая возвращает значение, связанное с ключом поиска. Возвращённое значение затем может использоваться как выражение с тем же типом, что и тип значений map. Если соответствующий ключ не найден, инструкция завершает вычисление правила раньше, и дальнейшие инструкции не выполняются.

Подсвеченные строки в следующем расширении примера используют map из IPv4- и IPv6-префиксов в метки Netfilter, представляющие realm происхождения, и устанавливают метку Netfilter с помощью расширяемого правила: новые элементы можно добавлять, чтобы сопоставить другие адреса с другими значениями метки. Помимо двух новых maps (ip4-known-addresses и ip6-known-addresses), изменения сделаны в обычной цепочке mark-inbound-determine. Правило meta mark set ip saddr map @ip4-known-addresses return можно разложить на:

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

  2. Поиск ключа в map @ip4-known-addresses: ip saddr map @ip4-known-addresses. Тип значений map — метка Netfilter. Если ключ не найден, правило завершается раньше, и вычисление продолжается со следующим правилом цепочки.

  3. Установка метки Netfilter пакета в значение, возвращённое поиском в map: meta mark set ip saddr map @ip4-known-addresses.

  4. Возврат из текущей цепочки: return. Это выполняется, только если предыдущее выражение ip saddr или инструкция map не завершили вычисление правила — либо потому что пакет не IPv4, либо потому что ключ не найден.

#!/usr/sbin/nft -f

# ... (символьные переменные и набор input-services — как ранее)

    map ip4-known-addresses {
        type ipv4_addr : mark
        flags interval
        elements = {
            # link-local addresses
            169.254.0.0/16  : $MARK_REALM_LAN,
            # RFC1918 private addresses
            10.0.0.0/8      : $MARK_REALM_LAN,
            172.16.0.0/12   : $MARK_REALM_LAN,
            192.168.0.0/16  : $MARK_REALM_LAN,
        }
    }

    map ip6-known-addresses {
        type ipv6_addr : mark
        flags interval
        elements = {
            # link-local addresses
            fe80::/64   : $MARK_REALM_LAN,
            # RFC4193 local addresses
            fc00::/7    : $MARK_REALM_LAN,
        }
    }

    # ...

    chain mark-inbound-determine {
        # Set the realm to LOCAL for packets received on the loopback interface.
        meta iif $IF_LOOPBACK meta mark set $MARK_REALM_LOCAL return

        # Set the realm to VIRT for packets received on bridge interfaces.
        meta iifkind "bridge" meta mark set $MARK_REALM_VIRT return

        # Set the realm for known addresses.
        meta mark set ip saddr map @ip4-known-addresses return
        meta mark set ip6 saddr map @ip6-known-addresses return
    }

    # ...

Verdict maps

Verdict maps похожи на maps, главное отличие в том, что значения, связанные с ключами, — вердикт-инструкции. Это позволяет использовать их с инструкцией vmap, которая ищет ключ в map и, если ключ найден, исполняет связанную вердикт-инструкцию; если ключ не найден, вычисляется следующее правило цепочки (подразумеваемая инструкция continue). Инструкция vmap терминальная (то есть должна быть последней инструкцией в правиле).

Эта функциональность позволяет принимать эффективные ветвящиеся решения. Например, приведённые выше правила в цепочке early-inbound, которые либо валидируют внешнюю метку Netfilter, либо определяют её локально, если она ещё не установлена, можно переписать с инструкцией vmap. Следует отметить, что два правила всё ещё могут быть эффективнее, чем использование красно-чёрного дерева для этого простого ветвления.

# Вместо следующих двух правил...
#meta mark != 0 jump mark-inbound-external-validate
#meta mark == 0 jump mark-inbound-determine

# ... используем инструкцию vmap:
meta mark vmap {
    0:              jump mark-inbound-determine,
    1-0xFFFFFFFF:   jump mark-inbound-external-validate,
}

Объекты с состоянием

Объекты с состоянием позволяют отслеживать информацию между несвязанными пакетами, чтобы реализовать функциональность вроде:

  • счётчиков байтов и пакетов: подсчёт числа пакетов или общего размера пакетов, проходящих через правило;

  • квот байтов: подсчёт общего размера пакетов, проходящих через правило, и исполнение инструкций, когда размер ниже или выше порога (например, разрешить только определённый объём данных для передачи между разными потоками);

  • лимитов: rate limiter на число пакетов или размер пакетов по токенам (token bucket), позволяющий исполнять инструкции, когда скорость пакетов или данных ниже или выше определённого порога;

  • лимитов соединений: интегрированных с conntrack Netfilter, позволяющих исполнять инструкции, когда число совпавших потоков выше или ниже определённого порога.

Объекты с состоянием бывают двух типов, определяющих, разделяется ли состояние между разными контекстами:

  • Именованные объекты связаны с конкретной таблицей и могут ссылаться более чем из одного правила. Поэтому одни и те же данные могут опрашиваться или изменяться из нескольких мест (например, один счётчик инкрементируется более чем одним правилом). Их также можно запрашивать или управлять ими из пользовательского пространства.

  • Анонимные объекты не имеют связанного имени и привязаны к единственному контексту: конкретному правилу или ключу набора. В частности, объекты лимита соединений, как объяснено в документации nftables, могут быть только анонимными объектами.

Создание именованных объектов с состоянием следует той же конвенции, что и для всех остальных объектов. Например, следующий файл создаёт счётчик, на который ссылаются при достижении конца цепочки limits-inbound:

destroy table inet limits
table inet limits {
    counter dropped-flows {
    }
    chain limits-inbound {
        # This must execute after conntrack lookup (priority -200).
        type filter hook prerouting priority filter; policy accept;

        # Only apply limits to packets that establish new flows.
        ct state != new accept

        # Anything that reaches here is dropped.
        counter name dropped-flows drop
    }
}

Получение данных счётчика:

sudo nft list counter ip limits dropped-flows

Сброс данных счётчика:

sudo nft reset counter ip limits dropped-flows

Анонимные объекты могут быть связаны с правилами. Подсвеченные строки в следующем расширении предыдущего примера добавляют некоторую функциональность:

  • Для HTTP-трафика (выбранного через tcp dport { 80, 443 }) используется анонимный лимит соединений (ct count over 1000), который условно завершает вычисление правила раньше, если порог не достигнут, останавливая исполнение последующих инструкций правила (counter name dropped-flows и drop). Любые потоки conntrack, совпавшие с критериями выбора, отслеживаются лимитом соединений; как только поток перестаёт отслеживаться conntrack, он удаляется из этого лимита соединений.

  • Для не-HTTP-трафика аналогично используется анонимный лимит соединений для отбрасывания пакетов, инициирующих новые потоки, при достижении порога (500 потоков).

  • Для HTTP-трафика используется анонимный лимит (limit rate over 20/minute burst 500 packets), который условно завершает вычисление правила раньше, если порог не достигнут, останавливая последующие инструкции правила (counter name dropped-flows и drop). Token bucket rate limiter обновляется для каждого пакета, совпавшего с правилом.

  • Для не-HTTP-трафика аналогично используется анонимный лимит для отбрасывания пакетов, инициирующих новые потоки, при достижении порога token bucket.

Обратите внимание: пакеты, инициирующие новые потоки, отбрасываются, если выполнено любое из условий: активное число потоков conntrack или скорость создания новых потоков conntrack.

destroy table inet limits
table inet limits {
    counter dropped-flows {
    }

    chain limits-inbound {
        # This must execute after conntrack lookup (priority -200).
        type filter hook prerouting priority filter; policy accept;

        # Only apply limits to packets that establish new flows.
        ct state != new accept
        # Do not apply limits to local and VMs communication.
        meta mark & $MARK_MASK_REALM {
            $MARK_REALM_LOCAL,
            $MARK_REALM_VIRT
        } accept

        jump drop-on-flow-count
        jump drop-on-new-flow-rate
    }

    chain drop-on-flow-count {
        # Allow at most 1000 simultaneous flows for HTTP.
        tcp dport { 80, 443 } ct count over 1000 \
            counter name dropped-flows \
            drop
        # If this rule is reached, the above threshold did not get reached, so
        # return in order to avoid counting this traffic towards subsequent
        # limits.
        tcp dport { 80, 443 } return

        # Allow at most 500 simultaneous flows for everything else.
        ct count over 500 \
            counter name dropped-flows \
            drop
    }

    chain drop-on-new-flow-rate {
        # Allow at most 20 new flows per minute (with a burst of 500) for HTTP.
        tcp dport { 80, 443 } limit rate over 20/minute burst 500 packets \
            counter name dropped-flows \
            drop
        # If this rule is reached, the above threshold did not get reached, so
        # return in order to avoid counting this traffic towards subsequent
        # limits.
        tcp dport { 80, 443 } return

        # Allow at most 10 new flows per minute (with a burst of 100) for
        # everything else.
        limit rate over 10/minute burst 100 packets \
            counter name dropped-flows \
            drop
    }
}

Анонимные объекты с состоянием, связанные с элементами наборов, создаются через расширение инструкций add и update, следующее тому же синтаксису, что и выражения правил. Это позволяет связывать отдельные объекты с состоянием с произвольным набором критериев. Фактически этот расширенный синтаксис превращает инструкции add и update в операцию поиска объекта с состоянием, связанного с ключом, и может завершать правило раньше (например, если достигнут порог token bucket limiter). Следующий пример добавляет поддержку отслеживания числа потоков и скорости новых потоков на подсеть:

  • Для отслеживания числа потоков создаются два набора: flow-count-ip4 и flow-count-ip6. Элементы с IPv4 /24-подсетями и IPv6 /48-подсетями добавляются в эти наборы со связанными лимитами соединений. Вычисление правила завершается раньше, если порог не достигнут; если достигнут — пакет, инициирующий новый поток, отбрасывается. Элементы автоматически удаляются из наборов, когда с ними больше не связано записей conntrack.

  • Для отслеживания скорости потоков создаются два набора: flow-rate-ip4 и flow-rate-ip6. Элементы с IPv4 /24-подсетями и IPv6 /48-подсетями добавляются в эти наборы со связанными лимитами (объектами с состоянием). Операция update фактически обновляет таймаут, после которого элементы наборов истекают (1 минута). Вычисление правила завершается раньше, если порог не достигнут; если достигнут — префикс добавляется в blocklist, и пакет, инициирующий новый поток, отбрасывается. Элементы автоматически удаляются из наборов, если не обновляются (то есть если ни одного пакета, инициирующего новый поток, для данной подсети не получено в течение таймаута набора в 1 минуту).

  • Наборы blocklist — blocklist-ip4 и blocklist-ip6 — содержат IPv4 /24-подсети и IPv6 /48-подсети, превысившие пороги скорости соединений. Элементи истекают из этих наборов через 10 минут, в течение которых новые потоки запрещены. Конечно, можно было бы обойтись без этих дополнительных наборов — тогда новый поток мог бы установиться, как только token bucket rate limiter это позволил бы.

  • Ранее продемонстрированные глобальные ограничения, применимые к любому IPv4- или IPv6-адресу источника, сохранены.

destroy table inet limits
table inet limits {
    counter dropped-flows {
    }

    set blocklist-ip4 {
        type ipv4_addr
        flags dynamic
        timeout 10m
        size 65536
    }
    set flow-rate-ip4 {
        type ipv4_addr
        flags dynamic, timeout
        timeout 1m
        size 65536
    }
    set flow-count-ip4 {
        type ipv4_addr
        flags dynamic
        size 65536
    }

    set blocklist-ip6 {
        type ipv6_addr
        flags dynamic
        timeout 10m
        size 65536
    }
    set flow-rate-ip6 {
        type ipv6_addr
        flags dynamic, timeout
        timeout 1m
        size 65536
    }
    set flow-count-ip6 {
        type ipv6_addr
        flags dynamic
        size 65536
    }

    chain limits-inbound {
        # This must execute after conntrack lookup (priority -200).
        type filter hook prerouting priority filter; policy accept;

        # Only apply limits to packets that establish new flows.
        ct state != new accept
        # Do not apply limits to local and VMs communication.
        meta mark & $MARK_MASK_REALM {
            $MARK_REALM_LOCAL,
            $MARK_REALM_VIRT
        } accept

        jump drop-on-flow-count
        jump drop-on-new-flow-rate
    }

    chain drop-on-flow-count {
        # Allow at most 50 simultaneous flows per IPv4 /24 subnet or IPv6 /48
        # subnet.
        add @flow-count-ip4 { ip saddr & 255.255.255.0 ct count over 50 } \
            counter name dropped-flows \
            drop
        add @flow-count-ip6 { ip6 saddr & ffff:ffff:ffff:: ct count over 50 } \
            counter name dropped-flows \
            drop

        # Allow at most 1000 simultaneous flows for HTTP.
        tcp dport { 80, 443 } ct count over 1000 \
            counter name dropped-flows \
            drop
        tcp dport { 80, 443 } return

        # Allow at most 500 simultaneous flows for everything else.
        ct count over 500 \
            counter name dropped-flows \
            drop
    }

    chain drop-on-new-flow-rate {
        # Drop packets from IPv4 /24 subnets that have been added to the
        # blocklist.
        ip saddr & 255.255.255.0 @blocklist-ip4 \
            counter name dropped-flows \
            drop
        # Update token bucket rater limiter per IPv4 /24 subnets; if over the
        # threshold, add the subnet to the blocklist and drop the packet.
        update @flow-rate-ip4 { \
                ip saddr & 255.255.255.0 \
                limit rate over 5/second burst 50 packets \
            } \
            add @blocklist-ip4 { ip saddr & 255.255.255.0 } \
            counter name dropped-flows \
            drop

        # Drop packets from IPv6 /48 subnets that have been added to the
        # blocklist.
        ip6 saddr & ffff:ffff:ffff:: @blocklist-ip6 \
            counter name dropped-flows \
            drop
        # Update token bucket rater limiter per IPv6 /48 subnets; if over the
        # threshold, add the subnet to the blocklist and drop the packet.
        update @flow-rate-ip6 { \
                ip6 saddr & ffff:ffff:ffff:: \
                limit rate over 5/second burst 50 packets \
            } \
            add @blocklist-ip6 { ip6 saddr & ffff:ffff:ffff:: } \
            counter name dropped-flows \
            drop

        # Allow at most 20 new flows per minute (with a burst of 500) for HTTP.
        tcp dport { 80, 443 } limit rate over 20/minute burst 500 packets \
            counter name dropped-flows \
            drop
        tcp dport { 80, 443 } return

        # Allow at most 10 new flows per minute (with a burst of 100) for
        # everything else.
        limit rate over 10/minute burst 100 packets \
            counter name dropped-flows \
            drop
    }
}

Flowtables

Flowtables — функциональность, ускоряющая переадресацию пакетов для хостов, выступающих маршрутизатором уровня 3 или мостом уровня 2. Функциональность действует как кеш для определения, принадлежит ли пакет известному потоку, и может быть почти напрямую отправлен в сетевой интерфейс после приёма (возможно, другим) сетевым интерфейсом. Поэтому функциональность нельзя использовать для потоков, у которых один конец — локальный процесс через сокет. Поток идентифицируется входным интерфейсом вместе с информацией уровней 2, 3 и 4: сессией PPPoE, адресами источника и назначения сети, портами источника и назначения транспортного протокола.

Для совпавших потоков стек переадресации полностью обходится, включая большинство хуков Netfilter и решения моста или маршрутизации — до точки, где нужно определить адрес уровня 2 (через IPv4 ARP или IPv6 NDP). Дисциплины очередей (queuing disciplines) всё же применяются, позволяя traffic shaping. Обход логики переадресации может быть проблемой для динамических настроек, где кешированная информация может устареть (например, L3-переадресация, интегрированная с L2-мостом, если следующий хоп может переместиться на другой порт моста).

Flowtables интегрированы с conntrack: быстрый путь flowtable активируется, только когда поток видел пакеты в обоих направлениях. Затем потоки обновляются с короткими фиксированными интервалами, пока пакеты поступают и таймауты conntrack не наступают. Интервалы обновления можно настроить для TCP и UDP через sysctl net.netfilter.nf_flowtable_tcp_timeout и net.netfilter.nf_flowtable_udp_timeout, в остальных случаях они фиксированы (30 секунд для Linux 6.15). Состояние conntrack синхронизируется в соответствии с принятыми пакетами.

Некоторые сетевые карты (NIC) также поддерживают аппаратный offload информации о потоках, что дополнительно оптимизирует переадресацию пакетов. Когда это недоступно или не включено (через флаг offload у flowtable), вместо него используется программный offload в ядре Linux.

Flowtables реализованы с использованием хука ingress. Приоритет, связанный с flowtable, фактически является приоритетом хука ingress, на котором выполняется поиск: это значит, что любые цепочки, зарегистрированные с более низким числовым приоритетом, всё равно обработают пакет, совпавший с flowtable, а цепочки с более высоким числовым приоритетом — нет, как и цепочки, зарегистрированные на любых других точках хуков Netfilter, кроме egress. Больше информации о flowtables — в документации Netfilter и документации ядра Linux.

Следующий пример включает ускоренную переадресацию пакетов между набором интерфейсов. Нужно регистрировать фактический сетевой интерфейс, принимающий пакет, даже если включён мост.

define LAN_DEVICES = { eth0, eth1, eth2 }

destroy table inet flow-offload
table inet flow-offload {
    flowtable lan-forwarding {
        hook ingress priority 0;
        devices = $LAN_DEVICES;
    }
    chain offload-forward {
        type filter hook forward priority filter; policy accept;

        # Only offload UDP packets.
        meta l4proto udp flow add @lan-forwarding
    }
}

Другие возможности

Поиск FIB и фильтрация reverse-path

Forwarding Information Base (FIB) — общий термин для таблицы поиска, используемой при переадресации пакетов уровня 2 или 3 (коммутация или маршрутизация). В этом контексте он относится к базе данных политик маршрутизации Linux (RPDB), управляющей структурами поиска решений L3-маршрутизации. Выражение fib можно использовать в правилах для выполнения поиска маршрутов и принятия решений на основе результата. Его синтаксис гибок и поддерживает несколько типов вызова, как описано в man-странице.

Одно из возможных использований — определить, локален ли сетевой адрес для хоста. Такой тест можно выполнить рано, до принятия решения о маршрутизации пакета, и не учитывать метку Netfilter, поскольку таблица маршрутизации local обычно опрашивается первой. Следующий пример считает пакеты, у которых адрес назначения сетевого уровня (fib daddr) — локальный или broadcast.

#!/usr/sbin/nft -f

destroy table inet test-firewall
table inet test-firewall {
    chain test-prerouting {
        type filter hook prerouting priority filter; policy accept;

        fib daddr type { local, broadcast } counter
    }
}

Другой сценарий использования поиска маршрутов — фильтрация reverse-path (rp-filter): отбрасывание пакетов, если они приходят на интерфейс, не связанный с маршрутом, покрывающим сетевой адрес источника пакета. При policy routing для выбора таблицы маршрутизации могут использоваться разные данные, и в этом случае проверка reverse-path может быть неточной; метку Netfilter можно учитывать в выражениях fib, что должно покрыть большинство конфигураций policy routing. Следующий пример демонстрирует этот сценарий. Следует отметить:

  • Метка Netfilter должна быть установлена до использования выражения fib — как если бы она была установлена для пакета до решения маршрутизации в обратном направлении (то есть для пакета с адресом назначения, равным адресу источника проверяемого пакета).

  • Конфигурации IPv6 обычно используют один и тот же link-local префикс (fe80::/64) на всех интерфейсах, что требует специальной обработки.

#!/usr/sbin/nft -f

destroy table inet test-firewall
table inet test-firewall {
    chain test-prerouting {
        type filter hook prerouting priority filter; policy accept;

        # Must determine Netfilter mark as if for reverse direction here.

        # Chain will drop packets which do not pass the reverse-path filter check
        jump rp-filter
    }

    chain rp-filter {
        # Ignore IPv6 packets with a link-local source address.
        ip6 saddr fe80::/64 return
        # FIB expression with oif output will return 0 if interface cannot
        # be determined.
        fib saddr . mark . iif oif 0 drop
    }
}

Payload-выражения

Payload-выражения позволяют выбирать конкретные поля внутри пакета. Утилита nft поддерживает большое число таких выражений для простого управления правилами — они задокументированы в разделе Payload Expressions man-страницы.

Когда их недостаточно или необходима глубокая инспекция пакетов (DPI), можно использовать сырые payload-выражения: они позволяют выбирать произвольные части (длиной до 128 бит) пакета с фиксированным смещением от одного из заголовков уровней 2, 3 или 4. Фактически символьные payload-выражения (например, icmpv6 taddr, выбирающий целевой адрес NDP- или MLD-пакетов IPv6) транслируются пользовательской утилитой в сырые payload-выражения, не требуя какой-либо специальной поддержки в компоненте ядра nftables Linux. Используется общий формат @base,offset,length, где смещение и длина заданы в битах. Вот базы, от которых можно вычислять смещения:

База Описание Пример
@ll канальный уровень (например, заголовок Ethernet) @ll,0,48 — 48 бит от начала заголовка канального уровня — для Ethernet это MAC-адрес назначения
@nh сетевой заголовок (например, IPv4, IPv6) @nh,48,8 — 8 бит от 48 бит от начала сетевого заголовка — для IPv6 это поле next header
@th транспортный заголовок (например, TCP, UDP) @th,110,2 — 2 бита от 110 бит от начала транспортного заголовка — для TCP это флаги SYN и FIN
@ih внутренний заголовок (payload после транспортного заголовка) @ih,8,16 — 16 бит от 8 бит от начала payload, инкапсулированного в транспортный протокол — для TLS это версия

Следует учитывать, что для пользовательской утилиты nft есть различия между использованием сырых и символьных payload-выражений:

  • Выражение вроде ip6 nexthdr получит подразумеваемый meta nfproto ip6, если выражение добавлено в таблицу inet или bridge, поскольку через них будут проходить и пакеты, не являющиеся IPv6. @nh,48,8 возьмёт 7-й байт от начала сетевого заголовка, каким бы протоколом он ни был (например, IPv4, ARP и т.д.).

  • Утилита nft выполняет проверку типов. icmpv6 taddr имеет тип ipv6_addr (в пользовательском пространстве), позволяющий сравнения с IPv6-адресами (включая префиксы вроде fe80::/64). @th,64,128 трактуется как 128-битное целое, даже если ему предшествует выражение вроде icmpv6 type nd-neighbor-advert, которое подразумевало бы, что эти 128 бит должны интерпретироваться как целевой адрес NDP IPv6. Поэтому использование сырых payload-выражений для поиска в наборах/maps или сравнений может быть ограничено.


nftables firewall сеть linux