Why?
It is frustrating having to toggle between Tailscale and another VPN based on which features you want to leverage from each in the moment - and you can never have all benefits at the same time.
As an avid homelabber, I self host a variety of apps and services - from Radicale CalDAV to Jellyfin for media streaming. A cardinal requirement of such a system is a mechansim by which I can connect my different devices to one another regardless of where they are located. This is where Tailscale comes in. It offers a mesh VPN that allows me to securely tunnel between devices. As big a Tailscale fan as I am, I’ll defer hailing its many benefits to a dedicated post in the future.
This is great. I can now securely connect to my Ubuntu Server homelab from anywhere in the world and access all of its services. So, is this enough? Well, one of the many motivations for self-hosting apps and DeGoogling in the first place is to own your data and stop others from selling it. One very common concern is ISPs and websites selling your browsing data - which is where a traditional VPN enters the scene. A VPN, such as ProtonVPN will obfuscate your internet traffic. It will encrypt and tunnel your internet traffic to their own server, as a proxy, meaning the proxy is the first face the internet sees, not your IP.
The objective is to be able to connect my devices together with Tailscale and obfuscate their internet usage using ProtonVPN concurrently.
The problem
It is worth noting that a traditional VPN also has many other benefits like accessing geo-restricted content, but these auxiliary use-cases need not be engendered on a given client while Tailscale is turned on as far as my priorities are concerned. So the big issue here is that it’s very difficult to get Tailscale and ProtonVPN to play nicely simultaneously on a single device. I tested on my Macbook Pro and they didn’t seem to conflict, but they did conflict on my Windows PC and you straight-up cannot enable two VPNs on iOS at the same time. I do not want to battle OS configuration and “hack” Tailscale/ProtonVPN on every client.
I then had the thought, “if I setup my homelab as a Tailscale Exit Node, and setup ProtonVPN on the machine, any device connected to my Tailnet will have its traffic securely routed to my homelab and then carried to the internet by ProtonVPN”. This indeed was the first big realisation. Tailscale exit nodes allow your devices on the same Tailnet to elect an exit node through which all their traffic will be routed. So I could reduce the problem to: how can I run ProtonVPN and Tailscale concurrently on Linux?
The solution
I will firstly declare that I am light-years away from an expert at Linux or networking in general. I happened upon this solution by the circular process of:
- Try something
- That something breaks or fixes something else
- Consult ChatGPT and Google to understand why
- Use that learned knowledge to make a more informed decision about the next thing to try
Wireguard
In congruence with the above remark, I’d never really heard, let alone used, WireGuard before tackling this problem. Wireguard is VPN that allows for secure tunneling between devices. Sound familiar? Yep, that’s exactly what Tailscale does - it is built on WireGuard. Sound familiar still? Since ProtonVPN connects its servers to clients with secure tunnels, it too implements WireGuard.
My Ubuntu Server version already shipped with WireGuard, so the first step was to download a ProtonVPN configuration file that looked like this:
[Interface]
# Key for harry-lab
# Bouncing = 2
# NetShield = 0
# Moderate NAT = off
# NAT-PMP (Port Forwarding) = off
# VPN Accelerator = on
PrivateKey = ...
Address = 10.2.0.2/32
DNS = 10.2.0.1
[Peer]
# AU#87
PublicKey = ...
AllowedIPs = 0.0.0.0/0
Endpoint = 103.108.229.18:51820
That config can then be moved to /etc/wireguard/wg-proton.conf
and ProtonVPN spun up with wg-quick up wg-proton
. This will create a wg-proton
network interface. If you were to follow along to this point, you would notice that everything seems broken. There is no free lunch today. So the next step was to wg-quick down wg-proton
and get Tailscale setup correctly.
Tailscale
Tailscale is a relatively easy one, we just need to make sure it is spun up with two key flags:
tailscale up --accept-dns=false --advertise-exit-node
The latter is self-evident, but the former is critical because we are going to have to get a bit funky with DNS and take control of it ourselves. We want Tailscale to stay in its lane here and not assert its own DNS config.
DNS
This part sucks. This took me a long time to figure out. I am going to gloss over certain details here because the scope of DNS and DNS in Linux is beyond the scope of this post. Firstly, I disabled resolvconf
and systemd-resolved
. I wanted to run my own system for managing DNS. I achieved this with a CoreDNS container. This container is bound to port 53 on the host - the port used for DNS communications.
So how do we get CoreDNS to do what it needs to do? Below is the Corefile
(configuration file) I used. I will break it down line-by-line.
. {
log
template IN A "hstu.net" {
match "^(.*\.)?hstu\.net\.$"
answer "{{ .Name }} 60 IN A {$HOST_IP}"
}
forward ts.net 100.100.100.100
forward tailscale.com 1.1.1.1
forward tailscale.io 1.1.1.1
forward . 10.2.0.1
}
template IN A "hstu.net"
takes all *.hstu.net and hstu.net DNS queries and resolves them to theHOST_IP
. To those wondering, I have a reverse proxy container that will pick up corresponding HTTP/S traffic and ship it to its destination. This is an existing configuration that is orthogonal to this project.forward ts.net 100.100.100.100
declares that any DNS queries relating to my Tailnet should be forwarded to100.100.100.100
which is the virtual DNS server running inside thetailscaled
deamon on the host. Once queries reach this server, Tailscale has full command of them.forward tailscale.com 1.1.1.1
andforward tailscale.io 1.1.1.1
. These domains are necessary for thetailscaled
deamon to communicate with the Tailnet’s control plane to send and receive updates. I found that these queries could not be routed through ProtonVPNs DNS servers as Tailscale would show me homelab as offline in such a case.forward . 10.2.0.1
is the line that gives ProtonVPN authority to marshall all other DNS queries to its own DNS server to prevent DNS leaks. You would be astute to notice that10.2.0.1
is ripped straight out of ourwg-proton.conf
.
Finally, we just edit our /etc/resolv.conf
to assert that 127.0.0.1
is the nameserver that all systems should use and we add our Tailnet domain as a search domain so we can run commands like ping mytailscaledevice
.
nameserver 127.0.0.1
search tailNNNNN.ts.net
Time to update wg-proton.conf
Okay, we’ve now forced our own DNS server and need wg-proton
to relinquish its attemped control of our DNS. Before we do that, let’s take care of a couple of other things. Firstly, ProtonVPN works well with IPv4 but not 6, so I want to stop all IPv6 traffic and can do so with sysctl -w net.ipv6.conf.wg-proton.disable_ipv6=1
. Otherwise, IPv6 traffic would reveal my IP.
Secondly, DNS queries are not the only sort of traffic we need to account for, so even though we’ve now setup our DNS system, we need all Tailscale traffic to use the tailscale0
network interface and not the wg-proton
interface. This can be quite simply achieved by configuring the followig IP route ip route add 100.64.0.0/10 dev tailscale0
- Tailscale needs to manage all traffic in the CGNAT address space.
Now, while wg-proton
is not containerised, I would like it to be self-contained. Fortunately, rather than manually running the above commands, WireGuard has a way for us to automatically run them upon startup (PostUp) and can prevent side-effects by running the reversal commands on shutdown (PostDown.) Below is our final wg-proton.conf
:
[Interface]
PrivateKey = ...
Address = 10.2.0.2/32
PostUp = ip route add 100.64.0.0/10 dev tailscale0
PostUp = sysctl -w net.ipv6.conf.%i.disable_ipv6=1
PostDown = ip route del 100.64.0.0/10 dev tailscale0
PostDown = sysctl -w net.ipv6.conf.%i.disable_ipv6=0
[Peer]
PublicKey = ...
AllowedIPs = 0.0.0.0/0
Endpoint = 103.108.229.18:51820
Btw, %i
is replaced with the name of the interface, i.e. wg-proton
. As long as Tailscale is spun up before wg-proton
(so the tailscale0
interface exists), we are off to the races.
The good, the bad and the ugly
Alas, any device connected to my tailnet in any location can now turn on Tailscale and select my homelab as an exit node and connect to the internet via ProtonVPN. With a 250MiB/100MiB internet connection (Australia FTTP NBN), I notice basically 0 impact on internet speed if my devices are on the LAN (where my homelab resides.)
I generally find internet speeds sufficient for all my purposes, but if need be, I can always use the ProtonVPN app on my phone and disable Tailscale or use Tailscale and disable the exit node if I don’t care for the security. A fair bit of this can be nicely automated with iOS Shortcuts on my iPhone. In any case, 95% of my internet-heavy activities take place on my LAN anyway. Additionally, I would be stoked even if only my homelab could use ProtonVPN because it means any webscrapers and other apps I build have their IPs obfuscated.
This solution does not tick every box, but it gets me 80% of the way there - and I can work with that. Tailscale has an option to use Mullvad VPN exit nodes, but this is not available in Australia… Regardless, these things always change and I’m sure that this solution will evolve over-time.
This is my inaugural post on this blog so I welcome any and all feedback - you can get in touch via the email address linked on the homepage. Please feel free to share this post if you think it may help others and enjoy the rest of your day. Adios.