OPNsenseXI · Firewall StatesFirewall state operations
State table anatomy — tuple, timeout, counters, packets, bytes
What you'll learn
- Read every field of a PF state entry and explain its meaning
- Distinguish the timeout from the counters and explain how they interact
- Identify the direction marker and the role of post-NAT addresses
- Diagnose state-table issues by reading state entries directly
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 flow that traverses an OPNsense firewall has a state entry — a small data structure in PF that records what the firewall knows about the flow. The state table is the firewall’s working memory; the state entries are the records. The operator who can read a state entry can answer most “what did the firewall do?” questions in seconds. The operator who cannot is reduced to guessing.
This lesson covers every field of a state entry, what it means, and how to read it for diagnostics.
What a state entry contains
A PF state entry is more than a five-tuple. It is a complete record of a flow — both directions of it: where the packet came from, where it is going, how much traffic has moved, when the flow was created, when it will time out, and (for TCP) what flags have been observed.
The fields:
- Five-tuple. Source IP, source port, destination IP, destination port, protocol. The identity of the flow.
- Direction marker.
->or<-. It records the direction of the packet that created the state, and so points from the initiator to the responder. It does not mean there is a second entry for the other direction — one flow is one entry. - TCP state. For TCP, the state machine position:
SYN_SENT,ESTABLISHED,TIME_WAIT,FIN_WAIT_2, etc. - Timeout. The absolute deadline when the entry will be removed.
- Age. How long the entry has existed.
- Packet and byte counters. How many packets and bytes have moved, counted separately for each direction of the flow.
- Interface and gateway. Which interface the flow is bound to and which gateway it is using.
- Pre-NAT and post-NAT tuples. For flows that have been NAT-translated, the original and translated tuples.
$ 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 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 five-tuple in detail
The five-tuple is the identity of the flow. PF matches incoming packets against the five-tuple (in the reversed direction for inbound packets). A packet whose source IP, source port, destination IP, destination port, and protocol all match an existing state entry matches that state.
A few details:
- Source port can be ephemeral. Most client-side source ports are in the ephemeral range (49152-65535 by default on FreeBSD).
- Protocol determines state behaviour. TCP, UDP, and ICMP have different state-tracking semantics.
- IPv4 and IPv6 are separate. The firewall maintains separate state entries for IPv4 and IPv6 flows.
Direction markers
A state entry is not recorded per direction. A TCP flow has exactly one entry, and that entry matches packets in both directions — which is precisely why return traffic needs no rule of its own. The -> or <- marker records which way the packet that created the state was travelling: -> for a state created on an outbound packet, <- for one created on an inbound packet. The arrow therefore points from the initiator to the responder.
The entry keeps separate packet and byte counters for each direction, printed as out:in in the verbose output. Two counters, one entry.
TCP state
For TCP flows, the state entry includes the current TCP state. The states mirror the standard TCP state machine:
| State | Meaning |
|---|---|
SYN_SENT:SYN_SENT | First SYN observed. Waiting for SYN-ACK. |
ESTABLISHED:ESTABLISHED | Handshake complete. Data flowing. |
FIN_WAIT_1 | First FIN sent. Waiting for ACK. |
FIN_WAIT_2 | First FIN acknowledged. Waiting for peer’s FIN. |
TIME_WAIT | Both sides have closed. Waiting for late packets. |
CLOSE_WAIT | Peer sent FIN. Local side has not yet sent FIN. |
LAST_ACK | Local side sent FIN. Waiting for final ACK. |
PF tracks these states and adjusts timeouts based on them. ESTABLISHED:ESTABLISHED flows get the long 24-hour timeout. TIME_WAIT flows get a short timeout (typically 30 seconds).
Timeout and age
The timeout is the absolute deadline when the entry will be removed. The age is how long the entry has existed. The entry is removed when age >= timeout.
For TCP ESTABLISHED, the default timeout is 86400 seconds (24 hours). For UDP, the default timeout is 60 seconds. For ICMP, the default timeout is 20 seconds.
Packet and byte counters
Every state entry records how many packets and how many bytes have moved in this direction. The counters increment on every matching packet.
The counters are useful for:
- Verifying traffic flow. A state with
packets: 1000, bytes: 500000shows that traffic has moved. - Identifying heavy flows. A state with
bytes: 10000000000shows a long-running heavy flow. - Detecting stale states. A state with
packets: 100andage: 23:59:55shows a long-lived but quiet flow.
$ pfctl -s state -v | head -6all tcp 203.0.113.50:443 <- 192.0.2.50:51820 ESTABLISHED:ESTABLISHED
age 00:03:21, expires in 23:56:39, 1421:1284 pkts, 142384:1820392 bytes, rule 15
all udp 8.8.8.8:53 <- 192.0.2.50:54210 MULTIPLE:MULTIPLE
age 00:00:45, expires in 00:00:15, 2:2 pkts, 148:312 bytes, rule 15Illustrative output
Pre-NAT and post-NAT tuples
For flows that pass through NAT, the state entry records both the original (pre-NAT) and translated (post-NAT) tuples. The pre-NAT tuple is what the rule matched; the post-NAT tuple is what the firewall forwarded.
Reading the state entry carefully tells the operator what NAT was applied. For inbound port forwards, the post-NAT destination is the internal server. For outbound NAT, the post-NAT source is the firewall’s WAN IP.
Narrowing state output
For diagnostics, the operator often wants to see only states matching a specific criterion. pfctl -s state accepts no filter expression, so the narrowing is text processing:
pfctl -s state | grep 192.0.2.50— show all states involving 192.0.2.50.pfctl -s state | grep ':443 '— show all states with port 443 on either end.pfctl -s state -i igb1— restrict the walk to states bound to one interface.
Beware -f: it loads a ruleset from a file. pfctl -s state -f 'port = 443' is not a filter and does not do what it looks like it does.
State-table statistics
Beyond per-entry data, PF tracks aggregate state-table statistics:
$ 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
The fields:
- current entries. The live state count right now.
- searches. Cumulative state-table lookups, with a per-second rate.
- inserts / removals. Cumulative state creations and expiries. In steady state the two rates track each other.
- memory. A drop counter in the Counters block. It increments when PF cannot allocate a state — which is what hitting the configured maximum looks like.
There is no field for the configured maximum. That is a pf runtime limit:
$ pfctl -smstates hard limit 409600
src-nodes hard limit 409600
frags hard limit 5000
table-entries hard limit 200000Illustrative output
Summary
- A state entry contains the five-tuple, direction marker, TCP state, timeout, age, counters, interface, gateway, and (where applicable) pre-NAT and post-NAT tuples.
- TCP states mirror the standard TCP state machine; timeout depends on state.
- Default timeouts: TCP ESTABLISHED 24 hours, UDP 60 seconds, ICMP 20 seconds.
- Packet and byte counters record traffic volume per direction, both kept inside the one state entry.
- Pre-NAT and post-NAT tuples appear in the state entry when NAT was applied.
- One flow is one state entry, matched in both directions; the counters are kept per direction within that single entry.
pfctl -s statelists the states;-vadds the metadata line with age, counters and rule number;pfctl -sishows aggregate statistics;pfctl -smshows the configured limits.
Knowledge check · 4 questions
Q1. A TCP state entry shows ESTABLISHED:ESTABLISHED, age 23:59:55, packets 100. The flow has been quiet for almost 24 hours. What will happen next?
Q2. A single TCP flow traversing the firewall produces a single state entry in the state table.
Q3. Which of the following are recorded in a PF state entry? Select all that apply.
Q4. pfctl -sm shows `states hard limit 200000`. pfctl -si shows current entries at 199,800 and a memory counter of 247 that is still climbing. What does that tell you?
Passing score: 75%. Answers are checked in this browser.