Leadaxe:sing-box-lx v1.14.4-lx.28 修复 masque 停止卡死与 cache.db 释放

在 sing-box-lx #61 的修复说明中,Leadaxe 表示 v1.14.4-lx.28 同时处理两类停止卡死:为 masque 经 detour 的 quic 连接补真实读超时并在关闭时取消连接、释放会话;cache.db 的 CacheFile.Release 改为幂等,补写 FakeIP metadata,并用按路径注册表在新 Box 打开前释放旧实例。原文称 release build running,设备尚未复核,LxBox 删除 cache.db 后重试的 fallback 保留。

作者原文

Fixed in v1.14.4-lx.28 (release build running), in two layers.

The actual hang turned out to be in masque, not in the WG endpoints. Two LxBox support packs from today carry the goroutine dump of the stop watchdog, and both show the same single blocked stack: Box.Close → masque.(*Outbound).Close waiting on runMu, which ensureSession holds across the whole dial. The dial sat in quic.DialEarly through a detour (the node is vhttp: h3 with detour to a selector, member vless + WebSocket). quic-go never closes a PacketConn it did not create: when its single-use transport shuts down it calls SetReadDeadline(now) and waits for its listener to return from ReadPacket. Through the detour that conn is a stream (XUDP over WebSocket) whose SetReadDeadline is a no-op, so the listener never woke, Transport.Close never returned, DialEarly never returned, and the mutex was never released. Trigger in the field: a ping of that node was in flight when Stop was pressed.

  • 1858657b0 (SPEC 146): the detour conn gets a real read deadline (deadline.NewConn, by NeedAdditionalReadDeadline) before quic-go sees it; Outbound.Close no longer takes the lock held across the dial — it cancels the connect in flight and tears the session down; a session that comes up after Close is disposed of. Three red/green tests.

This issue's layer, cache.db, is in as the second line of defence, with one change against the plan above:

  • e8ea92cd8 (SPEC 147): idempotent CacheFile.Release() (flushes pending FakeIP metadata, closes bbolt, from then on writes return os.ErrClosed and resetDB never reopens the file), a per-path registry so a new Box releases the previous instance before bbolt.Open instead of waiting 10 s, and the normal scope cleanup goes through Release too. FakeIPMetadata() now seeds the pending metadata it read-and-deleted, so a takeover does not reset the FakeIP pool on the next start.
  • Step 2 of the plan (Release() as the first step of Box.Close) is not implemented: the registry already covers the in-process case completely, and an early release would have broken the fakeip store's synchronous metadata write on every normal stop, on every platform.

Not checked on a device yet. The LxBox fallback (§703, delete cache.db and retry once) stays in place and should stop firing with lx.28.