Lead Image © alphaspirit, 123RF.com

Lead Image © alphaspirit, 123RF.com

Linux nftables packet filter

Screened

Article from ADMIN 55/2020
By
The latest nftables packet filter implementation, now available in the Linux kernel, promises better performance and simpler syntax and operation.

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.

Figure 1: nftables uses the hooks already known from Netfilter. However, the ingress entry point, which also supports Layer 2 filtering, is new.

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

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy ADMIN Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

comments powered by Disqus