Linux nftables packet filter
Screened
The Linux kernel already contains a variety of packet filters, starting with ipfwadm
and followed by ipchains
and iptables
. Kernel 3.13 saw the introduction of nftables [1], which uses the nft
tool to create and manage rules. With the help of its own virtual machine, nftables ensures that rulesets are converted into bytecode, which is then loaded into the kernel. Not only does it improve performance, but it also allows administrators to enable new rules dynamically without having to reload the entire ruleset.
Parts of the old Netfilter framework use nftables, removing the need to develop new hooks, which are nothing more than certain points in the network stack of the Linux kernel at which a packet is inspected and, in the case of a match, one or more actions executed. For this purpose, tables that store chains exist at these hook points. The chains in turn contain the rules.
The way in which the individual packets are now checked against the rules is another new feature of nftables. The classification is now far more sophisticated and elegant than it was in the days of iptables. For example, address families now allow you to process different packages with a single rule. If you wanted to examine IPv4 and IPv6 packets in the past, you not only needed different rules, you even had to load them into the kernel with different tools: iptables
and ip6tables
.
The simple nftables inet
table type includes both IPv4 and IPv6. Now, you can also merge different statements with nftables. With iptables, writing a packet to the log first and then performing another action, such as dropping the packet, was a very roundabout approach that required two rules:
iptables -A INPUT -p tcp --dport 23 -j LOG iptables -A INPUT -p tcp --dport 23 -j DROP
With nftables, this is reduced to a single rule:
nft add rule filter input tcp dport 23 log drop
This kind of facilitation can be found at many different places in nftables.
In addition to the hooks, nftables continues to use Netfilter code for connection tracking, network address translation (NAT), userspace queuing, and logging. The compatibility layer is very helpful if you are migrating from iptables, because it lets you continue using the iptables netfilter
tool, even if the underlying framework is now nftables, not Netfilter. If you don't need Netfilter and prefer to have all new features available instead, you can use the new nft
tool instead.
To make sure the kernel you are using supports nftables, call the modinfo
tool (Listing 1). You should then see some information about the nf_tables
kernel module.
Listing 1
modinfo nf_tables
# modinfo nf_tables filename: /lib/modules/4.20.5-200.fc29.x86_64/kernel/net/netfilter/nf_tables.ko.xz alias: nfnetlink-subsys-10 author: Patrick McHardy <kaber@trash.net> license: GPL depends: nfnetlink retpoline: Y intree: Y name: nf_tables vermagic: 4.20.5-200.fc29.x86_64 SMP mod_unload sig_id: PKCS#7 signer: sig_key: sig_hashalgo: md4
Kernel-Dependent Routing
Because the old Netfilter hooks are still used, the route of a packet through the network stack with nftables is similar to that of Netfilter (Figure 1): In prerouting
, a decision is made as to whether a network packet is either intended for a process on the local machine or simply needs to be forwarded in-line with the routing table. In the first case, the package reaches the local process by way of the input
entry point, where it is processed. It then passes through the output
and postrouting
hooks before leaving the network stack again.
If the package is not intended for a local process, though, routing is performed on the basis of existing routing entries. Make sure that the kernel supports this routing, which is defined in the Linux kernel by the /proc/sys/net/ipv4/ip_forward
or /proc/sys/net/ipv6/ip_forward
file. In this case, the package only passes through the prerouting
, forward
, and postrouting
hooks.
In these three hooks, the packet can be rewritten by NAT in terms of the IP address and the port. Nftables allows changes to the target address for the prerouting
and input
hooks, and to the sender's address for the postrouting
and output
hooks. If you want to filter the packet instead, you can create corresponding tables in the input
, forward
, or output
hooks and store your rulesets there. Another innovation in nftables is the new ingress
entry point, which allows the filtering of packets on Layer 2, providing functions similar to the tc
(traffic control) [2] tool.
Unlike Netfilter, nftables has no predefined constructs for tables and chains in which the actual rules end up. Administrators need to create these themselves with the nft
tool:
nft add|list|flush|delete table|chain|rule <options>
If you now want to create a new table, you must consider a few points. For example, it is essential to assign an address family to a table. The following address families are provided by nftables: ip
, ip6
, inet
, bridge
, arp
, and netdev
. Although the first four families can be used in all hooks, nftables only allows the arp
address family in tables that are created as part of the input
or output
hooks, and netdev
is only allowed for ingress
tables. If no address family is specified when creating a table, nftables uses ip
by default.
To load rulesets into the kernel that ensure that both IPv4 and IPv6 packets are checked for their properties and filtered, you need to create a table with the inet
address family. In the following example, this table is named firewall
:
nft add table inet firewall
A call to nft list tables
confirms that the table was created correctly:
nft list tables inet table inet firewall
If needed, you can limit the output to certain address families.
Creating a New Chain
The next step is to create a new chain within this table that has the task of incorporating the rules. As with the address families, which are linked with tables, different types of chains exist: filter
, nat
, and route
. The filter
chains can be created in all hooks; nat
chains are allowed in prerouting
, input
, output
, and postrouting
hooks; and route
chains can only be created in output
hooks. Because the purpose of this example is to filter IP packets for a local computer, you need to create a filter
chain, assign it to the previously created firewall
table, and specify where in the network stack it should be placed:
nft create chain inet firewall incoming { type filter hook input priority 0\; }
You have now successfully created a base chain for filtering IP packets within the firewall table, defined a default priority, and named the chain incoming
. The call to nft list chains
confirms that everything worked successfully:
nft list chains table inet firewall { chain incoming { type filter hook input priority 0; policy accept; } }
Within this chain you can then create rules that ensure that incoming and outgoing packets are inspected according to certain criteria, such as the source and target IP addresses, source or target ports, or state variables (e.g., membership of an existing connection). If all these criteria apply to a data packet flowing through the network stack and thus through each Netfilter hook, a match has occurred, and a specific action is performed. This action should also be defined as a rule. For example, you can tell nftables to accept or reject the packet in a match, or simply create a log entry.
In the following example, I present some simple rules to give you a feel for the new nftables syntax. The first rule ensures that nftables accepts all packets passing through the loopback interface:
nft add rule inet firewall incoming iif lo accept
Furthermore, new SSH connections (ct state new
) to port 22 will be allowed (tcp dport 22
). Packets that belong to existing SSH connections are also allowed (ct state established,related
) and are detected by nftables connection tracking. All other packets are dropped:
nft add rule inet firewall incoming ct state established,related accept nft add rule inet firewall incoming tcp dport 22 ct state new accept nft add rule inet firewall incoming drop
The individual objects and their hierarchy are now displayed by nftables with nft list ruleset
(Listing 2). The -a
option ensures that the internal enumeration (handles) of the individual rules is also displayed. A new rule can be inserted later easily enough with the command:
nft add rule inet firewall incoming position 4 tcp dport 443 ct state new accept
Listing 2
nft list ruleset
nft list ruleset -a table inet firewall { chain incoming { type filter hook input priority 0; policy accept; iif "lo" accept # handle 5 ct state established,related accept # handle 7 tcp dport ssh ct state new accept # handle 8 drop # handle 9 } }
This rule now also allows all new connections to secure HTTPS port 443. You do not have to worry about packets that belong to existing connections at this point, because they are already detected and accepted by the connection tracking match with handle 7
. The matches already mentioned above are extremely diverse in nftables and allow very complex rulesets [3]. Thanks to tcpdump
-based syntax, however, they look quite compact and can be understood intuitively.
Flexible Sorting of Rules
If you want to add some order into your rulesets, you can do this with some of the other new nftables features. For example, rules can be sorted very easily with the help of non-base chains. However, they are not assigned directly to an entry point in the kernel and therefore do not see any network traffic. That said, you can jump from other chains into these non-base chains and thus evaluate the rules that exist there.
The idea behind this functionality is that a multitude of rules can be sorted logically in a very simple and elegant way. The following example creates a chain named smtp-chain
that contains just a single rule with a traffic counter pointing at the local SMTP port. From the existing incoming
chain, the system then jumps into this new chain, evaluates the existing rule, and then continues with the rules from the incoming
base chain. In this case, only the catch-all rule is evaluated, and all packets that have not already been captured by other rules are dropped:
nft add chain inet firewall smtp-chain nft add rule inet firewall incoming position 8 tcp dport 25 ct state new jump smtp-chain nft add rule inet firewall smtp-chain counter
Also important at this point is to insert the jump
rule at the correct position in the incoming
chain; otherwise, the rule would come after the drop
catch-all statement and never be executed. After the last changes, the new set of rules now looks like Listing 3.
Listing 3
Revised Ruleset
nft list table inet firewall table inet firewall { chain smtp-chain { counter packets 1 bytes 80 } chain incoming { type filter hook input priority 0; policy accept; iif "lo" accept ct state established,related accept tcp dport ssh ct state new accept tcp dport https ct state new accept tcp dport smtp ct state new jump smtp-chain drop } }
The set
is another new feature in nftables that lets you to merge elements of a rule, such as an IP address or port, into an array. You can then use this array in the desired rule. The following example of a named set assigns IP address ranges to allow-smtp-set
:
nft add set inet firewall allow-smtp-set {type ipv4_addr\; flags interval\; } nft add element inet firewall allow-smtp-set { 10.1.0.0/24, 192.168.0.0/24 }
You can then access this named set in any rule:
nft add rule inet firewall incoming position 8 tcp dport { 25, 587 } ip saddr @allow-smtp-set accept
In this case, a new rule is placed at a defined point in the incoming
chain and uses the previously defined allow-smtp-set
to specify the sender address. For the SMTP ports, on the other hand, an "anonymous set" is used, which you can use directly in a rule without having to define it beforehand.
Buy this article as PDF
(incl. VAT)