Skip to main content
RunBook Academy

LinuxXXV · Firewallsnftables chains

nftables chains, tables, and hooks in depth

Intermediate⏱ ~12 minnft

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

Not yet marked complete on this device.

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”):

NameValueHooksPurpose
raw-300allbefore conntrack
mangle-150allpacket mangling
dstnat-100prerouting onlyDNAT
filter0allstandard accept/drop
security50allafter filter
srcnat100postrouting onlySNAT

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.

VerdictEffect
acceptStop processing, accept the packet
dropStop processing, silently drop
rejectStop processing, drop with ICMP error
queueSend to userspace (nfqueue)
continueContinue to next rule
returnReturn 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

  1. Q1. What makes a chain a base chain?

  2. Q2. Lower priority values run first in nftables.

  3. Q3. Which of the following are valid nftables verdicts? Select all that apply.

Passing score: 75%. Answers are checked in this browser.