fix(client): 节点拉取 401 自愈+失败不清空列表;看门狗 path A 跨端(urltest 陈旧判活)
bug:周期刷新拿到 401(token 过期)时 _fetchNodes return const [] → 把节点列表清空, 用户「看不到节点」误以为 server 挂。修:① 非 200 抛异常,refresh 失败保留旧列表(绝不清空); ② 401 用 refresh token 续期重试一次自愈,续期失败才登出。 看门狗 path A 全平台统一:不再只靠原生报 off/error(libbox/NE 行为不一),改用内核 urltest 陈旧度(经 proxy 探测,四端都有 stats)——urltest 超 45s 未成功即判上游死 → _onNodeUnhealthy。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -57,17 +57,20 @@ class ConnectionState {
|
||||
|
||||
// ── 连通看门狗 ─────────────────────────────────────────────────────
|
||||
//
|
||||
// 已连接(本地 TUN 已起)≠ 远端节点数据面可用。两条可靠路径发现节点异常:
|
||||
// A. 意外内核掉线:节点数据面死 → REALITY 断 → 内核自报 off/error。凡「非用户主动」
|
||||
// 的掉线一律判为节点异常 → 置 error「节点异常,连接已中断」+ 刷节点列表(让 down 浮现)。
|
||||
// 这是节点真死时最先、最可靠的信号。见 _onKernelStatus。
|
||||
// 已连接(本地 TUN 已起)≠ 远端节点数据面可用。三条路径发现节点异常,全平台一致:
|
||||
// A. urltest 陈旧(跨端统一主路径):内核 urltest 经 proxy 出站(REALITY→节点)探测,
|
||||
// 节点死则探测失败、urltest 停更。看门狗发现「上次 urltest 成功超过 _kUrltestStale」
|
||||
// 即判上游死 → _onNodeUnhealthy。urltest 四端都有(stats),不依赖各原生端的状态上报。
|
||||
// A'. 意外内核掉线:原生端真把「上游断」上报成 off/error 时(桌面子进程确认有),走
|
||||
// _onKernelStatus 的非用户主动 off → 节点异常提示。属 A 的快路径,有则更快、无则靠 A 兜底。
|
||||
// B. 服务端权威 down:看门狗连接态周期刷 /v1/nodes,所连节点被判 down(dp_healthy=0 /
|
||||
// agent 掉线 / 运维下线)即处理。覆盖「本地 TUN 还没断、但节点已被判不可用」。
|
||||
// 注:不再做客户端 TCP 探数据口——全局 TUN 下 App 的 socket 被本地 TUN 当场接住(假 1ms),
|
||||
// 测不到真节点。延迟改由内核 urltest(握着到数据面真实直连)给,见 connect_page。
|
||||
// 判活针对「实际所连节点」(_connectedNode),非会随 ping 漂移的 effectiveNode。
|
||||
// 注:不做客户端 TCP 探数据口——全局 TUN 下 App 的 socket 被本地 TUN 当场接住(假 1ms),
|
||||
// 测不到真节点。判活针对「实际所连节点」(_connectedNode),非会随 ping 漂移的 effectiveNode。
|
||||
|
||||
const _kWatchdogInterval = Duration(seconds: 15);
|
||||
// urltest 超过此时长未成功(连接态)即判上游(节点数据面)死。须 > 原生 urltest 探测间隔。
|
||||
const _kUrltestStale = Duration(seconds: 45);
|
||||
|
||||
// ── 状态机 ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -87,6 +90,8 @@ class ConnectionController extends StateNotifier<ConnectionState> {
|
||||
Timer? _elapsed;
|
||||
Timer? _watchdog;
|
||||
bool _probing = false;
|
||||
// 最近一次 urltest 成功(经 proxy 真实可达)的时刻;路径 A 用其陈旧度判上游死。
|
||||
DateTime? _lastUrltestOk;
|
||||
// 本次 off 的提示语(节点异常/自动切换);kernel off 与显式断开都读它,避免被互相覆盖。
|
||||
// 仅 _connect 开始时清空。用户主动断开则保持 null(无提示)。
|
||||
String? _offNotice;
|
||||
@@ -123,6 +128,7 @@ class ConnectionController extends StateNotifier<ConnectionState> {
|
||||
Future<void> _connect() async {
|
||||
_offNotice = null; // 新连接:清掉上次的中断提示
|
||||
_userDisconnect = false;
|
||||
_lastUrltestOk = null; // 重置 urltest 判活基准(连上后由 _onStats 首次成功置位)
|
||||
state = const ConnectionState(phase: VpnPhase.connecting);
|
||||
|
||||
final node = _ref.read(effectiveNodeProvider);
|
||||
@@ -233,6 +239,8 @@ class ConnectionController extends StateNotifier<ConnectionState> {
|
||||
if (node == null || node.uuid.isEmpty) return;
|
||||
final ds = e.urltestResults.where((r) => r.delayMs > 0).map((r) => r.delayMs);
|
||||
if (ds.isEmpty) return;
|
||||
// urltest 成功:经 proxy 出站(REALITY 到节点)真实可达 → 记录时刻(供路径 A 判活)+ 回写延迟。
|
||||
_lastUrltestOk = DateTime.now();
|
||||
final best = ds.reduce((a, b) => a < b ? a : b);
|
||||
_ref.read(nodesProvider.notifier).setLivePing(node.uuid, best);
|
||||
}
|
||||
@@ -271,6 +279,15 @@ class ConnectionController extends StateNotifier<ConnectionState> {
|
||||
logLine('Watchdog', 'server marked node ${node.code} down → unhealthy');
|
||||
_stopWatchdog();
|
||||
await _onNodeUnhealthy();
|
||||
return;
|
||||
}
|
||||
// 路径 A(跨端统一):urltest 曾成功过、但已超过 _kUrltestStale 未再成功 → 上游(节点
|
||||
// 数据面)死。urltest 经 proxy 出站探测,四端都有;不依赖原生端把「上游断」报成 off/error。
|
||||
final lastOk = _lastUrltestOk;
|
||||
if (lastOk != null && DateTime.now().difference(lastOk) > _kUrltestStale) {
|
||||
logLine('Watchdog', 'urltest stale ${DateTime.now().difference(lastOk).inSeconds}s → upstream dead → unhealthy');
|
||||
_stopWatchdog();
|
||||
await _onNodeUnhealthy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user