Skip to main content
RunBook Academy

OPNsenseXXXVII · Packet Flow MethodologyFirewall rule evaluation

Firewall rule evaluation — how PF decides to pass or block

Intermediate⏱ ~14 minpfctltcpdumpWireshark

What you'll learn

  • Describe the rule evaluation order in PF
  • Explain the first-match-wins principle and the role of quick rules
  • Distinguish pass, block, and reject actions
  • Read PF log entries to verify the decision for a given packet

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.

PF’s job is to evaluate each packet against the ruleset and decide what to do with it. It is offered the packet once per pfil hook — inbound on the ingress interface and, for a forwarded packet, again outbound on the egress interface — but it only walks the ruleset for a packet that does not already match a state. In practice that means the ruleset decides a forwarded flow once, on the ingress interface, and the outbound hook rides the state that decision created. Each walk is in rule order, and with quick set the first rule that matches wins. The operator who knows how PF evaluates can predict which rule will match a given packet; the operator who does not will be confused when traffic that “should be allowed” is blocked.

This lesson covers the rule evaluation order, the first-match-wins principle, the actions (pass, block, reject), the role of quick rules, and the discipline of reading PF log entries.

The ruleset

OPNsense stores the active ruleset in /etc/pf.conf (or the equivalent compiled file). The GUI’s Firewall → Rules page generates this ruleset from the operator’s configured rules. Each rule has:

  • Action — pass, block, or reject.
  • Interface — the interface the rule is bound to (or floating, which matches any).
  • Direction — in or out. A rule only matches on the hook whose direction it names; a rule with no direction matches both. OPNsense’s default is in, so most rules police the ingress interface.
  • Quick — whether to stop evaluation at this rule.
  • Source — IP, network, or alias.
  • Destination — IP, network, or alias.
  • Port — single port, range, or list.
  • Protocol — TCP, UDP, ICMP, etc.
  • Logging — whether to log matching packets.

PF evaluates rules in order, top to bottom, on any hook where the packet does not match an existing state. The first rule that matches — accounting for quick, interface, direction, source, destination, port, and protocol — decides the packet’s fate on that hook. A forwarded packet that a pass rule allowed inbound normally crosses the outbound hook on that rule’s state and is never compared to the ruleset a second time, which is why an interface’s rules are written for traffic entering on it.

Read-only / Safepfctl -sr (show rules)
$ pfctl -sr
block drop log inet all label "..."
block drop log inet6 all label "..."
pass out log all flags S/SA keep state label "..."
pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = 80 flags S/SA keep state label "..."
pass in quick on igb0 inet proto tcp from 192.0.2.0/24 to any port = 443 flags S/SA keep state label "..."
pass in quick on igb1 inet proto tcp from any to 192.0.2.50 port = 443 flags S/SA keep state label "..."
block drop in quick on igb1 inet from 198.51.100.0/24 to any label "..."

Illustrative output

The first-match-wins principle

PF evaluates rules in order. The first rule that matches the packet decides the packet’s fate. Subsequent rules are not evaluated.

The trap is the operator who writes a specific allow rule below a general allow rule. The general rule matches first; the specific rule never matches; the operator concludes the specific rule is wrong when in fact the order is wrong.

The discipline:

  • Most specific rules first. A rule allowing host A to port X goes above a rule allowing network B to port Y.
  • Default deny is a real rule. PF’s own behaviour when nothing matches is to pass. The block-everything behaviour operators rely on comes from a rule OPNsense generates — a non-quick block that matches everything and wins by last-match whenever no quick rule has claimed the packet. It is visible in pfctl -sr; it is not a property of PF.

The quick flag

By default, every rule in the ruleset is evaluated even after a match — only the last matching rule’s action applies. This is “last-match-wins”. PF’s quick flag changes this: when a quick rule matches, evaluation stops immediately and the action applies.

In OPNsense, every GUI-generated rule is a quick rule by default. This makes the ruleset behave like “first-match-wins” for the operator’s intended logic.

The discipline:

  • Quick rules for normal firewall logic. “Allow SSH from this network” — quick, stops at this rule.
  • Non-quick rules for stateful overrides. A non-quick rule that matches a packet can be overridden by a later non-quick rule. Useful for complex policies but easy to get wrong.

Pass, block, reject

The three actions PF can take:

  • Pass. Allow the packet through. If state-tracking is enabled, create or update state.
  • Block. Drop the packet silently. No response is sent. The peer times out.
  • Reject. Drop the packet and send a response. For TCP, send RST. For UDP and ICMP, send ICMP destination unreachable. The peer learns immediately that the packet was rejected.

The operator chooses based on the desired behaviour:

  • Pass for allowed traffic.
  • Block for traffic that should be silently dropped (typical for inbound public traffic).
  • Reject for traffic where the operator wants the peer to know quickly (useful for diagnostic reasons, but reveals the firewall’s existence to scanners).
Read-only / Safesilent block (no response)
$ tcpdump -ni igb0 -c 5 'src host 198.51.100.10 and dst port 22'
12:34:56.789012 ... 198.51.100.10.51820 > 192.0.2.50.22: Flags [S], seq ...
12:34:59.789012 ... 198.51.100.10.51820 > 192.0.2.50.22: Flags [S], seq ...
12:35:02.789012 ... 198.51.100.10.51820 > 192.0.2.50.22: Flags [S], seq ...
12:35:05.789012 ... 198.51.100.10.51820 > 192.0.2.50.22: Flags [S], seq ...
12:35:08.789012 ... 198.51.100.10.51820 > 192.0.2.50.22: Flags [S], seq ...

Illustrative output

State creation

When a pass rule matches a packet, PF creates or updates state for the flow. The state is a record of the tuple (source IP, source port, destination IP, destination port, protocol) that identifies the flow.

Subsequent packets matching the state are passed without re-evaluating the ruleset. This is what makes stateful filtering fast and what makes return traffic “automatically” allowed without an explicit inbound rule.

The discipline:

  • keep state is the default for pass rules in OPNsense. The rule creates state on the first packet; return traffic matches state and is passed.
  • no state prevents state creation. Used for stateless rules or rules that should not generate state (rare).
  • modulate state, synproxy state for special state behaviours. Modulate state randomises the initial sequence number; synproxy state hands the SYN to a synproxy that completes the handshake before passing to the backend (DDoS protection).

Reading the PF log

The filter log is the operator’s evidence for what PF decided about each packet. OPNsense’s filterlog daemon reads the pflog0 interface and writes one comma-separated record per logged packet into the syslog stream. The leading fields of that record are the ones that matter for reading the model:

  1. Rule number. The position of the matching rule in the loaded ruleset.
  2. Sub-rule number and anchor name. Empty for a top-level rule, which is the common case.
  3. Rule id. The stable identifier for the rule, the same value that appears as its label in pfctl -sr. Prefer this over the rule number for anything kept longer than the current ruleset, because inserting a rule renumbers everything below it.
  4. Interface. The interface whose hook fired.
  5. Reason and action. match and block, match and pass, and so on.
  6. Direction. in or out.

Fields 4 and 6 together identify the hook, and that is the first thing to read: block/in/igb0 and block/out/igb1 are different rules with different fixes, and an out record on a forwarded flow means something prevented the state match you were expecting. The remaining fields carry the addresses, ports and protocol detail.

Read-only / Safefilter log entries by rule
$ grep -F '] 14,,,' /var/log/filter/latest.log | tail -3
<134>1 2026-08-14T03:14:01+00:00 fw.example.com filterlog 44481 - [meta sequenceId="5521"] 14,,,3b91c7e40d2a5f8619ac0e73d5b2f184,igb0,match,block,in,4,0x0,,64,41522,0,DF,6,tcp,60,192.0.2.50,203.0.113.5,51820,443,0,S,2847113905,,64240,,mss;sackOK;TS;nop;wscale
<134>1 2026-08-14T03:14:01+00:00 fw.example.com filterlog 44481 - [meta sequenceId="5522"] 14,,,3b91c7e40d2a5f8619ac0e73d5b2f184,igb0,match,block,in,4,0x0,,64,41523,0,DF,6,tcp,60,192.0.2.50,203.0.113.5,51822,443,0,S,2847113906,,64240,,mss;sackOK;TS;nop;wscale
<134>1 2026-08-14T03:14:02+00:00 fw.example.com filterlog 44481 - [meta sequenceId="5523"] 14,,,3b91c7e40d2a5f8619ac0e73d5b2f184,igb0,match,block,in,4,0x0,,64,41524,0,DF,6,tcp,60,192.0.2.50,203.0.113.5,51824,443,0,S,2847113907,,64240,,mss;sackOK;TS;nop;wscale

Illustrative output

The rule evaluation trace

When a packet does not match the expected rule, the operator traces the evaluation:

  1. Identify the packet. Source IP, destination IP, source port, destination port, protocol.
  2. Read the ruleset. pfctl -sr shows the active rules in order.
  3. Walk the ruleset. For each rule, check whether the packet matches: interface, direction, source, destination, port, protocol.
  4. Identify the matching rule. The first rule that matches wins.
  5. Check the action. Pass, block, or reject.

The trap is to assume a rule matches when it does not. The operator who writes “allow host A to port X” but the host is on a different subnet than the rule’s source expects concludes PF is broken. PF is not broken; the rule did not match.

Floating rules and direction

Floating rules are evaluated before interface rules and can match on either direction. They are useful for global policies (e.g. “block all RFC1918 traffic from WAN”) that should apply regardless of which interface the traffic arrives on.

The discipline:

  • Floating rules at the top of the ruleset. They evaluate before interface rules; interface rules are the fallback.
  • Use floating rules sparingly. Every floating rule is evaluated for every packet. A floating rule with a complex match can slow the firewall.
  • Match the direction explicitly. A floating rule that matches in is different from one that matches out.

Summary

  • PF is offered the packet on every hook it crosses — inbound on the ingress interface, outbound on the egress interface — but walks the ruleset only where the packet matches no state, so a forwarded flow is normally decided once, inbound. On each walk, rules are evaluated in order and the first match wins (when quick is set, which is the default).
  • Actions: pass (allow), block (silent drop), reject (drop with response).
  • State is created by pass rules; return traffic matches state without re-evaluating the ruleset.
  • The filter log shows what PF decided for each packet. Read the interface and direction fields first — they name the hook. The rule id (field four) identifies the rule durably; the rule number does not, because inserting a rule renumbers everything below it.
  • Quick rules stop evaluation on match; non-quick rules allow later rules to override.
  • Trace the evaluation by walking the ruleset in order and identifying the first match.

Knowledge check · 4 questions

  1. Q1. You write a pass rule "allow host 192.0.2.50 to port 22" but traffic from that host to port 22 is still blocked. You check the ruleset and see the rule is at position 20. Above it at position 10 is a "block all from any to any" rule. What is wrong?

  2. Q2. A pass rule with `no state` allows return traffic because the source and destination IPs are the same.

  3. Q3. Which of the following are actions PF can take on a matching packet? Select all that apply.

  4. Q4. A filter log record begins 14,,,3b91c7e40d2a5f8619ac0e73d5b2f184,igb1,match,block,out — before you read a single address, what have you learned?

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