Docker + Tailscale Network Fix

Note: these scripts were written by an LLM (Claude Code), diagnosing and fixing an issue live on one specific machine (Fedora + NetworkManager + Tailscale + Docker). They worked there, but review them yourself before running as root on another machine — network/firewall behavior varies across distros, Tailscale versions, and configurations.

Fixes Docker containers (e.g. ddev) losing internet access when Tailscale is running with an exit node ("route all traffic") enabled.

The problem

With an exit node and "route all traffic" enabled, Tailscale installs a catch-all ip rule that captures every packet not explicitly exempted and routes it out via the tailscale0 interface. Tailscale exempts its own daemon traffic from that capture with a firewall mark, but has no way to exempt traffic that only passes through the host (forwarded) rather than originating from it.

Docker containers' outbound traffic is exactly that: forwarded, not locally-originated. It gets no exemption, so it's swept into Tailscale's catch-all route, sent out via tailscale0, and dropped by Tailscale's own anti-spoofing firewall rule (ts-forward) because it isn't a recognized Tailscale-tunneled flow.

Symptom: every DNS lookup / outbound HTTP request from inside a container hangs until timeout whenever Tailscale is connected — WP core update checks, plugin license pings, composer/npm installs, anything. The host's own applications aren't affected, since their traffic is locally-originated and gets Tailscale's exemption mark.

The fix

A higher-priority ip rule that routes Docker's private bridge subnet (default: 172.16.0.0/12, covering Docker's default address pool) through the normal routing table instead of Tailscale's catch-all. This doesn't weaken the VPN for anything else on the host — it only affects packets forwarded from Docker's own bridge networks.

The rule is kept in place by a systemd timer that re-checks/re-adds it every 15s (and 5s after boot), rather than hooked to a network event: tailscale0 is a tun device created directly by tailscaled, not through a NetworkManager connection profile, so NetworkManager never fires dispatcher/udev events we could reliably hook into for it (confirmed by testing — don't waste time re-trying that route).

Install

sudo ./install.sh

Optional overrides if Docker's default address pool doesn't apply on your machine (e.g. you have 15+ concurrent Docker networks and Docker has fallen back to a different private range):

sudo ./install.sh --subnet 172.16.0.0/12 --priority 5200 --table main

Verify

ip rule list | grep 172.16.0.0/12
ddev exec curl -sS -o /dev/null -w '%{time_total}\n' https://api.wordpress.org

Uninstall

sudo ./uninstall.sh

Caveats

  • Only covers Docker networks within 172.16.0.0/12. If you have more than ~15 concurrent Docker networks on one machine, Docker falls back to 192.168.0.0/20 blocks and then 10.0.0.0/8 — those aren't covered by default. Don't just widen the subnet to 192.168.0.0/16 or 10.0.0.0/8 to compensate: those ranges are also real LAN/VPN subnet-route space, and blanket-exempting them could route traffic around the tunnel that's actually supposed to go through it. If you hit this, either raise --subnet to the specific overflow range Docker actually assigned, or extend the script to enumerate live docker network inspect subnets instead of using a fixed range.
  • Assumes tailscaled's own ip rules stay in the 5210+ priority range observed on the machine this was built on; if a future Tailscale version changes that, priority 5200 should still safely sit above it as long as it's below whatever Tailscale uses for its catch-all rule.
Description
Small vibe-coded workaround for the docker containers not reaching the network while using Tailscale
Readme 32 KiB
Languages
Shell 100%