initial import from /x/ monorepo

Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Xe Iaso 2025-03-17 19:33:07 -04:00
commit 9923878c5c
No known key found for this signature in database
61 changed files with 5615 additions and 0 deletions

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

View 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)
}
})
}
}

View 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"
}
]
}

View file

@ -0,0 +1,5 @@
{
"bots": [
{}
]
}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,9 @@
{
"bots": [
{
"name": "generic-browser",
"user_agent_regex": "Mozilla",
"action": "CHALLENGE"
}
]
}

View file

@ -0,0 +1,10 @@
{
"bots": [
{
"name": "everything",
"user_agent_regex": ".*",
"action": "DENY"
}
],
"dnsbl": false
}

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

View 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)
}

View 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) + ")"
}
}