Skip to main content
RunBook Academy

LinuxXXV · Firewallsnftables stateful

nftables stateful filtering and conntrack

Intermediate⏱ ~12 minnftconntrack

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

Not yet marked complete on this device.

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

StateMeaning
NEWFirst packet of a connection (no existing entry)
ESTABLISHEDSubsequent packets of a known connection
RELATEDNew connection related to an existing one (e.g. FTP data)
INVALIDPacket that does not match any known connection and is not SYN
UNTRACKEDPacket 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:

StateDefault timeout
TCP ESTABLISHED5 days
TCP TIME_WAIT120 seconds
TCP CLOSE_WAIT60 seconds
TCP FIN_WAIT120 seconds
UDP30 seconds
ICMP30 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

  1. Q1. What is the right sysctl to set the maximum number of conntrack entries?

  2. Q2. A conntrack entry in TIME_WAIT is normal and should be allowed.

  3. Q3. Which of the following are valid conntrack states? Select all that apply.

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