TUN 的 fake-ip 让抓取工具拒绝了所有域名
一个抓取网页的工具在 Clash Verge 的 TUN 模式下全量报错:域名被解析成 198.18.x.x(RFC 2544 保留段),它的公网地址校验判定成内网地址。改 DNS 的 enhanced-mode 修好了主体,第二个坑在 AAAA 上。
网页抓取工具 `web_fetch` 在任意 URL 上都立即失败:
```plaintextError: URL hostname "example.com" resolves to a non-public IP address
```
失败是瞬时的,没有网络等待。同一时间 `web_search` 正常,浏览器和 `curl` 访问同一个 URL 也正常。
环境
| 项 | 值 |
|---|---|
| 系统 | CachyOS(Arch) |
| 代理 | Clash Verge Rev,mihomo 内核,TUN 模式 |
| DNS | systemd-resolved → NetworkManager 的 dnsmasq |
| 工具 | DSH `web_fetch`(`@deepseek-ai/dsh-web-fetch-http`) |
复现这个问题需要两个条件:内核开着 fake-ip(TUN 模式最容易碰到),以及被调用的工具会校验目标地址是不是公网地址。
这套做法在什么环境上验证过
```plaintextCachyOS (Arch) | Clash Verge Rev(mihomo 内核)| TUN 模式 | 混合端口 7897
```
只在这一个组合上实测过。下面这些地方会因环境而异:
- `
198.18.0.0/15` 这个假地址段:⚠️ 取决于内核的 `fake-ip-range`。判断方法不变:解析结果落在保留段里就是它。 - `
enhanced-mode` 与 `IPV6` 两个键:✅ mihomo 系内核通用,键名一致。 - 用 Merge 覆写订阅配置:⚠️ 这是 Clash Verge Rev 的做法,别的客户端一般直接改配置本体。
- 拦截发生在工具侧:⚠️ 本次被测的是 DSH 的 `
web_fetch`。换一个工具,只要它做公网地址校验就会复现;不做校验的工具不会。 - `
dns.ipv6: false` 从哪来:❌ 因 Verge 版本而异。本次是它的 IPv6 开关注入的,关掉那个开关这条就没了。
一、先看域名解析出来的是什么
```bashgetent hosts example.com # 198.18.0.64 example.com getent ahostsv4 www.baidu.com # 198.18.0.65 STREAM www.baidu.com
```
`198.18.0.0/15` 是 RFC 2544 给网络设备基准测试留的保留段,不可能是真实网站地址。
这正是 Clash / mihomo 的 fake-ip 模式:给每个域名发一个假地址,应用连到假地址,由 TUN 网卡按域名转发。这台机器上那张网卡叫 `Meta`,地址是 `198.18.0.1/30`。
二、glibc 与 c-ares 走的是两条路
同一个域名,两次查询结果不同:
```bashnode -e "require('node:dns').lookup('example.com',{all:true},(e,a)=>console.log(e||a))" # [ { address: '198.18.0.64', family: 4 } ] node -e "require('node:dns').resolve4('example.com',(e,a)=>console.log(e?e.code:a))" # [ '104.20.23.154', '172.66.147.243' ]
```
`dns.lookup()` 走 glibc 的 `getaddrinfo`,`dns.resolve4()` 是 c-ares 直接问 DNS 服务器。差别在 `nsswitch.conf` 的顺序:
```bashgrep '^hosts' /etc/nsswitch.conf # hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns
```
`resolve`(nss-resolve,问 systemd-resolved)排在 `dns`(127.0.0.1 上的 dnsmasq)前面。而 resolved 把 TUN 那条链路当成了默认 DNS 源:
```bashresolvectl status Meta # Current DNS Server: 198.18.0.2 # DNS Servers: 198.18.0.2 # DNS Domain: ~. # Default Route: yes
```
`DNS Domain: ~.` 加上 `Default Route: yes`,意思是所有域名都交给 mihomo 的 DNS,于是全部返回 fake-ip。
绕开 resolved 直接问本机 dnsmasq,拿到的是真实地址:
```bashdig +short @127.0.0.1 example.com A # 104.20.23.154 # 172.66.147.243
```
三、工具为什么拒绝
`web_fetch` 在发请求前会先解析主机名,再逐个校验地址范围:
```jsif (parsed instanceof ipaddr.IPv4) return parsed.range() === "unicast";
```
`ipaddr.js` 把 `198.18.0.0/15` 归类为 `reserved`:
```bashnode -e "const i=require('ipaddr.js');console.log(i.parse('198.18.0.64').range())" # reserved
```
校验不通过就抛错。这是有意的 SSRF 防护,目的是不让抓取工具去访问回环、内网和链路本地这些地址。fake-ip 让所有公网域名都“看起来”是保留地址,于是全被拦下。
同一个文件里还有一条给代理留的分支:请求若被判为走代理,就不再本地解析、也不做地址钉扎,域名交给代理去解析。
```jsconst route = proxyRouteFor(url); if (route.proxied && !isNonPublicIpLiteral(url.hostname)) return await publicHttpNetwork.requestVia(route.dispatcher, url, headers, signal); const addresses = await this.resolveAddresses(url.hostname, signal)
```
四、三条修法
-
给工具配出站代理。在工具的用户级环境文件里写 `
HTTP_PROXY` / `HTTPS_PROXY`,走上面那条代理分支。 改动最小、随时可回滚。代价是它依赖代理进程一直在跑:代理完全退出时这条工具会连不上,而且没有直连回退。 另外 TUN 关掉、系统代理关掉时这条照样成立 —— 混合端口是内核自己的监听,与那两个开关无关。 -
改 `
nsswitch.conf` 顺序,把 `dns` 提到 `resolve` 前面,让 glibc 走 dnsmasq 拿真实地址。 从根上恢复真实解析。代价是它改变全系统的解析行为,而且需要 root;mihomo 每次重启都会重设 TUN 链路的 DNS。 -
关掉 fake-ip(本次采用)。在 Clash Verge 的全局扩展配置里把 DNS 的 `
enhanced-mode` 覆写成 `redir-host`。 改完不依赖任何代理进程存活,TUN 开着也正常。
五、关掉 fake-ip
mihomo 的 DNS `enhanced-mode` 有两个取值:`fake-ip` 和 `redir-host`,默认是 `redir-host`,所以这一步是回到默认行为,不是 hack。
Clash Verge Rev 的全局扩展配置(Merge)可以覆写订阅里的设置,`dns` 不在禁止覆写的名单里(名单是 mixed-port、log-level、external-controller 这一批)。
在 `Merge.yaml` 末尾追加:
```yamldns: enhanced-mode: redir-host
```
改完不会自动生效。实测:改完当时 `clash-verge.yaml` 的 mtime 没变,里面的 `enhanced-mode` 还是 `fake-ip`,`getent` 也还是返回 `198.18.0.64`。
让它生效的办法是重启 Clash Verge(实测有效:重启后 `clash-verge.yaml` 与 `config.yaml` 先后更新,`enhanced-mode: redir-host` 落盘)。
注意
只「重开内核」不生效 —— 实测重启内核不会重新读 profile,`
enhanced-mode` 还是旧值。 得让 Verge 本体重启一次。
六、第二个坑:AAAA 污染
`redir-host` 生效后普通站点恢复了,但 `www.google.com` 仍然报同一个错。原因跟 fake-ip 无关:
```bashdig +short @198.18.0.2 www.google.com A # 31.13.92.37 ← Facebook 的地址 dig +short @198.18.0.2 www.google.com AAAA # 2001::1 ← Teredo 段
```
这台机器用的是订阅自带的 DNS(全是国内 DNS),也没有配 `fallback`:这类配置对部分境外域名的应答本身就可能被污染。
而校验的策略是整个应答集里只要有一个地址不是公网地址就整体拒绝:A 记录没问题,AAAA 记录把它拖垮了。同一批里 `youtube.com`、`www.facebook.com`、`www.wikipedia.org`、`twitter.com` 的 AAAA 也是 `2001::1`。
让 DNS 干脆不返回 AAAA 就行:`dns.ipv6: false`。这台机器上它是 Clash Verge 的 IPv6 开关注入的(`config.yaml` 与合并后的 `clash-verge.yaml` 里各有一处,而 `profiles/` 下所有文件都不含 `ipv6`)。
丢掉 AAAA 不损失可达性:本机没有全局 IPv6 地址,也没有 IPv6 默认路由。mihomo 文档里 `IPV6` 为 false 的行为是“回应 AAAA 的空解析”,是正规做法。
提示
两个条件缺一不可:`
dns.enhanced-mode: redir-host` 消除 fake-ip, `dns.ipv6: false` 消除污染 IPv6。只做前者时,应答被污染的那些域名仍然会失败。
七、验证结果
在 TUN 开启(`Meta` 网卡在)的状态下实测:
- `
getent ahostsv4 example.com`:`104.20.23.154`,不再是 `198.18.x.x` - `
resolvectl query example.com`:真实 IPv4(仍标注 `-- link: Meta`,内容是真实地址) - `
web_fetch` 访问 `example.com` / `www.google.com` / `www.youtube.com` / `www.wikipedia.org` / `www.facebook.com` / `github.com` / mihomo 官方文档 / `raw.githubusercontent.com`:全部 200 - `
web_search`:正常 - `
env | grep -i proxy` 为空,`~/.dsh/.env` 不存在:修复不依赖代理进程 - 浏览器与 `
curl` 侧回归:`google.com` / `youtube.com` / `baidu.com` / `github.com` 全部 200,标题正确
八、自检命令
```bash# 是否还在 fake-ip:出现 198.18.x.x 就是中招 getent ahostsv4 example.com # resolved 是否把 TUN 链路当成了全域名 DNS resolvectl status Meta | grep -E 'DNS Server|DNS Domain|Default Route' resolvectl query example.com # 绕开 resolved 的真实解析 dig +short @127.0.0.1 example.com A # AAAA 有没有被污染 dig +short @198.18.0.2 www.google.com AAAA # 混合代理端口可用性(默认 7897) curl -s -o /dev/null -w '%{http_code}\n' -x http://127.0.0.1:7897 https://example.com/
```
九、排错
-
改了 `
Merge.yaml` 没变化可能原因:Verge 不监听 profile 文件,改完不会自动重新应用。
怎么办:重启 Clash Verge(只重开内核不生效,见第五节);然后用 `
grep enhanced-mode` 看 `clash-verge.yaml` 里落盘的值。 -
普通站点好了,个别站点仍然报同一个错
可能原因:那个域名的 AAAA 应答被污染,整个应答集因此被判为非公网。
怎么办:`
dig +short @<你的 DNS> <域名> AAAA` 看一眼;是 `2001::1` 之类就确认 `dns.ipv6: false` 生效了。 -
TUN 关掉后一切正常,开着就坏
可能原因:TUN 链路带来了 `
DNS Domain: ~.` 的 DNS 源,也就是 fake-ip 的来源。怎么办:按第五节改 `
enhanced-mode`;只想临时绕开就先关 TUN。 -
`
dns.lookup()` 与 `dns.resolve4()` 结果不一致可能原因:前者走 glibc 的 nsswitch 链,后者走 c-ares 直接查 DNS。
怎么办:`
grep '^hosts' /etc/nsswitch.conf` 看顺序。判断“解析是否被代理接管”要用前者的结果。
参考
- mihomo DNS 文档 —— `
enhanced-mode`、`IPV6`、`nameserver-policy` 的定义 - Clash Verge Rev — Merge Configuration —— 全局扩展配置能覆写什么、不能覆写什么
- RFC 2544 —— `
198.18.0.0/15` 这个保留段的出处