Add docker-tailscale-route: fix Docker losing internet under Tailscale exit-node
Tailscale's exit-node "route all traffic" mode captures Docker's forwarded container traffic into its tunnel and drops it as an unrecognized flow, causing every outbound request from inside a container to hang until timeout. This installs a systemd timer that keeps an ip rule in place routing Docker's bridge subnet around Tailscale's catch-all route instead. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
57
docker-tailscale-route.sh
Executable file
57
docker-tailscale-route.sh
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user