Skip to content

Commit 47a00bf

Browse files
committed
Fix IP data merge correctness and overlays
1 parent f067399 commit 47a00bf

12 files changed

Lines changed: 571 additions & 153 deletions

File tree

internal/merger/merger.go

Lines changed: 134 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,13 @@ type Stats struct {
101101
GeoWhoisCountryHits int64
102102
QQWryHits int64
103103
OpenproxyDBHits int64
104+
OpenproxyDBCIDRRangesInserted int64
104105
BadASNHits int64
105106
EmptyRecords int64
106107
ProcessedNetworks int64
107108
SingleProxyIPsInserted int64
108109
ICloudPrivateRelayRangesInserted int64
110+
AnycastPrefixesInserted int64
109111
}
110112

111113
type asnSource uint8
@@ -331,12 +333,24 @@ func (m *Merger) Merge() error {
331333
}
332334
logMemStats("After DB-IP")
333335

336+
fmt.Println("Processing OpenProxyDB CIDR ranges (direct CIDR insertion)...")
337+
if err := m.processOpenProxyDBCIDRRanges(); err != nil {
338+
return fmt.Errorf("failed to process OpenProxyDB CIDR ranges: %w", err)
339+
}
340+
logMemStats("After OpenProxyDB CIDR ranges")
341+
334342
fmt.Println("Processing iCloud Private Relay ranges (direct CIDR insertion)...")
335343
if err := m.processICloudPrivateRelayRanges(); err != nil {
336344
return fmt.Errorf("failed to process iCloud Private Relay ranges: %w", err)
337345
}
338346
logMemStats("After iCloud Private Relay")
339347

348+
fmt.Println("Processing anycast prefixes (direct CDN insertion)...")
349+
if err := m.processAnycastPrefixes(); err != nil {
350+
return fmt.Errorf("failed to process anycast prefixes: %w", err)
351+
}
352+
logMemStats("After Anycast Prefixes")
353+
340354
fmt.Println("Processing single proxy IPs (direct /32 and /128 insertion)...")
341355
if err := m.processSingleProxyIPs(); err != nil {
342356
return fmt.Errorf("failed to process single proxy IPs: %w", err)
@@ -357,46 +371,6 @@ func (m *Merger) Merge() error {
357371
return nil
358372
}
359373

360-
// processGeoLiteCityNetworks iterates through GeoLite2-City and merges with other sources
361-
func (m *Merger) processGeoLiteCityNetworks() error {
362-
networks := m.geoLiteCity.Networks()
363-
364-
// Reuse a single record to reduce allocations
365-
var record MergedRecord
366-
367-
for networks.Next() {
368-
var geoRecord reader.GeoLite2CityRecord
369-
network, err := networks.Network(&geoRecord)
370-
if err != nil {
371-
fmt.Printf("Warning: failed to read network: %v\n", err)
372-
continue
373-
}
374-
375-
m.stats.TotalNetworks++
376-
377-
record.Reset()
378-
m.buildMergedRecord(network, &geoRecord, &record)
379-
380-
if record.IsEmpty() {
381-
m.stats.EmptyRecords++
382-
continue
383-
}
384-
385-
if err := m.tree.Insert(network, record.ToMMDBType()); err != nil {
386-
fmt.Printf("Warning: failed to insert network %s: %v\n", network, err)
387-
continue
388-
}
389-
390-
m.stats.ProcessedNetworks++
391-
392-
if m.stats.ProcessedNetworks%100000 == 0 {
393-
fmt.Printf(" Processed %d networks...\n", m.stats.ProcessedNetworks)
394-
}
395-
}
396-
397-
return networks.Err()
398-
}
399-
400374
// processGeoLiteCityNetworksParallel processes GeoLite2-City networks using parallel workers.
401375
// This significantly speeds up processing on multi-core systems by:
402376
// 1. Reading networks from GeoLite2-City sequentially (iterator is not thread-safe)
@@ -524,7 +498,7 @@ func (m *Merger) processDBIPReader(r *reader.Reader) error {
524498

525499
// Use reusable record to check if GeoLite2 has data for this IP
526500
m.reusableGeoLiteCityRecord.Reset()
527-
if err := m.geoLiteCity.LookupTo(ip, &m.reusableGeoLiteCityRecord); err == nil && m.reusableGeoLiteCityRecord.HasGeoData() {
501+
if err := m.geoLiteCity.LookupTo(ip, &m.reusableGeoLiteCityRecord); err == nil && m.reusableGeoLiteCityRecord.HasPrimaryGeoData() {
528502
continue
529503
}
530504

@@ -555,78 +529,20 @@ func (m *Merger) processDBIPReader(r *reader.Reader) error {
555529
return networks.Err()
556530
}
557531

558-
// buildMergedRecord creates a merged record for a network using GeoLite2-City as primary.
559-
// The record parameter should be pre-reset before calling this function.
560-
func (m *Merger) buildMergedRecord(network *net.IPNet, geoRecord *reader.GeoLite2CityRecord, record *MergedRecord) {
561-
if geoRecord.HasGeoData() {
562-
m.stats.GeoLiteCityHits++
563-
latitude, longitude, hasCoordinates := geoRecord.Coordinates()
564-
565-
// Source maps from maxminddb are read-only, safe to reference directly
566-
record.City = CityRecord{
567-
GeonameID: geoRecord.City.GeonameID,
568-
Names: geoRecord.City.Names,
569-
}
570-
571-
record.Continent = ContinentRecord{
572-
Code: geoRecord.Continent.Code,
573-
GeonameID: geoRecord.Continent.GeonameID,
574-
Names: geoRecord.Continent.Names,
575-
}
576-
577-
record.Country = CountryRecord{
578-
GeonameID: geoRecord.Country.GeonameID,
579-
ISOCode: geoRecord.Country.ISOCode,
580-
Names: geoRecord.Country.Names,
581-
}
582-
583-
record.Location = LocationRecord{
584-
AccuracyRadius: geoRecord.Location.AccuracyRadius,
585-
Latitude: latitude,
586-
Longitude: longitude,
587-
MetroCode: geoRecord.Location.MetroCode,
588-
TimeZone: geoRecord.Location.TimeZone,
589-
HasCoordinates: hasCoordinates,
590-
}
591-
592-
record.Postal = PostalRecord{
593-
Code: geoRecord.Postal.Code,
594-
}
595-
596-
record.RegisteredCountry = CountryRecord{
597-
GeonameID: geoRecord.RegisteredCountry.GeonameID,
598-
ISOCode: geoRecord.RegisteredCountry.ISOCode,
599-
Names: geoRecord.RegisteredCountry.Names,
600-
}
601-
602-
if len(geoRecord.Subdivisions) > 0 {
603-
record.Subdivisions = make([]SubdivisionRecord, len(geoRecord.Subdivisions))
604-
for i, sub := range geoRecord.Subdivisions {
605-
record.Subdivisions[i] = SubdivisionRecord{
606-
GeonameID: sub.GeonameID,
607-
ISOCode: sub.ISOCode,
608-
Names: sub.Names,
609-
}
610-
}
611-
}
612-
}
613-
614-
m.enrichWithASNData(network.IP, record)
615-
m.enrichWithCountryFallback(network.IP, record)
616-
m.enrichWithQQWryData(network.IP, record)
617-
m.enrichWithProxyData(network.IP, record)
618-
}
619-
620532
// buildMergedRecordFromDBIP creates a merged record using DB-IP as primary geo source.
621533
// The record parameter should be pre-reset before calling this function.
622534
func (m *Merger) buildMergedRecordFromDBIP(network *net.IPNet, dbipRecord *reader.DBIPCityRecord, record *MergedRecord) {
623535
if dbipRecord.HasGeoData() {
624-
record.City = CityRecord{
625-
Names: map[string]string{"en": dbipRecord.City},
536+
if dbipRecord.City != "" {
537+
record.City = CityRecord{
538+
Names: map[string]string{"en": dbipRecord.City},
539+
}
626540
}
627541

628-
record.Country = CountryRecord{
629-
ISOCode: dbipRecord.CountryCode,
542+
if dbipRecord.CountryCode != "" {
543+
record.Country = CountryRecord{
544+
ISOCode: dbipRecord.CountryCode,
545+
}
630546
}
631547

632548
if dbipRecord.HasLocationData() {
@@ -710,8 +626,10 @@ func (m *Merger) enrichWithQQWryData(ip net.IP, record *MergedRecord) {
710626
}
711627

712628
// Add Chinese country name if not present
713-
if _, ok := record.Country.Names["zh-CN"]; !ok {
714-
record.Country.Names = withName(record.Country.Names, "zh-CN", m.reusableQQWryRecord.CountryName)
629+
if m.reusableQQWryRecord.CountryName != "" {
630+
if _, ok := record.Country.Names["zh-CN"]; !ok {
631+
record.Country.Names = withName(record.Country.Names, "zh-CN", m.reusableQQWryRecord.CountryName)
632+
}
715633
}
716634
}
717635

@@ -866,6 +784,95 @@ func (m *Merger) processICloudPrivateRelayRanges() error {
866784
return nil
867785
}
868786

787+
// processOpenProxyDBCIDRRanges directly overlays every CIDR range from
788+
// OpenProxyDB. Lookup-time enrichment only samples a source network's base IP,
789+
// so direct insertion is required for exact proxy coverage when proxy ranges
790+
// are narrower than the geo/ASN networks already in the tree.
791+
func (m *Merger) processOpenProxyDBCIDRRanges() error {
792+
ranges := m.openproxyDB.CIDRRanges()
793+
if len(ranges) == 0 {
794+
fmt.Println("OpenProxyDB CIDR ranges: 0 inserted, 0 skipped")
795+
return nil
796+
}
797+
798+
inserted := 0
799+
skipped := 0
800+
for _, cidrRange := range ranges {
801+
proxy := ProxyRecord{
802+
IsProxy: cidrRange.Record.IsProxy,
803+
IsVPN: cidrRange.Record.IsVPN,
804+
IsTor: cidrRange.Record.IsTor,
805+
IsHosting: cidrRange.Record.IsHosting,
806+
IsCDN: cidrRange.Record.IsCDN,
807+
IsSchool: cidrRange.Record.IsSchool,
808+
IsAnonymous: cidrRange.Record.IsAnonymous,
809+
}
810+
proxyMMDB := proxy.toMMDBType()
811+
if proxyMMDB == nil {
812+
skipped++
813+
continue
814+
}
815+
816+
network := netipPrefixToIPNet(cidrRange.Prefix)
817+
if network == nil {
818+
skipped++
819+
continue
820+
}
821+
822+
if err := m.insertProxyMap(network, proxyMMDB); err != nil {
823+
if isSkippableInsertError(err) {
824+
skipped++
825+
continue
826+
}
827+
fmt.Printf("Warning: failed to insert OpenProxyDB CIDR range %s: %v\n", cidrRange.Prefix, err)
828+
skipped++
829+
continue
830+
}
831+
inserted++
832+
}
833+
834+
fmt.Printf("OpenProxyDB CIDR ranges: %d inserted, %d skipped (of %d total)\n", inserted, skipped, len(ranges))
835+
m.stats.OpenproxyDBCIDRRangesInserted = int64(inserted)
836+
return nil
837+
}
838+
839+
// processAnycastPrefixes directly overlays bgp.tools anycast prefixes with
840+
// the CDN flag. This avoids relying on geo/ASN source network boundaries to
841+
// happen to align with anycast CIDRs.
842+
func (m *Merger) processAnycastPrefixes() error {
843+
prefixes := m.openproxyDB.AnycastPrefixes()
844+
if len(prefixes) == 0 {
845+
fmt.Println("Anycast prefixes: 0 inserted, 0 skipped")
846+
return nil
847+
}
848+
849+
proxyMMDB := (&ProxyRecord{IsCDN: true}).toMMDBType()
850+
inserted := 0
851+
skipped := 0
852+
for _, prefix := range prefixes {
853+
network := netipPrefixToIPNet(prefix)
854+
if network == nil {
855+
skipped++
856+
continue
857+
}
858+
859+
if err := m.insertProxyMap(network, proxyMMDB); err != nil {
860+
if isSkippableInsertError(err) {
861+
skipped++
862+
continue
863+
}
864+
fmt.Printf("Warning: failed to insert anycast prefix %s: %v\n", prefix, err)
865+
skipped++
866+
continue
867+
}
868+
inserted++
869+
}
870+
871+
fmt.Printf("Anycast prefixes: %d inserted, %d skipped (of %d total)\n", inserted, skipped, len(prefixes))
872+
m.stats.AnycastPrefixesInserted = int64(inserted)
873+
return nil
874+
}
875+
869876
// processSingleProxyIPs directly inserts every single IP from OpenProxyDB and BadIPList
870877
// as /32 (IPv4) or /128 (IPv6) networks into the MMDB tree.
871878
// This ensures complete proxy coverage for individual IPs that would otherwise be missed
@@ -1002,8 +1009,23 @@ func mergeMMDBMaps(existing, new mmdbtype.Map) mmdbtype.Map {
10021009
}
10031010

10041011
for k, v := range new {
1005-
if _, exists := result[k]; !exists {
1012+
existingValue, exists := result[k]
1013+
if !exists {
10061014
result[k] = v
1015+
continue
1016+
}
1017+
if k == keyProxy {
1018+
if existingProxy, ok := existingValue.(mmdbtype.Map); ok {
1019+
if newProxy, ok := v.(mmdbtype.Map); ok {
1020+
result[k] = unionProxyMaps(existingProxy, newProxy)
1021+
}
1022+
}
1023+
continue
1024+
}
1025+
if existingMap, ok := existingValue.(mmdbtype.Map); ok {
1026+
if newMap, ok := v.(mmdbtype.Map); ok {
1027+
result[k] = mergeMMDBMaps(existingMap, newMap)
1028+
}
10071029
}
10081030
}
10091031

@@ -1046,8 +1068,10 @@ func (m *Merger) printStats() {
10461068
fmt.Printf(" GeoWhois Country fallback hits: %d\n", m.stats.GeoWhoisCountryHits)
10471069
fmt.Printf(" QQWry (Chunzhen) China enrichment hits: %d\n", m.stats.QQWryHits)
10481070
fmt.Printf(" OpenProxyDB proxy enrichment hits: %d\n", m.stats.OpenproxyDBHits)
1071+
fmt.Printf(" OpenProxyDB CIDR ranges inserted: %d\n", m.stats.OpenproxyDBCIDRRangesInserted)
10491072
fmt.Printf(" Bad ASN fallback hits: %d\n", m.stats.BadASNHits)
10501073
fmt.Printf(" iCloud Private Relay ranges inserted: %d\n", m.stats.ICloudPrivateRelayRangesInserted)
1074+
fmt.Printf(" Anycast prefixes inserted: %d\n", m.stats.AnycastPrefixesInserted)
10511075
fmt.Printf(" Single proxy IPs inserted (/32, /128): %d\n", m.stats.SingleProxyIPsInserted)
10521076
fmt.Printf(" Empty records skipped: %d\n", m.stats.EmptyRecords)
10531077
fmt.Printf(" Final network count: %d\n", m.stats.ProcessedNetworks)

0 commit comments

Comments
 (0)