OPNsenseXI · Firewall StatesFirewall state operations
State inspection with pfctl — reading the state table for diagnostics
What you'll learn
- Use pfctl -s state, -ss, and -si to inspect the state table
- Narrow the state output with grep, awk and the -i interface restriction
- Query and clear states by address with pfctl -k
- Combine state inspection with packet capture for incident diagnosis
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
The state table is the firewall’s working memory. Reading it is the fastest way to answer most “what is the firewall doing with this flow?” questions. The reading has three layers: a one-line summary per state (pfctl -s state), the same list with metadata (pfctl -s state -v), and aggregate statistics (pfctl -si). pfctl has no query language for the state table, so the narrowing is done with grep and awk, with the -i interface restriction, or by asking about one address with pfctl -k.
This lesson covers the commands, the text-processing patterns, and the queries that get to the answer in seconds.
The three views of the state table
Three commands give three views:
pfctl -s state— one line per state. Fast to scan, suitable for piping to grep or awk.pfctl -ssis an abbreviation of the same command and prints exactly the same output.pfctl -s state -v— the same list plus a metadata line per state: age, remaining lifetime, packet and byte counters, and the rule number.pfctl -si— aggregate statistics: the State Table block (current entries, searches, inserts, removals) and the Counters block (match, bad-offset, fragment, short, normalize, memory, and the rest of the drop reasons).
$ pfctl -s state | head -3all tcp 192.0.2.50:51820 -> 203.0.113.50: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:0Illustrative output
The verbose view
pfctl -s state -v adds a metadata line below each entry: age, remaining lifetime, packet and byte counters per direction, and the rule that created the state.
$ pfctl -s state -v | grep -A 1 192.0.2.50 | head -4all tcp 192.0.2.50:51820 -> 203.0.113.50:443 ESTABLISHED:ESTABLISHED
age 00:03:21, expires in 23:56:39, 1421:1284 pkts, 142384:1820392 bytes, rule 15
all udp 192.0.2.50:54210 -> 8.8.8.8:53 MULTIPLE:MULTIPLE
age 00:00:45, expires in 00:00:15, 2:2 pkts, 148:312 bytes, rule 15Illustrative output
The aggregate statistics
pfctl -si shows aggregate counts and rates.
$ 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 counter that matters most for capacity is memory. When
the state limit is reached, pf_alloc_state fails and the
packet is dropped with reason memory, so a rising memory
counter alongside current entries sitting at the limit is
the signature of a full state table. state-limit is a
different counter: it increments when a rule’s own
max-states is reached, not the global limit. src-limit
increments for max-src-states and max-src-conn.
To see the limit itself:
$ pfctl -smstates hard limit 409600
src-nodes hard limit 409600
frags hard limit 5000
table-entries hard limit 200000Illustrative output
Filtering by IP
The most common diagnostic question: “is there a state for IP X?” The answer is a grep:
pfctl -s state | grep 192.0.2.50
This returns every state entry that involves the IP, regardless of direction. Combine with awk to summarise:
pfctl -s state | grep 192.0.2.50 | awk '{print $4}' | sort -u
This extracts the destination IPs from states involving the source and sorts uniquely.
Filtering by port
“Which clients are connecting to port 443 on the firewall?” — grep on the destination port:
pfctl -s state | grep ':443 '
The trailing space anchors the match to a port field boundary.
Filtering by interface
“Show me all states on the WAN interface” — pfctl supports an -i flag for interface filtering:
pfctl -s state -i igb1
Querying by address with pfctl -k
pfctl -k takes a host, a network, a label, a state id, or a
gateway, and acts on the states that match it. It is the only
address-aware query pfctl offers for the state table — and
it is destructive, because it kills what it matches:
pfctl -k 192.0.2.50
pfctl -k 192.0.2.50 -k 203.0.113.50
The first form kills every state involving that host. The
second form takes a pair and kills only the states between
those two addresses. pfctl -k reports how many states it
killed, which is useful as a count — but only when killing
them is what the operator wanted.
For a read-only answer to “how many states involve this host?”, use grep:
pfctl -s state | grep -c 192.0.2.50
Common diagnostic queries
A handful of queries cover most incidents:
“Is the state for this flow present?”
pfctl -s state | grep 192.0.2.50
A state present means the firewall has the flow in memory.
“How many states per source IP?”
pfctl -s state | awk '{print $3}' | sort | uniq -c | sort -rn | head -20
“How many states by protocol?”
pfctl -s state | awk '{print $1}' | sort | uniq -c
“What is the firewall’s WAN IP doing?”
pfctl -s state -i igb1
“Which states are the oldest?”
pfctl -s state -v | grep '^ age' | sort -r | head -5
The metadata lines start with three spaces and age, so the
sort is on the age field directly.
State inspection with packet capture
State inspection and packet capture are complementary. State tells you what the firewall thinks is happening; capture tells you what is actually on the wire. When they disagree, the firewall is wrong.
The combined workflow:
- State says flow exists.
pfctl -s state | grep 192.0.2.50shows a state for the flow. - Capture on the egress interface.
tcpdump -ni igb1 host 203.0.113.50shows traffic on the wire. - Compare. State says packets are moving (counter > 0); capture shows nothing on the wire.
- Diagnose. Look for NAT misconfigurations, routing loops, or kernel-level drops.
$ pfctl -s state -v | grep -E 'packets: [0-9]+' | sort -t: -k2 -n -r | head -5 packets: 1284214 bytes: 1820392192
packets: 892341 bytes: 524288000
packets: 245312 bytes: 18874368
packets: 142193 bytes: 9437184
packets: 98213 bytes: 6291456Illustrative output
Saving state for incident review
When investigating an incident, save the state table to a file for later analysis:
pfctl -s state > /tmp/state-20260814-0314.txt
pfctl -s state -v > /tmp/state-verbose-20260814-0314.txt
pfctl -si > /tmp/state-stats-20260814-0314.txt
pfctl -sm > /tmp/state-limits-20260814-0314.txt
Attach the files to the incident ticket.
Summary
pfctl -s stateshows one line per state;-vadds a metadata line;pfctl -sishows aggregate counts;pfctl -smshows the configured limits.pfctl -s statetakes no filter expression. Narrow with grep and awk, restrict with-i <interface>, or act on one address withpfctl -k.- One flow is one state entry; the entry matches both directions.
- State inspection combined with packet capture tells the operator what the firewall is doing and what is on the wire.
- Save state to a file for incident review; attach to the ticket.
- Use
pfctl -sifor continuous monitoring;pfctl -s stateis for ad-hoc diagnosis.
Knowledge check · 4 questions
Q1. You want to list the TCP state entries whose destination port is 443. Which approach works?
Q2. pfctl -si prints the configured state limit alongside the current entry count.
Q3. Which of the following actually narrow the state-table output? Select all that apply.
Q4. A monitoring script runs pfctl -s state every second to track state count. The firewall performance degrades. What is the most likely cause?
Passing score: 75%. Answers are checked in this browser.