26.4 nftables: The Modern Replacement for iptables
Alright, let’s talk about nftables. You’ve probably heard it’s the replacement for iptables, and you’d be right. It’s not just a new coat of paint; it’s a complete engine swap. The old iptables, ip6tables, arptables, and ebtables utilities were a tangled mess of four different tools trying to do four related but separate jobs. nftables replaces them all with a single, unified framework. It’s cleaner, more efficient, and frankly, a lot more sane.
The core idea is simple: you build your ruleset using the nft command, which talks to the kernel’s new packet classification subsystem. The syntax is vastly improved—it actually resembles a modern programming language instead of a string of arcane incantations passed to a binary. Less memorization of obscure flags, more logical construction of rules.
The Building Blocks: Tables, Chains, and Rules
Think of nftables in layers. At the top, you have tables. A table is simply a container for your chains, and it’s defined by which protocol family it operates on (ip, ip6, inet, bridge, arp, netdev). The inet family is a godsend—it allows you to write one set of rules that handles both IPv4 and IPv6 simultaneously. No more duplicating your entire firewall twice.
Within tables, you create chains. Chains are where the action happens. They can be base chains, which are the entry points for packets from the network stack (like input, output, forward), or non-base chains, which you use for jumping to for better organization (like a named subroutine).
Finally, chains contain rules. A rule is made up of two parts: an expression (the “if this”) and a statement (the “then that”). This is where the new syntax shines.
Let’s build a basic example. We’ll create an inet table (works for both IPv4/v6) and add a base chain to filter input traffic.
# Create the table
nft add table inet my_filter_table
# Create a base chain for input traffic with a default drop policy
nft add chain inet my_filter_table my_input_chain { type filter hook input priority 0 \; policy drop \; }
# Add a rule to allow established/related connections (CRITICAL)
nft add rule inet my_filter_table my_input_chain ct state established,related accept
# Add a rule to allow loopback traffic
nft add rule inet my_filter_table my_input_chain iifname "lo" accept
# Add a rule to allow incoming SSH (on port 22)
nft add rule inet my_filter_table my_input_chain tcp dport 22 accept
# List our new ruleset
nft list table inet my_filter_table
See? That’s readable. You can almost parse it like English: “add rule in table my_filter_table, chain my_input_chain: if TCP destination port is 22, then accept.”
Why It’s Actually Better
The real magic isn’t just the syntax; it’s the performance and flexibility. Under the hood, nftables rules are compiled into bytecode by the kernel. This means it can perform complex rule evaluation in fewer steps. While iptables checks a packet against every single rule one-by-one, nftables can often evaluate sets of rules simultaneously.
Speaking of sets, this is a killer feature. You can define named sets of IPs, ports, or other data, and use them across multiple rules. Updating the set automatically updates every rule that uses it. Need to ban a list of IPs? Don’t add 10 rules; add them to a set and reference it once.
# Create a named set for web ports
nft add set inet my_filter_table web_ports { type inet_service \; flags interval \; }
nft add element inet my_filter_table web_ports { 80, 443, 8080-8081 }
# Create a set of bad_ips to drop
nft add set inet my_filter_table bad_ips { type ipv4_addr \; flags timeout \; }
nft add element inet my_filter_table bad_ips { 192.0.2.100 timeout 5m }
# Use the sets in a rule
nft add rule inet my_filter_table my_input_chain ip saddr @bad_ips drop
nft add rule inet my_filter_table my_input_chain tcp dport @web_ports accept
The timeout flag is brilliant—the IP 192.0.2.100 will automatically be removed from the set after 5 minutes. Perfect for temporary blocks.
Common Pitfalls and How to Avoid Them
Locking Yourself Out: I’ve done it. You’ve done it. We’ve all done it. The classic mistake is setting a default
droppolicy on yourinputchain before adding a rule to allow SSH. The solution? Use a multiterminal session. Have one terminal session running annft monitor(to watch what’s happening) and another where you run your commands. Better yet, use a cron job or asystemdtimer to flush all rules and accept everything every 5 minutes while you test, then disable it once your rules are solid.Forgetting the Default Policy: The
policystatement in a base chain only applies to packets that traverse the entire chain without hitting a terminating verdict (accept,drop,reject). It’s your catch-all. Make itdropfor security, but only after you’ve explicitly allowed what you need.Mixing Old and New: You cannot use
iptablesandnftablesrules simultaneously on the same table family. The kernel uses one subsystem or the other. If you’re migrating, you need to fully disable the oldiptablesservice and fully enablenftables. Theiptables-nftcompatibility layer is there to help you translate legacy commands, but it’s a crutch for migration, not for long-term use.
The best practice? Ditch the legacy tools entirely. Learn the nft syntax. Write your rulesets in a config file and load them with nft -f /etc/nftables.conf. It’s more maintainable, more powerful, and it’s the future. The iptables ship has sailed, and it’s time to get on the modern, faster, and considerably less-maddening nftables boat.