Offline PF rule evaluator that catches firewall logic bugs before deployment. Parses real pf.conf syntax, walks packet headers against the full ruleset using PF's last-match-wins semantics, and tells you which rule wins. Found three production-blocking bugs in its first use.
Before the first deploy to hardware, pftest found:
-
DHCP broken by antispoof —
antispoof quickgenerated block rules that matched0.0.0.0(DHCP discover source) before the DHCP pass rule could fire.pfctl -nfpassed clean. -
Printer exception killed by missing
quick— apass inrule for a specific device was overridden by a laterblock quickon the same port. The printer lost HTTP access. -
VPN route-to silently overridden — port-specific pass rules loaded after a broad
route-torule. Last-match-wins meant VPN clients lost their tunnel on standard ports. Kill-switch then blocked everything.
None of these are syntax errors. pfctl -nf cannot catch them.
# drop your config next to pftest.py
cp /etc/pf.conf .
cp -r /etc/pf.d .The engine auto-detects your topology from macros and match rules. No configuration needed.
from pftest import PFSimulator, Packet
sim = PFSimulator()
rules = sim.load()
result = sim.evaluate(Packet(
src="192.0.2.50", dst="203.0.113.1", proto="tcp",
dport=443, iface="vlan10", direction="in",
))
assert result.action == "pass"See TESTING.md for the full API, test patterns, and example test suites.
pftest is a rule evaluator, not a network simulator. There are no real packets, no TCP handshakes, no sockets.
You give it a set of packet headers (source, destination, protocol, port, interface, direction). It walks the parsed ruleset top to bottom, applying PF's evaluation semantics:
- Last-match-wins (no
quick= keep evaluating, last pass/block wins) quickshort-circuits immediatelymatchrules apply transformations (tags, rdr-to, nat-to) without deciding pass/block- Anchors evaluated in declared load order
- Tags assigned on ingress survive into egress evaluation
The result tells you: pass or block, which rule matched, what tag was assigned, whether rdr-to rewrote the destination.
The value isn't simulating the network — it's that PF's evaluation order across 14 anchor files with quick rules, tags, and mid-evaluation rdr-to rewrites is genuinely hard to reason about by reading the config. Humans get it wrong. The three bugs it caught were all ordering/override issues that looked correct reading the rules individually.
- Last-match-wins rule ordering
quickshort-circuit- Anchor ordering
- Tag assignment via
match, tag carry through NAT - Antispoof expansion into real block rules
rdr-todestination rewriting with re-evaluationmax-src-connrate limit enforcementroute-topolicy routingpfctl -nfintegration (runs automatically on systems with pfctl)- Auto-detects topology from your macros — zero hardcoded values
See LIMITS.md for the full honest assessment. The short version:
- No real packets, no TCP state, no fragmentation
- Regex parser, not PF's grammar — always run
pfctl -nftoo (iface)self-addresses are approximatedscrubis parsed and ignoreddivert-to,binat-to, inline anchors not supported
It's a rule evaluator, not a replacement for pfctl or pen testing.
Why Python? It's a test tool, not a daemon. Runs on your workstation, not the router. Zero dependencies beyond stdlib.
Why not just pfctl -nf?
pfctl checks syntax. pftest checks logic. Three bugs passed
pfctl -nf clean and would have broken production. They're
complementary — pftest runs pfctl -nf automatically when it's
available.
Does it actually match PF behavior? For rule evaluation order — yes. Verified against config-independent engine tests and parity-tested against real PF rulesets. It doesn't simulate TCP connections, state tables, or scrub — those are runtime behaviors that require real packets on real PF. Documented in LIMITS.md.
Will it work with my config?
If your config uses standard pf.conf syntax with macros, tables,
and anchors loaded from files — yes. Drop it in and run it. If it
uses divert-to, binat-to, inline anchors, or probability — those
features aren't parsed. See LIMITS.md for the full list.
How do I write tests for my network?
See TESTING.md. Import PFSimulator and Packet, build packets
that match your topology, assert the results.
BSD 2-Clause — see LICENSE