TTR Networks

Intermediate · nftables and iptables on Debian: host and router firewalls · section 2 of 10

Tables, chains and hooks

✓ Lab-tested 21 October 2026 on Debian 13.7, nftables 1.1.3, iptables 1.8.11

nftables has a small vocabulary. Once it makes sense, every ruleset reads the same way.

  • A table is a container for chains, sets and maps. It belongs to one family, which decides which packets it sees: ip (IPv4), ip6 (IPv6), inet (both at once, the usual choice), plus arp, bridge and netdev. A table's name is up to you.
  • A base chain is attached to a hook, a point on the packet's path through the kernel, and that's what makes packets enter it. The hooks you'll use: input (packets for this machine), forward (packets routed through it), output (packets it sends), and prerouting and postrouting for NAT. Chains without a hook are regular chains, reached only by jump.
  • A base chain has a type (filter or nat), a priority (when several chains share a hook, lower runs first; names like filter, dstnat and srcnat stand for the standard numbers), and a policy, the verdict for packets that reach the end of the chain: accept or drop.
  • A rule is a list of matches followed by a statement: tcp dport 22 accept, counter, log, drop. Rules are checked in order, and the first final verdict (accept, drop, reject) ends that chain.

Connection tracking

Almost every firewall starts with ct state established,related accept. conntrack remembers every flow the kernel has seen, so once a connection is allowed, its replies don't need rules of their own. Its states are new, established, related (for example an ICMP error about an existing connection) and invalid, which you drop.

One hook, several chains

A hook can have several base chains from different tables, and every one of them must let a packet through. accept only ends the current chain: the packet then meets the next chain on the same hook, and a drop anywhere is final. That rule explains most of the surprises when nftables, iptables, Docker and other tools share a machine.

Debian's default /etc/nftables.conf is just an empty inet filter table with three chains and no policy (so accept). On the lab machine, nft list ruleset also shows a table nobody wrote by hand: Docker added NAT rules for its internal DNS. Other software (Docker, fail2ban, libvirt) adds tables the same way.

Debian's defaults (real output)
root@fw:~# systemctl is-enabled nftables; cat /etc/nftables.conf
disabled
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
	chain input {
		type filter hook input priority filter;
	}
	chain forward {
		type filter hook forward priority filter;
	}
	chain output {
		type filter hook output priority filter;
	}
}

root@fw:~# nft list ruleset
# Warning: table ip nat is managed by iptables-nft, do not touch!
table ip nat {
	chain DOCKER_OUTPUT {
		ip daddr 127.0.0.11 tcp dport 53 counter packets 0 bytes 0 dnat to 127.0.0.11:37581
		ip daddr 127.0.0.11 udp dport 53 counter packets 0 bytes 0 dnat to 127.0.0.11:47288
	}

	chain OUTPUT {
		type nat hook output priority dstnat; policy accept;
		ip daddr 127.0.0.11 counter packets 0 bytes 0 jump DOCKER_OUTPUT
	}

	chain DOCKER_POSTROUTING {
		ip saddr 127.0.0.11 tcp sport 37581 counter packets 0 bytes 0 snat to :53
		ip saddr 127.0.0.11 udp sport 47288 counter packets 0 bytes 0 snat to :53
	}

	chain POSTROUTING {
		type nat hook postrouting priority srcnat; policy accept;
		ip daddr 127.0.0.11 counter packets 0 bytes 0 jump DOCKER_POSTROUTING
	}
}
  • Line 2: The service is installed but not enabled.
  • Line 5: Loading this file first removes every rule, from every program.
  • Lines 7–17: One inet table (IPv4 and IPv6), three base chains on the input, forward and output hooks, no rules, no policy: accept everything.
  • Lines 20–41: Rules nobody here wrote: Docker's NAT for its internal DNS, added through iptables-nft. Note the warning on the first line.

Check your understanding

A packet is accepted by a chain on the forward hook in one table. Another table also has a chain on the forward hook that drops it. What happens?

  • It's dropped: every chain on the hook must let it through.
  • It's accepted, because accept came first.
  • It depends on which table was created first.
  • nftables refuses to load two chains on the same hook.