起因:两个页面,两个不同的 IP
前段时间调一个问题:同一台机器、同一个浏览器,访问 A 站和 B 站,后台日志里记录到的客户端 IP 居然不一样。
一开始以为是日志记错了。后来才反应过来------ "我的 IP"这个说法本身就不严谨。你以为你只有一个公网 IP,但真实网络环境里,出口 IP 取决于走哪条路由:多线路的机器、企业网关、IPv4/IPv6 双栈、运营商的 CGNAT,都可能让"不同的目标站点,看到不同的你"。
问题来了:我怎么在浏览器里,不装抓包工具、不自己搭后端,就知道"某个具体网站看到的我的出口 IP 是哪个"?
翻常规办法,基本都要靠第三方 API(ip.sb、ipify 之类)。但它们只能告诉你"那个 API 站 看到的你的 IP",不能告诉你"claude.ai 或任意某站看到的你的 IP"。而我恰恰想知道后者。
转机:Cloudflare 的 /cdn-cgi/trace
后来发现一个冷门端点。凡是套了 Cloudflare 的站点(现在这类站占比很高),都会在 /cdn-cgi/trace 暴露一段纯文本。直接 curl 一下:
arduino
curl -s https://claude.ai/cdn-cgi/trace
拿到的是这么一坨 key=value(我实测的真实输出,IP 打码):
ini
fl=1009f28
h=claude.ai
ip=207.56.226.xxx
ts=1784612887.000
visit_scheme=https
uag=curl/8.7.1
colo=NRT
http=http/2
loc=JP
tls=TLSv1.3
warp=off
kex=X25519
逐个字段拆一下,有用的几个:
| 字段 | 含义 |
|---|---|
ip |
该站点这一侧看到的你的出口 IP(重点) |
loc |
这个 IP 被判定的国家(两位码,如 JP) |
colo |
接住你请求的 Cloudflare 数据中心(机场三字码,NRT=东京成田) |
http |
协商用的 HTTP 版本 |
tls |
TLS 版本 |
warp |
是否走了 Cloudflare 自家的 WARP |
colo=NRT 这个挺有意思------它是 Anycast 就近接入的结果,顺带能看出你的流量从哪个地理节点进的 Cloudflare 网络。
关键一步:它跨域可读
光能 curl 没用,curl 是在我本机跑的。我要的是在浏览器页面里、用 JS 读到 。这就撞上浏览器的同源策略------默认情况下,你在 a.com 的页面里 fetch('https://b.com/...'),响应体是拿不到的。
于是去看它的响应头。这里我踩了个小坑:一开始用 curl -I(HEAD 请求)去看,返回的 content-type 是 text/html,还没有 CORS 头,我以为读不了。后来改成正常 GET + 加个 Origin 头再看,才是真相:
bash
curl -s -D - -o /dev/null -H "Origin: https://foo.example" https://claude.ai/cdn-cgi/trace
响应头里明明白白:
less
content-type: text/plain
access-control-allow-origin: *
access-control-allow-origin: * ------任意来源都允许跨域读取。也就是说,在任何网页里都能直接 fetch 它、拿到响应体。(教训:验 CORS 别用 HEAD,很多端点根本不按 HEAD 的语义返回,得用真实方法 + 带 Origin 头。)
封装成一个函数
原理通了,代码就很短:
javascript
// 读取"指定 CF 站点这一侧,看到的你的出口 IP 及网络信息"
// 纯前端,无需任何后端或第三方 API
async function readTrace(host) {
const res = await fetch(`https://${host}/cdn-cgi/trace`, { cache: 'no-store' });
if (!res.ok) throw new Error(`${host} 未返回 trace(可能没走 Cloudflare)`);
const text = await res.text();
// 响应是一行一个 key=value 的纯文本,手动解析
return Object.fromEntries(
text.trim().split('\n').map((line) => {
const i = line.indexOf('=');
return [line.slice(0, i), line.slice(i + 1)];
})
);
}
// 用法
const t = await readTrace('claude.ai');
console.log(t.ip, t.loc, t.colo); // 该站看到的你的出口 IP / 国家 / 接入节点
{ cache: 'no-store' } 别省------trace 端点响应短,某些情况下会被缓存,不加可能读到旧值。
顺手能做的一件事:横向对比多个站的出口
一个站能读,那就把一批 CF 站点挨个读一遍,看看它们各自看到的出口 IP 是不是同一个:
ini
const hosts = ['claude.ai', 'openai.com', 'www.cloudflare.com', 'anthropic.com'];
const results = await Promise.allSettled(
hosts.map(async (h) => ({ host: h, ...(await readTrace(h)) }))
);
for (const r of results) {
if (r.status === 'fulfilled') {
const { host, ip, loc, colo } = r.value;
console.log(`${host.padEnd(22)} ip=${ip} loc=${loc} colo=${colo}`);
}
}
如果所有站看到的 ip 都一致,说明你的出口是统一的;如果某个站看到的 IP 或 loc 跟别的不一样,那就定位到了"是哪一段网络路径把这个站的流量引到了不同出口"。前面那个"两个页面两个 IP"的诡异问题,我就是这么定位到的------比翻日志直观多了。
(用 Promise.allSettled 而不是 all:总有些站不走 CF,或路径不同会抛错,别让一个失败拖垮整批。)
再往下一层:这个 IP 本身"干不干净"?
知道了出口 IP,自然会想再问一句:这个 IP 的"网络身份"是什么?是住宅宽带、还是机房 IP?属于哪个 ASN?有没有被各类情报源标记过?
这属于另一个话题了------IP 信誉评估。简单说,判断一个 IP 的"成分",通常看这几个维度:
- 类型:住宅 / 机房(IDC)/ 移动。反查 ASN 和 rDNS 能大致区分。
- ASN 归属:这个 IP 属于哪个自治域,是电信运营商还是云厂商。
- 同段邻居 :同一
/24网段里跑的是住宅域名还是一堆机房主机,能反推整段性质。 - 黑名单命中:是否进过公开的 DNSBL、是否被蜜罐网络记录过。
这些数据分散在十几个不同的免费情报源里,单独查很累。我后来把"读出口 IP + 多源聚合打个信誉分"这套流程整个做成了个页面,平时排查网络问题顺手用。
小结
/cdn-cgi/trace是 Cloudflare 在每个托管站点都会暴露的纯文本诊断端点;- 它
access-control-allow-origin: *,跨域可读,所以能在任意网页里纯前端 fetch; - 由此可以读到"任意 CF 站点这一侧看到的你的出口 IP",而不只是某个 IP 查询 API 看到的;
- 横向对比多个站的出口,能直观定位路由差异;
- 验 CORS 记得用真实请求方法 + Origin 头,别用 HEAD 被误导。
一个挺冷门、但排查网络问题时很趁手的小技巧。