Update 2: I have since found a nice way to have everything containerised so you don't have to mess with networking on the host at all. You can see details at my repo over at connectorr but the basic idea is a Gluetun container for VPN communication and then you can opt other stacks in by using the connectorr to route traffic through the VPN by default but it has built in support for having certain subnets bypassed and can do stuff like health checks. I've included an example stack here with a Gluetun container and Tailscale container using it as a gateway for most communication except the local network or other Tailscale machines. Then I can use that Tailscale machine as an exit node from other devices when I want to route something through the VPN. If I want to route something through on the same machine then I just drop another connectorr into whatever stack I want to route for. With this solution I can very easily opt other containers in to routing through the VPN and either leave Plex routing normally on the host or add it container that just isn't opted in.
This is just an example, see connectorr for more context
services:
tailscale:
image: tailscale/tailscale
container_name: tailscale
cap_add:
- NET_ADMIN
- SYS_MODULE
volumes:
- ./tailscale/state:/var/lib/tailscale
environment:
- TS_STATE_DIR=/var/lib/tailscale/
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_EXTRA_ARGS=--advertise-exit-node
- TS_HOSTNAME=${TS_HOSTNAME}
- TS_USERSPACE=true
network_mode: service:connectorr
restart: always
healthcheck:
test: tailscale status --peers=false --json | grep -q 'Online.*true'
connectorr:
image: ghcr.io/wolffshots/connectorr:latest
cap_add:
- NET_ADMIN
environment:
- GATEWAY_IP=172.21.0.2
- BYPASS_IP=172.21.0.1
- BYPASS_SUBNETS=192.168.88.0/24
restart: unless-stopped
networks:
wgnet:
ipv4_address: 172.21.0.22
networks:
wgnet:
external: true
Update 1: Before I get into the details of how I worked out how to do this I have since found an alternative to these changes that may suit more people more is to use Wireguard and Tailscale in a Docker compose stack and then just use that Tailscale container as an exit node or alternatively the Wireguard container's network as a the network for whatever other container you want to route through it. See compose.yml for how you could set something like that up.
Instructions and scripts for getting Tailscale to work nicely with a Wireguard VPN on Linux and optionally getting Plex or another app to bypass both.
The instructions are primarily aimed at my use case which is Ubuntu Server (so with systemd
) but the basic idea and scripts should work with any Unix system that uses iproute2
(or something similar).
The script included with this project (split_routes
) can be used to add, delete and refresh ip rule
s and ip route
s even outside of this use case. You should be able to adapt it to work with things other than Plex.
Run sudo systemctl edit [email protected]
to edit the override file for the wg-quick
service for profile wg0.conf
.
# /etc/systemd/system/[email protected]/override.conf
[Unit]
Before=tailscaled.service plexmediaserver.service
This just tells the wg-quick
service for profile wg0.conf
that it should start before the tailscaled
and plexmediaserver
services. If you don't plan to use Plex then you can just leave out plexmediaserver.service
.
This is important because the wg-quick
script inserts ip rules above whatever is currently in your rules. This means it takes precedence over Tailscale and our Plex rules if it starts after them.
Run sudo systemctl edit tailscaled.service
to edit the override file for the tailscaled
service
# /etc/systemd/system/tailscaled.service.d/override.conf
[Unit]
[email protected]
[Service]
ExecStartPre=-/usr/sbin/ip rule add pref 65 table 52
ExecStop=-/usr/sbin/ip rule del pref 65 table 52
This adds a rule to check table 52 and since it's preference if 65 it should happen before routing stuff through Wireguard. This allows traffic to check the Tailscale table by default and then route everything that doesn't match that through the Wireguard VPN.
If you're interested you can check the Tailscale table with ip route show table 52
(while Tailscale is up). It should show the same IPs as are allocated to your devices on your Tailnet (as you should be able to see with tailscale status
)
If you set the Tailscale device as an exit node then connecting to it should allow you to use the Wireguard VPN connection as well.
You can follow similar steps to make any other app excluded from the Wireguard routing.
You'll need to add some rules for Plex to your config and there are a couple ways of doing that.
The first way is a little more robust in case IPs change but is has some security considerations because it allows the plex
user to change rules and routes without a password.
-
Copy the
split_routes
script in this project to somewhere accessible to theplex
user like/usr/local/bin
-
Run
sudo systemctl edit plexmediaserver.service
to edit the override file for theplexmediaserver
service# /etc/systemd/system/plexmediaserver.service.d/override.conf [Unit] [email protected] tailscaled.service [Service] ExecStartPre=-/usr/local/bin/split_routes refresh plexroute 60 plex.tv app.plex.tv metadata.provider.plex.tv v4.plex.tv resources-cdn.plexapp.com meta.plex.tv download.plex.tv assets.plex.tv analytics.plex.tv plex.direct ExecStop=-/usr/local/bin/split_routes del plexroute
You may need to add
<your public ip>.<identifier>.plex.direct
and<server ip>.<identifier>.plex.direct
to the start and stop script list. I got the identifier from my Plex logs but am not 100% sure of the meaning of it. -
Create a
sudoer
file for theplex
user to usesudo ip route
,sudo ip rule
andsudo iptables ... --set-mark 1
without a password for the script.sudo vim /etc/sudoers.d/plex
plex ALL=(ALL) NOPASSWD: /sbin/ip route add *, /sbin/ip route del *, /sbin/ip rule add to *, /sbin/ip rule del to *, /sbin/ip rule del from *, /usr/sbin/iptables -t mangle -A PREROUTING -p tcp --dport 32400 -j MARK --set-mark 1, /usr/sbin/iptables -t mangle -D PREROUTING -p tcp --dport 32400 -j MARK --set-mark 1, /usr/sbin/iptables -t mangle -A OUTPUT -p tcp --sport 32400 ! -d 192.168.88.0/24 -j MARK --set-mark 1, /usr/sbin/iptables -t mangle -D OUTPUT -p tcp --sport 32400 ! -d 192.168.88.0/24 -j MARK --set-mark 1, /sbin/ip rule add fwmark 1 table plexroute, /sbin/ip rule del fwmark 1 table plexroute
-
Edit the
crontab
for theroot
user withsudo crontab -e
# macro command @reboot sleep 60 && /usr/local/bin/split_routes refresh plexroute 60 plex.tv app.plex.tv metadata.provider.plex.tv v4.plex.tv resources-cdn.plexapp.com meta.plex.tv download.plex.tv assets.plex.tv analytics.plex.tv plex.direct # m h dom mon dow command 00 12 * * * /usr/local/bin/split_routes refresh plexroute 60 plex.tv app.plex.tv metadata.provider.plex.tv v4.plex.tv resources-cdn.plexapp.com meta.plex.tv download.plex.tv assets.plex.tv analytics.plex.tv plex.direct
This example runs the script to refresh the routes at noon every day and on boot. The
sleep
might not be necessary but the idea is to allow Wireguard and Tailscale a little extra time since we can't trigger the script explicitly after they start.