conntrack is the unsung hero of modern firewalls, letting them remember who’s talking to whom so they don’t have to re-evaluate every single packet.

Let’s watch conntrack in action with nftables. Imagine a simple setup: we want to allow established and related connections, and then block everything else.

# First, flush existing rules and set up a basic table
nft flush ruleset
nft add table ip filter
nft add chain ip filter input { type filter hook input priority 0 \; policy accept \; }
nft add chain ip filter forward { type filter hook forward priority 0 \; policy accept \; }
nft add chain ip filter output { type filter hook output priority 0 \; policy accept \; }

# Now, the crucial part: allow established and related connections
nft add rule ip filter input ct state established,related accept
nft add rule ip filter forward ct state established,related accept
nft add rule ip filter output ct state established,related accept

# And finally, drop everything else that isn't explicitly allowed
nft add rule ip filter input ct state invalid drop
nft add rule ip filter forward ct state invalid drop
nft add rule ip filter output ct state invalid drop

# Let's also allow new connections for SSH (port 22) and HTTP (port 80)
nft add rule ip filter input tcp dport 22 ct state new accept
nft add rule ip filter input tcp dport 80 ct state new accept

# And for good measure, block all other new incoming traffic
nft add rule ip filter input ct state new drop

Here’s what’s happening:

  • nft add table ip filter: We create a table named filter for IPv4 traffic.
  • nft add chain ... input|forward|output: We define the standard chains for packet processing. input for packets destined for the local machine, forward for packets passing through, and output for packets originating from the local machine. priority 0 is the default, and policy accept means if no rule matches, the packet is accepted by default (we’ll override this with drops later).
  • ct state established,related accept: This is the core. ct state tells nftables to consult the connection tracking module (conntrack). established means the packet is part of an existing, already-approved connection. related means it’s a new connection that’s related to an existing one (like an FTP data connection). By accepting these, we don’t need to re-evaluate every single packet in a TCP handshake or a UDP stream.
  • ct state invalid drop: If conntrack deems a packet "invalid" (e.g., a TCP packet with no SYN flag that isn’t part of an established connection), we drop it. This is a common security measure.
  • ct state new accept: For new connections, we’re explicitly allowing them for specific services like SSH (port 22) and HTTP (port 80).
  • ct state new drop: Crucially, after allowing specific new connections, we drop all other new incoming connections. This creates a default-deny policy for new traffic.

The mental model is this: conntrack acts like a bouncer at a club. When a new connection arrives (state new), the bouncer checks the guest list (our nftables rules for new traffic). If it’s on the list (e.g., SSH), they get in. If not, they’re rejected. Once a connection is approved, conntrack notes it down. For subsequent packets from that same connection, the bouncer recognizes them (state established) and waves them through immediately, without checking the guest list again. If a packet arrives that doesn’t seem to belong to any known connection or isn’t a valid start to a new one (state invalid), it’s immediately kicked out.

The surprising thing about conntrack is how it handles state for protocols that aren’t inherently connection-oriented, like UDP or ICMP. For UDP, conntrack creates a "connection" the first time it sees traffic from a specific source IP/port to a destination IP/port. It then assumes subsequent UDP packets between those same endpoints are part of the same "connection" and marks them as established. This allows you to firewall UDP services without needing to allow every single ephemeral port that might respond.

The next concept you’ll grapple with is how conntrack interacts with NAT, specifically how it tracks connections that have had their IP addresses or ports rewritten.

Want structured learning?

Take the full Nftables course →