check / check (push) Successful in 3m13s
Every *.rules file in SWWAF_RULES_DIR is read at start and on each change, and each request is checked against the rules after the rate limits: log notes a match, block refuses with 403, ban refuses and bans the netblock for SWWAF_ATTACK_BAN_DURATION, made permanent by its next request or clear sign of attack. path, query and uri are matched as the request line sent them. bans.json gains each ban's cause, and ban notes count earlier bans by cause. The image ships 00-default.rules. Judgement call: a header sent twice is matched with its values joined by ", ". Judgement call: SWWAF_MAX_BAN_DURATION does not cap a ban for an attack. Not in this unit: offences for rule matches, with the error burst. Model: opus-5-5
562 lines
18 KiB
Go
562 lines
18 KiB
Go
package config_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"maps"
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
|
)
|
|
|
|
// The settings, by name.
|
|
const (
|
|
listenAddr = "SWWAF_LISTEN_ADDR"
|
|
upstreamURL = "SWWAF_UPSTREAM_URL"
|
|
mode = "SWWAF_MODE"
|
|
trustedProxies = "SWWAF_TRUSTED_PROXIES"
|
|
clientRequestTimeout = "SWWAF_CLIENT_REQUEST_TIMEOUT"
|
|
clientHeaderMaxBytes = "SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES"
|
|
clientIdleTimeout = "SWWAF_CLIENT_IDLE_TIMEOUT"
|
|
clientResponseTimeout = "SWWAF_CLIENT_RESPONSE_TIMEOUT"
|
|
upstreamRequestTimeout = "SWWAF_UPSTREAM_REQUEST_TIMEOUT"
|
|
upstreamResponseTimeout = "SWWAF_UPSTREAM_RESPONSE_TIMEOUT"
|
|
requestMaxBytes = "SWWAF_REQUEST_MAX_BYTES"
|
|
responseMaxBytes = "SWWAF_RESPONSE_MAX_BYTES"
|
|
allowNets = "SWWAF_ALLOW_NETS"
|
|
rateLimitExemptNets = "SWWAF_RATE_LIMIT_EXEMPT_NETS"
|
|
denyNets = "SWWAF_DENY_NETS"
|
|
rateLimitPerMinute = "SWWAF_RATE_LIMIT_PER_MINUTE"
|
|
rateLimitPerHour = "SWWAF_RATE_LIMIT_PER_HOUR"
|
|
rateLimitPerDay = "SWWAF_RATE_LIMIT_PER_DAY"
|
|
deniedCountries = "SWWAF_DENIED_COUNTRIES"
|
|
allowedCountries = "SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES"
|
|
banResponse = "SWWAF_BAN_RESPONSE"
|
|
limitBanDuration = "SWWAF_LIMIT_BAN_DURATION"
|
|
limitBanRepeatWindow = "SWWAF_LIMIT_BAN_REPEAT_WINDOW"
|
|
maxBanDuration = "SWWAF_MAX_BAN_DURATION"
|
|
attackBanDuration = "SWWAF_ATTACK_BAN_DURATION"
|
|
maxBans = "SWWAF_MAX_BANS"
|
|
banScopeV4Prefix = "SWWAF_BAN_SCOPE_V4_PREFIX"
|
|
stateDir = "SWWAF_STATE_DIR"
|
|
stateWriteDelay = "SWWAF_STATE_WRITE_DELAY"
|
|
stateCounterInterval = "SWWAF_STATE_COUNTER_INTERVAL"
|
|
metricsToken = "SWWAF_METRICS_TOKEN" //nolint:gosec // the setting's name
|
|
metricsTopN = "SWWAF_METRICS_TOP_N"
|
|
rulesDir = "SWWAF_RULES_DIR"
|
|
rulesEnabled = "SWWAF_RULES_ENABLED"
|
|
)
|
|
|
|
// token is a token of 32 characters, the shortest allowed.
|
|
const token = "0123456789abcdef0123456789abcdef"
|
|
|
|
// off switches a timeout, a size limit or a rate limit off.
|
|
const off = "off"
|
|
|
|
// environment is a set of environment variables, for FromEnvironment.
|
|
type environment map[string]string
|
|
|
|
// lookupEnv reads one of the variables, as os.LookupEnv does.
|
|
func (e environment) lookupEnv(name string) (string, bool) {
|
|
value, ok := e[name]
|
|
|
|
return value, ok
|
|
}
|
|
|
|
// fromEnvironment reads the settings from env, which must be valid.
|
|
func fromEnvironment(t *testing.T, env environment) *config.Config {
|
|
t.Helper()
|
|
|
|
cfg, err := config.FromEnvironment(env.lookupEnv)
|
|
if err != nil {
|
|
t.Fatalf("settings %v: %v", env, err)
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
func TestDefaults(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := fromEnvironment(t, environment{})
|
|
|
|
wantSettings(t, cfg, config.Config{
|
|
ListenAddr: ":8080",
|
|
Observe: false,
|
|
ClientRequestTimeout: time.Minute,
|
|
ClientRequestHeaderMaxBytes: 32 << 10,
|
|
ClientIdleTimeout: 2 * time.Minute,
|
|
ClientResponseTimeout: 30 * time.Minute,
|
|
UpstreamRequestTimeout: time.Minute,
|
|
UpstreamResponseTimeout: 30 * time.Minute,
|
|
RequestMaxBytes: 100 << 20,
|
|
ResponseMaxBytes: 5 << 30,
|
|
RateLimitPerMinute: 1000,
|
|
RateLimitPerHour: 10000,
|
|
RateLimitPerDay: 50000,
|
|
BanResponse: 403,
|
|
LimitBanDuration: time.Hour,
|
|
LimitBanRepeatWindow: 24 * time.Hour,
|
|
MaxBanDuration: 7 * 24 * time.Hour,
|
|
AttackBanDuration: 7 * 24 * time.Hour,
|
|
MaxBans: 5000,
|
|
BanScopeV4Prefix: 32,
|
|
StateDir: "/var/lib/smallwebwaf",
|
|
StateWriteDelay: 10 * time.Second,
|
|
StateCounterInterval: 15 * time.Minute,
|
|
MetricsToken: "",
|
|
MetricsTopN: 50,
|
|
RulesDir: "/etc/smallwebwaf/rules.d",
|
|
RulesEnabled: true,
|
|
})
|
|
|
|
if cfg.UpstreamURL.String() != "http://127.0.0.1:8081" {
|
|
t.Errorf("%s is %s", upstreamURL, cfg.UpstreamURL)
|
|
}
|
|
|
|
wantNetblocks(t, cfg.TrustedProxies,
|
|
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
|
|
wantNetblocks(t, cfg.AllowNets)
|
|
wantNetblocks(t, cfg.RateLimitExemptNets)
|
|
wantNetblocks(t, cfg.DenyNets)
|
|
wantCountries(t, deniedCountries, cfg.DeniedCountries)
|
|
wantCountries(t, allowedCountries, cfg.ExclusivelyAllowedCountries)
|
|
}
|
|
|
|
func TestValuesAsSet(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := fromEnvironment(t, environment{
|
|
listenAddr: "127.0.0.1:9000",
|
|
upstreamURL: "https://app.internal:8443/",
|
|
mode: "observe",
|
|
trustedProxies: " 192.0.2.1, 10.1.2.3/8 ,2001:db8::/32",
|
|
clientRequestTimeout: "90s",
|
|
clientHeaderMaxBytes: "8K",
|
|
clientIdleTimeout: "5m",
|
|
clientResponseTimeout: "7d",
|
|
upstreamRequestTimeout: "1h30m",
|
|
upstreamResponseTimeout: off,
|
|
requestMaxBytes: "512K",
|
|
responseMaxBytes: "1234",
|
|
allowNets: "192.0.2.7",
|
|
rateLimitExemptNets: "2001:db8::/48, 10.9.8.7",
|
|
denyNets: "198.51.100.0/24",
|
|
rateLimitPerMinute: "60",
|
|
rateLimitPerHour: "600",
|
|
rateLimitPerDay: "6000",
|
|
deniedCountries: "cn, RU,kp,Xk",
|
|
allowedCountries: "de",
|
|
banResponse: "429",
|
|
limitBanDuration: "15m",
|
|
limitBanRepeatWindow: "2d",
|
|
maxBanDuration: "30d",
|
|
attackBanDuration: "1d",
|
|
maxBans: "100",
|
|
banScopeV4Prefix: "24",
|
|
stateDir: "/srv/waf-state",
|
|
stateWriteDelay: "500ms",
|
|
stateCounterInterval: "1h",
|
|
metricsToken: token,
|
|
metricsTopN: "10",
|
|
rulesDir: "/srv/waf-rules",
|
|
rulesEnabled: "false",
|
|
})
|
|
|
|
wantSettings(t, cfg, config.Config{
|
|
ListenAddr: "127.0.0.1:9000",
|
|
Observe: true,
|
|
ClientRequestTimeout: 90 * time.Second,
|
|
ClientRequestHeaderMaxBytes: 8 << 10,
|
|
ClientIdleTimeout: 5 * time.Minute,
|
|
ClientResponseTimeout: 7 * 24 * time.Hour,
|
|
UpstreamRequestTimeout: 90 * time.Minute,
|
|
UpstreamResponseTimeout: 0,
|
|
RequestMaxBytes: 512 << 10,
|
|
ResponseMaxBytes: 1234,
|
|
RateLimitPerMinute: 60,
|
|
RateLimitPerHour: 600,
|
|
RateLimitPerDay: 6000,
|
|
BanResponse: 429,
|
|
LimitBanDuration: 15 * time.Minute,
|
|
LimitBanRepeatWindow: 48 * time.Hour,
|
|
MaxBanDuration: 30 * 24 * time.Hour,
|
|
AttackBanDuration: 24 * time.Hour,
|
|
MaxBans: 100,
|
|
BanScopeV4Prefix: 24,
|
|
StateDir: "/srv/waf-state",
|
|
StateWriteDelay: 500 * time.Millisecond,
|
|
StateCounterInterval: time.Hour,
|
|
MetricsToken: token,
|
|
MetricsTopN: 10,
|
|
RulesDir: "/srv/waf-rules",
|
|
RulesEnabled: false,
|
|
})
|
|
|
|
if cfg.UpstreamURL.String() != "https://app.internal:8443/" {
|
|
t.Errorf("%s is %s", upstreamURL, cfg.UpstreamURL)
|
|
}
|
|
|
|
wantNetblocks(t, cfg.TrustedProxies, "192.0.2.1/32", "10.0.0.0/8", "2001:db8::/32")
|
|
wantNetblocks(t, cfg.AllowNets, "192.0.2.7/32")
|
|
wantNetblocks(t, cfg.RateLimitExemptNets, "2001:db8::/48", "10.9.8.7/32")
|
|
wantNetblocks(t, cfg.DenyNets, "198.51.100.0/24")
|
|
wantCountries(t, deniedCountries, cfg.DeniedCountries, "CN", "RU", "KP", "XK")
|
|
wantCountries(t, allowedCountries, cfg.ExclusivelyAllowedCountries, "DE")
|
|
}
|
|
|
|
func TestCodeOnBothCountryListsStopsTheStart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := config.FromEnvironment(environment{
|
|
deniedCountries: "cn,ru",
|
|
allowedCountries: "de,RU",
|
|
}.lookupEnv)
|
|
if err == nil {
|
|
t.Fatal("ru on both country lists was accepted")
|
|
}
|
|
|
|
if !strings.HasPrefix(err.Error(), allowedCountries+": ") ||
|
|
!strings.Contains(err.Error(), `"RU"`) {
|
|
t.Errorf("error %q does not name %s and RU", err, allowedCountries)
|
|
}
|
|
}
|
|
|
|
func TestSizesAndOff(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := fromEnvironment(t, environment{
|
|
requestMaxBytes: "3G",
|
|
responseMaxBytes: off,
|
|
clientRequestTimeout: off,
|
|
clientIdleTimeout: off,
|
|
})
|
|
|
|
if cfg.RequestMaxBytes != 3<<30 || cfg.ResponseMaxBytes != 0 ||
|
|
cfg.ClientRequestTimeout != 0 || cfg.ClientIdleTimeout != 0 {
|
|
t.Errorf("3G, off, off and off read as %d, %d, %s and %s",
|
|
cfg.RequestMaxBytes, cfg.ResponseMaxBytes, cfg.ClientRequestTimeout,
|
|
cfg.ClientIdleTimeout)
|
|
}
|
|
}
|
|
|
|
func TestRequestHeaderMaxBytesJustOver4K(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := fromEnvironment(t, environment{clientHeaderMaxBytes: "4097"})
|
|
if cfg.ClientRequestHeaderMaxBytes != 4097 {
|
|
t.Errorf("4097 read as %d", cfg.ClientRequestHeaderMaxBytes)
|
|
}
|
|
}
|
|
|
|
func TestRequestHeaderMaxBytesRefusalNeverOffersOff(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, value := range []string{"32KB", "0", "4K", off} {
|
|
t.Run(value, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := config.FromEnvironment(
|
|
environment{clientHeaderMaxBytes: value}.lookupEnv)
|
|
|
|
want := clientHeaderMaxBytes + `: "` + value +
|
|
`" is not a size of more than 4K, such as 32K`
|
|
if err == nil || err.Error() != want {
|
|
t.Errorf("error %v, want %s", err, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRateLimitsOff(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := fromEnvironment(t, environment{
|
|
rateLimitPerMinute: off,
|
|
rateLimitPerHour: off,
|
|
rateLimitPerDay: off,
|
|
})
|
|
|
|
if cfg.RateLimitPerMinute != 0 || cfg.RateLimitPerHour != 0 ||
|
|
cfg.RateLimitPerDay != 0 {
|
|
t.Errorf("off read as %d, %d and %d",
|
|
cfg.RateLimitPerMinute, cfg.RateLimitPerHour, cfg.RateLimitPerDay)
|
|
}
|
|
}
|
|
|
|
func TestBanResponseCloseIsZero(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := fromEnvironment(t, environment{banResponse: "close"})
|
|
if cfg.BanResponse != 0 {
|
|
t.Errorf("close read as %d, want 0", cfg.BanResponse)
|
|
}
|
|
}
|
|
|
|
func TestTrustedProxiesSetButEmptyTrustNothing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := fromEnvironment(t, environment{trustedProxies: ""})
|
|
if len(cfg.TrustedProxies) != 0 {
|
|
t.Errorf("trusted proxies %v, want none", cfg.TrustedProxies)
|
|
}
|
|
}
|
|
|
|
func TestInvalidValueStopsTheStart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct{ name, value string }{
|
|
{listenAddr, "8080"},
|
|
{listenAddr, ":http"},
|
|
{listenAddr, ":65536"},
|
|
{upstreamURL, "127.0.0.1:8081"},
|
|
{upstreamURL, "ftp://127.0.0.1:8081"},
|
|
{upstreamURL, "http://"},
|
|
{upstreamURL, "http://:8081"},
|
|
{upstreamURL, "http://127.0.0.1:0"},
|
|
{upstreamURL, "http://127.0.0.1:99999"},
|
|
{upstreamURL, "http://127.0.0.1:8081/app"},
|
|
{upstreamURL, "http://127.0.0.1:8081/?a=1"},
|
|
{upstreamURL, "http://user:secret@127.0.0.1:8081"},
|
|
{mode, "Observe"}, {mode, "block"}, {mode, ""},
|
|
{trustedProxies, "10.0.0.0/33"},
|
|
{trustedProxies, "traefik"},
|
|
{trustedProxies, "10.0.0.0/8,,192.168.0.0/16"},
|
|
{trustedProxies, "fe80::1%eth0"},
|
|
{allowNets, "192.0.2.0/24,monitoring"},
|
|
{rateLimitExemptNets, "2001:db8::/129"},
|
|
{denyNets, "198.51.100.0/24,"},
|
|
{clientRequestTimeout, "60"},
|
|
{clientRequestTimeout, ""},
|
|
{clientIdleTimeout, "0s"},
|
|
{clientIdleTimeout, "2 minutes"},
|
|
{clientResponseTimeout, "1y"},
|
|
{upstreamRequestTimeout, "-1s"},
|
|
{upstreamResponseTimeout, "0s"},
|
|
{upstreamResponseTimeout, "1.5d"},
|
|
{requestMaxBytes, "100MB"},
|
|
{requestMaxBytes, "100m"},
|
|
{requestMaxBytes, "1.5M"},
|
|
{responseMaxBytes, "0"},
|
|
{responseMaxBytes, "-5"},
|
|
{responseMaxBytes, "99999999999G"},
|
|
{rateLimitPerMinute, ""},
|
|
{rateLimitPerMinute, "1K"},
|
|
{rateLimitPerHour, "0"},
|
|
{rateLimitPerHour, "1.5"},
|
|
{rateLimitPerDay, "-1"},
|
|
{rateLimitPerDay, "lots"},
|
|
{deniedCountries, "nk"},
|
|
{deniedCountries, "kp,,ir"},
|
|
{deniedCountries, "prk"},
|
|
{deniedCountries, "408"},
|
|
{deniedCountries, "k"},
|
|
{deniedCountries, "eu"},
|
|
{deniedCountries, "un"},
|
|
{deniedCountries, "su"},
|
|
{allowedCountries, "ac"},
|
|
{allowedCountries, "uk"},
|
|
{allowedCountries, "zz"},
|
|
{allowedCountries, "de,germany"},
|
|
{banResponse, "404"}, {banResponse, "drop"}, {banResponse, ""},
|
|
{limitBanDuration, off}, {limitBanDuration, "0s"}, {limitBanDuration, "1"},
|
|
{limitBanRepeatWindow, off}, {limitBanRepeatWindow, "-1h"},
|
|
{maxBanDuration, off}, {maxBanDuration, "1w"}, {attackBanDuration, off},
|
|
{maxBans, off}, {maxBans, "0"}, {maxBans, "5K"},
|
|
{banScopeV4Prefix, "33"}, {banScopeV4Prefix, "-1"}, {banScopeV4Prefix, "/24"},
|
|
{stateDir, ""}, {stateDir, "state"}, {stateDir, "./var/lib/smallwebwaf"},
|
|
{stateWriteDelay, off}, {stateWriteDelay, "0s"},
|
|
{stateCounterInterval, off}, {stateCounterInterval, "15"},
|
|
{metricsTopN, off}, {metricsTopN, "0"}, {metricsTopN, "-1"},
|
|
{rulesEnabled, "yes"}, {rulesEnabled, "True"},
|
|
} {
|
|
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
|
|
if err == nil {
|
|
t.Fatalf("%s=%q was accepted", tc.name, tc.value)
|
|
}
|
|
|
|
if !strings.HasPrefix(err.Error(), tc.name+": ") {
|
|
t.Errorf("error %q does not name %s", err, tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShortTokenStopsTheStartWithoutShowingIt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Characters are counted, not bytes: each é takes two.
|
|
for _, value := range []string{"", token[1:], strings.Repeat("é", 31)} {
|
|
t.Run(value, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := config.FromEnvironment(environment{metricsToken: value}.lookupEnv)
|
|
|
|
want := metricsToken + ": is shorter than 32 characters"
|
|
if err == nil || err.Error() != want {
|
|
t.Errorf("error %v, want %s", err, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTokenIsLoggedMasked(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := fromEnvironment(t, environment{metricsToken: token})
|
|
|
|
var out bytes.Buffer
|
|
|
|
slog.New(slog.NewJSONHandler(&out, nil)).Info("starting", "settings", cfg)
|
|
|
|
if strings.Contains(out.String(), token) ||
|
|
!strings.Contains(out.String(), `"`+metricsToken+`":"********"`) {
|
|
t.Errorf("the token is not logged masked: %s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestLogsEachSettingWithItsValue(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := fromEnvironment(t, environment{clientRequestTimeout: "45s"})
|
|
|
|
var out bytes.Buffer
|
|
|
|
slog.New(slog.NewJSONHandler(&out, nil)).Info("starting", "settings", cfg)
|
|
|
|
var line struct {
|
|
Settings map[string]string `json:"settings"`
|
|
}
|
|
|
|
err := json.Unmarshal(out.Bytes(), &line)
|
|
if err != nil {
|
|
t.Fatalf("decode %s: %v", out.Bytes(), err)
|
|
}
|
|
|
|
want := map[string]string{
|
|
listenAddr: ":8080",
|
|
upstreamURL: "http://127.0.0.1:8081",
|
|
mode: "enforce",
|
|
trustedProxies: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16",
|
|
clientRequestTimeout: "45s",
|
|
clientHeaderMaxBytes: "32K",
|
|
clientIdleTimeout: "120s",
|
|
clientResponseTimeout: "30m",
|
|
upstreamRequestTimeout: "60s",
|
|
upstreamResponseTimeout: "30m",
|
|
requestMaxBytes: "100M",
|
|
responseMaxBytes: "5G",
|
|
allowNets: "",
|
|
rateLimitExemptNets: "",
|
|
denyNets: "",
|
|
rateLimitPerMinute: "1000",
|
|
rateLimitPerHour: "10000",
|
|
rateLimitPerDay: "50000",
|
|
deniedCountries: "",
|
|
allowedCountries: "",
|
|
banResponse: "403",
|
|
limitBanDuration: "1h",
|
|
limitBanRepeatWindow: "24h",
|
|
maxBanDuration: "7d",
|
|
attackBanDuration: "7d",
|
|
maxBans: "5000",
|
|
banScopeV4Prefix: "32",
|
|
stateDir: "/var/lib/smallwebwaf",
|
|
stateWriteDelay: "10s",
|
|
stateCounterInterval: "15m",
|
|
metricsToken: "",
|
|
metricsTopN: "50",
|
|
rulesDir: "/etc/smallwebwaf/rules.d",
|
|
rulesEnabled: "true",
|
|
}
|
|
if !maps.Equal(line.Settings, want) {
|
|
t.Errorf("logged settings\n%v\nwant\n%v", line.Settings, want)
|
|
}
|
|
}
|
|
|
|
// wantSettings checks the settings that are plain values.
|
|
func wantSettings(t *testing.T, got *config.Config, want config.Config) {
|
|
t.Helper()
|
|
|
|
if got.ListenAddr != want.ListenAddr ||
|
|
got.Observe != want.Observe ||
|
|
got.ClientRequestTimeout != want.ClientRequestTimeout ||
|
|
got.ClientRequestHeaderMaxBytes != want.ClientRequestHeaderMaxBytes ||
|
|
got.ClientIdleTimeout != want.ClientIdleTimeout ||
|
|
got.ClientResponseTimeout != want.ClientResponseTimeout ||
|
|
got.UpstreamRequestTimeout != want.UpstreamRequestTimeout ||
|
|
got.UpstreamResponseTimeout != want.UpstreamResponseTimeout ||
|
|
got.RequestMaxBytes != want.RequestMaxBytes ||
|
|
got.ResponseMaxBytes != want.ResponseMaxBytes ||
|
|
got.RateLimitPerMinute != want.RateLimitPerMinute ||
|
|
got.RateLimitPerHour != want.RateLimitPerHour ||
|
|
got.RateLimitPerDay != want.RateLimitPerDay {
|
|
t.Errorf("settings\n%+v\nwant\n%+v", got, want)
|
|
}
|
|
|
|
wantBanSettings(t, got, want)
|
|
}
|
|
|
|
// wantBanSettings checks the settings for bans, the state files, the
|
|
// metrics and the rule files.
|
|
func wantBanSettings(t *testing.T, got *config.Config, want config.Config) {
|
|
t.Helper()
|
|
|
|
if got.BanResponse != want.BanResponse ||
|
|
got.LimitBanDuration != want.LimitBanDuration ||
|
|
got.LimitBanRepeatWindow != want.LimitBanRepeatWindow ||
|
|
got.MaxBanDuration != want.MaxBanDuration ||
|
|
got.AttackBanDuration != want.AttackBanDuration ||
|
|
got.MaxBans != want.MaxBans ||
|
|
got.BanScopeV4Prefix != want.BanScopeV4Prefix {
|
|
t.Errorf("ban settings\n%+v\nwant\n%+v", got, want)
|
|
}
|
|
|
|
if got.RulesDir != want.RulesDir || got.RulesEnabled != want.RulesEnabled {
|
|
t.Errorf("rule files in %q, on: %t, want %q, %t",
|
|
got.RulesDir, got.RulesEnabled, want.RulesDir, want.RulesEnabled)
|
|
}
|
|
|
|
if got.StateDir != want.StateDir ||
|
|
got.StateWriteDelay != want.StateWriteDelay ||
|
|
got.StateCounterInterval != want.StateCounterInterval {
|
|
t.Errorf("state settings\n%+v\nwant\n%+v", got, want)
|
|
}
|
|
|
|
if got.MetricsToken != want.MetricsToken || got.MetricsTopN != want.MetricsTopN {
|
|
t.Errorf("metrics token %q and top %d, want %q and %d",
|
|
got.MetricsToken, got.MetricsTopN, want.MetricsToken, want.MetricsTopN)
|
|
}
|
|
}
|
|
|
|
// wantNetblocks checks a list of netblocks.
|
|
func wantNetblocks(t *testing.T, got []netip.Prefix, want ...string) {
|
|
t.Helper()
|
|
|
|
gotText := make([]string, 0, len(got))
|
|
for _, netblock := range got {
|
|
gotText = append(gotText, netblock.String())
|
|
}
|
|
|
|
if !slices.Equal(gotText, want) {
|
|
t.Errorf("netblocks %v, want %v", gotText, want)
|
|
}
|
|
}
|
|
|
|
// wantCountries checks the list of countries the setting name gave.
|
|
func wantCountries(t *testing.T, name string, got []string, want ...string) {
|
|
t.Helper()
|
|
|
|
if !slices.Equal(got, want) {
|
|
t.Errorf("%s gave %v, want %v", name, got, want)
|
|
}
|
|
}
|