fix: 修复连接状态下切换配置后断开重连导致异常的问题
问题现象:连接中切换配置 -> 断开 -> 连接,会出现连接后瞬间断开、 长时间无法重连。而先断开再切换配置则正常。 根因分析(三个层面的资源泄漏叠加): 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:
@@ -11,7 +11,7 @@ GO = go
|
|||||||
CGO_ENABLED = 1
|
CGO_ENABLED = 1
|
||||||
WINDRES ?= x86_64-w64-mingw32-windres
|
WINDRES ?= x86_64-w64-mingw32-windres
|
||||||
MINGW_CC ?= x86_64-w64-mingw32-gcc
|
MINGW_CC ?= x86_64-w64-mingw32-gcc
|
||||||
SEMVER ?= 0.4.1
|
SEMVER ?= 0.4.2
|
||||||
GIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
GIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
VERSION = $(SEMVER)-$(GIT_HASH)
|
VERSION = $(SEMVER)-$(GIT_HASH)
|
||||||
LDFLAGS = -s -w -X lmvpn/internal/version.Version=$(VERSION)
|
LDFLAGS = -s -w -X lmvpn/internal/version.Version=$(VERSION)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"lmvpn/internal/ipc"
|
"lmvpn/internal/ipc"
|
||||||
@@ -92,6 +93,7 @@ func chownToUser(path string, uid, gid int) {
|
|||||||
|
|
||||||
type daemon struct {
|
type daemon struct {
|
||||||
server *ipc.Server
|
server *ipc.Server
|
||||||
|
mu sync.Mutex
|
||||||
session *vpn.SessionManager
|
session *vpn.SessionManager
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
@@ -109,8 +111,11 @@ func (d *daemon) handle(conn net.Conn, req ipc.Request) {
|
|||||||
d.server.Close()
|
d.server.Close()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
case ipc.CmdStats:
|
case ipc.CmdStats:
|
||||||
if d.session != nil {
|
d.mu.Lock()
|
||||||
snap := d.session.Stats().Snapshot()
|
sess := d.session
|
||||||
|
d.mu.Unlock()
|
||||||
|
if sess != nil {
|
||||||
|
snap := sess.Stats().Snapshot()
|
||||||
d.server.Broadcast(ipc.Event{Event: ipc.EvStats, Stats: &snap})
|
d.server.Broadcast(ipc.Event{Event: ipc.EvStats, Stats: &snap})
|
||||||
}
|
}
|
||||||
_ = ipc.WriteOK(conn)
|
_ = 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) {
|
func (d *daemon) startSession(conn net.Conn, req ipc.Request) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
if req.Config == nil {
|
if req.Config == nil {
|
||||||
_ = ipc.WriteErr(conn, "missing config")
|
_ = ipc.WriteErr(conn, "missing config")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if d.session != nil {
|
if d.session != nil {
|
||||||
d.stopSession()
|
d.stopSessionLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := vpn.SessionConfig{
|
cfg := vpn.SessionConfig{
|
||||||
@@ -170,6 +177,12 @@ func (d *daemon) startSession(conn net.Conn, req ipc.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *daemon) stopSession() {
|
func (d *daemon) stopSession() {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
d.stopSessionLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *daemon) stopSessionLocked() {
|
||||||
if d.cancel != nil {
|
if d.cancel != nil {
|
||||||
d.cancel()
|
d.cancel()
|
||||||
d.cancel = nil
|
d.cancel = nil
|
||||||
|
|||||||
+41
-2
@@ -39,6 +39,13 @@ func (a *App) setConnButtons(connectEnabled, disconnectEnabled bool) {
|
|||||||
} else {
|
} else {
|
||||||
a.disconnectBtn.Disable()
|
a.disconnectBtn.Disable()
|
||||||
}
|
}
|
||||||
|
if a.profileSelect != nil {
|
||||||
|
if connectEnabled {
|
||||||
|
a.profileSelect.Enable()
|
||||||
|
} else {
|
||||||
|
a.profileSelect.Disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
a.setupTray()
|
a.setupTray()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,8 +139,13 @@ func (a *App) onConnect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
|
oldClient := a.ipcClient
|
||||||
a.ipcClient = client
|
a.ipcClient = client
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
|
if oldClient != nil {
|
||||||
|
_ = ipc.SendStop(oldClient)
|
||||||
|
oldClient.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// Get password from keychain.
|
// Get password from keychain.
|
||||||
password, err := a.kc.GetPassword(a.currentProfile.Name)
|
password, err := a.kc.GetPassword(a.currentProfile.Name)
|
||||||
@@ -190,13 +202,24 @@ func (a *App) onConnect() {
|
|||||||
func (a *App) onDisconnect() {
|
func (a *App) onDisconnect() {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
client := a.ipcClient
|
client := a.ipcClient
|
||||||
|
a.ipcClient = nil
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
if client == nil {
|
if client == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = ipc.SendStop(client)
|
_ = ipc.SendStop(client)
|
||||||
|
client.Close()
|
||||||
a.setConnButtons(true, false)
|
a.setConnButtons(true, false)
|
||||||
a.stateLabel.SetText(i18n.T("StateDisconnected"))
|
a.stateLabel.SetText(i18n.T("StateDisconnected"))
|
||||||
|
a.ipLabel.SetText(i18n.T("IpNone"))
|
||||||
|
a.ip6Label.SetText(i18n.T("Ip6None"))
|
||||||
|
a.uptimeLabel.SetText(i18n.T("UptimeNone"))
|
||||||
|
a.rxV4Label.SetText(i18n.T("RxV4Zero"))
|
||||||
|
a.txV4Label.SetText(i18n.T("TxV4Zero"))
|
||||||
|
a.rxV6Label.SetText(i18n.T("RxV6Zero"))
|
||||||
|
a.txV6Label.SetText(i18n.T("TxV6Zero"))
|
||||||
|
a.rxTotalLabel.SetText(i18n.T("RxTotalZero"))
|
||||||
|
a.txTotalLabel.SetText(i18n.T("TxTotalZero"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// eventLoop reads IPC events from the daemon and updates the UI.
|
// eventLoop reads IPC events from the daemon and updates the UI.
|
||||||
@@ -234,17 +257,33 @@ func (a *App) eventLoop() {
|
|||||||
switch ev.Event {
|
switch ev.Event {
|
||||||
case ipc.EvState:
|
case ipc.EvState:
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
a.applyState(ev.State)
|
a.mu.Lock()
|
||||||
|
current := a.ipcClient
|
||||||
|
a.mu.Unlock()
|
||||||
|
if current == client {
|
||||||
|
a.applyState(ev.State)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
case ipc.EvStats:
|
case ipc.EvStats:
|
||||||
if ev.Stats != nil {
|
if ev.Stats != nil {
|
||||||
s := *ev.Stats
|
s := *ev.Stats
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
a.applyStats(s)
|
a.mu.Lock()
|
||||||
|
current := a.ipcClient
|
||||||
|
a.mu.Unlock()
|
||||||
|
if current == client {
|
||||||
|
a.applyStats(s)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case ipc.EvError:
|
case ipc.EvError:
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
|
a.mu.Lock()
|
||||||
|
current := a.ipcClient
|
||||||
|
a.mu.Unlock()
|
||||||
|
if current != client {
|
||||||
|
return
|
||||||
|
}
|
||||||
if ev.Code == "tls_error" {
|
if ev.Code == "tls_error" {
|
||||||
showError(i18n.T("DlgTLSError"),
|
showError(i18n.T("DlgTLSError"),
|
||||||
i18n.T("TLSErrorVerification")+"\n\n"+ev.Message, a.window)
|
i18n.T("TLSErrorVerification")+"\n\n"+ev.Message, a.window)
|
||||||
|
|||||||
+16
-2
@@ -60,6 +60,7 @@ type SessionManager struct {
|
|||||||
dev tun.Device
|
dev tun.Device
|
||||||
routeMgr *route.Manager
|
routeMgr *route.Manager
|
||||||
conn *transport.Conn
|
conn *transport.Conn
|
||||||
|
done chan struct{}
|
||||||
|
|
||||||
// EWMA speed smoothing state. Only touched by reportStats (single
|
// EWMA speed smoothing state. Only touched by reportStats (single
|
||||||
// goroutine), so no lock needed. ewma* fields hold the smoothed
|
// 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)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
sm.cancel = cancel
|
sm.cancel = cancel
|
||||||
sm.running = true
|
sm.running = true
|
||||||
|
sm.done = make(chan struct{})
|
||||||
sm.mu.Unlock()
|
sm.mu.Unlock()
|
||||||
|
|
||||||
go sm.run(ctx, cfg)
|
go sm.run(ctx, cfg)
|
||||||
@@ -126,21 +128,32 @@ func (sm *SessionManager) Disconnect() {
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
// Close the transport to unblock the packet pump.
|
// Close the transport to unblock the WS->TUN goroutine.
|
||||||
sm.mu.Lock()
|
sm.mu.Lock()
|
||||||
conn := sm.conn
|
conn := sm.conn
|
||||||
|
dev := sm.dev
|
||||||
sm.mu.Unlock()
|
sm.mu.Unlock()
|
||||||
if conn != nil {
|
if conn != nil {
|
||||||
conn.Close()
|
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
|
// run is the main session loop with exponential-backoff reconnection
|
||||||
// and CDN IP failover.
|
// and CDN IP failover.
|
||||||
func (sm *SessionManager) run(ctx context.Context, cfg SessionConfig) {
|
func (sm *SessionManager) run(ctx context.Context, cfg SessionConfig) {
|
||||||
fatal := false
|
fatal := false
|
||||||
|
defer close(sm.done)
|
||||||
defer func() {
|
defer func() {
|
||||||
if !fatal {
|
if !fatal && ctx.Err() == nil {
|
||||||
sm.setState(stats.StateDisconnected)
|
sm.setState(stats.StateDisconnected)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -154,6 +167,7 @@ func (sm *SessionManager) run(ctx context.Context, cfg SessionConfig) {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
|
sm.cleanup()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user