Table of Contents

nftables

This advanced guide explains how nftables works under the hood, using a manual nftables configuration.

This guide is incompatible with fw4 since it is also generating nftables rules.

So, if you simply want to configure the firewall on your device, this is the wrong documentation!

nftables project is an enhancement to netfilter, re-using most of the existing code but enhancing/streamlining based on experience.

As with iptables, there is a large amount of information and examples available on the web for nftables. Some links include:

nftables in OpenWrt (22.03 and later)

Since OpenWrt 22.03, fw4 is used by default, and it generates nftables rules.

See firewall configuration to configure firewall rules with UCI and netfilter management to explore the nftables rules created by fw4.

In any case, the guide below will probably not work, because the manual rules will clash with rules generated by fw4.

nftables in OpenWrt (21.02 and earlier)

Historically in OpenWrt (in 21.02 and before), nftables was not the primary form of firewall and NAT in OpenWrt, that role was taken by iptables - and that was what is set via the web interface in OpenWrt. However if users are comfortable using the command line interface, nftables are supported by OpenWrt 21.02 and earlier. The rest of this article describes how to use them, users should do this with care, and read the general documentation for nftables.

Firstly nftables kernel modules, are in part not compatible with iptables, in particular the kernel module iptable_nat the nftables equivalents nf_nat* and nf_masq; so before using nftables you have to switch off the loading of iptable NAT. This is easily done by:

Which means that

are not loaded.

The nf kernel modules are already loaded via:

For nftables though need additional kernel modules, and user space executables and libraries:

Kernel:

Executable:

And Library:

Most of these can all be installed at at the same time via

Which pulls in the needed libraries, kernel modules and executables.

Not all the kernel modules are loaded, so for example if you wish to do NAT you will also need:

And possibly more, depending on your needs.

nftables can be configured via the command line, just like iptables, all be it with a different syntax. However nftables can also read a “c” like script - and this script is far more readable, and the suggested way to use nftables. This needs to be saved in a file, and the suggested location is /etc/nftables.conf. An example script is given at the bottom of this page, and worth studying. The rest of this page uses this script as an example. In this example there are 4 different ip inputs:

The aim here is to join together the first three, so they can communicate. To set up firewall towards the WAN. To set up NAT for ipv4. And to let in a few ports on the WAN.

table ip nat {
	chain prerouting {
		type nat hook prerouting priority 0; policy accept;
	}

	chain postrouting {
		type nat hook postrouting priority 100; policy accept;
	}
}

This introduces several concepts. The script is made up of tables, that contain chains, that contain rules. In iptables tables also exist, but in only certain types. nftables is more flexible, in that the tables can be called anything. Convention is though to use the iptables names by default. So in this case the table is called “nat” as it contains NAT rules, the NAT nature though is only set up in the chains with the type nat. The table contains two chains, one for “prerouting” and one for “postrouting” again these are just names, their behaviour is only set up with the “hook {pre|post}routing”. The last point is this acts on the family “ip”, there are only six families in nftables:

This is similar to iptables family of commands, but under nftables there are all under the same command. Also nftables contains the concept inet that applies to all IP packets, which means one set of rules can cover both. The one exception to inet packets in in NAT tables (like this) where ip and ipv6 need to be separate.

table ip nat {
	chain prerouting {
		type nat hook prerouting priority 0; policy accept;
		tcp dport 12345 dnat to 192.168.2.111:ssh
	}
}

This is the rule, it says router when a packet arrives for tcp port 12345, that it is forward only the ssh port on a machine on the LAN. This is forwarded using dnat - destination NAT, as its a ssh connection, and anything back from the machine on LAN is send back to machine sending the original packet. This is done in prerouting it is done as soon as the packet enters the machine. A similar command would used if you wanted a WWW server on a local machine exported to the WAN so people could connect over the internet.

table ip nat {
	chain postrouting {
		type nat hook postrouting priority 100; policy accept;
		oiftype ppp masquerade
	}
}

This is another NAT, its the main NAT so that LAN can get out to the internet on the WAN, but looking like the WAN connection. masquerade is a type of snat - Source NAT. On packets it changes the source address, in a snat the address can be set, but on a masquerade the address is always set to the port it is going out on. In this case the port is set by oiftype ppp, this means the WAN, but the reason isn't easy to see. The WAN is on an ADSL connection, this is a serial connection, which packets are sent across by using ppp. Now the oiftype ppp says only run this command on the output port if it is running ppp. However how are you to know this - you won't find this command described almost everywhere on the www. The simple way to learn about it though, is the command nft describe oiftype, and that shows what types of port nftable can detect, and includes ppp. This part of the code could also be written oifname “pppoa-wan” masquerade however testing for ppp is the simpler difference between the WAN and LAN ports. Note you can see which type ports are under using the ip a command.

table inet filter {
	chain input {
		type filter hook input priority 0; policy drop;
	}

	chain forward {
		type filter hook forward priority 0; policy drop;
	}

	chain output {
		type filter hook output priority 0; policy accept;
	}
}

This is the main filter table, it is set up in the same way as the nat table. The chain names (and table name) can be anything, but they only become tied to what they do on the type line which sets up their description.

The rules for input and forward are exactly the same, this is saying we treat packets from the LAN as the same for going on to the internet, as going to the router. Also in reverse, anything from the WAN needs to pass either through the input or forward chain, so we treat both the same. The code is:

		ct state { related, established } accept
		ct state invalid drop  
		iiftype != ppp accept
		tcp dport ssh accept
		ip protocol icmp accept     
		ip6 nexthdr ipv6-icmp accept
		iiftype ppp drop

The first ct state says that any packet we get back, that is related to something we sent, that it should be accepted. So when you ask for something from the WAN, you get to see the reply. There is an important concept here, the test is on two different states related and established, if the ct state is either then the packet is accepted. Writing a rule that matches multiple things at once is not possible in iptables, and this is one way nftables is simpler. The second ct state just drops bad packets. the iiftype != ppp accept and we see iiftype again - it this says that any packet that comes from somewhere that isn't a ppp port (e.g. the WAN) then we should accept this. This allows everything from the LAN to propagate.

Next tcp dport ssh accept says accept all ssh connections, even from the WAN. This is needed as the dnat is forwarding ssh connections across the router. Note though you probably don't want this on the input, as it means you allow the WAN to log onto the router via ssh. This will quickly be picked up by port scans, and you find attacks on this port, so the ssh port (22) is best not enabled on the WAN direction.

The ip protocol icmp accept says we take icmp packets from anywhere, these are mainly used for testing the internet, so do things like ping packets - note this only works on IPv4 packets. During testing you probably want this, but do you want ping responded to on the WAN. The ip6 nexthdr ipv6-icmp accept does exactly the same on IPv6 packets, this is needed for things like Neighbour Discovery Protocol - needed for stateless addresses to be set. However an important point with these two command, although the table acts on inet and this means both IPv4 and IPv6, internal roles can be set to work on one or the other.

And finally the iiftype ppp drop just says that anything from the WAN, that we haven't yet accepted, we should drop. So its setting up the firewall. Note that with the icmp packets, that if we wanted to drop these on the WAN, we could just remove the icmp tests - as on the LAN all packets have already been accepted.

So this gives the full script, saved in /etc/nftables.conf as:

flush ruleset
table ip nat {
	chain prerouting {
		type nat hook prerouting priority 0; policy accept;
		tcp dport 12345 dnat to 192.168.2.111:ssh
	}

	chain postrouting {
		type nat hook postrouting priority 100; policy accept;
		oiftype ppp masquerade
	}
}
table inet filter {
	chain input {
		type filter hook input priority 0; policy drop;
		ct state { related, established } accept
		ct state invalid drop
		iiftype != ppp accept
		tcp dport ssh accept
		ip protocol icmp accept
		ip6 nexthdr ipv6-icmp accept
		iiftype ppp drop
	}

	chain forward {
		type filter hook forward priority 0; policy drop;
		ct state { related, established } accept
		ct state invalid drop  
		iiftype != ppp accept
		tcp dport ssh accept
		ip protocol icmp accept     
		ip6 nexthdr ipv6-icmp accept
		iiftype ppp drop
	}

	chain output {
		type filter hook output priority 0; policy accept;
	}
}

The flush ruleset should be explained, its not part of the rules loaded into the kernel, but its an instruction to the kernel to clear out all existing rules. This is needed, as otherwise when adding the rules they would add to whatever is already in the kernel; but we wish the rules loaded from the file to be complete. There is another advantage to doing flush ruleset in the file with the rules, nft -f does an atomic replacement of the rules, e.g. all packets are processed either by the prexisting rules, or by the new rules, there is never a time when the old rules have been removed, but the new rules are not in place. Hence this is the safe way of replacing rules.

Finally how is this loaded into the kernel. The easiest way is to add a lines to /etc/rc.local - nft -f /etc/nftables.conf. This file is run at boot, if first ensures the rules are empty (good for if the script is run twice) and then loads the nft rules from the file just set up. You can check it has been loaded correctly using nft list ruleset, which will take configuration loaded into kernel and decompile it into the c style of code.

Reflections

And so some reflections on using nftables, why use it over iptables. Well it some ways its a choice, of which there are many , e.g {uClibc|musl libc} or {busybox|toybox}, this is the same {iptables|nftables}. Both do similar things, and I've used both professionally. For me, I prefer nftables and for me its the c like script that is used to set up the tables - I find this far more readable than the command line that you have to use in iptables. Now this doesn't say you should use nftables, but you have a choice between iptables and nftables.