现象
用户操作:Settings → Enable DNS Mode(成功,TCC 弹窗确认)→ Disable DNS Mode(UI 立即显示 "DNS Mode: Off")→ 浏览器/系统任何域名都解析失败,必须手动到 System Settings → Network → Wi-Fi → DNS 把 127.0.0.1 删掉才能恢复上网。
复现路径:用户原始系统 DNS 是 DHCP-empty(即 networksetup -getdnsservers Wi-Fi 输出 There aren't any DNS Servers set on Wi-Fi)。这是绝大多数家用 Wi-Fi 用户的默认状态。
期望
Disable DNS Mode 后系统 DNS 应该回到 Disable 之前的状态。DhcpEmpty 的情况下应该回到 networksetup -getdnsservers Wi-Fi 输出为空(DHCP 自动获取)。
实际
Disable 返回 Ok(()) 后,networksetup -getdnsservers Wi-Fi 仍然输出 127.0.0.1。系统级 DNS 解析全部失败,浏览器无法访问任何域名。
根因分析(基于 fix/dns-cancel-149 当前代码 + 现场日志)
PR #148 (fix(#148), commit 5a0ebd3) + PR #149 (feat(#149), commit d06822c) 的 disable 路径在 DhcpEmpty 场景下漏了兜底。
platform.rs::disable_dns_mode() (~line 863-1006) 的分支
match is_expected_proxy_alive() {
true => {
// 等 proxy 自我清理(最多 5s),NO fallback
for _ in 0..50 { ... } // wait loop
return Ok(());
}
false => {
if interactive {
osascript_restore(original).await?; // 只有这条会调 networksetup
}
}
}
proxy 自我清理走的是 proxy.rs::restore_dns_and_exit()(~line 188-263),它会调 networksetup -setdnsservers Wi-Fi Empty(line 240-241)来还原 DhcpEmpty 状态。
但用户的现场日志显示:
- proxy 收到
shutdown signal(server.rs:230 打了 "DNS server received shutdown signal")
- 但
restore_dns_and_exit 没执行到 networksetup 那行
platform.rs::disable_dns_mode 返回 Ok(()) 之前也没走 osascript_restore 分支
也就是说 proxy 在收到 shutdown signal 后没能完成 restore_dns_and_exit,但 is_expected_proxy_alive() 在 5s 等待后返回了 false(proxy 确实死了),可 disable 还是返回 Ok(()) —— 因为 false 分支要么走 interactive=true 的 osascript 兜底(现场没打 osascript log),要么什么都不做(interactive=false)。两条分支都没成功还原 DNS。
链路 1: Proxy 还原失败 + interactive=false → 系统 DNS 泄漏
即使 is_expected_proxy_alive() 检测到 proxy 死掉,如果 interactive=false(disable IPC 默认值,从前端调用过来时),根本不会调 osascript 兜底。proxy 死了,restore 没完成,系统 DNS 卡在 127.0.0.1。
链路 2: Proxy 还原 "成功" 但 on-disk original 被污染
更隐蔽的是,即使 proxy 的 networksetup 调用成功了,下次 enable 也会失败:
1. 第一次 enable:DhcpEmpty,enable 不写 original.txt
2. disable:proxy 死了,restore 没跑
3. 系统 DNS = 127.0.0.1(脏)
4. 第二次 enable:capture_dns_state() 读到 Manual(["127.0.0.1"]) ← BUG
5. enable 把 127.0.0.1 当作 original 写到 manifest + original.txt
6. 下次 disable 时 proxy 调 networksetup -setdnsservers Wi-Fi 127.0.0.1
7. 等于 "还原" = "不变",但实际真正的原始 DHCP 状态已经永久丢失
capture_dns_state 必须过滤掉 127.0.0.1(这是 mHost 自己注入的,不是用户的 original)。
复现步骤
# 0. 准备:网络 DNS 是 DHCP-empty(绝大多数家用 Wi-Fi 默认状态)
networksetup -getdnsservers Wi-Fi
# 期望:There aren't any DNS Servers set on Wi-Fi
# 1. 启动
MHOST_AUTO_DNS=1 pnpm tauri dev
# (我自己加了 temp trigger:enable → wait → disable → wait → enable,
# 方便在没有 UI 自动化下复现。TRIGGER 在 src-tauri/src/lib.rs setup() 里,
# 提交前必须移除。)
# 2. 等 ~10s 让 3 个阶段都跑完
# 3. 验证(应该看到 Empty,但实际看到 127.0.0.1)
networksetup -getdnsservers Wi-Fi
# 实际:127.0.0.1
# 期望:There aren't any DNS Servers set on Wi-Fi
# 4. 验证 on-disk original 也被污染
cat "$HOME/Library/Application Support/mHost/.runtime/mhost-dns-original.txt"
# 实际:127.0.0.1
# 期望:空文件
提议修复(TBD — 等 issue 跟踪后决定方向)
最小修复(blocker):disable_dns_mode 在 is_expected_proxy_alive() == false 时永远走 osascript 兜底(不再受 interactive 限制),保证系统 DNS 一定被还原。
推荐修复:
disable_dns_mode 总在 proxy 死后做 networksetup -setdnsservers <iface> Empty 自检(不需要 sudo,因为 networksetup 不需要 root)。
capture_dns_state 过滤掉 127.0.0.1(这是 mHost 自身注入的 sentinel,永远不应该被当成用户 original)。
- 加 eprintln 到
proxy.rs::restore_dns_and_exit 的所有分支,确认它到底执行到哪一步(exit_reason)—— 这是定位「为什么 proxy 收到 signal 但 restore 没跑」的关键。
相关 issue / PR
环境
现象
用户操作:Settings → Enable DNS Mode(成功,TCC 弹窗确认)→ Disable DNS Mode(UI 立即显示 "DNS Mode: Off")→ 浏览器/系统任何域名都解析失败,必须手动到 System Settings → Network → Wi-Fi → DNS 把
127.0.0.1删掉才能恢复上网。复现路径:用户原始系统 DNS 是 DHCP-empty(即
networksetup -getdnsservers Wi-Fi输出There aren't any DNS Servers set on Wi-Fi)。这是绝大多数家用 Wi-Fi 用户的默认状态。期望
Disable DNS Mode 后系统 DNS 应该回到 Disable 之前的状态。DhcpEmpty 的情况下应该回到
networksetup -getdnsservers Wi-Fi输出为空(DHCP 自动获取)。实际
Disable 返回
Ok(())后,networksetup -getdnsservers Wi-Fi仍然输出127.0.0.1。系统级 DNS 解析全部失败,浏览器无法访问任何域名。根因分析(基于
fix/dns-cancel-149当前代码 + 现场日志)PR #148 (
fix(#148), commit5a0ebd3) + PR #149 (feat(#149), commitd06822c) 的 disable 路径在DhcpEmpty场景下漏了兜底。platform.rs::disable_dns_mode()(~line 863-1006) 的分支proxy 自我清理走的是
proxy.rs::restore_dns_and_exit()(~line 188-263),它会调networksetup -setdnsservers Wi-Fi Empty(line 240-241)来还原 DhcpEmpty 状态。但用户的现场日志显示:
shutdown signal(server.rs:230 打了 "DNS server received shutdown signal")restore_dns_and_exit没执行到 networksetup 那行platform.rs::disable_dns_mode返回Ok(())之前也没走osascript_restore分支也就是说 proxy 在收到 shutdown signal 后没能完成 restore_dns_and_exit,但
is_expected_proxy_alive()在 5s 等待后返回了false(proxy 确实死了),可 disable 还是返回Ok(())—— 因为false分支要么走interactive=true的 osascript 兜底(现场没打 osascript log),要么什么都不做(interactive=false)。两条分支都没成功还原 DNS。链路 1: Proxy 还原失败 + interactive=false → 系统 DNS 泄漏
即使
is_expected_proxy_alive()检测到 proxy 死掉,如果interactive=false(disable IPC 默认值,从前端调用过来时),根本不会调 osascript 兜底。proxy 死了,restore 没完成,系统 DNS 卡在 127.0.0.1。链路 2: Proxy 还原 "成功" 但 on-disk original 被污染
更隐蔽的是,即使 proxy 的 networksetup 调用成功了,下次 enable 也会失败:
capture_dns_state必须过滤掉127.0.0.1(这是 mHost 自己注入的,不是用户的 original)。复现步骤
提议修复(TBD — 等 issue 跟踪后决定方向)
最小修复(blocker):
disable_dns_mode在is_expected_proxy_alive() == false时永远走 osascript 兜底(不再受interactive限制),保证系统 DNS 一定被还原。推荐修复:
disable_dns_mode总在 proxy 死后做networksetup -setdnsservers <iface> Empty自检(不需要 sudo,因为 networksetup 不需要 root)。capture_dns_state过滤掉127.0.0.1(这是 mHost 自身注入的 sentinel,永远不应该被当成用户 original)。proxy.rs::restore_dns_and_exit的所有分支,确认它到底执行到哪一步(exit_reason)—— 这是定位「为什么 proxy 收到 signal 但 restore 没跑」的关键。相关 issue / PR
fix: enable_dns_mode must kill orphan proxy via sudo + add AppleScript trap) — 修了 enable 的 orphan 问题,但 disable 的还原路径还有 DhcpEmpty gapfeat: Settings cancel button + IPC-level abort signal for DNS enable/disable) — 加了 cancel,没动 disable 还原逻辑fix: DNS mode enable leaves orphan mhost-dns-proxy when osascript is cancelled) — 同源问题regression: enable DNS mode hangs, no authorization dialog appears) — 已 close 的同类问题(enable 端)环境
576bb03) 之前是好的,之后 PR fix: ship mhost-dns-proxy sidecar in .app + kill orphan proxy on enable + robust Ctrl+C exit #146 改了 disable 路径fix/dns-cancel-149@d06822c(HEAD)