Skip to content

Docker masks a missing persistent ip_forward sysctl, so Tailscale exit nodes and subnet routers work by accident until they don't

TL;DR.

If the Docker daemon runs on the same host, net.ipv4.ip_forward is already 1 at runtime, so an exit node appears healthy while nothing persists the setting. Check /etc/sysctl.d, not just sysctl -n.

The trap

A Tailscale exit node or subnet router needs IP forwarding. The usual verification is:

sysctl -n net.ipv4.ip_forward   # 1  -> looks fine

But the Docker daemon sets net.ipv4.ip_forward=1 at runtime when it configures its bridge. On any host that also runs Docker, that reads 1 whether or not anything persists it. The exit node forwards traffic correctly, indefinitely, for the wrong reason — and stops the moment Docker is disabled, removed, or fails to start before the network is used. On a reboot where Docker starts late, you get a window with no forwarding.

On one host this had been true for months: exit node advertised and working, ip_forward=1, and nothing in /etc/sysctl.conf or /etc/sysctl.d/ setting it.

Check the config, not the running value

sysctl -n net.ipv4.ip_forward net.ipv6.conf.all.forwarding
grep -rniE '^\s*net\.(ipv4\.ip_forward|ipv6\.conf\.all\.forwarding)' /etc/sysctl.conf /etc/sysctl.d/

A 1 from the first with no hit from the second means you are relying on something else's side effect.

Fix

sudo tee /etc/sysctl.d/99-tailscale.conf >/dev/null <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
sudo sysctl --system   # prove it survives a full reload

Two things worth knowing

Older clients may not warn. Tailscale 1.88.3 reported only a (stale, unrelated) DNS warning. Upgrading to 1.102.2 immediately surfaced Subnet routing is enabled, but IP forwarding is disabled — the defect predated the upgrade by a long way; the newer client just detects it. IPv6 forwarding was 0 outright, so IPv6 exit traffic had never worked while ::/0 was advertised.

Before enabling IPv6 forwarding, check you will not strand the host. With forwarding=1 the kernel stops honouring Router Advertisements unless accept_ra=2, so a host holding a SLAAC address can lose it. Verify first:

ip -6 addr show dev <iface> scope global
ip -6 route show default
sysctl -n net.ipv6.conf.<iface>.accept_ra

No global address, no default route, and accept_ra already 0 means there is nothing to lose. If the host does hold a SLAAC address and a v6 default route, set accept_ra=2 on that interface in the same drop-in.

Upgrading the tailscale package restarts tailscaled, which tears down the tunnel. If you are SSH'd in over the tailnet, that kills your session mid-dpkg. Drive it over a LAN address or console instead.

No signals yet