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 Tailscale ping works but SSH/HTTP fails, browser returns 503 while curl works, Git SSH closes through the proxy, Docker pull/build fails behind TUN, getaddrinfo stalls while nslookup is fast, raw probes report physically impossible results, domestic DIRECT-routed sites fail while proxied sites work, a blocked port could be your tunnel or the destination's firewall, or the proxy is reachable but real downloads and full Git clones crawl. Also use when Git reports "failed to begin relaying via HTTP", ssh -vvv freezes at "debug2: resolving", ping works but dig times out, or setting up Tailscale SSH to WSL. Use debugging-network-issues only when the root cause remains unknown or belongs to an application/protocol layer.
1实现原理 · 为什么它能做到
它把「Tailscale 与代理/VPN 共存」的冲突按网络栈层级拆成 5 层,每层标注「什么坏了 / 什么还好 / 根因」,用「谁还在工作」反推故障层——这是整个 skill 的诊断骨架。
| 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 |
它的核心方法论是「先验证组件自己的健康端点,别从旁证下结论」——因为多层可以产生重叠症状,先跳到最具体的验证可以避免钻进错误的一层。
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.**
针对 TUN 全局模式下「探针会撒谎」这一现象,它给出可判定的证伪标准:0.00s 的跨洋连接、亚毫秒 ping 在物理上不可能,因此测到的是本地 TUN 而非网络。
**Counter-move**: before citing any latency / reachability number while a TUN is up, ask *"would this number be physically possible if the packet really traversed to the destination?"* A `0.00s` connect or a `0.2ms` ping to another continent is the tell that you measured the TUN, not the network.
对同一句错误信息给出两套互斥解释并要求先做区分实验:198.18.x.x 只证明有 TUN,不证明 fake-IP 导致了失败;(A) 协议误判与 (B) 转发路径间歇性抖动需要相反的修法。
**First, a warning about this symptom**: the `198.18.x.x` in the error message is **not evidence that fake-IP caused the failure**. Under TUN, *every* hostname resolves to a fake IP, so that address appears in the error whether the connection failed for this reason or any other.
它把「端口探测在 TUN 下全不可信」变成了一条必须先做的校准实验:探一个目标肯定不服务的端口,若也报开,则该机器上所有端口探测零信息量。
**First: stop trusting `nc -z`.** Under a TUN the local stack completes the TCP handshake on the destination's behalf, so `nc -z` succeeds for **every** port, including ports the destination has closed.
「隧道还是对端防火墙」这个在客户端无法区分的问题,被要求走带外通道(云厂商 guest-agent)进入目标机取证,并给出「journalctl 而非 auth.log」的实测反例。
**Then ask the destination out-of-band.** The only way to settle "did my packets arrive?" is a channel that does not traverse the failing data path. On a cloud VM that is the provider's guest-agent control plane
自带的快速诊断脚本把最常见的四类 macOS 冲突(env 代理、系统代理例外、直连/代理路径分裂、本地 TLS 信任)压成一条命令,并给出可判读的三行结论。
python3 scripts/quick_diagnose.py --host local.example.com --url https://local.example.com/health
对「破坏性网络变更」设了前置门:动 VPN/路由/服务之前必须先读 recovery 参考、先验证前置条件、先布好有界恢复手段,并明确「停成功/进程脱离/脚本末尾重启」都不算恢复证据。
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.
2核心能力
3外部依赖
| Kind | Dependency / Endpoint |
|---|---|
| cli | tailscale(status / ssh / up / version) |
| cli | macOS 网络诊断工具族(ifconfig / netstat / route / scutil / lsof / dscacheutil) |
| cli | curl(含 -x 显式代理、--noproxy '*'、-w 计时模板) |
| cli | dig / nslookup(逐 nameserver 二分 DNS) |
| cli | nc / tcpdump / ss(端口探测与到达性取证,含 sudo tcpdump) |
| cli | ssh / ssh -vvv / ssh -T(连通性与 git over SSH 诊断) |
| cli | docker / docker build / docker run(VM 与容器代理传播诊断) |
| cli | pnpm(SOP 示例,含 npmmirror 镜像参数) |
| cli | 阿里云 CLI(aliyun ecs …,带外命令执行取证) |
| cli | journalctl / grep(目标机日志取证;经带外通道执行) |
| api | Shadowrocket 配置编辑器 API(读/写整份代理配置,LAN 内 HTTP) |
| network | 通用可达性目标(Google generate_204、api.github.com、alpine CDN)用于路径健康与容量测量 |
| network | Tailscale 官方站点(ACL 管理页与独立版安装包) |
| network | 本地/内网服务与 vanity 域名(文档中的占位目标,非固定端点) |
4管控建议与判定依据
- scripts/: 执行本地脚本或命令: python3 scripts/quick_diagnose.py --host … --url … [--tailscale-ip …](仓库自带,
- 网络端点: 声明外部网络访问: curl 经本地/局域网代理端口(127.0.0.1:1082、host.internal:1082 等)与直连访问外部站点做路径健康与容量测量(ap
- 本地工作目录: 文件读写与工件输出: Shadowrocket 配置写回:POST /api/save --data-binary @config.txt 替换编辑器缓冲区(referen
5第二遍独立确认
- [discrepancy] 每条外部依赖的调用点是否真实存在(13 个 CLI/API/network 条目) — 【本项在第二遍自查时曾误判为 ok,后经磁盘级校验工具抓出并已修正】。逐条回查后确认:tailscale 调用点见 Step 2D 的 `tailscale status` 与诊断纪律表的 `| Tailscale daemon | \`tailscale status\` returns peer list (not connection error) |`、Step 5 的 `sudo tailscale up --ssh` / `--ssh=false` / `tailscale ssh` / `tailscale version`;其余依赖亦各有真实调用点(env/ifconfig/route/scutil 见 2A/2B、curl 见 2A/2C/2E 与容量参考、dig 见 2I/2J/2K、nc/tcpdump/ss 见 2K 与 Step 6、ssh 见 Step 6 与 SSH 双隧道、docker 见 2G、pnpm 见 SOP 第 5 节、aliyun ecs 与 journalctl 见 2K、Shadowrocket config API 见 proxy_conflict_reference.md、google/github/alpine CDN 见 2H/2K/2G、Tailscale 官方站点见 Step 4/5B、localhost/local.<domain> 见 Step 1 症状清单)。**纠正的具体错误**:首稿有两条 evidence 的 quote 被误取自同批另一份源码(windows-remote-desktop-connection-doctor/SKILL.md)——`tailscale status` / `tailscale netcheck` 与 `ifconfig | grep -E "^[a-z]|inet |utun"` / `netstat -rn | head -40` / `scutil --proxy` 均属该文件而非本 skill;本 skill 内 tailscale 的实际命令是 `tailscale status`、`tailscale ssh`、`sudo tailscale up --ssh`、`tailscale version`(**不含 netcheck**),macOS 工具的实际命令是 `ifconfig | grep -A2 'inet 100\.'`。两条 quote 已按 pin 源码逐字替换,`netcheck` 相关表述已从 name/scripts_executed 中清除。教训记录:跨文件批量取证时,同批并行读取的相邻源码极易被错误归属,evidence 必须在写入前逐条回到目标文件复核。
- [ok] 自带脚本是否与文档声明一致(是否只做诊断、不写盘、无 shell 注入) — quick_diagnose.py 的 docstring 自述只做五类冲突检测(env+NO_PROXY、系统代理例外、代理路径 vs 直连、本地 TLS 信任、可选路由归属);其 subprocess.run 全部传参数列表(无 shell=True);os.environ.copy() 后仅 pop 代理变量以做对照请求;无文件写入、无 subprocess 之外的网络栈。结论成立。
- [ok] 是否漏记外发目标(把每个域名回查调用点) — 全目录域名提取:api.github.com、www.google.com、dl-cdn.alpinelinux.org、registry.npmmirror.com、login.tailscale.com、pkgs.tailscale.com、docs.python.org、docs.ansible.com、tailscale.com、local.example.com、localhost(多端口)、127.0.0.1:1082、host.internal:1082、192.168.31.110:8080(Shadowrocket IP 占位)、100.101.102.103:8002、203.0.113.199(RFC 5737 对照)、198.18.x.x(fake-IP 段)。均已归入 external_deps 或注入面说明;无未记载的隐蔽外发。
- [discrepancy] 『安全降级』检索是否有命中 — 命中并构成文档内部张力:Step 6 的端到端验证命令为 `ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no <user>@<tailscale-ip> 'echo SSH_OK && hostname && whoami'`,即关闭主机密钥校验;而 Step 2H 在讨论『不要硬编码 GitHub IP』时明确把『pressure you into loosening `StrictHostKeyChecking`』列为真实成本,并称「That is a real cost paid for an uncertain benefit.」。同一文档一处反对放宽该检查、另一处把它写进推荐验证命令,属于未消解的表述张力(实际影响有限:该命令只执行 echo/hostname/whoami,无数据外发)。按分级范式这不足以把本 skill 判红,但应作为使用提醒记录下来。
- [ok] 『读取代理配置』这一凭据接触面是否被文档自己披露 — 披露且被谨慎处理:proxy_conflict_reference.md 明确要求不要硬编码设备 IP('The device IP changes with DHCP. Do not hardcode it.')、给出端口 8080 可能被别的服务占用的 lsof 检查、并用粗体警告 -d 会 URL 编码从而『destroys the entire configuration』。即作者意识到了该接口的高风险性。此事以「凭据读取 + 配置写入」双重性质记入 security,不构成隐蔽行为。
- [ok] 是否存在混淆或隐蔽脚本 — 全目录扫描 >200 字符无空格长行零命中;无 base64 大块(仅 2K 节的 aliyun --ContentEncoding Base64 说明与 RFC 引述);唯一可执行文件 quick_diagnose.py 全文可读、无动态代码加载。
- [ok] 文档内部是否另有自相矛盾(除 StrictHostKeyChecking 外) — 抽查了 nc -z 的表述:Step 1 的关键区分里曾以 nc -z 成功作为判据,Step 2K 则明确『This supersedes the plain `nc -z` reading in the bullet above whenever a TUN — not just Tailscale — is in the path』——即作者主动标注了覆盖关系而非留矛盾,属已消解的张力。其余抽查(utun MTU 判据、TUN 可信探针清单)内部一致。
- [ok] pin commit 与 skill.path 是否与任务书表格一致 — git rev-parse HEAD = d5c4678cb5d4fd6acc9c922690df035dbd33d247,与任务书表格『库内 HEAD』一致;目录位于仓库根,相对路径 tunnel-doctor;本目录最后一次提交 829d9758(2026-09-08 22:22)。GitHub API 复核 MIT / stars 1392 / pushed 2026-09-15T09:30:04Z。
6结论
a9f26e4f66655f0e…