kevin 3e7df0f4d8
Release / build-macos (push) Canceled after 0s
Release / build-windows (push) Canceled after 0s
Release / release (push) Canceled after 0s
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 连接时禁用配置下拉框
2026-07-08 20:57:50 +08:00
2026-07-07 12:24:20 +08:00

LMVPN Client

LMVPN 是一个基于 WebSocket 隧道与 TUN 虚拟网卡的三层(网络层)VPN 客户端。客户端通过 WebSocketws:///wss://)连接服务端,完成认证后在本地 TUN 虚拟网卡与 WebSocket 连接之间双向转发原始 IP 数据包,实现透明隧道。

客户端使用 Go + Fyne 开发,支持 macOS、Windows、Linux 三大桌面平台。

服务端项目https://github.com/wuwenfengmi1998/lmvpn_server


目录


功能特性

  • 双进程架构GUIlmvpn,普通用户)+ 守护进程(lmvpnd,root/管理员),自动拉起与生命周期管理
  • 多种认证JWT 令牌 / 用户名密码
  • 隧道模式:全量隧道、分流隧道(按目标绕过)、自定义隧道
  • 多服务器管理:配置文件 + SQLite 存储多个服务器配置(Profile)
  • 国际化:中文(简体)、英文,跟随系统语言
  • 安全存储macOS Keychain / Windows Credential Manager 加密保存凭据
  • 系统集成:系统托盘图标、Dock 显隐、开机自启
  • 日志轮转:基于 lumberjack 的文件日志自动轮转

系统要求

平台 最低版本 架构 备注
macOS 11.0Big Sur amd64 / arm64 需 Xcode Command Line Tools
Windows 10 x86_64 仅支持 64 位,需 WinTun 驱动(已内置)
Linux 现代发行版 amd64 / arm64 需 OpenGL/GLFW/libdbus 开发库

所有平台均要求 CGO_ENABLED=1Fyne 依赖 OpenGL/GLFW 的 CGO 绑定,不可关闭)。


各平台编译

通用前置依赖

  • Go 1.26 或更高版本(见 go.mod
  • Git:用于在编译时注入版本号,格式为 0.3.7-<git短哈希>(如 0.3.7-019df7b
  • C 编译器GCCLinux/Windows 交叉编译)或 clangmacOS),由 CGO 调用
  • 网络访问:首次构建需拉取 Go 模块依赖

版本号通过 -ldflags "-X lmvpn/internal/version.Version=$(VERSION)" 在链接期注入到 internal/version 包,GUI 与守护进程共享同一版本字符串。

macOS

依赖

  • Xcode Command Line Tools:提供 clangCGO)、Cocoa 框架、sipsiconutil(图标生成)

    xcode-select --install
    
  • 最低部署目标 macOS 11.0(见 resources/Info.plistLSMinimumSystemVersion

编译命令

# 默认目标:编译二进制并组装 LMVPN.app bundle
make            # 等价于 make app

# 仅编译裸二进制(build/lmvpn、build/lmvpnd),不打包 .app
make build

# 重新生成 macOS 图标 resources/icon.icns(需要 icon.png 或 logo.svg
make icon

# 编译并直接运行 GUI
make run

make app 会将 build/lmvpnbuild/lmvpnd 连同 resources/Info.plistresources/icon.icns 组装成 LMVPN.app/

运行

守护进程创建 TUN 网卡与修改路由需要 root 权限,GUI 通过 osascript ... with administrator privileges 弹出系统授权对话框提权拉起守护进程。日常使用直接双击 LMVPN.app 即可。


Windows(交叉编译)

Windows 版本从 macOS 或 Linux 交叉编译生成(Fyne 无法用 CGO_ENABLED=0 构建,因此必须借助 mingw-w64 提供 C 交叉编译器)。仅支持 x86_64 架构。

依赖

  • mingw-w64 工具链:提供 x86_64-w64-mingw32-gccC 编译器)与 x86_64-w64-mingw32-windres(资源编译器)

    # macOS
    brew install mingw-w64
    
  • Inno Setup 6(仅打包安装程序时需要):用于生成 .exe 安装包

    • 原生 Windows:将 ISCC 加入 PATH
    • macOS/Linux:通过 Wine 调用,需安装 Wine 并将 Inno Setup 6 装到 Wine 的 C:\Program Files (x86)\Inno Setup 6\

编译命令

# 1. 生成 Windows 图标与资源(.ico + .syso),并交叉编译 exe
make build-windows

# 2. 单独生成图标资源(生成 resource_windows_amd64.syso,会被 go build 自动链接)
make icon-windows

# 3. 编译并打包 Inno Setup 安装程序
make installer-windows

make build-windows 实际执行:

# GUI(带 -H windowsgui,无控制台窗口)
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc \
    go build -ldflags "-s -w -X ... -H windowsgui" -o build/lmvpn.exe ./cmd/lmvpn

# 守护进程(控制台程序)
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc \
    go build -ldflags "-s -w -X ..." -o build/lmvpnd.exe ./cmd/lmvpnd

产物:build/lmvpn.exebuild/lmvpnd.exe,安装包 build/LMVPN-Setup-<version>.exe

关于 WinTun

Windows 版使用 WinTun 驱动创建虚拟网卡。wintun.dll 在编译期通过 //go:embed 嵌入二进制,运行时释放到 exe 同级目录;安装包也会单独安装一份 wintun.dll

运行

守护进程需要管理员权限,GUI 通过 UAC(ShellExecuteW + runas)提权拉起。


Linux

Linux 没有独立的 Make 目标,使用平台无关的 make build 原生编译。

依赖

  • GCCCGO 编译器)
  • OpenGL 与 GLFW 开发库Fyne 依赖):libgllibglfwlibx11、xorg 开发头
  • libdbus-1 开发库(系统托盘,经 godbus/dbus):libdbus-1-dev 或对应开发包

请根据所用发行版(apt / dnf / pacman 等)自行安装上述开发库。

编译命令

# 原生编译(平台无关,直接产出 build/lmvpn、build/lmvpnd
make build

# 编译并运行 GUI
make run

# 以 root 运行守护进程(调试用)
make daemon

运行

  • TUN 网卡创建与路由配置需要 root 或 CAP_NET_ADMIN 能力
  • 提权方式:pkexecinternal/ui/elevation_other.go
  • 系统命令依赖:ipip routeinternal/route/route_linux.gointernal/tun/tun_linux.go
  • 密钥链:Linux 暂使用内存存储(不持久化),重启后凭据丢失,后续需接入 Secret Service 等后端

Make 目标速查表

目标 说明
make / make all / make app 编译并组装 macOS LMVPN.app bundle(默认)
make build 仅编译 lmvpn + lmvpnd 裸二进制(macOS/Linux 原生)
make run 编译并运行 GUI
make daemon 编译并以 sudo 运行守护进程
make build-windows 交叉编译 Windows x64 exe(含图标资源生成)
make icon-windows 生成 Windows .ico.syso 资源文件
make installer-windows 编译 exe 并打包 Inno Setup 安装程序
make icon icon.png/logo.svg 重新生成 macOS icon.icns
make vet 运行 go vet ./...
make fmt 运行 go fmt ./...
make tidy 运行 go mod tidy
make clean 清理 build/LMVPN.app/

运行使用

GUI 会自动管理守护进程的生命周期(拉起、停止),无需手动启动 lmvpnd

  • macOS:双击 LMVPN.app,首次连接时系统弹出授权对话框输入密码以提权守护进程
  • Windows:运行安装包或直接运行 lmvpn.exe,首次连接时 UAC 弹窗确认提权
  • Linuxmake run 或运行编译好的 lmvpn,首次连接时 pkexec 弹窗输入密码提权

GUI 与守护进程通过本地 IPC 通信:macOS/Linux 使用 Unix socket /tmp/lmvpn.sockWindows 使用 TCP 127.0.0.1:18923(因 Windows 上 AF_UNIX 有完整性级别校验,会阻止非提权 GUI 访问提权守护进程的 socket)。


配置与数据目录

各平台的数据目录布局(Bundle ID 为 com.lmvpn.client):

平台 配置/数据 缓存 日志
macOS ~/Library/Application Support/com.lmvpn.client/ ~/Library/Caches/com.lmvpn.client/ ~/Library/Logs/com.lmvpn.client/
Windows %APPDATA%\com.lmvpn.client\ %LOCALAPPDATA%\com.lmvpn.client\ 同数据目录下 log/
Linux ~/.local/share/com.lmvpn.client/ ~/.cache/com.lmvpn.client/ ~/.local/state/com.lmvpn.client/log/
  • 配置文件支持 TOML 与 YAML 两种格式(internal/config
  • 服务器配置(Profile)存储于 SQLite 数据库(internal/db,使用纯 Go 的 modernc.org/sqlite,无需 CGO
  • 凭据保存于系统密钥链(macOS Keychain / Windows Credential Manager / Linux 内存)

架构概览

双进程设计

┌──────────────┐   IPC (Unix socket / TCP)   ┌──────────────┐
│   lmvpn      │ ◄─────────────────────────► │   lmvpnd     │
│   (GUI)      │      控制命令 / 状态         │  (守护进程)   │
│  Fyne UI     │                             │  WebSocket   │
│  普通用户     │                             │  TUN 网卡    │
│              │                             │  root/管理员  │
└──────────────┘                             └──────┬───────┘
                                                    │
                                              WebSocket (ws/wss)
                                                    ▼
                                              ┌──────────────┐
                                              │  LMVPN 服务端 │
                                              └──────────────┘

拆分 GUI 与守护进程的原因:避免 Fyne(及其 locale/字体初始化)加载进 root 进程,同时让提权范围最小化——仅守护进程需要 root 权限操作网卡与路由。

目录结构

lmvpn_client/
├── cmd/
│   ├── lmvpn/          # GUI 入口
│   └── lmvpnd/         # 守护进程入口
├── internal/
│   ├── auth/           # 认证(JWT / 用户名密码)
│   ├── config/         # 配置文件解析(TOML / YAML
│   ├── daemon/         # 守护进程生命周期、IPC、提权拉起
│   ├── db/             # SQLite 存储、Profile、日志记录
│   ├── i18n/           # 国际化(en / zh-Hans
│   ├── ipc/            # GUI ↔ 守护进程 IPC 协议
│   ├── keychain/       # 密钥链(darwin/windows/other
│   ├── log/            # 日志(lumberjack 轮转)
│   ├── model/          # 数据模型
│   ├── paths/          # 平台路径解析(darwin/windows/other
│   ├── protocol/       # 与服务端的 WebSocket 协议
│   ├── route/          # 路由管理(全量/分流/自定义)
│   ├── stats/          # 流量统计
│   ├── transport/      # WebSocket 传输层
│   ├── tun/            # TUN 虚拟网卡(darwin/linux/windows
│   ├── ui/             # Fyne 界面、托盘、提权、Dock
│   ├── version/        # 版本号(链接期注入)
│   └── vpn/            # VPN 会话管理
├── resources/          # 图标、Info.plist、wintun.dll、资源生成器
├── installer/          # Inno Setup 安装脚本(lmvpn.iss
├── docs/               # 开发文档(协议规范)
└── Makefile

平台适配约定

项目使用 Go 文件后缀构建约束实现平台适配,主要分布:

模块 macOS Windows Linux
tun tun_darwin.gowater/utun tun_windows.gowintun + embed dll tun_linux.gowater/tun
route route_darwin.goroute route_windows.goroute route_linux.goip route
keychain keychain_darwin.goKeychain keychain_windows.goCredential Manager keychain_other.go(内存)
paths paths_darwin.go paths_windows.go paths_other.goXDG
daemon 提权 launch_unix.go + osascript launch_windows.go + UAC runas launch_unix.go + pkexec
ui Dock dock_darwin.goCocoa CGO dock_other.gono-op dock_other.gono-op

开发指南

# 静态检查
make vet

# 格式化
make fmt

# 整理依赖
make tidy

# 清理构建产物
make clean

添加新平台适配

如需适配新的平台能力,参考现有约定新增带构建约束的文件,例如:

  • TUN 实现 → internal/tun/tun_<os>.go
  • 路由命令 → internal/route/route_<os>.go
  • 密钥存储 → internal/keychain/keychain_<os>.go
  • 路径解析 → internal/paths/paths_<os>.go

协议规范

客户端与服务端的完整通信协议见 docs/client-development.md(含认证、握手、数据面、心跳、错误码等)。


注意事项

  • 不支持移动端:虽 Fyne 支持 Android/iOS,本项目未配置移动端构建目标与适配代码
  • Windows 仅 x64:交叉编译仅目标 GOARCH=amd64,内置的 wintun.dll 为 x64 版本
  • Linux 密钥链待完善:当前为内存存储,凭据不持久化,生产环境建议接入 Secret Service
  • CI/CD:通过 GitHub Actions 自动构建 macOS / Windows 产物并发布 Release(见 .github/workflows/release.yml),打 v* tag 即触发
  • CGO 必开:任何平台都不可关闭 CGO,否则 Fyne 无法编译
  • 版本一致性:GUI 与守护进程共享同一版本字符串,若版本不一致通常意味着旧守护进程仍在运行

许可证

详见仓库 LICENSE 文件。

S
Description
No description provided
Readme MIT
28 MiB
Languages
Go 89.5%
Inno Setup 8.5%
Makefile 2%