第 3 章 · 出口为什么不同

为什么三个入口的出口可以不一样

这是整个工具存在的理由,值得单独一份文档。


一、根源:三个入口读的不是同一份代理配置

入口读什么匹配依据
Claude Code(CLI,Node 运行时)只认 HTTPS_PROXY / HTTP_PROXY 环境变量。没设就直连直连时如果本机有 TUN / 透明代理,分流器按 IP 匹配规则
Claude 桌面端(Electron / Chromium)认操作系统的代理设置通过 HTTP CONNECT 把域名交给代理,分流器按域名匹配规则
浏览器里的 claude.ai认系统代理,除非装了代理插件自己接管同上;插件可以单独改写

Node 默认不读系统代理。这一条是所有困惑的起点: 你在系统设置里配好了代理,桌面端立刻生效,而 Claude Code 完全不知道它的存在。

"按 IP 匹配"和"按域名匹配"是两个不同的匹配过程,命中的规则可以不同。 于是同一个域名,从 CLI 出去和从桌面端出去,落地国家可以不一样。


二、这个工具怎么测

对每个入口造一条等价探测路径:用和该入口完全相同的代理配置, 去问目的地的 cdn-cgi/trace(见 01-endpoints.md 第三节)。 目的地返回的 ip= 就是该入口真实的出口地址 —— 不是本机接口地址, 也不是猜的。

  • CLI 路径:按 HTTPS_PROXY 是否存在,走代理或直连;
  • 桌面端 / 网页路径:走系统代理(读 scutil --proxy), 用真正的 HTTP CONNECT 建隧道,DNS 交给代理做 —— 和 Chromium 一样;
  • 对照路径:强制不用任何代理。
python3 -m cem doctor     # 先看清本机有哪几条路径
python3 -m cem probe      # 再量每条路径的出口

系统代理是 PAC 脚本时

PAC(自动配置脚本)对每个 URL 可以返回不同的代理,不存在"一个"代理。 本工具检测到 PAC 时退回直连并在输出里说明,结论仅供参考 —— 硬凑一个数字比说不知道更糟。


三、三个会让人得出错误结论的陷阱

陷阱一:fake-ip —— dig 查出来的地址是假的

开了 TUN 模式的分流器(Clash / mihomo / sing-box)通常同时开 fake-ip: 它拦下 DNS 查询,给每个域名返回一个 198.18.x.x 的占位地址, 自己记住"这个占位地址对应哪个域名",等程序连上来时再按域名匹配规则。

后果:

dig +short api.anthropic.com
# 198.18.0.56    ← 这不是任何真实主机
  • 查这个地址的 ASN / 归属毫无意义;
  • lsof 里看到进程连 198.18.0.140 也不代表它连了什么可疑的东西。

198.18.0.0/15 是 RFC 2544 给网络设备基准测试保留的段,公网不可路由 —— 所以拿来做占位很安全,也正因如此,它出现在你的连接表里一定是本地分流器造成的。

这个坑反过来有一个好处:分流器给每个域名分配独立的占位地址, 所以可以反查 —— 看到 198.18.0.140 就能推出它是哪个域名。 在正常环境里做不到这件事(多个域名共用一个 CDN IP)。 本仓的 cem/resolve.py 就是用这个办法给连接表补上域名。

要拿到真实解析,得走 DoH:报文是端到端加密的,中间的分流器改不了里面的答案。

fake-ip 会不会让「出口 IP」也测不准?

不会。 这是这一节最容易读岔的一句,值得单独说清楚。

这个工具的测量项怎么拿到的fake-ip 有影响吗
出口 IP / 落地国家 / 边缘机房对域名建一条真的 HTTPS 连接,问目的地 cdn-cgi/trace没有
DNS / TCP / TLS / 首字节 四段延迟同一条连接上分段计时没有
逐跳延迟与丢包(路径质量)往一个具体 IP 发 ICMP 探测包有,而且是致命的

区别在于问的对象不同:

  • 前两项是问目的地。占位地址只是本机内部的一个标签,程序连上去之后 分流器照样把这条连接转发出去,Cloudflare 那侧看到的就是你真实的出口地址, 它把这个值原样告诉你。这条问答走的是真实链路,fake-ip 拦不住。
  • 路径质量是往一个 IP 号码上打包。而这个号码是假的、公网上不存在, 包发出去就被本机分流器接住了 —— 得到的是"到本机分流器"的一跳, 不是"到 Claude"的路径。

所以在一台全程 fake-ip 的机器上,界面表现是:出口 IP、地区、colo、四段延迟 全部正常显示,只有「路径质量」那一屏说"测不了",并给出三种绕开的办法。 这不是工具瞎了,是那一项的测量前提被破坏了,而它选择说出来而不是给个假数字。

顺带一提,这也解释了为什么本工具不靠 dig 判断出口: dig 拿到的是本机解析结果(可能是占位地址),而"目的地看到的是谁"只能问目的地。

curl -s -H 'accept: application/dns-json' \
  'https://cloudflare-dns.com/dns-query?name=api.anthropic.com&type=A'

界面上的「解析对账」面板就是把这两个结果并排放,不一致就标出来。

陷阱二:按域名分派的解析器(split-DNS)

macOS 支持给不同域名指定不同的 DNS 服务器。VPN、Tailscale MagicDNS、 企业配置都会往里写。

scutil --dns | grep -A2 -E '^\s*domain'

如果输出里出现类似

  domain   : api.anthropic.com
  nameserver[0] : <某个隧道内的地址>

说明这个域名的解析被交给了另一条隧道 —— 而解析走哪条隧道,流量通常也走那条。 这就解释了一类极难排查的现象:同一台机器上,某几个域名的行为和其他域名完全不同。

cem doctor 会把命中的域名和对应的 nameserver 都列出来。

陷阱三:lsof 看不到代理后面的目的地

走本机 HTTP 代理的进程,lsof 只能看到

127.0.0.1:52044 -> 127.0.0.1:<代理端口>

真实目的地在代理进程内部。桌面端和浏览器正好属于这一类, 所以它们的连接表天生是不完整的。

工具不会假装这条限制不存在:这类连接标成 local-proxy, 并在结论里写明有多少条目的地不可见。


四、补上第三个陷阱:读分流器自己的连接表

Clash / mihomo(含 Clash Verge、各种 GUI 前端)以及兼容 clash-api 的 sing-box 都提供一个控制接口。开了之后:

curl -s http://127.0.0.1:9090/connections | python3 -m json.tool | head -40

每条连接会带上这些字段,正是 lsof 缺的那一半:

字段含义
metadata.host真实目标域名(不是占位地址)
metadata.processPath哪个进程发起的
rule / rulePayload命中了哪条分流规则
chains走了哪条代理链,第一个元素就是最终出口节点
upload / download这条连接的流量

怎么打开:在配置文件里加

external-controller: 127.0.0.1:9090
secret: "换成一个你自己的随机字符串"

安全注意:

  • 地址只写 127.0.0.1。绑到 0.0.0.0 等于把"改我全部代理规则"的权限 开给局域网。
  • 一定设 secret。没有 secret 的控制接口,本机任何进程(包括网页里的 JavaScript,如果它能访问回环)都能改你的配置。
  • 本仓只读 /connections,从不写任何配置。secret 只从命令行参数或 环境变量读,不落盘。

cem/clash.py 是这部分的实现,接了之后连接表里的 local-proxy 行就能 换成真实域名和出口节点名。


五、把配置改成一致的

量出来不一致之后,通常有三种改法:

改法一:让 CLI 也走系统代理那条路。 在 shell 配置里显式设:

export HTTPS_PROXY=http://127.0.0.1:<你的代理端口>
export HTTP_PROXY=http://127.0.0.1:<你的代理端口>
export NO_PROXY=localhost,127.0.0.1,::1

设完再跑一次 cem probe,两个入口的出口应当一致。

改法二:让分流规则对 Claude 的所有域名给同一个出口。 在分流器里给这一组域名建一个规则集合,指向同一个节点组。 这样按 IP 命中还是按域名命中都落到同一个出口。 清单可以直接从 python3 -m cem endpoints --json 生成。

改法三:什么都不改,但知道自己在做什么。 有时候不一致是有意的 (业务走低延迟节点、更新下载走另一条)。这时候监控的价值是 在它意外变化时告诉你 —— 界面上的「出口变更」时间轴就是为这个存在的。


六、受限地区

Claude 的服务条款把若干地区列为受限。出口落在这些地区是账号风险, 不是"慢一点":

CN 中国大陆 · HK 香港 · MO 澳门 · RU 俄罗斯 · KP 朝鲜 · IR 伊朗 · SY 叙利亚 · CU 古巴 · BY 白俄罗斯 · VE 委内瑞拉

这份名单在 cem/view.py 的 RESTRICTED_CC 里,界面上命中会标红色药丸。

需要说明清楚的是:这份名单是本工具依据公开条款整理的,不是 Anthropic 的 官方接口返回的。它会过期,最终以官方条款为准。工具的职责是 "出口落在这里时提醒你去核对",不是"替官方判定"。