From b592aaf65659a2ad09878469cda5fe43c0c0bb27 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Mon, 29 Dec 2025 10:14:11 -0500 Subject: [PATCH] feat: iplist2rule utility command Assisted-By: GLM 4.7 via Claude Code Signed-off-by: Xe Iaso --- utils/cmd/iplist2rule/blocklist.go | 57 ++++++++++++++++++ utils/cmd/iplist2rule/main.go | 97 ++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 utils/cmd/iplist2rule/blocklist.go create mode 100644 utils/cmd/iplist2rule/main.go diff --git a/utils/cmd/iplist2rule/blocklist.go b/utils/cmd/iplist2rule/blocklist.go new file mode 100644 index 00000000..72bb47d8 --- /dev/null +++ b/utils/cmd/iplist2rule/blocklist.go @@ -0,0 +1,57 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "net/http" + "net/netip" + "strings" +) + +// FetchBlocklist reads the blocklist over HTTP and returns every non-commented +// line parsed as an IP address in CIDR notation. IPv4 addresses are returned as +// /32, IPv6 addresses as /128. +// +// This function was generated with GLM 4.7. +func FetchBlocklist(url string) ([]string, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP request failed with status: %s", resp.Status) + } + + var lines []string + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Text() + // Skip empty lines and comments (lines starting with #) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + addr, err := netip.ParseAddr(line) + if err != nil { + // Skip lines that aren't valid IP addresses + continue + } + + var cidr string + if addr.Is4() { + cidr = fmt.Sprintf("%s/32", addr.String()) + } else { + cidr = fmt.Sprintf("%s/128", addr.String()) + } + lines = append(lines, cidr) + } + + if err := scanner.Err(); err != nil && err != io.EOF { + return nil, err + } + + return lines, nil +} diff --git a/utils/cmd/iplist2rule/main.go b/utils/cmd/iplist2rule/main.go new file mode 100644 index 00000000..b7441cb7 --- /dev/null +++ b/utils/cmd/iplist2rule/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/TecharoHQ/anubis/lib/config" + "github.com/facebookgo/flagenv" + "sigs.k8s.io/yaml" +) + +type Rule struct { + Name string `yaml:"name" json:"name"` + Action config.Rule `yaml:"action" json:"action"` + RemoteAddr []string `json:"remote_addresses,omitempty" yaml:"remote_addresses,omitempty"` + Weight *config.Weight `json:"weight,omitempty" yaml:"weight,omitempty"` +} + +func init() { + flag.Usage = func() { + fmt.Printf(`Usage of %[1]s: + + %[1]s [--action=DENY|ALLOW] [--rule-name=] + +Grabs the contents of the blocklist, converts it to an Anubis ruleset, and writes it to filename. +`, filepath.Base(os.Args[0])) + } +} + +var ( + action = flag.String("action", "DENY", "Anubis action to take (ALLOW / DENY)") + manualRuleName = flag.String("rule-name", "", "If set, prefer this name over inferring from filename") + weight = flag.Int("weight", 0, "If set to any number, add/subtract this many weight points when --action=WEIGH") +) + +func main() { + flagenv.Parse() + flag.Parse() + + if flag.NArg() != 2 { + flag.Usage() + os.Exit(2) + } + + blocklistURL := flag.Arg(0) + foutName := flag.Arg(1) + ruleName := strings.TrimSuffix(foutName, filepath.Ext(foutName)) + + if *manualRuleName != "" { + ruleName = *manualRuleName + } + + ruleAction := config.Rule(*action) + if err := ruleAction.Valid(); err != nil { + log.Fatalf("--action=%q is invalid: %v", *action, err) + } + + result := &Rule{ + Name: ruleName, + Action: ruleAction, + } + + if *weight != 0 { + if ruleAction != config.RuleWeigh { + log.Fatalf("used --weight=%d but --action=%s", *weight, *action) + } + + result.Weight = &config.Weight{ + Adjust: *weight, + } + } + + ips, err := FetchBlocklist(blocklistURL) + if err != nil { + log.Fatalf("can't fetch blocklist %s: %v", blocklistURL, err) + } + + result.RemoteAddr = ips + + fout, err := os.Create(foutName) + if err != nil { + log.Fatalf("can't create output file %q: %v", foutName, err) + } + + defer fout.Close() + + data, err := yaml.Marshal([]*Rule{result}) + if err != nil { + log.Fatalf("can't marshal yaml") + } + + fout.Write(data) +}