AJ ONeal 07d4a2652c
feat!(ippolicy): share refreshed IP policy behavior
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
2026-09-09 02:55:09 -06:00
..

net/ippolicy

ippolicy composes independently refreshed IP sources into a fast policy check. It uses net/iplist for local/HTTP source refresh and provides IP matching, DNS refresh, and the legacy git blacklist.

Quick start

Construct each source with net/iplist.NewSource, then pass them to ippolicy.New:

whitelist, err := iplist.NewSource(ctx, iplist.SourceConfig{
    Source:     "allowed.csv",
    CacheDir:   "/var/cache/app/iplist",
    // HTTPClient: nil, // uses golib's internal client
})
if err != nil {
    return err
}
defer whitelist.Close()

blacklistExtra, err := iplist.NewSource(ctx, iplist.SourceConfig{
    Source:     "blocked-ips-extra.tsv",
    CacheDir:   "/var/cache/app/iplist",
    Optional:   true,
})
if err != nil {
    return err
}
defer blacklistExtra.Close()

gitBlacklist, err := ippolicy.NewPrefixSet(
    ctx,
    "https://github.com/bitwire-it/ipblocklist.git",
    "/var/cache/app/ipblocklist",
    []string{"ipblocklist.txt"},
    ippolicy.DefaultPrefixSetRefreshInterval,
)
if err != nil {
    return err
}
defer gitBlacklist.Close()

policy := ippolicy.New(ctx, ippolicy.Config{
    Whitelist:      whitelist,
    Blacklist:      gitBlacklist,
    BlacklistExtra: blacklistExtra,
    OnRefresh: func(e ippolicy.RefreshEvent) {
        slog.Info("ippolicy refresh", "kind", e.Kind, "err", e.Err)
    },
})
defer policy.Close()

switch policy.Evaluate(addr) {
case ippolicy.Blacklisted:
    // reject
case ippolicy.Whitelisted, ippolicy.Unlisted:
    // continue
}

Behavior

Each source refreshes independently and keeps its last valid data on failure. Whitelist matches take precedence over blacklist matches. The policy does not log; use OnRefresh for application logging and metrics.

A nil Whitelist produces a policy where every address evaluates to Unlisted. This intentionally drops the blacklists as well: without a whitelist the policy cannot distinguish "allowed" from "unknown", so blocking would be meaningless. The caller receives a RefreshFallback event so it can log or alert.