diff --git a/README.md b/README.md index e3ea529..9ae05b3 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,37 @@ -# ddev-tailscale-docker-fix +# 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. +> **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. +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) +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. +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. +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. +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). +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 @@ -51,9 +39,8 @@ fires dispatcher/udev events we could reliably hook into for it 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): +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 @@ -74,17 +61,12 @@ 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. +- 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.