OpenBSD pf — The Packet Filter
pf is OpenBSD's packet filter, introduced in 3.0 as a replacement for IPFilter after a licensing dispute. It's one of those tools where the design decisions are clearly right — the syntax is readable, the behavior is predictable, and the documentation is excellent. If you've wrestled with iptables and then sat down with pf for the first time, the difference is noticeable immediately.
Getting pf Running
On OpenBSD, pf is enabled in /etc/rc.conf.local:
pf=YES
On FreeBSD, it's a kernel module:
# /etc/rc.conf pf_enable="YES" pf_rules="/etc/pf.conf"
Runtime control is through pfctl:
pfctl -e # enable pf pfctl -d # disable pf pfctl -f /etc/pf.conf # load config pfctl -nf /etc/pf.conf # test config without loading (do this first) pfctl -sr # show current ruleset pfctl -ss # show state table pfctl -si # show info and statistics
Structure of pf.conf
pf.conf has a defined order: macros, tables, options, normalization, then rules. This order matters — pf processes the file top to bottom and some things must be defined before they're referenced.
Macros
Variables that make the config readable and easier to update:
ext_if = "em0"
int_if = "em1"
lan = "192.168.1.0/24"
ssh_port = "22"
web_ports = "{ 80, 443 }"
Macros are used with a $ prefix: $ext_if, $lan, etc. Changing an interface name or IP range is one line instead of twenty.
Tables
Tables hold lists of IP addresses and can be updated at runtime without reloading the whole ruleset:
table <bruteforce> persist
table <martians> const { 0.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, \
172.16.0.0/12, 192.0.2.0/24, 224.0.0.0/3 }
block in quick from <martians>
block in quick from <bruteforce>
Add to a table at runtime:
pfctl -t bruteforce -T add 203.0.113.42 pfctl -t bruteforce -T show # list all entries pfctl -t bruteforce -T expire 86400 # expire entries older than 1 day
Options
set skip on lo # don't filter loopback traffic set block-policy drop # silently drop blocked packets (vs. return RST) set loginterface $ext_if # log stats on the external interface set state-policy if-bound # bind states to the interface they arrived on
Normalization
scrub in all fragment reassemble # reassemble fragments before filtering scrub in on $ext_if all random-id # randomize IP ID field (fingerprint hardening)
A Real pf.conf
A complete example for a machine with one external interface acting as a basic firewall:
ext_if = "em0"
lan = "192.168.1.0/24"
table <bruteforce> persist
set skip on lo
set block-policy drop
scrub in all fragment reassemble
# NAT for internal network
nat on $ext_if from $lan to any -> ($ext_if)
# Default deny
block all
# Block brute-forcers
block in quick from <bruteforce>
# Allow established connections
pass in on $ext_if proto tcp from any to any keep state \
(max-src-conn-rate 15/5, overload <bruteforce> flush global)
# Allow specific inbound services
pass in on $ext_if proto tcp to port 22 keep state
pass in on $ext_if proto tcp to port { 80, 443 } keep state
# Allow all outbound
pass out on $ext_if all keep state
# Allow internal traffic
pass in on em1 from $lan to any keep state
The max-src-conn-rate 15/5, overload <bruteforce> part is pf's built-in brute-force protection: if any source makes more than 15 connections in 5 seconds, it gets added to the bruteforce table and all its existing states are flushed. Elegant.
NAT and Redirection
# Basic masquerade NAT nat on $ext_if from $lan to any -> ($ext_if) # Port forward — send incoming port 80 to internal web server rdr on $ext_if proto tcp to port 80 -> 192.168.1.10 port 80 # Redirect local traffic meant for external IP back to internal server # (hairpin NAT) rdr on $int_if proto tcp from $lan to $ext_if port 80 -> 192.168.1.10
Logging
Add log to any rule to send matching packets to the pflog0 interface:
pass in log on $ext_if proto tcp to port 22 keep state
Read the log in real time:
tcpdump -n -e -ttt -i pflog0
Or write to a file and inspect later:
tcpdump -n -e -ttt -r /var/log/pflog
Anchors
Anchors let you load additional rulesets from separate files, or update parts of your ruleset without touching the main config. Useful for things like authpf (per-user firewall rules) or separating rules by function:
anchor "webserver" load anchor "webserver" from "/etc/pf.webserver.conf"
dispelled