LinuxXXV · Firewallsnftables stateful
nftables stateful filtering and conntrack
What you'll learn
- Describe how conntrack tracks connections
- Use ct state matches in nftables
- Tune conntrack timeouts for production
- Inspect the conntrack table
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
Stateful firewall rules depend on conntrack, the kernel subsystem that records active connections. Without conntrack, every packet is judged independently; with it, the firewall knows “this packet belongs to an established TCP connection” and lets it through without re-matching.
What conntrack tracks
For every packet the firewall sees, conntrack records a tuple:
protocol, src_ip, src_port, dst_ip, dst_port
For TCP, conntrack also tracks the connection state (NEW, ESTABLISHED, RELATED, etc.). For UDP and ICMP, conntrack uses timeouts to decide when a “connection” has ended.
The connection states
| State | Meaning |
|---|---|
| NEW | First packet of a connection (no existing entry) |
| ESTABLISHED | Subsequent packets of a known connection |
| RELATED | New connection related to an existing one (e.g. FTP data) |
| INVALID | Packet that does not match any known connection and is not SYN |
| UNTRACKED | Packet explicitly excluded from conntrack |
The standard pattern:
nft add rule inet filter input ct state established,related accept
nft add rule inet filter input ct state invalid drop
This allows traffic belonging to existing connections and drops everything else that does not look like a real new connection.
Inspect conntrack
conntrack -L # all entries
conntrack -L -p tcp --src 10.0.0.5 # specific protocol/source
conntrack -S # statistics
cat /proc/net/nf_conntrack # raw table
Output looks like:
tcp 6 60 TIME_WAIT src=10.0.0.5 dst=10.0.0.10 sport=51234 dport=22 src=10.0.0.10 dst=10.0.0.5 sport=22 dport=51234 [ASSURED] mark=0 use=1
Fields:
- protocol (tcp), state (TIME_WAIT).
- original direction: src to dst, sport to dport.
- reply direction: reversed.
- [ASSURED]: the entry has been confirmed by both sides.
- mark: a 32-bit value attached to the connection (used by policy routing).
Timeouts
Each conntrack entry has a timeout based on protocol and state:
| State | Default timeout |
|---|---|
| TCP ESTABLISHED | 5 days |
| TCP TIME_WAIT | 120 seconds |
| TCP CLOSE_WAIT | 60 seconds |
| TCP FIN_WAIT | 120 seconds |
| UDP | 30 seconds |
| ICMP | 30 seconds |
Tune via sysctl:
sysctl net.netfilter.nf_conntrack_tcp_timeout_established
sysctl net.netfilter.nf_conntrack_udp_timeout
sudo sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=7200
Reduce timeouts to free conntrack table slots on busy hosts.
sysctl -w changes only the running kernel; persist anything
you keep in /etc/sysctl.d/ (shown under table sizing below).
Conntrack table size
The conntrack table has a maximum size. When it fills, new connections are dropped (with a kernel log message).
sysctl net.netfilter.nf_conntrack_max
cat /proc/sys/net/netfilter/nf_conntrack_max
Default sizes:
nf_conntrack_max: 65536 (often too low for busy hosts).nf_conntrack_buckets: derived from max.
Measure before you size. Steady state is not the number that matters; the peak is, and on most hosts the peak is several times steady state:
conntrack -C # entries right now
sysctl net.netfilter.nf_conntrack_max # current ceiling
Size nf_conntrack_max to about twice the observed peak.
A host peaking near 130k connections therefore wants 262144.
Doubling rather than shaving a few percent on top is
deliberate: the failure mode is not gradual. There is no
degraded mode between “table has room” and “kernel drops every
new connection”, so the headroom has to absorb a traffic spike
or a retry storm, not just normal variance. An entry costs
roughly 300 bytes of unswappable kernel memory, so 262144
entries is on the order of 80 MB — cheap next to an outage.
sysctl -w sets the running value and nothing else. It is
gone at the next reboot, which is how a host that was tuned
during an incident quietly reverts and fails the same way six
months later. Write the values to a file:
cat <<'EOF' | sudo tee /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 262144
net.netfilter.nf_conntrack_tcp_timeout_established = 7200
EOF
sudo sysctl --system
Alert on conntrack -C against nf_conntrack_max and page at
80% — that is the only warning you get before the cliff.
Conntrack tools
# Statistics
conntrack -S
# Watch events (new, update, destroy)
conntrack -E
# Delete a specific entry
conntrack -D -p tcp --dport 22
# Delete all entries for an IP
conntrack -D -s 10.0.0.5
The -E event watcher is invaluable when debugging “why is
this connection getting dropped” - watch for entries being
destroyed unexpectedly.
Complex protocols (FTP, SIP)
Some protocols open secondary connections (FTP data channel, SIP media stream). conntrack recognises these with helpers:
Loading the module is no longer enough. Since kernel 4.7, automatic helper assignment is off by default — it was a security problem, because any connection to the helper’s well-known port got the helper attached and could open arbitrary RELATED holes through the firewall. Check the current state:
cat /proc/sys/net/netfilter/nf_conntrack_helper # 0 = auto-assignment off
0 is the expected modern value. With auto-assignment off you
must declare a helper object and assign it explicitly.
ct helper "name" on its own is a match on an
already-assigned helper, so a rule like ct helper ftp tcp dport 21 accept matches nothing and the FTP data channel is
still dropped. It parses cleanly, which is why the failure is
silent.
modprobe nf_conntrack_ftp
# /etc/nftables.d/ftp-helper.nft
table inet filter {
# Declare the helper object
ct helper ftp-standard {
type "ftp" protocol tcp
}
# ASSIGN it to the control connection. Assignment must run after the
# conntrack lookup completes, which the default filter priority gives you.
chain prerouting {
type filter hook prerouting priority filter;
tcp dport 21 ct helper set "ftp-standard"
}
chain input {
type filter hook input priority filter; policy drop;
# RELATED now covers the data channel the helper predicted
ct state established,related accept
}
}
Check it before loading it — nft -c -f parses the file and
reports errors without touching the running ruleset:
sudo nft -c -f /etc/nftables.d/ftp-helper.nft
sudo nft list ct helpers table inet filter # confirm the object exists
Modern best practice is to avoid complex protocol helpers altogether and use ALGs (Application Layer Gateways) or pin the protocol to predictable ports — FTP in passive mode with a configured port range, for example. A helper parses attacker- influenced payload in the kernel to decide what to let through; that is a bad trade unless the protocol leaves you no choice.
Conntrack exhaustion on busy hosts
When nf_conntrack_max is reached, the kernel logs
“nf_conntrack: table full” and drops new connections. The
host appears to have intermittent network failures.
Symptoms:
- New SSH connections fail but existing ones work.
- Web requests fail with “Connection reset by peer”.
- DNS lookups succeed (short-lived) but HTTP fails (long- lived).
Fix: raise nf_conntrack_max and tune timeouts.
Knowledge check
Knowledge check · 3 questions
Q1. What is the right sysctl to set the maximum number of conntrack entries?
Q2. A conntrack entry in TIME_WAIT is normal and should be allowed.
Q3. Which of the following are valid conntrack states? Select all that apply.
Passing score: 75%. Answers are checked in this browser.