tunnel-doctor

Diagnoses concrete tunnel and proxy-path failures across macOS and Windows/WSL: Tailscale routing, proxy env/system bypass, SSH double tunneling, VM/container propagation, stalled DNS, TUN DIRECT split-brain, Windows-host TUN cascades, and single-hop or chained proxy node/exit throughput. Use when T

By daymade · 571 installs

npx skills add daymade/claude-code-skills --skill tunnel-doctor

Source repository · Upstream listing

Tunnel Doctor Diagnose and fix conflicts when Tailscale coexists with proxy/VPN tools on macOS, with specific guidance for SSH access to WSL instances. Methodology base: the general diagnostic discipline this skill builds on — evidence over assumption, falsification over confirmation, layered isolation, counter review — lives in the debugging network issues skill. This skill is the macOS Tailscale⨯proxy domain layer on top of it; reach for the base skill when the symptom is not a known Tailscale/proxy conflict. Ownership boundary: this skill owns concrete local network path operations: Tailscale, route/DNS/proxy interception, VM/WSL propagation, TUN forwarding planes, and proxy node/exit/chain capacity. Use debugging network issues as the general method when the boundary is still unknown or the symptom belongs to SSE/CDN/application protocol behavior. This keeps one concrete operator instead of forcing the user to choose between two overlapping proxy doctors. Target and task scope Before remote probes or changes, establish whether the target is a designated test machine, an explicitly requested colleague's working computer, or unknown. Use the owner's existing machine purpose ledger and current task; do not log into a colleague's machine to classify it. Saved SSH access and previous successful tests do not grant ongoing use. Use designated test machines for routine testing; their unavailability does not authorize another person's computer. Keep an explicitly requested one off test on a colleague's computer within its stated result and necessary verification. Stop there on completion, cancellation, or a need for an unapproved environment change. Move later performance, migration, and regression tests to designated test machines. A test failure does not authorize disabling that person's proxy/VPN, quitting apps, changing routes, or installing a background task. Proceed with disruptive repair there only when the user has explicitly requested that repair and its interruption. Existing authorization for owner test machine maintenance remains valid; do not ask again for its routine steps. Apply this boundary even when invoked directly, through another Skill, after compaction, or through a peer handoff. After access is stopped, use existing records rather than another SSH probe to verify that it stopped. Disruptive network changes Before any authorized VPN disconnect, app termination, route cutover, or service restart, read [network change recovery.md](references/network change recovery.md). Validate prerequisites before interruption, arm bounded recovery before the first disruptive action, and verify both the repaired path and the user's original network use. A successful stop command, a detached process, or a restart placed at the end of a script is not recovery evidence. Conflict Layers Proxy/VPN tools on macOS create conflicts at several independent layers. Layers 1 3 affect Tailscale connectivity; Layer 4 affects SSH git operations; Layer 5 affects VM/container runtimes. TUN state failure modes beyond this table — SSH/git connection drops, resolver stall, DIRECT split brain — are covered in Steps 2H–2J: Layer What breaks What still works Root cause 1. Route table Everything (SSH, curl, browser) tailscale ping tun excluded routes adds en0 route overriding Tailscale utun 2. HTTP env vars curl , Python requests, Node.js fetch SSH, browser http proxy set without NO PROXY for Tailscale 3. System proxy (browser) Browser only (HTTP 503) SSH, curl (both with/without proxy) Browser uses VPN system proxy; DIRECT rule routes via Wi Fi, not Tailscale utun 4. SSH ProxyCommand double tunnel git push/pull (intermittent) ssh T (small data) connect H creates HTTP CONNECT tunnel redundant with Shadowrocket TUN; landing proxy drops large/long lived transfers 5. VM/Container proxy propagation docker pull , docker build Host curl , running containers VM runtime (OrbStack/Docker Desktop) auto injects or caches proxy config; removing proxy makes it worse (VM traffic via TUN → TLS timeout) Diagnostic Workflow Step 1: Identify the Symptom Determine which scenario applies: Browser returns HTTP 503, but curl and SSH both work → System proxy bypass conflict (Step 2C) local.<domain fails in browser/default curl , but direct/no proxy request works → Local vanity domain proxy interception (Step 2C 1) Tailscale ping works, SSH works, but curl/HTTP times out → HTTP proxy env var conflict (Step 2A) Tailscale ping works, SSH/TCP times out → Route conflict (Step 2B) Remote dev server auth redirects to localhost → browser can't follow → SSH tunnel needed (Step 2D) make status / scripts curl to localhost fail with proxy → localhost proxy interception (Step 2E) git push/pull fails with FATAL: failed to begin relaying via HTTP → SSH double tunnel (Step 2F) docker build RUN apk/apt fails with Connection refused instantly → OrbStack transparent proxy + TUN conflict (Step 2G 1, fix: network host ) docker pull fails with TLS handshake timeout → VM proxy misconfiguration (Step 2G 2, fix: docker.json with host.internal ) Container healthcheck (unhealthy) but app runs fine → Lowercase proxy env var leak (Step 2G 4, fix: clear http proxy + HTTP PROXY ) docker build can't fetch base images → VM/container proxy propagation (Step 2G) docker pull works but docker build still dies fetching base image tokens (TLS/i o timeout) on a WSL2 + Docker Desktop host, or every runbook proxy port on a Windows+v2rayN host silently died after a v2rayN upgrade → buildkit direct dials past the DD proxy override / v2rayN ≥7.17 LAN port split (references/windows host tun wsl cascade.md §2026 08 23) git clone fails with Connection closed by 198.18.x.x → two different mechanisms produce this; Step 2H separates them Every domestic/DIRECT rule site fails at once (TLS unexpected EOF mid handshake, proxy port CONNECT returns 503, Node CLIs report UNKNOWN CERTIFICATE VERIFICATION ERROR ) while proxied overseas sites keep working → TUN DIRECT split brain (Step 2J) Everything on macOS fails at once — domestic AND overseas, and even Step 2J's real IP probe fails → do not declare an outage yet; run the physical interface discriminator in "TUN full stall vs genuine outage" (above Step 2A) SSH connects but operation not permitted → Tailscale SSH config issue (Step 4) SSH connects but be child ssh exits code 1 → WSL snap sandbox issue (Step 5) TCP port 22 reachable ( nc z succeeds) but SSH fails with kex exchange identification: Connection closed → Tailscale SSH proxy intercept on WSL (Step 5A) Same kex exchange identification / Connection closed by UNKNOWN port 65535 , but there is no Tailscale — you are behind a TUN and the host is a public cloud VM whose HTTPS still works → could be your TUN or the destination's firewall allowlist; they are indistinguishable from the client. Step 2K settles it out of band tailscale ssh returns "not available on App Store builds" → Wrong Tailscale distribution on macOS (Step 5B) Any tool using system DNS ( ssh , curl , git ) hangs ~60s before resolving, but nslookup returns instantly → Stalled resolver in getaddrinfo chain (Step 2I) Windows+WSL host: everything offline at once (domestic AND overseas), WSL dead too, "even Tailscale won't come online" — and/or the user tried several recovery actions (switched NICs, toggled TUN) and can't tell which one fixed it → Windows host TUN cascade + event log forensics (Step 5C) Tailscale, DNS, route ownership, and proxy reachability all pass, but large transfers remain slow and switching the active proxy node changes throughput → proxy node / exit / chain capacity workflow in [references/proxy node chain throughput.md](references/proxy node chain throughput.md) You want one app's bulk traffic (BaiduNetdisk downloads, Feishu/Lark uploads) off the metered proxy — or out of the status bar counter entirely → not a conflict; a splitting task. On this Mac the shadowrocket splitting skill owns the whole workflow (target list, replay, CIDR capture, wipe guard); the mechanism background is [references/proxy conflict reference.md](references/proxy conflict reference.md) § "Per app traffic splitting on macOS" — which of the three mechanisms (DIRECT rule / tun excluded routes / always real ip ) can work for the app depends on whether it connects to literal IPs or the system resolver, and the DNS layer keys are inert on the macOS Catalyst build. Key distinctions : SSH does NOT use http proxy / NO PROXY env vars. If SSH works but HTTP doesn't → Layer 2. curl uses http proxy env var, NOT the system proxy. Browser uses system proxy (set by VPN). If curl works but browser doesn't → Layer 3. If tailscale ping works but regular ping doesn't → Layer 1 (route table corrupted). If ssh T git@github.com works but git push fails intermittently → Layer 4 (double tunnel). If host curl https://... works but docker pull times out → Layer 5 (VM proxy propagation). If docker pull works but docker build RUN apk add fails instantly with Connection refused → OrbStack transparent proxy broken by TUN (Step 2G 1). If container healthcheck shows (unhealthy) but app works → lowercase http proxy leaked into container (Step 2G 4). If DNS resolves to 198.18.x.x virtual IPs → a TUN is active, which is not by itself a diagnosis: under TUN every hostname resolves this way, on success as well as failure. Go to Step 2H to find out which mechanism you have. If connections to fake IP resolved domains die but the same host works via curl resolve <host :443:<real ip (real IP from dig @<public resolver ) → the TUN tool's DIRECT forwarding state is broken, not your network and not the destination (Step 2J). If nc z succeeds on port 22 but SSH gets no banner ( kex exchange identification ) → Tailscale SSH proxy intercept (Step 5A). Confirm with tcpdump i any port 22 on the remote — 0 packets means Tailscale intercepts above the kernel. If nc z also succeeds on a port the destination does not serve (probe 65001) → under a TUN the local stack answers for the destination, so no nc z result on that host means anything . Never read a port probe you have not calibrated this way. This supersedes the plain nc z reading in the bullet above whenever a TUN — not just Tailscale — is in the path (Step 2K). If the destination's own journalctl u ssh shows zero entries for your attempts → something before sshd is eating them, but not necessarily the network : a host local packet filter drops them just as silently. tcpdump on the host separates "never arrived" from "arrived and was dropped locally"; only the first implicates your TUN. Reach the machine through the cloud provider's guest agent channel to ask, since that path does not use the port you cannot reach. Check the journal, not auth.log : on Ubuntu 24.04 sshd writes nothing to auth.log , so an empty file there is not evidence (Step 2K). A cloud host enforces an inbound port twice — provider security group and host firewall ( ufw / firewalld / nftables ). Adding your IP to one leaves the other blocking, with no signal that you only did half the job (Step 2K). If tailscale ssh fails with "not available on App Store builds" → install Standalone Tailscale (Step 5B). If nslookup <host is fast (<0.1s) but dscacheutil q host a name <host takes 60s+ → a supplemental resolver in scutil dns is dead (Step 2I). If ping <resolver ip succeeds but dig @<resolver ip times out → daemon dead, utun interface zombied. ICMP is answered by the interface; the actual port 53 service is gone (Step 2I). If ssh vvv hangs immediately after debug2: resolving "<host " port <port and never reaches debug1: connect