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.
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.
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.
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.
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.
Let's break down this file line by line.
set skip on lo
This is part of the defaultpf.conf(5)
file. It stopspf
from evaluating traffic on the loopback interfaces. This is fine.block return
Block all traffic by default# pass
We comment out thepass
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.
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.