Tailscale + Proxmox: When Your Cluster Routes Against Itself
TL;DR
Running Tailscale directly on Proxmox hosts with --advertise-routes and --accept-routes for the same subnet causes hosts to route their own local traffic through the VPN tunnel, breaking cluster communication. The fix is adding a higher-priority policy routing rule in /etc/network/interfaces that forces local subnet traffic to stay on the physical bridge. This enables true HA subnet routing without needing a dedicated VM.
The Problem
I've been running Tailscale in a VM on one of my Proxmox hosts to provide subnet routing for my homelab. This worked fine until I needed to reboot that host - suddenly I'd lose all remote access to the network and have to fall back on UniFi Teleport or the UDM Pro's OpenVPN. Not ideal.
The obvious solution: run Tailscale on all the Proxmox hosts for redundancy. Just install it on each node, advertise the same subnet, and let Tailscale handle failover.
So I ran tailscale up --advertise-routes=10.0.4.0/24 --accept-routes=true on the first host (note: 10.0.4.0/24 is my local subnet - you'll need to substitute your own throughout this guide). That's when things got weird. The host became unreachable at its LAN IP (10.0.4.30) from other machines on the network. When I tried deploying Tailscale to more hosts, the problem spread - each host I added would disappear from the local network.
Even worse, the Proxmox cluster started having issues. Corosync couldn't maintain quorum properly. The web UI would work from some nodes but not others. ZFS replication between nodes slowed to a crawl.
Turns out when you tell a Linux box to both advertise AND accept the same subnet it lives on, it starts routing its own local traffic through the VPN tunnel.
I backed off and just created a dedicated VM for Tailscale routing. The VM itself couldn't be reached locally either, but that was fine since its only job was forwarding tailnet traffic. Still, this felt like a hack.
Initial Attempts
First thing I did was check if Tailscale had a built-in solution. Maybe a flag like --ignore-local-routes or something.
Nope. GitHub issue #1227 confirms this is a known limitation. Several people have requested this exact feature but it hasn't been implemented.
The community suggested a few workarounds:
- Keep Tailscale in a dedicated VM/LXC
- Disable
--accept-routeson hosts that advertise - Use policy routing to override the behavior
Option 2 was a non-starter since I need these hosts to reach other Tailscale subnets. Option 1 defeats the purpose of getting rid of the VM. That left policy routing.
The Root Cause
When Tailscale accepts a route, it adds a policy rule that takes precedence over your normal routing table:
$ ip rule show
# 52: from all to 10.0.4.0/24 lookup 52
Table 52 is Tailscale's routing table pointing everything to tailscale0. So when host 10.0.4.30 tries to reach 10.0.4.31 on the same bridge, Linux checks the policy rules first and sends it through WireGuard instead of vmbr0.
You can verify this is happening:
$ ip route get 10.0.4.31
# If broken: 10.0.4.31 dev tailscale0 table 52 src 100.x.x.x
# If fixed: 10.0.4.31 dev vmbr0 src 10.0.4.30
This explains why each host disappeared from the LAN - all their traffic was being tunneled through whichever node Tailscale picked as the active router, creating a bizarre loop where nodes tried to reach each other through themselves.
The Fix
⚠️ Safety Warning: Before making any changes to your network configuration, back up your /etc/network/interfaces file (cp /etc/network/interfaces /etc/network/interfaces.backup). Incorrect network configuration can break connectivity. It's highly recommended to have console access (IPMI or physical keyboard) available before proceeding in case something goes wrong.
The solution is to add a higher-priority routing policy rule that catches local traffic before Tailscale's rule can intercept it. You'll need to edit /etc/network/interfaces on each Proxmox host (see the Proxmox Network Configuration documentation for more details on this file).
Edit /etc/network/interfaces on each Proxmox host and add the following to your vmbr0 configuration (replace the placeholders with your details):
<HOST_IP_WITH_CIDR>= the IP/CIDR of this Proxmox host on the LAN (e.g.,10.0.4.30/24)<GATEWAY_IP>= your LAN gateway (e.g.,10.0.4.1)<LAN_INTERFACE>= the physical NIC bridged into vmbr0 (e.g.,eno1)<LOCAL_SUBNET>= the subnet you are advertising/keeping local (e.g.,10.0.4.0/24)
To find your values: run ip -4 addr show to see the host IP/CIDR on your LAN, ip route to see the default gateway, and ip link to identify the physical NIC that vmbr0 uses (look for the interface listed under bridge-ports in your current vmbr0 config).
auto vmbr0
iface vmbr0 inet static
address <HOST_IP_WITH_CIDR>
gateway <GATEWAY_IP>
bridge-ports <LAN_INTERFACE>
bridge-stp off
bridge-fd 0
# Enable WireGuard/UDP offloads for better throughput
post-up ethtool -K <LAN_INTERFACE> rx-udp-gro-forwarding on rx-gro-list off
pre-down ethtool -K <LAN_INTERFACE> rx-udp-gro-forwarding off rx-gro-list on
# Turn on IP forwarding
post-up sysctl -w net.ipv4.ip_forward=1
post-up sysctl -w net.ipv6.conf.all.forwarding=1
pre-down sysctl -w net.ipv4.ip_forward=0
pre-down sysctl -w net.ipv6.conf.all.forwarding=0
# Keep <LOCAL_SUBNET> on the local bridge (beats Tailscale table 52)
post-up ip rule add to <LOCAL_SUBNET> table main priority 1000
pre-down ip rule del to <LOCAL_SUBNET> table main priority 1000
Apply that block to each host, then restart networking and bring Tailscale up with --accept-routes=true and --advertise-routes=<your_subnet>. Tailscale will handle failover automatically once each node is advertising the same subnet.
If you want the same NIC tuning I used, add these UDP offload lines to the same /etc/network/interfaces vmbr0 block so GRO behaves well with Tailscale's UDP traffic:
# UDP offloads for performance
post-up ethtool -K <LAN_INTERFACE> rx-udp-gro-forwarding on rx-gro-list off
pre-down ethtool -K <LAN_INTERFACE> rx-udp-gro-forwarding off rx-gro-list on
Swap <LAN_INTERFACE> for your actual NIC name (example: eno1). These lines are optional but help keep UDP forwarding throughput high (see Tailscale's UDP throughput notes for the reasoning).
The priority 1000 rule evaluates before Tailscale's rules, keeping local traffic local. Priority numbers in Linux policy routing range from 0 to 32767, where lower numbers = higher priority. Quick reference:
| Priority | Meaning |
|---|---|
0 |
Local rules (reserved by kernel) |
1-999 |
Custom high-priority rules |
1000 |
Our rule (beats Tailscale) |
5210+ |
Tailscale's rules |
32766 |
Main routing table (default) |
32767 |
Default rule (fallback) |
Quick checks and extras:
- After editing, run
ifreload -a(orsystemctl restart networking) and verify withip rule show | grep <LOCAL_SUBNET>plusip route get <peer_on_subnet>to ensure it uses vmbr0. - Hardware caveat: confirm your NIC supports the offload flags with
ethtool -k <LAN_INTERFACE> | grep -E \"udp-gro-forwarding|gro-list\"; if not supported, drop those lines. - If you advertise IPv6, mirror the rule/forwarding for your IPv6 prefix:
# Keep IPv6 subnet on the local bridge
post-up ip -6 rule add to <LOCAL_IPV6_SUBNET> table main priority 1000
pre-down ip -6 rule del to <LOCAL_IPV6_SUBNET> table main priority 1000
# Enable IPv6 forwarding
post-up sysctl -w net.ipv6.conf.all.forwarding=1
pre-down sysctl -w net.ipv6.conf.all.forwarding=0
- Test plan: reboot one node at a time, confirm LAN reachability and tailnet subnet access, then rotate through the cluster.
Since Tailscale's rules start at 5210, our priority 1000 rule is evaluated first and wins.
Results
Now I have:
- Redundant subnet routing (LAN reachability survives host reboots as long as your router static routes point at an available node; add keepalived or similar if you need automatic gateway failover)
- Local cluster traffic stays on the physical bridge
- Tailscale handles failover automatically in about 15 seconds
Testing failover is simple:
# From a remote machine, ping something on the subnet
$ ping 10.0.4.100
# On the Proxmox host currently routing, stop Tailscale
$ systemctl stop tailscaled
# Within 15-20 seconds, pings resume through another host
And most importantly, the Proxmox cluster is happy. Corosync maintains quorum, the web UI is accessible from any node, and ZFS replication runs at full speed.
The whole setup is simpler than maintaining a dedicated router VM, and I can bounce any host for maintenance without thinking twice about remote access. Just took some creative policy routing to make Linux do what I wanted.