About me: My name is Solène Rapenne, pronouns she/her. I like learning and sharing knowledge. Hobbies: '(BSD OpenBSD Qubes OS Lisp cmdline gaming security QubesOS internet-stuff). I love percent and lambda characters. OpenBSD developer solene@. No AI is involved in this blog.

Contact me: solene at dataswamp dot org or @solene@bsd.network (mastodon).

I'm a freelance OpenBSD, FreeBSD, Linux and Qubes OS consultant, this includes DevOps, DevSecOps, technical writing or documentation work.

If you enjoy this blog, you can sponsor my open source work financially so I can write this blog and contribute to Free Software as my daily job.

Full WireGuard setup with OpenBSD

Written by Solène, on 09 October 2021.
Tags: #openbsd #wireguard #vpn

Comments on Fediverse/Mastodon

1. Introduction §

We want all our network traffic to go through a WireGuard VPN tunnel automatically, both WireGuard client and server are running OpenBSD, how to do that? While I thought it was simple at first, it soon became clear that the "default" part of the problem was not easy to solve, fortunately there are solutions.

This guide should work from OpenBSD 6.9.

pf.conf man page about NAT

WireGuard interface man page

ifconfig man page, WireGuard section

2. Setup §

For this setup I assume we have a server running OpenBSD with a public IP address ( for the example) and an OpenBSD computer with Internet connectivity.

Because you want to use the WireGuard tunnel as the default route, you can't define a default route through WireGuard as this, that would prevent our interface to reach the WireGuard endpoint to make the tunnel working. We could play with the routing table by deleting the default route found on the interface, create a new route to reach the WireGuard server and then create a default route through WireGuard, but the whole process is fragile and there is no right place to trigger a script doing this.

Instead, you can assign the network interface used to access the Internet to the rdomain 1, configure WireGuard to reach its remote peer through rdomain 1 and create a default route through WireGuard on the rdomain 0. Quick explanation about rdomain: they are different routing tables, default is rdomain 0 but you can create new routing tables and run commands using a specific routing table with "route -T 1 exec ping perso.pw" to make a ping through rdomain 1.

    |   server    | wg0:
    |             |---------------+
    +-------------+               |
           | public IP            |
           |              |
           |                      |
           |                      |
    /\/\/\/\/\/\/\                |WireGuard
    |  internet  |                |VPN
    \/\/\/\/\/\/\/                |
           |                      |
           |                      |
           |rdomain 1             |
    +-------------+               |
    |   computer  |---------------+
    +-------------+ wg0:
                    rdomain 0 (default)

3. Configuration §

The configuration process will be done in this order:

  1. create the WireGuard interface on your computer to get its public key
  2. create the WireGuard interface on the server to get its public key
  3. configure PF to enable NAT and enable IP forwarding
  4. reconfigure computer's WireGuard tunnel using server's public key
  5. time to test the tunnel
  6. make it default route

Our WireGuard server will accept connections on address at the UDP port 4433, we will use the network for the VPN, the server IP on WireGuard will be and this will be our future default route.

3.1. On your computer §

We will make a simple script to generate the configuration file, you can easily understand what is being done. Replace " 4433" by your IP and UDP port to match your setup.

PRIVKEY=$(openssl rand -base64 32)
cat <<EOF > /etc/hostname.wg0
wgkey $PRIVKEY
wgpeer wgendpoint 4433 wgaip

# start interface so you can get the public key
# we should have an error here, this is normal
sh /etc/netstart wg0

PUBKEY=$(ifconfig wg0 | grep 'wgpubkey' | cut -d ' ' -f 2)
echo "You need $PUBKEY to setup the remote peer"

3.2. On the server §

3.2.1. WireGuard §

Like we did on the computer, we will use a script to configure the server. It's important to get the PUBKEY displayed in the previous step.

PRIVKEY=$(openssl rand -base64 32)

cat <<EOF > /etc/hostname.wg0
wgkey $PRIVKEY
wgpeer $PUBKEY wgaip
wgport 4433

# start interface so you can get the public key
# we should have an error here, this is normal
sh /etc/netstart wg0

PUBKEY=$(ifconfig wg0 | grep 'wgpubkey' | cut -d ' ' -f 2)
echo "You need $PUBKEY to setup the local peer"

Keep the public key for next step.

3.3. Firewall §

You want to enable NAT so you can reach the Internet through the server using WireGuard, edit /etc/pf.conf to add the following line (after the skip lines):

pass out quick on egress from wg0:network to any nat-to (egress)

Reload with "pfctl -f /etc/pf.conf".

NOTE: if you block all incoming traffic by default, you need to open UDP port 4433. You will also need to either skip firewall on wg0 or configure PF to open what you need. This is beyond the scope of this guide.

3.4. IP forwarding §

We need to enable IP forwarding because we will pass packets from an interface to another, this is done with "sysctl net.inet.ip.forwarding=1" as root. To make it persistent across reboot, add "net.inet.ip.forwarding=1" to /etc/sysctl.conf (you may have to create the file).

From now, the server should be ready.

3.5. On your computer §

Edit /etc/hostname.wg0 and paste the public key between "wgpeer" and "wgaip", the public key is wgpeer's parameter. Then run "sh /etc/netstart wg0" to reconfigure your wg0 tunnel.

After this step, you should be able to ping from your computer (and from the server). If not, please double check the WireGuard and PF configurations on both side.

3.6. Default route §

This simple setup for the default route will truly make WireGuard your default route. You have to understand services listening on all interfaces will only attach to WireGuard interface because it's the only address in rdomain 0, if needed you can use a specific routing table for a service as explained in rc.d man page.

Replace the line "up" with the following:

wgrtable 1
!route add -net default

Your configuration file should look like this:

wgkey YOUR_KEY
wgpeer YOUR_PUBKEY wgendpoint REMOTE_IP 4433 wgaip
wgrtable 1
!route add -net default

Now, add "rdomain 1" to your network interface used to reach the Internet, in my setup it's /etc/hostname.iwn0 and it looks like this.

join network wpakey superprivatekey
join home wpakey notsuperprivatekey
rdomain 1

Now, you can restart network with "sh /etc/netstart" and all the network should pass through the WireGuard tunnel.

4. Handling DNS §

Because you may use a nameserver in /etc/resolv.conf that was provided by your local network, it's not reachable anymore. I highly recommend to use unwind (in every case anyway) to have a local resolver, or modify /etc/resolv.conf to use a public resolver.

unwind can be enabled with "rcctl enable unwind" and "rcctl start unwind", from OpenBSD 7.0 you should have resolvd running by default that will rewrite /etc/resolv.conf if unwind is started, otherwise you need to write "nameserver" in /etc/resolv.conf

5. Bypass VPN §

If you need for some reason to run a program and not route its traffic through the VPN, it is possible. The following command will run firefox using the routing table 1, however depending on the content of your /etc/resolv.conf you may have issues resolving names (because is only reachable on rdomain 0!). So a simple fix would be to use a public resolver if you really need to do so often.

route -T 1 exec firefox

route man page about exec command

6. WireGuard behind a NAT §

If you are behind a NAT you may need to use the KeepAlive option on your WireGuard tunnel to keep it working. Just add "wgpka 20" to enable a KeepAlive packet every 20 seconds in /etc/hostname.wg0 like this:

wgpeer YOUR_PUBKEY wgendpoint REMOTE_IP 4433 wgaip wgpka 20

ifconfig man page explaining wgpka parameter

7. Conclusion §

WireGuard is easy to deploy but making it a default network interface adds some complexity. This is usually simpler for protocols like OpenVPN because the OpenVPN daemon can automatically do the magic to rewrite the routes (and it doesn't do it very well) and won't prevent non-VPN access until the VPN is connected.