fix: 修复连接状态下切换配置后断开重连导致异常的问题
Release / build-macos (push) Canceled after 0s
Release / build-windows (push) Canceled after 0s
Release / release (push) Canceled after 0s

问题现象:连接中切换配置 -> 断开 -> 连接,会出现连接后瞬间断开、
长时间无法重连。而先断开再切换配置则正常。

根因分析(三个层面的资源泄漏叠加):

1. pumpPackets 死锁 (session.go)
   Disconnect() 只关闭 WebSocket transport,不关闭 TUN 设备。
   TUN->WS goroutine 阻塞在 dev.Read(),pumpPackets 的 wg.Wait()
   永远不返回,cleanup()(关闭 TUN/删路由)无法执行,导致
   TUN 设备和路由泄漏。

2. 僵尸 eventLoop + 僵尸 IPC 连接 (view.go)
   onDisconnect 不清空 ipcClient、不关闭 IPC 连接,旧 eventLoop
   持续运行接收广播事件。eventLoop 正常事件处理无身份校验,
   旧 session 的 disconnected 广播会覆盖新 session 的 connected 状态。

3. stopSession 不等待 goroutine 退出 (daemon.go)
   d.session = nil 在 goroutine 仍在运行时即设置,新旧 session
   并发执行导致 TUN/路由冲突。无 mutex 保护并发访问。

修复内容(10 项,3 个文件):

session.go:
- Disconnect() 新增 dev.Close() 解除 pumpPackets 死锁
- 新增 done channel,Disconnect() 阻塞等待 run goroutine 完全退出
- run() deferred setState 跳过用户主动断开时的冗余广播
- run() 顶部 ctx.Err() 检查后补 cleanup()

daemon.go:
- 新增 sync.Mutex 保护 session/cancel 并发访问
- 拆分 stopSession/stopSessionLocked 避免死锁

view.go:
- onDisconnect 置 ipcClient=nil + client.Close() 终止僵尸 eventLoop
- eventLoop 三个事件分支均加 if current == client 身份校验
- onConnect 覆盖前清理旧 IPC client
- setConnButtons 连接时禁用配置下拉框
This commit is contained in:
2026-07-08 20:57:50 +08:00
parent bf4744bb1d
commit 3e7df0f4d8
4 changed files with 74 additions and 8 deletions
+16 -2
View File
@@ -60,6 +60,7 @@ type SessionManager struct {
dev tun.Device
routeMgr *route.Manager
conn *transport.Conn
done chan struct{}
// EWMA speed smoothing state. Only touched by reportStats (single
// goroutine), so no lock needed. ewma* fields hold the smoothed
@@ -106,6 +107,7 @@ func (sm *SessionManager) Connect(ctx context.Context, cfg SessionConfig) error
ctx, cancel := context.WithCancel(ctx)
sm.cancel = cancel
sm.running = true
sm.done = make(chan struct{})
sm.mu.Unlock()
go sm.run(ctx, cfg)
@@ -126,21 +128,32 @@ func (sm *SessionManager) Disconnect() {
if cancel != nil {
cancel()
}
// Close the transport to unblock the packet pump.
// Close the transport to unblock the WS->TUN goroutine.
sm.mu.Lock()
conn := sm.conn
dev := sm.dev
sm.mu.Unlock()
if conn != nil {
conn.Close()
}
// Close the TUN device to unblock the TUN->WS goroutine's dev.Read().
if dev != nil {
dev.Close()
}
// Wait for the run goroutine to fully exit so that cleanup
// (route removal, TUN teardown) is complete before returning.
if sm.done != nil {
<-sm.done
}
}
// run is the main session loop with exponential-backoff reconnection
// and CDN IP failover.
func (sm *SessionManager) run(ctx context.Context, cfg SessionConfig) {
fatal := false
defer close(sm.done)
defer func() {
if !fatal {
if !fatal && ctx.Err() == nil {
sm.setState(stats.StateDisconnected)
}
}()
@@ -154,6 +167,7 @@ func (sm *SessionManager) run(ctx context.Context, cfg SessionConfig) {
for {
if ctx.Err() != nil {
sm.cleanup()
return
}