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.

  1. Find the dev entry and make a note of the device name, in my case it is ‘tun
  2. 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/

2 thoughts on “OpenVPN: Create A Linux VPN iptables Killswitch”

Leave a Reply

%d bloggers like this: