@@ -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
111113type 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.
622534func (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