Skip to main content
RunBook Academy

OPNsenseXI · Firewall StatesFirewall state operations

State inspection with pfctl — reading the state table for diagnostics

Intermediate⏱ ~12 minpfctlgreptcpdump

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

Not yet marked complete on this device.

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 -ss is 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).
Read-only / Safepfctl -s state
$ pfctl -s state | head -3
all 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:0

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

Read-only / Safepfctl -s state -v
$ pfctl -s state -v | grep -A 1 192.0.2.50 | head -4
all 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 15

Illustrative output

The aggregate statistics

pfctl -si shows aggregate counts and rates.

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

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

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:

  1. State says flow exists. pfctl -s state | grep 192.0.2.50 shows a state for the flow.
  2. Capture on the egress interface. tcpdump -ni igb1 host 203.0.113.50 shows traffic on the wire.
  3. Compare. State says packets are moving (counter > 0); capture shows nothing on the wire.
  4. Diagnose. Look for NAT misconfigurations, routing loops, or kernel-level drops.
Read-only / Safetop flows by packets
$ 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: 6291456

Illustrative 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 state shows one line per state; -v adds a metadata line; pfctl -si shows aggregate counts; pfctl -sm shows the configured limits.
  • pfctl -s state takes no filter expression. Narrow with grep and awk, restrict with -i <interface>, or act on one address with pfctl -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 -si for continuous monitoring; pfctl -s state is for ad-hoc diagnosis.

Knowledge check · 4 questions

  1. Q1. You want to list the TCP state entries whose destination port is 443. Which approach works?

  2. Q2. pfctl -si prints the configured state limit alongside the current entry count.

  3. Q3. Which of the following actually narrow the state-table output? Select all that apply.

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