diff --git a/cmd/check-ip/main.go b/cmd/check-ip/main.go index 689a930..0f6093d 100644 --- a/cmd/check-ip/main.go +++ b/cmd/check-ip/main.go @@ -37,7 +37,7 @@ type IPCheck struct { inbound *dataset.View[ipcohort.Cohort] outbound *dataset.View[ipcohort.Cohort] - geo *geoip.Databases + geo *dataset.View[geoip.Databases] } func printVersion(w *os.File) { @@ -103,16 +103,16 @@ func main() { } maxmind := filepath.Join(cfg.CacheDir, "maxmind") - geo, err := geoip.OpenDatabases( - cfg.ConfPath, - filepath.Join(maxmind, geoip.CityEdition+".mmdb"), - filepath.Join(maxmind, geoip.ASNEdition+".mmdb"), - ) - if err != nil { + cityPath := filepath.Join(maxmind, geoip.CityEdition+".mmdb") + asnPath := filepath.Join(maxmind, geoip.ASNEdition+".mmdb") + geoGroup := dataset.NewGroup(geoFetcher(cfg.ConfPath, cityPath, asnPath)) + cfg.geo = dataset.Add(geoGroup, func() (*geoip.Databases, error) { + return geoip.Open(cityPath, asnPath) + }) + if err := geoGroup.Load(context.Background()); err != nil { log.Fatalf("geoip: %v", err) } - defer func() { _ = geo.Close() }() - cfg.geo = geo + defer func() { _ = cfg.geo.Value().Close() }() if cfg.Bind == "" { return @@ -121,9 +121,48 @@ func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() go group.Tick(ctx, refreshInterval, func(err error) { - log.Printf("refresh: %v", err) + log.Printf("blocklists refresh: %v", err) + }) + go geoGroup.Tick(ctx, refreshInterval, func(err error) { + log.Printf("geoip refresh: %v", err) }) if err := cfg.serve(ctx); err != nil { log.Fatalf("serve: %v", err) } } + +// geoFetcher returns a Fetcher for the GeoLite2 City + ASN .mmdb files. +// With a GeoIP.conf (explicit path or auto-discovered) both files are +// downloaded via httpcache conditional GETs; otherwise the files are +// expected to exist on disk and are polled for out-of-band changes. +func geoFetcher(confPath, cityPath, asnPath string) dataset.Fetcher { + if confPath == "" { + for _, p := range geoip.DefaultConfPaths() { + if _, err := os.Stat(p); err == nil { + confPath = p + break + } + } + } + if confPath == "" { + return dataset.PollFiles(cityPath, asnPath) + } + conf, err := geoip.ParseConf(confPath) + if err != nil { + log.Fatalf("geoip-conf: %v", err) + } + dl := geoip.New(conf.AccountID, conf.LicenseKey) + city := dl.NewCacher(geoip.CityEdition, cityPath) + asn := dl.NewCacher(geoip.ASNEdition, asnPath) + return dataset.FetcherFunc(func() (bool, error) { + cityUpdated, err := city.Fetch() + if err != nil { + return false, fmt.Errorf("fetch %s: %w", geoip.CityEdition, err) + } + asnUpdated, err := asn.Fetch() + if err != nil { + return false, fmt.Errorf("fetch %s: %w", geoip.ASNEdition, err) + } + return cityUpdated || asnUpdated, nil + }) +} diff --git a/cmd/check-ip/server.go b/cmd/check-ip/server.go index 7c3379d..bf5be04 100644 --- a/cmd/check-ip/server.go +++ b/cmd/check-ip/server.go @@ -42,7 +42,7 @@ func (c *IPCheck) handle(w http.ResponseWriter, r *http.Request) { Blocked: in || out, BlockedInbound: in, BlockedOutbound: out, - Geo: c.geo.Lookup(ip), + Geo: c.geo.Value().Lookup(ip), } if r.URL.Query().Get("format") == "json" || diff --git a/net/geoip/databases.go b/net/geoip/databases.go index e87c43c..e3c5274 100644 --- a/net/geoip/databases.go +++ b/net/geoip/databases.go @@ -4,8 +4,6 @@ import ( "errors" "fmt" "net/netip" - "os" - "path/filepath" "github.com/oschwald/geoip2-golang" ) @@ -16,41 +14,6 @@ type Databases struct { ASN *geoip2.Reader } -// OpenDatabases resolves configuration, downloads stale .mmdb files (when a -// GeoIP.conf with credentials is available), and opens the readers. -// -// - confPath="" → auto-discover from DefaultConfPaths -// - conf found → auto-download to cityPath/asnPath -// - no conf → cityPath and asnPath must point to existing .mmdb files -func OpenDatabases(confPath, cityPath, asnPath string) (*Databases, error) { - if confPath == "" { - for _, p := range DefaultConfPaths() { - if _, err := os.Stat(p); err == nil { - confPath = p - break - } - } - } - - if confPath != "" { - cfg, err := ParseConf(confPath) - if err != nil { - return nil, fmt.Errorf("geoip-conf: %w", err) - } - if err := os.MkdirAll(filepath.Dir(cityPath), 0o755); err != nil { - return nil, err - } - dl := New(cfg.AccountID, cfg.LicenseKey) - if _, err := dl.NewCacher(CityEdition, cityPath).Fetch(); err != nil { - return nil, fmt.Errorf("fetch %s: %w", CityEdition, err) - } - if _, err := dl.NewCacher(ASNEdition, asnPath).Fetch(); err != nil { - return nil, fmt.Errorf("fetch %s: %w", ASNEdition, err) - } - } - return Open(cityPath, asnPath) -} - // Open opens city and ASN .mmdb files from the given paths. func Open(cityPath, asnPath string) (*Databases, error) { city, err := geoip2.Open(cityPath)