How to route specific traffic through a VPN on Linux with nftables

Introduction

Here I explain a strong solution to route traffic to a VPN using the group of the processes. We discuss security at the end. After the configuration, if you want to run ktorrent using the VPN you just have to run sudo -g vpn_euro ktorrent. You will be able to use serveral VPN and no VPN the same time, per process.

How it works

We create one group per VPN. Using nftables we mark traffic from process using these groups (one mark per group). We use one routing table per VPN and we add a rule to use these table using the marks. We reject IPv6 output traffic from processes with these group. If the VPN is off then the routing table exists but the traffic is dropped (to avoid using a non VPN when the VPN connection droped).

Thanks

Thanks to this article from where I took a lot of materials.

Warning

I’m not a network administrator but just a tech guy who use linux perhaps before your birth and who manage a personal VPS and some computers.

How to

Create groups

Create one group per VPN, for example

groupadd vpn_euro
groupadd vpn_us

Declare the routing tables

Modifie /etc/iproute2/rt_tables , add 2 lines (for 2 VPN) :

1 vpn_euro
2 vpn_us

Add the routing table before the network

You will need to add a service to systemd to run a script to add the tables.

The script to run (file /root/bin/route-vpn-init.sh):

#!/bin/bash
# The "local-route" script, used for the VPN.

ip rule add fwmark 1 table vpn_euro 
ip route flush table vpn_euro
ip route add prohibit default table vpn_euro

ip rule add fwmark 2 table vpn_us
ip route flush table vpn_us
ip route add prohibit default table vpn_us

ip route flush cache

The service to install (file /etc/systemd/system/local-route.service)

[Unit]
DefaultDependencies=no
Description=The "local-route" script, used for the VPN.
Before=network-pre.target
Wants=network-pre.target

[Service]
Type=oneshot
ExecStart=/root/bin/route-vpn-init.sh
TimeoutSec=0
RemainAfterExit=yes

[Install]
RequiredBy=local-fs.target

You need to add the service : systemctl enable local-route.service

Configure the firewall

Mark IPv4 packets

Exception for X11 remote ports (6000-6007) to allow graphic applications to contact the X11 remote server in the local network.

table ip mangle {
	chain OUTPUT {
		type route hook output priority -150; policy accept;
		tcp dport 6000-6007 return
		skgid vpn_euro tcp dport != 6000-6007 meta mark set 0x1 
		skgid vpn_us tcp dport != 6000-6007 meta mark set 0x2
	}
}

Reject IPv6 for these groups

table ip6 removeip6 {
	chain OUTPUT {
		type route hook output priority -120; policy accept;
		skgid vpn_euro counter reject;
		skgid vpn_us counter reject;
	}
}

Accept tunnel traffic

table inet filter {
	chain input {
		type filter hook input priority 0; policy drop;

		# For the vpn
		iifname "tun_*" ct state established,related counter accept

Masquerade traffic to the tunnel

table ip nat {
	chain POSTROUTING {
		type nat hook postrouting priority 100; policy accept;
		oifname "tun_*" counter masquerade 
	}
}

Configure the VPNs

This is instructions for openvpn, adapt them to your vpn software.

In the .conf file for the VPN, force the device name :

dev tun_euro

and add these lines :

# LOCAL
route-nopull
up-restart
script-security 2
up /etc/openvpn/airvpn/up-euro.sh
  • route-nopull avoid automatic route configuration
  • up-restart tell to call the up and down script on restart
  • script-security 2 allow the call of external configured scripts
  • up : the script to call

/etc/openvpn/airvpn/up-euro.sh :

#!/bin/bash

set -x

ip route add 0.0.0.0/1 via  $ifconfig_local table vpn_euro
ip route add 128.0.0.0/1 via  $ifconfig_local table vpn_euro

Why not adding the route in one line : because it doesn’t work, I forgot the reason except I know it’s a strange behavior of the routing system.

Configure the interfaces

This is for the Debian /etc/network/interfaces, adapt it to your distribution:

iface tun_euro inet manual
	openvpn air_euro

iface tun_us inet manual
	openvpn air_us

Test it

I don’t know how to reload the routing table configured in /etc/iproute2/rt_tables, so, hum, yes, shame on me, … lets reboot.
Search “my ip” with a web navigator (run it with sudo -g …) to check if it uses the VPN. It if doesn’t work, did you kill the navigator before to avoid the use of the same process ? (see also firefox -P -no-remote).
To debug, use these commands :

ip rule show 
ip route show table vpn_euro
nft list ruleset
tcpdump -n -i tun_euro

Use the counters in the firewall to see where things goes.

Security

When a VPN solution can leaks :

When the VPN is down

You feel safe behind the VPN but knock knock the FBI is at your door. Oops the VPN was down and your traffic was not protected (real story, a hacker has been caught with that error).

The solution exposed here avoid this error because the traffic of processes using special groups can’t go, at anytime, anywhere else but in the tunnel.

By DNS

If your local internet is spied, your need to secure every traffic that you wants to hide, like DNS requests. This is not done here.

By IPv6 hell

Generally you are behind a firewall from your internet provider and your host use the local IPv4 to identify itself. So when this address is leaked you don’t care because it doesn’t carry valuable information. This is not the case with IPv6, where addresses identifies hosts. To avoid this here, we reject anytime all outgoing IPv6 traffic from processes with the specials vpn groups.

When the VPN is up

Seriously, you think that the NSA is sleeping ? VPN providers are very good spots to track people. So be careful if you are a good guy because there is a lot of bad guys on top. Hopefully they don’t track little hackers.