nftables Stateful Firewall with Connection Tracking - 夜莺博客

nftables Stateful Firewall with Connection Tracking

nftables replaced iptables as the Linux packet filtering framework and does the same job with a single ruleset, named tables and chains, and an integrated connection-tracking match. The stateful pattern is the core of any host firewall: accept traffic belonging to an established connection, accept related traffic such as ICMP errors, drop packets that conntrack considers invalid, and only then evaluate explicit port rules for new connections. This article builds that ruleset from scratch and explains the order of rules, which is where most mistakes live.

Rule Order Is the Whole Design

nftables evaluates rules in the chain top to bottom and stops at the first terminal verdict. The stateful pattern only works if the conntrack rules come first: if you write your port-accept rules above them, an inbound packet with a spoofed source port will be accepted regardless of connection state.

Create the Table and Chains

nft add table inet filter
nft add chain inet filter input   { type filter hook input priority 0 \; policy drop \; }
nft add chain inet filter forward { type filter hook forward priority 0 \; policy drop \; }
nft add chain inet filter output  { type filter hook output priority 0 \; policy drop \; }

Using inet gives you one table handling both IPv4 and IPv6, which is the main reason to prefer nftables over iptables in a dual-stack environment. A default drop policy is the only safe starting point.

The Stateful Rules, in Order

# 1. Loopback
nft add rule inet filter input iif lo accept

# 2. Established and related connections
nft add rule inet filter input ct state established,related accept

# 3. Drop invalid packets
nft add rule inet filter input ct state invalid drop

# 4. Only new connections reach the port rules
nft add rule inet filter input ct state new jump tcp_services
nft add rule inet filter input ct state new jump udp_services

Step 2 is the one that makes this a stateful firewall rather than a stateless one: return traffic for your outbound SSH session is accepted without any inbound port rule, because conntrack already knows the session exists. Step 3 rejects packets that do not belong to any known connection and are not a valid new connection — typically spoofed or out-of-window TCP segments.

Defining the Service Chains

nft add chain inet filter tcp_services
nft add rule inet filter tcp_services tcp dport { 22, 80, 443 } accept

nft add chain inet filter udp_services
nft add rule inet filter udp_services udp dport { 53, 123 } accept

Anchoring the new-connection jump on a SYN check is a refinement worth adding if you want to be strict about TCP:

nft add rule inet filter input meta l4proto tcp \
    tcp flags \& (fin|syn|rst|ack) == syn ct state new jump tcp_services

Making It Persistent and Inspectable

nft list ruleset                  # current live rules
nft -c -f /etc/nftables.conf      # syntax-check without applying
nft flush ruleset                 # start over

For persistence, keep the whole ruleset in /etc/nftables.conf and enable nftables.service; hand-entered rules are lost on reboot, and a firewall that disappears after a restart is worse than none because nobody notices.

Debugging

# Counters tell you which rule matched
nft add rule inet filter input ct state invalid drop counter
nft list ruleset | head -40

# Watch conntrack entries
conntrack -L | head
conntrack -E                       # live events
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

Attach counter to the rules you are unsure about and watch the numbers move. If the invalid-drop counter is climbing constantly, you usually have asymmetric routing or a load balancer rewriting addresses behind conntrack's back rather than an actual attack.

Common Pitfalls

  • Missing the loopback accept. Shell tools break in confusing ways and local database connections fail.
  • Stateful firewalling on a router with asymmetric paths. conntrack sees only half the flow and drops the reply — use notrack on those flows instead of fighting it.
  • Forgetting IPv6. With an inet table this is automatic; with separate ip/ip6 tables it is the most common accidental exposure, because the default v6 policy often stays accept.

Related on this site: nftables Migration: iptables and ipset to Sets for the iptables-to-nftables translation, fail2ban jail.local and nftables Backend Setup for the banaction that writes block rules into an nftables set, and Ubuntu netplan 配置:bond、bridge 与 VLAN 实战 for the host-side interface configuration.

原文链接:https://wiki.archlinux.org/title/Nftables