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>
58 lines
2.7 KiB
Bash
Executable File
58 lines
2.7 KiB
Bash
Executable File
#!/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
|