Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pftest

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.

What it caught

Before the first deploy to hardware, pftest found:

  1. DHCP broken by antispoofantispoof quick generated block rules that matched 0.0.0.0 (DHCP discover source) before the DHCP pass rule could fire. pfctl -nf passed clean.

  2. Printer exception killed by missing quick — a pass in rule for a specific device was overridden by a later block quick on the same port. The printer lost HTTP access.

  3. VPN route-to silently overridden — port-specific pass rules loaded after a broad route-to rule. 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.

Quick start

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

Write your own tests

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.

How it works

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)
  • quick short-circuits immediately
  • match rules 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.

What it evaluates

  • Last-match-wins rule ordering
  • quick short-circuit
  • Anchor ordering
  • Tag assignment via match, tag carry through NAT
  • Antispoof expansion into real block rules
  • rdr-to destination rewriting with re-evaluation
  • max-src-conn rate limit enforcement
  • route-to policy routing
  • pfctl -nf integration (runs automatically on systems with pfctl)
  • Auto-detects topology from your macros — zero hardcoded values

What it doesn't do

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 -nf too
  • (iface) self-addresses are approximated
  • scrub is parsed and ignored
  • divert-to, binat-to, inline anchors not supported

It's a rule evaluator, not a replacement for pfctl or pen testing.

Because you'll ask

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.

License

BSD 2-Clause — see LICENSE

About

Offline PF ruleset matcher that catches firewall logic bugs before deployment — parses real pf.conf syntax, evaluates packet flow with full last-match-wins semantics, and found three production-blocking bugs in its first use.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages