How to get generic interface names and IPs in OpenBSD PF

December 4, 2020

Over on Twitter, I had a grump that led to me learning some things:

Since OpenBSD Ethernet interface names are tied to the physical hardware, I wish OpenBSD pf.conf syntax had an abstract name that meant 'the interface with the default route, whatever that is'.

We have straightforward OpenBSD systems with a single active interface that get installed on various hardware, with various interface names, and I would really like to not have to change their pf.conf for every different piece of hardware they get put on (physical or virtual).

Thanks to @oclsc I've now learned about OpenBSD interface group names, and reading the manpage pointed me to the predefined 'egress' group name, which means 'all interfaces with the default route'. You can use 'egress:0' to mean the (first) egress IP address.

We're not savages, so of course we use pf.conf macros in our pf.confs:

server_if = "bnx0"
server_ip =

pass in on $server_if from any to $server_ip port = 22

But this still means that we have to define the name of the interface and the server's IP once. When we have two OpenBSD machines that are clones of each other, for example two OpenVPN servers for redundancy, this historically means that we've had to have two copies of pf.conf that are supposed to differ only in server IP.

(Usually we put the two servers on identical hardware, so the interface names are the same. If we used sufficiently different hardware that the interface names changed, we'd have to vary that too.)

However, it turns out that you can get what I want and, for simple configurations, reduce this to a completely generic version. First and well documented in the manpage for pf.conf is that you can use an interface name in PF rules in place of an IP address (cf). If you do, it means all of the IP addresses associated with that interface. If you want to not accept aliases, you can add :0 on the end to get the first one (cf). Combined with macros, we can write:

server_ip = $server_if:0

Written this way, the address is looked up once when the PF ruleset is loaded and then substituted in. If you dump the installed rules with 'pfctl -s rules', you'll see the actual IPs, and the resulting rules are exactly the same as if you'd specified the IP directly.

To get generic names for interfaces, we need to use the name of interface groups, which are documented in the ifconfig manpage. Conveniently there is a predefined group that does what I want, the 'egress' group:

  • The interfaces the default routes point to are members of the “egress” interface group.

(There can be more than one default route pointing out more than one interface, but in normal use on our servers there is exactly one interface with a default route.)

If you don't want to rely on where the default route is pointing you can explicitly specify a custom group in /etc/hostname.ifname and then use it in a macro in the PF rules. I'm not sure how to best write this in the file, but sort of following information from the hostname.if(5) manpage, I found that it worked to write it on two lines:

inet 0xffffff00
group net-sandbox

All of this sounds great but it has a tiny little drawback, which is that it makes your PF configuration a bit more magical. Explicitly writing out the interface name and IP may be annoying some of the time, but it's always extremely obvious what is going on. You don't have to try to remember what 'egress' or 'net-sandbox' means when used as an interface name (or an IP address); it's always right there. Also, you're absolutely guaranteed that your rules are matching only a single IP address or a single interface. With interface group names, you're relying on the rest of your configuration to insure that there is only ever one 'egress' or 'net-sandbox' interface, no matter what you do to the machine.

A related issue is that the meaning of 'egress', 'net-sandbox', and the like can change between PF ruleset loads (and the associated IPs along with them), without any direct changes to pf.conf. This means that you can boot with the system in one setup, change it for some reason, do a 'pfctl -f /etc/pf.conf' with an unchanged pf.conf, and wind up with a different set of rules. In some environments this is a feature; in others it is a drawback, or at least a potential unpleasant surprise.

(What triggered this otday was testing out a version of our OpenBSD VPN servers on the current OpenBSD in a virtual machine. Of course I needed their standard pf.conf, but my virtual machine had both a different IP address and a different interface than the current real servers.)

Comments on this page:

By Corey at 2020-12-13 11:40:35:

I don’t know how well this works in larger-scale scenarios, but you can also define a macro for your interface name in pf.conf, like $internet = em0, and then reference it that way in the rest of the file. Even syntax like $internet:0 works. At least then you can see the definition in the same file, so perhaps not as much magic.

Another option to use in conjunction with that is the include directive, so all your machine-specific definitions can be in a small header, with the bulk of your pf..conf include-d as a generic piece.

By Janne Johansson at 2020-12-16 05:06:23:

A small note that egress can point to several interfaces if IPv4 and IPv6 have different default routes, which could be more common than multiple default route entries for IPv4 only.

Written on 04 December 2020.
« Some thoughts about low power loads and power supply efficiency
Linux's hostname -s switch is now safe for many people, but the situation is messy »

Page tools: View Source, View Normal, Add Comment.
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Fri Dec 4 23:42:38 2020
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.