WireGuard kernel support on OpenBSD is coming (hopefully) soon! It’s not in the -current branch or snapshots quite yet. The repository where it’s being worked on is here and patches have been submitted by the developers to the tech@ mailing list. Once kernel support is in the -current branch of OpenBSD, I’ll try to publish an updated post using the kernel support instead of wireguard-go.

I was inspired to write this by a friend who wanted to automatically tunnel traffic from specific devices on their local network over a WireGuard VPN. A few other resources were helpful in getting up to speed on WireGuard and OpenBSD routing:

Prerequisites

You need a WireGuard configuration file from your VPN provider. For example, Mullvad has a page to generate a config file here. There is an interesting post here about why Mullvad supports WireGuard.

You also need root access to an OpenBSD router with PF running. It shouldn’t matter if the OpenBSD router is directly connected to the internet or not. These steps were tested successfully on version 6.6 of OpenBSD, and they should work on 6.7 as well. All commands should be run as root (or with doas).

Install WireGuard

This will install WireGuard from packages. wireguard-go is a userland implementation of WireGuard written in Go. wireguard-tools provides the wg and wg-quick command-line tools. These steps will only use the wireguard_go daemon and the wg command.

pkg_add wireguard-go wireguard-tools

Configure the service

Set the tun interface you want WireGuard to use (we’ll use tun2 just because it’s available; you can use any available tun device number you want, but the rest of the steps here will assume you’re using tun2) and enable it so it will start on boot.

rcctl set wireguard_go flags tun2
rcctl enable wireguard_go

Set up the config file

This will put your WireGuard config file into /etc/wireguard and recursively set permissions on it so its only accessible by the root user. This assumes the config file is named mullvad-se12.conf and is in your current working directory (you can copy any valid config file to /etc/wireguard/client.conf).

mkdir /etc/wireguard
cp mullvad-se12.conf /etc/wireguard/client.conf
chown -R root:wheel /etc/wireguard
chmod -R 600 /etc/wireguard

For the config file to work with wg (instead of wg-quick, which doesn’t allow for the level of customization we’re doing here) you have to delete both the Address and DNS lines from the Interface section. Copy these lines somewhere else for now since you’ll need the values later.

Set up the network interface

Create the file /etc/hostname.tun2 with the following contents. Replace 10.32.4.3 with the IPv4 address from the Address line that you previously deleted from /etc/wireguard/client.conf (note that the /32 in the IPv4 address is replaced with the equivalent in dot-decimal notation).

description "WireGuard"
inet 10.32.4.3 255.255.255.255
mtu 1420
up

Bring up the interface.

sh /etc/netstart tun2

Apply your Wireguard configuration to the interface.

wg setconf tun2 /etc/wireguard/client.conf

Configure a routing table

This will configure routes in an alternate routing table that can be used for the traffic we want to be routed over the VPN. I initially also put the tun2 interface in an alternate rdomain, which worked fine, but I don’t think it’s strictly required here and we can get away with just a dedicated routing table (we’ll use PF later to select the routing table for specific incoming traffic).

We’ll use an ID of 2 for the routing table but you can use any value up to 255 (the default routing table is ID 0). Remember to replace 10.32.4.3 here with the actual address you configured in the hostname.tun2 file!

route -T 2 -n add -inet default -iface 10.32.4.3

One more route is needed to ensure that traffic to the WireGuard VPN peer is not routed over the VPN. Replace 185.65.134.128 with the IP address from the Endpoint line in /etc/wireguard/client.conf (make sure to remove the :51820 port specification) and replace 172.15.1.1 with your internet connection’s default gateway. If your OpenBSD router is connected directly to the internet, this is the gateway value in the output of the route -T 0 -n get default command.

route -T 2 -n add 185.65.134.128 -gateway 172.15.1.1

Configure PF

Add the following rules to your pf.conf file. The first rule will match any incoming traffic from the local device with IP address 10.0.0.100 and assign it to the alternate routing table you created. The second rule will then match outgoing traffic on the tun2 interface and NAT it to the IP address of the interface. Replace em1 with the name of the interface that is connected to your local network. You can also specify a range of IP addresses here, or adjust the rule to match all incoming traffic on an interface (e.g., a VLAN interface).

match in on em1 inet from 10.0.0.100 to any rtable 2
match out on tun2 inet from !(tun2:network) to any nat-to (tun2:0)

Save your changes and reload your ruleset to apply the changes.

pfctl -f /etc/pf.conf

Your local device(s) should have all traffic routed over the WireGuard VPN now! On the local device(s), you can check for DNS leaks and confirm your external IP address by using dnsleaktest.com or confirm you’re connected to Mullvad by using am.i.mullvad.net. On your OpenBSD router, you can use the wg show command to see current VPN peers and traffic statistics.

DNS

Nothing in these instructions covers DNS configuration to prevent DNS leaks! Mullvad does hijack all DNS requests and routes them to a Mullvad DNS server. For good measure (or if you’re not using Mullvad or another provider that does this automatically), you can manually set the DNS server on your local device, or configure your DHCP server to hand out the desired DNS server addresses to your local device or local subnet. For example, if your OpenBSD router is also your DHCP server and you want to configure Mullvad’s DNS server for the entire 10.0.1.0/24 subnet, include the following in /etc/dhcpd.conf.

subnet 10.0.1.0 netmask 255.255.255.0 {
    option routers 10.0.1.1;
    option domain-name-servers 193.138.218.74;
    range 10.0.1.100 10.0.1.199;
}

The Mullvad DNS server address is available here.

If you use Firefox you may also want to disable DNS over HTTPS (DoH).

Running as a non-root user

This is optional!

WireGuard merged a patch in 2019 to support running as a non-root user if you set the MTU on the tun interface instead of relying on the WireGuard daemon to do it. Unfortunately even with this patch, you still need to change the group ID (giving the group the daemon runs as rw permissions) on the tun device file in /dev for it to work. My knowledge of Go is extremely limited, but I think this is because the OpenFile function is called with O_RDWR, meaning it tries to open the file in read/write access mode. You can see this on line 125 of tun_openbsd.go (this link points to the version of the file that corresponds with the version of the package available for OpenBSD 6.6, but the line is the same in more recent versions).

If you still want to set up WireGuard to run as a non-root user, add a user for it to run as.

useradd -s /sbin/nologin -c "WireGuard" -d /var/empty _wireguard

Configure the daemon to run as the user you just created.

rcctl set wireguard_go user _wireguard

Set permissions on your configuration files and /dev/tun2.

chown -R _wireguard:_wireguard /etc/wireguard
chmod -R 600 /etc/wireguard
chown root:_wireguard /dev/tun2
chmod 660 /dev/tun2

Restart the daemon.

rcctl restart wireguard_go

Thanks!

Thanks for reading. This was a fun exercise in learning about WireGuard and OpenBSD routing for me and I hope it’s helpful for someone else. If you have any issues or questions feel free to contact me. Stay tuned for an updated post using the OpenBSD kernel support when it’s available.