Read every setting through viper and rename the settings (closes #211)
check / check (push) Waiting to run
check / check (push) Waiting to run
Every setting of waf 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: waf'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_. Model: deepseek-v4-pro (first version); opus-5-5 (rework)
This commit is contained in:
1 parent
b93bad5842
commit
c516e34145
122 files changed
+2288
-2170
No files matched your search
+13
-15
@@ -10,6 +10,8 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||
)
|
||||
@@ -21,17 +23,17 @@ var errHealthEndpoint = errors.New("the health endpoint answered")
|
||||
|
||||
// HealthCheck is the container's health check. In waf's container, where
|
||||
// SWW_COMMAND is waf, it returns 0 while waf 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 == "waf":
|
||||
err = checkWAF(ctx, lookupEnv)
|
||||
err = checkWAF(ctx, v)
|
||||
default:
|
||||
err = checkRProxyAndApp(ctx, lookupEnv)
|
||||
err = checkRProxyAndApp(ctx, v)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -65,10 +67,8 @@ func HealthCheck(
|
||||
}
|
||||
|
||||
// checkWAF asks waf's health endpoint.
|
||||
func checkWAF(
|
||||
ctx context.Context, lookupEnv func(string) (string, bool),
|
||||
) error {
|
||||
listenAddr, err := config.WAFListenAddr(lookupEnv)
|
||||
func checkWAF(ctx context.Context, v *viper.Viper) error {
|
||||
listenAddr, err := config.WAFListenAddr(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid setting: %w", err)
|
||||
}
|
||||
@@ -82,10 +82,8 @@ func checkWAF(
|
||||
}
|
||||
|
||||
// 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,6 +12,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
|
||||
"sneak.berlin/go/smallwebwaf/internal/sww"
|
||||
)
|
||||
|
||||
@@ -54,9 +55,9 @@ func TestHealthCheckInWAFsContainer(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: wafCommand,
|
||||
@@ -72,7 +73,7 @@ func TestHealthCheckInWAFsContainer(t *testing.T) {
|
||||
}, 1, "unhealthy: ask waf: ")
|
||||
wantHealthCheck(t, map[string]string{
|
||||
commandSetting: wafCommand, listenAddr: "8080",
|
||||
}, 1, "unhealthy: invalid setting: SWW_LISTEN_ADDR: ")
|
||||
}, 1, "unhealthy: invalid setting: SWW_WAF_LISTEN_ADDR: ")
|
||||
|
||||
stop()
|
||||
|
||||
@@ -111,8 +112,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 +124,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 +147,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"}, configtest.Viper(nil), &stderr)
|
||||
|
||||
want := "sww healthcheck: unexpected argument \"now\"\n"
|
||||
if got != 1 || stderr.String() != want {
|
||||
@@ -166,11 +164,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, configtest.Viper(env), &stderr)
|
||||
|
||||
wrote := stderr.String()
|
||||
if got != status || !strings.HasPrefix(wrote, message) ||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+19
-22
@@ -19,19 +19,20 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
|
||||
"sneak.berlin/go/smallwebwaf/internal/sww"
|
||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||
)
|
||||
|
||||
const (
|
||||
// wafBackend is the setting that names waf 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 waf.
|
||||
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 +44,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: configtest.Viper(env),
|
||||
Stdout: out,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,10 +61,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)
|
||||
}()
|
||||
|
||||
@@ -524,7 +521,7 @@ func startRProxyProcess(t *testing.T, env ...string) (string, func()) {
|
||||
|
||||
cmd.Env = append(os.Environ(),
|
||||
commandForMain+"=rproxy",
|
||||
"SWRP_LISTEN_ADDR="+localhost+":0")
|
||||
"SWW_RPROXY_LISTEN_ADDR="+localhost+":0")
|
||||
cmd.Env = append(cmd.Env, env...)
|
||||
|
||||
exited := startProcess(t, cmd)
|
||||
@@ -578,7 +575,7 @@ func TestRProxyAsksAWAFThatSpeaksHTTP2OverHTTP1(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{
|
||||
@@ -643,10 +640,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)
|
||||
@@ -720,7 +717,7 @@ func TestRProxyStoppingCutsOffARequestAfterFiveSeconds(t *testing.T) {
|
||||
|
||||
const (
|
||||
cutOffAfter = 5 * time.Second
|
||||
upstreamSetting = "SWRP_UPSTREAM_URL"
|
||||
upstreamSetting = "SWW_RPROXY_UPSTREAM_URL"
|
||||
)
|
||||
|
||||
asked := make(chan struct{})
|
||||
@@ -842,7 +839,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
@@ -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/coreruleset"
|
||||
@@ -38,7 +40,7 @@ import (
|
||||
const shutdownTimeout = 5 * time.Second
|
||||
|
||||
// remoteLogStopTimeout is how long, as waf 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 "waf":
|
||||
return RunWAF(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 {
|
||||
// ctx is done. It returns the process's exit status, 1 when waf cannot start.
|
||||
func RunWAF(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 RunWAF(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 RunWAF(ctx context.Context, params Params) int {
|
||||
}
|
||||
|
||||
// newServer returns the server waf 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,
|
||||
@@ -283,7 +286,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,
|
||||
|
||||
+123
-123
@@ -20,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
|
||||
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
|
||||
"sneak.berlin/go/smallwebwaf/internal/sww"
|
||||
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||
@@ -34,22 +35,22 @@ 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"
|
||||
cacheDir = "SWW_CACHE_DIR"
|
||||
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"
|
||||
cacheDir = "SWW_WAF_CACHE_DIR"
|
||||
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.
|
||||
@@ -150,26 +151,24 @@ func (o *output) text() string {
|
||||
}
|
||||
|
||||
// run runs `sww waf` 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 := configtest.Viper(env)
|
||||
if _, set := env["SWW_WAF_LOOKUP_SOURCE"]; !set {
|
||||
v.Set("SWW_WAF_LOOKUP_SOURCE", "off")
|
||||
}
|
||||
|
||||
// The cache files go beside the state files unless the test says
|
||||
// otherwise.
|
||||
if _, set := env[cacheDir]; !set && env[stateDir] != "" {
|
||||
v.Set(cacheDir, env[stateDir])
|
||||
}
|
||||
|
||||
return sww.RunWAF(ctx, sww.Params{
|
||||
Version: testVersion,
|
||||
LookupEnv: func(name string) (string, bool) {
|
||||
value, ok := env[name]
|
||||
if !ok && name == "SWW_LOOKUP_SOURCE" {
|
||||
return "off", true
|
||||
}
|
||||
|
||||
// The cache files go beside the state files unless the test
|
||||
// says otherwise.
|
||||
if !ok && name == cacheDir {
|
||||
return env[stateDir], env[stateDir] != ""
|
||||
}
|
||||
|
||||
return value, ok
|
||||
},
|
||||
Stdout: out,
|
||||
Viper: v,
|
||||
Stdout: out,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -179,8 +178,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)
|
||||
@@ -191,7 +190,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)
|
||||
}
|
||||
}
|
||||
@@ -247,7 +246,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) {
|
||||
@@ -311,11 +310,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)
|
||||
}()
|
||||
|
||||
@@ -418,7 +417,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",
|
||||
@@ -538,7 +537,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) {
|
||||
@@ -581,7 +580,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")
|
||||
}
|
||||
|
||||
@@ -593,14 +592,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 {
|
||||
@@ -625,7 +624,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)
|
||||
@@ -644,14 +644,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_TAG_DENY": "country=KP",
|
||||
listenAddr: localhost + ":0",
|
||||
stateDir: t.TempDir(),
|
||||
rulesDir: t.TempDir(),
|
||||
metricsToken: token,
|
||||
instanceName: instance,
|
||||
lookupSource: "file",
|
||||
lookupDBPath: path,
|
||||
"SWW_WAF_TAG_DENY": "country=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.
|
||||
@@ -741,13 +741,13 @@ func TestBlocklistTriesAndCopiesKeptInListsJSONAcrossRestarts(t *testing.T) {
|
||||
torURL, dropURL := server.URL+torPath, server.URL+dropPath
|
||||
cache := t.TempDir()
|
||||
env := map[string]string{
|
||||
listenAddr: localhost + ":0",
|
||||
stateDir: t.TempDir(),
|
||||
cacheDir: cache,
|
||||
rulesDir: t.TempDir(),
|
||||
metricsToken: token,
|
||||
instanceName: instance,
|
||||
"SWW_BLOCKLIST_URLS": torURL,
|
||||
listenAddr: localhost + ":0",
|
||||
stateDir: t.TempDir(),
|
||||
cacheDir: cache,
|
||||
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.
|
||||
@@ -763,12 +763,12 @@ func TestBlocklistTriesAndCopiesKeptInListsJSONAcrossRestarts(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 lists.json,
|
||||
// waits SWW_WAF_BLOCKLIST_REFRESH after its failed try, kept in lists.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)
|
||||
@@ -824,12 +824,12 @@ func TestCrowdSecDecisionListKeptInListsJSONAcrossARestart(t *testing.T) {
|
||||
|
||||
dir, cache := t.TempDir(), t.TempDir()
|
||||
env := map[string]string{
|
||||
listenAddr: localhost + ":0",
|
||||
stateDir: dir,
|
||||
cacheDir: cache,
|
||||
rulesDir: t.TempDir(),
|
||||
"SWW_CROWDSEC_LAPI_URL": engine.URL,
|
||||
"SWW_CROWDSEC_LAPI_KEY": bouncerKey,
|
||||
listenAddr: localhost + ":0",
|
||||
stateDir: dir,
|
||||
cacheDir: cache,
|
||||
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.
|
||||
@@ -906,7 +906,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")
|
||||
@@ -928,10 +928,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) {
|
||||
@@ -983,13 +983,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) {
|
||||
@@ -1053,12 +1053,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) {
|
||||
@@ -1106,7 +1106,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) {
|
||||
@@ -1146,12 +1146,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) {
|
||||
@@ -1216,7 +1216,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: ")
|
||||
}
|
||||
|
||||
func TestUnwritableCacheDirStopsTheStart(t *testing.T) {
|
||||
@@ -1237,8 +1237,8 @@ func TestUnwritableCacheDirStopsTheStart(t *testing.T) {
|
||||
|
||||
line := out.line(t, "msg", "cannot use the state and cache files")
|
||||
if message, _ := line["error"].(string); !strings.HasPrefix(message,
|
||||
"SWW_CACHE_DIR cannot be written: ") {
|
||||
t.Errorf("start refused with %q, want SWW_CACHE_DIR named", message)
|
||||
"SWW_WAF_CACHE_DIR cannot be written: ") {
|
||||
t.Errorf("start refused with %q, want SWW_WAF_CACHE_DIR named", message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1345,28 +1345,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_TAG_DENY": "",
|
||||
"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_TAG_DENY": "",
|
||||
"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 {
|
||||
@@ -1566,7 +1566,7 @@ func onlyBan(t *testing.T, dir string) map[string]any {
|
||||
}
|
||||
|
||||
// wantRefused checks that waf 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()
|
||||
|
||||
@@ -1607,8 +1607,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
|
||||
@@ -1667,7 +1667,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
|
||||
|
||||
Reference in new issue
Block a user