From 1581be1722e5c66c00f03d94e1b269719abeae23 Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Sun, 17 May 2026 13:17:08 +0400 Subject: [PATCH] monitoring/crowdsec: add MaxMind GeoLite2-City.mmdb --- crowdsec/.env.example | 9 +++++ crowdsec/data/.gitignore | 1 + crowdsec/data/geoip/.gitkeep | 0 crowdsec/docker-compose.yaml | 22 ++++++++++ crowdsec/exporter/go.mod | 6 ++- crowdsec/exporter/go.sum | 4 ++ crowdsec/exporter/main.go | 40 ++++++++++++++++--- grafana/data/grafana/dashboards/crowdsec.json | 8 ++-- 8 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 crowdsec/data/geoip/.gitkeep diff --git a/crowdsec/.env.example b/crowdsec/.env.example index f55f21c..dc0f53c 100644 --- a/crowdsec/.env.example +++ b/crowdsec/.env.example @@ -10,3 +10,12 @@ CROWDSEC_BOUNCER_KEY_FW= # Leave blank to run without Console (no community blocklist subscriptions). CROWDSEC_ENROLL_KEY= CROWDSEC_ENROLL_INSTANCE_NAME= + +############# MaxMind GeoIP ############# +# Free account: https://www.maxmind.com/en/geolite2/signup +# After signup: Manage License Keys -> Generate new license key. +# Used by the geoipupdate sidecar to download/refresh GeoLite2-City.mmdb, +# consumed by the exporter for the Country column in the Local bans table. +# Leave blank to disable: exporter still runs, country labels will be empty. +MAXMIND_ACCOUNT_ID= +MAXMIND_LICENSE_KEY= diff --git a/crowdsec/data/.gitignore b/crowdsec/data/.gitignore index 77c62e6..11e5673 100644 --- a/crowdsec/data/.gitignore +++ b/crowdsec/data/.gitignore @@ -1,3 +1,4 @@ /db/* /config/* +/geoip/* !.gitkeep diff --git a/crowdsec/data/geoip/.gitkeep b/crowdsec/data/geoip/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/crowdsec/docker-compose.yaml b/crowdsec/docker-compose.yaml index b73e975..b0b2646 100644 --- a/crowdsec/docker-compose.yaml +++ b/crowdsec/docker-compose.yaml @@ -63,6 +63,9 @@ services: CROWDSEC_API_KEY: "${CROWDSEC_BOUNCER_KEY_CADDY}" POLL_INTERVAL_SECS: "30" LISTEN_PORT: "9100" + GEOIP_CITY_DB: "/geoip/GeoLite2-City.mmdb" + volumes: + - ./data/geoip:/geoip:ro depends_on: - crowdsec logging: @@ -70,3 +73,22 @@ services: options: max-size: "50m" max-file: "3" + + geoipupdate: + image: ghcr.io/maxmind/geoipupdate:v7 + container_name: crowdsec_geoipupdate + restart: unless-stopped + labels: + - "com.centurylinklabs.watchtower.enable=true" + environment: + GEOIPUPDATE_ACCOUNT_ID: "${MAXMIND_ACCOUNT_ID}" + GEOIPUPDATE_LICENSE_KEY: "${MAXMIND_LICENSE_KEY}" + GEOIPUPDATE_EDITION_IDS: "GeoLite2-City" + GEOIPUPDATE_FREQUENCY: "24" + volumes: + - ./data/geoip:/usr/share/GeoIP + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "2" diff --git a/crowdsec/exporter/go.mod b/crowdsec/exporter/go.mod index d37ce6e..5690b46 100644 --- a/crowdsec/exporter/go.mod +++ b/crowdsec/exporter/go.mod @@ -2,13 +2,17 @@ module crowdsec-exporter go 1.26.3 -require github.com/prometheus/client_golang v1.23.2 +require ( + github.com/oschwald/geoip2-golang/v2 v2.1.0 + github.com/prometheus/client_golang v1.23.2 +) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/klauspost/compress v1.18.6 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oschwald/maxminddb-golang/v2 v2.1.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect diff --git a/crowdsec/exporter/go.sum b/crowdsec/exporter/go.sum index a064a53..39df14e 100644 --- a/crowdsec/exporter/go.sum +++ b/crowdsec/exporter/go.sum @@ -12,6 +12,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7LB23UBI2Amo= +github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc= +github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc= +github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= diff --git a/crowdsec/exporter/main.go b/crowdsec/exporter/main.go index 0dbe500..ac79f46 100644 --- a/crowdsec/exporter/main.go +++ b/crowdsec/exporter/main.go @@ -4,14 +4,33 @@ import ( "encoding/json" "log" "net/http" + "net/netip" "os" "strconv" "time" + "github.com/oschwald/geoip2-golang/v2" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) +var geoDB *geoip2.Reader + +func lookupCountry(ip string) string { + if geoDB == nil { + return "" + } + addr, err := netip.ParseAddr(ip) + if err != nil { + return "" + } + rec, err := geoDB.City(addr) + if err != nil || rec == nil { + return "" + } + return rec.Country.ISOCode +} + var ( decisionsActive = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "crowdsec_decisions_active", @@ -31,7 +50,7 @@ var ( decisionRemaining = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "crowdsec_decision_remaining_seconds", Help: "Remaining seconds before each local decision expires. Only origin=crowdsec is exposed to keep cardinality bounded (CAPI/lists can contain tens of thousands of IPs).", - }, []string{"origin", "ip", "scenario", "type"}) + }, []string{"origin", "ip", "country", "scenario", "type"}) lastFetchUnix = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "crowdsec_exporter_last_fetch_unix", @@ -106,8 +125,8 @@ func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) { ips := map[string]map[string]struct{}{} allIPs := map[string]struct{}{} type localDecision struct { - origin, ip, scenario, dtype string - remaining float64 + origin, ip, country, scenario, dtype string + remaining float64 } var locals []localDecision @@ -137,7 +156,7 @@ func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) { if perr != nil { dur = 0 } - locals = append(locals, localDecision{origin, d.Value, scenario, dtype, dur.Seconds()}) + locals = append(locals, localDecision{origin, d.Value, lookupCountry(d.Value), scenario, dtype, dur.Seconds()}) } } @@ -152,7 +171,7 @@ func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) { } decisionsUniqueIPsTotal.Set(float64(len(allIPs))) for _, l := range locals { - decisionRemaining.WithLabelValues(l.origin, l.ip, l.scenario, l.dtype).Set(l.remaining) + decisionRemaining.WithLabelValues(l.origin, l.ip, l.country, l.scenario, l.dtype).Set(l.remaining) } fetchErrors.Set(0) @@ -175,6 +194,17 @@ func main() { } port := getenv("LISTEN_PORT", "9100") + geoDBPath := getenv("GEOIP_CITY_DB", "") + if geoDBPath != "" { + if db, err := geoip2.Open(geoDBPath); err == nil { + geoDB = db + defer geoDB.Close() + log.Printf("[start] GeoIP DB loaded: %s", geoDBPath) + } else { + log.Printf("[warn] GeoIP DB unavailable (%v); country label will be empty", err) + } + } + client := &http.Client{Timeout: 10 * time.Second} go func() { diff --git a/grafana/data/grafana/dashboards/crowdsec.json b/grafana/data/grafana/dashboards/crowdsec.json index 1780663..636afab 100644 --- a/grafana/data/grafana/dashboards/crowdsec.json +++ b/grafana/data/grafana/dashboards/crowdsec.json @@ -429,12 +429,14 @@ }, "indexByName": { "ip": 0, - "scenario": 1, - "type": 2, - "Value": 3 + "country": 1, + "scenario": 2, + "type": 3, + "Value": 4 }, "renameByName": { "ip": "IP", + "country": "Country", "scenario": "Scenario", "type": "Type", "Value": "Time remaining"