VPN Policy Based Routing for Hostnames

I need to route traffic to our staging server (and the production server, so I can access our administration interface) through a VPN. But I don’t want to have to connect to the VPN all of the time, it should automatically stay up, but only route traffic that goes to the specific sites.

I’ve had limited success with using domain-name based VPN policy routing on OpenWRT. It feels like it doesn’t really work. It could be that it doesn’t add all of the IP addresses for each domain name, and if the IP address changes, then it stops working.

Turns out these are distinct problems, but it is possible to solve both of them.

Instead of trying to configure it using the UI, you can add a new “Custom User File Include” at the bottom. I have /etc/vpn-policy-routing.<name>.user, with the following:

#!/bin/sh

TARGET_IPSET='wg'
HOSTS='<production-server> <staging-server>'

for host in $HOSTS
do
  host $host | grep -v 'IPv6' | awk -v ipset="$TARGET_IPSET" '{print "add " ipset " " $(NF)}' | ipset restore
  host $host | grep 'IPv6' | awk -v ipset="$TARGET_IPSET" '{print "add " ipset "6 " $(NF)}' | ipset restore
done

Because the two ipset targets need to be different (wg for IPv4, and wg6 for IPv6), then we need to deal with them differently.

In this case, we just iterate through each hostname, and extract each matching IP address. We then create a string that looks like add wg 1.1.1.1 (or the IPv6 version), and pass these to ipset restore.

You will want to make sure that your IP addresses are only added using this method, as ipset restore will complain about duplicates.

You can execute this file directly, to see that it works.

Then, the other part of the problem is making sure this is kept up to date. I use a cron job to /etc/init.d/vpn-policy-routing reload every 10 minutes.

You will need to put this file back after updating OpenWRT, which is a bit annoying. Took me a while to figure out why my VPN connection was not routing correctly.

Wireguard and Dynamic Hostnames

We use Wireguard at work for our VPN: this allows us to limit who can access our administration interface based on being on our company VPN.

I’ve set up Wireguard on my router so that I don’t need to connect to the VPN on each of my devices, but I also use a split tunnel so that only the IP addresses that are required go through there. I discuss in a previous post how I use VPN Policy Based Routing to ensure that the IP addresses are updated there.

However, I also run a different Wireguard interface allowing me to connect back to my LAN. This sits behind a dynamic IP address (even though it doesn’t change much). In most cases that’s okay - if my IP address happens to have changed, I can just reconnect my VPN, and in reality it’s very unlikely that I’d be actually using the VPN when the IP address changed.

However, I also have a device I’m going to put into the beachhouse, allowing me to manage the network from there. It would be nice not to have to maintain a seperate set of dynamic hostnames, but instead set up Wireguard to keep a connection between those two devices.

After a bunch of mucking around (turns out the OpenWRT UI page is misleading about the purpose of the AllowedIPs field. Pro tip: don’t have overlapping network ranges in the “server” configuration, else only one of them will work at a time), I finally got this working, and my old Raspberry Pi (which is still at my house) is all ready to go for a long holiday at the beach.

But, I’m still worried about losing the VPN when my IP address changes. There’s not always someone at the beachhouse, although there could be tenants at the other beachhouse (which shares the network, although it’s all segmented using VLANs). So, I may need to do stuff at any time.

There are a couple of tools out there that refresh the connection on a Wireguard peer, but I thought I’d have a go at writing my own script.

#! /bin/bash

update_endpoint() {
  local IFACE=$1
  local ENDPOINT=$(cat /etc/wireguard/${IFACE}.conf | grep '^Endpoint' | cut -d '=' -f 2)
  # No need to refresh if ne endpoint.
  [ -z ${ENDPOINT} ] && return 0;
  local HOSTNAME=$(echo ${ENDPOINT} | cut -d : -f 1)
  local PORT=$(echo ${ENDPOINT} | cut -d : -f 2)
  local PUBLIC_KEY="$(wg show ${IFACE} peers)"
  # No need to refresh if no handshake
  [ -z $(wg show ${IFACE} latest-handshakes | grep ${PUBLIC_KEY} | awk '{print $2}') ] && return 0;
  local ADDRESS=$(host -4 ${HOSTNAME} | grep 'has address' | awk '{print $4}')
  # Return if we don't find any matching lines here - that means our IP address matches.
  [ -z "$(wg show ${IFACE} endpoints | grep ${PUBLIC_KEY} | grep ${ADDRESS})" ] || return 0;
  wg set ${IFACE} peer ${PUBLIC_KEY} endpoint "${HOSTNAME}:${PORT}"
}


WG_IFS=$(wg show interfaces)

for WG_IF in $WG_IFS ; do 
  update_endpoint $WG_IF
done

This sweet little number iterates through each wireguard interface, and checks to see if the current IP address matches the hostname’s IP address. If not, it updates the running configuration.