Skip to main content
RunBook Academy

OPNsenseXI · Firewall StatesFirewall state operations

State table anatomy — tuple, timeout, counters, packets, bytes

Intermediate⏱ ~13 minpfctl

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

Not yet marked complete on this device.

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.
Read-only / Safepfctl -s state -v
$ pfctl -s state -v
all 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 15

Illustrative 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:

StateMeaning
SYN_SENT:SYN_SENTFirst SYN observed. Waiting for SYN-ACK.
ESTABLISHED:ESTABLISHEDHandshake complete. Data flowing.
FIN_WAIT_1First FIN sent. Waiting for ACK.
FIN_WAIT_2First FIN acknowledged. Waiting for peer’s FIN.
TIME_WAITBoth sides have closed. Waiting for late packets.
CLOSE_WAITPeer sent FIN. Local side has not yet sent FIN.
LAST_ACKLocal 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: 500000 shows that traffic has moved.
  • Identifying heavy flows. A state with bytes: 10000000000 shows a long-running heavy flow.
  • Detecting stale states. A state with packets: 100 and age: 23:59:55 shows a long-lived but quiet flow.
Read-only / Safepfctl -s state -v
$ pfctl -s state -v | head -6
all 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 15

Illustrative 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:

Read-only / Safepfctl -si
$ pfctl -si
Status: 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/s

Illustrative 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:

Read-only / Safepfctl -sm
$ pfctl -sm
states        hard limit   409600
src-nodes     hard limit   409600
frags         hard limit     5000
table-entries hard limit   200000

Illustrative 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 state lists the states; -v adds the metadata line with age, counters and rule number; pfctl -si shows aggregate statistics; pfctl -sm shows the configured limits.

Knowledge check · 4 questions

  1. 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?

  2. Q2. A single TCP flow traversing the firewall produces a single state entry in the state table.

  3. Q3. Which of the following are recorded in a PF state entry? Select all that apply.

  4. 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.