OPNsenseIII · Stateful Firewalling and PFStateful firewalling and PF
Stateful vs stateless filtering
What you'll learn
- Define stateful and stateless packet filtering and the differences in operational behaviour
- Describe what state PF tracks and what it does not
- Explain why stateful filtering is the default but not the answer to every problem
- Identify situations where a stateless rule is the correct production choice
Prerequisites
Verified against OPNsense 25.x · FreeBSD 14.x · PF (FreeBSD packet filter) FreeBSD 14.x · Unbound 1.20+ · Kea DHCP OPNsense 25.x plugin · WireGuard in-kernel + OPNsense plugin · strongSwan (IPsec plugin) OPNsense 25.x plugin · OpenVPN 2.6.x · Suricata 7.x · 2026-08-14
Every OPNsense firewall runs PF, the FreeBSD packet filter, and PF is stateful by default. That word — stateful — is on every firewall vendor’s marketing page, and it is also the single most common source of operator confusion. Stateful filtering is not “smarter” filtering; it is filtering that remembers the packets it has seen. The memory — the state table — is what makes the modern firewall possible, and it is also what makes it fragile under load, asymmetric paths, and ICMP-heavy traffic.
This lesson covers what stateful means, what PF records, what it does not record, and where the right answer is to disable state tracking entirely.
Stateful filtering, defined
A stateless firewall looks at each packet in isolation and asks “does this packet match a rule?”. The decision does not depend on any other packet. A stateful firewall maintains a memory of recent packets — the state table — and asks “is this packet part of an existing flow? If yes, allow. If no, evaluate the rules.”
The stateful check is faster (the state-table lookup is a hash match; rule evaluation is a linear walk) and it is what makes rules like “allow LAN → any” return-traffic-permissive without writing a separate inbound rule.
PF state covers:
- The five-tuple: source IP, source port, destination IP, destination port, protocol.
- The direction the first packet took (interface, gateway).
- The translated addresses, if NAT was applied.
- The packet and byte counters in each direction.
- A timeout that controls when the state is forgotten.
$ pfctl -s state | head -10all tcp 192.0.2.50:51820 <- 203.0.113.50:443 ESTABLISHED:ESTABLISHED
all tcp 192.0.2.50:51820 -> 203.0.113.50:443 ESTABLISHED:ESTABLISHED
all udp 192.0.2.50:53 <- 8.8.8.8:53 SINGLE:NO_TRAFFIC
all udp 192.0.2.50:53 -> 8.8.8.8:53 SINGLE:NO_TRAFFIC
all icmp 192.0.2.50:1289 <- 8.8.8.8:1289 0:0
all icmp 192.0.2.50:1289 -> 8.8.8.8:1289 0:0Illustrative output
What stateful filtering buys you
Three operational properties follow from the state table:
- Return traffic is permitted without an inbound rule. A rule that permits outbound TCP from the LAN creates state on the SYN; the matching return packets come back as state-matched and are allowed without a separate inbound rule. This is what makes the standard “LAN → any” rule work.
- Out-of-order or unsolicited inbound traffic is rejected. A SYN with no matching state is rejected. PF refuses to allocate state for unsolicited inbound traffic.
- The firewall is fast in the steady state. The state lookup
is
O(1)in the typical implementation (a hash of the five-tuple). Rule evaluation isO(n)in the number of rules. Most packets in production match state and skip rule evaluation entirely.
What stateful filtering cannot do
The state table has limits. Knowing them is what separates an operator who can debug state from one who cannot.
State is per-interface. A state created on WAN-A is bound to WAN-A. If return traffic arrives on WAN-B (asymmetric routing), the state does not match and the packet is dropped. This is the lesson on asymmetric routing in depth.
State is best-effort. PF allocates state from a pool. If the pool is exhausted (state-table full), PF cannot allocate new state and falls back to rule evaluation — and rule evaluation typically rejects the packet. State exhaustion under load is a real production failure mode; the lesson on state creation and the OPNsense tuning guide cover sizing.
State is not “intent”. PF state records that two endpoints exchanged a packet. It does not record why. A rule that permits a TCP connection permits any TCP connection matching the state — including connections whose payload the firewall should have inspected but did not. Application-layer inspection (a proxy or IDS) sits above PF state.
State does not survive across interfaces for asymmetric flows. Even with stateful rules, an asymmetric flow is dropped. There is no “let the return through because there is a state on the other interface” — PF requires the return to arrive on the same interface as the state.
When stateless is the right answer
PF supports a no state modifier on rules. With no state, PF
does not allocate a state entry for matching traffic — each
packet is evaluated against the rules individually. This is
useful for:
- High-volume, ephemeral flows that would exhaust the state table. Examples: DNS query floods, ICMP-heavy monitoring probes, certain broadcast traffic.
- Traffic the firewall must inspect on every packet because the protocol is stateful at a different layer (e.g. some routing protocols that PF would not interpret correctly anyway).
- Troubleshooting. A rule with
no statelets the operator see every packet that matches the rule in the log without waiting for state-table updates.
The cost of no state is loss of the three properties above
(return-traffic permitted, unsolicited blocked, fast lookup).
Use it deliberately.
The state table’s lifetime
A state entry exists from creation until one of:
- Both ends close the TCP connection — PF sees FIN or RST and removes the state.
- The state times out — the configured timeout expires with no matching traffic.
- An explicit clear —
pfctl -k stateorpfctl -F stateremoves it. - The firewall reloads — a config apply reloads PF and starts with an empty state table. (This is why every config apply drops all active connections.)
The fourth case is a critical operational fact. Every GUI apply, every API call that changes the firewall config, terminates every active TCP session that traverses the firewall. Plan maintenance windows accordingly.
Summary
- Stateful filtering means PF remembers packets and matches new packets against that memory.
- PF state is bound to the interface, the five-tuple, the direction, and the translated addresses. Asymmetric paths break it.
- Stateful filtering requires the firewall to see both directions of every flow.
- Stateless filtering (
no state) is the right tool for ephemeral, high-volume, or per-packet-inspection flows. - Every config apply resets the state table. Active TCP sessions are terminated.
Knowledge check · 4 questions
Q1. A TCP connection from the LAN to a remote server establishes normally, but the application reports the connection drops after 60 seconds even when traffic is flowing. The PF state table shows an entry. What is the most likely explanation?
Q2. Editing a firewall rule and clicking Apply in the OPNsense GUI terminates every active TCP session that traverses the firewall.
Q3. Which of the following are valid use cases for a rule with the "no state" modifier on a production OPNsense firewall? Select all that apply.
Q4. You are debugging why an HTTP request from a LAN host to an Internet server fails. The SYN leaves the firewall, but no SYN-ACK returns. The firewall logs do not show a drop. The remote server logs show no SYN arriving. What is the most likely cause?
Passing score: 75%. Answers are checked in this browser.