Files
smallwebwaf/internal/config/rproxy_test.go
clawbot 4dad4fff43
check / check (push) Waiting to run
Rename the settings whose names do not say what they control (closes #234)
Applies the setting renames of the naming audit under the SWW_WAF_ and
SWW_RPROXY_ prefixes, with sneak's names for the IP reputation cache
time and the two time limits, which now take milliseconds. The rproxy
log field default_disposition becomes waf_backend_no_answer, and
config.Config and config.FromEnvironment become config.WAF and
config.WAFFromEnvironment. Go fields, test names and prose follow. No
aliases; nothing reads the old names.

Model: opus-5-5
2026-11-12 11:13:27 +01:00

259 lines
6.9 KiB
Go

package config_test
import (
"net/netip"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"sneak.berlin/go/smallwebwaf/internal/config"
"sneak.berlin/go/smallwebwaf/internal/config/configtest"
)
// rproxy's settings, by name, and their values in these tests.
const (
wafBackend = "SWW_RPROXY_WAF_BACKEND"
wafBackendTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
rproxyNoAnswer = "SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION"
rproxyListen = "SWW_RPROXY_LISTEN_ADDR"
rproxyUpstream = "SWW_RPROXY_UPSTREAM_URL"
rproxyTrusted = "SWW_RPROXY_TRUSTED_PROXIES"
rproxyMaxRequest = "SWW_RPROXY_MAX_REQUEST_SIZE"
rproxyMaxResponse = "SWW_RPROXY_MAX_RESPONSE_SIZE"
rproxyRequestTime = "SWW_RPROXY_MAX_REQUEST_TIME_MS"
rproxyAnswerTime = "SWW_RPROXY_MAX_RESPONSE_TIME_MS"
backendURL = "http://waf:8080"
rproxyNoAnswerAddr = ":8080"
rproxyNoAnswerApp = "http://127.0.0.1:8081"
notASize = "a lot"
)
// rproxyConfig reads rproxy's settings from env.
func rproxyConfig(env map[string]string) (*config.RProxy, error) {
return config.RProxyFromEnvironment(configtest.Viper(env))
}
func TestRProxyDefaults(t *testing.T) {
t.Parallel()
cfg, err := rproxyConfig(map[string]string{wafBackend: backendURL})
if err != nil {
t.Fatalf("settings: %v", err)
}
if cfg.ListenAddr != rproxyNoAnswerAddr ||
cfg.UpstreamURL.String() != rproxyNoAnswerApp ||
cfg.WAFBackend.String() != backendURL {
t.Errorf(
"addresses %q, %v, %v",
cfg.ListenAddr,
cfg.UpstreamURL,
cfg.WAFBackend,
)
}
if cfg.WAFBackendTimeout != 2*time.Second ||
cfg.WAFBackendNoAnswerDisposition != "allow" {
t.Errorf(
"waf backend timeout %v, no-answer disposition %q",
cfg.WAFBackendTimeout,
cfg.WAFBackendNoAnswerDisposition,
)
}
if cfg.MaxRequestSize != 100<<20 || cfg.MaxResponseSize != 1<<30 {
t.Errorf("sizes %d and %d", cfg.MaxRequestSize, cfg.MaxResponseSize)
}
if cfg.MaxRequestTime != time.Minute ||
cfg.MaxResponseTime != 30*time.Minute {
t.Errorf("times %v and %v", cfg.MaxRequestTime, cfg.MaxResponseTime)
}
want := []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/8"),
netip.MustParsePrefix("172.16.0.0/12"),
netip.MustParsePrefix("192.168.0.0/16"),
}
if !slices.Equal(cfg.TrustedProxies, want) {
t.Errorf("trusted proxies %v, want %v", cfg.TrustedProxies, want)
}
}
func TestRProxySettingsAreRead(t *testing.T) {
t.Parallel()
cfg, err := rproxyConfig(map[string]string{
wafBackend: "https://waf.example:8443",
rproxyListen: ":9000",
rproxyUpstream: "http://app:3000",
rproxyTrusted: "192.0.2.0/24",
wafBackendTimeout: "150",
rproxyNoAnswer: "deny",
rproxyMaxRequest: "1K",
rproxyMaxResponse: off,
rproxyRequestTime: off,
rproxyAnswerTime: "250",
})
if err != nil {
t.Fatalf("settings: %v", err)
}
if cfg.ListenAddr != ":9000" || cfg.UpstreamURL.Host != "app:3000" ||
cfg.WAFBackend.Host != "waf.example:8443" ||
cfg.TrustedProxies[0] != netip.MustParsePrefix("192.0.2.0/24") {
t.Errorf("addresses %+v", cfg)
}
if cfg.WAFBackendTimeout != 150*time.Millisecond ||
cfg.WAFBackendNoAnswerDisposition != "deny" ||
cfg.MaxRequestSize != 1024 ||
cfg.MaxResponseSize != 0 ||
cfg.MaxRequestTime != 0 ||
cfg.MaxResponseTime != 250*time.Millisecond {
t.Errorf("limits %+v", cfg)
}
}
func TestRProxyInvalidSettingsAreRefused(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name, value string
}{
{wafBackend, ""},
{wafBackend, "waf:8080"},
{wafBackend, "http://waf:8080/path"},
{wafBackendTimeout, "0"},
{wafBackendTimeout, "-1"},
{wafBackendTimeout, off},
{wafBackendTimeout, "2s"},
{wafBackendTimeout, "1.25"},
{wafBackendTimeout, "99999999999999999"},
{rproxyNoAnswer, "maybe"},
{rproxyListen, "8080"},
{rproxyUpstream, "app"},
{rproxyUpstream, "127.0.0.1:8081"},
{rproxyUpstream, "ftp://127.0.0.1:8081"},
{rproxyUpstream, "http://"},
{rproxyUpstream, "http://:8081"},
{rproxyUpstream, "http://127.0.0.1:0"},
{rproxyUpstream, "http://127.0.0.1:99999"},
{rproxyUpstream, "http://127.0.0.1:8081/app"},
{rproxyUpstream, "http://127.0.0.1:8081/?a=1"},
{rproxyUpstream, "http://user:secret@127.0.0.1:8081"},
{rproxyTrusted, "not a netblock"},
{rproxyTrusted, "10.0.0.0/33"},
{rproxyTrusted, "10.0.0.0/8,,192.168.0.0/16"},
{rproxyTrusted, "fe80::1%eth0"},
{rproxyMaxRequest, notASize},
{rproxyMaxResponse, notASize},
{rproxyRequestTime, "0"},
{rproxyRequestTime, "1m"},
{rproxyAnswerTime, "0"},
} {
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel()
_, err := rproxyConfig(
map[string]string{wafBackend: backendURL, tc.name: tc.value},
)
if err == nil || !strings.HasPrefix(err.Error(), tc.name+": ") {
t.Errorf("error %v, want one naming %s", err, tc.name)
}
})
}
}
func TestRProxyTrustedProxiesSetButEmptyTrustNothing(t *testing.T) {
t.Parallel()
cfg, err := rproxyConfig(
map[string]string{wafBackend: backendURL, rproxyTrusted: ""},
)
if err != nil {
t.Fatalf("settings: %v", err)
}
if len(cfg.TrustedProxies) != 0 {
t.Errorf("trusted proxies %v, want none", cfg.TrustedProxies)
}
}
func TestRProxyNeedsItsBackend(t *testing.T) {
t.Parallel()
_, err := rproxyConfig(nil)
if err == nil ||
!strings.HasPrefix(err.Error(), wafBackend+": is required") {
t.Errorf("error %v, want one saying %s is required", err, wafBackend)
}
}
func TestRProxySettingReadFromFile(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "backend")
err := os.WriteFile(path, []byte(backendURL+"\n"), 0o600)
if err != nil {
t.Fatalf("write: %v", err)
}
cfg, err := rproxyConfig(map[string]string{wafBackend + "_FILE": path})
if err != nil {
t.Fatalf("settings: %v", err)
}
if cfg.WAFBackend.String() != backendURL {
t.Errorf("backend %v, want %s", cfg.WAFBackend, backendURL)
}
}
func TestRProxyUnreadableSettingFileStopsTheStart(t *testing.T) {
t.Parallel()
dir := t.TempDir()
for _, path := range []string{filepath.Join(dir, "missing"), dir} {
_, err := rproxyConfig(map[string]string{
wafBackend: backendURL,
wafBackendTimeout + "_FILE": path,
})
want := wafBackendTimeout + "_FILE: cannot be read: "
if err == nil || !strings.HasPrefix(err.Error(), want) {
t.Errorf("%s: error %v, want one starting %s", path, err, want)
}
}
}
func TestRProxySettingsAreLoggedByName(t *testing.T) {
t.Parallel()
cfg, err := rproxyConfig(map[string]string{wafBackend: backendURL})
if err != nil {
t.Fatalf("settings: %v", err)
}
group := cfg.LogValue().Group()
names := make([]string, 0, len(group))
for _, attr := range group {
names = append(names, attr.Key)
}
for _, name := range []string{
rproxyListen, rproxyUpstream, rproxyTrusted, wafBackend, wafBackendTimeout,
rproxyNoAnswer,
rproxyMaxRequest, rproxyMaxResponse, rproxyRequestTime, rproxyAnswerTime,
} {
if !slices.Contains(names, name) {
t.Errorf("%s is not logged, only %v", name, names)
}
}
}