Allow filtering by remote addresses (#52)

* Added the possibility to define rules for remote addresses

* Added change in changelog

* Added check for X-Real-Ip and X-Forwarded-For when checking for remote address filtering

* cmd/anubis: refine IP filtering logic

* Optimize the configuration so that the IP trie is created once at
  application start instead of dynamically being created every request.
* Document the changes in the changelog and docs site.
* Allow pure IP range filtering.
* Allow user agent based IP range filtering.
* Allow path based IP range filtering.
* Create --debug-x-real-ip-default flag for testing Anubis locally
  without a HTTP load balancer.

---------

Co-authored-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Remilia Da Costa Faro 2025-03-21 20:39:34 +01:00 committed by GitHub
parent e7b9b17b92
commit d6d879133e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 554 additions and 27 deletions

View file

@ -5,13 +5,16 @@ import (
"errors"
"fmt"
"io"
"log"
"log/slog"
"net"
"net/http"
"regexp"
"github.com/TecharoHQ/anubis/cmd/anubis/internal/config"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/yl2chen/cidranger"
)
var (
@ -32,8 +35,9 @@ type Bot struct {
Name string
UserAgent *regexp.Regexp
Path *regexp.Regexp
Action config.Rule
Action config.Rule `json:"action"`
Challenge *config.ChallengeRules
Ranger cidranger.Ranger
}
func (b Bot) Hash() (string, error) {
@ -77,6 +81,19 @@ func parseConfig(fin io.Reader, fname string, defaultDifficulty int) (*ParsedCon
Action: b.Action,
}
if b.RemoteAddr != nil && len(b.RemoteAddr) > 0 {
parsedBot.Ranger = cidranger.NewPCTrieRanger()
for _, cidr := range b.RemoteAddr {
_, rng, err := net.ParseCIDR(cidr)
if err != nil {
return nil, fmt.Errorf("[unexpected] range %s not parsing: %w", cidr, err)
}
parsedBot.Ranger.Insert(cidranger.NewBasicRangerEntry(*rng))
}
}
if b.UserAgentRegex != nil {
userAgent, err := regexp.Compile(*b.UserAgentRegex)
if err != nil {
@ -140,18 +157,47 @@ func cr(name string, rule config.Rule) CheckResult {
}
}
func (s *Server) checkRemoteAddress(b Bot, addr net.IP) bool {
if b.Ranger == nil {
return false
}
ok, err := b.Ranger.Contains(addr)
if err != nil {
log.Panicf("[unexpected] something very funky is going on, %q does not have a calculable network number: %v", addr.String(), err)
}
return ok
}
// Check evaluates the list of rules, and returns the result
func (s *Server) check(r *http.Request) (CheckResult, *Bot) {
func (s *Server) check(r *http.Request) (CheckResult, *Bot, error) {
host := r.Header.Get("X-Real-Ip")
if host == "" {
return zilch[CheckResult](), nil, fmt.Errorf("[misconfiguration] X-Real-Ip header is not set")
}
addr := net.ParseIP(host)
if addr == nil {
return zilch[CheckResult](), nil, fmt.Errorf("[misconfiguration] %q is not an IP address", host)
}
for _, b := range s.policy.Bots {
if b.UserAgent != nil {
if b.UserAgent.MatchString(r.UserAgent()) {
return cr("bot/"+b.Name, b.Action), &b
if uaMatch := b.UserAgent.MatchString(r.UserAgent()); uaMatch || (uaMatch && s.checkRemoteAddress(b, addr)) {
return cr("bot/"+b.Name, b.Action), &b, nil
}
}
if b.Path != nil {
if b.Path.MatchString(r.URL.Path) {
return cr("bot/"+b.Name, b.Action), &b
if pathMatch := b.Path.MatchString(r.URL.Path); pathMatch || (pathMatch && s.checkRemoteAddress(b, addr)) {
return cr("bot/"+b.Name, b.Action), &b, nil
}
}
if b.Ranger != nil {
if s.checkRemoteAddress(b, addr) {
return cr("bot/"+b.Name, b.Action), &b, nil
}
}
}
@ -162,5 +208,5 @@ func (s *Server) check(r *http.Request) (CheckResult, *Bot) {
ReportAs: defaultDifficulty,
Algorithm: config.AlgorithmFast,
},
}
}, nil
}