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:

  1. Ubuntu
  2. Arch
  3. Debian – default packet filtering framework starting from Buster
  4. Gentoo
  5. 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.

  1. 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). 

  2. This is because IPTables processes rules in order. The more rules in the ruleset, the longer the filtering will take 

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

Travis

Travis is on a mission to secure business networks