OPNsenseIII · Stateful Firewalling and PFStateful firewalling and PF
State creation and tracking in PF
What you'll learn
- Describe how PF creates state for TCP, UDP, ICMP, and other protocols
- Explain the default timeouts and how to tune them for production workloads
- Read a PF state entry to identify the flow it represents
- Diagnose common state-related production incidents
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
PF state is the firewall’s working memory. Every flow that traverses the firewall has a state entry; the entry is what makes return traffic permitted, what tells PF whether a packet is part of an existing connection, and what tells the operator what the firewall actually did. This lesson covers how state is created for each protocol, how long it lasts, and how to read the state table for diagnostics.
What PF records in a state entry
A PF state entry is more than a five-tuple. It records:
- The five-tuple: source IP, source port, destination IP, destination port, protocol.
- The direction the first packet took (interface, gateway, NAT pre/post addresses).
- The packet and byte counters for each direction.
- The TCP flags observed so far, for TCP flows.
- A timeout controlling when the entry is forgotten.
The state entry is created when a packet that matches a
keep state, modulate state, or synproxy state rule is
permitted. The entry is created on the first packet that
matches (typically the SYN for TCP, the first datagram for UDP).
$ pfctl -s state -vall tcp 203.0.113.50:443 <- 192.0.2.50:51820 (203.0.113.1:51820) ESTABLISHED:ESTABLISHED
age 00:03:21, expires in 23:56:39, 1421:1284 pkts, 142384:1820392 bytes, rule 15
all tcp 203.0.113.50:443 <- 192.0.2.51:51821 (203.0.113.1:51821) ESTABLISHED:ESTABLISHED
age 00:00:12, expires in 23:59:48, 12:9 pkts, 1840:11284 bytes, rule 15
all udp 8.8.8.8:53 <- 192.0.2.50:54210 (203.0.113.1:54210) MULTIPLE:MULTIPLE
age 00:00:45, expires in 00:00:15, 2:2 pkts, 148:312 bytes, rule 15Illustrative output
The address in parentheses on the summary line is the translated address, shown next to the address it replaces. For traffic that has been NAT-translated, PF records the original (pre-NAT) tuple for state matching and the translated (post-NAT) tuple for forwarding — both in the same entry. When NAT is involved, the state table is the only place the operator can see both tuples.
State creation per protocol
TCP
TCP state is the most sophisticated. PF tracks the handshake:
- SYN only — first packet of a new connection. PF creates a
state in
SYN_SENT:SYN_SENT(client side: SYN_SENT, server side: SYN_SENT). Timeout: 30 seconds. - SYN-ACK — second packet. PF updates state to
ESTABLISHED:ESTABLISHED. Timeout: 24 hours. - ACK — third packet, completes the handshake. State remains
ESTABLISHED:ESTABLISHEDfor the duration of the connection. - FIN or RST — connection closes. PF removes the state immediately.
- Idle timeout — 24 hours without traffic removes the state.
PF also recognises half-closed connections (one side sent FIN, the other has not). Half-closed states live until the second FIN or until the timeout.
UDP
UDP has no handshake. PF creates a state on the first datagram
in a single direction; the state permits return packets that
match the reversed tuple. PF’s state for UDP is
SINGLE:NO_TRAFFIC until the first return packet, then
SINGLE:MULTIPLE until the timeout.
UDP state timeout is 60 seconds by default. Refreshed by any matching packet in either direction. Not refreshed by application-level keepalives (because the firewall does not parse the application layer).
For long-lived UDP flows (VoIP calls, video streaming, IPsec NAT-T keepalives), the default 60-second timeout is too short. Production tuning requires either raising the global UDP timeout or applying a per-rule timeout override.
ICMP
ICMP state is the simplest. The first packet (echo request, destination unreachable, time exceeded, etc.) creates a state; the matching reply uses the state. Default timeout: 20 seconds.
ICMP states are short because ICMP is meant to be transactional.
For Path MTU Discovery, the ICMP type 3 code 4 state must live
as long as the TCP connection that depends on it; the default
20-second timeout is fine because PMTUD messages only need to
flow during the initial handshake.
Other protocols
PF tracks state for any IP protocol the operator permits, not just TCP/UDP/ICMP. GRE, ESP (IPsec), OSPF, and any other IP protocol can have state. The default behaviour for non-TCP/UDP state is conservative:
- One packet creates state.
- Matching packets in either direction match the state.
- Timeout is 60 seconds for any non-TCP/UDP flow unless overridden.
For routing protocols (OSPF, BGP), the recommended pattern is
to use stateless rules (no state) because the routing
protocol itself maintains its own session state.
Reading the state table for diagnostics
The state table is the operator’s primary evidence for “what did the firewall do?” Three patterns to recognise:
- Many short-lived states for the same source. A host
making rapid connections (a port scanner, a buggy
application) is creating and tearing down states quickly.
Look at
ageto see how long each state has lived. - States with very high packet or byte counters. A flow that has transferred gigabytes. Useful for verifying that bulk transfer (backup, replication) is actually flowing.
- States with the same five-tuple but different interfaces. Should not happen in normal operation — but if it does, asymmetric routing or NAT rebinding is the cause.
The most useful diagnostic commands:
pfctl -s state— table summary, one line per state. One flow is one entry; the entry matches both directions.pfctl -ssis an abbreviation of the same command.pfctl -s state -v— adds a second line per state with age, remaining lifetime, packet and byte counters, and the rule number.pfctl -s state | grep <address>— there is no filter expression for the state table, so narrowing is grep and awk.-i <interface>restricts the walk to one interface.pfctl -si— show state-table statistics (entries, searches, insertions, removals) and the drop counters.pfctl -sm— show the pf runtime limits, including the maximum number of states.
$ pfctl -siStatus: Enabled for 14 days 03:27:18 Debug: Urgent
State Table Total Rate
current entries 4382
searches 12847123 10.6/s
inserts 189324 0.2/s
removals 184942 0.2/s
Counters
match 8452394 7.0/s
bad-offset 0 0.0/s
fragment 12 0.0/s
short 0 0.0/s
normalize 3 0.0/s
memory 0 0.0/s
bad-timestamp 0 0.0/s
congestion 0 0.0/s
ip-option 0 0.0/s
proto-cksum 0 0.0/s
state-mismatch 41 0.0/s
state-insert 0 0.0/s
state-limit 0 0.0/s
src-limit 0 0.0/s
synproxy 0 0.0/s
map-failed 0 0.0/sIllustrative output
Tuning timeouts
Three places to change state timeouts:
- Global default —
System → Firewall → Advanced → Optimisation. Profiles likenormal,high-latency,satellite,aggressiveset global timeouts.normalis the default for most estates. - Per-rule override — edit a firewall rule and set “State timeout” in seconds. The rule’s timeout overrides the global default for matching flows.
- The ruleset — timeouts are
set timeoutstatements in the compiled ruleset.pfctl -stprints the values in force. The OPNsense GUI generates them; editing the generated ruleset by hand does not survive an apply.
Tuning is case-by-case. VoIP needs longer UDP timeouts. Video streaming needs longer UDP timeouts. SSH sessions are long-lived TCP — the 24-hour default is fine. RPC services with idle connections need longer timeouts than the default.
Summary
- PF state records the five-tuple, direction, NAT translations, flags, counters, and a timeout.
- TCP state is the most sophisticated, with handshake tracking and 24-hour ESTABLISHED timeout.
- UDP state is single-packet initiated, 60-second default timeout, refreshed by any matching packet.
- ICMP state is short-lived (20 seconds).
- State-table exhaustion is a real production failure mode. Size the state table to expected concurrent flow count.
- Tune timeouts per-rule for protocols whose natural pacing does not match the defaults.
Knowledge check · 4 questions
Q1. A user reports that long SSH sessions (idle for hours) are dropped after 24 hours. PF state table shows the entries disappear at exactly that mark. What is the most appropriate fix?
Q2. The default UDP state timeout is 60 seconds; it is refreshed only by matching packets in either direction, not by application-level keepalives.
Q3. Which of the following are recorded in a PF state entry? Select all that apply.
Q4. pfctl -sm reports a states hard limit of 204,800. pfctl -si shows current entries at 204,800 and a memory counter climbing at 3/s. Users are reporting that new HTTPS connections fail intermittently. What is the most appropriate first action?
Passing score: 75%. Answers are checked in this browser.