Technical Beauty ■ Episode 33
You have written this rule, or you have written something near
enough to it:
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT.
You have written it because the alternative is dropping every
packet that is the response to a packet you let through, which
is, on reflection, nearly all of them. You have also, at some
point, lost twenty minutes to the question of which chain a
packet enters first when it is being forwarded but also locally
addressed, and twenty more to whether INPUT runs
before or after FORWARD for a particular interface
bridge.
You are not a bad systems administrator. You are a systems administrator reading a tool that does not want to be read.
The Mid-Release Crisis
In May 2001, OpenBSD did something most operating systems would not have the nerve to do mid-release. It pulled its firewall out of the source tree.
The firewall was IPFilter, written and maintained by Darren Reed since the mid-1990s. It was the de-facto BSD-world packet filter, deeply integrated into OpenBSD, and a fixture of the OpenBSD security story. Reed informed the OpenBSD project that IPFilter's licence did not in fact permit the kinds of modifications OpenBSD was making. After some unproductive correspondence, Theo de Raadt removed IPFilter from the OpenBSD CVS tree on 30 May 2001. The release was four months away.
This left OpenBSD without a packet filter. Nobody, internally, was lined up to write one.
The Wrong Person for the Job
Daniel Hartmeier was a Swiss developer who had been contributing peripherally to OpenBSD. He had never written kernel code. He read about Drawbridge, an Ethernet-layer filter from Texas A&M, and noticed that the filter itself was essentially a single C module with a small, comprehensible kernel interface. That, he thought, looked like something he could learn.
In a later interview he was characteristically dry about the experience: "If I had known in advance how many nights I would spend, I might have given up. But the progress kept me motivated."
He committed the first version of pf to the OpenBSD CVS tree on 24 June 2001, twenty-five days after IPFilter was removed. By the end of that month, his code was filtering packets and performing network address translation. Five months later, on 1 December 2001, pf shipped as the default firewall in OpenBSD 3.0.
A man who had never touched the kernel wrote, in four weeks of summer evenings and a few months of polish, a firewall that would still be running, twenty-five years later, on roughly a billion devices.
The Design
The point of pf is that the rules read like English. Here is a working firewall:
block in all
pass in on em0 proto tcp to port 22 keep state
pass out keep state
Three lines. The first denies all inbound traffic by default. The second allows inbound TCP to port 22 (SSH) on the external interface, and tells pf to remember the connection in its state table so the responses get back. The third allows all outbound traffic, again with state.
That is the whole firewall. Not the introduction. Not the simplified example. The whole firewall.
There are no chains. There are no separate tables for filter,
NAT, mangle, and raw. There is no
conntrack
module to load and configure. There is no question about which
hook a packet enters first under what bridging conditions.
Rules are read top to bottom. The last matching rule wins,
unless a rule is marked quick, in which case it
short-circuits. That is the entire evaluation model.
The grammar is small enough to fit on the back of a postcard, but expressive enough that the same vocabulary handles filtering, network address translation, traffic queueing, packet scrubbing, anchors (named subgroups for hierarchical configuration), tables (named lists of addresses, updatable at runtime), and policy routing. New features extend the grammar. They do not invent new tools and new flags.
The Contrast
iptables was, as it happens, also released in 2001, also as a successor to an older system (ipchains). Rusty Russell wrote it. It is, by any reasonable engineering standard, a competent piece of work. It is also, by any reasonable readability standard, the wrong shape.
In iptables, a rule is a tuple of flags applied imperatively to
a table inside a chain inside a hook point. The user has to
know about chains (INPUT, OUTPUT,
FORWARD, PREROUTING,
POSTROUTING), tables (filter,
nat, mangle, raw,
security), targets (ACCEPT,
DROP, REJECT, LOG,
MASQUERADE, SNAT, DNAT),
and the order in which all of these interact for a packet
that is, say, locally generated and forwarded over a bridge to
a destination behind NAT. Connection tracking is a separate
kernel module with its own configuration. Quality of service
is a separate tool entirely (tc). The whole
apparatus rewards memorisation rather than reasoning.
This is, as I understand it, why nftables exists. The Linux netfilter team, in roughly the late 2000s, took a long look at iptables, took a long look at pf, and rather quietly concluded that the second one had the right shape. nftables, released on 19 January 2014, borrows pf's design philosophy without quite borrowing pf itself: one consistent grammar, one tool, one configuration model. One does admire the gesture, even as one notes that the conversion is still ongoing more than a decade later.
The Proof
Twenty-five years on, pf is the firewall in OpenBSD, FreeBSD, NetBSD, DragonFly BSD, and macOS. It is what your iPhone and iPad use to filter network traffic. It is what runs underneath pfSense, OPNsense, and a substantial fraction of the commercial firewall appliances sold to enterprises that have no idea their security perimeter is configured in a Swiss developer's grammar.
The current OpenBSD pf is not the same code as Hartmeier's 2001 commit. Henning Brauer and Ryan McBride, in particular, have extended it considerably: redesigned the rule evaluation engine, added sophisticated state-tracking options, integrated with CARP for high availability, and rebuilt the queueing system. The grammar, however, has remained backward-compatible to a remarkable degree. A pf.conf written in 2003 still mostly parses on a current OpenBSD machine. The dialect has grown, but the language has not changed.
This is one of the markers of a well-designed system: the original creators no longer need to be there for the work to continue, and the work that follows feels of a piece with the work that began.
The Point
pf was not engineered to scale to a billion devices. It was engineered to be readable. Hartmeier wrote it because the alternative was unreadable, and because OpenBSD needed a firewall in less time than was reasonable, and because he wanted to learn the kernel. Twenty-five years later it is the firewall language of the BSD world and a substantial portion of the Apple device estate, and Linux has, in nftables, paid it the considerable compliment of imitation.
Sometimes a system holds up because the original design knew what was missing. pf is one of those systems.
30 May 2001: IPFilter pulled from OpenBSD over a licence dispute. 24 June 2001: Hartmeier's first pf commit. End of June: filtering and NAT both working. 1 December 2001: ships in OpenBSD 3.0. 2004: FreeBSD adopts. 2011: macOS adopts. 2014: nftables ships, and rather noticeably looks like pf. A grammar that fits on a postcard, running on roughly a billion devices.