1 Commits
Author SHA1 Message Date
sneak 9e6abf8496 Name log fields, metrics and the Core Rule Set package for what they are (closes #192)
check / check (push) Failing after 5m32s
Log field, metric, action and event names now say what they count
and, for timings, the unit. Package internal/waf is internal/coreruleset.

Model: opus-5-5
2026-11-09 07:21:52 +00:00
24 changed files with 91 additions and 86 deletions

No files matched your search

+7 -5
View File
@@ -220,7 +220,7 @@ type State struct {
// requests to it failed, and how many alerts were dropped from its full // requests to it failed, and how many alerts were dropped from its full
// queue or given up as it refused them. // queue or given up as it refused them.
type Counts struct { type Counts struct {
Sent, Failed, Dropped int64 Sent, SendFailures, Dropped int64
} }
// Queue takes the alerts raised, holds back those it must, and sends the // Queue takes the alerts raised, holds back those it must, and sends the
@@ -264,7 +264,7 @@ type destination struct {
// waiting are the alerts waiting to be sent, oldest first. // waiting are the alerts waiting to be sent, oldest first.
waiting []*Alert waiting []*Alert
sent, failed, dropped atomic.Int64 sent, sendFailures, dropped atomic.Int64
} }
// cooldownKey is what makes an alert a repeat of another: the same event // cooldownKey is what makes an alert a repeat of another: the same event
@@ -426,7 +426,9 @@ func (q *Queue) Counts(name string) Counts {
for _, d := range q.destinations { for _, d := range q.destinations {
if d.name == name { if d.name == name {
return Counts{ return Counts{
Sent: d.sent.Load(), Failed: d.failed.Load(), Dropped: d.dropped.Load(), Sent: d.sent.Load(),
SendFailures: d.sendFailures.Load(),
Dropped: d.dropped.Load(),
} }
} }
} }
@@ -675,7 +677,7 @@ func (d *destination) run(ctx context.Context) {
retryAt = time.Time{} retryAt = time.Time{}
case errors.Is(err, errRefused): case errors.Is(err, errRefused):
d.remove(alert) d.remove(alert)
d.failed.Add(1) d.sendFailures.Add(1)
d.dropped.Add(1) d.dropped.Add(1)
retryDelay = 0 retryDelay = 0
@@ -684,7 +686,7 @@ func (d *destination) run(ctx context.Context) {
d.processLog.Warn("gave up an alert "+d.setting+" refused", d.processLog.Warn("gave up an alert "+d.setting+" refused",
"event", alert.Event, "error", err.Error()) "event", alert.Event, "error", err.Error())
case ctx.Err() == nil: // not cut off as sww stops case ctx.Err() == nil: // not cut off as sww stops
d.failed.Add(1) d.sendFailures.Add(1)
retryDelay = min(max(retryDelayFactor*retryDelay, firstRetryDelay), retryDelay = min(max(retryDelayFactor*retryDelay, firstRetryDelay),
maxRetryDelay) maxRetryDelay)
+4 -4
View File
@@ -897,7 +897,7 @@ func TestDestinationThatDoesNotAnswerHoldsUpNeitherOther(t *testing.T) {
synctest.Wait() synctest.Wait()
wantDestinationCounts(t, q, alerts.DestinationSlack, wantDestinationCounts(t, q, alerts.DestinationSlack,
alerts.Counts{Sent: 3, Failed: 1}) alerts.Counts{Sent: 3, SendFailures: 1})
logged := log.String() logged := log.String()
if !strings.Contains(logged, if !strings.Contains(logged,
@@ -934,7 +934,7 @@ func TestDropsAreCountedForEachDestination(t *testing.T) {
for destination, want := range map[string]alerts.Counts{ for destination, want := range map[string]alerts.Counts{
alerts.DestinationWebhook: {Sent: all}, alerts.DestinationWebhook: {Sent: all},
alerts.DestinationSlack: {Dropped: 1}, alerts.DestinationSlack: {Dropped: 1},
alerts.DestinationNtfy: {Failed: all, Dropped: all}, alerts.DestinationNtfy: {SendFailures: all, Dropped: all},
} { } {
wantDestinationCounts(t, q, destination, want) wantDestinationCounts(t, q, destination, want)
} }
@@ -1223,12 +1223,12 @@ func wantEvents(t *testing.T, webhook *standIn, want ...string) {
// requests to it that it counts as failed, the alerts it counts as held // requests to it that it counts as failed, the alerts it counts as held
// back, and those it counts as dropped for the webhook. // back, and those it counts as dropped for the webhook.
func wantCounts( func wantCounts(
t *testing.T, q *alerts.Queue, sent, failed, suppressed, dropped int64, t *testing.T, q *alerts.Queue, sent, sendFailures, suppressed, dropped int64,
) { ) {
t.Helper() t.Helper()
wantDestinationCounts(t, q, alerts.DestinationWebhook, wantDestinationCounts(t, q, alerts.DestinationWebhook,
alerts.Counts{Sent: sent, Failed: failed, Dropped: dropped}) alerts.Counts{Sent: sent, SendFailures: sendFailures, Dropped: dropped})
if q.Suppressed() != suppressed { if q.Suppressed() != suppressed {
t.Errorf("%d alerts held back, want %d", q.Suppressed(), suppressed) t.Errorf("%d alerts held back, want %d", q.Suppressed(), suppressed)
+1 -1
View File
@@ -391,7 +391,7 @@ func alertFor(r Request, k key, o overThreshold) alerts.Alert {
detail["asn"] = k.asn detail["asn"] = k.asn
case ScopeTotal: case ScopeTotal:
counted = "the whole service" counted = "the whole service"
default: // watch default: // watched_net
counted = "the named netblock " + k.name + ", " + k.netblock.String() counted = "the named netblock " + k.name + ", " + k.netblock.String()
detail["name"] = k.name detail["name"] = k.name
} }
+4 -4
View File
@@ -284,7 +284,7 @@ func checkExclusions(exclusions string) error {
// CoreRuleSet is the Core Rule Set, ready to inspect requests. It is safe // CoreRuleSet is the Core Rule Set, ready to inspect requests. It is safe
// for concurrent use. // for concurrent use.
type CoreRuleSet struct { type CoreRuleSet struct {
waf coraza.WAF engine coraza.WAF
bodyLimit int64 bodyLimit int64
} }
@@ -327,14 +327,14 @@ func New(params Params) (*CoreRuleSet, error) {
text += "SecRuleRemoveById " + strings.Join(ids, " ") + "\n" text += "SecRuleRemoveById " + strings.Join(ids, " ") + "\n"
} }
waf, err := coraza.NewWAF(coraza.NewWAFConfig(). engine, err := coraza.NewWAF(coraza.NewWAFConfig().
WithRootFS(coreruleset.FS). WithRootFS(coreruleset.FS).
WithDirectives(text)) WithDirectives(text))
if err != nil { if err != nil {
return nil, fmt.Errorf("load the Core Rule Set: %w", err) return nil, fmt.Errorf("load the Core Rule Set: %w", err)
} }
return &CoreRuleSet{waf: waf, bodyLimit: params.BodyLimit}, nil return &CoreRuleSet{engine: engine, bodyLimit: params.BodyLimit}, nil
} }
// Result is what the Core Rule Set found in a request. // Result is what the Core Rule Set found in a request.
@@ -354,7 +354,7 @@ type Result struct {
func (c *CoreRuleSet) Inspect( func (c *CoreRuleSet) Inspect(
r *http.Request, client netip.Addr, body io.Reader, r *http.Request, client netip.Addr, body io.Reader,
) (Result, []byte, error) { ) (Result, []byte, error) {
tx := c.waf.NewTransaction() tx := c.engine.NewTransaction()
// Closing would remove the files Coraza wrote, and it writes none. // Closing would remove the files Coraza wrote, and it writes none.
defer func() { _ = tx.Close() }() defer func() { _ = tx.Close() }()
+7 -7
View File
@@ -40,7 +40,7 @@ func TestAdminEndpointsAreOffWhileTheTokenIsUnset(t *testing.T) {
// An empty token does not match the unset one either. // An empty token does not match the unset one either.
for _, authorization := range []string{adminBearer, bearer, "Bearer ", ""} { for _, authorization := range []string{adminBearer, bearer, "Bearer ", ""} {
for _, e := range adminEndpoints() { for _, e := range adminEndpoints() {
s.adminRequest(authorization, e.method, e.path, e.body, http.StatusNotFound) s.ownEndpointRequest(authorization, e.method, e.path, e.body, http.StatusNotFound)
} }
} }
@@ -67,7 +67,7 @@ func TestAdminEndpointsNeedTheAdminToken(t *testing.T) {
for _, authorization := range []string{ for _, authorization := range []string{
"", bearer, "Bearer " + strings.ToUpper(adminSecret), "Basic " + adminSecret, "", bearer, "Bearer " + strings.ToUpper(adminSecret), "Basic " + adminSecret,
} { } {
got := s.adminRequest(authorization, e.method, e.path, e.body, got := s.ownEndpointRequest(authorization, e.method, e.path, e.body,
http.StatusUnauthorized) http.StatusUnauthorized)
if got.header.Get("WWW-Authenticate") != "Bearer" { if got.header.Get("WWW-Authenticate") != "Bearer" {
t.Errorf("%s %s with %q was answered without WWW-Authenticate: Bearer", t.Errorf("%s %s with %q was answered without WWW-Authenticate: Bearer",
@@ -329,7 +329,7 @@ func TestAdminEndpointsNeedTheTokenInObserveMode(t *testing.T) {
}) })
for _, e := range adminEndpoints() { for _, e := range adminEndpoints() {
s.adminRequest("", e.method, e.path, e.body, http.StatusUnauthorized) s.ownEndpointRequest("", e.method, e.path, e.body, http.StatusUnauthorized)
} }
if held := server.Ledger.Snapshot(); len(held) != 0 { if held := server.Ledger.Snapshot(); len(held) != 0 {
@@ -360,12 +360,12 @@ func adminEndpoints() []adminEndpoint {
func (s *sender) admin(method, path, body string, status int) answer { func (s *sender) admin(method, path, body string, status int) answer {
s.t.Helper() s.t.Helper()
return s.adminRequest(adminBearer, method, path, body, status) return s.ownEndpointRequest(adminBearer, method, path, body, status)
} }
// adminRequest is admin with authorization as the request's Authorization header // ownEndpointRequest is admin with authorization as the request's Authorization
// unless it is "". // header unless it is "".
func (s *sender) adminRequest( func (s *sender) ownEndpointRequest(
authorization, method, path, body string, status int, authorization, method, path, body string, status int,
) answer { ) answer {
s.t.Helper() s.t.Helper()
+3 -3
View File
@@ -36,8 +36,8 @@ const (
// tooLarge is how discriminator refuses a request over the header limits. // tooLarge is how discriminator refuses a request over the header limits.
tooLarge = http.StatusRequestHeaderFieldsTooLarge tooLarge = http.StatusRequestHeaderFieldsTooLarge
// silentAbuseIPDBURL is where AbuseIPDB never answers: at the stand-in // silentAbuseIPDBURL is where AbuseIPDB never answers: at the stand-in
// that unansweredGeoJSURL names too. // that silentGeoJSURL names too.
silentAbuseIPDBURL = "unanswered://abuseipdb/api/v2/check" silentAbuseIPDBURL = "silent://abuseipdb/api/v2/check"
// wholeLimit is the percentage of a rate limit that lowers it not at all. // wholeLimit is the percentage of a rate limit that lowers it not at all.
wholeLimit = 100 wholeLimit = 100
// clientsDecision is the CrowdSec decision list holding client alone. // clientsDecision is the CrowdSec decision list holding client alone.
@@ -340,7 +340,7 @@ func newDiscriminatorInBubble(
settings := map[string]string{tagDeny: "country=KP"} settings := map[string]string{tagDeny: "country=KP"}
maps.Copy(settings, env) maps.Copy(settings, env)
server, out, _ := newDiscriminator(t, unansweredGeoJSURL, time.Now, settings) server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now, settings)
return server, out return server, out
} }
+2 -2
View File
@@ -134,9 +134,9 @@ func (rq *request) banForLimit(now time.Time, hit ratelimit.Hit, status int) {
switch hit.Kind { switch hit.Kind {
case ratelimit.KindRequests: case ratelimit.KindRequests:
notes.LimitPercent, notes.LimitPercentSetting = rq.limitPercent.logged() notes.LimitPercent, notes.LimitPercentSetting = rq.rateLimitPercent.logged()
case ratelimit.KindBytes: case ratelimit.KindBytes:
notes.LimitPercent, notes.LimitPercentSetting = rq.bytesPercent.logged() notes.LimitPercent, notes.LimitPercentSetting = rq.bytesLimitPercent.logged()
} }
if rq.h.config.Observe { if rq.h.config.Observe {
+1 -1
View File
@@ -415,7 +415,7 @@ func startWithClock(
// sender sends requests to rproxy one after another, each on a connection of its // sender sends requests to rproxy one after another, each on a connection of its
// own, and checks each one's answer and log line. discriminatorAddr is where // own, and checks each one's answer and log line. discriminatorAddr is where
// discriminator's own endpoints are, which adminRequest asks. Every request // discriminator's own endpoints are, which ownEndpointRequest asks. Every request
// discriminator gets must come through the sender, since the log lines are // discriminator gets must come through the sender, since the log lines are
// matched to them in order. // matched to them in order.
type sender struct { type sender struct {
+7 -7
View File
@@ -143,9 +143,9 @@ func TestBytesPercentSettingsTakeThePlaceOfTheOthersForByteLimits(t *testing.T)
for _, tc := range []struct { for _, tc := range []struct {
name string name string
env map[string]string env map[string]string
// limitPercent and bytesPercent are the log line's, as percentText // rateLimitPercent and bytesLimitPercent are the log line's, as
// gives them, and limitHit is its limit_hit. // percentText gives them, and limitHit is its limit_hit.
limitPercent, bytesPercent, limitHit string rateLimitPercent, bytesLimitPercent, limitHit string
}{ }{
{ {
"lowering the byte limits alone", "lowering the byte limits alone",
@@ -183,10 +183,10 @@ func TestBytesPercentSettingsTakeThePlaceOfTheOthersForByteLimits(t *testing.T)
wantPercent(t, "rate_limit_percent", wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting, line.RateLimitPercent, line.RateLimitPercentSetting,
tc.limitPercent) tc.rateLimitPercent)
wantPercent(t, "bytes_limit_percent", wantPercent(t, "bytes_limit_percent",
line.BytesLimitPercent, line.BytesLimitPercentSetting, line.BytesLimitPercent, line.BytesLimitPercentSetting,
tc.bytesPercent) tc.bytesLimitPercent)
}) })
} }
} }
@@ -291,7 +291,7 @@ func TestClientWithoutAnAnswerInTimeHasTheUnknownLimitPercent(t *testing.T) {
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays // In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
// says, with a GeoJS that never answers. // says, with a GeoJS that never answers.
synctest.Test(t, func(t *testing.T) { synctest.Test(t, func(t *testing.T) {
server, out, _ := newDiscriminator(t, unansweredGeoJSURL, time.Now, server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
map[string]string{unknownLimitPercent: "0"}) map[string]string{unknownLimitPercent: "0"})
// Once the time the request waits for its answer is up, the client // Once the time the request waits for its answer is up, the client
@@ -335,7 +335,7 @@ func TestRequestWaitsForItsLookupWhileABiasedThresholdIsSet(t *testing.T) {
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays // In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
// says, with a GeoJS that never answers. // says, with a GeoJS that never answers.
synctest.Test(t, func(t *testing.T) { synctest.Test(t, func(t *testing.T) {
server, out, _ := newDiscriminator(t, unansweredGeoJSURL, time.Now, server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
map[string]string{ map[string]string{
lookupTimeout: timeout.String(), tagLists: "vpn=" + vpnList, tc.setting: tc.value, lookupTimeout: timeout.String(), tagLists: "vpn=" + vpnList, tc.setting: tc.value,
}) })
+1 -1
View File
@@ -26,7 +26,7 @@ func (byteLimits) check(_ context.Context, rq *request, now time.Time) *verdict
} }
counts, hit, over := rq.h.limiter.CountBytes(rq.h.clientGroup(rq.client), now, counts, hit, over := rq.h.limiter.CountBytes(rq.h.clientGroup(rq.client), now,
rq.countedBytes(), rq.bytesPercent.percent) rq.countedBytes(), rq.bytesLimitPercent.percent)
rq.line.Counts.MinuteBytes = counts.MinuteBytes rq.line.Counts.MinuteBytes = counts.MinuteBytes
rq.line.Counts.HourBytes = counts.HourBytes rq.line.Counts.HourBytes = counts.HourBytes
rq.line.Counts.DayBytes = counts.DayBytes rq.line.Counts.DayBytes = counts.DayBytes
+1 -1
View File
@@ -183,7 +183,7 @@ func (h *handler) answerQuestion(w http.ResponseWriter, r *http.Request) {
rq.line.DiscriminatorLine = requestlog.DiscriminatorLineDecision rq.line.DiscriminatorLine = requestlog.DiscriminatorLineDecision
refused := rq.check(r.Context()) refused := rq.check(r.Context())
rq.checked = time.Now() rq.checked = time.Now()
underOwnPrefix := strings.HasPrefix(r.URL.Path, adminPrefix) underOwnPrefix := strings.HasPrefix(r.URL.Path, ownEndpointPrefix)
if refused == nil && !underOwnPrefix { if refused == nil && !underOwnPrefix {
// In observe mode, what enforce mode would have refused counts here. // In observe mode, what enforce mode would have refused counts here.
+2 -1
View File
@@ -21,7 +21,8 @@ func TestHealthEndpointCountsTowardNoLimit(t *testing.T) {
) )
for range healthChecks { for range healthChecks {
got := s.adminRequest("", http.MethodGet, discriminator.HealthPath, "", http.StatusOK) got := s.ownEndpointRequest("", http.MethodGet, discriminator.HealthPath, "",
http.StatusOK)
if string(got.body) != "ok\n" || got.header.Get("Content-Type") != contentType { if string(got.body) != "ok\n" || got.header.Get("Content-Type") != contentType {
t.Errorf("health endpoint answered %q with Content-Type %q, want ok "+ t.Errorf("health endpoint answered %q with Content-Type %q, want ok "+
+2 -2
View File
@@ -100,8 +100,8 @@ func TestOwnEndpointsAreNotInTheHistory(t *testing.T) {
s, _, server := startWithClock(t, "", map[string]string{metricsToken: token}) s, _, server := startWithClock(t, "", map[string]string{metricsToken: token})
s.adminRequest("", http.MethodGet, discriminator.HealthPath, "", http.StatusOK) s.ownEndpointRequest("", http.MethodGet, discriminator.HealthPath, "", http.StatusOK)
s.adminRequest("", http.MethodGet, discriminator.MetricsPath, "", s.ownEndpointRequest("", http.MethodGet, discriminator.MetricsPath, "",
http.StatusUnauthorized) http.StatusUnauthorized)
s.scrape() s.scrape()
+7 -7
View File
@@ -385,7 +385,7 @@ func TestRequestWaitsAsLongAsTheLookupTimeoutSays(t *testing.T) {
// is never reached. // is never reached.
const timeout = 3 * time.Second const timeout = 3 * time.Second
server, out, _ := newDiscriminator(t, unansweredGeoJSURL, time.Now, server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
map[string]string{ map[string]string{
lookupTimeout: timeout.String(), lookupTimeout: timeout.String(),
allowedCountries: "DE", allowedCountries: "DE",
@@ -405,25 +405,25 @@ func TestRequestWaitsAsLongAsTheLookupTimeoutSays(t *testing.T) {
}) })
} }
// unansweredGeoJSURL is where a GeoJS that never answers is asked: a // silentGeoJSURL is where a GeoJS that never answers is asked: a
// request to it waits, without the network, until it is abandoned. // request to it waits, without the network, until it is abandoned.
// TestMain registers it with Go's default transport, through which GeoJS // TestMain registers it with Go's default transport, through which GeoJS
// is asked. // is asked.
const unansweredGeoJSURL = "unanswered://geojs/v1/ip/geo.json" const silentGeoJSURL = "silent://geojs/v1/ip/geo.json"
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
transport, _ := http.DefaultTransport.(*http.Transport) transport, _ := http.DefaultTransport.(*http.Transport)
transport.RegisterProtocol("unanswered", unansweredGeoJS{}) transport.RegisterProtocol("silent", silentGeoJS{})
transport.RegisterProtocol("abuseipdb", abuseIPDBStandIn{}) transport.RegisterProtocol("abuseipdb", abuseIPDBStandIn{})
m.Run() m.Run()
} }
// unansweredGeoJS is the GeoJS at unansweredGeoJSURL. // silentGeoJS is the GeoJS at silentGeoJSURL.
type unansweredGeoJS struct{} type silentGeoJS struct{}
// RoundTrip waits until req is abandoned. // RoundTrip waits until req is abandoned.
func (unansweredGeoJS) RoundTrip(req *http.Request) (*http.Response, error) { func (silentGeoJS) RoundTrip(req *http.Request) (*http.Response, error) {
<-req.Context().Done() <-req.Context().Done()
return nil, req.Context().Err() return nil, req.Context().Err()
+4 -4
View File
@@ -32,7 +32,7 @@ func TestMetricsAreOffWhileTheTokenIsUnset(t *testing.T) {
// An empty token does not match the unset one either. // An empty token does not match the unset one either.
for _, authorization := range []string{bearer, "Bearer ", ""} { for _, authorization := range []string{bearer, "Bearer ", ""} {
s.adminRequest(authorization, http.MethodGet, discriminator.MetricsPath, "", s.ownEndpointRequest(authorization, http.MethodGet, discriminator.MetricsPath, "",
http.StatusNotFound) http.StatusNotFound)
} }
} }
@@ -60,7 +60,7 @@ func TestMetricsNeedTheTokenAndOtherPathsAreNotFound(t *testing.T) {
{http.MethodGet, "/_sww/bans", bearer, http.StatusNotFound}, {http.MethodGet, "/_sww/bans", bearer, http.StatusNotFound},
{http.MethodPost, discriminator.HealthPath, "", http.StatusNotFound}, {http.MethodPost, discriminator.HealthPath, "", http.StatusNotFound},
} { } {
got := s.adminRequest(tc.authorization, tc.method, tc.path, "", tc.status) got := s.ownEndpointRequest(tc.authorization, tc.method, tc.path, "", tc.status)
if tc.status == http.StatusUnauthorized && if tc.status == http.StatusUnauthorized &&
got.header.Get("WWW-Authenticate") != "Bearer" { got.header.Get("WWW-Authenticate") != "Bearer" {
@@ -381,8 +381,8 @@ func scrape(t *testing.T, addr string) string {
func (s *sender) scrape() string { func (s *sender) scrape() string {
s.t.Helper() s.t.Helper()
return string(s.adminRequest(bearer, http.MethodGet, discriminator.MetricsPath, "", return string(s.ownEndpointRequest(bearer, http.MethodGet, discriminator.MetricsPath,
http.StatusOK).body) "", http.StatusOK).body)
} }
// metric returns the value of series in metrics, which are in the // metric returns the value of series in metrics, which are in the
+3 -3
View File
@@ -25,9 +25,9 @@ import (
"sneak.berlin/go/smallwebwaf/internal/rules" "sneak.berlin/go/smallwebwaf/internal/rules"
) )
// adminPrefix starts the path of each of discriminator's own endpoints. The app never // ownEndpointPrefix starts the path of each of discriminator's own endpoints. The app
// sees a path under it. // never sees a path under it.
const adminPrefix = "/_sww/" const ownEndpointPrefix = "/_sww/"
// HealthPath is the health endpoint of discriminator and of rproxy, which the // HealthPath is the health endpoint of discriminator and of rproxy, which the
// container's health check asks. // container's health check asks.
+6 -4
View File
@@ -28,12 +28,14 @@ func (rateLimits) check(_ context.Context, rq *request, now time.Time) *verdict
return nil return nil
} }
rq.limitPercent, rq.bytesPercent = rq.limitPercentages() rq.rateLimitPercent, rq.bytesLimitPercent = rq.limitPercentages()
rq.line.RateLimitPercent, rq.line.RateLimitPercentSetting = rq.limitPercent.logged() rq.line.RateLimitPercent, rq.line.RateLimitPercentSetting =
rq.line.BytesLimitPercent, rq.line.BytesLimitPercentSetting = rq.bytesPercent.logged() rq.rateLimitPercent.logged()
rq.line.BytesLimitPercent, rq.line.BytesLimitPercentSetting =
rq.bytesLimitPercent.logged()
counts, hit, over := rq.h.limiter.Count(rq.h.clientGroup(rq.client), now, counts, hit, over := rq.h.limiter.Count(rq.h.clientGroup(rq.client), now,
rq.limitPercent.percent) rq.rateLimitPercent.percent)
rq.line.Counts = counts rq.line.Counts = counts
if !over { if !over {
+5 -5
View File
@@ -256,9 +256,9 @@ func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(t *testing.T)
name string name string
env map[string]string env map[string]string
file string file string
// limitPercent and bytesPercent are the upload's, as percentText // rateLimitPercent and bytesLimitPercent are the upload's, as
// gives them. // percentText gives them.
limitPercent, bytesPercent string rateLimitPercent, bytesLimitPercent string
}{ }{
{"the file's alone", nil, asnDEQuarter, fromURL, fromURL}, {"the file's alone", nil, asnDEQuarter, fromURL, fromURL},
{ {
@@ -293,10 +293,10 @@ func TestASNLimitPercentFileCountsAsTheSettingDoesTheLowerWinning(t *testing.T)
line := s.uploadFrom(fromDE) line := s.uploadFrom(fromDE)
wantPercent(t, "rate_limit_percent", wantPercent(t, "rate_limit_percent",
line.RateLimitPercent, line.RateLimitPercentSetting, line.RateLimitPercent, line.RateLimitPercentSetting,
tc.limitPercent) tc.rateLimitPercent)
wantPercent(t, "bytes_limit_percent", wantPercent(t, "bytes_limit_percent",
line.BytesLimitPercent, line.BytesLimitPercentSetting, line.BytesLimitPercent, line.BytesLimitPercentSetting,
tc.bytesPercent) tc.bytesLimitPercent)
}) })
} }
} }
+4 -4
View File
@@ -48,11 +48,11 @@ type decision struct {
// so that not waiting never lowers its client's limits. // so that not waiting never lowers its client's limits.
skippedLookupWait bool skippedLookupWait bool
// counted is true for a request the rate limits counted, whose bytes // counted is true for a request the rate limits counted, whose bytes
// the byte limits count once it has ended. limitPercent and // the byte limits count once it has ended. rateLimitPercent and
// bytesPercent are then its client's limit percentages for the rate // bytesLimitPercent are then its client's limit percentages for the rate
// limits and for the byte limits. // limits and for the byte limits.
counted bool counted bool
limitPercent, bytesPercent percentage rateLimitPercent, bytesLimitPercent percentage
// attack is true for a request that matched a ban rule or asked for a // attack is true for a request that matched a ban rule or asked for a
// trap path, blockedByBlockRule for one a block rule refused, and // trap path, blockedByBlockRule for one a block rule refused, and
// blockedByCoreRuleSet for one the Core Rule Set refused, each an offence // blockedByCoreRuleSet for one the Core Rule Set refused, each an offence
+7 -7
View File
@@ -362,7 +362,7 @@ func TestWithoutWaitTheRequestGoesOnAtOnceAndTheAnswerIsGivenWhenItComes(
mu.Unlock() mu.Unlock()
wantCountry(t, g, client, germany) wantCountry(t, g, client, germany)
wantUnanswered(t, m, 0) wantRequestsWithoutGeoJSAnswer(t, m, 0)
}) })
} }
@@ -596,7 +596,7 @@ func TestClientsWithoutAnAnswerAreCounted(t *testing.T) {
// GeoJS fails, so the first client goes without an answer, and GeoJS // GeoJS fails, so the first client goes without an answer, and GeoJS
// is left alone for a second, which does not pass in this test. // is left alone for a second, which does not pass in this test.
wantCountry(t, g, clients(), "") wantCountry(t, g, clients(), "")
wantUnanswered(t, m, 1) wantRequestsWithoutGeoJSAnswer(t, m, 1)
// Meanwhile each new client goes without one at once, while there is // Meanwhile each new client goes without one at once, while there is
// room for it among the 10,000 that may wait. // room for it among the 10,000 that may wait.
@@ -604,11 +604,11 @@ func TestClientsWithoutAnAnswerAreCounted(t *testing.T) {
wantCountry(t, g, clients(), "") wantCountry(t, g, clients(), "")
} }
wantUnanswered(t, m, 10000) wantRequestsWithoutGeoJSAnswer(t, m, 10000)
// One more, for which there is no room, goes without one too. // One more, for which there is no room, goes without one too.
wantCountry(t, g, clients(), "") wantCountry(t, g, clients(), "")
wantUnanswered(t, m, 10001) wantRequestsWithoutGeoJSAnswer(t, m, 10001)
}) })
} }
@@ -841,9 +841,9 @@ func wantAsked(t *testing.T, geojs *standIn, i int, want ...string) {
} }
} }
// wantUnanswered checks how many requests m counts as having gone without // wantRequestsWithoutGeoJSAnswer checks how many requests m counts as having
// an answer from GeoJS. // gone without an answer from GeoJS.
func wantUnanswered(t *testing.T, m *metrics.Metrics, want float64) { func wantRequestsWithoutGeoJSAnswer(t *testing.T, m *metrics.Metrics, want float64) {
t.Helper() t.Helper()
got := testutil.ToFloat64(m.RequestsWithoutGeoJSAnswer) got := testutil.ToFloat64(m.RequestsWithoutGeoJSAnswer)
+1 -1
View File
@@ -359,7 +359,7 @@ func (m *Metrics) AddAlerts(queue *alerts.Queue) {
Help: "Requests to the destination that failed.", Help: "Requests to the destination that failed.",
ConstLabels: destination, ConstLabels: destination,
}, func() float64 { }, func() float64 {
return float64(queue.Counts(name).Failed) return float64(queue.Counts(name).SendFailures)
}), }),
prometheus.NewCounterFunc(prometheus.CounterOpts{ prometheus.NewCounterFunc(prometheus.CounterOpts{
Name: "sww_alerts_suppressed_total", Name: "sww_alerts_suppressed_total",
+5 -5
View File
@@ -45,9 +45,9 @@ const (
idleTimeout = 120 * time.Second idleTimeout = 120 * time.Second
) )
// adminPrefix starts the path of every request for sww itself, which // ownEndpointPrefix starts the path of every request for sww itself, which
// never reaches the app. // never reaches the app.
const adminPrefix = "/_sww/" const ownEndpointPrefix = "/_sww/"
// allowedMethods lists the methods rproxy passes on, as an Allow header gives // allowedMethods lists the methods rproxy passes on, as an Allow header gives
// them. It answers any other method 405 itself. // them. It answers any other method 405 itself.
@@ -211,8 +211,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
// passToApp passes the request on to the app, unless a limit or its path keeps // passToApp passes the request on to the app, unless a limit or its path keeps
// it from the app. discriminator answers a path under adminPrefix itself, so such a // it from the app. discriminator answers a path under ownEndpointPrefix itself, so
// request gets here only when SWRP_DEFAULT_DISPOSITION allowed it. ctx is the // such a request gets here only when SWRP_DEFAULT_DISPOSITION allowed it. ctx is the
// request's own context. // request's own context.
func (rq *request) passToApp(ctx context.Context) { func (rq *request) passToApp(ctx context.Context) {
maxBytes := rq.h.config.MaxRequestSize maxBytes := rq.h.config.MaxRequestSize
@@ -220,7 +220,7 @@ func (rq *request) passToApp(ctx context.Context) {
switch { switch {
case rq.refused.Load() != 0 || (maxBytes > 0 && rq.in.ContentLength > maxBytes): case rq.refused.Load() != 0 || (maxBytes > 0 && rq.in.ContentLength > maxBytes):
rq.answer(http.StatusRequestEntityTooLarge) rq.answer(http.StatusRequestEntityTooLarge)
case strings.HasPrefix(rq.in.URL.Path, adminPrefix): case strings.HasPrefix(rq.in.URL.Path, ownEndpointPrefix):
rq.answer(http.StatusNotFound) rq.answer(http.StatusNotFound)
default: default:
rq.forward(ctx) rq.forward(ctx)
+6 -6
View File
@@ -64,7 +64,7 @@ var (
errCause = errors.New("is not limit, attack, admin or crowdsec") errCause = errors.New("is not limit, attack, admin or crowdsec")
errExpires = errors.New("is not after its start") errExpires = errors.New("is not after its start")
errDestination = errors.New("is not webhook, slack or ntfy") errDestination = errors.New("is not webhook, slack or ntfy")
errScope = errors.New("is not client, net, asn, total or watch") errScope = errors.New("is not client, net, asn, total or watched_net")
errWaitingList = errors.New(`waiting is a list, but now lists the alerts by ` + errWaitingList = errors.New(`waiting is a list, but now lists the alerts by ` +
`destination: put the list under "webhook", as "waiting": {"webhook": [...]}, ` + `destination: put the list under "webhook", as "waiting": {"webhook": [...]}, ` +
`or remove the file`) `or remove the file`)
@@ -858,8 +858,8 @@ func (f *alertsFile) check([]byte) error {
} }
// checkAnomalyCounters refuses an anomaly counter whose scope is not // checkAnomalyCounters refuses an anomaly counter whose scope is not
// client, net, asn, total or watch, most likely misspelt, and one without // client, net, asn, total or watched_net, most likely misspelt, and one
// a field it needs, as missingFromCounter tells. // without a field it needs, as missingFromCounter tells.
func checkAnomalyCounters(counters []anomaly.Counter) error { func checkAnomalyCounters(counters []anomaly.Counter) error {
for i, counter := range counters { for i, counter := range counters {
if !slices.Contains(anomaly.Scopes(), counter.Scope) { if !slices.Contains(anomaly.Scopes(), counter.Scope) {
@@ -879,9 +879,9 @@ func checkAnomalyCounters(counters []anomaly.Counter) error {
// missingFromCounter returns the first field counter, an anomaly counter, // missingFromCounter returns the first field counter, an anomaly counter,
// needs and has not, or "" when it has them all: what tells it from the // needs and has not, or "" when it has them all: what tells it from the
// others in its scope, without which it would never be counted again, the // others in its scope, without which it would never be counted again, the
// netblock of a client, net or watch counter, the AS number of an asn one // netblock of a client, net or watched_net counter, the AS number of an
// and the name of a watch one; and the start of a window in which it has // asn one and the name of a watched_net one; and the start of a window in
// requests or bytes, without which they would be dropped. // which it has requests or bytes, without which they would be dropped.
func missingFromCounter(counter anomaly.Counter) string { func missingFromCounter(counter anomaly.Counter) string {
scope := counter.Scope scope := counter.Scope
+1 -1
View File
@@ -534,7 +534,7 @@ func TestFileThatDoesNotParseStopsTheStart(t *testing.T) {
`{"version": 1, "anomaly_counters": [{"scope": "total"}, ` + `{"version": 1, "anomaly_counters": [{"scope": "total"}, ` +
`{"scope": "nett", "netblock": "203.0.113.0/24"}]}`, `{"scope": "nett", "netblock": "203.0.113.0/24"}]}`,
`: anomaly_counters entry 2's scope "nett" is not client, net, asn, total ` + `: anomaly_counters entry 2's scope "nett" is not client, net, asn, total ` +
`or watch`, `or watched_net`,
}, },
{ {
"a copy of a list with a line that does not read", reputationJSON, "a copy of a list with a line that does not read", reputationJSON,