Rename net/ipgate to net/ippolicy and compose independently refreshed
whitelist and blacklist sources into one IP decision policy.
- rename slog group and error prefixes from ipgate to ippolicy
- delete unused ParseDomainSet and RecordReader from domainsetcsv.go
- rewrite README to match the actual Config API
- document nil-whitelist behavior on New()
- bump iplist dependency to v0.8.5
- go mod tidy: remove stale ipgate and iplist entries
Add net/iplist for shared IP-policy source loading.
- Accept local TSV/CSV file paths or HTTP(S) URLs (including Google Sheets).
- Auto-detect TSV vs CSV format from file content.
- Send Accept: text/tab-separated-values header for content negotiation.
- Parse the first column and ignore comments (#) and optional "network" header.
- Expand nested list URLs up to MaxNestedSources (2) levels.
- Support Basic Auth from URL userinfo without sending credentials in the URL.
- Persist cached URL responses across restarts via httpcache.
- Refresh periodically and preserve the known-good cache on fetch or parse failure.
- Validate entries as IP address, CIDR range, or domain name (Validate()).
- Use golib/https.NewInternalClient() through httpcache.NewWith.
- Add README with usage examples and entry format documentation.
Ensure cohort is never nil:
- NewDomainSet stores an empty Cohort before rebuildCohort, so a parse
failure leaves an empty (non-nil) cohort instead of nil
- Contains uses CompareAndSwap to lazily self-initialise a zero-value
struct, making nil impossible after first access — no explicit nil
check needed
Extract the cache-validity check into a testable cachedCohortValid()
helper and add filesPresent() to detect when data files exist on disk.
Use strings.Builder in commaify() for efficiency.
Tests:
- TestPrefixSetContainsZeroValue: zero-value self-heals via CAS,
cohort is non-nil after first Contains call
- TestCachedCohortValidRequiresFiles: cache invalidation logic
- Downloads all editions listed in a GeoIP.conf
- Flags: -config, -dir, -fresh-days, -base-url (override
DownloadBase for custom mirror/proxy), -V/--version, -help
- Version output shows name, version, commit, date, copyright and
license (populated via ldflags at build time)
- Uses Basic auth with split credential encoding for readability
- ETag/Last-Modified caching via httpcache
Add a Go package for reading MaxMind GeoLite2 City and ASN databases.
- ParseConf: parses geoipupdate-style config files (AccountID,
LicenseKey, EditionIDs, DatabaseDirectory)
- Databases: holds open City + ASN readers; Open() loads both
editions from a directory, extracting .mmdb from tar.gz in memory
without writing to disk
- FindTarGz: resolves cache paths, preferring *_LATEST.tar.gz
- Lookup: returns city, region, country, ASN, and AS org for an IP
- DownloadBase constant, TarGzName helper, DefaultConfPaths and
DefaultCacheDir conveniences
Integration tests verify download and conditional-get freshness.
Conditional GET cache for periodic data sources. Persists ETag and
Last-Modified to a .meta sidecar so 304s survive process restarts.
Two independent rate-limit gates (MaxAge file-mtime, MinInterval
in-memory). Body cap via MaxBytes defends against fill-disk from a
hostile upstream or redirect target. Authorization (and any
caller-supplied) headers stripped on every redirect hop. Userinfo
in URL is redacted from error messages.
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).
Resolves A, CNAME chain, and SRV records via miekg/dns. Queries all
configured servers in parallel and returns the first success. Falls
back to OpenDNS/Cloudflare/Quad9 when /etc/resolv.conf is absent or empty.
- Default depth changed from 1 to 3: enough overlap for delta
compression even if the remote pushes 2 commits between fetches
- Keep --depth on fetch (not just clone) so shallow stays windowed
- GCInterval default changed: 0 now means every fetch (was disabled)
- GC sequence: reflog expire → prune → gc --prune=now
Expire makes old shallow entries' objects unreachable, prune removes
them cheaply (5MB RSS), then gc delta-compresses what remains (~10MB)
- Only GC when HEAD actually changed (skip no-op fetches)
- Dropped GCAggressive (no benefit in shallow clones)
Periodically-updated shallow clone for repos used as a data source.
One Fetch verb (clone-or-pull-or-skip) drives both the Fetcher
interface and per-File handles. MaxAge gates on FETCH_HEAD mtime so
short-lived CLI invocations don't hammer the remote across process
restarts; an in-memory 1s debounce coalesces concurrent Fetches.
Hardens git invocations against argument injection: validates
branch/ref shape, rejects URLs starting with '-', and uses '--'
separators before positional args. Userinfo in URL is redacted from
error output (both args echo and git's own messages).
IPv4 host + CIDR membership testing backed by sorted /32 binary search
and a linear scan of CIDR ranges. CSV/text loader with bounded reads
(maxBytes is required so callers pick a limit appropriate to their
input source — 1 KB allowlists through 100+ MB threat feeds). Returns
errors for unparseable IPs so callers can decide fail-open vs
fail-closed; partial cohorts are returned alongside parse errors.