feat: iplist2rule utility command (#1373)

* feat: iplist2rule utility command

Assisted-By: GLM 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: update CHANGELOG

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: fix spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: fix spelling again

Signed-off-by: Xe Iaso <me@xeiaso.net>

* feat(iplist2rule): add comment describing how rule was generated

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: add iplist2rule docs

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: fix spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Xe Iaso 2025-12-29 12:10:17 -05:00 committed by GitHub
parent 1d8e98c5ec
commit 359613f35a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 223 additions and 4 deletions

View file

@ -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
}

View file

@ -0,0 +1,103 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"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 [flags] <blocklist-url> <filename>
Grabs the contents of the blocklist, converts it to an Anubis ruleset, and writes it to filename.
Flags:
`, filepath.Base(os.Args[0]))
flag.PrintDefaults()
}
}
var (
action = flag.String("action", "DENY", "Anubis action to take (ALLOW / DENY / WEIGH)")
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()
fmt.Fprintf(fout, "# Generated by %s on %s from %s\n\n", filepath.Base(os.Args[0]), time.Now().Format(time.RFC3339), blocklistURL)
data, err := yaml.Marshal([]*Rule{result})
if err != nil {
log.Fatalf("can't marshal yaml")
}
fout.Write(data)
}