feat: 路由模式重构(full/proxy/bypass) + CIDR动态URL获取 + 命中统计 + 连接稳定性修复

路由模式与CIDR配置重构:
- 路由模式从 full/split/custom 改为 full/proxy/bypass,移除 split 和 custom
- CIDR 按 IPv4/IPv6 分开配置,支持静态 CIDR + URL 动态获取
- URL 支持"代理前获取"(直连)和"代理后获取"(走隧道)两种时机
- 数据库迁移 v5 自动转换旧数据(custom_cidrs 拆分为 cidr_v4/v6,custom->proxy,split->full)

CIDR命中统计:
- 状态栏显示当前路由模式及 IPv4/IPv6 CIDR 命中数
- 代理模式: 显示命中/总数; 绕过模式: 显示已配置数 + 未命中目的地址数
- cidrTracker 在 pumpPackets 中逐包检查出站目的IP

连接稳定性修复(多轮):
- transport.Connect 添加 ctx 监听 goroutine,取消时关闭WS中断阻塞的ReadMessage
- auth.Login 加 context.Context 参数,可被 cancel 中断
- Disconnect() 先删路由再关TUN,避免断网窗口
- startSession 在 Connect 前释放 d.mu,避免锁持有过久
- onConnect 移除 SendStop 消除竞态
- before-proxy CIDR 获取移到 run 循环前,并行获取+5s超时,重连复用
- cidrTracker 加 RWMutex 防数据竞争

UI修复:
- CIDR URL 输入框改为水平滚动+纵向布局,防止撑宽窗口
- 路由模式为full时隐藏CIDR配置区域
This commit is contained in:
2026-07-09 08:21:47 +08:00
parent 15af9ef72c
commit 7b4289ae00
18 changed files with 1310 additions and 157 deletions
+111 -1
View File
@@ -8,6 +8,7 @@ package db
import (
"database/sql"
"fmt"
"net"
"strings"
"lmvpn/internal/paths"
@@ -51,7 +52,10 @@ func (s *Store) migrate() error {
if err := s.migrateV3(); err != nil {
return err
}
return s.migrateV4()
if err := s.migrateV4(); err != nil {
return err
}
return s.migrateV5()
}
func (s *Store) migrateV2() error {
@@ -207,6 +211,108 @@ func (s *Store) migrateV4() error {
return nil
}
// migrateV5 replaces the single custom_cidrs column with separate
// cidr_v4, cidr_v6, cidr_v4_urls, cidr_v6_urls columns. It migrates
// existing routing modes: 'custom' -> 'proxy', 'split' -> 'full'.
// Existing custom_cidrs are split into v4/v6 based on address family.
// Idempotent: skips columns that already exist.
func (s *Store) migrateV5() error {
cols := []struct {
name string
sql string
}{
{"cidr_v4", "ALTER TABLE server_profiles ADD COLUMN cidr_v4 TEXT NOT NULL DEFAULT ''"},
{"cidr_v6", "ALTER TABLE server_profiles ADD COLUMN cidr_v6 TEXT NOT NULL DEFAULT ''"},
{"cidr_v4_urls", "ALTER TABLE server_profiles ADD COLUMN cidr_v4_urls TEXT NOT NULL DEFAULT ''"},
{"cidr_v6_urls", "ALTER TABLE server_profiles ADD COLUMN cidr_v6_urls TEXT NOT NULL DEFAULT ''"},
}
needMigration := false
for _, c := range cols {
if !columnExists(s.db, "server_profiles", c.name) {
needMigration = true
break
}
}
if !needMigration {
return nil
}
for _, c := range cols {
if !columnExists(s.db, "server_profiles", c.name) {
if _, err := s.db.Exec(c.sql); err != nil {
return fmt.Errorf("migrate v5 add %s: %w", c.name, err)
}
}
}
// Migrate existing custom_cidrs into cidr_v4 / cidr_v6 and update
// routing mode codes. Only process rows that still have a non-empty
// custom_cidrs or an old routing mode.
rows, err := s.db.Query(`SELECT id, routing_mode, custom_cidrs FROM server_profiles`)
if err != nil {
return fmt.Errorf("migrate v5 read rows: %w", err)
}
type row struct {
id int64
routingMode string
customCIDRs string
}
var toUpdate []row
for rows.Next() {
var r row
if err := rows.Scan(&r.id, &r.routingMode, &r.customCIDRs); err != nil {
rows.Close()
return fmt.Errorf("migrate v5 scan: %w", err)
}
toUpdate = append(toUpdate, r)
}
rows.Close()
for _, r := range toUpdate {
newMode := r.routingMode
switch newMode {
case "custom":
newMode = "proxy"
case "split":
newMode = "full"
}
v4CIDRs, v6CIDRs := splitCIDRsByFamily(r.customCIDRs)
_, err := s.db.Exec(
`UPDATE server_profiles SET routing_mode = ?, cidr_v4 = ?, cidr_v6 = ? WHERE id = ?`,
newMode, v4CIDRs, v6CIDRs, r.id)
if err != nil {
return fmt.Errorf("migrate v5 update row %d: %w", r.id, err)
}
}
return nil
}
// splitCIDRsByFamily splits a comma-separated CIDR string into IPv4 and
// IPv6 parts. Used for migration from the old custom_cidrs column.
func splitCIDRsByFamily(customCIDRs string) (v4, v6 string) {
if customCIDRs == "" {
return "", ""
}
var v4Parts, v6Parts []string
for _, part := range strings.Split(customCIDRs, ",") {
c := strings.TrimSpace(part)
if c == "" {
continue
}
if _, ipNet, err := net.ParseCIDR(c); err == nil {
if ipNet.IP.To4() != nil {
v4Parts = append(v4Parts, c)
} else {
v6Parts = append(v6Parts, c)
}
}
}
return strings.Join(v4Parts, ", "), strings.Join(v6Parts, ", ")
}
// columnExists reports whether a column exists on a table.
func columnExists(db *sql.DB, table, column string) bool {
rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table))
@@ -242,6 +348,10 @@ CREATE TABLE IF NOT EXISTS server_profiles (
auth_mode TEXT NOT NULL DEFAULT 'both',
routing_mode TEXT NOT NULL DEFAULT 'full',
custom_cidrs TEXT NOT NULL DEFAULT '',
cidr_v4 TEXT NOT NULL DEFAULT '',
cidr_v6 TEXT NOT NULL DEFAULT '',
cidr_v4_urls TEXT NOT NULL DEFAULT '',
cidr_v6_urls TEXT NOT NULL DEFAULT '',
mtu_override INTEGER NOT NULL DEFAULT 0,
auto_connect INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+19 -10
View File
@@ -14,12 +14,14 @@ func (s *Store) CreateProfile(p *model.ServerProfile) (int64, error) {
`INSERT INTO server_profiles
(name, protocol, host, server_ips, port, path,
username, auth_mode, routing_mode,
custom_cidrs, mtu_override, auto_connect,
cidr_v4, cidr_v6, cidr_v4_urls, cidr_v6_urls,
mtu_override, auto_connect,
tls_ca_cert, tls_ca_path, tls_insecure, tls_pinned_hash)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
p.Name, p.Protocol, p.Host, p.ServerIPs, p.Port, p.Path,
p.Username, p.AuthMode, p.RoutingMode,
p.CustomCIDRs, p.MTUOverride, p.AutoConnect,
p.CIDRV4, p.CIDRV6, p.CIDRV4URLs, p.CIDRV6URLs,
p.MTUOverride, p.AutoConnect,
p.TLSCACert, p.TLSCAPath, p.TLSInsecure, p.TLSPinnedHash,
)
if err != nil {
@@ -38,13 +40,16 @@ func (s *Store) GetProfile(id int64) (*model.ServerProfile, error) {
err := s.db.QueryRow(
`SELECT id, name, protocol, host, server_ips, port, path,
username, auth_mode, routing_mode,
custom_cidrs, mtu_override, auto_connect,
cidr_v4, cidr_v6, cidr_v4_urls, cidr_v6_urls,
mtu_override, auto_connect,
tls_ca_cert, tls_ca_path, tls_insecure, tls_pinned_hash,
created_at, last_connected_at
FROM server_profiles WHERE id = ?`, id,
).Scan(&p.ID, &p.Name, &p.Protocol, &p.Host, &p.ServerIPs, &p.Port, &p.Path,
&p.Username, &p.AuthMode, &p.RoutingMode, &p.CustomCIDRs, &p.MTUOverride,
&p.AutoConnect, &p.TLSCACert, &p.TLSCAPath, &p.TLSInsecure, &p.TLSPinnedHash,
&p.Username, &p.AuthMode, &p.RoutingMode,
&p.CIDRV4, &p.CIDRV6, &p.CIDRV4URLs, &p.CIDRV6URLs,
&p.MTUOverride, &p.AutoConnect,
&p.TLSCACert, &p.TLSCAPath, &p.TLSInsecure, &p.TLSPinnedHash,
&p.CreatedAt, &last)
if err != nil {
return nil, fmt.Errorf("get profile %d: %w", id, err)
@@ -60,7 +65,8 @@ func (s *Store) ListProfiles() ([]model.ServerProfile, error) {
rows, err := s.db.Query(
`SELECT id, name, protocol, host, server_ips, port, path,
username, auth_mode, routing_mode,
custom_cidrs, mtu_override, auto_connect,
cidr_v4, cidr_v6, cidr_v4_urls, cidr_v6_urls,
mtu_override, auto_connect,
tls_ca_cert, tls_ca_path, tls_insecure, tls_pinned_hash,
created_at, last_connected_at
FROM server_profiles ORDER BY name`)
@@ -75,7 +81,8 @@ func (s *Store) ListProfiles() ([]model.ServerProfile, error) {
var last sql.NullTime
if err := rows.Scan(&p.ID, &p.Name, &p.Protocol, &p.Host, &p.ServerIPs,
&p.Port, &p.Path, &p.Username, &p.AuthMode, &p.RoutingMode,
&p.CustomCIDRs, &p.MTUOverride, &p.AutoConnect,
&p.CIDRV4, &p.CIDRV6, &p.CIDRV4URLs, &p.CIDRV6URLs,
&p.MTUOverride, &p.AutoConnect,
&p.TLSCACert, &p.TLSCAPath, &p.TLSInsecure, &p.TLSPinnedHash,
&p.CreatedAt, &last); err != nil {
return nil, err
@@ -94,12 +101,14 @@ func (s *Store) UpdateProfile(p *model.ServerProfile) error {
`UPDATE server_profiles SET
name = ?, protocol = ?, host = ?, server_ips = ?, port = ?, path = ?,
username = ?, auth_mode = ?, routing_mode = ?,
custom_cidrs = ?, mtu_override = ?, auto_connect = ?,
cidr_v4 = ?, cidr_v6 = ?, cidr_v4_urls = ?, cidr_v6_urls = ?,
mtu_override = ?, auto_connect = ?,
tls_ca_cert = ?, tls_ca_path = ?, tls_insecure = ?, tls_pinned_hash = ?
WHERE id = ?`,
p.Name, p.Protocol, p.Host, p.ServerIPs, p.Port, p.Path,
p.Username, p.AuthMode, p.RoutingMode,
p.CustomCIDRs, p.MTUOverride, p.AutoConnect,
p.CIDRV4, p.CIDRV6, p.CIDRV4URLs, p.CIDRV6URLs,
p.MTUOverride, p.AutoConnect,
p.TLSCACert, p.TLSCAPath, p.TLSInsecure, p.TLSPinnedHash, p.ID)
if err != nil {
return fmt.Errorf("update profile %d: %w", p.ID, err)