BSD vs. Linux Firewalling
If you've used pf on OpenBSD and then had to write iptables rules on Linux, the culture shock is real. The tools reflect the different philosophies of the systems they come from — BSD tends toward integrated, opinionated design; Linux tends toward modularity and flexibility, which sometimes means more pieces to learn. Here's how they compare.
The Tools at a Glance
| OpenBSD pf | FreeBSD ipfw | Linux nftables | |
|---|---|---|---|
| Integration | Tightly built into OpenBSD; also available on FreeBSD | Native to FreeBSD | In the Linux kernel; works across all distros |
| Syntax | Clean, human-readable pf.conf | Command-line oriented, less intuitive | Modern and consistent, steeper initial curve |
| Stateful | On by default with keep state |
Requires explicit keep-state |
Via conntrack, on by default with ct state |
| NAT | Built-in, clean syntax | Built-in via ipnat or divert | Via nat table hooks |
| Traffic shaping | altq (built-in) | dummynet (built-in) | tc (separate tool) |
Syntax Comparison
The same rule — allow SSH in, block everything else — written three ways:
OpenBSD pf
block all pass in on egress proto tcp to port 22 keep state
This reads almost like a sentence. block all is the default deny. pass in punches the specific hole.
FreeBSD ipfw
ipfw add 100 allow tcp from any to any 22 in keep-state ipfw add 65535 deny all from any to any
ipfw processes rules by number, lowest first. The 65535 deny all is the catch-all at the end. It works, but rule ordering is your problem to manage.
Linux nftables
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
tcp dport 22 ct state new accept
ct state established,related accept
}
}
nftables groups rules into tables and chains. More verbose upfront, but scales better for complex rulesets — you can add tables for specific purposes without them interfering with each other.
Stateful Filtering
All three support stateful filtering, but they surface it differently:
# pf — keep state on the pass rule pass in proto tcp to port 80 keep state # ipfw — keep-state flag ipfw add allow tcp from any to any 80 keep-state # nftables — conntrack matching ct state established,related accept tcp dport 80 ct state new accept
The nftables approach of separating the "allow established" rule from the "allow new" rule is actually clearer once you're used to it. pf's keep state inline is more compact.
NAT
# pf
nat on egress from 192.168.1.0/24 to any -> (egress)
# nftables
table ip nat {
chain postrouting {
type nat hook postrouting priority 100;
ip saddr 192.168.1.0/24 oifname "eth0" masquerade
}
}
pf's NAT syntax is simpler. nftables' hook-based approach is more explicit about where in the packet processing pipeline the rule applies — which matters if you're doing complex things, and doesn't matter at all if you're just masquerading a home network.
Which to Use
This is mostly decided for you by the OS. If you're on OpenBSD, you use pf — it's deeply integrated and the rest of the system (authpf, relayd, etc.) assumes it. On FreeBSD you have a choice; pf is available and well-supported there too, and most people prefer it over ipfw for new setups. On Linux you use nftables, or ufw/firewalld on top of it if you want a simpler interface.
The philosophical difference is real but rarely matters in practice: BSD firewalls feel more opinionated and integrated; Linux firewalls feel more like infrastructure you assemble. Both get the job done.
dispelled