Switching to Proton VPN

I’d been using ExpressVPN for some time, but got sick of the price increases. So I’m now using Proton VPN instead. At the time of this writing, ExpressVPN was about to increase their price I think to $150/yr. Proton is $70, with a $20 discount as a new customer. So 1/3 the cost up front, and a little less than half next year.

ExpressVPN uses their own application, which is pretty easy to use, whereas Proton is built on WireGuard. I.e. Proton has you configure what you want on their website, which country and server #, port forwarding, etc., and then generates a config file for you to download and use with WireGuard. Not quite as easy, but WireGuard in general is pretty easy to use, so it should be fine.

The Situation So Far

Transmission is already installed and configured on the VM, and on the client. Make sure to use a username and password, it’s not all that hard to configure (edit the .json file and restart the transmission-daemon service).

The Problem

First we just get rid of expressvpn.

sudo apt remove --purge expressvpn

Then we install wireguard.

sudo apt install wireguard wireguard-tools

Then we go onto Proton VPN, select our server and whatnot, and download our config file.

Now we just have to connect to the VPN and Bob’s your uncle! Right?

sudo wg-quick up ./proton-nl.conf

First, transmission-remote client loses its connection to the remote host. Maybe that’s not such a big deal. It’s still downloading in the background, right?

No progress has been made. All of the torrents show errors about storage not being available. Transmission is telling me that I need to set the storage location, or manually re-validate the existing data before it will continue downloading.

What’s going on here?

When I connect to the VPN on that VM, all other traffic is blocked. This is good, because I want to be safe on the internet, right? It’s working as intended.

But I want to be able to SSH in if needed. I want to be able to connect with transmission-remote on port 9091. I need to connect to my NFS shares where the files are ultimately being downloaded to.

I can’t do any of that if I’m blocking local traffic.

So how do we get all traffic to flow through the wireguard connection, except for anything to/from other local IP ranges?

First, let’s try to understand a bit about what’s going on in the network stack.

How traffic is flowing nominally

The VM has 2 interfaces, loopback “lo” and ethernet “ens18”. When I bring the wireguard connection up, it creates a third interface. I’m using The Netherlands as my server, and called it “proton-nl.conf”, so the interface created is called “proton-nl”.

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether bc:24:11:cb:62:5f brd ff:ff:ff:ff:ff:ff
    altname enp0s18
    inet 10.10.20.2/24 brd 10.10.20.255 scope global ens18
       valid_lft forever preferred_lft forever
5: proton-nl: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.2.0.2/32 scope global proton-nl
       valid_lft forever preferred_lft forever

Notice that the VPN interface as a netmask of 32? So we only have one single address in that network space. Also notice that the IP address chosen for that interface doesn’t conflict with ens18. If I were already using the 10.2.1.x/24 subnet on this machine, I would have to choose something else in my wireguard config file.

Which by the way, here’s the original wireguard config file:

[Interface]
# Bouncing = 8
# NetShield = 0
# Moderate NAT = off
# NAT-PMP (Port Forwarding) = on
# VPN Accelerator = on
PrivateKey = __________
Address = 10.2.0.2/32
DNS = 10.2.0.1

[Peer]
PublicKey = __________
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = x.x.x.x:51820

All traffic is being routed to the VPN host in the Netherlands. That device gets packets with destination in 192.168.x.x or 10.x.x.x (besides 10.2.0.2), which are defined as private ranges, and it should be dropping them. Which I guess is the intended behavior, which it’s doing!

The Solution

We’re going to add a static route in our [main] routing table.

When the linux kernel gets a packet, it checks the routing tables to see what to do with it, starting with the lowest numbered table. If it matches a rule, it gets routed as the table directs it. If it’s not matched, it goes to the next rule to try and match again.

$ip rule
0:	from all lookup local
32764:	from all lookup main suppress_prefixlength 0
32765:	not from all fwmark 0xca6c lookup 51820
32766:	from all lookup main
32767:	from all lookup default

You can read more about routing in linux here. I’m no expert yet. But per the great explanation, when wireguard is running and the VPN connection is up it creates the two rules in the middle (65 and 66), and it is sending all traffic to the “proton-nl” interface.

$ip route list table 51820
default dev proton-nl scope link

So let’s add some routes in the local routing table and see if it works. The idea is that we hit table 0 first, and it will be true, never giving it a chance to check against the 51820 routing table. I want to use my legacy 192.168.0.x/24, and the newer 10.10.x.x/16 (using 3rd octet for different VLANs).

$ip route add 10.10.0.0/16 via 10.10.20.1 dev ens18
$ip route add 192.168.0.0/24 via 10.10.20.1 dev ens18

$ip route show
default via 10.10.20.1 dev ens18 proto static
10.10.0.0/16 via 10.10.20.1 dev ens18
10.10.20.0/24 dev ens18 proto kernel scope link src 10.10.20.2
192.168.0.0/24 via 10.10.20.1 dev ens18

And it’s successfully showing entries 2 and 4 now. Essentially I’m just telling linux that if a packet is in that range, to act like a router, and that the next hop is the router that I myself (the transmission VM) is connected to. That next hop is on device ens18.

Once that happens, it does in fact go to 10.10.20.1, which is the router for my VLAN 20, and I have access control lists (ACLs) setup to flow traffic to other VLANs on 10.10.30/40/etc.x, or to 192.168.0.x. If those ACLs were not in place, the packets would die at my physical router, instead of dying in the VM. So make sure you have rules in place to handle them when they get to the desired device.

Here’s a better explanation here if you’re a visual learner like me.

Here’s what it looks like when done correctly:

The local traffic hits the first routing table, passes the check, and gets sent along to ens18. The other traffic fails the check on routing table 0, continues on to table 51820, then gets forwarded to the VPN interface.

Now we need to make sure that this is done automatically every time I bring the wireguard interface up, and undone when I take it down. (Not technically, it wouldn’t hurt anything to keep these in place regardless of VPN status. They’d just be covered under the other rules.)

So go back and edit your wireguard config file with the PreUp and PostDown commands. And throw in PersistentKeepalive for good measure, since there could be long periods of time where this connection sits idle.

[Interface]
# Bouncing = 8
# NetShield = 0
# Moderate NAT = off
# NAT-PMP (Port Forwarding) = on
# VPN Accelerator = on
PrivateKey = __________
Address = 10.2.0.2/32
DNS = 10.2.0.1
PreUp = ip route add 192.168.0.0/24 via 10.10.20.1 dev ens18
PostDown = ip route del 192.168.0.0/24 via 10.10.20.1 dev ens18
PreUp = ip route add 10.10.0.0/16 via 10.10.20.1 dev ens18
PostDown = ip route del 10.10.0.0/16 via 10.10.20.1 dev ens18

[Peer]
PublicKey = __________
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = x.x.x.x:51820
PersistentKeepalive=30

Bonus Points

Ensure port forwarding is enabled and working. This could keep peer discovery from happening as intended. Making things much slower. Tutorial here

Bind transmission-daemon to the IP address of the wireguard interface. If someone were to get through my public IP address of my router and try to connect as a peer, transmission would allow it. This is unlikely since I have only 3 ports open (my own wireguard instance, which no one has credentials to, and 80/443 which go to my reverse proxy). But if it did happen, transmission would indeed allow it.

You can use either the public IP, or in our case which is much easier, the local IP assigned to the interface. Which for me is 10.2.0.2, as defined in the WG config file.

$service transmission-daemon stop
"bind-address-ipv4": "10.2.0.2",
"rpc-whitelist": "127.0.0.1,10.10.*.*,192.168.0.*",
"rpc-whitelist-enabled": true,

This also binds local addresses to the transmission Remote Protocol Call (RPC) aka transmission remote client connecting to the host. This way I can get to the web interface at 10.10.20.2:9091/transmission/web or use a remote client directly, but only from one of my local machines.

End

That’s it. When I setup ExpressVPN, I never had these issues, I could still connect to local IPs just fine. I think their client is probably just doing these things in an automated fashion that’s transparent to the user. But it also begs the question; why is the ExpressVPN client for Windows 329MB in size?! Surely it’s not that sophisticated. And surely not worth an extra $80+/year!

But this wasn’t really all that hard. Most of this post was explaining the why behind it. The actual configuration to make it work is just 2 lines (PreUp and PostDown commands in the wireguard config file).