Self-Healing Randomized VPN with Kill Switch in OpenWRT Router
Published: 2023-02-15 01:44:57

This article describes scripts and configuration to enable kill switch enabled VPN router that randomly chooses configuration files from a pool to randomize the VPN server it connects to in each attempt. The kill switch enables the traffic from interfaces not to leak during reconnects or router reboots.
Introduction
Those who are familiar with the standard way of enabling VPN on OpenWRT routers, as described in the OpenWRT site here and here will know that it does not allow ‘kill switch’, which means blocking the traffic from interfaces when openvpn command is not running or has exited. If the openvpn command fails due to disconnection or other reasons, then the network will fall back the WAN interface and internet traffic from clients leaks out to regular network until the command is restarted. This article describes how to implement VPN kill switch on LAN and wireless interfaces so that clients can only access internet from the tunnel if openvpn command is active, and blocked when command is not run yet or until a failed or bad tunnel is automatically restored.
This article also describes how a random configuration file from a directory can be chosen every time openvpn command runs, adding a level of randomization to which VPN server the client connects to.
Implementing the above concepts on an OpenWRT router will make any network much safer from tracking or logging by ISPs. This article is mostly creating a basic configuration and some scripts which enable such behaviour on the OpenWRT router.
Setting up OpenWRT
This article assumes the reader has a freshly flashed or resetted OpenWRT router with the initial configuration. After setting the main admin password at http://192.168.1.1/cgi-bin/luci/admin/system/admin/password, the router’s terminal can be accessed with ssh root@192.168.1.1 Firstly, the following commands have to be executed to set lan interface at 192.168.2.1.
uci set network.lan.proto='static'
uci set network.lan.ipaddr='192.168.2.1'
uci set network.lan.netmask='255.255.255.0'
uci commit network
/etc/init.d/network restart
At this point, the ssh connection will disconnect as LAN interface changes, and terminal can now be accessed at ssh root@192.168.2.1. The below commands will setup a new interface to be used with wireless device at 192.168.3.1.
uci set network.wifi24='interface'
uci set network.wifi24.proto='static'
uci set network.wifi24.ipaddr='192.168.3.1'
uci set network.wifi24.netmask='255.255.255.0'

uci set dhcp.wifi24='dhcp'
uci set dhcp.wifi24.interface='wifi24'
uci add_list firewall.@zone[0].network='wifi24'

uci commit dhcp
uci commit network
uci commit firewall

/etc/init.d/firewall restart
/etc/init.d/network restart 
This new interface can be associated with a wireless radio in luci at http://192.168.2.1/cgi-bin/luci/admin/network/wireless. Internet access should now be verified in both LAN devices and wireless devices before proceeding to the next step.
The following command will install openvpn on the router:
opkg update
opkg install openvpn-openssl 
This is the only package needed for this article and we can proceed to the next step of creating an interface ovpn for the tunnel tun0 which will be created by openvpn command. Execute the following commands in the terminal to create the interface.
uci set network.ovpn='interface'
uci set network.ovpn.proto='none'
uci set network.ovpn.ifname='tun0'

uci commit network
uci add_list firewall.@zone[1].network='ovpn'

uci commit firewall

/etc/init.d/firewall restart
/etc/init.d/network restart 
The creation of the currently inactive interface can be verified at http://192.168.2.1/cgi-bin/luci/admin/network/network.
Prevent DNS Leak
The DNS addresses of WAN will be used despite main traffic begin routed from the tunnel, leaking DNS information to the local network. This below configuration will avoid that:
uci set network.wan.peerdns='0'
uci set network.ovpn.peerdns='0'
uci commit network

/etc/init.d/network restart 
Setting up Folders and Files
There needs to exist a folder that will contain all the *.ovpn configurations that a script will randomly choose from to be passed to the openvpn command and a file to store the vpn provider credentials. Execute the below commands to create the folder and file.
mkdir /etc/openvpn/configs
touch /etc/openvpn/credentials 
This configs folder must be populated with ovpn configurations from your VPN provider, and the credentials can be populated with credentials in the following manner:
echo "<username of vpn provider>" >> /etc/openvpn/credentials
echo "<password of vpn provider>" >> /etc/openvpn/credentials 
Testing VPN Tunnel and Config
At this point, you can test the setup by executing the following test command using any config file.
openvpn –config /etc/openvpn/configs/<random-config-file> 
        --dev tun0 –auth-user-pass /etc/openvpn/credentials 
This should execute successfully now and a new tunnel tun0 should be created. A new router terminal can be used to verify pings through the tunnel.
ping -I tun0 8.8.8.8 
Internet connectivity should be verified from LAN and Wireless devices before proceeding to the next step. As reader will notice, all traffic is now protected by the tunnel, but the traffic will fall back to wan network when the openvpn command is exited. We are now ready to implement the ‘kill switch’ where when the openvpn command is not active, the internet from LAN and wireless devices is blocked.
Setting up Kill Switch Scripts
A new routing table has to be created on the router that will route the traffic from LAN and wireless interfaces to tunnel created by openvpn command and routes on this new table can be modified by scripts on the fly to enable the kill switch.
Creating New Routing Table
You can modify the file /etc/iproute2/rt_tables to add a new table we will call ‘custom’.
cat /etc/iproute2/rt_tables 
The table should look like this after modification is done by the tool of your choice. The line added is ‘40 custom’.
#
# reserved values
#
128     prelocal
255     local
254     main
253     default
40      custom
0       unspec
#
# local
# 
Creating Kill Switch Script
Create a new file /etc/openvpn/kill-switch-setup.sh with the following content:
Bash
#!/bin/sh

ip route flush table custom

interface_cidr=192.168.2.0/24
interface_name=br-lan
interface_gateway=192.168.2.1

ip route flush $interface_cidr
ip rule add from $interface_cidr lookup custom
ip rule add from all to $interface_cidr lookup custom
ip route add $interface_cidr dev $interface_name scope link src $interface_gateway table custom

interface_cidr=192.168.3.0/24
interface_name=wifi24
interface_gateway=192.168.3.1

ip route flush $interface_cidr
ip rule add from $interface_cidr lookup custom
ip rule add from all to $interface_cidr lookup custom
ip route add $interface_cidr dev $interface_name scope link src $interface_gateway table custom 
Make the file executable:
chmod +x /etc/openvpn/kill-switch-setup.sh     
Executing this script by ./kill-switch-setup.sh will block the internet from LAN and wifi interfaces until the custom table is updated by scripts described later in this article, but the ssh and the luci interface can still be accessed. The script’s functionality can be tested by executing the script and to restore internet again you can restart the network by the command below:
/etc/init.d/network restart 
The internet is restored by the above command as and when the network is restarted, the ip rules and routes are purged back to the originals. If you are unable to access ssh or internet due to flukes, restarting the router will fix the issue.
Creating OpenVPN Command ‘Up’ Script
The below script up.sh will be executed when the openvpn command is successful by passing it to openvpn parameter --up. This will update the custom routing with ip routes that will route traffic to the tunnel created. This should not be executed directly from the command line as the openvpn command itself will execute it and pass the parameters required.
Create a new file /etc/openvpn/up.sh with the following content:
Bash
Shrink ▲   
!/bin/sh

wan_cidr=192.168.1.0/24
wan_interface_name=wan
vpn_table_name=custom

vpn_gateway=$route_vpn_gateway
vpn_local=$ifconfig_local
vpn_mask=$ifconfig_netmask
remote_ip=$trusted_ip
device_name=$dev
router_gateway=$route_net_gateway

vpn_interface_cidr=`awk -v val="$vpn_gateway|$vpn_mask" '
function count1s(N){
    c = 0
    for(i=0; i<8; ++i) if(and(2**i, N)) ++c
    return c
}
function subnetmaskToPrefix(input) {
    split(input, inputParts, "|")
    split(inputParts[2], subnetParts, ".")
    split(inputParts[1], mainParts, ".")

  if (subnetParts[1] == 0 ) {
        mainParts[1] = 0
    }
    if (subnetParts[2] == 0 ) {
        mainParts[2] = 0
    }
    if (subnetParts[3] == 0 ) {
        mainParts[3] = 0
    }
    if (subnetParts[4] == 0 ) {
        mainParts[4] = 0
    }

     printf "%d.%d.%d.%d/%d", mainParts[1], mainParts[2], 
     mainParts[3], mainParts[4], count1s(subnetParts[1]) + count1s     }
BEGIN {
    subnetmaskToPrefix(val)
}'`

ip route add 0.0.0.0/1 via $vpn_gateway dev $device_name table $vpn_table_name
ip route add 128.0.0.0/1 via $vpn_gateway dev $device_name table $vpn_table_name
ip route add $vpn_interface_cidr dev $device_name scope link src 
             $vpn_local table $vpn_table_name
ip route add $remote_ip via $router_gateway dev wan table $vpn_table_name
ip route add $wan_cidr dev $wan_interface_name table $vpn_table_name

ip route flush cache 
Make the file executable:
chmod +x /etc/openvpn/up.sh 
Creating OpenVPN Command ‘Down’ Script
The below script down.sh will be executed when the openvpn command exits upon disconnection or ping failure by passing it to the parameter --down. This will remove the routes created by the up.sh command from the custom routing table and internet will blocked from the interfaces. This should not be executed directly from the command line as the openvpn command itself will execute it and pass the parameters required.
Create a new file /etc/openvpn/down.sh with the following content:
Bash
Shrink ▲   
!/bin/sh

wan_cidr=192.168.1.0/24
wan_interface_name=wan
vpn_table_name=custom

vpn_gateway=$route_vpn_gateway
vpn_local=$ifconfig_local
vpn_mask=$ifconfig_netmask
remote_ip=$trusted_ip
device_name=$dev
router_gateway=$route_net_gateway

vpn_interface_cidr=`awk -v val="$vpn_gateway|$vpn_mask" '
function count1s(N){
    c = 0
    for(i=0; i<8; ++i) if(and(2**i, N)) ++c
    return c
}
function subnetmaskToPrefix(input) {
    split(input, inputParts, "|")
    split(inputParts[2], subnetParts, ".")
    split(inputParts[1], mainParts, ".")

  if (subnetParts[1] == 0 ) {
        mainParts[1] = 0
    }
    if (subnetParts[2] == 0 ) {
        mainParts[2] = 0
    }
    if (subnetParts[3] == 0 ) {
        mainParts[3] = 0
    }
    if (subnetParts[4] == 0 ) {
        mainParts[4] = 0
    }

     printf "%d.%d.%d.%d/%d", mainParts[1], mainParts[2], mainParts[3], 
                              mainParts[4], count1s(subnetParts[1]) + count1s     }
BEGIN {
    subnetmaskToPrefix(val)
}'`

ip route flush 0.0.0.0/1 via $vpn_gateway dev $device_name table $vpn_table_name
ip route flush 128.0.0.0/1 via $vpn_gateway dev $device_name table $vpn_table_name
ip route flush $vpn_interface_cidr dev $device_name scope link src 
               $vpn_local table $vpn_table_name
ip route flush $remote_ip via $router_gateway dev wan table $vpn_table_name
ip route flush $wan_cidr dev $wan_interface_name table $vpn_table_name

ip route flush cache 
Make the file executable:
chmod +x /etc/openvpn/down.sh 
Setting up OpenVPN Client Start Scripts
As the openvpn command updates the kernel routing table when executed, kill switch is not possible, hence it has to be passed additional parameters which stop it from adding any routes to routing tables by itself and asks it to execute the up.sh and down.sh commands described above to add routes to custom routing table. The below script start-openvpn-client.sh also selects a random *.ovpn configuration file from /etc/openvpn/configs folder along with executing openvpn command with required parameters to enable kill switch setup.
Creating OpenVPN Randomized Client Start Script
Create a new file /etc/openvpn/start-openvpn-client.sh with the following content:
Bash
dir='/etc/openvpn/configs'
n_files=`/bin/ls -1 "$dir" | wc -l | cut -f1`
rand_num=`awk "BEGIN{srand();print int($n_files * rand()) + 1;}"`
file=`/bin/ls -1 "$dir" | sed -ne "${rand_num}p"`
path=`cd $dir && echo "$PWD/$file"` # Converts to full path.
echo "Chosen file ${path}"
echo "${path}" > /tmp/openvpn-server.log
openvpn --config ${path} --auth-user-pass /etc/openvpn/credentials 
--up /etc/openvpn/up.sh --down-pre --down /etc/openvpn/down.sh --route-noexec 
--dev tun0 --ping 10 --ping-exit 60 --persist-local-ip --script-security 2  
Make the file executable:
chmod +x /etc/openvpn/start-openvpn-client .sh 
This script can be tested directly now by executing ./start-openvpn-client .sh and verified by pinging through the tunnel created. As this script does not affect the main routing table, it can be started and exiting without breaking anything. If this script is run after the ./kill-switch-setup.sh is executed, the tunneled internet for LAN and wireless interfaces is activated with kill switch enabled. All effects of this script needs to be verified before the next step, which is – tunneled internet on LAN and wireless devices work when script is running and blocks out when script has exited.
Creating Entry Point Script
The script start.sh below will activate the kill switch on LAN and wireless interfaces and enable a self healing openvpn command that restarts itself on exit. This is intended to run automatically after the router starts and interfaces WAN and LAN, etc. are active.
Create a new file /etc/openvpn/start.sh with the following content:
Bash
sh /etc/openvpn/kill-switch-setup.sh
sh /etc/openvpn/start-openvpn-client.sh
exec sh /etc/openvpn/start-openvpn-client.sh 
Make the file executable:
chmod +x /etc/openvpn/start.sh 
To test this command manually, reboot the router, execute ./start.sh and verify tunnel pings and protected internet access on LAN and wireless devices.
Adding Entry Script to Router Startup
As described above, start.sh should only run after the WAN and LAN interfaces are active, hence a new hotplug script is to be created.
Create a new file /etc/hotplug.d/iface/99-ifup-wan-lan with the following content:
Bash
Shrink ▲   
!/bin/sh

if [ "${ACTION}" == "ifup" ] && [ "${DEVICE}" = "wan" ]
then
    rm /tmp/wanstate
    echo started >> /tmp/wanstate

    wanstateret=`cat /tmp/wanstate`
    lanstateret=`cat /tmp/lanstate`
    vpnstateret=`cat /tmp/vpnstate`

    wanstarted=`echo "$wanstateret" started | awk '{ print ($1 == $2) ? 1 : 0 }'`
    lanstarted=`echo "$lanstateret" started | awk '{ print ($1 == $2) ? 1 : 0 }'`
    vpnstarted=`echo "$vpnstateret" started | awk '{ print ($1 == $2) ? 1 : 0 }'`

    st=1

    if [ $wanstarted -eq $st ] && [ $lanstarted -eq $st ] && [ $vpnstarted -eq 0 ]
    then
            echo started >> /tmp/vpnstate
            (sh /etc/openvpn/start.sh >/dev/null 2>&1 )&
    fi
fi

if [ "${ACTION}" == "ifup" ] && [ "${DEVICE}" = "br-lan" ]
then
    rm /tmp/lanstate
    echo started >> /tmp/lanstate

    wanstateret=`cat /tmp/wanstate`
    lanstateret=`cat /tmp/lanstate`
    vpnstateret=`cat /tmp/vpnstate`

    wanstarted=`echo "$wanstateret" started | awk '{ print ($1 == $2) ? 1 : 0 }'`
    lanstarted=`echo "$lanstateret" started | awk '{ print ($1 == $2) ? 1 : 0 }'`
    vpnstarted=`echo "$vpnstateret" started | awk '{ print ($1 == $2) ? 1 : 0 }'`

    st=1

    if [ $wanstarted -eq $st ] && [ $lanstarted -eq $st ] && [ $vpnstarted -eq 0 ]
    then
            echo started >> /tmp/vpnstate
            (sh /etc/openvpn/start.sh >/dev/null 2>&1 )&
    fi
fi

exit 0 
After this file is added, rebooting the router will automatically activate the kill switch vpn mode.
Further Modifications
As the main routing table remains unaffected by this method, other wan interfaces can be added and split tunnelling of interfaces can be enabled. Some interfaces can use a different tunnel than others by adding additional routing tables like 'custom_lan' or 'custom_wireless' and script modifications. Hope the reader uses a bit of imagination and the scripts above as reference for that.
Additional Notes
This method as been tested for stability on OpenWRT version 22.03. But it will hopefully work on future versions of OpenWRT and on 19.07 versions as well. I will post here if I get a chance to check on 19.07. Happy browsing!