TUN 的 fake-ip 让抓取工具拒绝了所有域名

一个抓取网页的工具在 Clash Verge 的 TUN 模式下全量报错:域名被解析成 198.18.x.x(RFC 2544 保留段),它的公网地址校验判定成内网地址。改 DNS 的 enhanced-mode 修好了主体,第二个坑在 AAAA 上。

网页抓取工具 `web_fetch` 在任意 URL 上都立即失败:

```plaintext
Error: 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 模式最容易碰到),以及被调用的工具会校验目标地址是不是公网地址。

这套做法在什么环境上验证过

```plaintext
CachyOS (Arch) | Clash Verge Rev(mihomo 内核)| TUN 模式 | 混合端口 7897
```

只在这一个组合上实测过。下面这些地方会因环境而异:

一、先看域名解析出来的是什么

```bash
getent 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 走的是两条路

同一个域名,两次查询结果不同:

```bash
node -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` 的顺序:

```bash
grep '^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 源:

```bash
resolvectl 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,拿到的是真实地址:

```bash
dig +short @127.0.0.1 example.com A
# 104.20.23.154
# 172.66.147.243
```

三、工具为什么拒绝

`web_fetch` 在发请求前会先解析主机名,再逐个校验地址范围:

```js
if (parsed instanceof ipaddr.IPv4) return parsed.range() === "unicast";
```

`ipaddr.js` 把 `198.18.0.0/15` 归类为 `reserved`:

```bash
node -e "const i=require('ipaddr.js');console.log(i.parse('198.18.0.64').range())"
# reserved
```

校验不通过就抛错。这是有意的 SSRF 防护,目的是不让抓取工具去访问回环、内网和链路本地这些地址。fake-ip 让所有公网域名都“看起来”是保留地址,于是全被拦下。

同一个文件里还有一条给代理留的分支:请求若被判为走代理,就不再本地解析、也不做地址钉扎,域名交给代理去解析。

```js
const 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)
```

四、三条修法

五、关掉 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` 末尾追加:

```yaml
dns:
  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 无关:

```bash
dig +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` 网卡在)的状态下实测:

八、自检命令

```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/
```

九、排错

参考