Fixing Tailscale Subnet Routing Conflicts with Local Networks

Why?

During the setup of Automatic WordPress Fail-over & Recovery (Tergum), we shared the LAN subnets of participating servers with each other over Tailscale. This was done to facilitate easy SSH access between the participants. Unfortunately, after enabling subnet routing, we discovered that packets sent to the master server via its local IPv4 interface were being returned via its Tailscale interface. This asymmetric routing had the effect of making the master server’s websites unreachable and caused the server to appear down—not answering pings or HTTP requests.

Diagnosing the Problem

Running tcpdump on the master server while pinging from the local network revealed the issue:

bash

sudo tcpdump -i any icmp and host 10.10.0.207
```

Output showed:
```
16:53:50.178125 enp1s0 In   IP abc.blandford.tech > www: ICMP echo request
16:53:50.178192 tailscale0 Out IP www > abc.blandford.tech: ICMP echo reply

The smoking gun: Ping requests arrived on enp1s0 (the physical network interface), but replies departed via tailscale0 (the Tailscale VPN interface). This is a classic asymmetric routing problem.

Root Cause

When we enabled Tailscale subnet sharing, Tailscale added our local subnet 10.10.0.0/24 to its routing table (table 52). We confirmed this with:

bash

ip route show table all | grep tailscale
```

Which showed:
```
10.10.0.0/24 dev tailscale0 table 52

Because of Tailscale’s firewall marks and routing rules, return packets were being routed through table 52 instead of the main routing table, sending them back through the VPN tunnel rather than the local interface.

The Solution

Rather than removing Tailscale’s route (which would break VPN connectivity), we added a policy routing rule with higher priority to handle local-to-local traffic:

bash

# Tell the kernel: traffic between 10.10.0.0/24 hosts uses the main routing table
sudo ip rule add from 10.10.0.0/24 to 10.10.0.0/24 lookup main priority 99

This surgical fix ensures:

  • ✅ Local network traffic (10.10.0.x ↔ 10.10.0.x) uses the physical interface
  • ✅ Tailscale traffic from remote networks continues working normally
  • ✅ SSH access via Tailscale remains intact

Making It Permanent

To ensure the fix survives reboots, create a network startup script:

bash

sudo nano /etc/network/if-up.d/local-routing

Add this content:

bash

#!/bin/bash
# Fix Tailscale routing for local subnet
ip rule add from 10.10.0.0/24 to 10.10.0.0/24 lookup main priority 99 2>/dev/null || true

Make it executable:

bash

sudo chmod +x /etc/network/if-up.d/local-routing

Verify it was created:

bash

ls -la /etc/network/if-up.d/local-routing
cat /etc/network/if-up.d/local-routing

Verification

Check that the routing rule is active:

bash

ip rule show
```

You should see:
```
99:     from 10.10.0.0/24 to 10.10.0.0/24 lookup main

That’s it! Local access should now work normally while preserving all Tailscale functionality.

Key Takeaway

When using Tailscale’s subnet routing feature, be aware that it can inadvertently capture local traffic. Policy routing rules allow you to maintain both local connectivity and VPN access without conflicts. Always test connectivity from multiple network paths after enabling subnet sharing.


Feel free to adjust the tone/style to match your blog! Want me to tweak anything?

SB

ok, this is wierd. I rebooted the server and no I cannot ping it or bring up the web pages again…

Oh no! Let’s troubleshoot quickly.

First, can you SSH in via Tailscale?

If yes, once you’re in:

bash

# Check if the routing rule survived
ip rule show

# Check if the network is up
ip addr show enp1s0

# Check routing table
ip route show

# Check if services are running
systemctl status nginx
systemctl status ufw

If you CAN’T SSH via Tailscale either, you’ll need console access (KVM/QEMU console, VNC, or physical access).

Most likely issues:

  1. The /etc/network/if-up.d/local-routing script didn’t run or had an error
  2. Network interface didn’t come up properly
  3. Tailscale started before the network was ready and grabbed the route again

Once you’re in, check:

bash

# Did the script run?
ls -la /etc/network/if-up.d/local-routing
cat /etc/network/if-up.d/local-routing

# Check system logs for errors
journalctl -b | grep -i "local-routing\|network\|tailscale"

Can you get console access? Try SSH via Tailscale first – that should still work since the VPN routes are probably fine.

SB

root@www:/# ip rule show 0:      from all lookup local 5210:   from all fwmark 0x80000/0xff0000 lookup main 5230:   from all fwmark 0x80000/0xff0000 lookup default 5250:   from all fwmark 0x80000/0xff0000 unreachable 5270:   from all lookup 52 32766:  from all lookup main 32767:  from all lookup default root@www:/#

There it is! The rule at priority 99 is missing. The script didn’t run.

Quick fix – add it back manually:

bash

sudo ip rule add from 10.10.0.0/24 to 10.10.0.0/24 lookup main priority 99

Now let’s figure out why the script didn’t run:

bash

# Check if the script exists and is executable
ls -la /etc/network/if-up.d/local-routing

# Check its contents
cat /etc/network/if-up.d/local-routing

# Check if if-up.d scripts are being executed at all
ls -la /etc/network/if-up.d/

# Check system logs for any errors
journalctl -b | grep -i "if-up\|local-routing"

Possible issues:

  1. Script lost execute permissions
  2. Script wasn’t saved properly
  3. The network subsystem doesn’t trigger if-up.d scripts on your system (some modern systems use systemd-networkd or netplan instead)

Let me know what you find – we may need to use a different method to make it permanent (like a systemd service).

SB

root@www:/# ls -la /etc/network/if-up.d/local-routing -rwxr-xr-x 1 root root 143 Dec  7 17:03 /etc/network/if-up.d/local-routing root@www:/# cat /etc/network/if-up.d/local-routing #!/bin/bash # Fix Tailscale routing for local subnet ip rule add from 10.10.0.0/24 to 10.10.0.0/24 lookup main priority 99 2>/dev/null || true root@www:/#  root@www:/# ls -la /etc/network/if-up.d/ total 32 drwxr-xr-x 2 root root 4096 Dec  7 22:00 . drwxr-xr-x 6 root root 4096 Aug 19  2021 .. -rwxr-xr-x 1 root root 1053 Aug 19  2020 000resolvconf -rwxr-xr-x 1 root root  923 Aug 20  2018 avahi-autoipd -rwxr-xr-x 1 root root 1689 Jan 30  2024 ethtool -rwxr-xr-x 1 root root  143 Dec  7 17:03 local-routing -rwxr-xr-x 1 root root  385 Jul 19  2021 openvpn -rwxr-xr-x 1 root root  207 Jan 31  2025 postfix lrwxrwxrwx 1 root root   32 Sep 19 07:07 wpasupplicant -> ../../wpa_supplicant/ifupdown.sh root@www:/#  root@www:/# journalctl -b | grep -i “if-up\|local-routing” Dec 07 21:59:41 www.blandford.tech sudo[10334]: root : TTY=/dev/pts/1 ; PWD=/ ; USER=root ; COMMAND=/usr/bin/nano /etc/network/if-up.d/local-routing root@www:/#Show more

The script exists, is executable, but never ran during boot. The only log entry is from when you created it with nano.

This suggests your system isn’t using the traditional /etc/network/interfaces system. Modern Ubuntu often uses netplan or systemd-networkd instead, which don’t trigger /etc/network/if-up.d/ scripts.

Let’s use a systemd service instead – this will work regardless:

bash

# Create a systemd service
sudo nano /etc/systemd/system/fix-tailscale-routing.service

Add this content:

ini

[Unit]
Description=Fix Tailscale local subnet routing
After=network-online.target tailscaled.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/sbin/ip rule add from 10.10.0.0/24 to 10.10.0.0/24 lookup main priority 99
RemainAfterExit=yes
StandardOutput=journal

[Install]
WantedBy=multi-user.target

Save and exit, then:

bash

# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable fix-tailscale-routing.service
sudo systemctl start fix-tailscale-routing.service

# Verify it worked
sudo systemctl status fix-tailscale-routing.service
ip rule show

This will run after the network and Tailscale are up, ensuring the rule is always applied. Much more reliable than the if-up.d approach!