diff --git a/cmd/check-ip/blacklist.go b/cmd/check-ip/blacklist.go index d5cc5be..d36c94e 100644 --- a/cmd/check-ip/blacklist.go +++ b/cmd/check-ip/blacklist.go @@ -2,6 +2,7 @@ package main import ( "path/filepath" + "strings" "github.com/therootcompany/golib/net/dataset" "github.com/therootcompany/golib/net/gitshallow" @@ -9,62 +10,76 @@ import ( "github.com/therootcompany/golib/net/ipcohort" ) -// HTTPSource pairs a remote URL with a local cache path. -type HTTPSource struct { - URL string - Path string -} - -// Sources holds fetch configuration for the three blocklist cohorts. -// It knows how to pull data from git or HTTP, but owns no atomic state. +// Sources holds fetch configuration for the blocklist cohorts. type Sources struct { whitelistPaths []string inboundPaths []string outboundPaths []string - - syncs []dataset.Syncer // all syncable sources + syncs []dataset.Syncer } -func newFileSources(whitelist, inbound, outbound []string) *Sources { - return &Sources{ - whitelistPaths: whitelist, - inboundPaths: inbound, - outboundPaths: outbound, - } -} - -func newGitSources(gitURL, repoDir string, whitelist, inboundRel, outboundRel []string) *Sources { - abs := func(rel []string) []string { - out := make([]string, len(rel)) - for i, p := range rel { - out[i] = filepath.Join(repoDir, p) +// buildSources constructs the right Sources from CLI flags. +// +// - gitURL set → clone/pull the bitwire-it repo; inbound/outbound from known relative paths +// - inbound/outbound set → use those explicit file paths, no network sync +// - neither set → HTTP-fetch the bitwire-it files into dataDir (or default cache dir) +func buildSources(gitURL, dataDir, whitelistFlag, inboundFlag, outboundFlag string) *Sources { + // Explicit file paths always win. + if inboundFlag != "" || outboundFlag != "" { + return &Sources{ + whitelistPaths: splitPaths(whitelistFlag), + inboundPaths: splitPaths(inboundFlag), + outboundPaths: splitPaths(outboundFlag), } - return out } - repo := gitshallow.New(gitURL, repoDir, 1, "") + + cacheDir := dataDir + if cacheDir == "" { + cacheDir = defaultCacheDir("bitwire-it") + } + + if gitURL != "" { + repo := gitshallow.New(gitURL, cacheDir, 1, "") + return &Sources{ + whitelistPaths: splitPaths(whitelistFlag), + inboundPaths: []string{ + filepath.Join(cacheDir, "tables/inbound/single_ips.txt"), + filepath.Join(cacheDir, "tables/inbound/networks.txt"), + }, + outboundPaths: []string{ + filepath.Join(cacheDir, "tables/outbound/single_ips.txt"), + filepath.Join(cacheDir, "tables/outbound/networks.txt"), + }, + syncs: []dataset.Syncer{repo}, + } + } + + // Default: HTTP fetch from bitwire-it into cacheDir. + inboundSingle := filepath.Join(cacheDir, "inbound_single_ips.txt") + inboundNetwork := filepath.Join(cacheDir, "inbound_networks.txt") + outboundSingle := filepath.Join(cacheDir, "outbound_single_ips.txt") + outboundNetwork := filepath.Join(cacheDir, "outbound_networks.txt") return &Sources{ - whitelistPaths: whitelist, - inboundPaths: abs(inboundRel), - outboundPaths: abs(outboundRel), - syncs: []dataset.Syncer{repo}, + whitelistPaths: splitPaths(whitelistFlag), + inboundPaths: []string{inboundSingle, inboundNetwork}, + outboundPaths: []string{outboundSingle, outboundNetwork}, + syncs: []dataset.Syncer{ + httpcache.New(inboundSingleURL, inboundSingle), + httpcache.New(inboundNetworkURL, inboundNetwork), + httpcache.New(outboundSingleURL, outboundSingle), + httpcache.New(outboundNetworkURL, outboundNetwork), + }, } } -func newHTTPSources(whitelist []string, inbound, outbound []HTTPSource) *Sources { - s := &Sources{whitelistPaths: whitelist} - for _, src := range inbound { - s.inboundPaths = append(s.inboundPaths, src.Path) - s.syncs = append(s.syncs, httpcache.New(src.URL, src.Path)) +func splitPaths(s string) []string { + if s == "" { + return nil } - for _, src := range outbound { - s.outboundPaths = append(s.outboundPaths, src.Path) - s.syncs = append(s.syncs, httpcache.New(src.URL, src.Path)) - } - return s + return strings.Split(s, ",") } -// Fetch pulls updates from all sources. Returns whether any new data arrived. -// Satisfies dataset.Syncer. +// Fetch pulls updates from all sources. Satisfies dataset.Syncer. func (s *Sources) Fetch() (bool, error) { var anyUpdated bool for _, syn := range s.syncs { @@ -77,9 +92,7 @@ func (s *Sources) Fetch() (bool, error) { return anyUpdated, nil } -// Datasets builds a dataset.Group backed by this Sources and returns typed -// datasets for whitelist, inbound, and outbound cohorts. Either whitelist or -// outbound may be nil if no paths were configured. +// Datasets builds a dataset.Group and returns typed views for each cohort. func (s *Sources) Datasets() ( g *dataset.Group, whitelist *dataset.View[ipcohort.Cohort], diff --git a/cmd/check-ip/geo.go b/cmd/check-ip/geo.go index 16eb278..43bbf8d 100644 --- a/cmd/check-ip/geo.go +++ b/cmd/check-ip/geo.go @@ -8,12 +8,32 @@ import ( "github.com/therootcompany/golib/net/geoip" ) -// setupGeo parses geoip-conf (if given) and returns a Databases ready to Init. -// Returns nil if no geoip flags were provided. -func setupGeo(confPath, cityPath, asnPath string) (*geoip.Databases, error) { - if confPath == "" && cityPath == "" && asnPath == "" { - return nil, nil +// discoverConf looks for GeoIP.conf in the current directory and then +// at ~/.config/maxmind/GeoIP.conf. Returns the path or "". +func discoverConf() string { + if _, err := os.Stat("GeoIP.conf"); err == nil { + return "GeoIP.conf" } + if home, err := os.UserHomeDir(); err == nil { + p := filepath.Join(home, ".config", "maxmind", "GeoIP.conf") + if _, err := os.Stat(p); err == nil { + return p + } + } + return "" +} + +// setupGeo returns a Databases ready to Init, or nil if geoip is not configured. +// +// - confPath="" → auto-discover GeoIP.conf from cwd and ~/.config/maxmind/ +// - conf found → auto-download; cityPath/asnPath override the default locations +// - conf absent → cityPath and asnPath must point to existing .mmdb files +// - no conf and no paths → geoip disabled (returns nil) +func setupGeo(confPath, cityPath, asnPath string) (*geoip.Databases, error) { + if confPath == "" { + confPath = discoverConf() + } + if confPath != "" { cfg, err := geoip.ParseConf(confPath) if err != nil { @@ -36,5 +56,10 @@ func setupGeo(confPath, cityPath, asnPath string) (*geoip.Databases, error) { } return geoip.New(cfg.AccountID, cfg.LicenseKey).NewDatabases(cityPath, asnPath), nil } + + if cityPath == "" && asnPath == "" { + return nil, nil + } + // Explicit paths only — no auto-download. Init will fail if files are absent. return geoip.NewDatabases(cityPath, asnPath), nil } diff --git a/cmd/check-ip/main.go b/cmd/check-ip/main.go index 86333ec..722b826 100644 --- a/cmd/check-ip/main.go +++ b/cmd/check-ip/main.go @@ -5,102 +5,65 @@ import ( "flag" "fmt" "os" - "strings" + "path/filepath" "time" - ) -// inbound blocklist +// Default HTTP sources for the bitwire-it blocklist. const ( inboundSingleURL = "https://github.com/bitwire-it/ipblocklist/raw/refs/heads/main/tables/inbound/single_ips.txt" inboundNetworkURL = "https://github.com/bitwire-it/ipblocklist/raw/refs/heads/main/tables/inbound/networks.txt" -) - -// outbound blocklist -const ( outboundSingleURL = "https://github.com/bitwire-it/ipblocklist/raw/refs/heads/main/tables/outbound/single_ips.txt" outboundNetworkURL = "https://github.com/bitwire-it/ipblocklist/raw/refs/heads/main/tables/outbound/networks.txt" ) -func defaultBlocklistDir() string { +func defaultCacheDir(sub string) string { base, err := os.UserCacheDir() if err != nil { - return os.Getenv("HOME") + "/.cache/bitwire-it" + base = filepath.Join(os.Getenv("HOME"), ".cache") } - return base + "/bitwire-it" + return filepath.Join(base, sub) } func main() { - dataDir := flag.String("data-dir", "", "blocklist cache dir (default ~/.cache/bitwire-it)") - cityDBPath := flag.String("city-db", "", "path to GeoLite2-City.mmdb (overrides -geoip-conf)") - asnDBPath := flag.String("asn-db", "", "path to GeoLite2-ASN.mmdb (overrides -geoip-conf)") - geoipConf := flag.String("geoip-conf", "", "path to GeoIP.conf; auto-downloads City+ASN into data-dir") - gitURL := flag.String("git", "", "clone/pull blocklist from this git URL into data-dir") + // Blocklist source flags — all optional; defaults pull from bitwire-it via HTTP. + dataDir := flag.String("data-dir", "", "blocklist cache dir (default ~/.cache/bitwire-it)") + gitURL := flag.String("git", "", "git URL to clone/pull blocklist from (alternative to HTTP)") + whitelist := flag.String("whitelist", "", "path to whitelist file (overrides block)") + inbound := flag.String("inbound", "", "comma-separated paths to inbound blocklist files") + outbound := flag.String("outbound", "", "comma-separated paths to outbound blocklist files") + + // GeoIP flags — auto-discovered from ./GeoIP.conf or ~/.config/maxmind/GeoIP.conf. + geoipConf := flag.String("geoip-conf", "", "path to GeoIP.conf (auto-discovered if absent)") + cityDB := flag.String("city-db", "", "path to GeoLite2-City.mmdb (skips auto-download)") + asnDB := flag.String("asn-db", "", "path to GeoLite2-ASN.mmdb (skips auto-download)") + flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [flags] \n", os.Args[0]) - fmt.Fprintf(os.Stderr, " Blocklists are fetched via HTTP by default (use -git for git source).\n") - fmt.Fprintf(os.Stderr, " Pass a .txt/.csv path as the first arg to load a single local file.\n") flag.PrintDefaults() } flag.Parse() - if flag.NArg() < 1 { + if flag.NArg() != 1 { flag.Usage() os.Exit(1) } - - // First arg is either a local file or the IP to check. - var dataPath, ipStr string - if flag.NArg() >= 2 || strings.HasSuffix(flag.Arg(0), ".txt") || strings.HasSuffix(flag.Arg(0), ".csv") { - dataPath = flag.Arg(0) - ipStr = flag.Arg(1) - } else { - ipStr = flag.Arg(0) - } - if *dataDir != "" { - dataPath = *dataDir - } - if dataPath == "" { - dataPath = defaultBlocklistDir() - } + ipStr := flag.Arg(0) // -- Blocklist ---------------------------------------------------------- - var src *Sources - switch { - case *gitURL != "": - src = newGitSources(*gitURL, dataPath, - nil, - []string{"tables/inbound/single_ips.txt", "tables/inbound/networks.txt"}, - []string{"tables/outbound/single_ips.txt", "tables/outbound/networks.txt"}, - ) - case strings.HasSuffix(dataPath, ".txt") || strings.HasSuffix(dataPath, ".csv"): - src = newFileSources(nil, []string{dataPath}, nil) - default: - src = newHTTPSources( - nil, - []HTTPSource{ - {inboundSingleURL, dataPath + "/inbound_single_ips.txt"}, - {inboundNetworkURL, dataPath + "/inbound_networks.txt"}, - }, - []HTTPSource{ - {outboundSingleURL, dataPath + "/outbound_single_ips.txt"}, - {outboundNetworkURL, dataPath + "/outbound_networks.txt"}, - }, - ) - } - - blGroup, whitelist, inbound, outbound := src.Datasets() + src := buildSources(*gitURL, *dataDir, *whitelist, *inbound, *outbound) + blGroup, whitelistDS, inboundDS, outboundDS := src.Datasets() if err := blGroup.Init(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) + fmt.Fprintf(os.Stderr, "error: blocklist: %v\n", err) os.Exit(1) } fmt.Fprintf(os.Stderr, "Loaded inbound=%d outbound=%d\n", - cohortSize(inbound), cohortSize(outbound)) + cohortSize(inboundDS), cohortSize(outboundDS)) // -- GeoIP (optional) -------------------------------------------------- - geo, err := setupGeo(*geoipConf, *cityDBPath, *asnDBPath) + geo, err := setupGeo(*geoipConf, *cityDB, *asnDB) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) @@ -119,8 +82,8 @@ func main() { // -- Check and report -------------------------------------------------- - blockedIn := isBlocked(ipStr, whitelist, inbound) - blockedOut := isBlocked(ipStr, whitelist, outbound) + blockedIn := isBlocked(ipStr, whitelistDS, inboundDS) + blockedOut := isBlocked(ipStr, whitelistDS, outboundDS) switch { case blockedIn && blockedOut: