A couple of years ago I published a blog post about creating an OpenBSD VPN gateway using OpenVPN.

I've recently switched from an OpenVPN-based VPN provider to one that uses Wireguard. As a result I've had to redo my VPN gateway.

I'll only be highlighting the things I've changed since the last setup in this post, so please refer to the previous post for more details.

One advantage this iteration has over the previous one is that it no longer requires third party software to be installed on the OpenBSD router. Everything required comes as part of the base system. We will also be taking advantage of routing tables to restrict what we send through the VPN.

The purpose of the VPN gateway is to allow any device on the network to send its traffic through a VPN without installing anything. Instead of installing one profile per device, the client just sets the VPN Gateway as its default route.

Here's a diagram of what we're building.

diagram.png

Unlike the previous setup, in this version we're going to create a separate routing table for the VPN. This affords us a lot of flexibility, as we can be very explicit how we route our traffic. In this setup, only packets coming in one interface with a source address on the local network will be sent through, as opposed to all traffic leaving the router. We can also selectively send traffic from the router through the VPN using the route(8) command.

route -T <rtable> exec <program>

The first step in the process is getting the VPN profile from the VPN provider. It should look something like the following.

[Interface]
PrivateKey = PRIVATEKEY
Address = XX.XX.XX.XX/32,YYYY:YYYY:YYYY:YYYY:YYYY:YYYY:YYYY/128
DNS = ZZ.ZZ.ZZ.ZZ

[Peer]
PublicKey = PUBLICKEY
AllowedIPs = 0.0.0.0/0,::0/0
Endpoint = ENDPOINT:51820
profile.conf

We then have to rewrite it into OpenBSD's hostname.if(5) format. We'll call it /etc/hostname.wg0 to create a Wireguard interface and execute the following commands when it's created.

inet XX.XX.XX.XX/32
inet6 YYYY:YYYY:YYYY:YYYY:YYYY:YYYY:YYYY/128
wgkey  PRIVATEKEY
wgpeer PUBLICKEY wgaip 0.0.0.0/0 wgaip ::0/0 wgendpoint ENDPOINT 51820
/etc/hostname.wg0

In our setup, since we want to setup a routing table where the VPN is the default route, we need to create it and set the routes accordingly. We can do this by adding commands to the end of our config file. Lines beginning with ! are commands that are run as root when the interface is being created. In this case our new routing table (rtable) will be number 1. The default routing table is number 0.

inet XX.XX.XX.XX/32
inet6 YYYY:YYYY:YYYY:YYYY:YYYY:YYYY:YYYY/128
wgkey  PRIVATEKEY
wgpeer PUBLICKEY wgaip 0.0.0.0/0 wgaip ::0/0 wgendpoint ENDPOINT 51820

!route -T 1 add -inet  default XX.XX.XX.XX
!route -T 1 add -inet6 default YYYY:YYYY:YYYY:YYYY:YYYY:YYYY:YYYY
/etc/hostname.wg0

We can bring up the interface using the command sh /etc/netstart wg0.

Now that our interfaces are setup, we need to create the firewall rules that will take care of the routing and NAT. We use a couple macros here ($ext_if and $vpn_if) to make it easy to change the interface names if we ever have to.

set skip on lo

block return    # block stateless traffic
# pass          # establish keep-state

ext_if = "vio0"
vpn_if = "wg0"

# Don't send traffic for us (ssh) through the VPN
pass in quick on $ext_if proto tcp from $ext_if:network to self port 22

pass in on $ext_if from $ext_if:network rtable 1
pass out on $ext_if from self

match out on $vpn_if from $ext_if:network to any nat-to $vpn_if
pass out on $vpn_if
/etc/pf.conf

Let's break down this file line by line.

  • set skip on lo This is part of the default pf.conf(5) file. It stops pf from evaluating traffic on the loopback interfaces. This is fine.
  • block return Block all traffic by default
  • # pass We comment out the pass rule. This is part of the default configuration to allow all traffic to pass in and out. We want to only allow traffic we explicitly specify through so we remove it.
  • ext_if = "vio0" Create a macro for the main egress interface. This interface will be connected to our network and also have access to the internet.
  • vpn_if = "wg0" Create a macro for the VPN interface.
  • pass in quick on $ext_if proto tcp from $ext_if:network to self port 22 Here we allow any traffic directly addressing our server on TCP port 22 to pass in without any further rule evaluations. This lets us SSH into our server without the packets being put into the VPN routing table.
  • pass out on $ext_if from self This lets us connect to the internet from the VPN server.
  • match out on $vpn_if from $ext_if:network to any nat-to $vpn_if This rule will take any traffic coming in from out network and NAT it to our VPN interface.
  • pass out on $vpn_if This lets the traffic out through the VPN interface.

We can apply the file without rebooting with the command pfctl -f /etc/pf.conf

Finally we need to make sure our machine will forward traffic. We can do this by adding a line to our sysctl.conf(5) file.

net.inet.ip.forwarding=1
/etc/sysctl.conf

We can change the variable without rebooting with the command sysctl net.inet.ip.forwarding=1

Now all traffic coming from the network through this router should be NAT-ed and sent over the VPN.