Running a transparent sing-box (Momo) proxy on OpenWrt solves the headache of configuring proxies on every device, but it can break inbound Tailscale SSH in a way that looks completely random:
- Cold inbound
sshto a home node hangs or times out. - If the home node runs
tailscale ping <peer>first, inbound SSH suddenly works. - After the SSH session sits idle for a few minutes, the terminal freezes—even though both nodes still show as online in Tailscale.
TL;DR
- Root cause: The transparent proxy intercepts Tailscale’s STUN and DERP relay traffic and sends it through a proxy outbound (
outbound/socks) that drops UDP or kills long-lived DERP connections withEOF. Inbound SSH only works while a temporary direct NAT hole stays warm. - Why DNS rules aren’t enough:
tailscaledfetches relay endpoints from its DERP map and connects to literal IP addresses, so domain rules (*.tailscale.com) never match DERP relay traffic. - Emergency recovery: Run
tailscale ping <CLOUD_NODE_TAILSCALE_IP>from the home node to punch a direct NAT path, then keep it warm with/usr/bin/ping -n -i 15 <CLOUD_NODE_TAILSCALE_IP>while you fix the router. - Permanent fix: In
sing-box, route Tailscale UDP (source_port: 41641), STUN (udp/3478), and literal DERP IPs (tcp/80,443via a local rule-set) todirectbefore Fake-IP, GeoIP, and proxy rules, while sending.tailscale.com/.tailscale.iocontrol-plane domains through a stable proxy outbound.
Why the failure feels intermittent
Tailscale prefers a direct peer-to-peer UDP path and falls back to DERP relays (or a configured Peer Relay) when NAT traversal cannot establish one.
In this incident, the failure followed a predictable loop:
- A cloud node attempted a cold inbound SSH connection to the home node.
- The home router had no active NAT mapping for a direct peer path, so Tailscale fell back to DERP.
- Momo routed the DERP connection into
outbound/socks, which failed immediately withEOF. - Running a one-off
tailscale pingfrom the home node to the cloud node punched an outbound UDP NAT state, bringing up a direct peer path. - SSH worked normally over that direct path—until the session went idle, the NAT mapping expired, and Tailscale had no working DERP fallback to catch the connection.
Because SSH works the moment a direct path exists and freezes silently instead of closing cleanly when NAT expires, sshd is not the culprit; the missing relay fallback is.
Separate control plane from transport plane
When OpenWrt runs a transparent proxy in mainland China, Tailscale traffic splits into two classes with opposite routing needs:
- Control plane (
.tailscale.com,.tailscale.io): handles authentication, coordination, key exchange, and policy updates. These HTTPS requests often need a stable proxy outbound to stay reachable. - Transport plane (peer UDP, STUN, and DERP relays): carries WireGuard packets, NAT discovery, and relay streams. This traffic must bypass the proxy and go
direct.
A whole-device DIRECT rule breaks control-plane reachability if direct access to Tailscale coordination servers is blocked; proxying everything breaks UDP and long-lived DERP streams. You need a split policy.
Diagnose the path
1. Check the Tailscale daemon on the home node
tailscale status
tailscale netcheck
systemctl status tailscaled
Look for a healthy, authenticated daemon whose netcheck or health output reports DERP/relay failures. Avoid restarting tailscaled over an active remote session—it drops your shell without fixing the router policy.
2. Compare cold inbound vs. warm NAT
From the cloud node, test SSH before the home node sends any traffic:
ssh <USER>@<HOME_NODE_TAILSCALE_IP>
Then punch an outbound packet from the home node:
tailscale ping <CLOUD_NODE_TAILSCALE_IP>
If SSH succeeds immediately after tailscale ping, outbound traffic is warming a direct NAT path while inbound relay fallback is broken.
3. Correlate tailscaled and Momo logs
On the home node:
journalctl -u tailscaled --since "30 minutes ago"
On OpenWrt:
logread -e momo | tail -n 120
Look for tailscaled DERP EOF errors paired with Momo routing literal DERP IPs into the proxy:
tailscaled: DERP connection -> EOF
Momo: <DERP_IP>:3478 -> outbound/socks
Momo: <DERP_IP>:80 -> outbound/socks
Momo: <DERP_IP>:443 -> outbound/socks
Why a DNS-only fix fails
An initial fix placed Tailscale domain rules ahead of Fake-IP and GeoIP rules, giving DERP hostnames real DNS resolution and a direct route. sing-box check passed, yet DERP connections still hit outbound/socks.
The router logs showed why: tailscaled downloads the DERP map from the control plane and dials relay servers by literal IPv4/IPv6 address alongside or instead of hostname SNI. Because those connections carry no domain name for Momo’s domain rules to match, they fall through to the default proxy outbound. Matching DERP traffic requires transport and IP rules, not just DNS rules.
Build the durable Momo / sing-box policy
Place the Tailscale transport rules immediately after protocol sniffing—ahead of source bypass, Fake-IP, GeoIP, private-CIDR, and selector rules:
{
"route": {
"rule_set": [
{
"type": "local",
"tag": "tailscale-derp",
"format": "source",
"path": "/etc/momo/ruleset/tailscale-derp.json"
}
],
"rules": [
{
"network": "udp",
"source_port": 41641,
"action": "route",
"outbound": "direct"
},
{
"network": "udp",
"port": 3478,
"action": "route",
"outbound": "direct"
},
{
"network": "tcp",
"port": [80, 443],
"rule_set": ["tailscale-derp"],
"action": "route",
"outbound": "direct"
},
{
"ip_cidr": [
"100.64.0.0/10",
"fd7a:115c:a1e0::/48",
"<CLOUD_NODE_PUBLIC_IP>/32"
],
"action": "route",
"outbound": "direct"
},
{
"domain_suffix": [".tailscale.com", ".tailscale.io"],
"action": "route",
"outbound": "<STABLE_PROXY_OUTBOUND>"
}
]
}
}
Confirm your node’s actual tailscaled UDP port before hard-coding 41641 (see Tailscale firewall ports). If your setup also matches DERP hostnames (derp*.tailscale.com), put that narrower direct rule right above the .tailscale.com control-plane rule.
Keep the DERP IP rule-set updated
Tailscale rotates DERP relay IPs over time, so don’t rely on a static hand-copied list. Pull the official DERP map from:
curl -fsS https://login.tailscale.com/derpmap/default | jq .
Run a periodic job on the router that:
- Downloads
https://login.tailscale.com/derpmap/defaultto a temporary file and validates the JSON. - Extracts all
IPv4andIPv6addresses into a temporarysing-boxsource rule-set (rule-set format). - Validates the full config with
sing-box check -c <MOMO_CONFIG>. - Atomically replaces
/etc/momo/ruleset/tailscale-derp.jsononly if validation succeeds, keeping the previous known-good file on failure.
Apply and verify with a cold-inbound test
Before reloading Momo over SSH, keep a second session open and schedule a timed rollback in case routing drops.
sing-box check -c <MOMO_CONFIG>
/etc/init.d/momo reload
/etc/init.d/momo status
logread -e momo | tail -n 120
Verify in logread that all four flows hit their intended outbounds:
UDP source 41641 -> outbound/direct
UDP destination 3478 -> outbound/direct
DERP TCP 80 / 443 -> outbound/direct
Tailscale control plane -> <STABLE_PROXY_OUTBOUND>
Once the logs look right, prove the fix from a cold state (a warm NAT test proves nothing):
- Stop any temporary
pingkeepalive between the home node and cloud node. - Wait longer than the NAT idle timeout without running
tailscale ping. - SSH from the cloud node to
<HOME_NODE_TAILSCALE_IP>and confirm it connects on the first try. - Leave the SSH session idle past the old freeze window and verify the terminal stays responsive.
- Reboot the home node during a maintenance window and repeat the cold-inbound SSH test.
Optional: add a self-hosted Peer Relay
If public DERP latency is still poor, a cloud node with a public IP running Tailscale 1.86+ can act as a private Tailscale Peer Relay:
sudo tailscale set \
--relay-server-port=<PEER_RELAY_UDP_PORT> \
--relay-server-static-endpoints="<CLOUD_NODE_PUBLIC_IP>:<PEER_RELAY_UDP_PORT>"
Open <PEER_RELAY_UDP_PORT>/udp in the cloud firewall, include <CLOUD_NODE_PUBLIC_IP>/32 in Momo’s direct CIDR list, and grant tailscale.com/cap/relay to the nodes that need it.