golib/net/ipgate/domainsetcsv.go
AJ ONeal dddfdd392c
feat(net/ipgate): IP allowlist/blocklist via PrefixSet and DomainSet
PrefixSet: git-backed CIDR files via gitshallow, refreshed every 47 min.
DomainSet: takes pre-parsed (staticPrefixes, domains []string); resolves
hostnames every 5 min, retaining stale IPs on failure. Both use
atomic-swapped ipcohort.Cohort for lock-free reads.

ParseDomainSet(RecordReader) parses rows into prefixes and hostnames;
caller constructs the record reader (e.g. csv.Reader with delimiter,
comment char). RecordReader interface is satisfied by *csv.Reader.

commaify uses plain string concat (integers max ~7 groups).
2026-05-22 12:23:35 -04:00

62 lines
1.3 KiB
Go

package ipgate
import (
"fmt"
"io"
"net/netip"
"strings"
)
// RecordReader is satisfied by *csv.Reader.
type RecordReader interface {
Read() (record []string, err error)
}
// ParseDomainSet reads rows from r and returns static CIDR prefixes and
// hostnames to resolve. The first row is skipped when it contains no IP,
// prefix, or dotted hostname (header detection).
func ParseDomainSet(r RecordReader) (staticPrefixes []string, domains []string, err error) {
firstRow := true
for {
record, readErr := r.Read()
if readErr == io.EOF {
break
}
if readErr != nil {
return nil, nil, fmt.Errorf("csv read: %w", readErr)
}
if len(record) == 0 {
continue
}
raw := strings.TrimSpace(record[0])
if firstRow {
firstRow = false
if _, err := netip.ParseAddr(raw); err != nil {
if _, err := netip.ParsePrefix(raw); err != nil {
if !strings.Contains(raw, ".") {
continue
}
}
}
}
if raw == "" {
continue
}
if _, err := netip.ParseAddr(raw); err == nil {
staticPrefixes = append(staticPrefixes, raw+"/32")
continue
}
if _, err := netip.ParsePrefix(raw); err == nil {
staticPrefixes = append(staticPrefixes, raw)
continue
}
domains = append(domains, raw)
}
return staticPrefixes, domains, nil
}