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 -3
View File
@@ -19,6 +19,7 @@ import (
"net"
"os"
"os/signal"
"sync"
"syscall"
"lmvpn/internal/ipc"
@@ -92,6 +93,7 @@ func chownToUser(path string, uid, gid int) {
type daemon struct {
server *ipc.Server
mu sync.Mutex
session *vpn.SessionManager
cancel context.CancelFunc
}
@@ -109,8 +111,11 @@ func (d *daemon) handle(conn net.Conn, req ipc.Request) {
d.server.Close()
os.Exit(0)
case ipc.CmdStats:
if d.session != nil {
snap := d.session.Stats().Snapshot()
d.mu.Lock()
sess := d.session
d.mu.Unlock()
if sess != nil {
snap := sess.Stats().Snapshot()
d.server.Broadcast(ipc.Event{Event: ipc.EvStats, Stats: &snap})
}
_ = ipc.WriteOK(conn)
@@ -122,12 +127,14 @@ func (d *daemon) handle(conn net.Conn, req ipc.Request) {
}
func (d *daemon) startSession(conn net.Conn, req ipc.Request) {
d.mu.Lock()
defer d.mu.Unlock()
if req.Config == nil {
_ = ipc.WriteErr(conn, "missing config")
return
}
if d.session != nil {
d.stopSession()
d.stopSessionLocked()
}
cfg := vpn.SessionConfig{
@@ -170,6 +177,12 @@ func (d *daemon) startSession(conn net.Conn, req ipc.Request) {
}
func (d *daemon) stopSession() {
d.mu.Lock()
defer d.mu.Unlock()
d.stopSessionLocked()
}
func (d *daemon) stopSessionLocked() {
if d.cancel != nil {
d.cancel()
d.cancel = nil