If you’ve spent much time around the Linux networking stack, chances are you’ve heard of IPTables. If you haven’t, IPTables is the framework that decides what to do with incoming network packets. It can be used to set up everything from simple firewalls up until complicated stateful routers and NATs. First released in 1998, this venerable software package has powered many networks for a venerable two decades.
TunnelHound does lots of routing. IPTables would have been the easy, obvious choice for constructing the TunnelHound state machine. However, like a lot of legacy software, IPTables has its limits that can make it difficult to scale to the kinds of routing problems many organizations face today. In 2014, a new filtering package NFTables was merged into the Linux kernel meant to fix some of these limits. Six years later, it’s safe to say that NFTables is the future of packet filtering. Here’s what you need to know and why you ought to switch.
Differences between IPTables and NFTables
Before we start on the differences between NFTables and IPTables, it’s worth pointing out the two
are more similar than different. Both perform the same basic task – filtering packets that arrive
at a Linux host and deciding what to do with it. Both frameworks offer CLI tools to interact with
the framework (iptables
for IPTables and nft
for NFTables). As they say, the differences are in
the details, and, as we’ll see, these differences matter a lot when it comes to building performant
and scalable filtering applications.
Difference 1: Syntax
IPTables offers a basic command-line interface to interact with the kernel. Rules are created by
calling the iptables
command using a getopt_long()
based parser.
NFTables also allows you to specify rules on the command line, but with its own custom EDSL instead. This makes rule specifications more concise.
Here’s an example IPTables based rule to drop any packets coming in on port 80.
iptables -A INPUT -p tcp --destination-port 80 -j DROP
Here’s the corresponding NFTables rule.
nft add rule inet firewall filter tcp dport 80 drop
Some of these syntactic changes allow you to significantly reduce complexity. For example, suppose you want to disallow TCP ports for SSH, HTTP, and HTTPS. In IPTables, this would require you to make three separate rules 1. In NFTables, you can specify values as sets. This is not shorthand for creating multiple rules. Rather it creates one rule with multiple disjoint matchers.
nft add rule inet firewall filter tcp dport '{80, 443, 22}' drop
As another added bonus, you can specify multiple actions in the same rule. This functionality is
much more difficult than in IPTables and would require you to make another table and add a JUMP
action. For example, to both drop and count the dropped packets. For example, to accept traffic from
IP network 1.2.3.0/24
on port 25, count the number of packets received, and log new connections,
you can add one simple rule.
nft add rule inet firewall \
filter ip saddr 1.2.3.0/24 tcp dport 25 \
counter \ # Count all packets arriving at port 25
accept \ # Accept the packet too
counter ct state new \ # When the connection is new
log prefix "New SMTP connection" accept # Log it
In the rule above, there are three actions: counter
, accept
, and log
. The rule is evaluated
left-to-right, so all packets from the given source address to TCP port 25 trigger the counter
and
accept
rules, which count the packet and accept it, respectively. The latter part of the rule only
matches packets that represent new connections. If the connection is new, then the packet is logged
as well.
Difference 2: Complexity
NFTables is a lot simpler than IPTables. Whereas IPTables comes with many different table sets, one
for each protocol (iptables
, ip6tables
, arptables
, ebtables
, etc), NFTables has no built-in
tables. A major performance bottlenecks with IPTables was that executing rulesets for empty tables
actually took up a surprisingly large (read: non-zero) amount of time. This means that simply
loading IPTables in the kernel could slow down your networking stack.
NFTables on the other hand has no built-in tables. Instead, tables are constructed by the
administrator, and then “hooked” into various parts of the networking stack. For example, in the
command-line above, we blocked incoming TCP connections on port 80 by adding a rule to the INPUT
table, which is a pre-defined table provided by IPTables. The nft
command-line above assumed we
have a chain named filter
in the firewall
table of the inet
“family”. By itself, this
table and chain won’t do anything. It has to be added as a hook into the input part of the
networking stack.
For example, to add the tables above, we have to run the following nft
commands.
nft 'add table inet firewall'
nft 'add chain inet firewall filter { type filter hook input priority 0; }`
The hook part says that this chain is a filter
type chain will be called whenever an input packet
arrives. The best part is that before you add this chain, no extra execution time is spent in the
kernel when processing input packets.
Difference 4: Efficiency
In IPTables, the rule set is maintained as a binary blob. This blob gets read from the kernel,
manipulated, and written back atomically every time you invoke the iptables
command. If changing
rules often, this can be a problem, because the time complexity scales linearly with the size of the
rules. NFTables uses a linked-list based approach to store rules, so inserting and deleting new
rules takes constant time.
Difference 5: Upgrades
In IPTables, each rule was a representation of functionality provided by the kernel. For example,
the iptables
rule above to match incoming traffic on port 80 was translated into a kernel object
representing the rule “Match TCP traffic on port 80.” NFTables takes a much lower-level
approach. Instead of providing rules to the kernel, NFTables rules are compiled into a bytecode
format which can match on any field in the packet headers. This means that rule functionality is
actually determined in userspace. The corresponding NFTables rule above would be translated into
bytecode that said something like “examine the 16-bit field at offset XXX in the header and match if
it is 80 in network byte order,” where XXX is the offset of the destination port in the combined
TCP/IP header. This may seem more complex, but actually it simplifies things quite a bit.
Most importantly, in IPTables, if you wanted access to a new rule type, this would often require a
kernel upgrade. Although your iptables
command may support a rule, if your kernel version didn’t
match, you would be unable to use it. This could be a major problem for some implementations because
distributions often ship a several year-old kernel for stability reasons. You were stuck with the
choice to accept a potentially unstable kernel you had to build yourself, or to resign to not using
the new features.
With NFTables, however, this problem is gone. Simply updating the nftables
package is enough to
give you access to the latest features. This is good news to system administrators: kernel upgrades
necessitate downtime, or significant planning, whereas userspace changes are much more easily
accomplished.
Difference 6: IPv4 and IPv6 interoperability
In IPTables, rules for IPv4 and IPv6 had to be specified using two separate commands iptables
and
ip6tables
. NFTables lets you use the pseudo inet
protocol to write rules that correspond to
either protocol. For example, the nft
command:
nft add rule inet firewall filter tcp dport '{80, 443, 22}' drop
Drops TCP traffic on the given ports for both IPv4 and IPv6. To write a rule specific to one protocol, you can specify either protocol specifically.
nft add rule ip firewall filter tcp dport 80 drop
would only block HTTP traffic on IPv4.
nft add rule ip6 firewall filter tcp dport 443 drop
would correspondingly block HTTPS traffic on IPv6 only.
Difference 7: Sets
Suppose you wanted to create an automatic IP blacklisting service (like fail2ban) using IPTables. Every time you wanted to ban an IP, you would add another rule:
iptables -A INPUT -s banned.ip -j DROP
Because of the efficiency problems identified above, for large public-facing services, this banning procedure takes longer and longer the more addresses you add. Moreover, the networking performance degrades as more rules are added to the rule set 2.
As stated above, NFTables allows O(1) rule insertion, but if that were all, it would still suffer from the linear-scaling rule execution time.
Fortunately, NFTables provides sets, a typed collection of objects. Because sets are based on hash tables, they add little overhead to our rules. We began to approach sets above when we specified filters that matched more than one port. For example, in the command
nft add rule inet firewall filter tcp dport '{80, 443, 22}' drop
the part in braces represents an anonymous set representing three ports. NFTables adds the ability to name sets, and then add and remove elements of sets at runtime.
For example, to add a set named blacklisted_ips
that can contain IP addresses,
nft 'add set ip firewall blacklisted_ip4s { type ipv4_addr; }'
Now, we can add a rule to block any IP in the named set.
nft add rule ip firewall filter ip saddr @blacklisted_ip4s drop
Upon set creation above, the set is empty. Instead of adding a new rule whenever our service wanted to block an IP, we can now add elements to the set.
nft add element ip firewall blacklisted_ip4s 44.92.123.21, 123.231.132.213
Or remove them:
nft delete element ip firewall blacklisted_ip4s 44.92.123.21
No more linear scaling for repeated rules! NFTables rocks for scalability.
Difference 8: Maps!
If that were not amazing enough, NFTables extends sets yet again by supporting mappings. For
example, suppose you wanted to build a public-facing endpoint that internally routed traffic to
different hosts (with only internal addresses) based on the TCP port. Suppose your HTTP server is at
address 192.168.1.100
and your SSH server at 192.168.1.102
in your private address space. In
IPTables, you’d again have to write two separate rules for this source NAT. In NFTables, you can
just write a mapping (below, we’ve assumed you’ve created and hooked up the appropriate tables and
chains for
NAT).
nft add rule ip nat prerouting dnat tcp dport { 80: 192.168.1.100, 22: 192.168.1.102 }
Just like sets, we can even create named maps, and then modify elements at runtime. This would allow you to set up a rule like the above where you can change the address for each port at runtime (for example, when bringing hosts up and down).
nft 'add map ip nat porttoip { type inet_service: ipv4_addr; }'
nft 'add element ip nat porttoip { 80: 192.168.1.100 }'
nft 'add rule ip nat postrouting dnat tcp dport @porttoip'
Switching to NFTables
Most major distributions have support for NFTables. Some, like Debian, use NFTables by default already. Here are guides for various distributions:
- Ubuntu
- Arch
- Debian – default packet filtering framework starting from Buster
- Gentoo
- Fedora – already installed by default
The xtables-translate
commands let you translate iptables
/ip6tables
/ebtables
/etc rules into
nft
’s accepted format. Here’s an example from the NFTables wiki:
> iptables-translate -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
nft add rule ip filter INPUT tcp dport 22 ct state new counter accept
> ip6tables-translate -A FORWARD -i eth0 -o eth3 -p udp -m multiport --dports 111,222 -j ACCEPT
nft add rule ip6 filter FORWARD iifname eth0 oifname eth3 meta l4proto udp udp dport { 111,222} counter accept
If you have an existing iptables
ruleset installed, you can translate the entire ruleset:
> iptables-save > rules.txt # Save all iptables rules to a file
> iptables-restore-translate -f rules.txt # Translate all rules in the file to nftables
Note that if you decide to switch to nftables
it’s best to purge your iptables
rules and unload
the kernel module. Using both frameworks is possible, but can lead to strange bugs. You can use
iptables -X
to delete all non-default chains, and iptables -F
to reset all chains. Then unload
iptables
by using modprobe -r iptables
.
Conclusion
NFTables is the new kid on the block, with significant advantages over IPTables for packet filtering. What it lacks in years is more than made up for by the extended functionality, the ease of use, and the independence from the underlying kernel. Nevertheless, many users haven’t yet made the switch. We hope that this guide has inspired you to take NFTables for a spin!
More information
We only touched the surface of what NFTables is capable of in this post. Over the next few months, we’ll be posting more about how we use NFTables to enable certain features in TunnelHound. In the meantime, you can browse NFTables’s extensive documentation3.
We’d like to know what you think of NFTables? Questions? Are there advantages of IPTables we didn’t mention? Let us know on social media or reach out to us.
-
Unless you use the
multiport
matcher, but that’s limited to fifteen ports. Moreover, NFTables lets you do this with any kind of parameter, not just ports. For example, you can only accept certain ICMP requests:nft 'add rule inet firewall ip protocol icmp icmp type {echo-request, redirect} accept' nft 'add rule inet firewall ip protocol icmp drop
This example drops all ICMP packet types unless they are pings (
echo-request
) or port redirects (redirect
). ↩ -
This is because IPTables processes rules in order. The more rules in the ruleset, the longer the filtering will take ↩
-
Some cool features we have not even mentioned are concatenations, which extend maps even further; intervals, which simplify adding multiple elements to sets and maps; and stateful objects, which let you build more sophisticated counters and quota systems. ↩