check / check (push) Waiting to run
Each check servewaf runs on a request is a type in its own file behind one interface, heuristic, and is registered in one of four ordered lists in heuristics.go. Order and behaviour are unchanged. Every refusal now logs heuristic, the name of the heuristic that made it, and heuristic_figures, the figures it acted on. A ban by refusal_limit or byte_limits after the answer logs report_heuristic and report_heuristic_figures instead, so a line can name both. All four are in docs/request-log.md, and SPEC.md names each heuristic at its step. Model: opus-5-5
182 lines
5.6 KiB
Go
182 lines
5.6 KiB
Go
package servewaf_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 SWWAF_ALLOW_NETS, and in SWWAF_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,
|
|
deniedCountries: "kp",
|
|
rateLimitPerMinute: "1",
|
|
})
|
|
|
|
// Neither SWWAF_DENY_NETS, the country lists 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.ActionRateLimited},
|
|
})
|
|
}
|
|
|
|
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",
|
|
deniedCountries: "kp",
|
|
})
|
|
|
|
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.ActionDenied)
|
|
|
|
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.ActionDenied)
|
|
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.ActionDenied},
|
|
{listedAddr, http.StatusForbidden, requestlog.ActionDenied},
|
|
{unlistedAddr, http.StatusOK, requestlog.ActionForward},
|
|
{unlistedAddr, http.StatusForbidden, requestlog.ActionRateLimited},
|
|
})
|
|
}
|
|
|
|
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,
|
|
lookupTimeout: "1h",
|
|
rateLimitExemptNets: listedAddr + "," + fromKP,
|
|
deniedCountries: "kp",
|
|
rateLimitPerMinute: "1",
|
|
})
|
|
|
|
// listedAddr's requests are neither refused nor counted, so the first
|
|
// request from unlistedAddr is within the limit of one a minute. The
|
|
// country lists still refuse 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.ActionRateLimited},
|
|
{fromKP, http.StatusForbidden, requestlog.ActionCountryDenied},
|
|
})
|
|
}
|
|
|
|
// 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 smallwebwaf at addr one after another and
|
|
// checks each one's answer and log line. They must be the first requests
|
|
// smallwebwaf 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)
|
|
}
|
|
}
|