Files
smallwebwaf/internal/waf/staticlists_test.go
T
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

208 lines
5.8 KiB
Go

package waf_test
import (
"net/http"
"strings"
"sync/atomic"
"testing"
"sneak.berlin/go/smallwebwaf/internal/requestlog"
)
// The rate limits count an IPv6 client by its /64, so these two addresses
// are one client for them. The static lists match each address on its own,
// and the tests list listedAddr alone.
const (
listedAddr = "2001:db8::1"
unlistedAddr = "2001:db8::2"
)
func TestAllowNetsSkipEveryCheck(t *testing.T) {
t.Parallel()
var calls atomic.Int32
app := startApp(t, func(http.ResponseWriter, *http.Request) {
calls.Add(1)
})
geojsURL, asked := startGeoJS(t)
// fromKP is in SWW_WAF_ALLOW_NETS, and in SWW_WAF_DENY_NETS too, which
// comes after it.
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
trustedProxies: trustLocalhost,
allowNets: "198.51.100.0/24",
denyNets: fromKP,
tagDeny: denyKP,
rateLimitPerMinute: "1",
})
// Neither SWW_WAF_DENY_NETS, SWW_WAF_TAG_DENY nor the limit of one request a
// minute refuses the client, and its country is not looked up.
wantAnswers(t, addr, out, []sentRequest{
{fromKP, http.StatusOK, requestlog.ActionForward},
{fromKP, http.StatusOK, requestlog.ActionForward},
})
if len(asked()) != 0 {
t.Errorf("GeoJS was asked about %v, want nothing", asked())
}
if calls.Load() != 2 {
t.Errorf("the app was called %d times, want 2", calls.Load())
}
}
func TestRequestFromAllowNetsIsNotCounted(t *testing.T) {
t.Parallel()
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
addr, out := startProxy(t, app.URL, map[string]string{
trustedProxies: trustLocalhost,
allowNets: listedAddr,
rateLimitPerMinute: "1",
})
// listedAddr's requests are not counted, so the first request from
// unlistedAddr is within the limit of one a minute.
wantAnswers(t, addr, out, []sentRequest{
{listedAddr, http.StatusOK, requestlog.ActionForward},
{listedAddr, http.StatusOK, requestlog.ActionForward},
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
{
unlistedAddr,
http.StatusForbidden,
requestlog.ActionClientIsRateLimited,
},
})
}
func TestDenyNetsRefuseBeforeTheLookup(t *testing.T) {
t.Parallel()
var calls atomic.Int32
app := startApp(t, func(http.ResponseWriter, *http.Request) {
calls.Add(1)
})
geojsURL, asked := startGeoJS(t)
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
trustedProxies: trustLocalhost,
denyNets: "203.0.113.0/24",
tagDeny: denyKP,
})
req := newRequest(
t,
http.MethodPost,
addr,
"/",
strings.NewReader("a body"),
)
req.Header.Set(forwardedFor, fromDE)
wantStatus(t, do(t, req), http.StatusForbidden)
line := out.endedLine(t)
wantLine(t, line, http.StatusForbidden, requestlog.ActionClientDenied)
if len(asked()) != 0 {
t.Errorf("GeoJS was asked about %v, want nothing", asked())
}
if calls.Load() != 0 {
t.Errorf("the app was called %d times, want none", calls.Load())
}
}
func TestDenyNetsRefusalNamesItsHeuristicAndNetblock(t *testing.T) {
t.Parallel()
s, _, _ := startWithClock(t, "", map[string]string{denyNets: clientsNet})
line := s.get(
client,
http.StatusForbidden,
requestlog.ActionClientDenied,
)
wantHeuristic(t, line, "deny_nets", `{"netblock": "`+clientsNet+`"}`)
}
func TestRequestRefusedByDenyNetsIsNotCounted(t *testing.T) {
t.Parallel()
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
addr, out := startProxy(t, app.URL, map[string]string{
trustedProxies: trustLocalhost,
denyNets: listedAddr,
rateLimitPerMinute: "1",
})
// listedAddr's refused requests are not counted, so the first request
// from unlistedAddr is within the limit of one a minute.
wantAnswers(t, addr, out, []sentRequest{
{listedAddr, http.StatusForbidden, requestlog.ActionClientDenied},
{listedAddr, http.StatusForbidden, requestlog.ActionClientDenied},
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
{
unlistedAddr,
http.StatusForbidden,
requestlog.ActionClientIsRateLimited,
},
})
}
func TestRateLimitExemptNetsAreNeitherCountedNorRefused(t *testing.T) {
t.Parallel()
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
geojsURL, _ := startGeoJS(t)
addr, out := startProxyWithGeoJS(t, app.URL, geojsURL, map[string]string{
trustedProxies: trustLocalhost,
asnAndCountryLookupTimeout: "3600000",
rateLimitExemptNets: listedAddr + "," + fromKP,
tagDeny: denyKP,
rateLimitPerMinute: "1",
})
// listedAddr's requests are neither refused nor counted, so the first
// request from unlistedAddr is within the limit of one a minute.
// SWW_WAF_TAG_DENY still refuses an exempt client.
wantAnswers(t, addr, out, []sentRequest{
{listedAddr, http.StatusOK, requestlog.ActionForward},
{listedAddr, http.StatusOK, requestlog.ActionForward},
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
{
unlistedAddr,
http.StatusForbidden,
requestlog.ActionClientIsRateLimited,
},
{fromKP, http.StatusForbidden, requestlog.ActionClientDenied},
})
}
// sentRequest is a GET request from client, as X-Forwarded-For names it,
// and the status and log line action it should get.
type sentRequest struct {
client string
status int
action string
}
// wantAnswers sends requests to sww at addr one after another and
// checks each one's answer and log line. They must be the first requests
// sww is sent, since the log lines are matched to them in order.
func wantAnswers(
t *testing.T,
addr string,
out *output,
requests []sentRequest,
) {
t.Helper()
for i, sent := range requests {
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)
req.Header.Set(forwardedFor, sent.client)
wantStatus(t, do(t, req), sent.status)
wantLine(t, out.endedLines(t, i+1)[i], sent.status, sent.action)
}
}