Route all traffic through your VPN

The issue

How to make sure all your network traffic is routed through your VPN?

That's a question many people can't easily answer. Typical VPN setups rely on the prioritization of the operating system to route network traffic through the VPN. This setup works but connections from the outside are still coming in and once the connection to the VPN closes, the normal connection is used seamlessly.
This is nice but it defeats the purpose of a VPN for some folks as it might leak data through and because of unsecured connections (see the latest news about the KRACK vulnerability in WPA2).

What can you do about this issue?

There are multiple ways to combat the issue of disconnecting from a VPN: kill-switches that kill specific applications or black-holes that discard any non-VPN traffic.

In this article I will walk you through the steps needed to configure your Linux system to prevent any data being send to any destination that isn't your VPN.

The prerequisites

In order to configure everything to prevent data leakage we need a few things to be setup first:

  1. A network and internet connection; it doesn't make sense to secure a connection that is non-existent.
  2. A keyboard (a mouse is optional).
  3. Root access on the system in question.

Actually securing the connection and preventing data leaking

The examples here are all done on a Debian 9 system but should be applicable to most Linux distributions.
All the commands assume your primary network connection to be on eth0 and your VPN to be on tun0.
The local network in these examples is on 192.168.0.0/24.

Installing the necessary packages

Whilst using bare-to-the-metal applications is one way of configuring a firewall, I will be using ufw, a wrapper around iptables.

So first we install ufw:

apt install ufw

That should be the first step done.
There is just one caveat: ufw is disabled by default because it would prevent incoming connections like ones over SSH. Before we activate ufw, we have to set up our rules.

Adding the necessary rules to ufw

By default ufw allows all outgoing traffic, this is great for ordinary use-cases but some applications send out data we don't want to be routed through the internet before going through our VPN.

ufw also reads rules from the top to the bottom: beginning with rule 1, going through each of them and applying the first rule that matches. This behaviour is inherited from iptables.

So to begin, we allow local network traffic, that way we don't accidentally prevent ourselves from connecting to our system over SSH:

ufw allow in on eth0 from 192.168.0.0/24
ufw allow out on eth0 to 192.168.0.0/24

Now that we can reach our local network, we can enable the firewall and deny outgoing connections that are not explicitly allowed:

ufw enable
ufw default deny outgoing

Querying ufw about its status reveals the installed rules:

# ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), deny (outgoing), allow (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
Anywhere on eth0           ALLOW IN    192.168.0.0/24

192.168.0.0/24             ALLOW OUT   Anywhere on eth0

Pinging a known IP like 8.8.8.8 fails spectacularly, just like we configured:

# ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
ping: sendmsg: Operation not permitted
ping: sendmsg: Operation not permitted
^C
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1013ms

Now even our VPN is blocked, so let's allow our system to connect to our VPN (replace <VPN IP> with the ip address or network of your VPN):

ufw allow in on eth0 from <VPN IP>
ufw allow out on eth0 to <VPN IP>

Connecting to our VPN works fine now, but traffic through the VPN tunnel itself is still blocked by our default rule:

# ip a
...
4: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 100
link/none 
inet 10.7.7.18/24 brd 10.7.7.255 scope global tun0
valid_lft forever preferred_lft forever

# ping 8.8.8.8 -I tun0
PING 8.8.8.8 (8.8.8.8) from 10.7.7.18 tun0: 56(84) bytes of data.
ping: sendmsg: Operation not permitted
ping: sendmsg: Operation not permitted
^C
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1020ms

So let us allow traffic through the VPN tunnel now:

ufw allow in on tun0 from any
ufw allow out on tun0 to any

Verifying our rules

Lastly we verify our rules to make sure that all traffic through our VPN is allowed but traffic through our ordinary connection is restricted to local and VPN traffic only:

# ping 8.8.8.8 -I tun0
PING 8.8.8.8 (8.8.8.8) from 10.7.7.18 tun0: 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=56 time=49.4 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=56 time=48.4 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=56 time=49.1 ms
^C
--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 48.476/49.017/49.405/0.394 ms

# ping 8.8.8.8 -I eth0
PING 8.8.8.8 (8.8.8.8) from 192.168.0.2 eth0: 56(84) bytes of data.
ping: sendmsg: Operation not permitted
ping: sendmsg: Operation not permitted
^C
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1002ms

# ping <VPN IP> -I eth0
PING <VPN IP> from 192.168.0.2 eth0: 56(84) bytes of data.
64 bytes from <VPN IP>: icmp_seq=1 ttl=54 time=42.7 ms
64 bytes from <VPN IP>: icmp_seq=2 ttl=54 time=41.2 ms
64 bytes from <VPN IP>: icmp_seq=3 ttl=54 time=41.8 ms
^C
--- <VPN IP> ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 41.275/41.951/42.750/0.630 ms

Conclusion

The steps above are all that is necessary to filter IP packets and deny any stray packets that want to leave on your primary, non-VPN connection but still allow you to communicate with local services like DNS or DHCP and connect to the system using SSH, FTP and more.

References

UFW page on the Ubuntu Community Help Wiki
UFW tutorial by DigitalOcean
Gufw - a frontend for ufw

{{ message }}

{{ 'Comments are closed.' | trans }}