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:
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.
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.
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
.