diff --git a/go.mod b/go.mod index 1fb2480..e8bbf76 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/yl2chen/cidranger v1.0.2 // indirect golang.org/x/sys v0.30.0 // indirect google.golang.org/protobuf v1.36.5 // indirect ) diff --git a/go.sum b/go.sum index f602bc3..62d4f44 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,7 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -30,11 +31,17 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= +github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 8356409..e98c1c9 100644 --- a/main.go +++ b/main.go @@ -19,8 +19,61 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/redis/go-redis/v9" + "github.com/yl2chen/cidranger" ) +// ────────────────────────────────────────────── +// Ranger-Cache (statt prefixCache) +// ────────────────────────────────────────────── +type rangerCacheEntry struct { + ranger cidranger.Ranger + expireAt time.Time +} + +var ( + rangerCache = map[string]rangerCacheEntry{} + rangerCacheMu sync.RWMutex +) + +// buildCategoryRanger holt alle CIDRs aus Redis, baut einen PCTrie +// und legt ihn 10 Minuten im Cache ab. +func buildCategoryRanger(cat string) (cidranger.Ranger, error) { + rangerCacheMu.Lock() + // Cache-Hit? + if e, ok := rangerCache[cat]; ok && time.Now().Before(e.expireAt) { + rangerCacheMu.Unlock() + return e.ranger, nil + } + rangerCacheMu.Unlock() + + // Redis auslesen + keys, err := rdb.HKeys(ctx, "bl:"+cat).Result() + if err != nil { + return nil, err + } + + r := cidranger.NewPCTrieRanger() + for _, k := range keys { + k = strings.TrimSpace(k) + _, ipNet, err := net.ParseCIDR(k) + if err != nil { + fmt.Printf("⚠️ Ungültiger Redis-Prefix %s: %s\n", cat, k) + continue + } + _ = r.Insert(cidranger.NewBasicRangerEntry(*ipNet)) + } + + // Cache aktualisieren + rangerCacheMu.Lock() + rangerCache[cat] = rangerCacheEntry{ + ranger: r, + expireAt: time.Now().Add(10 * time.Minute), + } + rangerCacheMu.Unlock() + + return r, nil +} + // Redis + Context var ctx = context.Background() var rdb = redis.NewClient(&redis.Options{ @@ -61,17 +114,6 @@ var blocklistURLs = map[string]string{ "bitwire": "https://raw.githubusercontent.com/bitwire-it/ipblocklist/refs/heads/main/ip-list.txt", } -// Präfix-Cache -type prefixCacheEntry struct { - prefixes []netip.Prefix - expireAt time.Time -} - -var ( - prefixCache = map[string]prefixCacheEntry{} - prefixCacheMu sync.RWMutex -) - // Prometheus Metriken var ( checkRequests = prometheus.NewCounter(prometheus.CounterOpts{ @@ -311,10 +353,6 @@ func handleWhitelist(w http.ResponseWriter, r *http.Request) { http.Error(w, "redis error", http.StatusInternalServerError) return } - - // Optional: Cache leeren für die IP - prefixCacheMu.Lock() - defer prefixCacheMu.Unlock() // Kein spezifischer IP-Cache in deinem Design, aber hier könnte man Cache invalidieren falls nötig writeJSON(w, map[string]string{ @@ -334,7 +372,7 @@ func handleCheck(w http.ResponseWriter, r *http.Request) { } var cats []string - for a, _ := range blocklistURLs { + for a := range blocklistURLs { cats = append(cats, a) } @@ -366,6 +404,12 @@ func handleTraefik(w http.ResponseWriter, r *http.Request) { if ipStr == "" { ipStr = r.RemoteAddr } + ipStr = strings.TrimSpace(strings.Split(ipStr, ",")[0]) // evtl. mehrere IPs + + // Port abschneiden – funktioniert für IPv4 und IPv6: + if host, _, err := net.SplitHostPort(ipStr); err == nil { + ipStr = host + } ip, err := netip.ParseAddr(ipStr) if err != nil { http.Error(w, "invalid IP", http.StatusBadRequest) @@ -373,7 +417,7 @@ func handleTraefik(w http.ResponseWriter, r *http.Request) { } var cats []string - for a, _ := range blocklistURLs { + for a := range blocklistURLs { cats = append(cats, a) } @@ -400,59 +444,28 @@ func handleTraefik(w http.ResponseWriter, r *http.Request) { // Check-Logik func checkIP(ip netip.Addr, cats []string) ([]string, error) { - wl, err := rdb.Exists(ctx, "wl:"+ip.String()).Result() - if err != nil { - return nil, err + // Whitelist zuerst prüfen + if wl, err := rdb.Exists(ctx, "wl:"+ip.String()).Result(); err == nil && wl > 0 { + return nil, nil } - if wl > 0 { - return []string{}, nil - } - matches := []string{} + + var matches []string + needle := net.IP(ip.AsSlice()) + for _, cat := range cats { - prefixes, err := loadCategoryPrefixes(cat) + r, err := buildCategoryRanger(cat) if err != nil { return nil, err } - for _, pfx := range prefixes { - if pfx.Contains(ip) { - fmt.Printf("💡 MATCH: %s in %s (%s)\n", ip, cat, pfx) - matches = append(matches, cat) - break - } + ok, _ := r.Contains(needle) + if ok { + fmt.Printf("💡 MATCH: %s in %s\n", ip, cat) + matches = append(matches, cat) } } return matches, nil } -func loadCategoryPrefixes(cat string) ([]netip.Prefix, error) { - prefixCacheMu.Lock() - defer prefixCacheMu.Unlock() - entry, ok := prefixCache[cat] - if ok && time.Now().Before(entry.expireAt) { - return entry.prefixes, nil - } - keys, err := rdb.HKeys(ctx, "bl:"+cat).Result() - if err != nil { - return nil, err - } - var prefixes []netip.Prefix - for _, k := range keys { - k = strings.TrimSpace(k) - pfx, err := netip.ParsePrefix(k) - if err == nil { - prefixes = append(prefixes, pfx) - } else { - fmt.Printf("⚠️ Ungültiger Redis-Prefix %s: %s\n", cat, k) - } - } - prefixCache[cat] = prefixCacheEntry{ - prefixes: prefixes, - expireAt: time.Now().Add(10 * time.Minute), - //Hier geändert von 1 * time.Second - } - return prefixes, nil -} - // JSON-Helfer func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json")