From a0224cb21c80218134900a774a4d328b874988cd Mon Sep 17 00:00:00 2001 From: WeebDataHoarder Date: Wed, 23 Apr 2025 18:58:45 +0200 Subject: [PATCH] policy: allow fetching ASN directly via RADb WHOIS service --- README.md | 3 +- examples/forgejo.yml | 12 +-- lib/policy/network.go | 12 ++- lib/state.go | 14 +++- utils/radb.go | 167 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 utils/radb.go diff --git a/README.md b/README.md index 8cc2a93..781bc58 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,8 @@ By default, a random temporary key is generated every run. Multiple backends are supported, and rules specific on backend can be defined, and conditions and rules can match this as well. +Subdomain wildcards like `*.example.com`, or full fallback wildcard `*` are supported. + This allows one instance to run multiple domains or subdomains. ### Package path @@ -217,7 +219,6 @@ This is tracked by tagging challenges with a readable flag indicating the type o The policy file at [examples/forgejo.yml](examples/forgejo.yml) provides a ready template to be used on your own Forgejo instance. Important notes: -* Edit the `homesite` rule, as it's targeted to common users or orgs on the instance. A better regex might be possible in the future. * Edit the `http-cookie-check` challenge, as this will fetch the listed backend with the given session cookie to check for user login. * Adjust the desired blocked networks or others. A template list of network ranges is provided, feel free to remove these if not needed. * Check the conditions and base rules to change your challenges offered and other ordering. diff --git a/examples/forgejo.yml b/examples/forgejo.yml index a200d1e..27edb54 100644 --- a/examples/forgejo.yml +++ b/examples/forgejo.yml @@ -8,17 +8,11 @@ networks: # Networks will get included from snippets huawei-cloud: - # AS136907 - - url: https://raw.githubusercontent.com/ipverse/asn-ip/refs/heads/master/as/136907/aggregated.json - jq-path: '.subnets.ipv4[], .subnets.ipv6[]' + - asn: 136907 alibaba-cloud: - # AS45102 - - url: https://raw.githubusercontent.com/ipverse/asn-ip/refs/heads/master/as/45102/aggregated.json - jq-path: '.subnets.ipv4[], .subnets.ipv6[]' + - asn: 45102 zenlayer-inc: - # AS21859 - - url: https://raw.githubusercontent.com/ipverse/asn-ip/refs/heads/master/as/21859/aggregated.json - jq-path: '.subnets.ipv4[], .subnets.ipv6[]' + - asn: 21859 challenges: diff --git a/lib/policy/network.go b/lib/policy/network.go index d741320..49a3efe 100644 --- a/lib/policy/network.go +++ b/lib/policy/network.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "git.gammaspectra.live/git/go-away/utils" "github.com/itchyny/gojq" "io" "net" @@ -13,16 +14,19 @@ import ( ) type Network struct { + // Fetches Url *string `yaml:"url,omitempty"` File *string `yaml:"file,omitempty"` + ASN *int `yaml:"asn,omitempty"` + // Filtering JqPath *string `yaml:"jq-path,omitempty"` Regex *string `yaml:"regex,omitempty"` Prefixes []string `yaml:"prefixes,omitempty"` } -func (n Network) FetchPrefixes(c *http.Client) (output []net.IPNet, err error) { +func (n Network) FetchPrefixes(c *http.Client, whois *utils.RADb) (output []net.IPNet, err error) { if len(n.Prefixes) > 0 { for _, prefix := range n.Prefixes { ipNet, err := parseCIDROrIP(prefix) @@ -51,6 +55,12 @@ func (n Network) FetchPrefixes(c *http.Client) (output []net.IPNet, err error) { } defer file.Close() reader = file + } else if n.ASN != nil { + result, err := whois.FetchASNets(*n.ASN) + if err != nil { + return nil, fmt.Errorf("failed to fetch ASN %d: %v", *n.ASN, err) + } + return result, nil } else { if len(output) > 0 { return output, nil diff --git a/lib/state.go b/lib/state.go index e1a5c62..acbd461 100644 --- a/lib/state.go +++ b/lib/state.go @@ -7,6 +7,7 @@ import ( "git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/condition" "git.gammaspectra.live/git/go-away/lib/policy" + "git.gammaspectra.live/git/go-away/utils" "github.com/google/cel-go/cel" "github.com/yl2chen/cidranger" "log/slog" @@ -19,6 +20,7 @@ import ( type State struct { client *http.Client + radb *utils.RADb urlPath string programEnv *cel.Env @@ -48,6 +50,11 @@ func NewState(p policy.Policy, settings policy.Settings) (handler http.Handler, return http.ErrUseLastResponse }, } + state.radb, err = utils.NewRADb() + if err != nil { + return nil, fmt.Errorf("failed to initialize RADb client: %w", err) + } + state.urlPath = "/.well-known/." + state.Settings().PackageName // set a reasonable configuration for default http proxy if there is none @@ -104,9 +111,12 @@ func NewState(p policy.Policy, settings policy.Settings) (handler http.Handler, if e.Url != nil { slog.Debug("loading network url list", "network", k, "url", *e.Url) } - prefixes, err := e.FetchPrefixes(state.client) + if e.ASN != nil { + slog.Debug("loading ASN", "network", k, "asn", *e.ASN) + } + prefixes, err := e.FetchPrefixes(state.client, state.radb) if err != nil { - slog.Error("error fetching network url list", "network", k, "url", *e.Url) + slog.Error("error fetching network list", "network", k, "url", *e.Url) continue } for _, prefix := range prefixes { diff --git a/utils/radb.go b/utils/radb.go new file mode 100644 index 0000000..76f2f26 --- /dev/null +++ b/utils/radb.go @@ -0,0 +1,167 @@ +package utils + +import ( + "bufio" + "bytes" + "fmt" + "net" + "regexp" + "strings" + "time" +) + +type RADb struct { + target string + dialer net.Dialer +} + +const RADBServer = "whois.radb.net:43" + +func NewRADb() (*RADb, error) { + + host, port, err := net.SplitHostPort(RADBServer) + if err != nil { + return nil, err + } + + return &RADb{ + target: fmt.Sprintf("%s:%s", host, port), + dialer: net.Dialer{ + Timeout: 5 * time.Second, + }, + }, nil +} + +var whoisRouteRegex = regexp.MustCompile("(?P(([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+)|([0-9a-f:]+::))/[0-9]+)") + +func (db *RADb) query(fn func(n int, record []byte) error, queries ...string) error { + + conn, err := db.dialer.Dial("tcp", db.target) + if err != nil { + return err + } + defer conn.Close() + + if len(queries) > 1 { + // enable persistent conn + _ = conn.SetDeadline(time.Now().Add(time.Second * 5)) + _, err = conn.Write([]byte("!!\n")) + if err != nil { + return err + } + } + + scanner := bufio.NewScanner(conn) + scanner.Split(bufio.ScanLines) + + for _, q := range queries { + + _ = conn.SetDeadline(time.Now().Add(time.Second * 5)) + _, err = conn.Write([]byte(strings.TrimSpace(q) + "\n")) + if err != nil { + return err + } + + n := 0 + + for scanner.Scan() { + buf := bytes.Trim(scanner.Bytes(), "\r\n") + if bytes.HasPrefix(buf, []byte("%")) || bytes.Equal(buf, []byte("C")) { + // end of record + break + } + err = fn(n, buf) + if err != nil { + return err + } + n++ + } + } + + if len(queries) > 1 { + // exit + _ = conn.SetDeadline(time.Now().Add(time.Second * 5)) + _, err = conn.Write([]byte("q\n")) + if err != nil { + return err + } + } + + return nil +} + +func init() { + db, _ := NewRADb() + db.FetchIPInfo(net.ParseIP("162.158.62.1")) +} + +func (db *RADb) FetchIPInfo(ip net.IP) (result []string, err error) { + var ipNet net.IPNet + if ip4 := ip.To4(); ip4 != nil { + ipNet = net.IPNet{ + IP: ip4, + // single ip + Mask: net.CIDRMask(len(ip4)*8, len(ip4)*8), + } + } else { + ipNet = net.IPNet{ + IP: ip, + // single ip + Mask: net.CIDRMask(len(ip)*8, len(ip)*8), + } + } + + err = db.query(func(n int, record []byte) error { + result = append(result, string(record)) + return nil + }, fmt.Sprintf("!r%s,l", ipNet.String())) + + if err != nil { + return nil, err + } + + return result, nil +} + +func (db *RADb) FetchASNets(asn int) (result []net.IPNet, err error) { + + ix := whoisRouteRegex.SubexpIndex("prefix") + if ix == -1 { + panic("invalid regex prefix") + } + + var data []byte + + err = db.query(func(n int, record []byte) error { + if n == 0 { + // do not append ASN number reply + return nil + } + // pad data + if n == 1 { + data = append(data, ' ') + } + data = append(data, record...) + return nil + }, + // See https://www.radb.net/query/help + // fetch IPv4 routes + fmt.Sprintf("!gas%d", asn), + // fetch IPv6 routes + fmt.Sprintf("!6as%d", asn), + ) + if err != nil { + return nil, err + } + + matches := whoisRouteRegex.FindAllSubmatch(data, -1) + for _, match := range matches { + _, ipNet, err := net.ParseCIDR(string(match[ix])) + if err != nil { + return nil, fmt.Errorf("invalid CIDR %s: %w", string(match[ix]), err) + } + result = append(result, *ipNet) + } + + return result, nil +}