TU

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.zip

Installs 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".
1077 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Advanced

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

You give it
Network symptom
You get back
Network configuration fix

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:

LayerWhat breaksWhat still worksRoot cause
1. Route tableEverything (SSH, curl, browser)tailscale pingtun-excluded-routes adds en0 route overriding Tailscale utun
2. HTTP env varscurl, Python requests, Node.js fetchSSH, browserhttp_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 tunnelgit 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 propagationdocker pull, docker buildHost curl, running containersVM 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)
  • git clone fails with Connection 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 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)
  • 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)

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 [email protected] 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 → TUN DNS hijack (Step 2H).
  • 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 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 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 componentAuthoritative 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 daemontailscale status returns peer list (not connection error)
A specific DNS resolverdig @<nameserver-ip> +tries=1 +timeout=3 example.com <100ms
Routing for an IProute -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.*
EntryCoversWhy
.ts.netMagicDNS domains (host.tailnet.ts.net)Matched before DNS resolution
100.64.0.0/10Tailscale IPs (100.64.*100.127.*)Precise CIDR, no public IP false positives
192.168.*,10.*,172.16.*RFC 1918 private networksLAN 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

TailscaleProxy/VPN tool

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.

SkillInstallsUpdatedSafetyDifficulty
tunnel-doctor (this skill)02moCautionAdvanced
incident-response-incident-response44moNo flagsAdvanced
checking-infrastructure-compliance027dReviewIntermediate
grafana-dashboards1345moNo flagsIntermediate

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

420

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".

02

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.

134441

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.

8155

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.

9110

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.

1799

Search skills

Search the agent skills registry