环境
- 设备:iPhone (iOS 18.x)
- App 构建:0920-0459;core 日志显示
tunnel started core=1.19.31
- 同一订阅在 Clash Verge (macOS) 和 ClashMi (iOS) 上均正常
问题描述
订阅使用了常见的"自引用本地 DNS"写法(机场配置,用户无法修改)。此处仅展示与问题相关的字段,上游解析器已省略:
dns:
enable: true
listen: '0.0.0.0:7874'
enhanced-mode: fake-ip
proxy-server-nameserver: ['udp://127.0.0.1:7874']
# nameserver / default-nameserver / nameserver-policy / fake-ip-filter:配置中存在,此处省略
在 iOS 版 Clash (Hako) 上,隧道启动并显示已连接,但所有出站连接都失败,原因是代理节点自身的域名无法解析。日志反复出现:
connect error: dns resolve failed: couldn't find ip
失败以每 5 秒一批的节奏重复,与 DNS 客户端超时一致,即查询是被静默黑洞而非被拒绝。同一份订阅在 Clash Verge (macOS) 和 ClashMi (iOS) 上不做任何修改即可正常使用。
根因分析(基于公开源码阅读)
我在公开仓库(commit 35c9bb6)中定位到了确切的缺口:
dns/server.go:103 按配置原样绑定 DNS UDP 服务——本订阅是通配地址监听(dns.listen: 0.0.0.0:7874),而 bind/hako/config_pipeline.go 明确按原文保留 dns.listen。
- 在 NE socket scope 下,
bind/hako/listener_scope_darwin.go 通过两种方式修复入站监听器的回环可达性:地址显式为回环的监听器被绑定到回环接口(listenerScopeControl);通配 TCP 监听器会获得回环伴生监听器(listenerLoopbackCompanion)。通配 UDP 两者都得不到——文件头明确记录:"Wildcard UDP keeps today's behavior -- the loopback face of a wildcard packet conn is a recorded gap, not a silent one."
- 出站方向已经修复:发往
127.0.0.1:7874 的查询会被豁免接口绑定(bind/hako/hook.go 的 interfaceScopableTarget;component/dialer/dialer.go 的 peer 豁免),所以数据包确实能到达回环接口。
- 最终效果:
proxy-server-nameserver: udp://127.0.0.1:7874 的查询被送到一个在 extension scope 下对回环"失聪"的通配 UDP socket。查询在 5 秒后超时(dns/client.go),Resolver.LookupIP 返回 ErrIPNotFound("couldn't find ip"),随后所有节点拨号都以 dns resolve failed: couldn't find ip 失败——与我的日志完全吻合,包括 5 秒的重试节奏。
值得注意的是,同样的配置若写成 dns.listen: 127.0.0.1:7874 今天是可以工作的(该监听器形态会被绑定到回环接口)。失败的形态专门是通配 listen——而这正是面向桌面的订阅普遍采用的写法。这也与 bind/hako/hook.go 中的注释一致:127.0.0.1 形态已被处理,0.0.0.0 形态似乎就是剩余的那个已记录缺口。
其他与此解读吻合的日志证据:
[Apple] inbound listeners run under the Network Extension socket scope; ...——证明 listener-scope 层在此构建中已激活。
- 没有
Start DNS server(UDP) error——通配绑定本身成功,监听器只是听不到回环。
- 已上报的 deviations:
allow-lan(stripped)、store-fake-ip(forced true)、19 条 PROCESS-NAME 规则(unavailable)。proxy-server-nameserver 未被上报为 stripped——与它被原样保留一致。
期望行为
希望 Clash (Hako) 能像桌面客户端和 ClashMi 一样处理 proxy-server-nameserver 指向自身 dns.listen 的配置。可能的修复方向:
- 将回环伴生监听器机制扩展到通配 UDP packet conn;或
- 检测
proxy-server-nameserver 指向所配置的 dns.listen 端口(自引用)的条目,改为路由到进程内 resolver,而不是走回环 socket 跳一跳;或
- 至少为这种配置形态发布 deviation/诊断提示(就像
allow-lan / store-fake-ip 那样)。
复现步骤
- 导入包含上述 DNS 配置的订阅
- 连接
- 日志中反复出现
dns resolve failed: couldn't find ip;无任何流量通过
补充说明
- 在 App 中开启"覆盖 DNS"并手动指定公共解析器可以绕过(workaround),同时说明配置其余部分没有问题。
- 日志中的 Apple deviations(
store-fake-ip 强制为 true、allow-lan 被剥离、PROCESS-NAME 规则不可用)看起来都是预期行为,不是本问题的原因。
日志(已脱敏,时间统一为 +08:00)
===== core =====
23:48:12.204 INFO [Memory] GC pacing armed: GOMEMLIMIT=39321600 (NE budget default; no explicit limit was configured)
23:48:12.208 INFO [Apple] inbound listeners run under the Network Extension socket scope; loopback faces will be explicitly bound to the loopback interface
23:48:12.214 WARNING [Apple] default path en0 (index=16) resolves `system` nameservers through [<路由器IPv6-DNS>]:53, 192.168.50.1:53, read from this network's scoped resolvers
23:48:12.215 INFO [Apple] default path -> en0 (index=16 expensive=false constrained=false ipv4=true ipv6=true)
23:48:12.321 INFO [Apple] allow-lan permitted=false
23:48:12.323 WARNING [Apple iosPacketTunnel] 19 PROCESS-NAME rules, first at rules[1286]: the owner metadata it tests cannot be resolved in this profile; the rule is kept and evaluated against empty metadata, exactly as with find-process-mode off
23:48:12.472 INFO Initial configuration complete, total time: 96ms
23:48:12.516 INFO [Apple] deviations published: entry=start seq=1 document=535407B sha256=3a4999d88613319f rows=3
23:48:12.517 WARNING [Apple] deviation allow-lan: given="false" effective="the configured listener stays on 127.0.0.1 instead of every interface" category=stripped -- exposing the device to the local network is a decision the person holding it makes, not one an imported subscription makes for them; the app has not recorded that agreement yet
23:48:12.518 WARNING [Apple] deviation profile.store-fake-ip: given="not set (core default: false)" effective="true: fake-ip mappings are written to disk so they survive an extension restart" category=forced
23:48:12.519 WARNING [Apple] deviation rules: given="19 rule(s), first at rules[1286]" effective="these rules never match on this platform; traffic falls through to the next rule" category=unavailable -- the metadata they test is not available to an Apple packet tunnel, so the comparison never succeeds
23:48:12.520 INFO [mem] after parsing the configuration: 18.2 MiB of the extension's budget
23:48:12.521 INFO [mem] before applying it (providers load here): 18.2 MiB of the extension's budget
(约 17 秒无日志——providers 加载中;期间无任何 DNS 监听器报错)
23:48:29.128 WARNING [TCP] dial 📢 <策略组A> (match Domain/mtalk.google.com) 198.18.0.1:57579 --> mtalk.google.com:443 error: <节点域名>:12011 connect error: dns resolve failed: couldn't find ip
23:48:29.128 WARNING [TCP] dial <策略组B> (match DomainKeyword/google) 198.18.0.1:64725 --> www.google.com:443 error: <节点域名>:12011 connect error: dns resolve failed: couldn't find ip
23:48:29.128 WARNING [UDP] dial <策略组B> (match DomainKeyword/google) 198.18.0.1:61382 --> www.google.com:443 error: couldn't find ip
23:48:29.128 WARNING [TCP] dial <策略组B> (match DomainKeyword/google) 198.18.0.1:57586 --> www.google.com:443 error: <节点域名>:12011 connect error: dns resolve failed: couldn't find ip
23:48:34.136 WARNING [TCP] dial 📢 <策略组A> (match Domain/mtalk.google.com) 198.18.0.1:57587 --> mtalk.google.com:443 error: <节点域名>:12011 connect error: dns resolve failed: couldn't find ip
23:48:34.136 WARNING [UDP] dial <策略组B> (match DomainKeyword/google) 198.18.0.1:64533 --> optimizationguide-pa.googleapis.com:443 error: couldn't find ip
23:48:39.140 WARNING [TCP] dial <策略组B> (match DomainKeyword/google) 198.18.0.1:57590 --> optimizationguide-pa.googleapis.com:443 error: <节点域名>:12011 connect error: dns resolve failed: couldn't find ip
23:48:44.140 WARNING [TCP] dial <策略组B> (match DomainKeyword/google) 198.18.0.1:64727 --> play.googleapis.com:443 error: <节点域名>:12011 connect error: dns resolve failed: couldn't find ip
(此后每 5 秒重复同样的 couldn't find ip,持续到断开连接,此处省略)
===== app =====
23:48:11.824 INFO activation build=0920-0459 total=196ms providers=30ms(n=0 reused=0 fetched=0 from=own(1)) finalize=66ms preflight=56ms geosite=43ms
23:48:12.107 WARNING vpn status changed to=connecting
23:48:12.198 INFO system resolvers before the tunnel: <路由器IPv6-DNS> 192.168.50.1
23:48:12.201 WARNING allow-lan permitted=false
23:48:12.218 INFO span proxies.offline 116.3ms nodes=328
23:48:12.522 INFO tunnel opened from pre-applied settings fd=11 bridge=none
23:48:12.630 INFO tunnel started core=1.19.31 status=running
23:48:12.632 WARNING vpn status changed to=connected
23:48:12.634 INFO widget: published connected group=<策略组B> reason=start reloads=true in 3 ms up=0 down=0 proxy=0/0 direct=0/0 reject=0 conns=0/0 egress=🇯🇵 <日本节点> members=16
另一次会话的日志中还有这条汇总诊断:
[Apple] 20 outbound connections in a row failed and none succeeded: ... dns resolve failed: couldn't find ip. the tunnel reports itself connected, so this is happening after the tunnel came up
补充一个交叉引用:#3 中官方评论(2026-08-27)曾描述过这个配置形态——"把 dns.proxy-server-nameserver 指向核自己的 DNS 监听地址……主机名就会拿到 198.18.x.x 而超时",并称"这一点上游 mihomo 的行为完全相同,我们实测比对过"。
两点与本 issue 的实测不一致,供参考:
- 同一份订阅在 Clash Verge (macOS) 和 ClashMi (iOS) 上均正常——至少在 iOS 的
dns.listen: 0.0.0.0 场景下,Hako 的行为与上游/其他客户端并不一致。
- 本 issue 的失败模式不是"拿到 fake-ip 198.18.x.x",而是 ErrIPNotFound(couldn't find ip,约 5 秒超时的静默黑洞)——对应 listener_scope_darwin.go 文件头记录的通配 UDP packet conn 无回环面的缺口(出站方向已被 hook.go 豁免修复,包能送到 lo0,但通配 UDP 监听器听不到)。
补充一个可参考的先例:Clash Verge Rev 在客户端层把 proxy-server-nameserver / proxy-server-nameserver-policy / nameserver-policy 定义为 "provider DNS fields"
(https://github.com/clash-verge-rev/clash-verge-rev/blob/ff1ce92/src-tauri/src/config/dns.rs#L18),其"DNS 覆写"开启时会用自身配置替换这些字段(默认模板将 proxy-server-nameserver 置空,https://github.com/clash-verge-rev/clash-verge-rev/blob/ff1ce92/src-tauri/src/utils/init.rs#L387),
并在检测到 profile 携带这些字段而用户未确认时自动禁用覆写并弹 dns_override::auto_disabled 提示(同 dns.rs 文件内 DnsOverrideState)。
感谢!
环境
tunnel started core=1.19.31问题描述
订阅使用了常见的"自引用本地 DNS"写法(机场配置,用户无法修改)。此处仅展示与问题相关的字段,上游解析器已省略:
在 iOS 版 Clash (Hako) 上,隧道启动并显示已连接,但所有出站连接都失败,原因是代理节点自身的域名无法解析。日志反复出现:
失败以每 5 秒一批的节奏重复,与 DNS 客户端超时一致,即查询是被静默黑洞而非被拒绝。同一份订阅在 Clash Verge (macOS) 和 ClashMi (iOS) 上不做任何修改即可正常使用。
根因分析(基于公开源码阅读)
我在公开仓库(commit
35c9bb6)中定位到了确切的缺口:dns/server.go:103按配置原样绑定 DNS UDP 服务——本订阅是通配地址监听(dns.listen: 0.0.0.0:7874),而bind/hako/config_pipeline.go明确按原文保留dns.listen。bind/hako/listener_scope_darwin.go通过两种方式修复入站监听器的回环可达性:地址显式为回环的监听器被绑定到回环接口(listenerScopeControl);通配 TCP 监听器会获得回环伴生监听器(listenerLoopbackCompanion)。通配 UDP 两者都得不到——文件头明确记录:"Wildcard UDP keeps today's behavior -- the loopback face of a wildcard packet conn is a recorded gap, not a silent one."127.0.0.1:7874的查询会被豁免接口绑定(bind/hako/hook.go的interfaceScopableTarget;component/dialer/dialer.go的 peer 豁免),所以数据包确实能到达回环接口。proxy-server-nameserver: udp://127.0.0.1:7874的查询被送到一个在 extension scope 下对回环"失聪"的通配 UDP socket。查询在 5 秒后超时(dns/client.go),Resolver.LookupIP返回ErrIPNotFound("couldn't find ip"),随后所有节点拨号都以dns resolve failed: couldn't find ip失败——与我的日志完全吻合,包括 5 秒的重试节奏。值得注意的是,同样的配置若写成
dns.listen: 127.0.0.1:7874今天是可以工作的(该监听器形态会被绑定到回环接口)。失败的形态专门是通配 listen——而这正是面向桌面的订阅普遍采用的写法。这也与bind/hako/hook.go中的注释一致:127.0.0.1形态已被处理,0.0.0.0形态似乎就是剩余的那个已记录缺口。其他与此解读吻合的日志证据:
[Apple] inbound listeners run under the Network Extension socket scope; ...——证明 listener-scope 层在此构建中已激活。Start DNS server(UDP) error——通配绑定本身成功,监听器只是听不到回环。allow-lan(stripped)、store-fake-ip(forced true)、19 条PROCESS-NAME规则(unavailable)。proxy-server-nameserver未被上报为 stripped——与它被原样保留一致。期望行为
希望 Clash (Hako) 能像桌面客户端和 ClashMi 一样处理
proxy-server-nameserver指向自身dns.listen的配置。可能的修复方向:proxy-server-nameserver指向所配置的dns.listen端口(自引用)的条目,改为路由到进程内 resolver,而不是走回环 socket 跳一跳;或allow-lan/store-fake-ip那样)。复现步骤
dns resolve failed: couldn't find ip;无任何流量通过补充说明
store-fake-ip强制为 true、allow-lan被剥离、PROCESS-NAME规则不可用)看起来都是预期行为,不是本问题的原因。日志(已脱敏,时间统一为 +08:00)
另一次会话的日志中还有这条汇总诊断:
补充一个交叉引用:#3 中官方评论(2026-08-27)曾描述过这个配置形态——"把 dns.proxy-server-nameserver 指向核自己的 DNS 监听地址……主机名就会拿到 198.18.x.x 而超时",并称"这一点上游 mihomo 的行为完全相同,我们实测比对过"。
两点与本 issue 的实测不一致,供参考:
dns.listen: 0.0.0.0场景下,Hako 的行为与上游/其他客户端并不一致。补充一个可参考的先例:Clash Verge Rev 在客户端层把 proxy-server-nameserver / proxy-server-nameserver-policy / nameserver-policy 定义为 "provider DNS fields"
(https://github.com/clash-verge-rev/clash-verge-rev/blob/ff1ce92/src-tauri/src/config/dns.rs#L18),其"DNS 覆写"开启时会用自身配置替换这些字段(默认模板将 proxy-server-nameserver 置空,https://github.com/clash-verge-rev/clash-verge-rev/blob/ff1ce92/src-tauri/src/utils/init.rs#L387),
并在检测到 profile 携带这些字段而用户未确认时自动禁用覆写并弹 dns_override::auto_disabled 提示(同 dns.rs 文件内 DnsOverrideState)。
感谢!