LinuxXXV · Firewallsnftables chains
nftables chains, tables, and hooks in depth
What you'll learn
- Choose the right chain for a rule
- Use priorities to control rule evaluation order
- Distinguish base chains from regular chains
- Use jump-to-chain for complex rulesets
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09
Chains are the heart of nftables organisation. The right chain structure makes a ruleset readable and maintainable; the wrong structure makes it impossible to debug.
Base chains vs regular chains
A base chain is attached to a netfilter hook and is called automatically for every packet that hits the hook. A regular chain is a list of rules you can jump to from another chain.
# Base chain: attached to INPUT hook
nft add chain inet filter input { type filter hook input priority 0 \; }
# Regular chain: not attached, jump to it
nft add chain inet filter tcp_handling
The hook (type filter hook input) is what makes a chain a
base chain. Without a hook, the chain is regular.
Priority
Multiple base chains can attach to the same hook. The priority decides the order:
nft add chain inet filter input_first { type filter hook input priority -100 \; }
nft add chain inet filter input_default { type filter hook input priority 0 \; }
nft add chain inet filter input_last { type filter hook input priority 100 \; }
Lower priorities run first. Negative priorities are valid.
nftables gives the well-known xtables priorities symbolic
names. For the ip, ip6 and inet families these are the
values (man nft, “Standard priority names, family and hook
compatibility matrix”):
| Name | Value | Hooks | Purpose |
|---|---|---|---|
raw | -300 | all | before conntrack |
mangle | -150 | all | packet mangling |
dstnat | -100 | prerouting only | DNAT |
filter | 0 | all | standard accept/drop |
security | 50 | all | after filter |
srcnat | 100 | postrouting only | SNAT |
Most rules live in priority filter (0).
Write the name, not the number:
nft add chain inet nat prerouting { type nat hook prerouting priority dstnat \; }
nft add chain inet nat postrouting { type nat hook postrouting priority srcnat \; }
nft list ruleset prints them symbolically too, so the file
you write and the ruleset you read back agree. Numbers are
where this goes wrong: mangle is -150, not -200, and 100 is
srcnat, not security. A chain created at a number you
half-remembered still loads — it just runs at the wrong point
relative to conntrack and NAT, and the symptom is a rule that
“does nothing” for reasons nothing in the ruleset explains.
Jump to chain
A regular chain is useful for grouping rules:
nft add chain inet filter inbound_ssh
nft add rule inet filter inbound_ssh tcp dport 22 accept
nft add rule inet filter inbound_ssh log prefix "ssh-drop: "
nft add rule inet filter input ip saddr 10.0.0.0/24 jump inbound_ssh
The jump verdict sends matching packets to inbound_ssh.
A verdict of accept or drop in the jumped-to chain ends
processing. A verdict of return returns to the caller.
Common chain patterns
Allow-list by source
nft add chain inet filter trusted_input
nft add rule inet filter trusted_input tcp dport { 22, 80, 443 } accept
nft add rule inet filter trusted_input ip protocol icmp accept
nft add rule inet filter trusted_input drop
nft add rule inet filter input ip saddr 10.0.0.0/24 jump trusted_input
Service-specific chain
nft add chain inet filter web_in
nft add rule inet filter web_in tcp dport 80 accept
nft add rule inet filter web_in tcp dport 443 accept
nft add rule inet filter web_in drop
nft add rule inet filter input ct state new tcp dport { 80, 443 } jump web_in
Logging chain
nft add chain inet filter log_drop
nft add rule inet filter log_drop log prefix "nft-drop: " level warn
nft add rule inet filter log_drop drop
nft add rule inet filter input jump log_drop
Verdict precedence
In a chain, rules are evaluated in order. The first matching rule’s verdict wins. If no rule matches, the chain’s default policy applies.
| Verdict | Effect |
|---|---|
accept | Stop processing, accept the packet |
drop | Stop processing, silently drop |
reject | Stop processing, drop with ICMP error |
queue | Send to userspace (nfqueue) |
continue | Continue to next rule |
return | Return from jumped-to chain |
continue is useful for adding exceptions:
nft add rule inet filter input ip saddr 10.0.0.0/24 accept # management: accept all
nft add rule inet filter input tcp dport 22 accept # SSH from anywhere
nft add rule inet filter input tcp dport 80 accept # HTTP
nft add rule inet filter input drop # default
Chains in different tables
Tables are isolated, and there is no bridging between them. A
chain in the filter table cannot jump or goto a chain in the
nat table — those verdicts only reach regular chains inside the
same table.
What actually happens is simpler: each table registers its own
base chains on the hooks it cares about. When a packet reaches a
hook, netfilter runs every base chain registered on that hook,
in priority order, lowest first. accept in one base chain ends
that chain but does not end the hook — the packet still goes on to
the next base chain, which is free to drop it. Only drop and
reject are final. So nat and filter never call each other;
they each get a turn, and each one sees whatever the previous
chain left behind, such as a rewritten destination address.
The practical consequence: an accept in your nat table is not
a permission slip. If a base chain that runs later on the same
hook drops the packet, it is dropped.
# nat table for DNAT
nft add table inet nat
nft add chain inet nat prerouting { type nat hook prerouting priority -100 \; }
# filter table for accept/drop
nft add table inet filter
nft add chain inet filter forward { type filter hook forward priority 0 \; }
Both tables and chains are needed for full functionality.
Debug chains
To see what chain a packet would hit:
nft add rule inet filter input meta nftrace set 1
# Now packets get a tracing handle; use perf or bpf to capture
Or use nft -j list ruleset (JSON output) for machine
parsing.
Knowledge check
Knowledge check · 3 questions
Q1. What makes a chain a base chain?
Q2. Lower priority values run first in nftables.
Q3. Which of the following are valid nftables verdicts? Select all that apply.
Passing score: 75%. Answers are checked in this browser.