tunnel-doctor

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Tunnel Doctor

隧道问题诊断与修复指南

Diagnose and fix conflicts when Tailscale coexists with proxy/VPN tools on macOS, with specific guidance for SSH access to WSL instances.
诊断并修复macOS系统上Tailscale与代理/VPN工具共存时的冲突,包含连接WSL实例的SSH访问专属指导。

Diagnostic Workflow

诊断流程

Step 1: Identify the Symptom

步骤1:确认症状

Determine which scenario applies:
  • 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)
  • 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)
Key distinction: SSH does NOT use
http_proxy
/
NO_PROXY
env vars, but curl/wget/Python requests/Node.js fetch do. If SSH works but HTTP doesn't, it's almost always a proxy env var issue, not a route issue.
判断符合以下哪种场景:
  • Tailscale ping正常,SSH可连接,但curl/HTTP请求超时 → HTTP代理环境变量冲突(步骤2A)
  • Tailscale ping正常,但SSH/TCP请求超时 → 路由冲突(步骤2B)
  • SSH可连接但返回
    operation not permitted
    → Tailscale SSH配置问题(步骤4)
  • SSH可连接但
    be-child ssh
    返回退出码1
    → WSL snap沙箱问题(步骤5)
关键区别:SSH不会使用
http_proxy
/
NO_PROXY
环境变量,但curl/wget/Python requests/Node.js fetch会使用。如果SSH正常但HTTP请求异常,几乎可以确定是代理环境变量问题,而非路由问题。

Step 2A: Fix HTTP Proxy Environment Variables

步骤2A:修复HTTP代理环境变量

Check if proxy env vars are intercepting Tailscale HTTP traffic:
bash
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
:
bash
export NO_PROXY=localhost,127.0.0.1,.ts.net,100.64.0.0/10,192.168.*,10.*,172.16.*
EntryCoversWhy
.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 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_fixes.md for the compatibility matrix.
Verify the fix:
bash
undefined
检查代理环境变量是否拦截了Tailscale的HTTP流量:
bash
env | grep -i proxy
异常输出 — 已设置代理但
NO_PROXY
未排除Tailscale相关配置:
http_proxy=http://127.0.0.1:1082
https_proxy=http://127.0.0.1:1082
NO_PROXY=localhost,127.0.0.1          ← 缺少Tailscale相关配置!
修复方案 — 将Tailscale MagicDNS域名+CIDR添加到
NO_PROXY
bash
export NO_PROXY=localhost,127.0.0.1,.ts.net,100.64.0.0/10,192.168.*,10.*,172.16.*
配置项覆盖范围原因
.ts.net
MagicDNS域名(
host.tailnet.ts.net
在DNS解析前匹配
100.64.0.0/10
Tailscale IP段(
100.64.*
100.127.*
精确的CIDR范围,不会误匹配公网IP
192.168.*,10.*,172.16.*
RFC 1918私有网络局域网流量永远不应被代理
两层配置互补
.ts.net
处理基于域名的访问,
100.64.0.0/10
处理直接IP访问。
NO_PROXY语法陷阱 — 查看references/proxy_fixes.md获取兼容性矩阵。
验证修复效果:
bash
undefined

Both must return HTTP 200:

以下两个命令均需返回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`).
NO_PROXY="...(新值)..." curl -s --connect-timeout 5 http://<host>.ts.net:<port>/health -w "HTTP %{http_code}\n" NO_PROXY="...(新值)..." curl -s --connect-timeout 5 http://<tailscale-ip>:<port>/health -w "HTTP %{http_code}\n"

然后将配置持久化到shell配置文件(`~/.zshrc`或`~/.bashrc`)。

Step 2B: Detect Route Conflicts

步骤2B:检测路由冲突

Check if a proxy tool hijacked the Tailscale CGNAT range:
bash
route -n get <tailscale-ip>
Healthy output — traffic goes through Tailscale interface:
destination: 100.64.0.0
interface: utun7    # Tailscale interface (utunN varies)
Broken output — proxy hijacked the route:
destination: 100.64.0.0
gateway: 192.168.x.1    # Default gateway
interface: en0           # Physical interface, NOT Tailscale
Confirm with full route table:
bash
netstat -rn | grep 100.64
Two competing routes indicate a conflict:
100.64/10  192.168.x.1   UGSc  en0       ← Proxy added this (wins)
100.64/10  link#N        UCSI  utun7     ← Tailscale route (loses)
Root cause: On macOS,
UGSc
(Static Gateway) takes priority over
UCSI
(Cloned Static Interface) for the same prefix length.
检查代理工具是否劫持了Tailscale的CGNAT网段:
bash
route -n get <tailscale-ip>
正常输出 — 流量通过Tailscale接口传输:
destination: 100.64.0.0
interface: utun7    # Tailscale接口(utunN编号不固定)
异常输出 — 代理劫持了路由:
destination: 100.64.0.0
gateway: 192.168.x.1    # 默认网关
interface: en0           # 物理接口,而非Tailscale接口
通过完整路由表确认:
bash
netstat -rn | grep 100.64
出现两条竞争路由表示存在冲突:
100.64/10  192.168.x.1   UGSc  en0       ← 代理添加的路由(优先级更高)
100.64/10  link#N        UCSI  utun7     ← Tailscale路由(优先级更低)
根本原因:在macOS系统中,相同前缀长度的路由中,
UGSc
(静态网关)优先级高于
UCSI
(克隆静态接口)。

Step 3: Fix Proxy Tool Configuration

步骤3:修复代理工具配置

Identify the proxy tool and apply the appropriate fix. See references/proxy_fixes.md for detailed instructions per tool.
Key principle: Do NOT use
tun-excluded-routes
to exclude
100.64.0.0/10
. This causes the proxy to add a
→ en0
route that overrides Tailscale. Instead, let the traffic enter the proxy TUN and use a DIRECT rule to pass it through.
Universal fix — add this rule to any proxy tool:
IP-CIDR,100.64.0.0/10,DIRECT
IP-CIDR,fd7a:115c:a1e0::/48,DIRECT
After applying fixes, verify:
bash
route -n get <tailscale-ip>
识别代理工具并应用对应的修复方案。查看references/proxy_fixes.md获取各工具的详细说明。
核心原则:不要使用
tun-excluded-routes
排除
100.64.0.0/10
。这会导致代理添加一条
→ en0
的路由,覆盖Tailscale的路由。正确做法是让流量进入代理TUN,然后使用DIRECT规则放行。
通用修复方案 — 向任意代理工具添加以下规则:
IP-CIDR,100.64.0.0/10,DIRECT
IP-CIDR,fd7a:115c:a1e0::/48,DIRECT
应用修复后验证:
bash
route -n get <tailscale-ip>

Should show Tailscale utun interface, NOT en0

应显示Tailscale的utun接口,而非en0

undefined
undefined

Step 4: Configure Tailscale SSH ACL

步骤4:配置Tailscale SSH ACL

If SSH connects but returns
operation not permitted
, the Tailscale ACL may require browser authentication for each connection.
At Tailscale ACL admin, ensure the SSH section uses
"action": "accept"
:
json
"ssh": [
    {
        "action": "accept",
        "src": ["autogroup:member"],
        "dst": ["autogroup:self"],
        "users": ["autogroup:nonroot", "root"]
    }
]
Note:
"action": "check"
requires browser authentication each time. Change to
"accept"
for non-interactive SSH access.
如果SSH可连接但返回
operation not permitted
,说明Tailscale ACL可能要求每次连接都通过浏览器认证。
Tailscale ACL管理页面中,确保SSH部分配置为
"action": "accept"
json
"ssh": [
    {
        "action": "accept",
        "src": ["autogroup:member"],
        "dst": ["autogroup:self"],
        "users": ["autogroup:nonroot", "root"]
    }
]
注意
"action": "check"
要求每次连接都进行浏览器认证。如需非交互式SSH访问,请改为
"action": "accept"

Step 5: Fix WSL Tailscale Installation

步骤5:修复WSL上的Tailscale安装

If SSH connects and ACL passes but fails with
be-child ssh
exit code 1 in tailscaled logs, the snap-installed Tailscale has sandbox restrictions preventing SSH shell execution.
Diagnosis — check WSL tailscaled logs:
bash
undefined
如果SSH可连接且ACL验证通过,但tailscaled日志中显示
be-child ssh
退出码1,说明通过snap安装的Tailscale存在沙箱限制,无法执行SSH shell。
诊断方法 — 查看WSL的tailscaled日志:
bash
undefined

For snap installs:

针对snap安装版本:

sudo journalctl -u snap.tailscale.tailscaled -n 30 --no-pager
sudo journalctl -u snap.tailscale.tailscaled -n 30 --no-pager

For apt installs:

针对apt安装版本:

sudo journalctl -u tailscaled -n 30 --no-pager

Look for:
access granted to user@example.com as ssh-user "username" starting non-pty command: [/snap/tailscale/.../tailscaled be-child ssh ...] Wait: code=1

**Fix** — replace snap with apt installation:

```bash
sudo journalctl -u tailscaled -n 30 --no-pager

查找以下日志内容:
access granted to user@example.com as ssh-user "username" starting non-pty command: [/snap/tailscale/.../tailscaled be-child ssh ...] Wait: code=1

**修复方案** — 替换snap安装版本为apt安装版本:

```bash

Remove snap version

移除snap版本

sudo snap remove tailscale
sudo snap remove tailscale

Install apt version

安装apt版本

Start with SSH enabled

启动并启用SSH

sudo tailscale up --ssh

**Important**: The new installation may assign a different Tailscale IP. Check with `tailscale status --self`.
sudo tailscale up --ssh

**重要提示**:新安装的Tailscale可能会分配不同的Tailscale IP。使用`tailscale status --self`查看。

Step 6: Verify End-to-End

步骤6:端到端验证

Run a complete connectivity test:
bash
undefined
运行完整的连通性测试:
bash
undefined

1. Check route is correct

1. 检查路由是否正确

route -n get <tailscale-ip>
route -n get <tailscale-ip>

2. Test TCP connectivity

2. 测试TCP连通性

nc -z -w 5 <tailscale-ip> 22
nc -z -w 5 <tailscale-ip> 22

3. Test SSH

3. 测试SSH连接

ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no <user>@<tailscale-ip> 'echo SSH_OK && hostname && whoami'

All three must pass. If step 1 fails, revisit Step 3. If step 2 fails, check WSL sshd or firewall. If step 3 fails, revisit Steps 4-5.
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no <user>@<tailscale-ip> 'echo SSH_OK && hostname && whoami'

三项测试必须全部通过。如果步骤1失败,返回步骤3重新检查。如果步骤2失败,检查WSL的sshd服务或防火墙。如果步骤3失败,返回步骤4-5重新检查。

References

参考资料

  • references/proxy_fixes.md — Detailed fix instructions for Shadowrocket, Clash, and Surge
  • references/proxy_fixes.md — Shadowrocket、Clash和Surge的详细修复说明