Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb4e4552d8 | ||
|
|
3e7df0f4d8 |
@@ -11,7 +11,7 @@ GO = go
|
||||
CGO_ENABLED = 1
|
||||
WINDRES ?= x86_64-w64-mingw32-windres
|
||||
MINGW_CC ?= x86_64-w64-mingw32-gcc
|
||||
SEMVER ?= 0.4.1
|
||||
SEMVER ?= 0.4.3
|
||||
GIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
VERSION = $(SEMVER)-$(GIT_HASH)
|
||||
LDFLAGS = -s -w -X lmvpn/internal/version.Version=$(VERSION)
|
||||
|
||||
@@ -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
|
||||
|
||||
+42
-7
@@ -48,11 +48,12 @@ type App struct {
|
||||
disconnectBtn *widget.Button
|
||||
|
||||
// State
|
||||
mu sync.Mutex
|
||||
ipcClient *ipc.Client
|
||||
profiles []model.ServerProfile
|
||||
currentProfile *model.ServerProfile
|
||||
langSetting string
|
||||
mu sync.Mutex
|
||||
ipcClient *ipc.Client
|
||||
profiles []model.ServerProfile
|
||||
currentProfile *model.ServerProfile
|
||||
defaultProfileID int64
|
||||
langSetting string
|
||||
}
|
||||
|
||||
// Run initialises and starts the GUI application.
|
||||
@@ -84,6 +85,7 @@ func Run() {
|
||||
db: store,
|
||||
kc: keychain.New(),
|
||||
langSetting: cfg.Language,
|
||||
defaultProfileID: cfg.DefaultProfileID,
|
||||
listSelectedIndex: -1,
|
||||
}
|
||||
|
||||
@@ -144,8 +146,21 @@ func (a *App) loadProfiles() {
|
||||
names := a.profileNames()
|
||||
a.profileSelect.Options = names
|
||||
if len(names) > 0 {
|
||||
a.profileSelect.SetSelectedIndex(0)
|
||||
a.selectProfileByName(names[0])
|
||||
selected := false
|
||||
if a.defaultProfileID > 0 {
|
||||
for i := range a.profiles {
|
||||
if a.profiles[i].ID == a.defaultProfileID {
|
||||
a.profileSelect.SetSelectedIndex(i)
|
||||
a.selectProfileByName(a.profiles[i].Name)
|
||||
selected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !selected {
|
||||
a.profileSelect.SetSelectedIndex(0)
|
||||
a.selectProfileByName(names[0])
|
||||
}
|
||||
} else {
|
||||
a.currentProfile = nil
|
||||
a.profileSelect.SetSelected("")
|
||||
@@ -182,6 +197,26 @@ func (a *App) selectProfileByName(name string) {
|
||||
}
|
||||
}
|
||||
|
||||
// saveDefaultProfile persists the currently selected profile ID to the
|
||||
// config file so it can be restored on the next launch.
|
||||
func (a *App) saveDefaultProfile() {
|
||||
if a.currentProfile == nil {
|
||||
return
|
||||
}
|
||||
a.defaultProfileID = a.currentProfile.ID
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
cfg = config.Default()
|
||||
}
|
||||
if cfg.DefaultProfileID == a.currentProfile.ID {
|
||||
return
|
||||
}
|
||||
cfg.DefaultProfileID = a.currentProfile.ID
|
||||
if err := config.Save(cfg); err != nil {
|
||||
log.L().Error("save default profile", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// onAddProfile shows a dialog to create a new profile.
|
||||
func (a *App) onAddProfile() {
|
||||
a.showProfileDialog(nil)
|
||||
|
||||
+42
-2
@@ -39,6 +39,13 @@ func (a *App) setConnButtons(connectEnabled, disconnectEnabled bool) {
|
||||
} else {
|
||||
a.disconnectBtn.Disable()
|
||||
}
|
||||
if a.profileSelect != nil {
|
||||
if connectEnabled {
|
||||
a.profileSelect.Enable()
|
||||
} else {
|
||||
a.profileSelect.Disable()
|
||||
}
|
||||
}
|
||||
a.setupTray()
|
||||
}
|
||||
|
||||
@@ -47,6 +54,7 @@ func (a *App) buildMainWindow() fyne.CanvasObject {
|
||||
// Profile selector.
|
||||
a.profileSelect = widget.NewSelect(a.profileNames(), func(sel string) {
|
||||
a.selectProfileByName(sel)
|
||||
a.saveDefaultProfile()
|
||||
})
|
||||
|
||||
// Status display.
|
||||
@@ -132,8 +140,13 @@ func (a *App) onConnect() {
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
oldClient := a.ipcClient
|
||||
a.ipcClient = client
|
||||
a.mu.Unlock()
|
||||
if oldClient != nil {
|
||||
_ = ipc.SendStop(oldClient)
|
||||
oldClient.Close()
|
||||
}
|
||||
|
||||
// Get password from keychain.
|
||||
password, err := a.kc.GetPassword(a.currentProfile.Name)
|
||||
@@ -190,13 +203,24 @@ func (a *App) onConnect() {
|
||||
func (a *App) onDisconnect() {
|
||||
a.mu.Lock()
|
||||
client := a.ipcClient
|
||||
a.ipcClient = nil
|
||||
a.mu.Unlock()
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
_ = ipc.SendStop(client)
|
||||
client.Close()
|
||||
a.setConnButtons(true, false)
|
||||
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.
|
||||
@@ -234,17 +258,33 @@ func (a *App) eventLoop() {
|
||||
switch ev.Event {
|
||||
case ipc.EvState:
|
||||
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:
|
||||
if ev.Stats != nil {
|
||||
s := *ev.Stats
|
||||
fyne.Do(func() {
|
||||
a.applyStats(s)
|
||||
a.mu.Lock()
|
||||
current := a.ipcClient
|
||||
a.mu.Unlock()
|
||||
if current == client {
|
||||
a.applyStats(s)
|
||||
}
|
||||
})
|
||||
}
|
||||
case ipc.EvError:
|
||||
fyne.Do(func() {
|
||||
a.mu.Lock()
|
||||
current := a.ipcClient
|
||||
a.mu.Unlock()
|
||||
if current != client {
|
||||
return
|
||||
}
|
||||
if ev.Code == "tls_error" {
|
||||
showError(i18n.T("DlgTLSError"),
|
||||
i18n.T("TLSErrorVerification")+"\n\n"+ev.Message, a.window)
|
||||
|
||||
+16
-2
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user