Files
lmvpn_client/internal/route/cidrmerge.go
T
kevin ebe082500f
Release / build-macos (push) Canceled after 0s
Release / build-windows (push) Canceled after 0s
Release / release (push) Canceled after 0s
perf: 路由批量执行+CIDR聚合+并行脚本+连接步骤显示+CIDR刷新按钮
路由性能优化:
- 修复 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 聚合与批量执行说明
2026-07-09 09:44:49 +08:00

211 lines
4.8 KiB
Go

// cidrmerge.go merges adjacent CIDR blocks to reduce the total number
// of routes. For example, 1.0.1.0/24 + 1.0.2.0/23 -> 1.0.0.0/22.
// This is critical for large lists like chnroute (8786 entries) where
// many blocks can be merged, cutting the route count by 50-70%.
package route
import (
"net"
"sort"
)
// mergeCIDRs takes a list of CIDR strings, parses them, merges
// adjacent blocks, and returns a deduplicated, minimized list.
// Invalid CIDR strings are silently skipped.
func mergeCIDRs(cidrs []string) []string {
if len(cidrs) == 0 {
return nil
}
var nets []netEntry
for _, s := range cidrs {
_, n, err := net.ParseCIDR(s)
if err != nil {
continue
}
ones, _ := n.Mask.Size()
isV6 := n.IP.To4() == nil
if isV6 {
nets = append(nets, netEntry{ip: n.IP.To16(), bits: ones})
} else {
nets = append(nets, netEntry{ip: n.IP.To4(), bits: ones})
}
}
// Split into v4 and v6, merge separately.
var v4nets, v6nets []netEntry
for _, n := range nets {
if n.ip.To4() != nil && len(n.ip) == 4 {
v4nets = append(v4nets, n)
} else if n.ip.To4() == nil && len(n.ip) == 16 {
v6nets = append(v6nets, n)
}
}
mergedV4 := mergeNets(v4nets, 32)
mergedV6 := mergeNets(v6nets, 128)
var result []string
for _, n := range mergedV4 {
result = append(result, n.ip.String()+"/"+itoa(n.bits))
}
for _, n := range mergedV6 {
result = append(result, n.ip.String()+"/"+itoa(n.bits))
}
return result
}
type netEntry struct {
ip net.IP
bits int
}
type sortableNet struct {
ip []byte
bits int
}
func mergeNets(nets []netEntry, maxBits int) []netEntry {
if len(nets) == 0 {
return nil
}
// Convert to sortable form.
var sn []sortableNet
for _, n := range nets {
sn = append(sn, sortableNet{ip: []byte(n.ip), bits: n.bits})
}
// Sort by IP then by prefix length (more specific first).
sort.Slice(sn, func(i, j int) bool {
return cmpIP(sn[i].ip, sn[j].ip) < 0 ||
(cmpIP(sn[i].ip, sn[j].ip) == 0 && sn[i].bits < sn[j].bits)
})
// Merge: repeatedly try to combine pairs into supernets.
// Two adjacent /n networks can merge into a /(n-1) if:
// 1. Both have the same prefix length n
// 2. Their network addresses differ only in bit n (i.e. one ends in 0, the other in 1 at position n)
// 3. The merged address has bit n cleared
changed := true
for changed {
changed = false
var merged []sortableNet
i := 0
for i < len(sn) {
if i+1 < len(sn) && canMerge(sn[i], sn[i+1], maxBits) {
mergedNet := mergePair(sn[i], maxBits)
merged = append(merged, mergedNet)
i += 2
changed = true
} else {
merged = append(merged, sn[i])
i++
}
}
sn = merged
// Re-sort after merge (merged pairs may be out of order).
if changed {
sort.Slice(sn, func(i, j int) bool {
return cmpIP(sn[i].ip, sn[j].ip) < 0 ||
(cmpIP(sn[i].ip, sn[j].ip) == 0 && sn[i].bits < sn[j].bits)
})
}
}
// Convert back.
var result []netEntry
for _, n := range sn {
result = append(result, netEntry{ip: net.IP(n.ip), bits: n.bits})
}
return result
}
func canMerge(a, b sortableNet, maxBits int) bool {
if a.bits != b.bits || a.bits == 0 {
return false
}
if len(a.ip) != len(b.ip) {
return false
}
// Check that they share the same prefix up to bit (a.bits - 1).
prefixBits := a.bits - 1
byteIdx := prefixBits / 8
bitIdx := uint(7 - (prefixBits % 8))
// All bytes before byteIdx must be equal.
for k := 0; k < byteIdx; k++ {
if a.ip[k] != b.ip[k] {
return false
}
}
// In byteIdx, bits above bitIdx must be equal.
mask := byte(0xFF << (bitIdx + 1))
if a.ip[byteIdx]&mask != b.ip[byteIdx]&mask {
return false
}
// The bit at bitIdx must differ (one is 0, one is 1).
bitA := (a.ip[byteIdx] >> bitIdx) & 1
bitB := (b.ip[byteIdx] >> bitIdx) & 1
if bitA == bitB {
return false
}
// The lower bits of both must be zero (network address).
lowerMask := byte(1<<bitIdx) - 1
if a.ip[byteIdx]&lowerMask != 0 || b.ip[byteIdx]&lowerMask != 0 {
return false
}
// All bytes after byteIdx must be zero.
for k := byteIdx + 1; k < len(a.ip); k++ {
if a.ip[k] != 0 || b.ip[k] != 0 {
return false
}
}
return true
}
func mergePair(a sortableNet, maxBits int) sortableNet {
result := make([]byte, len(a.ip))
copy(result, a.ip)
// Clear the bit at position (a.bits - 1).
prefixBits := a.bits - 1
byteIdx := prefixBits / 8
bitIdx := uint(7 - (prefixBits % 8))
result[byteIdx] &^= 1 << bitIdx
return sortableNet{ip: result, bits: a.bits - 1}
}
func cmpIP(a, b []byte) int {
for i := 0; i < len(a) && i < len(b); i++ {
if a[i] < b[i] {
return -1
}
if a[i] > b[i] {
return 1
}
}
return len(a) - len(b)
}
func itoa(n int) string {
if n == 0 {
return "0"
}
var buf [20]byte
pos := len(buf)
negative := n < 0
if negative {
n = -n
}
for n > 0 {
pos--
buf[pos] = byte('0' + n%10)
n /= 10
}
if negative {
pos--
buf[pos] = '-'
}
return string(buf[pos:])
}