How to setup a VPN kill switch on Linux for OpenVPN and WireGuard?

This guide will show you how to set up a manual kill switch for OpenVPN in GNU/Linux, with all details, explained. A VPN firewall (or kill switch) kills network connections that are active when there is no active connection on a given system through the use of IPTables, UFW or Firewalld.

How to setup a VPN kill switch on Linux for OpenVPN and WireGuard?
Photo by Privecstasy / Unsplash

This guide will show you how to set up a manual kill switch for OpenVPN in GNU/Linux, with all details, explained. A VPN firewall (or kill switch) kills network connections that are active when there is no active connection on a given system through the use of IPTables, UFW or Firewalld. This prevents any traffic leaks, but not limited to DNS leaks outside the VPN network at all, but completely denying internet access to a given system without an active encrypted VPN connection.

OpenVPN

For example, we are using VPN service with remote example.com - look into your .ovpn config. This domain has the VPN address 93.184.216.34:

$ host example.com
example.com has address 93.184.216.34

To get all hostnames and IP addresses of your current VPN provider, check the official website. They usually provide all IP addresses of their servers, but sometimes it's hard to find them or company changing their server IP's very fast, and you should rely on host command only.

With this automation we can extract app gateway IPs from VPN config:

REMOTE=`grep -oP 'remote \K.*' *.ovpn`
IP4=`for R in REMOTE; do getent ahostsv4 $R | cut -d' ' -f1 | tr '\n' ' '; done`
IP6=`for R in REMOTE; do getent ahostsv6 $R | cut -d' ' -f1 | tr '\n' ' '; done`
  • genentis a Linux tools that allow to get the entries from several system databases.
  • cut helps to extract only IP addresses
  • tr for merging the ouput into one line

We also need to know our default network interface, and the beautiful and powerful ip tool will help:

$ ip route | grep default
default via 192.168.0.1 dev wlp6s0 proto dhcp metric 600

So, our desired interface is wlp6s0. We used the Operating System network route table and found the default route, and it's interface.

We can automate it with the next trick:

$ ip route | grep default | cut -f 5 -d ' '
wlp6s0

Next step - we need to replace all hostnames to IP addresses in our VPN config, let's call it MY_VPN_CONFIG.ovpn.

From

remote example.com 1194 udp

To

remote 93.184.216.34 1194 udp

IPtables

The most of GNU/Linux distributions still use iptables or provide firewalls like ufw or firewalld. In case you use a one from the last two firewalls, check the next chapter below.

Let's create the script called ks-ipt.sh:

#!/bin/bash
REMOTE=`grep -oP 'remote \K.*' *.ovpn`
IP4=`for R in REMOTE; do getent ahostsv4 $R | cut -d' ' -f1 | tr '\n' ' '; done`
IP6=`for R in REMOTE; do getent ahostsv6 $R | cut -d' ' -f1 | tr '\n' ' '; done`
INTERFACE=$(ip route | grep default | cut -f 5 -d ' ')
iptables-save > rules.backup
iptables --flush
iptables --delete-chain
iptables -t nat --flush
iptables -t nat --delete-chain
iptables -P OUTPUT DROP
iptables -A INPUT -j ACCEPT -i lo
iptables -A OUTPUT -j ACCEPT -o lo
for IP in IP4; do
    iptables -A OUTPUT -j ACCEPT -d "$IP" -o $INTERFACE -p udp -m udp --dport 1194
    iptables -A INPUT -j ACCEPT -s "$IP" -i $INTERFACE -p udp -m udp --sport 1194
done
for IP in IP6; do
    ip6tables -A OUTPUT -j ACCEPT -d "$IP" -o $INTERFACE -p udp -m udp --dport 1194
    ip6tables -A INPUT -j ACCEPT -s "$IP" -i $INTERFACE -p udp -m udp --sport 1194
done
iptables -A INPUT -j ACCEPT -i tun0
iptables -A OUTPUT -j ACCEPT -o tun0

Then explain what this script does, step by step:

  • Save all current rules to the rules.backup file
  • Remove all rules, chains and NAT setups
  • Set the OUTPUT policy to DROP
  • Accept input and output traffic to local interface lo
  • Drop all connections except all VPN traffic on port 1194 and interface wlp6s0
  • Exclusively allow all traffic on interface tun0.

To undo all changes, let's create another script called reset-ks-ipt.sh:

#!/bin/sh
iptables --flush && ip6tables --flush
iptables --delete-chain && ip6tables --flush
iptables -t nat --flush
iptables -t nat --delete-chain
iptables-restore < rules.backup

Now we can use these scripts easily:

chmod +x ks-ipt.sh
sudo ./ks-ipt.sh
sudo ./reset-ks-ipt.sh

To make the scripts working, we can use the iptables-persistent software, a systemd service or Cron task. The easiest way is creating the crontab task by adding to /etc/crontab this line:

@reboot root /your/path/to/ks-ipt.sh

Ufw firewall

Ubuntu and derivatives like Linux mint uses a firewall called ufw, which is never than iptables and supports much more human-readable commands.

Let's create the script called ks-uwf.sh:

#!/bin/sh
REMOTE=`grep -oP 'remote \K.*' *.ovpn`
IP4=`for R in REMOTE; do getent ahostsv4 $R | cut -d' ' -f1 | tr '\n' ' '; done`
IP6=`for R in REMOTE; do getent ahostsv6 $R | cut -d' ' -f1 | tr '\n' ' '; done`INTERFACE=$(ip route | grep default | cut -f 5 -d ' ')
ufw --force reset
ufw default deny incoming
ufw default deny outgoing
ufw allow in on tun0
ufw allow out on tun0
for IP in IP4; do
    ufw allow out on $INTERFACE to $IP port 1194 proto udp
    ufw allow in on $INTERFACE from $IP port 1194 proto udp
done
for IP in IP6; do
    ufw allow out on $INTERFACE to $IP port 1194 proto udp
    ufw allow in on $INTERFACE from $IP port 1194 proto udp
done
ufw enable

To undo all the changes, create next script called reset-ks-uwf.sh:

#!/bin/sh
ufw --force reset
ufw enable

Now we can use these scripts easily:

chmod +x ks-ipt.sh
sudo ./ks-ipt.sh
sudo ./reset-ks-ipt.sh

In case you want to add more VPN servers, these boots scripts can be easily modified:

ufw allow out on $INTERFACE to 1.1.1.1 port 1194 proto udp
ufw allow in on $INTERFACE from 1.1.1.1 port 1194 proto udp

We just allowed connection to Cloudflare VPN together with example.com provides.

WireGuard VPN

For WireGuard we will use the Firewalld firewall, which is provided by default in Fedora, CentOS, Red Hat Enterprise Linux, Oracle Linux, Amazon Linux... I think it's enough for a while, if you know more distributions with firewalld - write in the comments below.

Firewalld makes it possible to control which applications are allowed to send traffic over the network. I find this much more powerful than earlier versions of UFW, and may eventually like it better.

Firewalld supports:

  • Zones: the Firewalld base objects, they can have the assigned interfaces

  • Policies: the Firewalld objects that control traffic flow between zones. Policies supports ingress zones (the traffic is coming from a zone) and egress zones (the traffic is going to a zone), this making way easier to write complex rules.

  • Rich rules: complex rules with similar syntax with IPtables rules but better look & feel.

Let's create the script called ks-fwd.sh:

#!/bin/sh
ENDPOINT=`grep -oP 'Endpoint = \K(.*)(?=:)' *.conf`
IP4=`for E in ENDPOINT; do getent ahostsv4 $E | cut -d' ' -f1 | tr '\n' ' '; done`
IP6=`for E in ENDPOINT; do getent ahostsv6 $E | cut -d' ' -f1 | tr '\n' ' '; done`
firewall-cmd --permanent --new-zone VPN-Only
firewall-cmd --permanent --zone VPN-Only --set-target DROP
firewall-cmd --permanent --new-policy Killswitch
firewall-cmd --permanent --policy VPN-Killswitch --set-target DROP
firewall-cmd --reload
for IP in IP4; do
    firewall-cmd --policy Killswitch --add-rich-rule='rule family="ipv4" destination address=$IP service name="wireguard" accept'
done
for IP in IP6; do
    firewall-cmd --policy Killswitch --add-rich-rule='rule family="ipv6" destination address=$IP service name="wireguard" accept'
done
firewall-cmd --policy Killswitch --add-ingress-zone HOST
firewall-cmd --policy Killswitch --add-egress-zone VPN-Only
firewall-cmd --permanent --zone VPN-Only --add-interface=wlp6s0
firewall-cmd --runtime-to-permanent

How it works, step-by-step:

  • We added new zone VPN-Only.
  • And new policy called Killswitch.
  • By default, all connection in Killswitch will be dropped.
  • But we don't need to filter all traffic, so time to add the exceptions: the VPN gateway and our local network subnet.
  • Now set the both exceptions to ingress and egress policy.

To undo all the changes, create next script called reset-ks-fwd.sh:

#!/bin/sh
firewall-cmd --permanent --delete-zone VPN-Only
firewall-cmd --permanent --delete-policy Killswitch
firewall-cmd --reload
firewall-cmd --runtime-to-permanent

Now we can use these scripts in one click:

chmod +x ks-fw.sh
sudo ./ks-fw.sh
sudo ./reset-ks-fw.sh

That's all for now, thanks for reading and stable connections to you all!

Read more