Read every setting through viper and rename the settings (closes #211)
check / check (push) Failing after 3m27s

Every setting of discriminator and rproxy is read through viper, in place
of the os.LookupEnv function Main passed down. NewViper turns AutomaticEnv
and AllowEmptyEnv on, so a setting that is set but empty reads as set. A
setting may also be given as a file, the _FILE form, which rproxy's
settings now have too; rproxy still writes nothing to disk.

The settings are renamed. Discriminator's take the prefix SWW_WAF_,
rproxy's take SWW_RPROXY_, and the shared SWW_COMMAND keeps SWW_. The Core
Rule Set's settings take SWW_WAF_CORE_RULE_SET_ rather than SWW_WAF_WAF_.
Every place a name appeared changed: the code, the viper keys, tests,
documents, examples, the README's settings table and the image files.

Model: deepseek-v4-pro
This commit is contained in:
sneak committed 2026-11-07 06:41:11 +00:00
1 parent beb503b233
commit 68da92f414
89 files changed
+2027 -1916

No files matched your search

+13 -15
View File
@@ -10,6 +10,8 @@ import (
"net/url"
"time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/discriminator"
)
@@ -21,17 +23,17 @@ var errHealthEndpoint = errors.New("the health endpoint answered")
// HealthCheck is the container's health check. In discriminator's container, where
// SWW_COMMAND is discriminator, it returns 0 while discriminator answers its health
// endpoint on 127.0.0.1, at the port in SWW_LISTEN_ADDR. In rproxy's container,
// endpoint on 127.0.0.1, at the port in SWW_WAF_LISTEN_ADDR. In rproxy's container,
// where SWW_COMMAND is rproxy, unset or empty, it returns 0 while rproxy
// answers its health endpoint on 127.0.0.1, at the port in SWRP_LISTEN_ADDR, and
// the app accepts connections at the address in SWRP_UPSTREAM_URL. Otherwise,
// answers its health endpoint on 127.0.0.1, at the port in SWW_RPROXY_LISTEN_ADDR, and
// the app accepts connections at the address in SWW_RPROXY_UPSTREAM_URL. Otherwise,
// any other value of SWW_COMMAND included, it writes why to stderr and
// returns 1. It reads no other setting, nor a file that another names, so neither
// can fail it.
// args are the arguments after `healthcheck`; it takes none, and given
// one it names it on stderr and returns 1 without checking anything.
func HealthCheck(
ctx context.Context, args []string, lookupEnv func(string) (string, bool),
ctx context.Context, args []string, v *viper.Viper,
stderr io.Writer,
) int {
if len(args) > 0 {
@@ -44,15 +46,15 @@ func HealthCheck(
ctx, cancel := context.WithTimeout(ctx, healthCheckTimeout)
defer cancel()
command, err := config.Command(lookupEnv)
command, err := config.Command(v)
switch {
case err != nil:
err = fmt.Errorf("invalid setting: %w", err)
case command == "discriminator":
err = checkDiscriminator(ctx, lookupEnv)
err = checkDiscriminator(ctx, v)
default:
err = checkRProxyAndApp(ctx, lookupEnv)
err = checkRProxyAndApp(ctx, v)
}
if err != nil {
@@ -65,10 +67,8 @@ func HealthCheck(
}
// checkDiscriminator asks discriminator's health endpoint.
func checkDiscriminator(
ctx context.Context, lookupEnv func(string) (string, bool),
) error {
listenAddr, err := config.DiscriminatorListenAddr(lookupEnv)
func checkDiscriminator(ctx context.Context, v *viper.Viper) error {
listenAddr, err := config.DiscriminatorListenAddr(v)
if err != nil {
return fmt.Errorf("invalid setting: %w", err)
}
@@ -82,10 +82,8 @@ func checkDiscriminator(
}
// checkRProxyAndApp asks rproxy's health endpoint, and connects to the app.
func checkRProxyAndApp(
ctx context.Context, lookupEnv func(string) (string, bool),
) error {
listenAddr, upstreamURL, err := config.RProxyListenAddrAndUpstreamURL(lookupEnv)
func checkRProxyAndApp(ctx context.Context, v *viper.Viper) error {
listenAddr, upstreamURL, err := config.RProxyListenAddrAndUpstreamURL(v)
if err != nil {
return fmt.Errorf("invalid setting: %w", err)
}
+12 -19
View File
@@ -54,9 +54,9 @@ func TestHealthCheckInDiscriminatorsContainer(t *testing.T) {
// or invalid token file, or an invalid value of another setting, does not
// fail it.
for _, other := range []struct{ name, value string }{
{"SWW_METRICS_TOKEN_FILE", filepath.Join(t.TempDir(), "removed")},
{"SWW_METRICS_TOKEN_FILE", writeFile(t, "too short\n")},
{"SWW_MODE", "neither"},
{"SWW_WAF_METRICS_TOKEN_FILE", filepath.Join(t.TempDir(), "removed")},
{"SWW_WAF_METRICS_TOKEN_FILE", writeFile(t, "too short\n")},
{"SWW_WAF_MODE", "neither"},
} {
wantHealthCheck(t, map[string]string{
commandSetting: discriminatorCommand,
@@ -72,7 +72,7 @@ func TestHealthCheckInDiscriminatorsContainer(t *testing.T) {
}, 1, "unhealthy: ask discriminator: ")
wantHealthCheck(t, map[string]string{
commandSetting: discriminatorCommand, listenAddr: "8080",
}, 1, "unhealthy: invalid setting: SWW_LISTEN_ADDR: ")
}, 1, "unhealthy: invalid setting: SWW_WAF_LISTEN_ADDR: ")
stop()
@@ -111,8 +111,8 @@ func TestHealthCheckInRProxysContainer(t *testing.T) {
0, "")
// Another setting does not fail it, even an invalid one, and neither does
// SWRP_WAF_BACKEND being unset.
wantHealthCheck(t, settings("SWRP_TIMEOUT_MS", "soon"), 0, "")
// SWW_RPROXY_WAF_BACKEND being unset.
wantHealthCheck(t, settings("SWW_RPROXY_TIMEOUT_MS", "soon"), 0, "")
// Another test can take a closed server's port at once, so the app's
// address here is a multicast address, to which TCP cannot connect.
@@ -123,13 +123,14 @@ func TestHealthCheckInRProxysContainer(t *testing.T) {
// itself.
wantHealthCheck(t, settings(rproxyListenAddr, ":0"), 1, "unhealthy: ask rproxy: ")
wantHealthCheck(t, settings(rproxyListenAddr, "8080"), 1,
"unhealthy: invalid setting: SWRP_LISTEN_ADDR: ")
"unhealthy: invalid setting: SWW_RPROXY_LISTEN_ADDR: ")
// No setting of rproxy's has a form given as a file.
// A setting of rproxy's can be given as a file too, here the listen
// address, which the health check reads.
wantHealthCheck(t, map[string]string{
rproxyListenAddr + "_FILE": writeFile(t, ":"+port+"\n"),
rproxyUpstreamURL: app.URL,
}, 1, "unhealthy: ask rproxy: ")
}, 0, "")
}
func TestHealthCheckFailsWithAnyOtherCommand(t *testing.T) {
@@ -145,11 +146,7 @@ func TestHealthCheckRefusesAnArgument(t *testing.T) {
var stderr bytes.Buffer
noSettings := func(string) (string, bool) {
return "", false
}
got := sww.HealthCheck(t.Context(), []string{"now"}, noSettings, &stderr)
got := sww.HealthCheck(t.Context(), []string{"now"}, viperFrom(nil), &stderr)
want := "sww healthcheck: unexpected argument \"now\"\n"
if got != 1 || stderr.String() != want {
@@ -166,11 +163,7 @@ func wantHealthCheck(t *testing.T, env map[string]string, status int, message st
var stderr bytes.Buffer
got := sww.HealthCheck(t.Context(), nil, func(name string) (string, bool) {
value, ok := env[name]
return value, ok
}, &stderr)
got := sww.HealthCheck(t.Context(), nil, viperFrom(env), &stderr)
wrote := stderr.String()
if got != status || !strings.HasPrefix(wrote, message) ||
+3 -3
View File
@@ -15,7 +15,7 @@ import (
// RunRProxy is `sww rproxy`: it reads rproxy's settings, then serves
// requests until ctx is done. Then it gives the requests in progress
// shutdownTimeout to finish, and the reports being sent SWRP_TIMEOUT_MS. It
// shutdownTimeout to finish, and the reports being sent SWW_RPROXY_TIMEOUT_MS. It
// returns the process's exit status, 1 when rproxy cannot start. Its process
// messages name the host's name as instance, and it writes those at info or
// more severe: rproxy has no setting for either.
@@ -23,7 +23,7 @@ func RunRProxy(ctx context.Context, params Params) int {
hostname, _ := os.Hostname() // "" when the host has no name to give
processLog := requestlog.NewProcessLogger(params.Stdout, hostname, slog.LevelInfo)
cfg, err := config.RProxyFromEnvironment(params.LookupEnv)
cfg, err := config.RProxyFromEnvironment(params.Viper)
if err != nil {
processLog.Error("invalid setting", "error", err.Error())
@@ -38,7 +38,7 @@ func RunRProxy(ctx context.Context, params Params) int {
listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", cfg.ListenAddr)
if err != nil {
processLog.Error("cannot listen on SWRP_LISTEN_ADDR", "error", err.Error())
processLog.Error("cannot listen on SWW_RPROXY_LISTEN_ADDR", "error", err.Error())
return 1
}
+18 -22
View File
@@ -25,13 +25,13 @@ import (
const (
// wafBackend is the setting that names discriminator to rproxy.
wafBackend = "SWRP_WAF_BACKEND"
wafBackend = "SWW_RPROXY_WAF_BACKEND"
// rproxyListenAddr is the setting that says where rproxy listens.
rproxyListenAddr = "SWRP_LISTEN_ADDR"
rproxyListenAddr = "SWW_RPROXY_LISTEN_ADDR"
// rproxyUpstreamURL is the setting that says where the app is.
rproxyUpstreamURL = "SWRP_UPSTREAM_URL"
rproxyUpstreamURL = "SWW_RPROXY_UPSTREAM_URL"
// rproxyTimeout is the setting that says how long rproxy waits for discriminator.
rproxyTimeout = "SWRP_TIMEOUT_MS"
rproxyTimeout = "SWW_RPROXY_TIMEOUT_MS"
// bannedProbe is a path the default rule file bans a client for.
bannedProbe = "/.env"
// otherClient is a client the tests do not ban.
@@ -43,12 +43,8 @@ const (
func runRProxy(ctx context.Context, env map[string]string, out *output) int {
return sww.RunRProxy(ctx, sww.Params{
Version: testVersion,
LookupEnv: func(name string) (string, bool) {
value, ok := env[name]
return value, ok
},
Stdout: out,
Viper: viperFrom(env),
Stdout: out,
})
}
@@ -64,10 +60,10 @@ func startRProxy(t *testing.T, backend, appURL string) (string, *output) {
go func() {
exited <- runRProxy(ctx, map[string]string{
rproxyListenAddr: localhost + ":0",
"SWRP_UPSTREAM_URL": appURL,
"SWRP_TRUSTED_PROXIES": localhost + "/32",
wafBackend: backend,
rproxyListenAddr: localhost + ":0",
"SWW_RPROXY_UPSTREAM_URL": appURL,
"SWW_RPROXY_TRUSTED_PROXIES": localhost + "/32",
wafBackend: backend,
}, out)
}()
@@ -437,7 +433,7 @@ func startRProxyProcess(t *testing.T, env ...string) (string, *exec.Cmd, *output
cmd.Env = append(os.Environ(),
commandForMain+"=rproxy",
"SWRP_LISTEN_ADDR="+localhost+":0")
"SWW_RPROXY_LISTEN_ADDR="+localhost+":0")
cmd.Env = append(cmd.Env, env...)
err = cmd.Start()
@@ -485,7 +481,7 @@ func TestRProxyAsksADiscriminatorThatSpeaksHTTP2OverHTTP1(t *testing.T) {
addr, _, _ := startRProxyProcess(t,
wafBackend+"="+backend.URL,
"SWRP_UPSTREAM_URL="+startApp(t),
"SWW_RPROXY_UPSTREAM_URL="+startApp(t),
"SSL_CERT_FILE="+certificates)
for name, headers := range map[string]string{
@@ -550,10 +546,10 @@ func TestRProxyStoppingWaitsForAReportBeingSent(t *testing.T) {
defer stop()
env := map[string]string{
rproxyListenAddr: localhost + ":0",
"SWRP_UPSTREAM_URL": startApp(t),
rproxyTimeout: "60000",
wafBackend: backend.URL,
rproxyListenAddr: localhost + ":0",
"SWW_RPROXY_UPSTREAM_URL": startApp(t),
rproxyTimeout: "60000",
wafBackend: backend.URL,
}
out := &output{}
exited := make(chan int, 1)
@@ -627,7 +623,7 @@ func TestRProxyStoppingCutsOffARequestAfterFiveSeconds(t *testing.T) {
const (
cutOffAfter = 5 * time.Second
upstreamSetting = "SWRP_UPSTREAM_URL"
upstreamSetting = "SWW_RPROXY_UPSTREAM_URL"
)
asked := make(chan struct{})
@@ -749,7 +745,7 @@ func TestRProxyAddressInUseStopsTheStart(t *testing.T) {
t.Errorf("exit status %d, want 1", status)
}
out.line(t, "msg", "cannot listen on SWRP_LISTEN_ADDR")
out.line(t, "msg", "cannot listen on SWW_RPROXY_LISTEN_ADDR")
}
func TestMainRunsRProxy(t *testing.T) {
+15 -12
View File
@@ -20,6 +20,8 @@ import (
"syscall"
"time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/alerts"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/discriminator"
@@ -38,7 +40,7 @@ import (
const shutdownTimeout = 5 * time.Second
// remoteLogStopTimeout is how long, as discriminator stops, the log lines still
// waiting are sent to SWW_LOG_REMOTE_URL before they are given up. stdout has
// waiting are sent to SWW_WAF_LOG_REMOTE_URL before they are given up. stdout has
// carried them.
const remoteLogStopTimeout = 2 * time.Second
@@ -59,8 +61,9 @@ commands:
type Params struct {
// Version is the version of the binary, set when it is built.
Version string
// LookupEnv reads an environment variable, normally os.LookupEnv.
LookupEnv func(string) (string, bool)
// Viper reads the settings, normally the viper config.NewViper returns
// for the process's environment.
Viper *viper.Viper
// Stdout receives the request log and the process's own messages.
Stdout io.Writer
}
@@ -79,7 +82,7 @@ func Main(version string) int {
syscall.SIGTERM, os.Interrupt)
defer stop()
params := Params{Version: version, LookupEnv: os.LookupEnv, Stdout: os.Stdout}
params := Params{Version: version, Viper: config.NewViper(), Stdout: os.Stdout}
switch args[0] {
case "rproxy":
@@ -87,7 +90,7 @@ func Main(version string) int {
case "discriminator":
return RunDiscriminator(ctx, params)
case "healthcheck":
return HealthCheck(ctx, args[1:], os.LookupEnv, os.Stderr)
return HealthCheck(ctx, args[1:], params.Viper, os.Stderr)
default:
return printUsage(os.Stderr)
}
@@ -105,18 +108,18 @@ func printUsage(w io.Writer) int {
// done. It returns the process's exit status, 1 when discriminator cannot start.
func RunDiscriminator(ctx context.Context, params Params) int {
// Until the settings are read, the one message is an invalid setting's
// error, which every SWW_LOG_LEVEL lets through.
// error, which every SWW_WAF_LOG_LEVEL lets through.
processLog := requestlog.NewProcessLogger(params.Stdout,
config.InstanceName(params.LookupEnv), slog.LevelError)
config.InstanceName(params.Viper), slog.LevelError)
cfg, err := config.FromEnvironment(params.LookupEnv)
cfg, err := config.FromEnvironment(params.Viper)
if err != nil {
processLog.Error("invalid setting", "error", err.Error())
return 1
}
// While SWW_LOG_REMOTE_URL is set, every line on stdout from here on
// While SWW_WAF_LOG_REMOTE_URL is set, every line on stdout from here on
// is sent there too.
stdout := params.Stdout
@@ -178,7 +181,7 @@ func RunDiscriminator(ctx context.Context, params Params) int {
listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", cfg.ListenAddr)
if err != nil {
processLog.Error("cannot listen on SWW_LISTEN_ADDR", "error", err.Error())
processLog.Error("cannot listen on SWW_WAF_LISTEN_ADDR", "error", err.Error())
return 1
}
@@ -192,7 +195,7 @@ func RunDiscriminator(ctx context.Context, params Params) int {
}
// newServer returns the server discriminator runs, with the metrics of the alerts,
// after reading the lookup database while SWW_LOOKUP_SOURCE is file. A lookup
// after reading the lookup database while SWW_WAF_LOOKUP_SOURCE is file. A lookup
// database that cannot be read is an error.
func newServer(
cfg *config.Config, stdout io.Writer, processLog *slog.Logger,
@@ -281,7 +284,7 @@ func newAlertQueue(
}
// newRemoteLogSender returns a sender of the log lines to
// SWW_LOG_REMOTE_URL, with the settings for it.
// SWW_WAF_LOG_REMOTE_URL, with the settings for it.
func newRemoteLogSender(cfg *config.Config) *remotelog.Sender {
return remotelog.New(remotelog.Params{
URL: cfg.LogRemoteURL,
+124 -112
View File
@@ -19,6 +19,8 @@ import (
"testing"
"time"
"github.com/spf13/viper"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/discriminator"
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
@@ -34,21 +36,21 @@ const (
testVersion = "test"
// localhost is where the tests listen.
localhost = "127.0.0.1"
listenAddr = "SWW_LISTEN_ADDR"
stateDir = "SWW_STATE_DIR"
stateWriteDelay = "SWW_STATE_WRITE_DELAY"
stateCounterInterval = "SWW_STATE_COUNTER_INTERVAL"
rateLimitPerDay = "SWW_RATE_LIMIT_PER_DAY"
rateLimitExemptNets = "SWW_RATE_LIMIT_EXEMPT_NETS"
rulesDir = "SWW_RULES_DIR"
lookupSource = "SWW_LOOKUP_SOURCE"
lookupDBPath = "SWW_LOOKUP_DB_PATH"
instanceName = "SWW_INSTANCE_NAME"
adminToken = "SWW_ADMIN_TOKEN" //nolint:gosec // the setting's name
metricsToken = "SWW_METRICS_TOKEN" //nolint:gosec // the setting's name
// adminSecret is the SWW_ADMIN_TOKEN the tests set.
listenAddr = "SWW_WAF_LISTEN_ADDR"
stateDir = "SWW_WAF_STATE_DIR"
stateWriteDelay = "SWW_WAF_STATE_WRITE_DELAY"
stateCounterInterval = "SWW_WAF_STATE_COUNTER_INTERVAL"
rateLimitPerDay = "SWW_WAF_RATE_LIMIT_PER_DAY"
rateLimitExemptNets = "SWW_WAF_RATE_LIMIT_EXEMPT_NETS"
rulesDir = "SWW_WAF_RULES_DIR"
lookupSource = "SWW_WAF_LOOKUP_SOURCE"
lookupDBPath = "SWW_WAF_LOOKUP_DB_PATH"
instanceName = "SWW_WAF_INSTANCE_NAME"
adminToken = "SWW_WAF_ADMIN_TOKEN" //nolint:gosec // the setting's name
metricsToken = "SWW_WAF_METRICS_TOKEN" //nolint:gosec // the setting's name
// adminSecret is the SWW_WAF_ADMIN_TOKEN the tests set.
adminSecret = "fedcba9876543210fedcba9876543210"
// instance is the SWW_INSTANCE_NAME the tests set where they look at
// instance is the SWW_WAF_INSTANCE_NAME the tests set where they look at
// it.
instance = "fsn1app1/blog"
// greeting is what the tests' app answers.
@@ -123,21 +125,30 @@ func (o *output) text() string {
return o.buf.String()
}
// viperFrom returns a viper Set with each of settings, as the viper
// config.NewViper returns one bound to a process's environment.
func viperFrom(settings map[string]string) *viper.Viper {
v := viper.New()
for name, value := range settings {
v.Set(name, value)
}
return v
}
// run runs `sww discriminator` with the settings in env until ctx is done,
// and returns its exit status. SWW_LOOKUP_SOURCE is off unless env sets it,
// and returns its exit status. SWW_WAF_LOOKUP_SOURCE is off unless env sets it,
// so that no test sends GeoJS its clients' addresses.
func run(ctx context.Context, env map[string]string, out *output) int {
v := viperFrom(env)
if _, set := env["SWW_WAF_LOOKUP_SOURCE"]; !set {
v.Set("SWW_WAF_LOOKUP_SOURCE", "off")
}
return sww.RunDiscriminator(ctx, sww.Params{
Version: testVersion,
LookupEnv: func(name string) (string, bool) {
value, ok := env[name]
if !ok && name == "SWW_LOOKUP_SOURCE" {
return "off", true
}
return value, ok
},
Stdout: out,
Viper: v,
Stdout: out,
})
}
@@ -147,8 +158,8 @@ func TestInvalidSettingStopsTheStart(t *testing.T) {
out := &output{}
status := run(t.Context(), map[string]string{
"SWW_BYTES_LIMIT_PER_MINUTE": "lots",
instanceName: instance,
"SWW_WAF_BYTES_LIMIT_PER_MINUTE": "lots",
instanceName: instance,
}, out)
if status != 1 {
t.Errorf("exit status %d, want 1", status)
@@ -159,7 +170,7 @@ func TestInvalidSettingStopsTheStart(t *testing.T) {
if line["type"] != "process" || line["instance"] != instance ||
line["level"] != "ERROR" ||
!strings.HasPrefix(message, "SWW_BYTES_LIMIT_PER_MINUTE: ") {
!strings.HasPrefix(message, "SWW_WAF_BYTES_LIMIT_PER_MINUTE: ") {
t.Errorf("start refused with %v", line)
}
}
@@ -215,7 +226,7 @@ func TestAddressInUseStopsTheStart(t *testing.T) {
t.Errorf("exit status %d, want 1", status)
}
out.line(t, "msg", "cannot listen on SWW_LISTEN_ADDR")
out.line(t, "msg", "cannot listen on SWW_WAF_LISTEN_ADDR")
}
func TestServesUntilToldToStop(t *testing.T) {
@@ -278,11 +289,11 @@ func TestLogLevelHoldsBackTheLessSevereProcessLines(t *testing.T) {
go func() {
exited <- run(ctx, map[string]string{
listenAddr: localhost + ":0",
stateDir: t.TempDir(),
rulesDir: t.TempDir(),
"SWW_BLOCKLIST_URLS": lists.URL + "/tor.txt",
"SWW_LOG_LEVEL": "warn",
listenAddr: localhost + ":0",
stateDir: t.TempDir(),
rulesDir: t.TempDir(),
"SWW_WAF_BLOCKLIST_URLS": lists.URL + "/tor.txt",
"SWW_WAF_LOG_LEVEL": "warn",
}, out)
}()
@@ -385,7 +396,7 @@ func TestStateKeptAcrossRestarts(t *testing.T) {
func TestBanRefusesItsNetblockAfterARestartWithAnotherScope(t *testing.T) {
t.Parallel()
const scope = "SWW_BAN_SCOPE_V4_PREFIX"
const scope = "SWW_WAF_BAN_SCOPE_V4_PREFIX"
env := map[string]string{
listenAddr: localhost + ":0",
@@ -505,7 +516,7 @@ func TestRuleFileAddedWhileRunningTakesEffect(t *testing.T) {
rulesDir: dir,
// The requests sent until the rule takes effect must not break a
// rate limit, whose ban would refuse them too.
"SWW_RATE_LIMIT_PER_MINUTE": "off",
"SWW_WAF_RATE_LIMIT_PER_MINUTE": "off",
}
out := runUntilStopped(t, env, func(url string) {
@@ -548,7 +559,7 @@ func TestRulesDirThatDoesNotExistStopsTheStart(t *testing.T) {
dir := filepath.Join(t.TempDir(), "rules.d")
wantRulesRefused(t, dir, "SWW_RULES_DIR cannot be read: open "+dir+
wantRulesRefused(t, dir, "SWW_WAF_RULES_DIR cannot be read: open "+dir+
": no such file or directory")
}
@@ -560,14 +571,14 @@ func TestCoreRuleSetExclusionsFileThatCannotBeReadStopsTheStart(t *testing.T) {
status := run(t.Context(), map[string]string{
listenAddr: localhost + ":0", stateDir: t.TempDir(), rulesDir: t.TempDir(),
"SWW_CORE_RULE_SET_EXCLUSIONS_FILE": path,
"SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE": path,
}, out)
if status != 1 {
t.Fatalf("exit status %d, want 1", status)
}
line := out.line(t, "msg", "cannot use the Core Rule Set")
want := "SWW_CORE_RULE_SET_EXCLUSIONS_FILE: read the exclusions file: open " +
want := "SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE: read the exclusions file: open " +
path + ": no such file or directory"
if line["error"] != want {
@@ -592,7 +603,8 @@ func TestCoreRuleSetExclusionsFileThatCannotBeUsedStopsTheStartWhileTheCoreRuleS
status := run(t.Context(), map[string]string{
listenAddr: localhost + ":0", stateDir: t.TempDir(), rulesDir: t.TempDir(),
"SWW_WAF_MODE": config.WAFModeOff, "SWW_CORE_RULE_SET_EXCLUSIONS_FILE": path,
"SWW_WAF_CORE_RULE_SET_MODE": config.WAFModeOff,
"SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE": path,
}, out)
if status != 1 {
t.Fatalf("%s: exit status %d, want 1", path, status)
@@ -611,14 +623,14 @@ func TestLookupDatabaseReplacedWhileRunningTakesEffect(t *testing.T) {
writeLookupDatabase(t, path, "DE")
env := map[string]string{
listenAddr: localhost + ":0",
stateDir: t.TempDir(),
rulesDir: t.TempDir(),
metricsToken: token,
instanceName: instance,
lookupSource: "file",
lookupDBPath: path,
"SWW_DENIED_COUNTRIES": "kp",
listenAddr: localhost + ":0",
stateDir: t.TempDir(),
rulesDir: t.TempDir(),
metricsToken: token,
instanceName: instance,
lookupSource: "file",
lookupDBPath: path,
"SWW_WAF_DENIED_COUNTRIES": "kp",
// The requests sent until a replacement takes effect, and those
// for the metrics, must not break a rate limit, whose ban would
// refuse them too.
@@ -708,12 +720,12 @@ func TestBlocklistTriesAndCopiesKeptInReputationJSONAcrossRestarts(t *testing.T)
torURL, dropURL := server.URL+torPath, server.URL+dropPath
dir := t.TempDir()
env := map[string]string{
listenAddr: localhost + ":0",
stateDir: dir,
rulesDir: t.TempDir(),
metricsToken: token,
instanceName: instance,
"SWW_BLOCKLIST_URLS": torURL,
listenAddr: localhost + ":0",
stateDir: dir,
rulesDir: t.TempDir(),
metricsToken: token,
instanceName: instance,
"SWW_WAF_BLOCKLIST_URLS": torURL,
// The requests sent until the list takes effect, and those for the
// metrics, must not break a rate limit, whose ban would refuse them
// too.
@@ -729,12 +741,12 @@ func TestBlocklistTriesAndCopiesKeptInReputationJSONAcrossRestarts(t *testing.T)
})
// Restarted with drop.txt named after it and the server answering, tor.txt
// waits SWW_BLOCKLIST_REFRESH after its failed try, kept in reputation.json,
// waits SWW_WAF_BLOCKLIST_REFRESH after its failed try, kept in reputation.json,
// while drop.txt, never tried, is fetched at once. Lists are fetched in the
// order named, so once drop.txt refuses the client, tor.txt has had its turn.
failing.Store(false)
env["SWW_BLOCKLIST_URLS"] = torURL + "," + dropURL
env["SWW_WAF_BLOCKLIST_URLS"] = torURL + "," + dropURL
out := runUntilStopped(t, env, func(url string) {
for statusFrom(t, url, placed) != http.StatusForbidden {
time.Sleep(pollInterval)
@@ -790,11 +802,11 @@ func TestCrowdSecDecisionListKeptInReputationJSONAcrossARestart(t *testing.T) {
dir := t.TempDir()
env := map[string]string{
listenAddr: localhost + ":0",
stateDir: dir,
rulesDir: t.TempDir(),
"SWW_CROWDSEC_LAPI_URL": engine.URL,
"SWW_CROWDSEC_LAPI_KEY": bouncerKey,
listenAddr: localhost + ":0",
stateDir: dir,
rulesDir: t.TempDir(),
"SWW_WAF_CROWDSEC_LAPI_URL": engine.URL,
"SWW_WAF_CROWDSEC_LAPI_KEY": bouncerKey,
}
// Once the list is fetched, the client's request bans it.
@@ -867,7 +879,7 @@ func TestLookupDatabaseThatCannotBeReadStopsTheStart(t *testing.T) {
t.Fatalf("exit status %d, want 1", status)
}
want := "SWW_LOOKUP_DB_PATH cannot be read: open " + path +
want := "SWW_WAF_LOOKUP_DB_PATH cannot be read: open " + path +
": no such file or directory"
line := out.line(t, "msg", "cannot use the lookup database")
@@ -889,10 +901,10 @@ func TestEveryLineIsAlsoSentToTheRemoteLogEndpoint(t *testing.T) {
}()
env := map[string]string{
listenAddr: localhost + ":0",
stateDir: t.TempDir(),
rulesDir: t.TempDir(),
"SWW_LOG_REMOTE_URL": "syslog+tcp://" + endpoint.Addr().String(),
listenAddr: localhost + ":0",
stateDir: t.TempDir(),
rulesDir: t.TempDir(),
"SWW_WAF_LOG_REMOTE_URL": "syslog+tcp://" + endpoint.Addr().String(),
}
out := runUntilStopped(t, env, func(url string) {
@@ -944,13 +956,13 @@ func TestStalledRemoteLogEndpointHoldsUpNoRequest(t *testing.T) {
}()
env := map[string]string{
listenAddr: localhost + ":0",
stateDir: t.TempDir(),
rulesDir: t.TempDir(),
"SWW_LOG_REMOTE_URL": "syslog+tls://" + endpoint.Addr().String(),
"SWW_LOG_REMOTE_BUFFER": "1",
metricsToken: token,
instanceName: instance,
listenAddr: localhost + ":0",
stateDir: t.TempDir(),
rulesDir: t.TempDir(),
"SWW_WAF_LOG_REMOTE_URL": "syslog+tls://" + endpoint.Addr().String(),
"SWW_WAF_LOG_REMOTE_BUFFER": "1",
metricsToken: token,
instanceName: instance,
}
out := runUntilStopped(t, env, func(url string) {
@@ -1003,12 +1015,12 @@ func TestBanIsAlertedAndAnAlertNotSentIsKeptAcrossARestart(t *testing.T) {
dir := t.TempDir()
env := map[string]string{
listenAddr: localhost + ":0",
stateDir: dir,
rulesDir: rules,
"SWW_ALERT_WEBHOOK_URL": webhook.url,
"SWW_ALERT_WEBHOOK_HEADERS": "Authorization:Bearer " + adminSecret,
instanceName: instance,
listenAddr: localhost + ":0",
stateDir: dir,
rulesDir: rules,
"SWW_WAF_ALERT_WEBHOOK_URL": webhook.url,
"SWW_WAF_ALERT_WEBHOOK_HEADERS": "Authorization:Bearer " + adminSecret,
instanceName: instance,
}
runUntilStopped(t, env, func(url string) {
@@ -1056,7 +1068,7 @@ func TestBanIsAlertedAndAnAlertNotSentIsKeptAcrossARestart(t *testing.T) {
webhook.failing.Store(false)
env["SWW_ALLOW_NETS"] = localhost
env["SWW_WAF_ALLOW_NETS"] = localhost
env[metricsToken] = token
runUntilStopped(t, env, func(url string) {
@@ -1092,12 +1104,12 @@ func TestBanIsAlertedToSlackAndNtfy(t *testing.T) {
rulesDir: t.TempDir(),
rateLimitPerDay: "1",
// The metrics are read from 127.0.0.1, which no limit counts.
"SWW_ALLOW_NETS": localhost + "/32",
metricsToken: token,
instanceName: instance,
"SWW_ALERT_SLACK_WEBHOOK_URL": slack.url,
"SWW_ALERT_NTFY_URL": ntfy.url,
"SWW_ALERT_NTFY_TOKEN": ntfyToken,
"SWW_WAF_ALLOW_NETS": localhost + "/32",
metricsToken: token,
instanceName: instance,
"SWW_WAF_ALERT_SLACK_WEBHOOK_URL": slack.url,
"SWW_WAF_ALERT_NTFY_URL": ntfy.url,
"SWW_WAF_ALERT_NTFY_TOKEN": ntfyToken,
}
runUntilStopped(t, env, func(url string) {
@@ -1162,7 +1174,7 @@ func TestUnwritableStateDirStopsTheStart(t *testing.T) {
t.Parallel()
wantStartRefused(t, filepath.Join(t.TempDir(), "missing"),
"SWW_STATE_DIR cannot be written: ")
"SWW_WAF_STATE_DIR cannot be written: ")
}
// wantStartRefused runs sww with its state files in dir, and
@@ -1268,28 +1280,28 @@ func wantStartingLine(t *testing.T, line map[string]any, dir string) {
settings, _ := line["settings"].(map[string]any)
want := map[string]any{
listenAddr: localhost + ":0",
stateDir: dir,
"SWW_MODE": "enforce",
stateWriteDelay: "10s",
stateCounterInterval: "15m",
"SWW_ALLOW_NETS": "",
rateLimitExemptNets: "",
"SWW_DENY_NETS": "",
"SWW_RATE_LIMIT_PER_MINUTE": "120",
"SWW_RATE_LIMIT_PER_HOUR": "1500",
rateLimitPerDay: "6000",
"SWW_RATE_LIMIT_EXEMPT_PATHS": "",
"SWW_DENIED_COUNTRIES": "",
"SWW_EXCLUSIVELY_ALLOWED_COUNTRIES": "",
"SWW_BAN_RESPONSE": "403",
"SWW_LIMIT_BAN_DURATION": "1h",
"SWW_LIMIT_BAN_REPEAT_WINDOW": "24h",
"SWW_MAX_BAN_DURATION": "7d",
"SWW_ATTACK_BAN_DURATION": "7d",
"SWW_MAX_BANS": "5000",
"SWW_BAN_SCOPE_V4_PREFIX": "32",
"SWW_RULES_ENABLED": "true",
listenAddr: localhost + ":0",
stateDir: dir,
"SWW_WAF_MODE": "enforce",
stateWriteDelay: "10s",
stateCounterInterval: "15m",
"SWW_WAF_ALLOW_NETS": "",
rateLimitExemptNets: "",
"SWW_WAF_DENY_NETS": "",
"SWW_WAF_RATE_LIMIT_PER_MINUTE": "120",
"SWW_WAF_RATE_LIMIT_PER_HOUR": "1500",
rateLimitPerDay: "6000",
"SWW_WAF_RATE_LIMIT_EXEMPT_PATHS": "",
"SWW_WAF_DENIED_COUNTRIES": "",
"SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES": "",
"SWW_WAF_BAN_RESPONSE": "403",
"SWW_WAF_LIMIT_BAN_DURATION": "1h",
"SWW_WAF_LIMIT_BAN_REPEAT_WINDOW": "24h",
"SWW_WAF_MAX_BAN_DURATION": "7d",
"SWW_WAF_ATTACK_BAN_DURATION": "7d",
"SWW_WAF_MAX_BANS": "5000",
"SWW_WAF_BAN_SCOPE_V4_PREFIX": "32",
"SWW_WAF_RULES_ENABLED": "true",
}
for name, value := range want {
@@ -1489,7 +1501,7 @@ func onlyBan(t *testing.T, dir string) map[string]any {
}
// wantRefused checks that discriminator at url refuses a request from localhost with
// 403, the default SWW_BAN_RESPONSE.
// 403, the default SWW_WAF_BAN_RESPONSE.
func wantRefused(t *testing.T, url string) {
t.Helper()
@@ -1530,8 +1542,8 @@ func saveUntilAnswered(t *testing.T, path, content, url, from string, status int
}
}
// destination is a stand-in for SWW_ALERT_SLACK_WEBHOOK_URL or
// SWW_ALERT_NTFY_URL. It notes each request it is sent, and answers
// destination is a stand-in for SWW_WAF_ALERT_SLACK_WEBHOOK_URL or
// SWW_WAF_ALERT_NTFY_URL. It notes each request it is sent, and answers
// 200.
type destination struct {
url string
@@ -1590,7 +1602,7 @@ func (d *destination) firstPost(t *testing.T) destinationPost {
}
}
// webhook is a stand-in for SWW_ALERT_WEBHOOK_URL. It notes each alert
// webhook is a stand-in for SWW_WAF_ALERT_WEBHOOK_URL. It notes each alert
// it is sent, and answers 204, or 503 while failing.
type webhook struct {
url string