nftables: The Modern Netfilter Framework Every Network Engineer Should Know
iptables is still around, still works, and still gets copy-pasted into new scripts in 2026. But it has been effectively frozen in maintenance mode for years; nftables is the actual default firewall framework on every major distribution now, with a cleaner syntax, native set/map support, and meaningfully better performance at scale. If you have not sat down and learned it properly, the iptables muscle memory will actively work against you.
This post covers the nftables model from first principles, aimed at engineers coming from either an iptables background or a FortiGate policy background.
Why nftables Replaced iptables
iptables evaluates rules linearly — every packet walks the full chain, rule by rule, until something matches. With a few hundred rules this is fine. With thousands of rules (a large NAT table, a long blocklist), it becomes a measurable performance problem, because the cost is O(n) per packet.
nftables introduces native sets and maps — hash-table-backed lookups that turn “does this IP match any of these 10,000 addresses” into an O(1) operation instead of 10,000 sequential rule evaluations. This alone is the single biggest practical reason to migrate anything with a large rule count.
Beyond performance, nftables unifies what used to be four separate tools (iptables, ip6tables, arptables, ebtables) into one framework with one consistent syntax across IPv4, IPv6, ARP, and bridging.
The Model: Tables, Chains, Rules
table (address family + namespace)
└── chain (ordered list of rules, optionally hooked into netfilter)
└── rule (match + statement)
Tables
A table is a namespace tied to an address family:
nft add table inet filter
inet is the family that matches both IPv4 and IPv6 in one table — this alone removes the duplicate-everything-twice burden that iptables/ip6tables always had. Other families: ip (IPv4 only), ip6 (IPv6 only), arp, bridge, netdev.
Chains
Chains hold rules. A chain can be a base chain (hooked into a netfilter point, so packets actually flow through it) or a regular chain (only reached via an explicit jump/goto from another chain — useful for organizing related rules).
nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }
Base chain parameters:
type—filter(allow/drop decisions),nat(address translation),route(policy routing decisions)hook—prerouting,input,forward,output,postrouting— same five netfilter hook points as iptablespriority— determines ordering when multiple base chains share a hook; lower numbers run firstpolicy— the default verdict (acceptordrop) for packets that fall through without matching any rule
policy drop here is the nftables equivalent of the classic “default deny, explicit allow” firewall posture — and just like a misordered FortiGate policy table or a misordered chrony allow-list, rule order within a chain still matters: nftables evaluates rules top-to-bottom and stops at the first match.
Building a Basic Firewall
#!/usr/sbin/nft -f
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow established/related traffic — almost always rule #1
ct state established,related accept
# Allow loopback
iif lo accept
# Allow ICMP (ping, PMTUD-relevant ICMP types)
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# Allow SSH from a specific management subnet only
ip saddr 192.168.100.0/24 tcp dport 22 accept
# Allow established inbound HTTPS
tcp dport 443 accept
# Everything else is dropped by the chain policy
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
}
chain output {
type filter hook output priority 0; policy accept;
}
}
Apply it:
nft -f /etc/nftables.conf
The ct state established,related accept rule near the top is the single most important line in almost any nftables ruleset — it lets return traffic for connections you initiated flow back without needing a matching rule for every possible response, exactly like a FortiGate firewall policy’s implicit session table behaviour.
Sets: The Performance Win
A set is a named collection of values that rules can test membership against in O(1):
# Define a set of trusted management IPs
nft add set inet filter trusted_mgmt { type ipv4_addr \; }
nft add element inet filter trusted_mgmt { 192.168.100.10, 192.168.100.11, 192.168.100.12 }
# Reference it in a rule
nft add rule inet filter input ip saddr @trusted_mgmt tcp dport 22 accept
Adding or removing addresses from trusted_mgmt does not require touching the rule itself — append or remove set elements directly:
nft add element inet filter trusted_mgmt { 192.168.100.13 }
nft delete element inet filter trusted_mgmt { 192.168.100.10 }
Anonymous sets (inline, one-off)
nft add rule inet filter input tcp dport { 22, 80, 443, 8080 } accept
The { } syntax here creates an anonymous set inline — useful for a one-off multi-value match that does not need to be referenced or updated elsewhere.
Sets with a timeout (auto-expiring entries — useful for rate limiting / temporary blocks)
nft add set inet filter blocklist { type ipv4_addr\; flags timeout\; }
nft add element inet filter blocklist { 203.0.113.55 timeout 1h }
An IP added to blocklist automatically drops out of the set after the timeout expires — no cron job or cleanup script required.
Maps: Key-Value Lookups in a Rule
A map associates a key with a value, letting one rule encode logic that would otherwise need many separate rules:
# Map destination port to a verdict
nft add map inet filter port_verdict { type inet_service : verdict \; }
nft add element inet filter port_verdict { 22 : accept, 23 : drop, 3389 : drop, 443 : accept }
nft add rule inet filter input tcp dport vmap @port_verdict
This single rule replaces what would otherwise be four separate tcp dport X accept/drop rules — and adding a fifth port/verdict pair is a map update, not a new rule.
Maps for NAT — a common real pattern
nft add map ip nat dnat_map { type ipv4_addr : ipv4_addr \; }
nft add element ip nat dnat_map { 203.0.113.10 : 10.0.0.10, 203.0.113.11 : 10.0.0.11 }
nft add rule ip nat prerouting dnat to ip daddr map @dnat_map
One rule handling DNAT for an arbitrary number of public-to-private IP mappings, all driven by the map’s contents.
NAT in nftables
table ip nat {
chain prerouting {
type nat hook prerouting priority -100;
# DNAT: forward external port 8443 to an internal host's 443
tcp dport 8443 dnat to 10.0.0.50:443
}
chain postrouting {
type nat hook postrouting priority 100;
# Masquerade outbound traffic from the LAN
ip saddr 192.168.1.0/24 oif "eth0" masquerade
}
}
This is the direct nftables equivalent of iptables -t nat -A PREROUTING ... / -A POSTROUTING ... -j MASQUERADE — same two hook points, same priority ordering concerns, cleaner syntax.
Logging
nft add rule inet filter input ip saddr @blocklist log prefix "blocked: " drop
log is a statement, not a terminal verdict — it can be chained with drop/accept in the same rule, unlike iptables where LOG and the actual verdict needed to be two separate rules matching the same condition.
Migrating from iptables
The iptables-translate and iptables-restore-translate tools (from the iptables package on most distributions) convert existing rules directly:
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
# nft add rule ip filter INPUT tcp dport 22 accept
# Convert an entire saved ruleset
iptables-save > rules.v4
iptables-restore-translate -f rules.v4 > rules.nft
nft -f rules.nft
The translated output is a reasonable starting point but rarely the idiomatic nftables ruleset — it preserves the iptables structure (separate rules where a set or map would consolidate cleanly) rather than taking advantage of nftables’ actual strengths. Treat it as a working baseline to refactor, not a final migration.
Inspecting and Debugging
# List everything
nft list ruleset
# List one table
nft list table inet filter
# Monitor matched rules and trace evaluation live
nft monitor trace
# Per-rule packet/byte counters
nft add rule inet filter input counter ip saddr 203.0.113.55 drop
nft list table inet filter
Adding a bare counter statement to a rule is the simplest way to confirm whether a rule is actually being matched at all — invaluable when a rule appears correct but traffic still is not behaving as expected. The same nft monitor trace workflow extends naturally once nftables instances live inside namespaces: see the network namespaces post for how each namespace gets its own independent nftables ruleset, evaluated and traced exactly the same way.
Connecting to the Series
This post pairs directly with Linux VRFs — both are about isolating and controlling traffic on a single Linux host, one at the routing layer and one at the firewall layer. They compose: a VRF’s traffic still passes through the same shared nftables ruleset unless you explicitly match on the VRF interface or use a separate table scoped to it. For Linux VRFs, full isolation including independent firewall rules means reaching for a namespace instead.
What to Read Next
- Linux network namespaces — the next post in this series; namespaces use their own nftables instances, so the two topics are tightly paired
- Cilium — eBPF-based policy enforcement that replaces nftables/iptables in Kubernetes environments; understanding nftables first makes the motivation for replacing it clearer