路由性能优化: - 修复 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 聚合与批量执行说明
211 lines
4.8 KiB
Go
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:])
|
|
}
|