master
go 1,068 lines 28.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package main
4
5 import (
6 "archive/tar"
7 "archive/zip"
8 "bufio"
9 "bytes"
10 "compress/gzip"
11 "encoding/csv"
12 "fmt"
13 "io"
14 "math/big"
15 "net"
16 "net/netip"
17 "path"
18 "strconv"
19 "strings"
20
21 "github.com/oschwald/maxminddb-golang"
22 "go4.org/netipx"
23 )
24
25 type dbipAsnMMDBRecord struct {
26 AutonomousSystemNumber *uint32 `maxminddb:"autonomous_system_number"`
27 AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
28 }
29
30 type dbipCountryMMDBValue struct {
31 ISOCode string `maxminddb:"iso_code"`
32 }
33
34 type dbipCityMMDBValue struct {
35 Names map[string]string `maxminddb:"names"`
36 }
37
38 type dbipSubdivisionMMDBValue struct {
39 ISOCode string `maxminddb:"iso_code"`
40 Names map[string]string `maxminddb:"names"`
41 }
42
43 type dbipLocationMMDBValue struct {
44 Latitude float64 `maxminddb:"latitude"`
45 Longitude float64 `maxminddb:"longitude"`
46 }
47
48 type dbipGeoMMDBRecord struct {
49 Country *dbipCountryMMDBValue `maxminddb:"country"`
50 City *dbipCityMMDBValue `maxminddb:"city"`
51 Subdivisions []dbipSubdivisionMMDBValue `maxminddb:"subdivisions"`
52 Region string `maxminddb:"region"`
53 Location *dbipLocationMMDBValue `maxminddb:"location"`
54 }
55
56 func loadRanges(
57 cfg config,
58 dl *downloader,
59 ) ([]asnRange, []geoRange, []generationDatasetRef, error) {
60 asnSources := make([][]asnRange, 0)
61 geoSources := make([][]geoRange, 0)
62 sourceRefs := make([]generationDatasetRef, 0, len(cfg.sources))
63
64 for i, source := range cfg.sources {
65 ref, content, err := dl.readDataset(source)
66 if err != nil {
67 return nil, nil, nil, fmt.Errorf("source %d (%s): %w", i, sourceLabel(source, i), err)
68 }
69 sourceRefs = append(sourceRefs, ref)
70
71 switch source.family {
72 case sourceFamilyASN:
73 asnRanges, err := parseASNSource(source, content)
74 if err != nil {
75 return nil, nil, nil, fmt.Errorf("source %d (%s): %w", i, sourceLabel(source, i), err)
76 }
77 asnSources = append(asnSources, asnRanges)
78 case sourceFamilyGeo:
79 geoRanges, err := parseGeoSource(source, content)
80 if err != nil {
81 return nil, nil, nil, fmt.Errorf("source %d (%s): %w", i, sourceLabel(source, i), err)
82 }
83 geoSources = append(geoSources, geoRanges)
84 default:
85 return nil, nil, nil, fmt.Errorf(
86 "source %d (%s): unsupported family %q",
87 i,
88 sourceLabel(source, i),
89 source.family,
90 )
91 }
92 }
93
94 mergedASN, err := mergeAsnSources(asnSources)
95 if err != nil {
96 return nil, nil, nil, err
97 }
98 mergedGeo, err := mergeGeoSources(geoSources)
99 if err != nil {
100 return nil, nil, nil, err
101 }
102
103 return mergedASN, mergedGeo, sourceRefs, nil
104 }
105
106 func sourceLabel(source sourceEntry, index int) string {
107 if source.name != "" {
108 return source.name
109 }
110 return fmt.Sprintf("%s-%d", source.family, index)
111 }
112
113 func parseASNSource(source sourceEntry, payload []byte) ([]asnRange, error) {
114 switch {
115 case source.provider == providerIPToASN && source.artifact == artifactIPToASNCombined:
116 if source.format != formatTSV {
117 return nil, fmt.Errorf("iptoasn combined requires tsv format, got %q", source.format)
118 }
119 return parseIPToASNCombinedTSVAsn(payload)
120 case source.provider == providerDBIP && source.artifact == artifactDBIPASNLite:
121 switch source.format {
122 case formatCSV:
123 return parseDBIPAsnCSV(payload)
124 case formatMMDB:
125 return parseDBIPAsnMMDB(payload)
126 default:
127 return nil, fmt.Errorf("unsupported dbip ASN format %q", source.format)
128 }
129 case source.provider == providerCAIDA && source.artifact == artifactCAIDAPrefix2AS:
130 if source.format != formatTSV {
131 return nil, fmt.Errorf("caida prefix2as requires tsv format, got %q", source.format)
132 }
133 return parseCAIDAPrefix2AS(payload)
134 case source.provider == providerMaxMind && source.artifact == artifactMaxMindGeoLite2ASN:
135 if source.format != formatMMDB {
136 return nil, fmt.Errorf("maxmind geolite2 ASN requires mmdb format, got %q", source.format)
137 }
138 return parseDBIPAsnMMDB(payload)
139 default:
140 return nil, fmt.Errorf(
141 "unsupported ASN source %q/%q",
142 source.provider,
143 source.artifact,
144 )
145 }
146 }
147
148 func parseGeoSource(source sourceEntry, payload []byte) ([]geoRange, error) {
149 switch {
150 case source.provider == providerIPToASN && source.artifact == artifactIPToASNCombined:
151 if source.format != formatTSV {
152 return nil, fmt.Errorf("iptoasn combined requires tsv format, got %q", source.format)
153 }
154 return parseIPToASNCombinedTSVGeo(payload)
155 case source.provider == providerDBIP && source.artifact == artifactDBIPCountryLite:
156 switch source.format {
157 case formatCSV:
158 return parseDBIPCountryCSV(payload)
159 case formatMMDB:
160 return parseDBIPGeoMMDB(payload)
161 default:
162 return nil, fmt.Errorf("unsupported dbip GEO format %q", source.format)
163 }
164 case source.provider == providerDBIP && source.artifact == artifactDBIPCityLite:
165 switch source.format {
166 case formatCSV:
167 return parseDBIPCityCSV(payload)
168 case formatMMDB:
169 return parseDBIPGeoMMDB(payload)
170 default:
171 return nil, fmt.Errorf("unsupported dbip GEO format %q", source.format)
172 }
173 case source.provider == providerMaxMind && source.artifact == artifactMaxMindGeoLite2Country:
174 if source.format != formatCSV {
175 return nil, fmt.Errorf("maxmind geolite2 country requires csv format, got %q", source.format)
176 }
177 return parseMaxMindCountryCSVZip(payload)
178 case source.provider == providerIP2Location && source.artifact == artifactIP2LocationCountryLite:
179 if source.format != formatCSV {
180 return nil, fmt.Errorf("ip2location country-lite requires csv format, got %q", source.format)
181 }
182 return parseIP2LocationCountryZip(payload)
183 case source.provider == providerIPDeny && source.artifact == artifactIPDenyCountryZones:
184 if source.format != formatCIDR {
185 return nil, fmt.Errorf("ipdeny country-zones requires cidr format, got %q", source.format)
186 }
187 return parseIPDenyCountryTarGZ(payload)
188 case source.provider == providerIPIP && source.artifact == artifactIPIPCountry:
189 if source.format != formatTXT {
190 return nil, fmt.Errorf("ipip country requires txt format, got %q", source.format)
191 }
192 return parseIPIPCountryZip(payload)
193 default:
194 return nil, fmt.Errorf(
195 "unsupported GEO source %q/%q",
196 source.provider,
197 source.artifact,
198 )
199 }
200 }
201
202 func estimatedRangeCapacity(size uint64, averageLineBytes, maxCapacity int) int {
203 if size == 0 || averageLineBytes <= 0 || maxCapacity <= 0 {
204 return 0
205 }
206 capacity := size / uint64(averageLineBytes)
207 if capacity > uint64(maxCapacity) {
208 return maxCapacity
209 }
210 return int(capacity)
211 }
212
213 func parseIPToASNCombinedTSVAsn(payload []byte) ([]asnRange, error) {
214 asnRanges := make([]asnRange, 0, estimatedRangeCapacity(uint64(len(payload)), 64, 1<<20))
215
216 scanner := bufio.NewScanner(bytes.NewReader(payload))
217 lineNo := 0
218 for scanner.Scan() {
219 lineNo++
220 line := strings.TrimSpace(scanner.Text())
221 if line == "" || strings.HasPrefix(line, "#") {
222 continue
223 }
224 parts := strings.Split(line, "\t")
225 if len(parts) < 5 {
226 return nil, fmt.Errorf("iptoasn line %d: expected at least 5 columns", lineNo)
227 }
228 start, end, err := parseRangeEndpoints(parts[0], parts[1])
229 if err != nil {
230 return nil, fmt.Errorf("iptoasn line %d: %w", lineNo, err)
231 }
232 asn, err := parseASN(parts[2])
233 if err != nil {
234 return nil, fmt.Errorf("iptoasn line %d: %w", lineNo, err)
235 }
236 org := strings.TrimSpace(parts[4])
237
238 asnRange := asnRange{start: start, end: end, asn: asn, org: org}
239 if err := asnRange.validate(); err != nil {
240 return nil, fmt.Errorf("iptoasn line %d: %w", lineNo, err)
241 }
242 asnRanges = append(asnRanges, asnRange)
243 }
244 if err := scanner.Err(); err != nil {
245 return nil, fmt.Errorf("failed to scan iptoasn payload: %w", err)
246 }
247 return asnRanges, nil
248 }
249
250 func parseIPToASNCombinedTSVGeo(payload []byte) ([]geoRange, error) {
251 geoRanges := make([]geoRange, 0, estimatedRangeCapacity(uint64(len(payload)), 64, 1<<20))
252
253 scanner := bufio.NewScanner(bytes.NewReader(payload))
254 lineNo := 0
255 for scanner.Scan() {
256 lineNo++
257 line := strings.TrimSpace(scanner.Text())
258 if line == "" || strings.HasPrefix(line, "#") {
259 continue
260 }
261 parts := strings.Split(line, "\t")
262 if len(parts) < 5 {
263 return nil, fmt.Errorf("iptoasn line %d: expected at least 5 columns", lineNo)
264 }
265 start, end, err := parseRangeEndpoints(parts[0], parts[1])
266 if err != nil {
267 return nil, fmt.Errorf("iptoasn line %d: %w", lineNo, err)
268 }
269 country := normalizeCountry(parts[3])
270 if country == "" {
271 continue
272 }
273 rec := geoRange{start: start, end: end, country: country}
274 if err := rec.validate(); err != nil {
275 return nil, fmt.Errorf("iptoasn line %d: %w", lineNo, err)
276 }
277 geoRanges = append(geoRanges, rec)
278 }
279 if err := scanner.Err(); err != nil {
280 return nil, fmt.Errorf("failed to scan iptoasn payload: %w", err)
281 }
282 return geoRanges, nil
283 }
284
285 func parseDBIPAsnCSV(payload []byte) ([]asnRange, error) {
286 reader := csv.NewReader(strings.NewReader(string(payload)))
287 reader.FieldsPerRecord = -1
288 reader.TrimLeadingSpace = true
289
290 out := make([]asnRange, 0, estimatedRangeCapacity(uint64(len(payload)), 80, 1<<18))
291 lineNo := 0
292 for {
293 row, err := reader.Read()
294 if err != nil {
295 if err == io.EOF {
296 break
297 }
298 return nil, fmt.Errorf("dbip asn line %d: %w", lineNo+1, err)
299 }
300 lineNo++
301 if len(row) == 0 {
302 continue
303 }
304 if strings.HasPrefix(strings.TrimSpace(row[0]), "#") {
305 continue
306 }
307 if len(row) < 3 {
308 return nil, fmt.Errorf("dbip asn line %d: expected >= 3 columns", lineNo)
309 }
310 start, end, err := parseRangeEndpoints(row[0], row[1])
311 if err != nil {
312 return nil, fmt.Errorf("dbip asn line %d: %w", lineNo, err)
313 }
314 asn, err := parseASN(row[2])
315 if err != nil {
316 return nil, fmt.Errorf("dbip asn line %d: %w", lineNo, err)
317 }
318 org := ""
319 if len(row) > 3 {
320 org = strings.TrimSpace(row[3])
321 }
322 rec := asnRange{start: start, end: end, asn: asn, org: org}
323 if err := rec.validate(); err != nil {
324 return nil, fmt.Errorf("dbip asn line %d: %w", lineNo, err)
325 }
326 out = append(out, rec)
327 }
328 return out, nil
329 }
330
331 func parseCAIDAPrefix2AS(payload []byte) ([]asnRange, error) {
332 out := make([]asnRange, 0, estimatedRangeCapacity(uint64(len(payload)), 32, 1<<20))
333 scanner := bufio.NewScanner(bytes.NewReader(payload))
334 scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
335
336 lineNo := 0
337 for scanner.Scan() {
338 lineNo++
339 line := strings.TrimSpace(scanner.Text())
340 if line == "" || strings.HasPrefix(line, "#") {
341 continue
342 }
343
344 fields := strings.Fields(line)
345 if len(fields) < 3 {
346 return nil, fmt.Errorf("caida prefix2as line %d: expected >= 3 columns", lineNo)
347 }
348
349 prefixLength, err := strconv.Atoi(strings.TrimSpace(fields[1]))
350 if err != nil {
351 return nil, fmt.Errorf("caida prefix2as line %d: invalid prefix length %q: %w", lineNo, fields[1], err)
352 }
353 prefix, err := netip.ParsePrefix(fmt.Sprintf("%s/%d", strings.TrimSpace(fields[0]), prefixLength))
354 if err != nil {
355 return nil, fmt.Errorf("caida prefix2as line %d: invalid prefix: %w", lineNo, err)
356 }
357 start, end := rangeFromPrefix(prefix.Masked())
358
359 asn, err := parsePrimaryASN(fields[2])
360 if err != nil {
361 return nil, fmt.Errorf("caida prefix2as line %d: %w", lineNo, err)
362 }
363 if asn == 0 {
364 continue
365 }
366
367 rec := asnRange{start: start, end: end, asn: asn}
368 if err := rec.validate(); err != nil {
369 return nil, fmt.Errorf("caida prefix2as line %d: %w", lineNo, err)
370 }
371 out = append(out, rec)
372 }
373 if err := scanner.Err(); err != nil {
374 return nil, fmt.Errorf("failed to scan caida prefix2as payload: %w", err)
375 }
376 return out, nil
377 }
378
379 func parseDBIPCountryCSV(payload []byte) ([]geoRange, error) {
380 reader := csv.NewReader(strings.NewReader(string(payload)))
381 reader.FieldsPerRecord = -1
382 reader.TrimLeadingSpace = true
383
384 out := make([]geoRange, 0, estimatedRangeCapacity(uint64(len(payload)), 64, 1<<18))
385 lineNo := 0
386 for {
387 row, err := reader.Read()
388 if err != nil {
389 if err == io.EOF {
390 break
391 }
392 return nil, fmt.Errorf("dbip country line %d: %w", lineNo+1, err)
393 }
394 lineNo++
395 if len(row) == 0 {
396 continue
397 }
398 if strings.HasPrefix(strings.TrimSpace(row[0]), "#") {
399 continue
400 }
401 if len(row) < 3 {
402 return nil, fmt.Errorf("dbip country line %d: expected >= 3 columns", lineNo)
403 }
404 start, end, err := parseRangeEndpoints(row[0], row[1])
405 if err != nil {
406 return nil, fmt.Errorf("dbip country line %d: %w", lineNo, err)
407 }
408 country := normalizeCountry(row[2])
409 if country == "" {
410 continue
411 }
412 rec := geoRange{start: start, end: end, country: country}
413 if err := rec.validate(); err != nil {
414 return nil, fmt.Errorf("dbip country line %d: %w", lineNo, err)
415 }
416 out = append(out, rec)
417 }
418 return out, nil
419 }
420
421 func parseDBIPCityCSV(payload []byte) ([]geoRange, error) {
422 reader := csv.NewReader(strings.NewReader(string(payload)))
423 reader.FieldsPerRecord = -1
424 reader.TrimLeadingSpace = true
425
426 out := make([]geoRange, 0, estimatedRangeCapacity(uint64(len(payload)), 128, 1<<18))
427 lineNo := 0
428 for {
429 row, err := reader.Read()
430 if err != nil {
431 if err == io.EOF {
432 break
433 }
434 return nil, fmt.Errorf("dbip city line %d: %w", lineNo+1, err)
435 }
436 lineNo++
437 if len(row) == 0 {
438 continue
439 }
440 if strings.HasPrefix(strings.TrimSpace(row[0]), "#") {
441 continue
442 }
443 if len(row) < 8 {
444 return nil, fmt.Errorf("dbip city line %d: expected >= 8 columns", lineNo)
445 }
446
447 start, end, err := parseRangeEndpoints(row[0], row[1])
448 if err != nil {
449 return nil, fmt.Errorf("dbip city line %d: %w", lineNo, err)
450 }
451
452 country := normalizeCountry(row[3])
453 state := strings.TrimSpace(row[4])
454 city := strings.TrimSpace(row[5])
455
456 rec := geoRange{
457 start: start,
458 end: end,
459 country: country,
460 state: state,
461 city: city,
462 }
463
464 if latRaw := strings.TrimSpace(row[6]); latRaw != "" {
465 lat, err := strconv.ParseFloat(latRaw, 64)
466 if err != nil {
467 return nil, fmt.Errorf("dbip city line %d: invalid latitude %q: %w", lineNo, latRaw, err)
468 }
469 rec.latitude = lat
470 rec.hasLocation = true
471 }
472 if lonRaw := strings.TrimSpace(row[7]); lonRaw != "" {
473 lon, err := strconv.ParseFloat(lonRaw, 64)
474 if err != nil {
475 return nil, fmt.Errorf("dbip city line %d: invalid longitude %q: %w", lineNo, lonRaw, err)
476 }
477 rec.longitude = lon
478 rec.hasLocation = true
479 }
480
481 if err := rec.validate(); err != nil {
482 return nil, fmt.Errorf("dbip city line %d: %w", lineNo, err)
483 }
484 out = append(out, rec)
485 }
486 return out, nil
487 }
488
489 func parseDBIPAsnMMDB(payload []byte) ([]asnRange, error) {
490 reader, err := maxminddb.FromBytes(payload)
491 if err != nil {
492 return nil, fmt.Errorf("failed to open dbip ASN mmdb: %w", err)
493 }
494
495 out := make([]asnRange, 0)
496 networks := reader.Networks(maxminddb.SkipAliasedNetworks)
497 for networks.Next() {
498 var record dbipAsnMMDBRecord
499 network, err := networks.Network(&record)
500 if err != nil {
501 return nil, fmt.Errorf("failed to decode dbip ASN network: %w", err)
502 }
503 start, end, err := rangeFromIPNet(network)
504 if err != nil {
505 return nil, err
506 }
507 asn := uint32(0)
508 if record.AutonomousSystemNumber != nil {
509 asn = *record.AutonomousSystemNumber
510 }
511 rec := asnRange{
512 start: start,
513 end: end,
514 asn: asn,
515 org: strings.TrimSpace(record.AutonomousSystemOrganization),
516 }
517 if rec.asn == 0 && rec.org == "" {
518 continue
519 }
520 if err := rec.validate(); err != nil {
521 return nil, err
522 }
523 out = append(out, rec)
524 }
525 if err := networks.Err(); err != nil {
526 return nil, fmt.Errorf("failed to iterate dbip ASN mmdb: %w", err)
527 }
528 return out, nil
529 }
530
531 func parseDBIPGeoMMDB(payload []byte) ([]geoRange, error) {
532 reader, err := maxminddb.FromBytes(payload)
533 if err != nil {
534 return nil, fmt.Errorf("failed to open dbip GEO mmdb: %w", err)
535 }
536
537 out := make([]geoRange, 0)
538 networks := reader.Networks(maxminddb.SkipAliasedNetworks)
539 for networks.Next() {
540 var record dbipGeoMMDBRecord
541 network, err := networks.Network(&record)
542 if err != nil {
543 return nil, fmt.Errorf("failed to decode dbip GEO network: %w", err)
544 }
545 start, end, err := rangeFromIPNet(network)
546 if err != nil {
547 return nil, err
548 }
549
550 rec := geoRange{
551 start: start,
552 end: end,
553 country: dbipCountryCode(record.Country),
554 state: dbipStateName(record),
555 city: dbipCityName(record.City),
556 }
557 if record.Location != nil {
558 rec.latitude = record.Location.Latitude
559 rec.longitude = record.Location.Longitude
560 rec.hasLocation = true
561 }
562 if rec.country == "" && rec.state == "" && rec.city == "" && !rec.hasLocation {
563 continue
564 }
565 if err := rec.validate(); err != nil {
566 return nil, err
567 }
568 out = append(out, rec)
569 }
570 if err := networks.Err(); err != nil {
571 return nil, fmt.Errorf("failed to iterate dbip GEO mmdb: %w", err)
572 }
573 return out, nil
574 }
575
576 func parseMaxMindCountryCSVZip(payload []byte) ([]geoRange, error) {
577 archive, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload)))
578 if err != nil {
579 return nil, fmt.Errorf("failed to open maxmind country csv zip: %w", err)
580 }
581
582 countryByID, err := parseMaxMindCountryLocations(archive)
583 if err != nil {
584 return nil, err
585 }
586
587 out := make([]geoRange, 0)
588 for _, suffix := range []string{
589 "GeoLite2-Country-Blocks-IPv4.csv",
590 "GeoLite2-Country-Blocks-IPv6.csv",
591 } {
592 ranges, err := parseMaxMindCountryBlocks(archive, suffix, countryByID)
593 if err != nil {
594 return nil, err
595 }
596 out = append(out, ranges...)
597 }
598 return out, nil
599 }
600
601 func parseMaxMindCountryLocations(archive *zip.Reader) (map[string]string, error) {
602 file, err := openZipEntrySuffix(archive, "GeoLite2-Country-Locations-en.csv")
603 if err != nil {
604 return nil, err
605 }
606 rc, err := file.Open()
607 if err != nil {
608 return nil, err
609 }
610 defer rc.Close()
611
612 csvr := csv.NewReader(rc)
613 csvr.FieldsPerRecord = -1
614 header, err := csvr.Read()
615 if err != nil {
616 return nil, fmt.Errorf("failed to read maxmind locations header: %w", err)
617 }
618 idx := csvHeaderIndex(header)
619 idIdx, ok := idx["geoname_id"]
620 if !ok {
621 return nil, fmt.Errorf("maxmind locations missing geoname_id column")
622 }
623 countryIdx, ok := idx["country_iso_code"]
624 if !ok {
625 return nil, fmt.Errorf("maxmind locations missing country_iso_code column")
626 }
627
628 out := map[string]string{}
629 lineNo := 1
630 for {
631 row, err := csvr.Read()
632 if err == io.EOF {
633 break
634 }
635 if err != nil {
636 return nil, fmt.Errorf("maxmind locations line %d: %w", lineNo+1, err)
637 }
638 lineNo++
639 if len(row) <= idIdx || len(row) <= countryIdx {
640 continue
641 }
642 id := strings.TrimSpace(row[idIdx])
643 country := normalizeCountry(row[countryIdx])
644 if id != "" && country != "" {
645 out[id] = country
646 }
647 }
648 return out, nil
649 }
650
651 func parseMaxMindCountryBlocks(
652 archive *zip.Reader,
653 suffix string,
654 countryByID map[string]string,
655 ) ([]geoRange, error) {
656 file, err := openZipEntrySuffix(archive, suffix)
657 if err != nil {
658 return nil, err
659 }
660 rc, err := file.Open()
661 if err != nil {
662 return nil, err
663 }
664 defer rc.Close()
665
666 csvr := csv.NewReader(rc)
667 csvr.FieldsPerRecord = -1
668 header, err := csvr.Read()
669 if err != nil {
670 return nil, fmt.Errorf("failed to read maxmind blocks header %s: %w", suffix, err)
671 }
672 idx := csvHeaderIndex(header)
673 networkIdx, ok := idx["network"]
674 if !ok {
675 return nil, fmt.Errorf("maxmind blocks %s missing network column", suffix)
676 }
677 idColumns := []string{"geoname_id", "registered_country_geoname_id", "represented_country_geoname_id"}
678
679 out := make([]geoRange, 0, estimatedRangeCapacity(file.UncompressedSize64, 96, 1<<18))
680 lineNo := 1
681 for {
682 row, err := csvr.Read()
683 if err == io.EOF {
684 break
685 }
686 if err != nil {
687 return nil, fmt.Errorf("maxmind blocks %s line %d: %w", suffix, lineNo+1, err)
688 }
689 lineNo++
690 if len(row) <= networkIdx {
691 continue
692 }
693
694 country := ""
695 for _, column := range idColumns {
696 columnIdx, ok := idx[column]
697 if !ok || len(row) <= columnIdx {
698 continue
699 }
700 if c := countryByID[strings.TrimSpace(row[columnIdx])]; c != "" {
701 country = c
702 break
703 }
704 }
705 if country == "" {
706 continue
707 }
708
709 rec, err := geoRangeFromToken(row[networkIdx], country)
710 if err != nil {
711 return nil, fmt.Errorf("maxmind blocks %s line %d: %w", suffix, lineNo, err)
712 }
713 out = append(out, rec)
714 }
715 return out, nil
716 }
717
718 func parseIP2LocationCountryZip(payload []byte) ([]geoRange, error) {
719 archive, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload)))
720 if err != nil {
721 return nil, fmt.Errorf("failed to open ip2location country zip: %w", err)
722 }
723 file, err := openZipEntryBase(archive, "IP2LOCATION-LITE-DB1.CSV")
724 if err != nil {
725 return nil, err
726 }
727 rc, err := file.Open()
728 if err != nil {
729 return nil, err
730 }
731 defer rc.Close()
732
733 csvr := csv.NewReader(rc)
734 csvr.FieldsPerRecord = -1
735 out := make([]geoRange, 0, estimatedRangeCapacity(file.UncompressedSize64, 64, 1<<18))
736 lineNo := 0
737 for {
738 row, err := csvr.Read()
739 if err == io.EOF {
740 break
741 }
742 if err != nil {
743 return nil, fmt.Errorf("ip2location line %d: %w", lineNo+1, err)
744 }
745 lineNo++
746 if len(row) < 3 {
747 continue
748 }
749 country := normalizeCountry(row[2])
750 if country == "" {
751 continue
752 }
753 start, end, err := parseRangeEndpoints(row[0], row[1])
754 if err != nil {
755 return nil, fmt.Errorf("ip2location line %d: %w", lineNo, err)
756 }
757 rec := geoRange{start: start, end: end, country: country}
758 if err := rec.validate(); err != nil {
759 return nil, fmt.Errorf("ip2location line %d: %w", lineNo, err)
760 }
761 out = append(out, rec)
762 }
763 return out, nil
764 }
765
766 func parseIPDenyCountryTarGZ(payload []byte) ([]geoRange, error) {
767 gz, err := gzip.NewReader(bytes.NewReader(payload))
768 if err != nil {
769 return nil, fmt.Errorf("failed to open ipdeny tar.gz: %w", err)
770 }
771 defer gz.Close()
772
773 out := make([]geoRange, 0)
774 tr := tar.NewReader(gz)
775 for {
776 header, err := tr.Next()
777 if err == io.EOF {
778 break
779 }
780 if err != nil {
781 return nil, fmt.Errorf("failed to read ipdeny tar: %w", err)
782 }
783 name := path.Base(header.Name)
784 zoneName, ok := strings.CutSuffix(strings.ToLower(name), ".zone")
785 if header.Typeflag != tar.TypeReg || !ok {
786 continue
787 }
788 country := normalizeCountry(zoneName)
789 if country == "" {
790 continue
791 }
792 ranges, err := parseCountryTokenLines(tr, country, "ipdeny "+header.Name)
793 if err != nil {
794 return nil, err
795 }
796 out = append(out, ranges...)
797 }
798 return out, nil
799 }
800
801 func parseIPIPCountryZip(payload []byte) ([]geoRange, error) {
802 archive, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload)))
803 if err != nil {
804 return nil, fmt.Errorf("failed to open ipip country zip: %w", err)
805 }
806 file, err := openZipEntryBase(archive, "country.txt")
807 if err != nil {
808 return nil, err
809 }
810 rc, err := file.Open()
811 if err != nil {
812 return nil, err
813 }
814 defer rc.Close()
815
816 out := make([]geoRange, 0, estimatedRangeCapacity(file.UncompressedSize64, 48, 1<<18))
817 scanner := bufio.NewScanner(rc)
818 scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
819 lineNo := 0
820 for scanner.Scan() {
821 lineNo++
822 fields := strings.Fields(strings.ReplaceAll(scanner.Text(), "\r", ""))
823 if len(fields) < 2 {
824 continue
825 }
826 country := normalizeCountry(fields[len(fields)-1])
827 if country == "" {
828 continue
829 }
830 rec, err := geoRangeFromToken(fields[0], country)
831 if err != nil {
832 return nil, fmt.Errorf("ipip line %d: %w", lineNo, err)
833 }
834 out = append(out, rec)
835 }
836 if err := scanner.Err(); err != nil {
837 return nil, fmt.Errorf("failed to scan ipip country payload: %w", err)
838 }
839 return out, nil
840 }
841
842 func dbipCountryCode(value *dbipCountryMMDBValue) string {
843 if value == nil {
844 return ""
845 }
846 return normalizeCountry(value.ISOCode)
847 }
848
849 func dbipCityName(value *dbipCityMMDBValue) string {
850 if value == nil {
851 return ""
852 }
853 if name := strings.TrimSpace(value.Names["en"]); name != "" {
854 return name
855 }
856 for _, name := range value.Names {
857 name = strings.TrimSpace(name)
858 if name != "" {
859 return name
860 }
861 }
862 return ""
863 }
864
865 func dbipStateName(record dbipGeoMMDBRecord) string {
866 if len(record.Subdivisions) > 0 {
867 if name := strings.TrimSpace(record.Subdivisions[0].Names["en"]); name != "" {
868 return name
869 }
870 for _, name := range record.Subdivisions[0].Names {
871 name = strings.TrimSpace(name)
872 if name != "" {
873 return name
874 }
875 }
876 if code := strings.TrimSpace(record.Subdivisions[0].ISOCode); code != "" {
877 return code
878 }
879 }
880 return strings.TrimSpace(record.Region)
881 }
882
883 func rangeFromIPNet(network *net.IPNet) (netip.Addr, netip.Addr, error) {
884 prefix, err := netip.ParsePrefix(network.String())
885 if err != nil {
886 return netip.Addr{}, netip.Addr{}, fmt.Errorf(
887 "failed to parse mmdb network %s: %w",
888 network.String(),
889 err,
890 )
891 }
892 rng := netipx.RangeOfPrefix(prefix.Masked())
893 return rng.From(), rng.To(), nil
894 }
895
896 func parseRangeEndpoints(startRaw, endRaw string) (netip.Addr, netip.Addr, error) {
897 start, err := parseIP(startRaw)
898 if err != nil {
899 return netip.Addr{}, netip.Addr{}, fmt.Errorf("invalid start address %q: %w", startRaw, err)
900 }
901 end, err := parseIP(endRaw)
902 if err != nil {
903 return netip.Addr{}, netip.Addr{}, fmt.Errorf("invalid end address %q: %w", endRaw, err)
904 }
905 if start.BitLen() != end.BitLen() {
906 return netip.Addr{}, netip.Addr{}, fmt.Errorf("mixed address families %q and %q", startRaw, endRaw)
907 }
908 if compareAddrs(start, end) > 0 {
909 return netip.Addr{}, netip.Addr{}, fmt.Errorf("start %s after end %s", start, end)
910 }
911 return start, end, nil
912 }
913
914 func parseIP(raw string) (netip.Addr, error) {
915 value := strings.TrimSpace(strings.Trim(raw, "\""))
916 if value == "" {
917 return netip.Addr{}, fmt.Errorf("empty ip")
918 }
919 if addr, err := netip.ParseAddr(value); err == nil {
920 return addr.Unmap(), nil
921 }
922
923 if num, ok := new(big.Int).SetString(value, 10); ok {
924 if num.Sign() < 0 {
925 return netip.Addr{}, fmt.Errorf("negative integer address")
926 }
927 if num.BitLen() <= 32 {
928 v := uint32(num.Uint64())
929 return netip.AddrFrom4([4]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}), nil
930 }
931 if num.BitLen() <= 128 {
932 b := num.FillBytes(make([]byte, 16))
933 var arr [16]byte
934 copy(arr[:], b)
935 return netip.AddrFrom16(arr), nil
936 }
937 }
938
939 return netip.Addr{}, fmt.Errorf("unsupported ip value")
940 }
941
942 func parseASN(raw string) (uint32, error) {
943 value := strings.TrimSpace(strings.Trim(raw, "\""))
944 if value == "" {
945 return 0, nil
946 }
947 if strings.HasPrefix(value, "AS") || strings.HasPrefix(value, "as") {
948 value = value[2:]
949 }
950 n, err := strconv.ParseUint(value, 10, 32)
951 if err != nil {
952 return 0, fmt.Errorf("invalid ASN %q: %w", raw, err)
953 }
954 return uint32(n), nil
955 }
956
957 func parsePrimaryASN(raw string) (uint32, error) {
958 value := strings.TrimSpace(strings.Trim(raw, "\"{}"))
959 if value == "" {
960 return 0, nil
961 }
962 for _, sep := range []string{"_", ","} {
963 if idx := strings.Index(value, sep); idx >= 0 {
964 value = strings.TrimSpace(value[:idx])
965 }
966 }
967 return parseASN(value)
968 }
969
970 func csvHeaderIndex(header []string) map[string]int {
971 out := make(map[string]int, len(header))
972 for i, name := range header {
973 out[strings.TrimSpace(name)] = i
974 }
975 return out
976 }
977
978 func openZipEntryBase(archive *zip.Reader, name string) (*zip.File, error) {
979 for _, file := range archive.File {
980 if path.Base(file.Name) == name {
981 return file, nil
982 }
983 }
984 return nil, fmt.Errorf("zip entry %q not found", name)
985 }
986
987 func openZipEntrySuffix(archive *zip.Reader, suffix string) (*zip.File, error) {
988 for _, file := range archive.File {
989 if strings.HasSuffix(file.Name, suffix) {
990 return file, nil
991 }
992 }
993 return nil, fmt.Errorf("zip entry with suffix %q not found", suffix)
994 }
995
996 func parseCountryTokenLines(r io.Reader, country, label string) ([]geoRange, error) {
997 out := make([]geoRange, 0)
998 scanner := bufio.NewScanner(r)
999 scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
1000 lineNo := 0
1001 for scanner.Scan() {
1002 lineNo++
1003 token := strings.TrimSpace(scanner.Text())
1004 if token == "" || strings.HasPrefix(token, "#") {
1005 continue
1006 }
1007 fields := strings.Fields(token)
1008 if len(fields) == 0 {
1009 continue
1010 }
1011 rec, err := geoRangeFromToken(fields[0], country)
1012 if err != nil {
1013 return nil, fmt.Errorf("%s line %d: %w", label, lineNo, err)
1014 }
1015 out = append(out, rec)
1016 }
1017 if err := scanner.Err(); err != nil {
1018 return nil, fmt.Errorf("failed to scan %s: %w", label, err)
1019 }
1020 return out, nil
1021 }
1022
1023 func geoRangeFromToken(raw, country string) (geoRange, error) {
1024 start, end, err := rangeFromToken(raw)
1025 if err != nil {
1026 return geoRange{}, err
1027 }
1028 rec := geoRange{start: start, end: end, country: country}
1029 if err := rec.validate(); err != nil {
1030 return geoRange{}, err
1031 }
1032 return rec, nil
1033 }
1034
1035 func rangeFromToken(raw string) (netip.Addr, netip.Addr, error) {
1036 token := strings.TrimSpace(strings.Trim(raw, "\""))
1037 if token == "" {
1038 return netip.Addr{}, netip.Addr{}, fmt.Errorf("empty range token")
1039 }
1040
1041 if strings.Contains(token, "/") {
1042 prefix, err := netip.ParsePrefix(token)
1043 if err != nil {
1044 return netip.Addr{}, netip.Addr{}, err
1045 }
1046 start, end := rangeFromPrefix(prefix.Masked())
1047 return start, end, nil
1048 }
1049
1050 if strings.Contains(token, "-") {
1051 left, right, ok := strings.Cut(strings.ReplaceAll(token, " ", ""), "-")
1052 if !ok {
1053 return netip.Addr{}, netip.Addr{}, fmt.Errorf("invalid range token %q", raw)
1054 }
1055 return parseRangeEndpoints(left, right)
1056 }
1057
1058 addr, err := parseIP(token)
1059 if err != nil {
1060 return netip.Addr{}, netip.Addr{}, err
1061 }
1062 return addr, addr, nil
1063 }
1064
1065 func rangeFromPrefix(prefix netip.Prefix) (netip.Addr, netip.Addr) {
1066 rng := netipx.RangeOfPrefix(prefix.Masked())
1067 return rng.From(), rng.To()
1068 }