Stateful firewalling, and why Cloudflare resisted it
Cloudflare’s network stack historically avoided Linux’s conntrack, the kernel’s stateful firewall facility. That choice simplified iptables rules, shaved a bit of latency off packet processing, and kept the inbound path easy to reason about. Stateless firewalls can only express coarse rules — allow SYNs to ports 80 and 443, drop everything else — which is not enough. Without tracking connection state you cannot tell a legitimate ACK from a port scan. So every OS ends up implementing connection tracking, and on Linux that subsystem is conntrack.
Conntrack maintains a table with at least six columns: protocol, source IP, source port, destination IP, destination port, and connection state. The maximum number of entries is a global setting, /proc/sys/net/nf_conntrack_max, but the limit applies per network namespace. On the author’s system that means each container can hold up to 256K flow entries.
The interesting question is what happens when a namespace exceeds that limit. The answer turns out to be less visible, and more aggressive, than a rule you can point to.
Testing conntrack in isolation
Testing conntrack used to demand dedicated hardware or elaborate VMs. Modern user namespaces change that: with unshare, an unprivileged user can get a network namespace with namespaced iptables and conntrack, letting you experiment without touching the host. One caution: raw sockets are not the right way to inject test packets, because the kernel treats SOCK_RAW sends differently from packets arriving on a real interface. A tun/tap device emulates a physical NIC more faithfully.
# Enable tun interface
ip tuntap add name tun0 mode tun
ip link set tun0 up
ip addr add 192.0.2.1 peer 192.0.2.2 dev tun0
ip route add 0.0.0.0/0 via 192.0.2.2 dev tun0
# Refer to conntrack at least once to ensure it's enabled
iptables -t raw -A PREROUTING -j CT
# Create a counter in mangle table
iptables -t mangle -A PREROUTING
# Make sure reverse traffic doesn't affect conntrack state
iptables -t raw -A OUTPUT -p tcp --sport 80 -j DROP
tcpdump -ni any -B 16384 -ttt &
...
./venv/bin/python3 send_syn.py
conntrack -L
# Show iptables counters
iptables -nvx -t raw -L PREROUTING
iptables -nvx -t mangle -L PREROUTING
Two details in that test script merit attention. First, conntrack does not activate merely because the kernel module is loaded. In the namespace world, conntrack starts tracking only when namespaced iptables rules reference it. Second, a rule in the mangle table with no target is legal iptables syntax that lets you read rule counters. Policy counters won’t work here because they only increment if a chain contains at least one rule.
That script sends ten SYNs to 127.0.0.1:80, then prints the conntrack table and iptables counters. With space available, the table records ten new flows, both raw and mangle prerouting rules see ten packets, and tcpdump confirms the SYNs arrived.
A full table drops packets implicitly
To see overfill behavior, reduce the host-side global to seven entries and rerun. Results diverge immediately. The raw prerouting chain still counts ten packets because it runs before conntrack. The mangle prerouting chain, which runs immediately after conntrack, counts only seven. The remaining three SYNs were hard-dropped by conntrack itself.

Sequence matters here: raw prerouting precedes conntrack, mangle prerouting follows it. The three missing packets never reached mangle because conntrack discarded them while creating new entries for the extra flows. There is no -j DROP rule involved, no configuration toggle, nothing to point to. Using conntrack at all means that an overfull table silently kills packets that would establish new connections.
Loose tracking amplifies the problem
Conntrack has two modes for out-of-order TCP traffic, governed by nf_conntrack_tcp_loose. The default, "loose," lets stray ACKs for unseen flows create table entries. That means "new flow" is not restricted to SYNs — any packet that would create a state entry gets dropped when the table is full.
Setting nf_conntrack_tcp_loose=0 does not cleanly fix this. The toggle is not settable per namespace, so testing requires root network namespace access. And even in strict mode, an ACK that does not create a flow is still dropped when the table is full; if space is available, the packet passes but is marked -ctstate INVALID from the mangle table onward.
When conntrack does not create state
Notifications about dropping SYNs at the firewall layer might seem harmless to flow tracking. They are not. Replacing the earlier raw/mangle rules with a single mangle rule that hits -j DROP changes the outcome: no conntrack entry is created for those SYNs at all. Even though conntrack saw them, the drop means the state table never records the flow.
User-visible failures: EPERM on sendto
A full conntrack table does not only affect inbound packets. Sending a UDP packet on a new flow can make sendto() return EPERM, behavior not documented in the man page. The same error can appear when an outbound packet is dropped in the OUTPUT chain via an explicit rule. In practice, treat EPERM from sendto() as transient if conntrack saturation is the cause, or permanent if an iptables rule is to blame. Even a raw socket’s send() fails with EPERM when conntrack is full.
Conntrack does not respect SYN-cookie mitigation
A listener that is correctly accepting connections shows the risk plainly. With the table size set to seven and an actual server on port 80, sending ten SYNs produces seven SYN+ACKs leaving the socket — one per conntrack entry — and the last three SYNs vanish before reaching the application. This matters in production: for publicly reachable ports guarded by conntrack, SYN-cookie mechanisms do not prevent conntrack exhaustion. A spoofed-source SYN flood still fills the state table with bogus entries even if the stack never completes the handshakes.
The practical guidance is to avoid conntrack on inbound connections using -j NOTRACK, or to enforce reasonable rate limits with drops before conntrack state is created. The cleanest future fix is triggering SYN cookies in a layer that runs ahead of conntrack, like XDP.
Conntrack has matured a great deal — throughput is no longer the objection it once was — but the failure modes remain sharp. A full table drops packets without asking, and an SYN flood can fill the table despite cookie-based defenses. Correctly scoped firewall rules need to account for both.



