bugfix for single IP lists
All checks were successful
release-tag / release-image (push) Successful in 1m32s

This commit is contained in:
2025-06-09 18:32:34 +02:00
parent c8d3dc66b1
commit 969c25ce9a

66
main.go
View File

@@ -25,33 +25,28 @@ import (
type Source struct { type Source struct {
Category string // e.g. "spam", "tor", "malware" Category string // e.g. "spam", "tor", "malware"
URL []string // one or many URLs that belong to the same category URL []string // one or many URLs belonging to this category
} }
type Config struct { type Config struct {
RedisAddr string RedisAddr string
Sources []Source // each Source now groups URLs per category Sources []Source // grouped by category
TTLHours int // Redis TTL for block entries TTLHours int // TTL for block entries in Redis
} }
func loadConfig() Config { func loadConfig() Config {
// --- default onecategory fallback -------------------------------------- // default single source
srcs := []Source{{ srcs := []Source{{
Category: "generic", Category: "generic",
URL: []string{"https://ipv64.net/blocklists/ipv64_blocklist_firehole_l1.txt"}, URL: []string{"https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset"},
}} }}
/* /*
ENV syntax supporting multiple URLs per category: ENV format supporting many URLs per category:
BLOCKLIST_SOURCES="spam:https://a.net|https://b.net,tor:https://c.net;https://d.net" BLOCKLIST_SOURCES="spam:https://a.net|https://b.net,tor:https://c.net;https://d.net"
categories separated by comma
URLs inside category separated by | or ;
*/ */
if env := os.Getenv("BLOCKLIST_SOURCES"); env != "" { if env := os.Getenv("BLOCKLIST_SOURCES"); env != "" {
srcs = nil // override default srcs = nil
for _, spec := range strings.Split(env, ",") { for _, spec := range strings.Split(env, ",") {
spec = strings.TrimSpace(spec) spec = strings.TrimSpace(spec)
if spec == "" { if spec == "" {
@@ -59,12 +54,12 @@ func loadConfig() Config {
} }
parts := strings.SplitN(spec, ":", 2) parts := strings.SplitN(spec, ":", 2)
if len(parts) != 2 { if len(parts) != 2 {
continue // malformed fragment continue
} }
cat := strings.TrimSpace(parts[0]) cat := strings.TrimSpace(parts[0])
rawURLs := strings.FieldsFunc(parts[1], func(r rune) bool { return r == '|' || r == ';' }) raw := strings.FieldsFunc(parts[1], func(r rune) bool { return r == '|' || r == ';' })
var urls []string var urls []string
for _, u := range rawURLs { for _, u := range raw {
if u = strings.TrimSpace(u); u != "" { if u = strings.TrimSpace(u); u != "" {
urls = append(urls, u) urls = append(urls, u)
} }
@@ -80,10 +75,8 @@ func loadConfig() Config {
fmt.Sscanf(env, "%d", &ttl) fmt.Sscanf(env, "%d", &ttl)
} }
fmt.Println(getenv("REDIS_ADDR", "localhost:6379"), srcs, ttl)
return Config{ return Config{
RedisAddr: getenv("REDIS_ADDR", "localhost:6379"), RedisAddr: getenv("REDIS_ADDR", "redis:6379"),
Sources: srcs, Sources: srcs,
TTLHours: ttl, TTLHours: ttl,
} }
@@ -104,12 +97,12 @@ func keyBlock(cat string, p netip.Prefix) string { return "bl:" + cat + ":" + p.
func keyWhite(a netip.Addr) string { return "wl:" + a.String() } func keyWhite(a netip.Addr) string { return "wl:" + a.String() }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// INMEMORY RANGER percategory CIDR map // IN-MEMORY RANGER
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
type Ranger struct { type Ranger struct {
mu sync.RWMutex mu sync.RWMutex
blocks map[string]map[netip.Prefix]struct{} // cat → set(prefix) blocks map[string]map[netip.Prefix]struct{}
whites map[netip.Addr]struct{} whites map[netip.Addr]struct{}
} }
@@ -132,7 +125,6 @@ func (r *Ranger) addWhite(a netip.Addr) {
r.mu.Unlock() r.mu.Unlock()
} }
// blockedInCats returns slice of categories in which IP is blocked
func (r *Ranger) blockedInCats(a netip.Addr, cats []string) []string { func (r *Ranger) blockedInCats(a netip.Addr, cats []string) []string {
r.mu.RLock() r.mu.RLock()
defer r.mu.RUnlock() defer r.mu.RUnlock()
@@ -163,7 +155,7 @@ func (r *Ranger) blockedInCats(a netip.Addr, cats []string) []string {
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// SYNC WORKER fetch lists → Redis + Ranger // SYNC WORKER
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
func syncOnce(ctx context.Context, cfg Config, rdb *redis.Client, ranger *Ranger) error { func syncOnce(ctx context.Context, cfg Config, rdb *redis.Client, ranger *Ranger) error {
@@ -179,7 +171,7 @@ func syncOnce(ctx context.Context, cfg Config, rdb *redis.Client, ranger *Ranger
newBlocks[src.Category][p] = struct{}{} newBlocks[src.Category][p] = struct{}{}
_ = rdb.Set(ctx, keyBlock(src.Category, p), "1", expiry).Err() _ = rdb.Set(ctx, keyBlock(src.Category, p), "1", expiry).Err()
}); err != nil { }); err != nil {
fmt.Println("Error initialising List:", err, src.Category, src.URL) return err
} }
} }
} }
@@ -210,6 +202,14 @@ func parseStream(r io.Reader, cb func(netip.Prefix)) error {
} }
if p, err := netip.ParsePrefix(line); err == nil { if p, err := netip.ParsePrefix(line); err == nil {
cb(p) cb(p)
continue
}
if addr, err := netip.ParseAddr(line); err == nil {
plen := 32
if addr.Is6() {
plen = 128
}
cb(netip.PrefixFrom(addr, plen))
} }
} }
return s.Err() return s.Err()
@@ -227,9 +227,9 @@ type Server struct {
func (s *Server) routes() http.Handler { func (s *Server) routes() http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/check/", s.handleCheck) // GET /check/<ip>?cats=spam,tor mux.HandleFunc("/check/", s.handleCheck)
mux.HandleFunc("/whitelist", s.handleAddWhite) // POST {"ip":"1.2.3.4"} mux.HandleFunc("/whitelist", s.handleAddWhite)
mux.HandleFunc("/categories", s.handleCats) // GET all categories mux.HandleFunc("/categories", s.handleCats)
return mux return mux
} }
@@ -240,18 +240,12 @@ func (s *Server) handleCheck(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bad ip", http.StatusBadRequest) http.Error(w, "bad ip", http.StatusBadRequest)
return return
} }
var cats []string var cats []string
if q := strings.TrimSpace(r.URL.Query().Get("cats")); q != "" { if q := strings.TrimSpace(r.URL.Query().Get("cats")); q != "" {
cats = strings.Split(q, ",") cats = strings.Split(q, ",")
} }
blocked := s.ranger.blockedInCats(addr, cats) blocked := s.ranger.blockedInCats(addr, cats)
writeJSON(w, map[string]any{ writeJSON(w, map[string]any{"ip": ipStr, "blocked": len(blocked) > 0, "categories": blocked})
"ip": ipStr,
"blocked": len(blocked) > 0,
"categories": blocked,
})
} }
func (s *Server) handleAddWhite(w http.ResponseWriter, r *http.Request) { func (s *Server) handleAddWhite(w http.ResponseWriter, r *http.Request) {
@@ -263,16 +257,16 @@ func (s *Server) handleAddWhite(w http.ResponseWriter, r *http.Request) {
IP string `json:"ip"` IP string `json:"ip"`
} }
if err := json.NewDecoder(r.Body).Decode(&body); err != nil { if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "bad json", 400) http.Error(w, "bad json", http.StatusBadRequest)
return return
} }
addr, err := netip.ParseAddr(strings.TrimSpace(body.IP)) addr, err := netip.ParseAddr(strings.TrimSpace(body.IP))
if err != nil { if err != nil {
http.Error(w, "bad ip", 400) http.Error(w, "bad ip", http.StatusBadRequest)
return return
} }
if err := s.rdb.Set(r.Context(), keyWhite(addr), "1", 0).Err(); err != nil { if err := s.rdb.Set(r.Context(), keyWhite(addr), "1", 0).Err(); err != nil {
http.Error(w, "redis", 500) http.Error(w, "redis", http.StatusInternalServerError)
return return
} }
s.ranger.addWhite(addr) s.ranger.addWhite(addr)