Vivian Voss

pf

freebsd security unix tooling

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.

From Crisis to Default Firewall in One Summer 30 May 2001 IPFilter removed licence dispute 24 Jun 2001 first pf commit 25 days later end of Jun 2001 filtering + NAT both working 1 Dec 2001 OpenBSD 3.0 pf ships default Hartmeier had never written kernel code in May. By December the firewall he wrote was the default. One does notice how short that interval is, compared to most rewrites.

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.

Same Job, Two Shapes pf iptables block in all pass in on em0 proto tcp to port 22 keep state pass out keep state three lines, one grammar chains: INPUT OUTPUT FORWARD PREROUTING POSTROUTING tables: filter nat mangle raw security targets: ACCEPT DROP REJECT LOG MASQUERADE SNAT DNAT … + conntrack module + tc for QoS five vocabularies, five tools filter, NAT, queue, scrub: same syntax last match wins, quick overrides state tracking built in tables and anchors extend, do not fork hook ordering matters, varies by path first match in chain, then jump --ctstate flag per rule, manually new feature, often new tool A rule in pf is a sentence. A rule in iptables is a tuple of flags applied to a table inside a chain. Both work. Only one of them you can read on a Friday afternoon and still trust on a Monday morning.

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.

Where pf Quietly Runs pf Hartmeier, 2001 BSD family OpenBSD 3.0 (2001), FreeBSD 5.3, NetBSD, DragonFly Apple platforms macOS Lion (2011), iOS, iPadOS Commercial pfSense, OPNsense, enterprise appliances The compliment nftables (2014): Linux's quiet imitation Roughly a billion devices, configured in a grammar that fits on a postcard.

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.