I’m going to show you how to create a VPN killswitch using iptables. You will want to do this to prevent data leakage in that moment between your network connection starting, and your VPN connecting.
We will be doing this using OpenVPN configuration files.
Most VPN providers, NordVPN, OpenVPN to name a couple, do provide an OpenVPN option.
Harden Your Physical System Using Systemctl
First, we are going to harden your system by blocking ipv6 on your system, which is problematic for some VPN providers.
Systemctl is as close as you can get to pulling physical levers, turning taps on and off, and pushing real buttons on your linux system.
The /proc Directory
The ‘proc’ directory probably deserves it’s own post, but for now I’ll go over the bare minimum to get you up and running.
If you list the directories in your root directory you will find the proc directory.
ubuntu@goodboy:/$ ls
bin dev lib libx32 mnt root srv tmp
boot etc lib32 lost+found opt run swapfile usr
cdrom home lib64 media proc sbin sys var
ubuntu@goodboy:/$
The proc directory is created at run time and contains files, which can be thought of as settings, which represent the current state of your system kernel. The system kernel being that part of your OS that basically controls and schedules the use of your physical hardware.
Check System States
You can check the state of any part of your system by looking at all of the files in the proc hierarchy.
Say you wanted to check if the system is wide open to ipv6 requests.
For example, if you look at the contents of the file ‘/proc/sys/net/ipv6/conf/all/disable_ipv6’ you will see a value, in this case 0. Which means ipv6 isn’t disabled.
root@goodboy:/proc/sys/net/ipv6/conf/all# cat disable_ipv6
0
Set System State Using /etc/sysctl.conf
You can set the system state on boot by making an entry in the file ‘/etc/sysctl.conf’ that matches the hierarchy of the proc kernel values.
For example, for the disable_ipv6 setting,
/proc/sys/net/ipv6/conf/all/disable_ipv6
you would ignore the top two parent directories, replace the directory slashes with a dot and add the following to your ‘/etc/sysctl.conf’ file.
net.ipv6.conf.all.disable_ipv6 1
On next boot, as this value is set to 1, your system will disable ipv6.
Have a good look around your /proc file system to see what else you can play with.
Disable ipv6 On Your System
The short story is that most VPN providers don’t support ipv6, so it is best to disable the damn thing to prevent any information leakage from your system.
Using the /proc file system we can as damn near close as we can to turning off a physical tap to stop ipv6 traffic.
If you list the directories under conf for ipv6 in your system you will see directories for all of your network interfaces. Each directory contains a disable_ipv6 file.
root@goodboy:/proc/sys/net/ipv6/conf# ls
all default enp0s3 lo
So, to completely disable ipv6 on your system we need to add the following entry to the end of your ‘/etc/sysctl.conf’ file.
Modify these entry names according to the available interfaces on your system.
net.ipv6.conf.all.disable_ipv6 1
net.ipv6.conf.default.disable_ipv6 1
net.ipv6.conf.enp0s3.disable_ipv6 1
net.ipv6.conf.lo.disable_ipv6 1
Generate Your OpenVPN Configuration File
Most VPN providers will give you the option to download an OpenVPN Configuration file.
I’ll assume you have downloaded and have been able to connect to your VPN provider using OpenVPN.
Before you do… open up your configuration file and do 2 things.
- Find the dev entry and make a note of the device name, in my case it is ‘tun‘
- Note the available ports for VPN provided by your VPN service. ‘1194‘ is the default, and am
happy to see that it is an available service, and we won’t need to mess around with custom ports (Something you would need to do in countries blocking ‘1194‘ for example). There are four other available ports listed in the config file.
client
dev tun
proto udp
remote 185.159.157.107 1194
remote 185.159.157.107 5060
remote 185.159.157.107 51820
remote 185.159.157.107 80
remote 185.159.157.107 4569
remote-random
resolv-retry infinite
nobind
Continue to setup your VPN connection using this config file as you would normally do for your system.
Build An iptables Killswitch Chain
Homework
For an overview of creating iptable rules have a read here, it is quite simple really. Please read through that post before continuing here.
Start Fresh
I’m going to start fresh and build our iptables up from scratch. Just checking that my iptables are empty, and that the policy is set to the default ACCEPT.
Drop All Traffic As Default
We want to start with a tight security policy of dropping/blocking all traffic on all chains.
You can do this by running the following commands.
iptables --policy INPUT DROP
iptables --policy FORWARD DROP
iptables --policy OUTPUT DROP
You should now see,
root@goodboy:~# iptables -L -v
Chain INPUT (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Related and Established Traffic
As mentioned here, you want any traffic incoming that is a result of an outgoing request to be let through. So we will create a RELATED_AND_ESTABLISHED chain.
iptables -N RELATED_AND_ESTABLISHED
Add a rule to that chain that allows Related and Established traffic to pass,
iptables -A RELATED_AND_ESTABLISHED -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
and route all INPUT traffic to our new chain,
iptables -A INPUT -j RELATED_AND_ESTABLISHED
Checking our iptables we see that we have,
root@goodboy:~# iptables -L -v
Chain INPUT (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 RELATED_AND_ESTABLISHED all -- any any anywhere anywhere
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain RELATED_AND_ESTABLISHED (1 references)
pkts bytes target prot opt in out source destination
0 0 ACCEPT all -- any any anywhere anywhere ctstate RELATED,ESTABLISHED
which does harden our system by not allowing any inbound connections.
Allow Loopback Device And Local Private Network Traffic
This is optional. If you want to connect to a printer on your local network for example, or look at a website you are developing locally on your laptop. The following commands will enable this on your OUTPUT chain. Here (‘-o’) defines the interface you want the rule active on.
To allow your loopback addresses, apply the following command.
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A RELATED_AND_ESTABLISHED -i lo -j ACCEPT
To connect to devices on your local network, apply the following command.
iptables -A output -d 192.168.0.0/16 -j ACCEPT
You need to check what class the local network you have connected to uses as your command will differ.
iptables -A output -d 172.16.0.0/12 -j ACCEPT
iptables -A output -d 10.0.0.0/12 -j ACCEPT
You can also turn on ping for the VPN interface too if you like.
To allow returning pings on your VPN, apply the following command.
‘tun0‘ is the usual name given to a VPN interface, check this for your system.
‘icmp‘ is the protocol used by pings.
iptables -A OUTPUT -o tun+ -p icmp -j ACCEPT
Build A Kill Switch Chain
We are going to put all of our kill switch rules in a chain called OUTPUT_VPN_KILL_SWITCH, we can create that chain with the following command.
iptables -N OUTPUT_VPN_KILL_SWITCH
Allow VPN Device
Currently the usual name given to your virtual networking device for your VPN is ‘tun0‘, you can check this by connecting to your VPN and using the ‘ifconfig‘ command. I’m going to use a ‘wild card’ and refer to my tunnel as ‘tun+‘, this will match tun0, tun1 etc…
You are going to want to allow OUTPUT traffic on your VPN device. You can do that with the following command.
iptables -A OUTPUT_VPN_KILL_SWITCH -o tun+ -j ACCEPT
Allow UDP Traffic On VPN Device
Referring back to your OpenVPN config file, for your service port append a rule to OPEN_VPN_KILL_SWITCH chain to accept VPN traffic. Our config file only targets UDP which is why our iptables command have protocol ‘-p udp‘ for out VPN service port ‘1194‘.
iptables -A OUTPUT_VPN_KILL_SWITCH -p udp -m udp --dport 1194 -j ACCEPT
Allow DNS Server
If you use the following command, you will see which gateway your VPN device uses.
ubuntu@goodboy:~$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.17.0.1 0.0.0.0 UG 50 0 0 tun0
If you are lucky, it will be caught by the local network rule you defined above. If not, you should add a new iptables rule to allow your VPN DNS server,
iptables -A OUTPUT_VPN_KILL_SWITCH -d 10.17.0.1 -j ACCEPT
Route All OUTPUT Traffic To Your KillSwitch
Finally, we refer all OUTPUT traffic to our OUTPUT_VPN_KILL_SWITCH chain. By using the ‘-I‘ switch we place it at the top of the chain.
iptables -I OUTPUT -j OUTPUT_VPN_KILL_SWITCH
If you check your final iptables it should looks something like this,
root@goodboy:~# iptables -L -v
Chain INPUT (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 RELATED_AND_ESTABLISHED all -- any any anywhere anywhere
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 OUTPUT_VPN_KILL_SWITCH all -- any any anywhere anywhere
0 0 ACCEPT all -- any lo anywhere anywhere
0 0 ACCEPT icmp -- any tun+ anywhere anywhere
0 0 ACCEPT all -- any any anywhere 10.0.0.0/8
Chain OUTPUT_VPN_KILL_SWITCH (1 references)
pkts bytes target prot opt in out source destination
0 0 ACCEPT udp -- any any anywhere anywhere udp dpt:openvpn
0 0 ACCEPT all -- any tun+ anywhere anywhere
0 0 ACCEPT all -- any any anywhere _gateway
Chain RELATED_AND_ESTABLISHED (1 references)
pkts bytes target prot opt in out source destination
0 0 ACCEPT all -- any any anywhere anywhere ctstate RELATED,ESTABLISHED
1 180 ACCEPT all -- lo any anywhere anywhere
You can see the gateway IP has been replaced by the string ‘_gateway’, and the default openvpn destination port by the string ‘openvpn’
Save Rules As Script
Here are all the rules in command form in a bash script, each time your run this script your linux system will have a VPN kill switch enabled.
#!/bin/bash
#Flush all chains, delete custom chains.
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
#Block all traffic as default.
iptables --policy INPUT DROP
iptables --policy FORWARD DROP
iptables --policy OUTPUT DROP
#Create a custom chain to contain rules to allow related and established traffic.
iptables -N RELATED_AND_ESTABLISHED
iptables -A RELATED_AND_ESTABLISHED -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
#Divert input traffic to the RELATED_AND_ESTABLISHED chain.
iptables -A INPUT -j RELATED_AND_ESTABLISHED
#Let's allow the loopback device.
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A RELATED_AND_ESTABLISHED -i lo -j ACCEPT
#Let's allow ping on the tun+
iptables -A OUTPUT -o tun+ -p icmp -j ACCEPT
#If you want to, you can also allow access to your local network.
#The most common local network is class C, 192.168.0.0/16
iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT
#You must check what local network class you car connected to.
#Adjust for Class C, 192.168.0.0/16 Class B, 172.16.0.0/12 and class A 10.0.0.0/8
#let's create a chain named OUTPUT_VPN_KILL_SWITCH to hold our VPN rules
iptables -N OUTPUT_VPN_KILL_SWITCH
iptables -A OUTPUT_VPN_KILL_SWITCH -p udp -m udp --dport 1194 -j ACCEPT
iptables -A OUTPUT_VPN_KILL_SWITCH -o tun+ -j ACCEPT
iptables -A OUTPUT_VPN_KILL_SWITCH -d 10.17.0.1 -j ACCEPT
#let's pop a reroute to our vpn rules at the top of the OUTPUT chain using -I insert
iptables -I OUTPUT -j OUTPUT_VPN_KILL_SWITCH
The above script will work well as a kill switch, but you may want something more granular.
By commenting out the allow all line,
iptables -A OUTPUT_VPN_KILL_SWITCH -o tun+ -j ACCEPT
you can add more granular rules, such as only allowing https traffic with the following command.
iptables -A OUTPUT_VPN_KILL_SWITCH -o tun+ -p tcp --dport 443 -j ACCEPT
You will have to look in to which ports you need for the network services you want to interact with.
Here is the same script file, with the granular changes we just applied.
#!/bin/bash
#Flush all chains, delete custom chains.
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
#Block all traffic as default.
iptables --policy INPUT DROP
iptables --policy FORWARD DROP
iptables --policy OUTPUT DROP
#Create a custom chain to contain rules to allow related and established traffic.
iptables -N RELATED_AND_ESTABLISHED
iptables -A RELATED_AND_ESTABLISHED -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
#Divert input traffic to the RELATED_AND_ESTABLISHED chain.
iptables -A INPUT -j RELATED_AND_ESTABLISHED
#iptables -A OUTPUT -d 10.29.0.1 -j ACCEPT
#Let's allow the loopback device.
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -o tun+ -p icmp -j ACCEPT
#If you want to, you can also allow access to your local network.
#The most common local network is class C, 192.168.0.0/16
iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT
#You must check what local network class you car connected to.
#Adjust for Class C, 192.168.0.0/16 Class B, 172.16.0.0/12 and class A 10.0.0.0/8
#let's create a chain named OUTPUT_VPN_KILL_SWITCH to hold our VPN rules
iptables -N OUTPUT_VPN_KILL_SWITCH
#iptables --policy OUTPUT_VPN_KILL_SWITCH DROP
#let's pop a reroute to our vpn rules at the top of the OUTPUT chain using -I insert
iptables -A OUTPUT_VPN_KILL_SWITCH -p udp -m udp --dport 1194 -j ACCEPT
#iptables -A OUTPUT_VPN_KILL_SWITCH -o tun+ -j ACCEPT
iptables -A OUTPUT_VPN_KILL_SWITCH -d 10.17.0.1 -j ACCEPT
# HTTPS uses port 443
iptables -A OUTPUT_VPN_KILL_SWITCH -o tun+ -p tcp --dport 443 -j ACCEPT
# apt-get and other applications need port 80
iptables -A OUTPUT_VPN_KILL_SWITCH -o tun+ -p tcp --dport 80 -j ACCEPT
# Network Time Protocol (NTP), port 123 , if you rely on your machine setting time using the network.
iptables -A OUTPUT_VPN_KILL_SWITCH -o tun+ -p udp --dport 123 -j ACCEPT
#iptables -A OUTPUT_VPN_KILL_SWITCH -o tun+ -p tcp --dport 993 -j ACCEPT
#iptables -A OUTPUT_VPN_KILL_SWITCH -o tun+ -p tcp --dport 465 -j ACCEPT
iptables -I OUTPUT -j OUTPUT_VPN_KILL_SWITCH
Run As systemd Service Unit Script
We previously covered how to run a bash script as a service to restore an iptable.
You can use the same method, but instead of running a script that restores and iptable, you can point your systemd service unit directly at the above scripts. Don’t forget to set executable permissions.
Here is my new system unit,
[Unit]
Description=Run iptable commands to create killswitch
Before=network-pre.target
Wants=network-pre.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/vpnkillswitch_granular.sh
Timeoutsec=60
[Install]
WantedBy=network.target
i’ve enabled it with the following command,
root@goodboy:/etc/systemd/system# systemctl enable vpn-v4-killswitch.service
Created symlink /etc/systemd/system/network.target.wants/vpn-v4-killswitch.service → /etc/systemd/system/vpn-v4-killswitch.service.
root@goodboy:/etc/systemd/system#
If you have a previous systemctl unit running, if you followed the previous tutorial, you MUST DEACTIVATE IT. IT will conflict with your new iptable rules. Here I am disabling my old iptable rules.
root@goodboy:/etc/systemd/system# systemctl disable restore-v4-iptables.service
Removed /etc/systemd/system/network.target.wants/restore-v4-iptables.service.
That it! One last tip… if you can’t reach a website your VPN DNS service IP may have changed, you may need to adjust it in your script. Use command ‘route -n’ to check, or read the value in the file ‘/etc/resolve.conf’. Make appropriate changes to your iptable rules.
I’ve put together a python package that does all this for you, read more here… https://rexbytes.com/2022/10/11/rexbytes-software-bumper-vpn-kill-switch/
[…] Here I go over how to create a VPN kill switch for your linux system, more here. […]
[…] OpenVPN: Create A Linux VPN iptables Killswitch […]