rproxy and servewaf replace the one-process smallwebwaf (closes #172, closes #165)
check / check (push) Failing after 23m49s

`smallwebwaf` with no command now prints the commands and exits with status 2.
The image runs rproxy, or servewaf with SMALLWEBWAF_COMMAND=servewaf. The
passing-on code, its settings and its log fields and metric go. internal/proxy
keeps the checks, answers, reports and own endpoints; its tests run through the
real rproxy. The documents describe the pair.

Judgement call: clients.json keeps token_refused, since state files refuse unknown fields.
Deviation: script/gitea-requests sets SWWAF_LOG_REQUEST_HEADERS, to label requests.

Model: opus-5-5
This commit is contained in:
2026-11-05 10:33:24 +00:00
committed by sneak
parent 17c5dd2438
commit ac1a69e863
71 changed files with 2653 additions and 5493 deletions
+17 -46
View File
@@ -17,8 +17,8 @@ import (
const errorBurstThreshold = "SWWAF_ERROR_BURST_THRESHOLD"
// refused is a request the tests here send, which smallwebwaf refuses
// after a rule file match, or for a missing or wrong token.
// refused is a request the tests here send, which servewaf refuses after a rule
// file match.
type refused int
const (
@@ -27,11 +27,6 @@ const (
// banRule is one its ban rule refuses with 403, and bans the client
// for.
banRule
// noMetricsToken is one for the metrics without a token, and
// wrongAdminToken one for the bans with the metrics token, each
// refused with 401.
noMetricsToken
wrongAdminToken
)
// send sends r from the client at from, checks its answer and log line as
@@ -45,14 +40,6 @@ func (r refused) send(s *sender, from string) logLine {
requestlog.ActionRuleBlocked)
case banRule:
return s.request(from, probePath, http.StatusForbidden, requestlog.ActionBanned)
case noMetricsToken:
return s.request(from, proxy.MetricsPath, http.StatusUnauthorized,
requestlog.ActionAdmin)
case wrongAdminToken:
line, _ := s.requestWithHeader(from, proxy.BansPath, "Authorization: "+bearer,
http.StatusUnauthorized, requestlog.ActionAdmin)
return line
}
s.t.Fatalf("no request for the refusal %d", r)
@@ -60,7 +47,7 @@ func (r refused) send(s *sender, from string) logLine {
return logLine{}
}
// startForErrorBurst is startWithClock with testRules, both tokens and
// startForErrorBurst is startWithClock with testRules, the metrics token and
// SWWAF_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
func startForErrorBurst(
t *testing.T, threshold string, env map[string]string,
@@ -70,7 +57,6 @@ func startForErrorBurst(
settings := map[string]string{
errorBurstThreshold: threshold,
rulesDir: writeRules(t, testRules),
adminToken: adminSecret,
metricsToken: token,
}
maps.Copy(settings, env)
@@ -87,14 +73,7 @@ func TestErrorBurstBreaksAtOneOverTheThreshold(t *testing.T) {
refusals []refused
}{
{"block rule", []refused{blockRule, blockRule, blockRule, blockRule}},
{
"missing or wrong token",
[]refused{noMetricsToken, wrongAdminToken, noMetricsToken, wrongAdminToken},
},
{
"a mix ending in a ban rule",
[]refused{blockRule, noMetricsToken, blockRule, banRule},
},
{"a ban rule last", []refused{blockRule, blockRule, blockRule, banRule}},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
@@ -130,13 +109,11 @@ func TestErrorBurstBreaksAtOneOverTheThreshold(t *testing.T) {
func TestErrorBurstBanNotesHistoryAndMetrics(t *testing.T) {
t.Parallel()
const scraper = "192.0.2.200"
s, clk, server := startForErrorBurst(t, "2", nil)
start := clk.Now()
blockRule.send(s, client)
wrongAdminToken.send(s, client)
blockRule.send(s, client)
line := blockRule.send(s, client)
expires := start.Add(time.Hour)
@@ -173,19 +150,17 @@ func TestErrorBurstBanNotesHistoryAndMetrics(t *testing.T) {
t.Fatalf("bans\n%+v\nwant\n%+v", got, want)
}
wantOffences := ratelimit.Offences{Limit: 1, RuleBlocked: 2, TokenRefused: 1}
wantOffences := ratelimit.Offences{Limit: 1, RuleBlocked: 3}
if offences := historyOf(t, server, client).Offences; offences != wantOffences {
t.Errorf("history counts the offences %+v, want %+v", offences, wantOffences)
}
metrics := s.scrape(scraper)
metrics := s.scrape()
wantMetric(t, metrics, `smallwebwaf_rate_limit_hits_total{instance="app",`+
`kind="refusals",window="minute"}`, 1)
wantMetric(t, metrics, `smallwebwaf_offences_total{instance="app",kind="limit"}`, 1)
wantMetric(t, metrics,
`smallwebwaf_offences_total{instance="app",kind="rule_blocked"}`, 2)
wantMetric(t, metrics,
`smallwebwaf_offences_total{instance="app",kind="token_refused"}`, 1)
`smallwebwaf_offences_total{instance="app",kind="rule_blocked"}`, 3)
wantMetric(t, metrics, `smallwebwaf_bans_made_total{cause="limit",instance="app"}`, 1)
}
@@ -291,19 +266,15 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
allowNets: allowed, rateLimitExemptNets: exempt,
})
// A client in SWWAF_ALLOW_NETS still needs the token, but is not
// counted.
// A client in SWWAF_ALLOW_NETS is not refused, and so not counted.
for range 3 {
line := noMetricsToken.send(s, allowed)
if line.LimitHit != "" {
t.Errorf("log line has limit_hit %q, want none", line.LimitHit)
}
s.request(allowed, blockedPath, http.StatusOK, requestlog.ActionForward)
}
// One the rate limits do not apply to is.
noMetricsToken.send(s, exempt)
blockRule.send(s, exempt)
line := wrongAdminToken.send(s, exempt)
line := blockRule.send(s, exempt)
if line.LimitHit != requestlog.LimitHitErrorBurst {
t.Errorf("log line has limit_hit %q, want error_burst", line.LimitHit)
}
@@ -336,7 +307,6 @@ func TestObserveModeLogsAndAlertsTheErrorBurst(t *testing.T) {
mode: observe,
errorBurstThreshold: "1",
rulesDir: writeRules(t, testRules),
adminToken: adminSecret,
})
start := clk.Now()
held := bans.Ban{
@@ -347,10 +317,11 @@ func TestObserveModeLogsAndAlertsTheErrorBurst(t *testing.T) {
}
server.Ledger.Load([]bans.Ban{held})
// Under a ban, enforce mode would have refused these before the
// endpoint, so their tokens are not counted.
// Under a ban, enforce mode would have refused these before the rule, so
// they are not counted.
for range 2 {
line := wrongAdminToken.send(s, otherClient)
line := s.request(otherClient, blockedPath, http.StatusOK,
requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionBanned)
if line.LimitHit != "" {
@@ -360,7 +331,7 @@ func TestObserveModeLogsAndAlertsTheErrorBurst(t *testing.T) {
// The block rule's refusal, which enforce mode would have answered 403,
// is the second of the client's, and would have banned it.
wrongAdminToken.send(s, client)
s.request(client, blockedPath, http.StatusOK, requestlog.ActionForward)
line := s.request(client, blockedPath, http.StatusOK, requestlog.ActionForward)
wantWouldAction(t, line, requestlog.ActionRuleBlocked)