perf: 路由批量执行+CIDR聚合+并行脚本+连接步骤显示+CIDR刷新按钮
Release / build-macos (push) Canceled after 0s
Release / build-windows (push) Canceled after 0s
Release / release (push) Canceled after 0s

路由性能优化:
- 修复 macOS route 命令语法错误(去掉 -net 参数)
- 路由应用拆分为 Essential(ready前,4-6条命令<1s)+ Deferred(ready后,批量脚本)
- 新增 CIDR 聚合算法(cidrmerge.go),合并相邻 CIDR 块减少路由数量(10826->7500)
- 批量脚本执行:3平台各实现8个批量函数,临时脚本单次执行替代逐条进程创建
- 并行批量:拆为4个子shell并行执行,总时间降为1/4

连接步骤显示:
- stats 新增 connectStep + cidrError 原子变量
- 连接生命周期中设置步骤(fetch_cidrs/connecting/load_routes),UI 实时显示
- after-proxy CIDR 获取期间显示加载状态,获取失败显示错误提示

CIDR 刷新按钮:
- ipc 新增 CmdRefreshCIDR 命令
- daemon 处理刷新请求,session.RefreshCIDRs() 重新获取+添加路由
- UI 状态卡新增刷新按钮,proxy/bypass 模式已连接时显示

README:
- 补充 URL 获取时机注意事项(bypass模式after-proxy可能失败)
- 补充 CIDR 聚合与批量执行说明
This commit is contained in:
2026-07-09 09:44:49 +08:00
parent 7c49ae4d72
commit ebe082500f
15 changed files with 1244 additions and 196 deletions
+99 -13
View File
@@ -70,6 +70,9 @@ type SessionManager struct {
// CIDR hit tracking. Set during setupTUN, cleared on cleanup.
cidrTracker *cidrTracker
// lastCfg stores the session config for RefreshCIDRs.
lastCfg SessionConfig
// EWMA speed smoothing state. Only touched by reportStats (single
// goroutine), so no lock needed. ewma* fields hold the smoothed
// bytes/sec; prev* hold the last snapshot's cumulative counters and
@@ -116,6 +119,7 @@ func (sm *SessionManager) Connect(ctx context.Context, cfg SessionConfig) error
sm.cancel = cancel
sm.running = true
sm.done = make(chan struct{})
sm.lastCfg = cfg
sm.mu.Unlock()
go sm.run(ctx, cfg)
@@ -193,6 +197,8 @@ func (sm *SessionManager) run(ctx context.Context, cfg SessionConfig) {
var beforeCIDRs []string
allURLSources := append(append([]model.CIDRURLSource{}, cfg.CIDRV4URLs...), cfg.CIDRV6URLs...)
if len(allURLSources) > 0 {
sm.stats.SetConnectStep("fetch_cidrs")
sm.setState(stats.StateConnecting)
log.L().Info("fetching before-proxy CIDR lists", "url_count", len(allURLSources))
fetched, err := cidrsource.FetchBeforeProxy(ctx, allURLSources)
if err != nil {
@@ -200,6 +206,7 @@ func (sm *SessionManager) run(ctx context.Context, cfg SessionConfig) {
}
beforeCIDRs = fetched
log.L().Info("before-proxy CIDR lists ready", "total_cidrs", len(beforeCIDRs))
sm.stats.SetConnectStep("")
}
backoff := time.Second
@@ -344,6 +351,7 @@ func fatalAuthError(err error) (protocol.AuthErrorCode, string, bool) {
// sources (fetched once in run(), reused across reconnection attempts).
func (sm *SessionManager) connectOnce(ctx context.Context, cfg SessionConfig, targetIP string, beforeCIDRs []string) error {
sm.setState(stats.StateConnecting)
sm.stats.SetConnectStep("connecting")
// Build URL for this attempt. If targetIP is set (CDN failover),
// build a URL with that IP. Otherwise use base ServerURL.
@@ -441,6 +449,7 @@ func (sm *SessionManager) connectOnce(ctx context.Context, cfg SessionConfig, ta
sm.mu.Unlock()
sm.stats.SetConnected(conn.Init().IP, conn.Init().IP6)
sm.stats.SetConnectStep("load_routes")
sm.setState(stats.StateConnected)
log.L().Info("VPN connected",
"ip", conn.Init().IP, "server_ip", conn.Init().ServerIP,
@@ -451,10 +460,26 @@ func (sm *SessionManager) connectOnce(ctx context.Context, cfg SessionConfig, ta
statsDone := make(chan struct{})
go sm.reportStats(statsDone, ctx)
// Fetch "after-proxy" CIDR lists via the tunnel and dynamically add
// their routes. This runs in a goroutine so it doesn't block the
// packet pump.
go sm.fetchAfterProxyCIDRs(ctx, cfg)
// Apply deferred routes (user CIDRs) and fetch after-proxy CIDR
// lists in a background goroutine. For proxy/bypass modes with
// thousands of CIDRs this uses batch script execution (~3-5s).
go func() {
sm.mu.Lock()
mgr := sm.routeMgr
sm.mu.Unlock()
if mgr != nil {
sm.stats.SetRouteLoading(true)
sm.stats.SetConnectStep("load_routes")
log.L().Info("applying deferred routes")
if err := mgr.ApplyDeferred(); err != nil {
log.L().Error("apply deferred routes failed (continuing)", "error", err)
}
sm.stats.SetRouteLoading(false)
sm.stats.SetConnectStep("")
log.L().Info("deferred routes applied")
}
sm.fetchAfterProxyCIDRs(ctx, cfg)
}()
// Run the packet pump (blocks until connection breaks).
sm.pumpPackets(ctx, conn)
@@ -562,34 +587,95 @@ func (sm *SessionManager) fetchAfterProxyCIDRs(ctx context.Context, cfg SessionC
return
}
sm.stats.SetRouteLoading(true)
sm.stats.SetConnectStep("fetch_cidrs")
log.L().Info("fetching after-proxy CIDR lists", "url_count", afterCount)
fetched, err := cidrsource.FetchAfterProxy(ctx, allURLSources)
sm.stats.SetConnectStep("load_routes")
if err != nil {
log.L().Error("fetch after-proxy CIDR lists completed with errors", "error", err)
sm.stats.SetCIDRError(err.Error())
} else {
sm.stats.SetCIDRError("")
}
if len(fetched) == 0 {
sm.stats.SetRouteLoading(false)
sm.stats.SetConnectStep("")
return
}
merged := fetched // AddRoutes already calls mergeCIDRs internally
added := sm.addCIDRRoutes(fetched)
if added > 0 {
log.L().Info("added after-proxy routes", "fetched", len(fetched), "added", added, "merged", len(merged))
}
sm.stats.SetRouteLoading(false)
sm.stats.SetConnectStep("")
}
// addCIDRRoutes adds routes and updates the CIDR tracker. Returns the
// number of CIDRs successfully added (after merge).
func (sm *SessionManager) addCIDRRoutes(cidrs []string) int {
sm.mu.Lock()
mgr := sm.routeMgr
tracker := sm.cidrTracker
sm.mu.Unlock()
if mgr == nil {
return 0
}
if err := mgr.AddRoutes(cidrs); err != nil {
log.L().Error("add CIDR routes failed (continuing)", "error", err)
}
if tracker != nil {
tracker.AddCIDRs(cidrs)
}
return len(cidrs)
}
// RefreshCIDRs re-fetches all CIDR URL sources (both before and after
// timing) via the current tunnel and dynamically adds their routes.
// This is called when the user clicks the "Refresh CIDR" button.
func (sm *SessionManager) RefreshCIDRs() {
sm.mu.Lock()
ctx := sm.cancel
cfg := sm.lastCfg
sm.mu.Unlock()
if ctx == nil {
return
}
if err := mgr.AddRoutes(fetched); err != nil {
log.L().Error("add after-proxy routes failed (continuing)", "error", err)
} else {
log.L().Info("added after-proxy routes", "count", len(fetched))
// Use a background context with 30s timeout so the refresh works
// even if the session context is in a weird state.
refreshCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = refreshCtx
allURLSources := append(append([]model.CIDRURLSource{}, cfg.CIDRV4URLs...), cfg.CIDRV6URLs...)
if len(allURLSources) == 0 {
return
}
sm.mu.Lock()
tracker := sm.cidrTracker
sm.mu.Unlock()
if tracker != nil {
tracker.AddCIDRs(fetched)
sm.stats.SetRouteLoading(true)
sm.stats.SetCIDRError("")
log.L().Info("refreshing CIDR lists", "url_count", len(allURLSources))
fetched, err := cidrsource.FetchAfterProxy(refreshCtx, allURLSources)
if err != nil {
log.L().Error("refresh CIDR lists completed with errors", "error", err)
sm.stats.SetCIDRError(err.Error())
} else {
sm.stats.SetCIDRError("")
}
if len(fetched) == 0 {
sm.stats.SetRouteLoading(false)
return
}
added := sm.addCIDRRoutes(fetched)
log.L().Info("refreshed CIDR routes", "fetched", len(fetched), "added", added)
sm.stats.SetRouteLoading(false)
}
// pumpPackets runs the bidirectional packet loop until the connection