#!/bin/bash # # Written by an LLM (Claude Code) — review before trusting on a machine # other than the one this was diagnosed on. See README.md. # # Ensures Docker's own bridge traffic bypasses Tailscale's exit-node # catch-all route. # # Background: a machine running Tailscale with an exit node and "route # all traffic" enabled installs a catch-all `ip rule` that captures # every packet not explicitly exempted and sends it out via the # tailscale0 interface. Tailscale exempts its own daemon traffic from # that capture with a firewall mark, but it has no way to exempt # traffic that only passes THROUGH the host (forwarded) rather than # originating from it. # # Docker containers' outbound traffic (dev environments, package # managers, anything containers do that talks to the internet) is # exactly that: forwarded traffic, not locally-originated. With no # exemption, it gets swept into Tailscale's catch-all route, sent out # via tailscale0, and dropped by Tailscale's own anti-spoofing firewall # rule 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. # # The fix: keep a higher-priority `ip rule` in place that routes # Docker's private bridge subnet through the normal routing table (the # real gateway/NIC) instead of Tailscale's catch-all. This does NOT # weaken or bypass the VPN for anything else on the host — it only # affects packets forwarded from Docker's own bridge networks, which # were never meant to be tunneled through the exit node in the first # place. # # Defaults assume Docker's default address-pool behavior (bridge # networks allocated from 172.17.0.0/16 through 172.31.0.0/16, all # within 172.16.0.0/12). If you have more than ~15 concurrent Docker # networks on a machine, Docker falls back to other private ranges # (192.168.0.0/20 blocks, then 10.0.0.0/8) that this default won't # cover — override DOCKER_SUBNET in /etc/default/docker-tailscale-route # if that applies to you. Widening the range casually isn't advisable: # 192.168.0.0/16 and 10.0.0.0/8 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. set -euo pipefail DOCKER_SUBNET="172.16.0.0/12" RULE_PRIORITY="5200" TARGET_TABLE="main" # Optional overrides, e.g. if Docker's address pool differs on this # machine or the priority collides with something else. [ -f /etc/default/docker-tailscale-route ] && . /etc/default/docker-tailscale-route if ! ip rule list | grep -q "from ${DOCKER_SUBNET} lookup ${TARGET_TABLE}"; then ip rule add from "$DOCKER_SUBNET" lookup "$TARGET_TABLE" priority "$RULE_PRIORITY" fi