servewaf always answers rproxy well within its time limit (closes #182)
check / check (push) Successful in 8m31s

A question waits on GeoJS at most 800ms, the new default of
SWWAF_LOOKUP_TIMEOUT. In enforce mode it does not wait at all when it is
over the header limits, on a fetched blocklist with deny, on the CrowdSec
decision list, or over a whole rate limit. Not waiting never lowers a
client's limit. Just before the Core Rule Set, servewaf refuses more than
50 cookies or 100 header fields with 431.

Shared default changed: SWWAF_LOOKUP_TIMEOUT also applies to smallwebwaf
with no command.
Judgement call: SWWAF_REPUTATION_TIMEOUT stays 2s, since no request waits
on the DNS blocklists or AbuseIPDB.

Model: opus-5-5
This commit was merged in pull request #183.
This commit is contained in:
2026-11-05 05:48:50 +01:00
parent 2eeea136e8
commit 4a4874ae2f
22 changed files with 937 additions and 55 deletions
+24
View File
@@ -228,6 +228,30 @@ func (l *Limiter) Count(
return l.count(client, now, 1, 0, percent)
}
// OverARateLimit reports whether one more request from client at now would
// take it over a whole rate limit. A client's percentage can only lower a
// limit, so that request is over the limit at any percentage. It counts
// nothing, and does not make the client the most recently seen.
func (l *Limiter) OverARateLimit(client netip.Prefix, now time.Time) bool {
l.mu.Lock()
defer l.mu.Unlock()
c, seen := l.clients.Peek(client)
if !seen {
return false
}
buckets := c.buckets()
for i, w := range l.windows {
if w.limit > 0 && buckets[i].Add(now, w.length, 0)+1 > float64(w.limit) {
return true
}
}
return false
}
// CountBytes counts bytes, those of a request from client that has ended,
// at now, in every window, and returns the client's counts in each window.
// It reports whether the bytes take the client over a byte limit, of which
+44
View File
@@ -26,6 +26,50 @@ const (
hour = "hour"
)
func TestOverARateLimitSaysWhatTheNextRequestWouldDoAndCountsNothing(t *testing.T) {
t.Parallel()
client := netip.MustParsePrefix("203.0.113.9/32")
now := midnight()
limiter := ratelimit.New(ratelimit.Limits{PerMinute: limit, PerDay: limit + 1},
tableSize)
if limiter.OverARateLimit(client, now) {
t.Error("a client never seen is over a rate limit")
}
if limiter.Len() != 0 {
t.Error("asking about a client never seen added it to the table")
}
for range limit {
if limiter.OverARateLimit(client, now) {
t.Error("a client with room for another request is over a rate limit")
}
wantCount(t, limiter, client, now, "")
}
if !limiter.OverARateLimit(client, now) {
t.Error("a client at its limit is not over a rate limit")
}
held, _ := limiter.Client(client)
if held.Minute.Current != limit {
t.Errorf("the client's minute holds %d requests, want %d", held.Minute.Current, limit)
}
wantCount(t, limiter, client, now, minute)
// A limit that is off is never gone over.
off := ratelimit.New(ratelimit.Limits{}, tableSize)
off.Count(client, now, whole)
if off.OverARateLimit(client, now) {
t.Error("a client is over a rate limit that is off")
}
}
func TestEachWindowRefusesAtItsLimitAndLetsTheClientBack(t *testing.T) {
t.Parallel()