// 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< 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:]) }