Running Multiple Client Instances of OpenVPN on Linux

I run multiple instances of OpenVPN providing VPN connectivity to endpoints provided by AirVPN, all on a single virtual machine. I use these endpoints on my home network WIFI for connectivity testing of clients as well as overall privacy and security. The load for these VPNs, even at over 50 megabits of transfer, is pretty low so using virtual machines made sense. This also helps isolate the sessions off in their own little virtual world of 1 CPU core, 512 megabytes of memory and 2 VLANs.

This configuration is for Linux, and uses the alternative routing tables in the operating system. A primary benefit of doing it that way is that it prevents OpenVPN from taking over the host’s primary routes, and if the tunnel fails, there isn’t any routing on that network. No need for additional watchdog programs like vpnfailsafe or adding ufw rules to prevent leaks over the primary internet connection. If the tunnel goes down, there is no route for that traffic. This method works well for just one tunnel.

Lets assume you have two OpenVPN endpoints, one in Chicago and the other in Los Angeles. Begin by getting basic working configurations for those tunnels either by setting up your endpoints or having your VPN provider give you a configration. Starting with Chicago, add the following to the OpenVPN config for that VPN.

dev tun0
route-nopull
script-security 2
up /etc/openvpn/chicago.post-up.sh

Name this configuration related to its endpoint, say chicago.conf

This tells OpenVPN not to make itself the default route for all traffic on the host, to permit running a post-up script and that script is chicago.post-up.sh.
This is critical when running multiple VPN instances on a single host because if it were not done this way, subsequent instances of OpenVPN would be routed OVER the tunnel of the first OpenVPN instance. A cute trick on security maybe, but definitely performnce killing.

There is also an instruction to OpenVPN to use device tun0 and not pick the first thing it finds. This is so we know which device goes where when we set up routing and firewalling rules.

These configration items need to be made to the “Los Angeles” VPN configuration.

dev tun3
route-nopull
script-security 2
up /etc/openvpn/los_angeles.post-up.sh

Again, we’ve specified tunnel device tun3for this instance of OpenVPN, and gave its own post-up script. Smartly name this config, say losangeles.conf.

Naming Interfaces

Each OpenVPN instance has its own tunnel interface and now it needs its own network interface. I use 802.1q VLAN tagging, but it will also work with virtual interfaces.
Replace the VLAN interfaces with whatever you decide to use. Although I will say, managed switches are cheap and VLAN tagging adds some nice flexibility. You can probably use VLAN tagging inside Linux KVM/QEMU or Microsoft Hyper-V (but not VMWare Player or Workstation) if you are going the virtual machine route.

Multiple instances of OpenVPN works because the post-up scripts set up NAT/forwarding and routing between an ethernet interface and a tunnel interface. Here’s that voodoo.

Set Up Custom Routing Table

We’re going to be using IP forwarding, so run this command on your Linux host:

echo 0 > /proc/sys/net/ipv4/ip_forward

And add it to sysctl.conf

net.ipv4.ip_forward = 1

The configuration avoids usurping the default route because it uses its own routing table. Run this bash command once for the “Chicago” VPN.

echo "10 vpn0" >> /etc/iproute2/rt_tables

And it needs to be run for the other tunnel as well, “Los Angeles”

echo "11 vpn3 >> /etc/iproute2/rt_tables"

These commands set up custom routing tables called vpn0 (for tun0, obviously) and vpn3. This only needs to be done once.

Configure Post-Up Scripts

#!/bin/bash
/sbin/iptables -t nat -A POSTROUTING -o ens8.192 -j MASQUERADE
/sbin/iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
/sbin/iptables -A FORWARD -i tun0 -o ens8.192 -m state --state RELATED,ESTABLISHED -j ACCEPT
/sbin/iptables -A FORWARD -i ens8.192 -o tun0 -j ACCEPT

/sbin/ip route flush table vpn0
ip rule add from 192.168.254.0/24 table vpn0
ip rule add to 192.168.254.0/24 table vpn0
/sbin/ip route show table main | grep -Ev ^default | while read ROUTE ; do ip route add table vpn0 $ROUTE; done
tunip=$(ip addr show tun0 | grep inet | grep -v inet6 | awk {'print $2'} | sed 's/\/[0-9][0-9]//')
/sbin/ip route add default via ${tunip} dev tun0 table vpn0

This is the post-up script for tun0 or “Chicago”. What is happening here is that once the OpenVPN connection is set-up, it configures iptables for natting, accepts only established and related traffic and then populates the custom routing table.

Again, we’re tying tun0 to VLAN device ens8.192 so traffic from network 192.168.254.0/24 is routed through tun0. Yes, we’ve essentially turned the Linux host into a router.

We’ll configure the other OpenVPN tunnel routing with a twist, it needs to accept inbound connections on a certain port for one host.

#!/bin/bash
/sbin/iptables -t nat -A POSTROUTING -o ens8.404 -j MASQUERADE
/sbin/iptables -t nat -A POSTROUTING -o tun3 -j MASQUERADE

iptables -t nat -A PREROUTING -p tcp -i tun3 --dport 35001 -j DNAT --to-destination 192.168.253.5:35001
iptables -A FORWARD -p tcp -d 192.168.253.5 --dport 35001 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT


/sbin/iptables -A FORWARD -i tun3 -o ens8.404 -m state --state RELATED,ESTABLISHED -j ACCEPT
/sbin/iptables -A FORWARD -i ens8.404 -o tun3 -j ACCEPT

/sbin/ip route flush table vpn3
ip rule add from 192.168.253.0/24 table vpn3
ip rule add to 192.168.253.0/24 table vpn3
/sbin/ip route show table main | grep -Ev ^default | while read ROUTE ; do ip route add table vpn3 $ROUTE; done
tunip=$(ip addr show tun3 | grep inet | grep -v inet6 | awk {'print $2'} | sed 's/\/[0-9][0-9]//')
/sbin/ip route add default via ${tunip} dev tun3 table vpn3

Pretty much the same as the previous post-up script with the exception of the interfaces tun3 and ens8.404. Don’t ask about the VLAN tag assignments on my network, I have no logical and sane explanation for using 404 other than I thought at one time it was funny to use 404. It isn’t so funny now that I’m too lazy to go in and change it.

There is a bit of DNAT magic pointing port 35001 to host 192.168.253.5, and this permits new connections from “Los Angeles” to route through the tunnel to this one host.

Now get systemd ready to start/stop each OpenVPN instance.

systemctl enable openvpn@chicago.service
systemctl enable openvpn@losangeles.service

My host provides routing for the entire VLAN, which appears on my Ubiquity Unifi access point.