Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb4e4552d8 | ||
|
|
3e7df0f4d8 |
@@ -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.3
|
||||||
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
|
||||||
|
|||||||
+42
-7
@@ -48,11 +48,12 @@ type App struct {
|
|||||||
disconnectBtn *widget.Button
|
disconnectBtn *widget.Button
|
||||||
|
|
||||||
// State
|
// State
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
ipcClient *ipc.Client
|
ipcClient *ipc.Client
|
||||||
profiles []model.ServerProfile
|
profiles []model.ServerProfile
|
||||||
currentProfile *model.ServerProfile
|
currentProfile *model.ServerProfile
|
||||||
langSetting string
|
defaultProfileID int64
|
||||||
|
langSetting string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run initialises and starts the GUI application.
|
// Run initialises and starts the GUI application.
|
||||||
@@ -84,6 +85,7 @@ func Run() {
|
|||||||
db: store,
|
db: store,
|
||||||
kc: keychain.New(),
|
kc: keychain.New(),
|
||||||
langSetting: cfg.Language,
|
langSetting: cfg.Language,
|
||||||
|
defaultProfileID: cfg.DefaultProfileID,
|
||||||
listSelectedIndex: -1,
|
listSelectedIndex: -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +146,21 @@ func (a *App) loadProfiles() {
|
|||||||
names := a.profileNames()
|
names := a.profileNames()
|
||||||
a.profileSelect.Options = names
|
a.profileSelect.Options = names
|
||||||
if len(names) > 0 {
|
if len(names) > 0 {
|
||||||
a.profileSelect.SetSelectedIndex(0)
|
selected := false
|
||||||
a.selectProfileByName(names[0])
|
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 {
|
} else {
|
||||||
a.currentProfile = nil
|
a.currentProfile = nil
|
||||||
a.profileSelect.SetSelected("")
|
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.
|
// onAddProfile shows a dialog to create a new profile.
|
||||||
func (a *App) onAddProfile() {
|
func (a *App) onAddProfile() {
|
||||||
a.showProfileDialog(nil)
|
a.showProfileDialog(nil)
|
||||||
|
|||||||
+42
-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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +54,7 @@ func (a *App) buildMainWindow() fyne.CanvasObject {
|
|||||||
// Profile selector.
|
// Profile selector.
|
||||||
a.profileSelect = widget.NewSelect(a.profileNames(), func(sel string) {
|
a.profileSelect = widget.NewSelect(a.profileNames(), func(sel string) {
|
||||||
a.selectProfileByName(sel)
|
a.selectProfileByName(sel)
|
||||||
|
a.saveDefaultProfile()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Status display.
|
// Status display.
|
||||||
@@ -132,8 +140,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 +203,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 +258,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