Sometimes you may want to route your home traffic through a commercial VPN such as NordVPN, Private Internet Access, HideMyAss or something similar.

These VPN providers often use OpenVPN protocol, and would require you to install an App into your device (if you are on a phone, or iPad). With OpenBSD, you can configure your OpenBSD based home router to connect to those services, and selectively allow your devices at home (or even on the road), to use those services without installing such App into your device.

Sounds like a peace of mind right? Let's see how it can be done. In this example, we will re-use some of the configurations from my previous article that talks about setting up a OpenVPN router. And the following are the basic assumptions:

  1. Internet port is em0.
  2. Internal network port is em1.
  3. VPN traffic will be routed to tun0, assuming your VPN service provider is using OpenVPN.
  4. Your own IPSec IKEv2 VPN is routed from enc0 IPSec interface.

Subscribe to a VPN provider

First of all, you will need to subscribe to a VPN provider. It is your personal choice and the only requirement is that it should support OpenVPN. Then you can login to those service and download a OpenVPN configuration file. Each provider will have a slightly different OpenVPN configuration so some adjustments may be required.

OpenVPN configuration

Normally, those OpenVPN configuration will be provided in a way that it will route all traffic to the VPN provider via the following configuration pushed from the remote gateway:

push redirect-gateway def1

But we do not want the OpenVPN to take over all traffic, instead, we will use packet filter to selectively identify traffic that will be route through OpenVPN. Hence, we need to make sure that OpenVPN software does not override our routing.

This can normally done by adding the finding and replacing the routing configuration lines in the OpenVPN configuration with the following lines:

route-noexec
ifconfig-noexec

The above line will instruct OpenVPN client not to run any routing or ifconfig related commands.

We will replace them with our own commands, this can be done by using following lines in OpenVPN.

script-security 2
up up

The above 2 lines instructs OpenVPN to call a shell script, simply called up, when it established a connection with the server.

The up script would look like below:

#!/bin/ksh

conf=`basename $config | cut -f 1 -d '.'`
dns=`grep $conf /etc/openvpn/dns | cut -f 2 -d ' '`
logger -i -p daemon.info -t "ovpn$conf" "OpenVPN up script called"

env > /var/run/openvpn.$conf
echo $1 $2 $3 $4 $5 $6 $7 >> /var/run/openvpn.$conf/usr/bin/doas /etc/openvpn/up-helper $dev $ifconfig_local $ifconfig_netmask $tun_mtu $conf $dns $script_context
up shell script, modify according to your own need if necessary.

You will notice that the up shell script will call an up-helper shell script to execute the command. The up-helper shell script would look like below:

#!/bin/ksh

grep $5 /etc/openvpn/dns > /dev/null

if [ $? -ne 0 ]
then
  logger -i -t "openvpn helper" -p daemon.info "Wrong 5th argument in up-helper script"
  exit 5
fi

if [[ "x$1" != "xtun0" && "x$1" != "xtun1" ]]
then
  logger -i -t "ovpn$5" -p daemon.info "Wrong 1st argument in up-helper script"
  exit 1
fi

expr "$2" : '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$' > /dev/null
if [ $? -ne 0 ]
then
  logger -i -t "ovpn$5" -p daemon.info "Wrong 2nd argument in up-helper script"
  exit 2
fi

expr "$3" : '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$' > /dev/null
if [ $? -ne 0 ]
then
  logger -i -t "ovpn$5" -p daemon.info "Wrong 3rd argument in up-helper script"
  exit 3
fi

expr "$4" : '[0-9]*' > /dev/null
if [ $? -ne 0 ]
then
  logger -i -t "ovpn$5" -p daemon.info "Wrong 4th argument in up-helper script"
  exit 4
fi

expr "$6" : '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$' > /dev/null
if [ $? -ne 0 ]
then
  logger -i -t "ovpn$5" -p daemon.info "Wrong 6th argument in up-helper script"
  exit 6
fi

/sbin/ifconfig $1 $2 netmask $3 mtu $4 group egress group $5
rst=$?
logger -i -t "ovpn$5" -p daemon.info "/sbin/ifconfig $1 $2 netmask $3 mtu $4 group egress group $5 : Exit code $rst"
if [ $rst -ne 0 ]
then
  exit $rst
fi

/sbin/route -host delete $6
rst=$?
if [ $rst -eq 0 ]
then
logger -i -t "ovpn$5" -p daemon.info "Old route to $6 deleted"
fi

/sbin/route add $6 -iface $2
rst=$?
logger -i -t "ovpn$5" -p daemon.info "/sbin/route add $6 -iface $2 : Exit code $rst"
if [ $rst -ne 0 ]
then

/sbin/route change $6 -iface $2
rst=$?
logger -i -t "ovpn$5" -p daemon.info "/sbin/route change $6 -iface $2 : Exit code $rst"
fi
if [ $rst -ne 0 ]
then
  exit $rst
fi

/usr/sbin/rcctl reload unbnd$5
rst=$?
logger -i -t "ovpn$5" -p daemon.info "/usr/sbin/rcctl reload unbnd$5 : Exit code $rst"
exit $rst
up-helper script

This script will be doas enabled, with the following doas configuration:

permit nopass root cmd /etc/openvpn/up-helper
permit nopass _openvpn cmd /etc/openvpn/up-helper
/etc/doas.conf fragment, just append it to the last lines of the config file.

This above will allow the OpenVPN client to run the up script, which will subsequently run the up-helper script with doas, to emulate root calling ifconfig and route command to set up the network interfaces and routes. After all the routing are set up, it will also update the DNS servers' routing so that the designated DNS, specified in the file /etc/openvpn/dns, will be routed through the OpenVPN server.

The content of the file /etc/openvpn/dns would look like this:

us 103.86.96.100
/etc/openvpn/dns

Lastly, the OpenVPN configuration file should be put under /etc/openvpn/ and the filename should be set as us.conf in our example. In this example, up-helper script will detect that your configuration file is called us.conf, and will use the DNS configuration with the first column specified as us.

Since OpenVPN supports includes another configuration file in its parent configuration file, and the options that were specified later will take precedence, you could have the us.conf looks like below:

conf /etc/openvpn/nord.conf
remote-random
ifconfig-noexec
route-noexec
cd /etc/openvpn
script-security 2
verb 2
up up
user _openvpn
group _openvpn
ping-exit 10
auth-user-pass nord.pass
dev tun0
/etc/openvpn/us.conf - Full example

The OpenVPN configuration file that, for example, you downloaded from OpenVPN should be named as nord.conf and copied to /etc/openvpn folder. The folder should then contains the following files:

-rw-r--r--  1 root  wheel     34 Jan  1 12:22 dns
-rw-------  1 root  wheel     50 Dec 30 12:35 nord.conf
-rwxr-xr-x  1 root  wheel    368 Jan  5 17:59 up
-rwxr--r--  1 root  wheel   1828 Jan 14 19:40 up-helper
-rw-r--r--  1 root  wheel    195 Jan 14 20:48 us.conf
-rw-------  1 root  wheel     14 Jan 14 12:35 nord.pass
ls -l /etc/openvpn

It should contain the routing shell scripts, the routing helper, us.conf (the main configuration file), nord.conf (the configuration file you downloaded from NordVPN in this example) and nord.pass (the file that is only readable by root containing your username and password for NordVPN. You can change the filenames accordingly, or add other kinds of helper shell scripts if necessary.

Then configure OpenVPN to start during system boot, and start the service accordingly:

# run as root, or use doas
rcctl enable openvpn
rcctl set openvpn flags --config /etc/openvpn/us.conf

# Start OpenVPN
rcctl start openvpn

DNS configuration for OpenVPN

You will need another DNS server to serve the traffic that are routing through the VPN tunnel. This is also important to avoid what the VPN provides called DNS leakage. This means that although all your traffic are routed through the VPN, the DNS queries that you are making are still done through your normal Internet connection, which a lot of geographical blocking mechanisms will easily detect.

In this example, we will still use unbound, following our previous example, to serve DNS for this VPN connection. Since the usual DNS port, 53, is already used, we will need to configure unbound to use another port (in this example, port 56) and point its forwarder to the NordVPN DNS specified in the configuration above.

Since it is another instance of unbound, it will also use another configuration file, which we will put under /var/unbound/etc and is named unbndus.conf.

The file would look like below:

server:
        # Fully allow access from all hosts, since the router
        # is protected by firewall for easy setup and
        # troubleshooting
        #
        access-control: 0.0.0.0/0 allow
        access-control: ::0/0 allow
        do-ip6: yes
        port: 56
        pidfile: /var/run/unbound.pid
        hide-identity: yes
        hide-version: yes

        # Uncomment to synthesize NXDOMAINs from DNSSEC NSEC chains
        # https://tools.ietf.org/html/rfc8198
        #
        aggressive-nsec: yes

        # Mostly performance related catching adjustments
        #
        prefetch: yes
        num-threads: 4
        msg-cache-slabs: 16
        rrset-cache-slabs: 16
        infra-cache-slabs: 16
        key-cache-slabs: 16
        so-reuseport: yes
        rrset-cache-size: 100m
        msg-cache-size: 50m
        cache-max-ttl: 86400
        cache-min-ttl: 3600
        qname-minimisation: yes
        interface-automatic: yes

        # Security related settings
        #
        minimal-responses: yes
        use-caps-for-id: yes
        chroot: /var/unbound
        directory: /var/unbound
        rrset-roundrobin: yes
        
        # Specify a root certificate bundle to allow
        # DNS over TLS to work. Unbound will need to
        # verify the DNS over TLS certificate against
        # the root certificate
        #
        tls-cert-bundle: /etc/ssl/cert.pem

		# !!! IMPORTANT !!!
        # This specify the internal domain name that
        # your internal network will use. Typically
        # people simply use "lan"
        # This must also match with the relevant DHCP
        # setting in DNSMASQ
        #
        domain-insecure: "lan"

        # UDP EDNS reassembly buffer advertised to peers.
        # Default 4096. May need lowering on broken networks
        # with fragmentation/MTU issues,
        # particularly if validating DNSSEC.
        #
        edns-buffer-size: 1280
        
        # To allow local forward zone to be used.
        local-zone: "0.168.192.in-addr.arpa." transparent

# Forward zone for internal network
# This allows all internal name resolution
# to be forwarded to DNSMASQ in port 55
# for internal direct and reverse DNS
# resolution
#

forward-zone:
        name: "lan."
        forward-addr: 192.168.0.1@55
        forward-no-cache: yes

forward-zone:
        name: "0.168.192.in-addr.arpa."
        forward-addr: 192.168.0.1@55
        forward-no-cache: yes

# Default forward zone for external Name resolution
#
forward-zone:
        name: "."                               # use for ALL queries
        forward-addr: 103.86.96.100
/var/unbound/etc/unbndjs.conf

The above configuration tells unbound to listen to port 56, and forward all outgoing DNS queries to 103.86.96.100, the NordVPN DNS server. Change it to your own providers' DNS if necessary.

Next, setup a new service in /etc/rc.d. In OpenBSD, it is really simple and can be done by:

# Run as root, or use doas
ln -s /etc/rc.d/unbound /etc/rc.d/unbndus
rcctl enable unbndus
rcctl set unbndus flags -c /var/unbound/etc/unbndjs.conf

# Start unbndus
rcctl start unbndus

Packet Filter (PF) configuration and routing

Now, you have OpenVPN and DNS configuration completed. Last step is to ensure that we route traffics from selected machines into this newly established channel by configuring NAT from internal network to this channel, and to select the right machines to route to this channel.

NAT configuration can be done by adding the following match rules in the top of pf.conf, assuming that the OpenVPN tunnel is called tun0, as specified in the configuration above.

match out on tun0 inet scrub (random-id max-mss 1328 no-df) nat-to (tun0:0)
/etc/pf.conf fragment

Then, we need to select the traffics to route to this channel. This includes:

  1. Block all IPv6 traffic, just in case you enabled IPv6 on your router. (Line 1)
  2. Routing the DNS traffic to the new DNS server setup, and (Line 2)
  3. Routing the actual traffic to the OpenVPN tunnel. (Line 3)
block quick on em1 inet6 tagged us.example.com
pass in quick on { enc0 em1 } inet proto {tcp udp} from any to < self:0 > port 53 tagged us.example.com rdr-to em1:0 port 56 keep state (if-bound) label usdns
pass in quick on { enc0 em1 } inet from any to !em1:network tagged us.example.com route-to tun0 keep state (if-bound) label us

In the above example, we use a tagging rule. There are a couple of ways that you can tag the machines that you want to route through this VPN. First one is simplest:

match in from em1 from <internal IP> tag us.example.com
/etc/pf.conf fragment in the beginning of the rules that tags machine with specific IP address.

Obviously you will need a fixed IP address from that machine. So this is useful for IP TVs, or IoTs.

Or you can use the OpenBSD bridge interface, which allows you to tag incoming packets based on mac address. To do so, you can create a bridge ruleset file in /etc folder. I call it /etc/bridge0.rules.

pass on em1 src 40:cb:c0:e9:76:0c tag us.example.com
/etc/bridge0.rules

And you will need to create an Ethernet bridge. The simpliest form would contain only 1 interface, em1 in this example, by setting up the file hostname.bridge0.

add em1
rulefile /etc/bridge0.rules
up
/etc/hostname.bridge0

You can bring this interface up by issuing the following command:

# Run as root, or use doas
sh /etc/netstart.sh bridge0

There are other recommendations on creating a vether interface, but this is optional as long as em1 in our example will always be the internal network port. Please note that using bridge interface in OpenBSD would slow down the routing performance a bit. In my setup, I can route at about 500Mbit/s using a Fitlet2 machine with bridge interface turned on.

Lastly, you can also tag a packet for your own IPSec IKEv2 tunnel. This can be done by adding the tag keyword in your /etc/iked.conf.

ikev2 "Home" passive esp \
        from 0.0.0.0/0 to 0.0.0.0/0 \
        peer 0.0.0.0/0 \
        ikesa enc aes-256-gcm \
                prf hmac-sha2-256 \
                group modp2048 \
        childsa enc aes-256-gcm \
                group modp2048 \
        srcid $vpn_name \
        ikelifetime 0 \
        lifetime 0 bytes 0 \
        config address 192.168.1.0/24 \
        config name-server 192.168.0.1 \
        tag us.example.com
/etc/iked.conf

Then all your gadgets, once established VPN to your home router, will be routed through your new VPN provider. Since iked tag command also supports tagging based on the domain of your certificate, you can create rules that only routes certain kind of traffic from IPSec IKEv2 to OpenVPN, and the rest will still behave normally.

Summary

In the above example, we have illustrated how to:

  1. Setup a OpenVPN tunnel to an external VPN service provider,
  2. Configure OpenBSD DNS and routing to allow DNS queries going to the VPN provider.
  3. Configure packet filter firewall to selectively route your internal network traffic as well as IKEv2 VPN traffic to the VPN provider instead of your Internet connection.

With the above configuration, you can easily create IPSec IKEv2 profiles to allow your family members to use the VPN service without installing any Apps. If they are using an iOS or Mac OS device, all they have to do is to install a configuration profile that you can easily generate using my previous guide.

Readout