Skip to content

[iOS][功耗] NEProvider wake 无条件重置全部 DNS transport,频繁 sleep/wake 时破坏连接复用 #5

Description

@wangwei354

环境

  • 客户端测试版本:1.0.4
  • 平台:iOS
  • 场景:VPN 保持连接,设备夜间闲置
  • 观测日期:2026-09-09(北京时间,UTC+8)

本次没有确认 1.0.4 安装包与当前公开源码的精确提交映射。下方源码链接用于说明与设备日志吻合的当前实现路径:Hako 35c9bb675f1cf5b0befd4290809d06dead728768,Hako-Client b05832246fcac69d13ff16df871a8d53fd394c10

问题概述

Hako 将每次 NEPacketTunnelProvider.wake() 转发给 BoxService.Wake()。该方法在没有确认物理网络路径变化、DNS transport 失效或查询失败的情况下,无条件执行一次全局 resolver.ResetConnection()

在本次 1.0.4 夜间测试中,iOS 高频调用 sleep/wake。通用 wake 因此可能反复关闭 DoH、DoH3、DoQ 和 DoT 等可复用 DNS transport,使后续查询重新建连或握手。该问题独立于另一个“wake 无条件触发全量健康检查”的问题:即使健康检查恢复逻辑修正,这里的 DNS Reset 仍会执行。

这里报告的是“Hako 对系统 wake 的处理方式”,不主张最初的 iOS 系统唤醒由 Hako 产生。

设备日志证据

按北京时间分析 2026-09-09 00:00–06:00

  • 01:01:14–05:59:57 共记录 2146 组 provider sleep/wake
  • 平均每小时约 431 次,平均每 8.35 秒一组。
  • wake → 下一次 sleep 中位数约 3.865 秒。
  • sleep → wake 中位数约 2.278 秒。
  • 05:23–06:00 保留了完整 core 证据:279 次 wake,其中 275 次 wake 都立即触发了后台健康检查批次,说明该区间核心服务处于运行状态。
  • 同一 05:23–06:00 区间没有记录 [Apple] default path -> ... 路径更新日志,但通用 wake 仍持续发生。

resolver.ResetConnection() 本身没有事件日志,因此当前不能从设备日志计算实际关闭了多少条 DNS 连接。根据确定的调用链,在服务存活时,每次上述 wake 都会发起一次全局 DNS Reset。脱敏 wake 样本见 issue-sample-ios-wake-healthcheck-storm-1.0.4.log

issue-sample-ios-wake-healthcheck-storm-1.0.4.log

与当前源码吻合的调用链

  1. Hako-Client 的 PacketTunnelProvider.wake() 将系统 wake 交给 ExtensionProvider
  2. ExtensionProvider.wake() 调用 currentService?.wake()
  3. BoxService.Wake() 先执行 pause.DeviceWake(),随后无条件调用 resolver.ResetConnection()
  4. component/resolver.ResetConnection() 遍历已配置 resolver,并异步调用各 resolver 的 ResetConnection。
  5. DNS resolver 会去重同一次调用中共享的 raw transport,然后对每个 transport 执行 ResetConnection。
  6. dnsOverHTTPS.ResetConnection() 关闭 idle HTTP 连接和 HTTP/2 连接,并清空当前 client。
  7. dnsOverQUIC.ResetConnection() 关闭当前 QUIC 连接。
  8. dnsOverTLS.ResetConnection() 清空并关闭可复用连接池。

当前 BoxService.Wake() 附近的源码注释已经指出:一次 sleep/wake 并不能证明连接已经失效,实际物理路径变化由独立 path monitor 处理;但方法末尾仍保留了无条件 DNS Reset。

为什么通用 wake 不足以作为 DNS Reset 条件

NEProvider.wake() 表示系统从睡眠状态恢复,不等同于 Wi-Fi、蜂窝接口、源地址、网关或 DNS 上游发生变化。短暂 sleep/wake 期间,原有 DNS transport 可能仍然有效。

当前 Hako 已有独立的 Apple 默认路径监控:

  • 对应用的 path update 更新物理网络能力和绑定接口;
  • 在认为旧 DNS socket 可能属于失效路径时清除 resolver cache 并重置连接;
  • 对真实路径变化保留必要的 DNS 恢复能力。

因此,通用 wake 再执行一次全局 Reset 并不能提供更精确的失效判断,却会覆盖没有路径变化的短暂唤醒场景。

可能影响

实际影响取决于生效 DNS 配置和当时是否存在已建立 transport:

  • DoH/HTTP2、DoH3、DoQ 或 DoT 的空闲连接被提前关闭;
  • 下一次 DNS 查询需要重新建立 TCP、TLS 或 QUIC 会话;
  • 证书验证、密钥交换、网络往返和无线活动增加;
  • 高频异步 Reset 可能与查询或刚刚重建的 transport 交错;
  • 首次查询延迟增加,弱网下更容易暴露重连或超时成本。

如果使用无连接复用的 DNS 路径,或者当时没有已建立 transport,实际影响会较小。当前证据没有测得真实关闭次数、重握手次数或单项耗电比例。

期望行为

  • 通用 DeviceWake 不应自动等同于 DNS 网络路径失效。
  • DNS transport 应在物理路径更新、明确连接错误、配置更新或服务关闭时重置。
  • 短暂 sleep/wake 后优先复用仍然可用的 transport;若复用失败,再走现有重连逻辑。
  • 高频 wake 应被合并或限频,不能重复对同一 transport 发起无条件全局 Reset。

建议的最小改进方向

  1. BoxService.Wake() 移除无条件 resolver.ResetConnection(),由现有 Apple path monitor 负责路径变化时的失效处理。
  2. 如果仍需要处理长时间睡眠后的 NAT 或连接失效,可先尝试复用,在真实 I/O 错误后重建;或者仅对长睡眠设置有最小间隔的条件性 Reset。
  3. 增加低频诊断计数:wake 次数、wake-triggered DNS Reset 次数、实际关闭 transport 数、因失败而重建的 transport 数。不要为每条连接增加高频 info 日志。
  4. 避免多个异步 Reset 与 transport 重建交错;如保留 Reset,应保证同类请求合并并具有 generation 边界。

建议验证

  1. 分别使用 DoH/HTTP2、DoH3、DoQ、DoT 和普通 UDP DNS 测试。
  2. 在物理路径不变的锁屏 sleep/wake 场景中,比较当前实现与移除 wake Reset 后的连接复用、查询延迟、握手数和扩展 CPU 时间。
  3. 覆盖 Wi-Fi/蜂窝切换、同接口漫游、热点切换、DNS server 失效和长时间睡眠后的首次查询,确认必要重连仍可靠。
  4. 验证正在进行的 DNS 查询不会因 wake Reset 获得伪造 SERVFAIL,连续多次 wake 不会关闭刚重建的 transport。
  5. 用 sysdiagnose、网络抓包或新增的低频计数确认实际 Reset 与重握手数量,再评估整机电量差异。

边界

  • 当前日志证明 wake 高频发生,并且源码在服务存活时无条件调用 DNS Reset;日志没有直接记录每次 transport 关闭。
  • 当前不能确定最初是什么事件唤醒了 iOS。
  • 不能把一次 Reset 调用直接换算成一次新握手或一次硬件唤醒。
  • 当前没有证据承诺固定节电比例。
  • 本问题建议提交到 Hako 内核项目;Hako-Client 只负责转发系统生命周期回调。

issue-sample-ios-wake-healthcheck-storm-1.0.4.log

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions