Skip to content

修复:macOS 网络切换与唤醒后恢复系统 DNS 和 TUN - #334

Closed
uclort wants to merge 2 commits into
appshubcc:mainfrom
uclort:codex/fix-macos-dns-network-recovery
Closed

修复:macOS 网络切换与唤醒后恢复系统 DNS 和 TUN#334
uclort wants to merge 2 commits into
appshubcc:mainfrom
uclort:codex/fix-macos-dns-network-recovery

Conversation

@uclort

@uclort uclort commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

背景

这是对 #319 的后续修复。

上游已通过 00bd2157 采纳了原 #320 中的系统 DNS 备份/恢复、退出恢复以及 TUN/总开关联动逻辑。本 PR 基于当前最新 maine553fe2b)重新整理,不重复提交或描述这些已合入内容,只处理 macOS 在运行期间发生网络环境变化时的恢复问题。

典型场景均已脱敏:

  • 从 Wi-Fi「网络 A」切换到同类型的 Wi-Fi「网络 B」后,系统 DNS 虽然仍显示 223.5.5.5,但部分 DIRECT 请求持续 DNS 超时;重新切换 TUN 后恢复。
  • 电脑锁屏或休眠后唤醒,代理流量可能正常,但部分 DIRECT 域名出现 DNS 超时;重新切换 TUN 后恢复。
  • 覆盖安装并重新授权辅助工具后,总开关与 TUN 设置均显示开启,但数据面未完整恢复;必须再次切换 TUN 才能联网。
  • 网络恢复进行期间执行停止或退出,旧恢复任务可能与停止流程竞争,导致开关长时间不可操作或旧任务重新启用 TUN。

相关修复版本已在真实使用环境中持续验证 2~3 天,上述场景未再次复现。

与此前提交的关系

#320 虽然处于关闭状态,但维护者已通过 00bd2157 合入等价实现。本 PR 已排除其中的 DNS 备份、恢复、退出清理和开关联动代码,不会重复提交或重复描述已采纳内容。

源代码存在的问题与修复

1. 同类型 Wi-Fi 切换可能没有进入恢复回调

源代码使用:

Connectivity().onConnectivityChanged.listen(...)

connectivity_plus 的公开流会对连接类型列表执行去重。Wi-Fi「网络 A」切换到 Wi-Fi「网络 B」时,前后类型都可能仍为 [wifi],事件因而被过滤,回调内的固定延迟和 updateDns() 根本不会执行。

macOS 新逻辑改为监听平台原始事件,其他平台仍保留原实现:

final connectivityStream = Platform.isMacOS
    ? ConnectivityPlatform.instance.onConnectivityChanged
    : Connectivity().onConnectivityChanged;

这项改动只绕过 macOS 上的 Dart 层去重,不改变 Android、Windows、Linux 等平台的事件语义。

2. 固定等待 1 秒无法确认 DHCP、默认路由和网络服务已经稳定

源代码在收到事件后固定等待 1 秒,再按当时的默认路由更新 DNS。网络切换过程中,这一时刻仍可能读到旧接口、旧网关或未完成的 DHCP 信息。

新逻辑读取并比较两次默认网络指纹:

final fingerprint = [
  route.device,
  route.gateway,
  serviceName,
  address,
  connectionId,
  dhcpServer,
  dhcpDns,
].join('|');
await macOS?.waitForStableDefaultNetwork(
  isCancelled: () => scheduledGeneration != currentGeneration,
);

只有连续采样一致后才优先执行恢复;最多重试 5 次,并允许更新的网络事件取消旧任务。这样既能识别同一接口上的 Wi-Fi/租约变化,也避免把 DNS 写入刚刚失效的网络服务。

3. 系统 DNS 需要从旧服务迁移到当前服务

原有备份/恢复能力已经由上游实现,但网络切换时 _setDns() 会再次自行查找默认服务,恢复流程与写入流程之间若默认路由继续变化,可能操作不同服务。

新逻辑把稳定采样得到的服务名传入同一 DNS 临界区:

await macOS?.updateDns(
  !shouldSetSystemDns,
  serviceName: networkState.serviceName,
);

启用自动系统 DNS 时,当前服务只写入 Bettbox 接管地址:

networksetup -setdnsservers <当前服务> 223.5.5.5

原始 DNS 仍沿用上游现有逻辑持久化备份,并在停用、退出或迁移服务时恢复。这样避免 DHCP DNS 排在 223.5.5.5 前面绕过接管,同时没有改变关闭 Bettbox 后由系统自动获取 DNS 的行为。

4. 只刷新 DNS/连接无法修复失效的 TUN 数据面

源代码在网络变化后主要关闭连接并重写系统 DNS。实际故障中,Mihomo 的 TUN listener、默认出口和路由仍可能保留切换前状态,因此会出现“系统 DNS 正确,但必须手动切换 TUN 才恢复”的现象。

新逻辑在 Bettbox 已启动且 TUN 已开启时执行一次受控重建:

await rebuildMacOSTunListener(
  disableTun: () => _applyCoreTunConfig(false, persist: false),
  stopListener: clashCore.stopListener,
  repairNetwork: repairNetwork,
  startListener: clashCore.startListener,
  enableTun: _updateClashConfig,
);

恢复顺序为:临时关闭 Core TUN → 停止 listener → 关闭旧连接并迁移系统 DNS → 清理 Core DNS/Fake-IP 缓存 → 重启 listener → 重新应用 TUN。临时关闭不会写入用户偏好,因此不会把恢复过程误保存为用户主动关闭 TUN。

5. 覆盖安装并授权辅助工具后的 TUN 启动顺序不完整

源代码可能先进入全局启动流程,再异步应用 TUN;如果授权过程要求重启 Core,listener 和配置应用之间会出现时序窗口,界面状态可能已开启,但 TUN 数据面没有完整建立。

新逻辑把 macOS 启动调整为原子顺序:

授权辅助工具
→ 必要时重启 Core
→ 启动 listener
→ 应用 TUN 配置
→ 更新运行状态

任一步失败都会显式中止;若应用 TUN 失败,会停止刚启动的 listener,避免留下半启动状态。非 macOS 的启动路径保持上游行为。

6. macOS 唤醒不一定产生可用的网络类型事件

锁屏/休眠后,网络类型和默认网络指纹都可能与休眠前相同,但底层 socket、路由或 TUN 状态已经失效。仅监听 connectivity 事件无法可靠覆盖这一场景。

本 PR 在 macOS 原生层监听:

NSWorkspace.didWakeNotification

事件通过现有 app MethodChannel 发送到 Dart,并强制执行一次稳定检测与恢复;Swift 观察者在窗口释放时同步注销。该桥接仅存在于 macOS Runner。

7. 新网络事件与手动停止不能共用同一种取消语义

网络连续变化可能排队多个恢复任务;若用户主动停止或退出,较旧任务不应在停止完成后重新开启 listener/TUN。源代码最初使用同一个 recoveryCancelled() 同时表示“出现了更新的网络事件”和“用户主动停止/退出”,并把它传给 TUN 重建的 finally

唤醒恢复会主动关闭旧 TUN,这个动作本身也可能产生 connectivity 事件。新事件递增调度 generation 后,正在执行的恢复会被判定为过期;旧实现随后跳过 startListener() 和重新启用 TUN,可能留下“旧 TUN 已关闭、新 TUN 未启动”的半完成状态。此时日志可出现 batch read packet: bad file descriptor,界面仍显示运行,但实际流量需要重新加载 Core 才能恢复。

修复将取消条件拆为两级:

bool lifecycleCancelled() {
  return recoveryGeneration != _macOSNetworkRecoveryGeneration;
}

bool recoveryCancelled() {
  return lifecycleCancelled() || isCancelled?.call() == true;
}
  • 更新的网络事件仍会使旧任务结果失效,并由最新任务继续协调网络;但只要 TUN teardown 已经开始,finally 必须先恢复 listener/TUN,不能把数据面留在关闭状态。
  • 只有用户主动停止或退出导致的 lifecycle cancellation,才允许 finally 保持 listener/TUN 关闭。
isLifecycleCancelled: lifecycleCancelled,

这样既保留“手动停止优先于后台恢复”的原设计,也避免唤醒恢复被自身产生的 connectivity 事件打断。

平台边界与明确排除项

  • 网络稳定检测、系统 DNS 服务迁移、TUN 重建和唤醒恢复均通过 macOS 平台判断进入。
  • 非 macOS 平台继续使用原 connectivity 流和原启动流程。
  • 不修改或注入 proxy-server-nameserver;此前相关现象已确认来自用户配置,不属于 Bettbox 代码问题。
  • 不监控配置引用的辅助网卡,也不加入 DHCP DNS fallback;相关现象最终由企业网络认证状态解释,不能作为通用代码修复依据。
  • 不回移 Mihomo Core 的 DNS/sniffer 补丁。

验证

  • flutter analyze:通过,无问题。
  • flutter test:20/20 通过。
  • 新增测试覆盖:同类型网络去重后的恢复判定、唤醒强制恢复、过期任务取消、macOS TUN 原子启动、listener 重建失败回滚、更新网络事件到达后仍恢复 TUN、手动停止优先级及原生唤醒事件转发。
  • 自定义构建版本已在真实网络切换、锁屏/唤醒和覆盖安装场景中持续使用 2~3 天,原问题未再次复现。

Follow-up to #319

@uclort

uclort commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

macOS 系统相关问题未修复,继续观察,稳定后重新提交 PR。

@uclort uclort closed this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant