initial import from /x/ monorepo
Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
commit
9923878c5c
61 changed files with 5615 additions and 0 deletions
99
cmd/anubis/internal/config/config.go
Normal file
99
cmd/anubis/internal/config/config.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Rule string
|
||||
|
||||
const (
|
||||
RuleUnknown = ""
|
||||
RuleAllow = "ALLOW"
|
||||
RuleDeny = "DENY"
|
||||
RuleChallenge = "CHALLENGE"
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
Name string `json:"name"`
|
||||
UserAgentRegex *string `json:"user_agent_regex"`
|
||||
PathRegex *string `json:"path_regex"`
|
||||
Action Rule `json:"action"`
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoBotRulesDefined = errors.New("config: must define at least one (1) bot rule")
|
||||
ErrBotMustHaveName = errors.New("config.Bot: must set name")
|
||||
ErrBotMustHaveUserAgentOrPath = errors.New("config.Bot: must set either user_agent_regex, path_regex")
|
||||
ErrBotMustHaveUserAgentOrPathNotBoth = errors.New("config.Bot: must set either user_agent_regex, path_regex, and not both")
|
||||
ErrUnknownAction = errors.New("config.Bot: unknown action")
|
||||
ErrInvalidUserAgentRegex = errors.New("config.Bot: invalid user agent regex")
|
||||
ErrInvalidPathRegex = errors.New("config.Bot: invalid path regex")
|
||||
)
|
||||
|
||||
func (b Bot) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if b.Name == "" {
|
||||
errs = append(errs, ErrBotMustHaveName)
|
||||
}
|
||||
|
||||
if b.UserAgentRegex == nil && b.PathRegex == nil {
|
||||
errs = append(errs, ErrBotMustHaveUserAgentOrPath)
|
||||
}
|
||||
|
||||
if b.UserAgentRegex != nil && b.PathRegex != nil {
|
||||
errs = append(errs, ErrBotMustHaveUserAgentOrPathNotBoth)
|
||||
}
|
||||
|
||||
if b.UserAgentRegex != nil {
|
||||
if _, err := regexp.Compile(*b.UserAgentRegex); err != nil {
|
||||
errs = append(errs, ErrInvalidUserAgentRegex, err)
|
||||
}
|
||||
}
|
||||
|
||||
if b.PathRegex != nil {
|
||||
if _, err := regexp.Compile(*b.PathRegex); err != nil {
|
||||
errs = append(errs, ErrInvalidPathRegex, err)
|
||||
}
|
||||
}
|
||||
|
||||
switch b.Action {
|
||||
case RuleAllow, RuleChallenge, RuleDeny:
|
||||
// okay
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("%w: %q", ErrUnknownAction, b.Action))
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("config: bot entry for %q is not valid:\n%w", b.Name, errors.Join(errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Bots []Bot `json:"bots"`
|
||||
DNSBL bool `json:"dnsbl"`
|
||||
}
|
||||
|
||||
func (c Config) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if len(c.Bots) == 0 {
|
||||
errs = append(errs, ErrNoBotRulesDefined)
|
||||
}
|
||||
|
||||
for _, b := range c.Bots {
|
||||
if err := b.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("config is not valid:\n%w", errors.Join(errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
168
cmd/anubis/internal/config/config_test.go
Normal file
168
cmd/anubis/internal/config/config_test.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func p[V any](v V) *V { return &v }
|
||||
|
||||
func TestBotValid(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
bot Bot
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "simple user agent",
|
||||
bot: Bot{
|
||||
Name: "mozilla-ua",
|
||||
Action: RuleChallenge,
|
||||
UserAgentRegex: p("Mozilla"),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "simple path",
|
||||
bot: Bot{
|
||||
Name: "well-known-path",
|
||||
Action: RuleAllow,
|
||||
PathRegex: p("^/.well-known/.*$"),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "no rule name",
|
||||
bot: Bot{
|
||||
Action: RuleChallenge,
|
||||
UserAgentRegex: p("Mozilla"),
|
||||
},
|
||||
err: ErrBotMustHaveName,
|
||||
},
|
||||
{
|
||||
name: "no rule matcher",
|
||||
bot: Bot{
|
||||
Name: "broken-rule",
|
||||
Action: RuleAllow,
|
||||
},
|
||||
err: ErrBotMustHaveUserAgentOrPath,
|
||||
},
|
||||
{
|
||||
name: "both user-agent and path",
|
||||
bot: Bot{
|
||||
Name: "path-and-user-agent",
|
||||
Action: RuleDeny,
|
||||
UserAgentRegex: p("Mozilla"),
|
||||
PathRegex: p("^/.secret-place/.*$"),
|
||||
},
|
||||
err: ErrBotMustHaveUserAgentOrPathNotBoth,
|
||||
},
|
||||
{
|
||||
name: "unknown action",
|
||||
bot: Bot{
|
||||
Name: "Unknown action",
|
||||
Action: RuleUnknown,
|
||||
UserAgentRegex: p("Mozilla"),
|
||||
},
|
||||
err: ErrUnknownAction,
|
||||
},
|
||||
{
|
||||
name: "invalid user agent regex",
|
||||
bot: Bot{
|
||||
Name: "mozilla-ua",
|
||||
Action: RuleChallenge,
|
||||
UserAgentRegex: p("a(b"),
|
||||
},
|
||||
err: ErrInvalidUserAgentRegex,
|
||||
},
|
||||
{
|
||||
name: "invalid path regex",
|
||||
bot: Bot{
|
||||
Name: "mozilla-ua",
|
||||
Action: RuleChallenge,
|
||||
PathRegex: p("a(b"),
|
||||
},
|
||||
err: ErrInvalidPathRegex,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range tests {
|
||||
cs := cs
|
||||
t.Run(cs.name, func(t *testing.T) {
|
||||
err := cs.bot.Valid()
|
||||
if err == nil && cs.err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil && cs.err != nil {
|
||||
t.Errorf("didn't get an error, but wanted: %v", cs.err)
|
||||
}
|
||||
|
||||
if !errors.Is(err, cs.err) {
|
||||
t.Logf("got wrong error from Valid()")
|
||||
t.Logf("wanted: %v", cs.err)
|
||||
t.Logf("got: %v", err)
|
||||
t.Errorf("got invalid error from check")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidKnownGood(t *testing.T) {
|
||||
finfos, err := os.ReadDir("testdata/good")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, st := range finfos {
|
||||
st := st
|
||||
t.Run(st.Name(), func(t *testing.T) {
|
||||
fin, err := os.Open(filepath.Join("testdata", "good", st.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
var c Config
|
||||
if err := json.NewDecoder(fin).Decode(&c); err != nil {
|
||||
t.Fatalf("can't decode file: %v", err)
|
||||
}
|
||||
|
||||
if err := c.Valid(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidBad(t *testing.T) {
|
||||
finfos, err := os.ReadDir("testdata/bad")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, st := range finfos {
|
||||
st := st
|
||||
t.Run(st.Name(), func(t *testing.T) {
|
||||
fin, err := os.Open(filepath.Join("testdata", "bad", st.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
var c Config
|
||||
if err := json.NewDecoder(fin).Decode(&c); err != nil {
|
||||
t.Fatalf("can't decode file: %v", err)
|
||||
}
|
||||
|
||||
if err := c.Valid(); err == nil {
|
||||
t.Fatal("validation should have failed but didn't somehow")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
14
cmd/anubis/internal/config/testdata/bad/badregexes.json
vendored
Normal file
14
cmd/anubis/internal/config/testdata/bad/badregexes.json
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"bots": [
|
||||
{
|
||||
"name": "path-bad",
|
||||
"path_regex": "a(b",
|
||||
"action": "DENY"
|
||||
},
|
||||
{
|
||||
"name": "user-agent-bad",
|
||||
"user_agent_regex": "a(b",
|
||||
"action": "DENY"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
cmd/anubis/internal/config/testdata/bad/invalid.json
vendored
Normal file
5
cmd/anubis/internal/config/testdata/bad/invalid.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"bots": [
|
||||
{}
|
||||
]
|
||||
}
|
||||
1
cmd/anubis/internal/config/testdata/bad/nobots.json
vendored
Normal file
1
cmd/anubis/internal/config/testdata/bad/nobots.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
9
cmd/anubis/internal/config/testdata/good/challengemozilla.json
vendored
Normal file
9
cmd/anubis/internal/config/testdata/good/challengemozilla.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"bots": [
|
||||
{
|
||||
"name": "generic-browser",
|
||||
"user_agent_regex": "Mozilla",
|
||||
"action": "CHALLENGE"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
cmd/anubis/internal/config/testdata/good/everything_blocked.json
vendored
Normal file
10
cmd/anubis/internal/config/testdata/good/everything_blocked.json
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"bots": [
|
||||
{
|
||||
"name": "everything",
|
||||
"user_agent_regex": ".*",
|
||||
"action": "DENY"
|
||||
}
|
||||
],
|
||||
"dnsbl": false
|
||||
}
|
||||
95
cmd/anubis/internal/dnsbl/dnsbl.go
Normal file
95
cmd/anubis/internal/dnsbl/dnsbl.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package dnsbl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:generate go tool golang.org/x/tools/cmd/stringer -type=DroneBLResponse
|
||||
|
||||
type DroneBLResponse byte
|
||||
|
||||
const (
|
||||
AllGood DroneBLResponse = 0
|
||||
IRCDrone DroneBLResponse = 3
|
||||
Bottler DroneBLResponse = 5
|
||||
UnknownSpambotOrDrone DroneBLResponse = 6
|
||||
DDOSDrone DroneBLResponse = 7
|
||||
SOCKSProxy DroneBLResponse = 8
|
||||
HTTPProxy DroneBLResponse = 9
|
||||
ProxyChain DroneBLResponse = 10
|
||||
OpenProxy DroneBLResponse = 11
|
||||
OpenDNSResolver DroneBLResponse = 12
|
||||
BruteForceAttackers DroneBLResponse = 13
|
||||
OpenWingateProxy DroneBLResponse = 14
|
||||
CompromisedRouter DroneBLResponse = 15
|
||||
AutoRootingWorms DroneBLResponse = 16
|
||||
AutoDetectedBotIP DroneBLResponse = 17
|
||||
Unknown DroneBLResponse = 255
|
||||
)
|
||||
|
||||
func Reverse(ip net.IP) string {
|
||||
if ip.To4() != nil {
|
||||
return reverse4(ip)
|
||||
}
|
||||
|
||||
return reverse6(ip)
|
||||
}
|
||||
|
||||
func reverse4(ip net.IP) string {
|
||||
splitAddress := strings.Split(ip.String(), ".")
|
||||
|
||||
// swap first and last octet
|
||||
splitAddress[0], splitAddress[3] = splitAddress[3], splitAddress[0]
|
||||
// swap middle octets
|
||||
splitAddress[1], splitAddress[2] = splitAddress[2], splitAddress[1]
|
||||
|
||||
return strings.Join(splitAddress, ".")
|
||||
}
|
||||
|
||||
func reverse6(ip net.IP) string {
|
||||
ipBytes := []byte(ip)
|
||||
var sb strings.Builder
|
||||
|
||||
for i := len(ipBytes) - 1; i >= 0; i-- {
|
||||
// Split the byte into two nibbles
|
||||
highNibble := ipBytes[i] >> 4
|
||||
lowNibble := ipBytes[i] & 0x0F
|
||||
|
||||
// Append the nibbles in reversed order
|
||||
sb.WriteString(fmt.Sprintf("%x.%x.", lowNibble, highNibble))
|
||||
}
|
||||
|
||||
return sb.String()[:len(sb.String())-1]
|
||||
}
|
||||
|
||||
func Lookup(ipStr string) (DroneBLResponse, error) {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return Unknown, errors.New("dnsbl: input is not an IP address")
|
||||
}
|
||||
|
||||
revIP := Reverse(ip) + ".dnsbl.dronebl.org"
|
||||
|
||||
ips, err := net.LookupIP(revIP)
|
||||
if err != nil {
|
||||
var dnserr *net.DNSError
|
||||
if errors.As(err, &dnserr) {
|
||||
if dnserr.IsNotFound {
|
||||
return AllGood, nil
|
||||
}
|
||||
}
|
||||
|
||||
return Unknown, err
|
||||
}
|
||||
|
||||
if len(ips) != 0 {
|
||||
for _, ip := range ips {
|
||||
return DroneBLResponse(ip.To4()[3]), nil
|
||||
}
|
||||
}
|
||||
|
||||
return UnknownSpambotOrDrone, nil
|
||||
}
|
||||
55
cmd/anubis/internal/dnsbl/dnsbl_test.go
Normal file
55
cmd/anubis/internal/dnsbl/dnsbl_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package dnsbl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReverse4(t *testing.T) {
|
||||
cases := []struct {
|
||||
inp, out string
|
||||
}{
|
||||
{"1.2.3.4", "4.3.2.1"},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(fmt.Sprintf("%s->%s", cs.inp, cs.out), func(t *testing.T) {
|
||||
out := reverse4(net.ParseIP(cs.inp))
|
||||
|
||||
if out != cs.out {
|
||||
t.Errorf("wanted %s\ngot: %s", cs.out, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverse6(t *testing.T) {
|
||||
cases := []struct {
|
||||
inp, out string
|
||||
}{
|
||||
{
|
||||
inp: "1234:5678:9ABC:DEF0:1234:5678:9ABC:DEF0",
|
||||
out: "0.f.e.d.c.b.a.9.8.7.6.5.4.3.2.1.0.f.e.d.c.b.a.9.8.7.6.5.4.3.2.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(fmt.Sprintf("%s->%s", cs.inp, cs.out), func(t *testing.T) {
|
||||
out := reverse6(net.ParseIP(cs.inp))
|
||||
|
||||
if out != cs.out {
|
||||
t.Errorf("wanted %s, got: %s", cs.out, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookup(t *testing.T) {
|
||||
resp, err := Lookup("27.65.243.194")
|
||||
if err != nil {
|
||||
t.Fatalf("it broked: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("response: %d", resp)
|
||||
}
|
||||
54
cmd/anubis/internal/dnsbl/droneblresponse_string.go
Normal file
54
cmd/anubis/internal/dnsbl/droneblresponse_string.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Code generated by "stringer -type=DroneBLResponse"; DO NOT EDIT.
|
||||
|
||||
package dnsbl
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[AllGood-0]
|
||||
_ = x[IRCDrone-3]
|
||||
_ = x[Bottler-5]
|
||||
_ = x[UnknownSpambotOrDrone-6]
|
||||
_ = x[DDOSDrone-7]
|
||||
_ = x[SOCKSProxy-8]
|
||||
_ = x[HTTPProxy-9]
|
||||
_ = x[ProxyChain-10]
|
||||
_ = x[OpenProxy-11]
|
||||
_ = x[OpenDNSResolver-12]
|
||||
_ = x[BruteForceAttackers-13]
|
||||
_ = x[OpenWingateProxy-14]
|
||||
_ = x[CompromisedRouter-15]
|
||||
_ = x[AutoRootingWorms-16]
|
||||
_ = x[AutoDetectedBotIP-17]
|
||||
_ = x[Unknown-255]
|
||||
}
|
||||
|
||||
const (
|
||||
_DroneBLResponse_name_0 = "AllGood"
|
||||
_DroneBLResponse_name_1 = "IRCDrone"
|
||||
_DroneBLResponse_name_2 = "BottlerUnknownSpambotOrDroneDDOSDroneSOCKSProxyHTTPProxyProxyChainOpenProxyOpenDNSResolverBruteForceAttackersOpenWingateProxyCompromisedRouterAutoRootingWormsAutoDetectedBotIP"
|
||||
_DroneBLResponse_name_3 = "Unknown"
|
||||
)
|
||||
|
||||
var (
|
||||
_DroneBLResponse_index_2 = [...]uint8{0, 7, 28, 37, 47, 56, 66, 75, 90, 109, 125, 142, 158, 175}
|
||||
)
|
||||
|
||||
func (i DroneBLResponse) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _DroneBLResponse_name_0
|
||||
case i == 3:
|
||||
return _DroneBLResponse_name_1
|
||||
case 5 <= i && i <= 17:
|
||||
i -= 5
|
||||
return _DroneBLResponse_name_2[_DroneBLResponse_index_2[i]:_DroneBLResponse_index_2[i+1]]
|
||||
case i == 255:
|
||||
return _DroneBLResponse_name_3
|
||||
default:
|
||||
return "DroneBLResponse(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue