Lower limits for listed AS numbers and countries (closes #21)
check / check (push) Waiting to run
check / check (push) Waiting to run
SWWAF_ASN_LIMIT_PERCENT and SWWAF_COUNTRY_LIMIT_PERCENT give the clients of the AS numbers and countries they list that percentage of every rate and byte limit, rounded down; SWWAF_ASN_BYTES_PERCENT and SWWAF_COUNTRY_BYTES_PERCENT take its place for the byte limits of those they list; SWWAF_UNKNOWN_LIMIT_PERCENT (100) covers clients without a country. The lowest applies. While one lowers a limit, a request waits for its client's lookup, and SWWAF_LOOKUP_SOURCE=off stops the start. Log lines give limit_percent and bytes_percent with their settings; ban notes, and so alerts, give the broken limit's. Judgement call: a client without a country is unknown, whatever its AS number. Judgement call: bytes_percent and its setting are log fields SPEC does not name. Rule suppressed: funlen on FromEnvironment, one line per setting. Model: opus-5-5
This commit was merged in pull request #103.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package ratelimit_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -11,6 +12,10 @@ import (
|
||||
// limit is the limit the tests set.
|
||||
const limit = 3
|
||||
|
||||
// whole is the percentage of each limit a client gets when nothing lowers
|
||||
// its limits.
|
||||
const whole = 100
|
||||
|
||||
// The windows, as Count names them.
|
||||
const (
|
||||
minute = "minute"
|
||||
@@ -62,14 +67,14 @@ func TestHitGivesTheLimitAndTheRequestsCounted(t *testing.T) {
|
||||
start := midnight()
|
||||
|
||||
for range limit {
|
||||
_, _, over := limiter.Count(client, start)
|
||||
_, _, over := limiter.Count(client, start, whole)
|
||||
if over {
|
||||
t.Fatal("a request within the limit is over it")
|
||||
}
|
||||
}
|
||||
|
||||
// Over both limits; the minute's is named, with the four requests.
|
||||
_, hit, over := limiter.Count(client, start)
|
||||
_, hit, over := limiter.Count(client, start, whole)
|
||||
|
||||
want := ratelimit.Hit{
|
||||
Kind: ratelimit.KindRequests, Window: minute, Limit: limit, Count: limit + 1,
|
||||
@@ -80,6 +85,61 @@ func TestHitGivesTheLimitAndTheRequestsCounted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientGetsItsPercentageOfEachLimitRoundedDown(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
limiter := ratelimit.New(ratelimit.Limits{PerMinute: 5, BytesPerDay: math.MaxInt64})
|
||||
client := netip.MustParsePrefix("203.0.113.9/32")
|
||||
start := midnight()
|
||||
|
||||
// Half of 5 requests is 2.5, rounded down to 2: the third is over.
|
||||
for range 2 {
|
||||
_, _, over := limiter.Count(client, start, 50)
|
||||
if over {
|
||||
t.Fatal("a request within half the limit is over it")
|
||||
}
|
||||
}
|
||||
|
||||
_, hit, over := limiter.Count(client, start, 50)
|
||||
|
||||
want := ratelimit.Hit{Kind: ratelimit.KindRequests, Window: minute, Limit: 2, Count: 3}
|
||||
if !over || hit != want {
|
||||
t.Errorf("the third request gives %+v and %t, want %+v and true", hit, over, want)
|
||||
}
|
||||
|
||||
// Half of the largest byte limit is still far above a TiB: working it
|
||||
// out does not overflow.
|
||||
_, hit, over = limiter.CountBytes(client, start, 1<<40, 50)
|
||||
if over {
|
||||
t.Errorf("a TiB is over half the largest byte limit: %+v", hit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroPercentIsAZeroAllowanceAndALimitOffStaysOff(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Only the hour has limits: the minute's and the day's are off.
|
||||
limiter := ratelimit.New(ratelimit.Limits{PerHour: limit, BytesPerHour: 1000})
|
||||
client := netip.MustParsePrefix("203.0.113.9/32")
|
||||
start := midnight()
|
||||
|
||||
// At 0 percent, the first request and the first byte are over the
|
||||
// hour's limits, which are 0; the minute's, which are off, stay off.
|
||||
_, hit, _ := limiter.Count(client, start, 0)
|
||||
|
||||
want := ratelimit.Hit{Kind: ratelimit.KindRequests, Window: hour, Limit: 0, Count: 1}
|
||||
if hit != want {
|
||||
t.Errorf("the first request gives %+v, want %+v", hit, want)
|
||||
}
|
||||
|
||||
_, hit, _ = limiter.CountBytes(client, start, 1, 0)
|
||||
|
||||
want = ratelimit.Hit{Kind: ratelimit.KindBytes, Window: hour, Limit: 0, Count: 1}
|
||||
if hit != want {
|
||||
t.Errorf("the first byte gives %+v, want %+v", hit, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEachByteLimitIsBrokenByTheBytesCounted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -100,12 +160,12 @@ func TestEachByteLimitIsBrokenByTheBytesCounted(t *testing.T) {
|
||||
client := netip.MustParsePrefix("203.0.113.9/32")
|
||||
|
||||
// 600 bytes are within the limit, 600 more over it.
|
||||
_, _, over := limiter.CountBytes(client, midnight(), 600)
|
||||
_, _, over := limiter.CountBytes(client, midnight(), 600, whole)
|
||||
if over {
|
||||
t.Fatal("600 bytes are over the limit of 1000")
|
||||
}
|
||||
|
||||
_, hit, over := limiter.CountBytes(client, midnight(), 600)
|
||||
_, hit, over := limiter.CountBytes(client, midnight(), 600, whole)
|
||||
|
||||
want := ratelimit.Hit{
|
||||
Kind: ratelimit.KindBytes, Window: tc.window, Limit: byteLimit, Count: 1200,
|
||||
@@ -148,16 +208,16 @@ func TestCountGivesTheBytesInEachWindow(t *testing.T) {
|
||||
client := netip.MustParsePrefix("203.0.113.9/32")
|
||||
start := midnight()
|
||||
|
||||
limiter.CountBytes(client, start, 300)
|
||||
limiter.CountBytes(client, start, 300, whole)
|
||||
|
||||
// A quarter into the next hour, the minute has only these 100 bytes.
|
||||
// The hour still covers three quarters of the bucket before, whose 300
|
||||
// bytes count 225, and these: 325. The day covers all 400.
|
||||
later := start.Add(time.Hour + time.Hour/4)
|
||||
limiter.CountBytes(client, later, 100)
|
||||
limiter.CountBytes(client, later, 100, whole)
|
||||
|
||||
// A request's counts give the bytes counted so far too.
|
||||
counts, _, _ := limiter.Count(client, later)
|
||||
counts, _, _ := limiter.Count(client, later, whole)
|
||||
|
||||
want := ratelimit.Counts{
|
||||
Minute: 1, Hour: 1, Day: 1, MinuteBytes: 100, HourBytes: 325, DayBytes: 400,
|
||||
@@ -189,14 +249,14 @@ func TestCountGivesTheRequestsInEachWindow(t *testing.T) {
|
||||
start := midnight()
|
||||
|
||||
for range 3 {
|
||||
limiter.Count(client, start)
|
||||
limiter.Count(client, start, whole)
|
||||
}
|
||||
|
||||
// A quarter into the next hour, the minute has only this request. The
|
||||
// hour still covers three quarters of the bucket before, with its three
|
||||
// requests, which count 2.25, and this one: 3.25. The day covers all
|
||||
// four.
|
||||
counts, _, _ := limiter.Count(client, start.Add(time.Hour+time.Hour/4))
|
||||
counts, _, _ := limiter.Count(client, start.Add(time.Hour+time.Hour/4), whole)
|
||||
|
||||
want := ratelimit.Counts{Minute: 1, Hour: 3.25, Day: 4}
|
||||
if counts != want {
|
||||
@@ -364,7 +424,7 @@ func wantCount(
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
_, hit, _ := limiter.Count(client, now)
|
||||
_, hit, _ := limiter.Count(client, now, whole)
|
||||
if hit.Window != want {
|
||||
t.Errorf("request from %s at %s is over %q, want %q",
|
||||
client, now.Format(time.RFC3339), hit.Window, want)
|
||||
@@ -379,7 +439,7 @@ func wantBytesCount(
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
_, hit, _ := limiter.CountBytes(client, now, bytes)
|
||||
_, hit, _ := limiter.CountBytes(client, now, bytes, whole)
|
||||
if hit.Kind != want {
|
||||
t.Errorf("%d bytes from %s at %s break a limit on %q, want %q",
|
||||
bytes, client, now.Format(time.RFC3339), hit.Kind, want)
|
||||
|
||||
Reference in New Issue
Block a user