tunnel-doctor
Troubleshooting suite for resolving network layer conflicts on macOS when using VPN/proxy software.
Install
mkdir -p .claude/skills/tunnel-doctor && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/11357" && unzip -o skill.zip -d .claude/skills/tunnel-doctor && rm skill.zipInstalls to .claude/skills/tunnel-doctor
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Diagnoses and fixes conflicts between Tailscale and proxy/VPN tools (Shadowrocket, Clash, Surge) on macOS. Covers six conflict layers - (1) route hijacking, (2) HTTP proxy env var interception, (3) system proxy bypass, (4) SSH ProxyCommand double tunneling, (5) VM/container runtime proxy propagation (OrbStack/Docker), and (6) stalled DNS resolver in macOS getaddrinfo chain (dead VPN daemon leaving zombie utun + DNS injection). Includes SOP for remote development via SSH tunnels with proxy-safe Makefile patterns. Use when Tailscale ping works but SSH/HTTP times out, when browser returns 503 but curl works, when git push fails with "failed to begin relaying via HTTP", when Docker pull times out behind TUN/VPN, when setting up Tailscale SSH to WSL instances, when bootstrapping remote dev environments over Tailscale, when ssh/curl/git hang ~60 seconds before resolving a hostname while nslookup returns instantly, when ping to a resolver IP works but dig to the same IP times out, or when ssh -vvv freezes at "debug2: resolving" without ever reaching "debug1: connect".Key capabilities
- →Diagnose routing conflicts
- →Fix proxy/VPN conflicts
- →Troubleshoot SSH tunnels
- →Resolve DNS issues
How it works
It identifies conflicts across six layers, including route hijacking and proxy environment variable leaks.
Inputs & outputs
When to use tunnel-doctor
- →Fixing Tailscale network issues
- →Troubleshooting SSH connectivity
- →Resolving proxy/VPN conflicts
About this skill
Tunnel Doctor
Diagnose and fix conflicts when Tailscale coexists with proxy/VPN tools on macOS, with specific guidance for SSH access to WSL instances.
Five Conflict Layers
Proxy/VPN tools on macOS create conflicts at five independent layers. Layers 1-3 affect Tailscale connectivity; Layer 4 affects SSH git operations; Layer 5 affects VM/container runtimes:
| 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
curland SSH both work → System proxy bypass conflict (Step 2C) local.<domain>fails in browser/defaultcurl, 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/pullfails withFATAL: failed to begin relaying via HTTP→ SSH double tunnel (Step 2F)docker buildRUN apk/aptfails withConnection refusedinstantly → OrbStack transparent proxy + TUN conflict (Step 2G-1, fix:--network host)docker pullfails withTLS handshake timeout→ VM proxy misconfiguration (Step 2G-2, fix:docker.jsonwithhost.internal)- Container healthcheck
(unhealthy)but app runs fine → Lowercase proxy env var leak (Step 2G-4, fix: clearhttp_proxy+HTTP_PROXY) docker buildcan't fetch base images → VM/container proxy propagation (Step 2G)git clonefails withConnection closed by 198.18.x.x→ TUN DNS hijack for SSH (Step 2H)- SSH connects but
operation not permitted→ Tailscale SSH config issue (Step 4) - SSH connects but
be-child sshexits code 1 → WSL snap sandbox issue (Step 5) - TCP port 22 reachable (
nc -zsucceeds) but SSH fails withkex_exchange_identification: Connection closed→ Tailscale SSH proxy intercept on WSL (Step 5A) tailscale sshreturns "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, butnslookupreturns instantly → Stalled resolver ingetaddrinfochain (Step 2I)
Key distinctions:
- SSH does NOT use
http_proxy/NO_PROXYenv vars. If SSH works but HTTP doesn't → Layer 2. curluseshttp_proxyenv var, NOT the system proxy. Browser uses system proxy (set by VPN). Ifcurlworks but browser doesn't → Layer 3.- If
tailscale pingworks but regularpingdoesn't → Layer 1 (route table corrupted). - If
ssh -T [email protected]works butgit pushfails intermittently → Layer 4 (double tunnel). - If host
curl https://...works butdocker pulltimes out → Layer 5 (VM proxy propagation). - If
docker pullworks butdocker buildRUN apk addfails instantly withConnection refused→ OrbStack transparent proxy broken by TUN (Step 2G-1). - If container healthcheck shows
(unhealthy)but app works → lowercasehttp_proxyleaked into container (Step 2G-4). - If DNS resolves to
198.18.x.xvirtual IPs → TUN DNS hijack (Step 2H). - If
nc -zsucceeds on port 22 but SSH gets no banner (kex_exchange_identification) → Tailscale SSH proxy intercept (Step 5A). Confirm withtcpdump -i any port 22on the remote — 0 packets means Tailscale intercepts above the kernel. - If
tailscale sshfails with "not available on App Store builds" → install Standalone Tailscale (Step 5B). - If
nslookup <host>is fast (<0.1s) butdscacheutil -q host -a name <host>takes 60s+ → a supplemental resolver inscutil --dnsis dead (Step 2I). - If
ping <resolver-ip>succeeds butdig @<resolver-ip>times out → daemon dead,utuninterface zombied. ICMP is answered by the interface; the actual port-53 service is gone (Step 2I). - If
ssh -vvvhangs immediately afterdebug2: resolving "<host>" port <port>and never reachesdebug1: connect to address→ DNS resolution stage, not network connect stage. This is Step 2I, not Step 2B/2H.
Diagnosis Discipline (Read Before Committing to a Hypothesis)
When symptoms point at a component (proxy, VPN, route table, DNS), don't commit to a hypothesis from circumstantial evidence — verify with that component's own health endpoint first. Each component has a one-line health check faster and more reliable than ruling out neighbors:
| Suspected component | Authoritative health check (run this first) |
|---|---|
| HTTP proxy (Shadowrocket / Clash / Surge) | curl -x http://127.0.0.1:<port> -m 10 https://api.github.com returns 200 |
| Tailscale daemon | tailscale status returns peer list (not connection error) |
| A specific DNS resolver | dig @<nameserver-ip> +tries=1 +timeout=3 example.com <100ms |
| Routing for an IP | route -n get <ip> shows expected interface |
| Per-resolver bisection (when DNS is suspect) | The for ns in ...; do dig @$ns ... loop in Step 2I |
Why this matters: A symptom that matches the description of Step 2X does not, by itself, prove component X is the problem. Multiple layers can produce overlapping symptoms (a 60-second hang during git push could be proxy node death, fakeip route corruption, or DNS resolver stall — all plausible from the user-visible symptom alone). Reaching for the most specific verification first avoids committing to a wrong layer and chasing it down a dead end.
If the failing operation involves DNS at all, run the per-nameserver bisection from Step 2I before suspecting proxy or routing. It rules in/out the largest single class of macOS-on-China-network failures in under 15 seconds.
Fast Path: Run Automated Checks
For common macOS conflicts (env proxy, system proxy exceptions, direct/proxy path split, local TLS trust), run:
python3 scripts/quick_diagnose.py --host local.claude4.dev --url https://local.claude4.dev/health
Optional route ownership check for a Tailscale destination:
python3 scripts/quick_diagnose.py --host <target-host> --url http://<target-host>:<port>/health --tailscale-ip <100.x.x.x>
Interpretation:
direct=PASS+forced_proxy=FAIL= host must bypass proxy (skip-proxy+NO_PROXY).strict_tls=FAIL+direct=PASS= path is reachable; trust issue only (install/trust local CA).host in scutil exceptions: no= browser/system clients still likely proxied.
Step 2A: Fix HTTP Proxy Environment Variables
Check if proxy env vars are intercepting Tailscale HTTP traffic:
env | grep -i proxy
Broken output — proxy is set but NO_PROXY doesn't exclude Tailscale:
http_proxy=http://127.0.0.1:1082
https_proxy=http://127.0.0.1:1082
NO_PROXY=localhost,127.0.0.1 ← Missing Tailscale!
Fix — add Tailscale MagicDNS domain + CIDR to NO_PROXY:
export NO_PROXY=localhost,127.0.0.1,.ts.net,100.64.0.0/10,192.168.*,10.*,172.16.*
| Entry | Covers | Why |
|---|---|---|
.ts.net | MagicDNS domains (host.tailnet.ts.net) | Matched before DNS resolution |
100.64.0.0/10 | Tailscale IPs (100.64.* – 100.127.*) | Precise CIDR, no public IP false positives |
192.168.*,10.*,172.16.* | RFC 1918 private networks | LAN should never be proxied |
Two layers complement each other: .ts.net handles domain-based access, 100.64.0.0/10 handles direct IP access.
NO_PROXY syntax pitfalls — see references/proxy_conflict_reference.md for the compatibility matrix.
Go net/http CIDR caveat: Go's standard net/http does NOT support CIDR notation in NO_PROXY. Setting NO_PROXY=100.64.0.0/10 works for curl and Python, but Go programs (including Tailscale-adjacent tooling) will still send traffic through the proxy. The fix is to use MagicDNS hostnames (e.g., workstation-4090-wsl) instead of raw IPs, or add explicit hostnames to NO_PROXY:
# WRONG for Go programs — CIDR is silently ignored
NO_PROXY=100.64.0.0/10 go-program http://100.101.102.103:8002/health # → goes through proxy
# CORRECT — use hostname (matched as suffix) or explicit IP
export NO_PROXY=localhost,127.0.0.1,.ts.net,workstation-4090-wsl,100.101.102.103,192.168.*,10.*,172.16.*
This is especially relevant when accessing Tailscale services from Go-based tools (e.g., custom CLIs, Go test suites hitting remote APIs).
Verify the fix:
# Both must return HTTP 200:
NO_PROXY="...(new value)..." curl -s --connect-timeout 5 http://<host>.ts.net:<port>/health -w "HTTP %{http_code}\n"
NO_PROXY="...(new value)..." curl -s --connect-timeout 5 http://<tailscale-ip>:<port>/health -w "HTTP %{http_code}\n"
Then persist in shell config (~/.zshrc or ~/.bashrc).
S
Content truncated.
When not to use it
- →Non-macOS environments
- →Simple network connectivity issues
Prerequisites
Limitations
- →macOS specific
- →Requires understanding of proxy layers
How it compares
It provides a systematic diagnostic workflow for complex network conflicts specific to macOS.
Compared to similar skills
tunnel-doctor side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| tunnel-doctor (this skill) | 0 | 2mo | Caution | Advanced |
| incident-response-incident-response | 4 | 4mo | No flags | Advanced |
| checking-infrastructure-compliance | 0 | 27d | Review | Intermediate |
| grafana-dashboards | 134 | 5mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
incident-response-incident-response
sickn33
Use when working with incident response incident response
checking-infrastructure-compliance
jeremylongshore
Execute use when you need to work with compliance checking. This skill provides compliance monitoring and validation with comprehensive guidance and automation. Trigger with phrases like "check compliance", "validate policies", or "audit compliance".
grafana-dashboards
wshobson
Create and manage production Grafana dashboards for real-time visualization of system and application metrics. Use when building monitoring dashboards, visualizing metrics, or creating operational observability interfaces.
cisco-network-diagram
neuro-synapse
Generate Draw.io network topology diagrams from Cisco router and switch configurations. Use when asked to visualize network topology, create network diagrams, map network infrastructure, or generate visual representations from Cisco device configs. Supports parsing show commands, configuration files, CDP/LLDP neighbor data, and routing protocol information to automatically create professional network diagrams.
home-assistant-manager
komal-SkyNET
Expert-level Home Assistant configuration management with efficient deployment workflows (git and rapid scp iteration), remote CLI access via SSH and hass-cli, automation verification protocols, log analysis, reload vs restart optimization, and comprehensive Lovelace dashboard management for tablet-optimized UIs. Includes template patterns, card types, debugging strategies, and real-world examples.
postmortem-writing
wshobson
Write effective blameless postmortems with root cause analysis, timelines, and action items. Use when conducting incident reviews, writing postmortem documents, or improving incident response processes.