Disclaimer
This article is a review of my personal configuration. Although I will try my best to describe each step to replicate the results I achieved, I can't guarantee that it will work on every system, so follow it at your own risk. I highly advise against configuring it over the network/headless (such as ssh connection), because a misconfiguration can lead to loss of connection until fixed. But the final result will allow running both VPNs on a headless setup.
Note that while I have some familiarity with technology, I am no expert in system security and I don't know what the possible consequences can be (such as packet/DNS leaks).
I welcome every one to point suggestions and improvements to the setup, so we, together, can create an useful and safe setup for the community to use :).
Introduction
This setup came from the motivation of running Proton VPN and Tailscale VPN alongside with each other. For me, each one of them serves a different purpose: Proton VPN allows me to access the web privately, while Tailscale offers security while connecting to my personal LAN/home lab. The tools chosen to create such configuration came from the idea of utilizing the least amount of package requirements possible, since I keep pushing myself on creating minimal setups, to utilize the least amount of RAM and energy as possible.
Before this setup I would configure each of my devices individually to access both VPNs, but I had multiple instances where a Proton VPN server would go down and I found myself having to reconfigure every device to a new server (which was kind of painful, since my router can't work as a VPN client). Setting one device as an Tailscale exit node, while keeping it connected to Proton VPN, allowed me to centralize the configuration, where the changes to one device would be applied to the rest.
Even though I am pretty satisfied with the final result, nothing is perfect. There's are some drawbacks to such setup: there's a noticeable loss of connection speed when having your connection going through multiple VPNs (for this setup, I would say around 20%, I am on a 700mbps link and I can reach around 550-600mbps using it), the dependency on one device to be constantly powered on and connected to the network (which is fairly common for home lab enthusiasts, so that shouldn't be a problem) and the dependency on the route chosen by Tailscale VPN (best results are found when devices are direct connected. There's a speed penalty when connection goes through relay servers).
I will have some references for the steps described in this article, so anyone can check where the ideas came from and further explore new possibilities of this configuration.
Materials and Methods
My setup was created using Arch Linux, so every package name and file paths will be as seen on an Arch Linux installation. For this article I will be using:
- systemd (which is pretty much present on every modern Linux installation)
- tailscale
- iwd (if you need wireless connection)
- ufw (if you want a killswitch)
For the extra sections of this article, I will be using ufw to explore the possibility of having a killswitch for the setup. Although I won't be able to provide much information on this part, since I am still experimenting with it, I believe it's possible to use it as a fairly good replacement for a proper killswitch. In case you have your own a DNS server, I will show how to use it as your exit node's primary DNS server.
Basic configuration for systemd-networkd
Systemd is one of the foundations of most Linux distributions nowadays. It's a set of tools to manage services, containers, networks, logs and other system resources[1]. Since it's present by default on Arch Linux, I try to use (and abuse) it as much as I can. The Arch Wiki has a detailed article on how to setup networks using it[2], but in this article I will try to go through the basic to get the setup up and running. This configuration should only be done to the device that will be running as the exit node. Other devices can be configured to your liking, as long as it's set to use the exit node via Tailscale.
[!WARNING]
If you have any other software that manages network connections, such as NetworkManager, I advise uninstalling it, since it can conflict with the setup.[3].
If systemd is already installed, the first thing to do is to get the service running. This can be achieved by executing the following command:
sudo systemctl enable --now systemd-networkd.service systemd-resolved.service
This command will enable both systemd-networkd and systemd-resolved services to auto-start on device boot, and the --now flag will also start for the current boot[4], so you can have both services started and running, so further configuration can be done.
Since you will be using systemd-resolved for domain name resolution, you need to link its configuration to the commonly used configuration, /etc/resolv.conf, to make sure some programs won't run into problems[5]. This is achieved by creating the symbolic link:
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
For the next steps, you will need to know the names of our network interfaces. They can be listed with the following command:
networkctl
For this article, I will use eno1 for the ethernet (wired) interface and wlan0 for the wireless interface, so replace them with the name of your interface. You don't need to configure both, only the one that you will be using.
The configuration files are located at /etc/systemd/network/. That's the default place systemd looks for network configuration. There are two types of configuration files that it will be needed for this setup: .network[6] and .netdev[7]. The first one is used to setup how network interfaces will interact with the network, while the second one is to setup a network device (which is used to created Wireguard connections).
Wired connection
The basic configuration for a wired connection would be creating a wired.network file at /etc/systemd/network/ with the following content[8]:
[Match]
Name=eno1 # Replace with your network interface name
[Network]
DHCP=ipv4
Once the file is created, run the following commands to reload the configuration and reconfigure the network interface:
sudo networkctl reload
sudo networkctl reconfigure eno1
This should be enough to give your device the ability to communicate with the DHCP server and connect to the internet. Please test your connection before going further.
Wireless connection
The basic configuration for a wireless connection would be creating a wireless.network file at /etc/systemd/network/ with the following content[9]:
[Match]
Name=wlan0 # Replace with your network interface name
[Network]
DHCP=ipv4
IgnoreCarrierLoss=3s
Once the file is created, run the following commands to reload the configuration and reconfigure the network interface:
sudo networkctl reload
sudo networkctl reconfigure wlan0
While systemd is able to manage network connections, it can't manage Wi-Fi credentials. For this, you can use iwd[10]. Once installed, you can run it[11] and then connect to your preferred network[12]. Please test your connection before going further.
Installing Tailscale
Tailscale offers a tutorial on how to install their services on Linux[13]. If you are using Arch Linux, you can refer to the Arch Wiki[14]. Once you get your device connected to your tailnet, run the following commands to proper advertise your device as an exit node[15]:
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
sudo tailscale set --advertise-exit-node
sudo tailscale up
You also need to go to the Admin Console, on Tailscale dashboard, and allow the device to be an exit node. If everything worked, you will see the offers exit node when prompting tailscale status.
Understanding iprules
Now that Tailscale is installed, I will explain how I was able to run it alongside with another Wireguard VPN (in my case, Proton VPN). I am no expert on iprules, but messing around a bit, I was able to figure out a way to configure the priority of each network connection and have them both working graciously.
If you run the following commands, you will see the ip rules that are currently configured on your setup:
ip rule
This should return something similar to:
0: from all lookup local
5210: from all fwmark 0x12345/0xab1234 lookup main
5230: from all fwmark 0x12345/0xab1234 lookup default
5250: from all fwmark 0x12345/0xab1234 unreachable
5270: from all lookup 12
32766: from all lookup main
32767: from all lookup default
I have obfuscated some of the information, because I am not sure how critical it would be sharing them, but what matters here is the first number in each line. That's the priority of each rule. From messing with the configuration, I found that rules 5210 to 5270 are the ones used by Tailscale to route its traffic. The layout for the final setup would be something like:
This way, the setup has Device 1 through 4 mutually connected via Tailscale VPN and Device 3 set as an exit node, communicating to the internet via Proton VPN. Note that if you ask Proton VPN to send a packet to address 100.100.100.100 (an example of tailnet device address), it wouldn't be able to do it, because Proton servers don't have access to your tailnet. So the setup needs to first check if the packet is to be sent to a tailnet device, if not it should send it to the internet instead, via Proton. That's when the IP rules come into play.
The lower the priority of the rule, the sooner it will be checked. So when Proton VPN is configured, its rule priority needs to be higher than the last tailscale rule, but lower than main/default.
Configuring Wireguard VPN
Now that Tailscale is configured and a plan based on the IP rules is set, it's time to configure the Wireguard VPN. It should work for any Wireguard VPN, as long as you can generate a generic configuration file. I will use Proton VPN as an example, which is the provider I use.
Generating Proton VPN configuration file
To generate a Wireguard configuration file, the following steps are done using their dashboard. Once in the dashboard, navigate on the left menu to the Wireguard section. This section is where you can generate Wireguard files to connect your device without using Proton's official app.
For the first part, Give a name to the config to be generated, give a symbolic name to the configuration. This information won't matter much, but can be helpful if you want to invalidate the generated configuration later.
For the second part, Select platform, select GNU/Linux. This shouldn't matter much, since it will only change the format of the output file, but selecting this option will be the closest to what you need.
For the third part, Select VPN options, select the configuration you want to the VPN connection. Once the file is generated, you can't change these values, so explore the articles referred in the Learn More links to make sure you select the proper values for your taste. The default options should be the best for privacy. If, later, you want to change any of these settings, you would only need to generate a new file and edit the configuration that will be created later in this article.
For the last part, Select a server to connect to, select the server where you want to connect your devices to. Same as last step, once selected, you can only swap servers by generating a new file. Later in the article, I will explain how you can have multiple files ready for easy server swap, if needed.
Lastly, click on the Create button and this will generate a new configuration file. Download it, as it will be needed for the configuration.
Creating a Wireguard Connection
To create a Wireguard connection using systemd-networkd, two files will need to be created, as mentioned before: .network and .netdev. This section is heavily inspired by Ihor Kalnytskyi's tutorial[16]. These files will be created at the default configuration folder, /etc/systemd/network.
First file to be created will be proton1.netdev, with the following content:
[NetDev]
Name=proton1
Kind=wireguard
[WireGuard]
PrivateKey=<your-private-key>
FirewallMark=1000 # theorically, you can set as any valid value
[WireGuardPeer]
PublicKey=<your-public-key>
AllowedIPs=<your-allowed-ips>
Endpoint=<your-endpoint>
The to-be-replaced values should be replaced by the values found for the same key in the file you generated and downloaded from Proton. The FirewallMark configuration is used to “tag” any packet originated by this Wireguard device. It will be used in the next configuration file.
For the next file, it will be named proton1.network, with the following content:
[Match]
Name=proton1
[Network]
Address=<your-address>
DNS=<your-dns>
DNSDefaultRoute=yes
[RoutingPolicyRule]
InvertRule=yes
FirewallMark=1000 # Keep the same as previous file
Table=6000
Priority=6000
[Route]
Gateway=10.0.0.1
GatewayOnLink=yes
Table=6000
This file will configure how packets will use Proton network. A few configuration to be commented on:
- DNSDefaultRoute: This will make sure that the address for DNS used in this configuration will be the one used as default.
- InvertRule: When creating a RoutingPolicyRule, any rule specified inside the block is used to decide if the packet will or not use the route defined by the Table. Setting it to invert, it's saying to use the Table route if there's no match.
- FirewallMark: This would try to match the packet FirewallMark.
- Table: The IP table used for this configuration.
- Priority: The priority of executing this rule (as previously discussed)
So it's set a priority higher than Tailscale, which means that it will be run later than Tailscale, and, since the InvertRule is present, it will use this network for any packet that is not originated from Proton, since it will be looking for packets without the FirewallMark that was created. This will guarantee that every packet will go through Proton VPN (unless it's a Tailscale packet).
Last step is to guarantee that the setup won't be using the DHCP DNS server. To do this, Go to the file /etc/systemd/network/wired.network (or the wireless.network file) and add the following line inside the [Network] block:
[Match]
Name=eno1
[Network]
DHCP=ipv4
DNSDefaultRoute=no # <--- This line
Now, to show the configured proton connection, run the following command:
sudo networkctl reload
sudo networkctl reconfigure proton1
The new network device, proton1, should be visible when running command:
networkctl
If it's not, you can't try reloading systemd configuration files and restart systemd-networkd service:
sudo systemctl daemon-reload
sudo systemctl restart systemd-networkd.service
Now you can test your IP address and DNS leak to make sure all connections are going through proton connection. Do the same tests with the devices connected to your exit node device.
[Extra] Setting your AdGuard Home / PiHole as your DNS server
If you already have an instance of AdGuard Home/PiHole and it's accessible by your device, you can set if as your default DNS server. In my setup, I have a quadlet running AdGuard Home on the same device, so here's how to use it.
[!NOTE]
Since it's being set as your exit node device DNS server, it will also be used by any device connected, using it as an exit node.
To do this, go to the .network file that was created for proton's connection and edit the DNS entry with your DNS route:
[Match]
Name=proton1
[Network]
Address=<your-address>
DNS=<your-dns> # <--- This line
DNSDefaultRoute=yes
[RoutingPolicyRule]
InvertRule=yes
FirewallMark=1000
Table=6000
Priority=6000
[Route]
Gateway=10.0.0.1
GatewayOnLink=yes
Table=6000
Now run the following commands to reconfigure the proton device and apply the changes:
sudo networkctl reload
sudo networkctl reconfigure proton1
You can check which DNS servers are being used by running:
networkctl status
If your AdGuard Home / PiHole is hosted on the same device that was configured as the exit node with Proton connection, you can set the upstream DNS server as seen in Proton's configuration file (ex. 10.2.0.1), to use Proton VPN DNS server.
[Extra] Using ufw as a killswitch
[!WARNING]
Experimental: I haven't done much testing, but should be possible if configured right
Ufw is a software for managing a netfilter firewall[17]. With it, you can allow or block connections based on IP, port or interface.
[!CAUTION]
You can create strict filters that would only allow connections you permitted. Note that Tailscale uses a wide range of IP addresses to function properly. Mapping them can be difficult and blocking needed connections can lead to unstable setups. Some of Tailscale's needed connections are documented here[18].
If you have ufw installed[19], you could run something like this to block, by default, any incoming and any outgoing packet:
sudo ufw default deny incoming
sudo ufw default deny outgoing
If you want to only allow packets going out via Proton device, you could do:
sudo ufw allow out on proton1 from any to any
Note that this would not be enough to get Proton connections working, because you need to allow outgoing packets on eno1 or wlan0 to Proton's IP address as well:
sudo ufw allow out on eno1 from any to <your-proton-endpoint>
Replace the with the same endpoint used in proton1 configuration file. This would be enough to block any connection not going to Proton. The same could be done to Tailscale, creating a hardened setup.
[Extra] Configuring IP based split tunnels
You can configure IP based split tunnels using the combination of RoutingPolicyRule and Route previously seen. For this example, I will show how to connect to all LAN devices, without passing through either Tailscale or Proton.
To achieve this, you need to edit the /etc/systemd/network/wired.network (or the wireless.network file) that was created and add the following:
[Match]
Name=eno1
[Network]
DHCP=ipv4
DNSDefaultRoute=no
# vvvv ADDED LINES vvvv
[RoutingPolicyRule]
To=192.168.0.0/24
Table=1
Priority=1
[Route]
Gateway=192.168.0.1
GatewayOnLink=yes
Table=1
Adding this to the wired.network file, would make any connection going to the subnet 192.168.0.0/24 use eno1 device. Since the RoutingPolicyRule is set with priority 1, it would be executed before checking either Tailscale or Proton rules, so leaving the setup without using any VPN. You can create multiple RoutingPolicyRule to create complex configurations and multiple split tunnels[20].
After any change, run:
sudo networkctl reload
sudo networkctl reconfigure eno1
[Extra] Creating multiple Wireguard configuration for easy swap
Repeating the steps to create the Wireguard connection, you should be able to have multiple connections set, as long as you keep unique names (although only the one with lowest priority would be used). You could create proton2 from a new configuration file like:
[NetDev]
Name=proton2
Kind=wireguard
[WireGuard]
PrivateKey=<your-private-key>
FirewallMark=1000 # theorically, you can set as any valid value
[WireGuardPeer]
PublicKey=<your-public-key>
AllowedIPs=<your-allowed-ips>
Endpoint=<your-endpoint>
[Match]
Name=proton2
[Link]
ActivationPolicy=manual <--- NEW LINE
[Network]
Address=<your-address>
DNS=<your-dns>
DNSDefaultRoute=yes
[RoutingPolicyRule]
InvertRule=yes
FirewallMark=1000 # Keep the same as previous file
Table=6000
Priority=6000
[Route]
Gateway=10.0.0.1
GatewayOnLink=yes
Table=6000
For this extra configuration, I added a new setting: ActivationPolicy[21]. This is used by systemd to know if the configuration should be started automatically or manually. The default value (if absent) is to automatically start the network. So having proton1 to automatically start, you can set numerous connections to manually start and swap them using following commands:
sudo networkctl up proton2
sudo networkctl down proton1
This would swap the connection from the server configured in proton1 to the server configured in proton2. And since the device is being used as an exit node, it would also change to all its clients.
-
https://wiki.archlinux.org/title/Systemd-networkd#Required\_services\_and\_setup
-
https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html
-
https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html
-
https://wiki.archlinux.org/title/Systemd-networkd#Wired\_adapter\_using\_DHCP
-
https://wiki.archlinux.org/title/Systemd-networkd#Wireless\_adapter
-
https://wiki.archlinux.org/title/Iwd#Connect\_to\_a\_network
-
https://tailscale.com/kb/1103/exit-nodes?tab=linux#advertise-a-device-as-an-exit-node
-
https://kalnytskyi.com/posts/setup-wireguard-client-systemd-networkd/
-
https://wiki.archlinux.org/title/Uncomplicated\_Firewall#Installation
-
https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ActivationPolicy=

Top comments (0)