feat(ipgate): configure matcher refresh intervals

This commit is contained in:
AJ ONeal 2026-09-09 02:05:50 -06:00
parent 03722a2934
commit 1cb0235d9d
No known key found for this signature in database
2 changed files with 87 additions and 28 deletions

View File

@ -3,6 +3,7 @@ package ipgate
import ( import (
"context" "context"
"net/netip" "net/netip"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -10,13 +11,21 @@ import (
"github.com/therootcompany/golib/net/ipcohort" "github.com/therootcompany/golib/net/ipcohort"
) )
const domainSetRefreshInterval = 5 * time.Minute const DefaultDomainSetRefreshInterval = 5 * time.Minute
type DomainSet struct { type domainSources struct {
staticPrefixes []string staticPrefixes []string
domains []string domains []string
}
type DomainSet struct {
ctx context.Context
cancel context.CancelFunc
sources atomic.Pointer[domainSources]
resolved atomic.Pointer[map[string][]string] resolved atomic.Pointer[map[string][]string]
cohort atomic.Pointer[ipcohort.Cohort] cohort atomic.Pointer[ipcohort.Cohort]
start sync.Once
interval time.Duration
} }
func EmptyDomainSet() *DomainSet { func EmptyDomainSet() *DomainSet {
@ -28,11 +37,16 @@ func EmptyDomainSet() *DomainSet {
// NewDomainSet creates a DomainSet from pre-parsed inputs. // NewDomainSet creates a DomainSet from pre-parsed inputs.
// staticPrefixes is a list of CIDRs or bare IPs. // staticPrefixes is a list of CIDRs or bare IPs.
// domains is a list of hostnames to resolve periodically. // domains is a list of hostnames to resolve periodically.
func NewDomainSet(ctx context.Context, staticPrefixes []string, domains []string) *DomainSet { func NewDomainSet(ctx context.Context, staticPrefixes []string, domains []string, interval time.Duration) *DomainSet {
ds := &DomainSet{ if interval <= 0 {
staticPrefixes: staticPrefixes, interval = DefaultDomainSetRefreshInterval
domains: domains,
} }
setCtx, cancel := context.WithCancel(ctx)
ds := &DomainSet{ctx: setCtx, cancel: cancel, interval: interval}
ds.sources.Store(&domainSources{
staticPrefixes: append([]string(nil), staticPrefixes...),
domains: append([]string(nil), domains...),
})
ds.cohort.Store(&ipcohort.Cohort{}) ds.cohort.Store(&ipcohort.Cohort{})
@ -43,11 +57,36 @@ func NewDomainSet(ctx context.Context, staticPrefixes []string, domains []string
log().Info("domain set loaded", "static", commaify(len(staticPrefixes)), "domains", commaify(len(domains))) log().Info("domain set loaded", "static", commaify(len(staticPrefixes)), "domains", commaify(len(domains)))
go ds.refreshLoop(ctx) if len(domains) > 0 {
ds.startRefreshLoop()
}
return ds return ds
} }
func (ds *DomainSet) Replace(staticPrefixes, domains []string) {
ds.sources.Store(&domainSources{
staticPrefixes: append([]string(nil), staticPrefixes...),
domains: append([]string(nil), domains...),
})
if len(domains) > 0 {
ds.startRefreshLoop()
}
ds.resolveDomains(ds.ctx)
ds.rebuildCohort()
}
func (ds *DomainSet) startRefreshLoop() {
ds.start.Do(func() { go ds.refreshLoop(ds.interval) })
}
func (ds *DomainSet) Close() error {
if ds != nil && ds.cancel != nil {
ds.cancel()
}
return nil
}
func (ds *DomainSet) Contains(addr netip.Addr) bool { func (ds *DomainSet) Contains(addr netip.Addr) bool {
cohort := ds.cohort.Load() cohort := ds.cohort.Load()
if cohort == nil { if cohort == nil {
@ -57,34 +96,35 @@ func (ds *DomainSet) Contains(addr netip.Addr) bool {
return cohort.ContainsAddr(addr) return cohort.ContainsAddr(addr)
} }
func (ds *DomainSet) refreshLoop(ctx context.Context) { func (ds *DomainSet) refreshLoop(interval time.Duration) {
ds.resolveDomains(ctx) ds.resolveDomains(ds.ctx)
ds.rebuildCohort() ds.rebuildCohort()
ticker := time.NewTicker(domainSetRefreshInterval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ds.ctx.Done():
return return
case <-ticker.C: case <-ticker.C:
ds.resolveDomains(ctx) ds.resolveDomains(ds.ctx)
ds.rebuildCohort() ds.rebuildCohort()
} }
} }
} }
func (ds *DomainSet) resolveDomains(ctx context.Context) { func (ds *DomainSet) resolveDomains(ctx context.Context) {
if len(ds.domains) == 0 { sources := ds.sources.Load()
if sources == nil || len(sources.domains) == 0 {
return return
} }
prev := *ds.resolved.Load() prev := *ds.resolved.Load()
next := make(map[string][]string, len(ds.domains)) next := make(map[string][]string, len(sources.domains))
resolver := dnsresolver.New() resolver := dnsresolver.New()
for _, domain := range ds.domains { for _, domain := range sources.domains {
ips, _, err := resolver.LookupIP(ctx, domain) ips, _, err := resolver.LookupIP(ctx, domain)
if err != nil || len(ips) == 0 { if err != nil || len(ips) == 0 {
@ -109,7 +149,11 @@ func (ds *DomainSet) resolveDomains(ctx context.Context) {
func (ds *DomainSet) rebuildCohort() { func (ds *DomainSet) rebuildCohort() {
var all []string var all []string
all = append(all, ds.staticPrefixes...) sources := ds.sources.Load()
if sources == nil {
return
}
all = append(all, sources.staticPrefixes...)
resolved := *ds.resolved.Load() resolved := *ds.resolved.Load()
for _, addrs := range resolved { for _, addrs := range resolved {

View File

@ -12,9 +12,11 @@ import (
"github.com/therootcompany/golib/net/ipcohort" "github.com/therootcompany/golib/net/ipcohort"
) )
const prefixSetRefreshInterval = 47 * time.Minute const DefaultPrefixSetRefreshInterval = time.Hour + 57*time.Minute + 13*time.Second
type PrefixSet struct { type PrefixSet struct {
ctx context.Context
cancel context.CancelFunc
repo *gitshallow.Repo repo *gitshallow.Repo
files []string files []string
cohort atomic.Pointer[ipcohort.Cohort] cohort atomic.Pointer[ipcohort.Cohort]
@ -26,24 +28,37 @@ func EmptyPrefixSet() *PrefixSet {
return ps return ps
} }
func NewPrefixSet(ctx context.Context, repoURL, dataPath string, files []string) (*PrefixSet, error) { func NewPrefixSet(ctx context.Context, repoURL, dataPath string, files []string, interval time.Duration) (*PrefixSet, error) {
if err := os.MkdirAll(dataPath, 0o755); err != nil { if err := os.MkdirAll(dataPath, 0o755); err != nil {
return nil, fmt.Errorf("ipgate: create data dir: %w", err) return nil, fmt.Errorf("ipgate: create data dir: %w", err)
} }
if interval <= 0 {
interval = DefaultPrefixSetRefreshInterval
}
setCtx, cancel := context.WithCancel(ctx)
repo := gitshallow.New(repoURL, dataPath, 0, "") repo := gitshallow.New(repoURL, dataPath, 0, "")
ps := &PrefixSet{ ps := &PrefixSet{
ctx: setCtx,
cancel: cancel,
repo: repo, repo: repo,
files: files, files: files,
} }
ps.cohort.Store(&ipcohort.Cohort{}) ps.cohort.Store(&ipcohort.Cohort{})
go ps.refreshLoop(ctx) go ps.refreshLoop(interval)
return ps, nil return ps, nil
} }
func (ps *PrefixSet) Close() error {
if ps != nil && ps.cancel != nil {
ps.cancel()
}
return nil
}
func (ps *PrefixSet) Contains(addr netip.Addr) bool { func (ps *PrefixSet) Contains(addr netip.Addr) bool {
cohort := ps.cohort.Load() cohort := ps.cohort.Load()
if cohort == nil { if cohort == nil {
@ -95,20 +110,20 @@ func filesPresent(paths []string) bool {
return true return true
} }
func (ps *PrefixSet) refreshLoop(ctx context.Context) { func (ps *PrefixSet) refreshLoop(interval time.Duration) {
if err := ps.reload(ctx); err != nil { if err := ps.reload(ps.ctx); err != nil {
log().Warn("prefix set initial load (will retry)", "err", err) log().Warn("prefix set initial load (will retry)", "err", err)
} }
ticker := time.NewTicker(prefixSetRefreshInterval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ps.ctx.Done():
return return
case <-ticker.C: case <-ticker.C:
if err := ps.reload(ctx); err != nil { if err := ps.reload(ps.ctx); err != nil {
log().Warn("prefix set reload failed", "err", err) log().Warn("prefix set reload failed", "err", err)
} }
} }