Halo 友链抓取 "Failed to fetch URL"解决方法
[!NOTE] 折腾一晚上后复盘:友链插件抓取报
Failed to fetch URL,根子既不是 "路由器坏了",也不是 "对方站点挂了",而是 fake-ip 和 Java 的 DNS 解析器——两个平时根本不会想到一起的东西。
环境
| 组件 | 版本 / 说明 |
|---|---|
| 博客 | Halo 2.26(群晖 DS720+ Docker) |
| 外网 | EdgeOne |
| 网络 | OpenClash(Mihomo) + MosDNS,fake-ip 模式 |
现象
有些友链站抓取一直失败,但浏览器能开、容器里 curl 也大多能通。翻日志,真正的报错长这样:
Caused by: java.net.UnknownHostException: Failed to resolve [blog.xxx.top/<unresolved>:443]
[!WARNING] 看到
UnknownHostException就要警惕:这是解析失败,不是连接超时。Halo 的网络栈在 DNS 这一步就挂了,压根没发起 TCP 连接。看日志一定要看到Caused by这一层。
排查
第一层:fake-ip 没被接管
先在容器里查 DNS:
getent ahosts blog.xxx.top
198.18.0.121 STREAM blog.xxx.top
198.18.x.x 是 fake-ip 段。这个域名被 MosDNS 判为 "要走代理",OpenClash 就给了个假 IP。浏览器能开,是因为你设备的流量被 OpenClash 完整接管、fake-ip 有人转发;Halo 容器拿到 fake-ip 却没人管,直连黑洞,超时。
第二层:同一个 DNS,三种解析器态度完全不同
同一个域名,curl 能解析出 fake-ip,Java 却解析失败。做了个对照:用 Java 自带的 InetAddress.getAllByName() 解析同一个域名——成功,能拿到 fake-ip。所以 JDK 自带解析器没问题,出问题的是 Netty 自己的 DNS resolver(reactor-netty 不走 JDK 解析,自己实现了一套 DNS 客户端)。
| 解析器 | 实现 | 对 fake-ip 响应 |
|---|---|---|
| curl(libc) | 系统 getaddrinfo |
✅ 照单全收,交给 OpenClash 转发 |
JDK(InetAddress) |
JDK 内置解析器 | ✅ 接受,能拿到假 IP |
| Netty resolver | reactor-netty 自带 DNS 客户端 | ❌ 判 unresolved,抛 UnknownHostException |
[!IMPORTANT] 同一个 DNS 响应,curl(libc)、JDK、Netty 是三个完全独立的解析器实现,接受度完全不一样。fake-ip 这种响应形态,Java(Netty)吃不下。
第三层:换 DNS 也没用——全局劫持
既然怀疑 Netty 跟上游 DNS 不兼容,就想着给容器换 DNS。先把 Halo 从自定义网络挪到默认 bridge(Docker 对自定义网络强制注入内嵌 DNS 127.0.0.11,配 --dns 无效,bridge 下才生效),再把容器 DNS 指向 223.5.5.5。
结果很打脸:resolv.conf 明明写着 223.5.5.5,解析结果还是 fake-ip。查路由器 nftables 才看到真相:
table inet mosdns {
chain prerouting {
type nat hook prerouting priority dstnat + 5;
udp dport 53 counter redirect to :5335
}
}
[!CAUTION] 路由器上有一条全局 DNS 劫持:任何设备发出去的 UDP 53 查询,不管目标是谁,一律重定向到本地 MosDNS。所以给容器指哪儿的 DNS 都是白搭。
根因
把整条链捋直:
- 个人博客域名(
.top/.cloud/.online)不在国内规则库geosite:cn里; - MosDNS 把它当未知/境外,交给 clash,返回 fake-ip;
- Halo 的 Java 栈(Netty resolver)解析不了 fake-ip 响应 →
UnknownHostException。
[!TIP] 白名单为什么有效?因为它本质上是把域名手动并入 geosite_cn,走本地 DNS 真实解析、返回真实 IP——真实 IP 的响应 Java 完全吃得下。所以问题从来不是 "站点不通",而是 "fake-ip 这种响应形态 Java 吃不下"。
解决:给 MosDNS 加"按 IP 归属自动分流"
改动集中在一个文件:路由器上的 /etc/mosdns/config_custom.yaml。
核心思路:凡是没被任何域名规则认领的查询,先做一次真实解析,看服务器 IP 在不在中国大陆——在,就返回真实 IP 走直连;不在,才交给 clash 发 fake-ip 走代理。
分四步,每步都能看懂:
第一步:加 "中国大陆 IP 集合"(数据源每天自动更新)
- tag: geoip_cn
type: ip_set
args:
files:
- "/var/mosdns/geoip_cn.txt"
第二步:加 "探测序列"
- tag: cn_probe_sequence
type: sequence
args:
- matches: qname $remote_domain
exec: return
- exec: $forward_local # 223.5.5.5 / 119.29.29.29 真实解析
- matches:
- has_resp
- resp_ip $geoip_cn
exec: accept
- exec: drop_resp
第三步:挂进主流程(在 "国内域名判断完 → 交给 clash" 之间插两行)
- exec: $query_is_local_domain
- exec: jump has_resp_sequence
- exec: jump cn_probe_sequence # 新增:先探真实 IP
- exec: jump has_resp_sequence # 新增:探到(国内)就结束,没探到继续
- exec: $fallback # 兜底:才走 clash fake-ip 代理
第四步:重启验证
/etc/init.d/mosdns restart
# 解析一个之前必然失败的域名,应看到真实 IP 而非 198.18.x.x
getent ahosts blog.xxx.top
另外在 OpenClash 规则里加一条 GEOIP,CN,DIRECT(放在 MATCH 之前),给 curl 这类系统请求兜底:拿到 fake-ip 被劫持进 clash 后,按真实 IP 一查是国内就直接直连,不用再绕香港节点。
效果
| 改之前 | 改之后 | |
|---|---|---|
| 解析结果 | fake-ip | 真实 IP |
| 抓取耗时 | 8 秒超时 | 0.3 秒直连 |
境内友链站从此全自动,再也不用加白名单;需要手动打点的只剩偶尔冒出来的境外可直连站。
剩一个尾巴:境外但国内能直连的站
这套逻辑有一个绕不开的边界:服务器在境外的站(套 Cloudflare 的、放海外 VPS 的博客),MosDNS 照常发 fake-ip,Halo 依然抓不了。这种站很少、也基本不变,放回 "真 IP 名单" 即可:
echo "tsoo.net" >> /etc/mosdns/rule/whitelist.txt
echo "transformjpg.cloud" >> /etc/mosdns/rule/whitelist.txt
/etc/init.d/mosdns restart
[!NOTE] 折腾一晚上的收获,其实就三条:①
UnknownHostException和 timeout 是两种病;② curl 通不代表 Java 通,同一个 DNS 响应不同解析器接受度不一样;③ 白名单不是目的,它只是 "并入国内名单走真实解析" 的快捷方式——理解了这点才能找到自动化的替代。
环境:Halo 2.26 · 群晖 DS720+ Docker · OpenClash(Mihomo) · MosDNS v5.3 · iStoreOS

