nftables and the nf_tables Subsystem
nftables is the modern Linux packet-classification framework that replaces the four legacy tools —
iptables,ip6tables,arptables, andebtables— with one userspace tool (nft) and one in-kernel subsystem (nf_tables). Where iptables hard-codes a fixed set of matches and exactly one target per rule into a flat blob,nf_tablesis a small register-based virtual machine in the kernel: a rule is a sequence of expressions that load packet data into registers, compare them, and emit a verdict, all as bytecode the kernel evaluates (Wikipedia, nftables wiki). This VM design is what makes the framework’s headline features possible: named sets and maps that turn what would have been thousands of sequential iptables rules into a single hash- or tree-backed lookup, verdict maps that branch on a packet field in one step, concatenations for tuple matching, and atomic ruleset replacement with no global lock. nftables was merged into the mainline kernel in 3.13 (released 2014-01-19); on current distributions it is also the backend thatiptables-nftquietly drives (Red Hat Developer, 2020).
This note is verified against the Linux 6.12 LTS (2024-11-17) and 6.18 LTS (2025-11-30) source trees; the VM evaluation loop nft_do_chain() is structurally identical in both, so claims are pinned to “the 6.x LTS line” unless a version is named. The nft userspace syntax is described against the nft(8) man page.
Mental Model
Think of nf_tables as a bytecode interpreter that runs once per packet per hook. Each chain holds a compiled program; the kernel walks the program’s rules, and each rule is a little straight-line sequence of expressions operating on a small register file. An expression either loads something into a register (a TCP destination port, a source address, a conntrack state), tests a register against a constant or a set, or acts (writes a packet field, sets a verdict). When a rule produces a terminal verdict the walk stops; otherwise it continues to the next rule. The radical simplification versus iptables is that the match types are not baked into the engine — they are just expressions, so a new protocol field can be supported by teaching userspace a new expression without recompiling the kernel.
flowchart TB PKT["Packet (struct sk_buff)<br/>arrives at a netfilter hook"] --> CHAIN subgraph CHAIN["base chain → nft_do_chain()"] direction TB subgraph RULE["one rule = sequence of expressions"] direction TB E1["payload expr<br/>load tcp dport → reg"] E2["lookup expr<br/>reg ∈ @set ? (hash/rbtree)"] E3["verdict expr<br/>write accept/drop/jump → verdict reg"] E1 --> E2 --> E3 end REGS[("register file<br/>nft_regs: 16×u32 data<br/>+ 1 verdict reg")] E1 -.->|writes| REGS E2 -.->|reads| REGS E3 -.->|writes verdict| REGS end CHAIN -->|"verdict reg = ACCEPT/DROP/..."| OUT["return verdict to netfilter"]
The nf_tables virtual machine. What it shows: a rule is not “matches + one target” but an ordered list of expressions that read from and write to a shared register file (struct nft_regs: 16 four-byte data registers plus a verdict register), with a lookup expression able to test a register against a whole set in one operation. The insight to take: because matching is a load-then-compare over registers rather than a fixed callback, one expression can consult a hash table or red-black tree of millions of entries in ~O(1)/O(log n) — collapsing what iptables would express as a linear chain of millions of rules into a single lookup.
The In-Kernel Virtual Machine: How nft_do_chain Evaluates a Packet
The evaluation core is nft_do_chain() in net/netfilter/nf_tables_core.c, verified identical in structure in v6.12 and v6.18. Its skeleton:
/* simplified from net/netfilter/nf_tables_core.c, v6.12 (identical in v6.18) */
struct nft_regs regs; /* the VM's register file */
struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];
bool genbit = READ_ONCE(net->nft.gencursor); /* which generation is live */
do_chain:
blob = genbit ? rcu_dereference(chain->blob_gen_1)
: rcu_dereference(chain->blob_gen_0); /* double-buffered ruleset */
rule = (struct nft_rule_dp *)blob->data;
next_rule:
regs.verdict.code = NFT_CONTINUE;
for (; !rule->is_last; rule = nft_rule_next(rule)) { /* linear over rules */
nft_rule_dp_for_each_expr(expr, last, rule) { /* over a rule's exprs */
... expr_call_ops_eval(expr, ®s, pkt); /* run one expression */
if (regs.verdict.code != NFT_CONTINUE)
break; /* expr set a verdict */
}
switch (regs.verdict.code) {
case NFT_BREAK: regs.verdict.code = NFT_CONTINUE; continue; /* rule failed, next */
case NFT_CONTINUE: continue; /* rule passed, next */
}
break; /* terminal verdict */
}
switch (regs.verdict.code & NF_VERDICT_MASK) {
case NF_ACCEPT: case NF_QUEUE: case NF_STOLEN: return regs.verdict.code;
case NF_DROP: return NF_DROP_REASON(...);
}
switch (regs.verdict.code) {
case NFT_JUMP: jumpstack[stackptr++].rule = nft_rule_next(rule); fallthrough;
case NFT_GOTO: chain = regs.verdict.chain; goto do_chain; /* descend into chain */
}
... /* pop jumpstack or fall through to base-chain policy */The mechanics, expression by expression:
- A register file, not a fixed match struct.
struct nft_regs(ininclude/net/netfilter/nf_tables.h) is aunionofu32 data[NFT_REG32_NUM]and astruct nft_verdict.NFT_REG32_NUMis 20, of which 16 are general data registers and the rest reserved. The UAPI header explains the history: “nf_tables used to have five registers: a verdict register and four data registers of size 16. The data registers have been changed to 16 registers of size 4” — oldNFT_REG_1..4map onto the newNFT_REG32_00..15for compatibility. Every expression reads its inputs from and writes its outputs to these registers. - Expressions are the bytecode ops.
nft_rule_dp_for_each_exprwalks the rule’s expressions;expr->ops->eval(expr, ®s, pkt)runs each. A payload expression copies bytes from the packet (tcp dport) into a register; a meta expression loads metadata (iif,mark); a cmp expression compares a register to a constant; a lookup expression tests a register against a set; an immediate expression writes a verdict. Hot expressions are special-cased inline for speed (nft_cmp_fast_eval,nft_bitwise_fast_eval,nft_payload_fast_eval) before the indirectexpr_call_ops_evalis used. - Verdicts drive control flow. The verdict register holds a code.
NFT_CONTINUEmeans “rule’s expressions all passed, go to the next rule”;NFT_BREAKmeans “an expression failed, abandon this rule” (reset to continue and move on);NF_ACCEPT/NF_DROP/NF_QUEUE/NF_STOLENare terminal netfilter verdicts that return immediately;NFT_JUMP/NFT_GOTOdescend into another chain (jump pushes a return address ontojumpstack, goto does not);NFT_RETURNpops back. - The chain policy applies if the walk falls off the end of a base chain — exactly like an iptables built-in chain’s policy.
Crucial honesty about complexity: nft_do_chain’s outer loop is itself a linear for (; !rule->is_last; rule = nft_rule_next(rule)) walk. A chain that is a long flat list of N sequential rules is traversed O(N) per packet, just like iptables. nftables does not magically make rule lists sub-linear. The algorithmic win comes from a different place — see the next section.
Where the O(1) Actually Comes From: Sets, Maps, and Verdict Maps
The single most important performance idea in nftables is that you stop expressing per-value rules and express one rule that consults a data structure. Suppose you want to allow 50 000 source IPs. In iptables that is (close to) 50 000 rules, walked linearly — O(n) per packet. In nftables it is one rule with a lookup expression against a named set of 50 000 elements, and the set is backed by a hash table or red-black tree, so the test is ~O(1) (hash) or O(log n) (tree) (nftables wiki, Sets).
- Anonymous sets live inline in a rule (
tcp dport { 22, 80, 443 }) and die with the rule; the kernel still backs them with a real data structure, not a rule expansion. - Named sets are standalone, named, persistent, and updatable at runtime (
add element …) without touching the rule that references them via@name. This is what makes nftables sets a live dataplane primitive (e.g. a fail2ban-style dynamic blocklist updated thousands of times a second). - Set backends. The kernel chooses an implementation from the element type and flags: a hash for exact-match scalar sets, a red-black tree (
nft_set_rbtree) forintervalsets (CIDR ranges), and pipapo (nft_set_pipapo.c— “PIle PAcket POlicies”, by Stefano Brivio of Red Hat, 2019-2020) for the hard case of concatenations of ranges. The choice is automatic; you just declare the set. - Maps associate a key with a value —
tcp dport map { 80 : jump http_chain, 443 : jump https_chain }looks up the port and yields a value in one step. - Verdict maps (
vmap) are maps whose value is a verdict, so a single statement both selects and acts:meta iifname vmap { "eth0" : jump wan, "eth1" : jump lan }branches on the input interface in one O(1) lookup instead of a chain ofif iif == X jump. This is how production nftables firewalls dispatch — one vmap replaces dozens of sequential comparison rules. - Concatenations form tuples:
ip saddr . tcp dportis a single composite key you can match against a set or map of(address, port)pairs. Concatenations have been supported since kernel 4.1, with “nearly O(1)” lookup (nftables wiki, differences) — and pipapo is the set backend that makes ranged concatenations efficient.
So the correct statement is: nftables is not unconditionally faster than iptables for a flat rule list; it is faster because it gives you the tools to express the same policy as a constant-time lookup instead of a linear scan. A naively translated iptables ruleset (rule-for-rule) gains little; a redesigned one using sets/maps/vmaps gains enormously.
Atomic Ruleset Replacement — and the Generation Cursor
iptables (legacy) updates the kernel by re-uploading the whole table blob and serialising on /run/xtables.lock. nf_tables does transactional, atomic, lock-free-to-readers updates, and the mechanism is visible right in nft_do_chain: each chain carries two rule blobs, chain->blob_gen_0 and chain->blob_gen_1, and a per-net generation cursor net->nft.gencursor selects which is live. A ruleset update builds the new blob in the inactive slot, then atomically flips the cursor; in-flight packets are reading the old generation under rcu_dereference and finish on it, while new packets pick up the new generation. There is no moment where a half-applied ruleset is visible, and no global file lock — updates are per-network-namespace and incremental.
From userspace this is the nft -f file transaction: nft batches all the changes in a file (or stdin) into one netlink transaction that either fully commits or fully rolls back (nft(8)). The benefits the kernel API confers — “atomic rules updates, per-network-namespace locking, no file-based locking, fast incremental updates” — are exactly the ones iptables-nft inherits by speaking this same API (Red Hat Developer).
The nft Syntax: Families, Tables, Chains, Rules
nft unifies the four legacy tools by making the address family an explicit dimension (nft(8)):
ip— IPv4.ip6— IPv6.inet— both at once (a single base chain filters IPv4 and IPv6, eliminating the iptables/ip6tables script duplication).arp— ARP (replaces arptables).bridge— L2 frames on a bridge (replaces ebtables).netdev— ingress/egress on a specific device, the earliest attach point.
Unlike iptables, there are no built-in tables or chains — nothing is registered until you create it, so an empty nftables has zero overhead at the hooks (nftables wiki, differences). You declare a table (a container for chains, sets, and stateful objects, namespaced by family), then chains. A base chain attaches to the stack with a type, hook, priority, and policy; a regular chain has none of these and exists only as a jump target.
- Chain types:
filter(accept/drop, all families/hooks),nat(address translation; onlyip/ip6/inet, on the prerouting/input/output/postrouting hooks), androute(re-route locally generated packets if a routing key changed;outputonly). - Standard priorities (named, mapping to the integers iptables tables used implicitly):
raw(−300),mangle(−150),dstnat(−100),filter(0),security(50),srcnat(100). Lower runs first.
A rule is “zero or more expressions followed by one or more statements,” and — unlike iptables — a single rule can take multiple actions by chaining statements (nftables wiki, differences). Statements split into terminal (accept, drop, reject, queue, return — end evaluation) and non-terminal (log, counter, limit, meta mark set, dnat, snat — continue). Counters are opt-in: nftables does not maintain per-rule counters unless you add a counter statement, avoiding the always-on cost iptables pays.
Configuration Example with Line-by-Line Commentary
A complete, idiomatic inet firewall in native nft syntax, exercising sets, a verdict map, and a port-forward:
#!/usr/sbin/nft -f
# Atomic load: nft applies this whole file as one transaction.
flush ruleset # start clean (atomic with the rest of the file)
table inet my_fw { # one table covers BOTH IPv4 and IPv6 (inet family)
set allowed_tcp { # a NAMED SET of allowed destination ports...
type inet_service # ...whose element type is a TCP/UDP port number
elements = { 22, 80, 443 } # backed by a hash table → O(1) membership test
}
set blocklist { # a dynamic blocklist of source addresses
type ipv4_addr
flags dynamic, timeout # elements can be added at runtime and auto-expire
timeout 1h # each element lives 1 hour unless refreshed
}
chain inbound {
type filter hook input priority filter; policy drop; # base chain; default DROP
iif "lo" accept # always allow loopback
ct state established,related accept # stateful: let replies back in
ip saddr @blocklist drop # one rule consults the whole set
tcp dport @allowed_tcp ct state new accept # one rule, one O(1) set lookup
# vmap: branch on input interface in a single lookup, not a chain of comparisons
meta iifname vmap { "eth0" : jump wan_in, "eth1" : jump lan_in }
log prefix "DROP-IN: " limit rate 5/minute # log (non-terminal) then fall to policy
}
chain wan_in { } # regular chains: jump targets only
chain lan_in { }
chain prerouting {
type nat hook prerouting priority dstnat; # NAT chain on the DNAT hook
tcp dport 8080 dnat to 10.0.0.5:80 # port-forward (one statement)
}
chain postrouting {
type nat hook postrouting priority srcnat;
oifname "eth0" masquerade # source-NAT outbound on eth0
}
}The teaching points: the inet table filters v4 and v6 in one place; @allowed_tcp and @blocklist are set lookups — each is one rule regardless of how many ports/addresses they hold; the vmap dispatches on interface in a single O(1) step where iptables would need a comparison rule per interface; and the entire file loads as one atomic transaction via nft -f. Compare this with the equivalent iptables ruleset, which needs separate iptables/ip6tables invocations, one rule per allowed port, and a fragile sequence of -A commands with no atomicity.
How iptables Maps Onto nf_tables: the Compat Path
On current systems the iptables command is usually iptables-nft, which speaks the nf_tables netlink API rather than the old setsockopt blob path (Red Hat Developer). Its rules are evaluated by the very nft_do_chain VM described above. But it does not rewrite your iptables matches into native nft expressions — instead the kernel module nft_compat (net/netfilter/nft_compat.c, by Pablo Neira Ayuso, 2012-2013) wraps a legacy struct xt_target/struct xt_match as an nf_tables expression. nft_target_eval_xt() and nft_match_eval() invoke the old xtables callbacks from inside the VM. So an iptables-nft rule is “an nf_tables rule whose one expression is an nft_compat wrapper around an xtables match/target” — it runs in the modern engine but reuses the legacy match code. This is distinct from iptables-translate, a userspace text-to-text tool that converts an iptables command line into native nft syntax for a human to review. The clean separation: same syntax, modern backend, legacy match code (iptables-nft) versus syntactic conversion to native nftables (iptables-translate). See iptables and x_tables for the legacy side in full.
Failure Modes and Common Misunderstandings
- “nftables is always faster than iptables” — false. A rule-for-rule translation of an iptables ruleset is not faster; both walk a linear rule list. The win is real only when you redesign the policy around sets/maps/vmaps. Quote the kernel:
nft_do_chain’s rule loop is linear. - Mixing legacy and nft. Running
iptables-legacy/x_tablesrules andnft/nf_tablesrules simultaneously means two kernel subsystems filter the same packets at the same hooks; verdict interleaving is order-dependent and surprising. The nftables wiki explicitly warns against it (wiki). - Priority collisions across subsystems. When
iptables-nft,firewalld, and hand-writtennftrules coexist, they register chains at hooks with priorities that determine ordering. Two base chains at the same hook and priority have undefined relative order — a real source of “my rule is bypassed” confusion. - Forgetting the chain has no policy unless it’s a base chain. Regular chains never drop on fall-through; only base chains have a
policy. Apolicy dropon a base chain plus an unreachable accept rule = everything dropped. - Set type mismatches. Declaring a set
type ipv4_addrand trying to add a CIDR range fails unless the set hasflags interval; the right backend (rbtree/pipapo) is only selected when you declare the interval flag. ct stateordering. As with iptables, the stateful-accept rule must precede the new-connection rules, or established replies get dropped by a later default-drop.
Alternatives and When to Choose Them
- vs x_tables: nftables is the strict successor — choose it for all new firewall work. iptables survives for legacy scripts and because
iptables-nftlets the old syntax ride the new backend. Red Hat has begun deprecating eveniptables-nftin favour of nativenftables. - vs XDP / eBPF: for line-rate drop/redirect (DDoS scrubbing, software load balancing), XDP runs an eBPF program in the driver before an sk_buff exists — far faster than any netfilter hook, but with a far more restrictive programming model. nftables is the right tier for policy (stateful firewalling, NAT); XDP for volume. nftables’ own flowtable offload bridges the gap for established forwarded flows (it short-circuits the forwarding path after the first packet).
- vs IPVS / nft
kube-proxymode: for Service load balancing at scale, the verdict-map/set design of nftables (now an officialkube-proxymode) gives O(1) Service lookup where iptables mode was O(n) — the Kubernetes view lives in Kubernetes MOC.
Production Notes
The clearest production validation of the set/map model is kube-proxy’s nftables mode, added precisely because iptables mode’s O(n) rule explosion melted down on large clusters; nftables maps give constant-time Service-to-endpoint lookup. firewalld switched its default backend to nftables years ago, so most desktop and server distros are already using nf_tables under a familiar UI. The dynamic-set primitive underpins runtime blocklisting tools (a set with flags dynamic, timeout updated from a userspace daemon), and flowtable hardware/software offload is used by routers to push established-flow forwarding off the slow path. The kernel keeps both x_tables and nf_tables compiled in for compatibility, but the trajectory across distributions, firewalld, and kube-proxy is unambiguous: nf_tables is the present and future of Linux packet classification, and the legacy tools are a compatibility shim over it.
Uncertain
Verify: which distributions default to native
nftablesvsiptables-nftvs deprecatingiptables-nftat the 2026 timeframe. This is a per-distro, per-release decision that keeps moving (Red Hat has been deprecatingiptables-nft). The kernel facts here (VM, sets, atomic replace, merge in 3.13) are verified against source; the distro-default claims rest on the Red Hat blog (2020) and Wikipedia and are not re-checked against 2026 release notes. To resolve: consult the target distro’s current release notes andnft --version/update-alternatives --display iptableson the actual system. uncertain
See Also
- iptables and x_tables — the legacy framework nftables replaces, and the
nft_compatpathiptables-nftdrives - The Netfilter Framework and Hooks — the five hooks base chains attach to, and priority ordering
- Network Address Translation NAT — the
nat-type chains,dnat/snat/masqueradestatements - Connection Tracking conntrack — the
ct statemachinery behind stateful nftables rules - XDP Express Data Path — the line-rate eBPF tier below netfilter
- UP: Linux Networking Stack MOC — §7 Netfilter