Skip to main content
RunBook Academy

OPNsenseXXXVII · Packet Flow MethodologyState creation and match

State creation and match — how PF tracks flows and passes return traffic

Intermediate⏱ ~14 minpfctltcpdumpnetstat

What you'll learn

  • Describe when PF creates state and what is recorded
  • Identify how a return packet matches state
  • Read state entries to understand a flow
  • Recognise state-related failure modes — asymmetric routing, state exhaustion, timeout

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.

State is the heart of stateful filtering. When PF passes the first packet of a flow, it creates a state entry that records the flow’s identity. Subsequent packets — including return packets — match the state and are passed without re-evaluating the ruleset. The operator who understands state can diagnose flow-level problems; the operator who does not will chase ghost rules.

This lesson covers when PF creates state, what is recorded, how return packets match, the state timeouts, and the state-related failure modes.

When state is created

PF creates state when a pass rule with keep state (the default) matches a packet. Specifically:

  • TCP. State is created on the SYN or the SYN-ACK (the second packet of the handshake), depending on which side the rule matches first. The full state record includes the SYN sequence numbers, so PF can validate subsequent sequence numbers within a window.
  • UDP. State is created on the first UDP packet (there is no handshake). PF records the tuple.
  • ICMP. State is created on the first ICMP echo or other tracked ICMP type.
  • Other protocols. State is created on the first packet if the rule has keep state.

The state record contains:

  • Source address and port.
  • Destination address and port.
  • Protocol.
  • Interface and the direction the creating packet travelled.
  • State flags — for TCP, the connection state (SYN, ESTABLISHED, FIN, etc.).
  • Packet and byte counters.
  • Timeout — when the state expires if not refreshed.
  • Routing information — for reply-to and route-to, the interface and gateway to use for return packets.
Read-only / Safepfctl -s state (state entries)
$ pfctl -s state | head -3
all tcp 192.0.2.50:51820 -> 203.0.113.5:443       ESTABLISHED:ESTABLISHED
all udp 192.0.2.50:54210 -> 8.8.8.8:53             MULTIPLE:MULTIPLE
all icmp 192.0.2.50:1289 -> 8.8.8.8:1289           0:0

Illustrative output

State match

When a packet arrives, PF first checks the state table. If the packet matches an existing state, PF passes it without re-evaluating the ruleset. The match is on:

  • Protocol. TCP matches TCP, UDP matches UDP, etc.
  • Source address and port. Match the recorded source.
  • Destination address and port. Match the recorded destination (with reversal — a return packet has the original source as its destination and vice versa).

The state does not have to be matched in the direction it was created in, and it does not have to be matched on the interface or the hook it was created on. That last part is a setting rather than a law: pf’s state policy is floating by default — pf.conf(5), “states can match packets on any interfaces” — and OPNsense leaves it there unless the Bind states to interface checkbox under Firewall → Settings → Advanced is ticked. So one entry serves the inbound hook, the outbound hook on a different interface, and both directions of traffic. That is what makes return traffic free of rule evaluation, and it is also why a forwarded packet allowed by an inbound rule is not re-tested against the ruleset on its way out.

For TCP, PF also checks:

  • Sequence numbers within a window. The packet’s sequence number must be within the expected window. Out-of-window packets are dropped (or matched against the sliding window).
  • TCP flags. Certain flag combinations (e.g. SYN-ACK before SYN) are not allowed.

The discipline:

  • State match is fast. A hash table lookup. Per-packet cost is small.
  • State match is permissive in some ways. A return packet matches state regardless of whether a rule would allow it. The state entry was created by a passing rule; return traffic is implicitly allowed.

State timeouts

State entries expire after a timeout if not refreshed. The defaults in OPNsense:

  • TCP established. 24 hours (86400 seconds).
  • TCP closing. 60 seconds (the time after a FIN to allow retransmits).
  • UDP. 60 seconds.
  • ICMP. 20 seconds.

The timeouts are conservative — they are designed to keep state around long enough for normal flows but not so long that the state table fills with dead flows.

The operator can see the remaining timeout in pfctl -s state — the column after the byte counters is the age, then the timeout. As the entry ages, the timeout counts down. When the timeout reaches zero, the entry is removed.

Read-only / Safepfctl -s state -v (verbose state)
$ pfctl -s state -v | head -4
all tcp 192.0.2.50:51820 -> 203.0.113.5:443       ESTABLISHED:ESTABLISHED
 age 00:00:05, expires in 23:59:55, 6:4 pkts, 892:2140 bytes, rule 15
all udp 192.0.2.50:54210 -> 8.8.8.8:53             MULTIPLE:MULTIPLE
 age 00:00:02, expires in 00:00:58, 1:1 pkts, 74:156 bytes, rule 15

Illustrative output

Three failure modes relate specifically to state:

  • Asymmetric routing. The packet leaves via OPNsense but the return packet comes via a different path (another router, a backup link, a load balancer). OPNsense has no state for the return packet; the return packet is evaluated against the ruleset and dropped if no rule matches it.

  • State exhaustion. The state table is full (the operator-configured maximum is reached). New flows cannot create state; the first packet of the new flow is dropped (no state exists, no rule matches by default).

  • State timeout. A flow is idle longer than the timeout; the state expires. The next packet of the flow is treated as a new flow and evaluated against the ruleset. For long-lived idle connections (some database connections, some monitoring flows), this can cause apparent outages.

The discipline for diagnosing each:

  • Asymmetric routing. Check the routing table on both ends; verify the same path is used in both directions. Use traceroute from both sides.
  • State exhaustion. Check the state count with pfctl -si and compare it to the states hard limit from pfctl -sm. The limit is a pf runtime limit set from Firewall → Settings → Advanced → Firewall Maximum States; left blank, OPNsense derives it from installed RAM.
  • State timeout. Increase the timeout for the specific protocol (e.g. set UDP timeout to 300 seconds for a flow that has packets every 200 seconds). Or use keep state with explicit timeout.

Inspecting the state table

The operator uses pfctl -s state to read the state table. Useful flags:

  • -s state (or its abbreviation -ss) — show the state table.
  • -s state -v — add the metadata line with age, remaining lifetime, counters and rule number.
  • -s state -i <interface> — restrict the walk to states bound to one interface.
  • pfctl -s state | grep <pattern> — narrow by source, destination, or port. There is no filter expression; -f loads a ruleset from a file.

For a specific flow:

pfctl -s state | grep 192.0.2.50

The output shows all state entries involving the host. The operator can see what flows are active, how long they have been active, and when they will expire.

The discipline:

  • Cross-reference state with captures. A packet captured on ingress that matches no state entry was evaluated against the ruleset.
  • Cross-reference state with the firewall log. A block in the log that does not match any state entry is a new flow being dropped; a pass in the log that matches a state entry is an established flow.
  • Look for unexpected state. A flow to an unexpected destination (e.g. an internal host reaching out to a known-bad IP) is the signature of compromised behaviour.

Killing state

The operator can kill state with pfctl -k:

pfctl -k 192.0.2.50

This kills all state involving the source IP. The next packet from that source creates new state (or is evaluated against the ruleset if no rule matches).

The discipline:

  • Kill state sparingly. Killing state mid-flow forces every subsequent packet to be re-evaluated. If the ruleset has changed in a way that affects the flow, the new evaluation may drop the packets.
  • Kill state for a specific flow, not the whole table. pfctl -k can target a specific source, destination, or tuple. Killing the whole state table (pfctl -F state) disrupts every active flow on the firewall — usually not what the operator wants.
Configuration changekill state for a host
$ pfctl -k 192.0.2.50
killed 7 states from 1 sources and 0 destinations

Illustrative output

State and the ruleset

State and the ruleset are separate but related. The ruleset decides whether a new flow is allowed; state decides whether a return packet is passed. A change to the ruleset does not affect existing state — only new flows. A change to state timeouts affects all flows using that protocol.

The discipline:

  • New rules apply to new flows. A new pass rule allows new flows matching it; existing flows continue using the rule that created them.
  • Removing a pass rule does not kill existing state. The operator who removes a pass rule expecting to drop the matching traffic is surprised when existing flows continue — until the state expires.
  • To drop an existing flow, kill its state. pfctl -k or wait for the timeout.

Summary

  • State is created on the first packet of a flow that matches a pass rule with keep state.
  • Subsequent packets matching state are passed without re-evaluating the ruleset.
  • State has timeouts per protocol; the defaults are 24 hours for TCP established, 60 seconds for UDP, 20 seconds for ICMP.
  • Return traffic matches state automatically — do not add inbound rules for return traffic.
  • State-related failures: asymmetric routing, state exhaustion, state timeout.
  • The discipline: cross-reference state with captures and the firewall log; kill state sparingly and targeted.

Knowledge check · 4 questions

  1. Q1. A host on the LAN can send packets to the Internet but never receives responses. The firewall log shows no blocks. The state table shows entries for outbound traffic from the host but no corresponding return entries. What is the most likely cause?

  2. Q2. An operator removes a pass rule from the ruleset. Existing flows that matched the rule continue to pass.

  3. Q3. Which of the following are state-related failure modes? Select all that apply.

  4. Q4. You want to drop a specific flow immediately. What is the best action?

Passing score: 75%. Answers are checked in this browser.