check / check (push) Waiting to run
SWWAF_BYTES_LIMIT_PER_MINUTE, _PER_HOUR and _PER_DAY (10G, 20G, 50G) and SWWAF_BYTES_COUNT (both). A request's bytes are counted once its answer has ended, for a request passed to the app that the rate limits count; what a WebSocket carries each way, once it closes. Bytes over a limit ban the client as a broken rate limit does, and cut nothing short. clients.json keeps the byte buckets, the log line's counts carry the byte totals, ban notes say what the limit is on, and the limit hits metric is labelled by kind. Judgement call: limit_hit names a byte window minute_bytes, hour_bytes or day_bytes, as counts names the byte totals. Judgement call: in observe mode, the bytes of a request enforce mode would have refused are not counted. Model: opus-5-5
523 lines
17 KiB
Go
523 lines
17 KiB
Go
package bans_test
|
|
|
|
import (
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
|
)
|
|
|
|
const day = 24 * time.Hour
|
|
|
|
func TestRepeatsTripleUntilPermanent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
now := midnight()
|
|
|
|
// Each ban is followed by another as soon as it ends: 1, 3, 9, 27 and
|
|
// 81 hours.
|
|
for i, hours := range []int{1, 3, 9, 27, 81} {
|
|
ban, _ := ledger.BanForLimit(netblock, now, bans.Notes{})
|
|
|
|
length := time.Duration(hours) * time.Hour
|
|
if !ban.Expires.Equal(now.Add(length)) ||
|
|
ban.Notes.EarlierBans != (bans.EarlierBans{Limit: i}) {
|
|
t.Fatalf("ban %d lasts %s with earlier bans %+v, want %d hours and %d for a limit",
|
|
i+1, ban.Expires.Sub(now), ban.Notes.EarlierBans, hours, i)
|
|
}
|
|
|
|
now = ban.Expires
|
|
}
|
|
|
|
// The sixth would last 243 hours, more than seven days: it is
|
|
// permanent, and never ends.
|
|
ban, _ := ledger.BanForLimit(netblock, now, bans.Notes{})
|
|
if !ban.Permanent() {
|
|
t.Fatalf("sixth ban ends at %s, want a permanent one", ban.Expires)
|
|
}
|
|
|
|
_, banned, _ := ledger.Check(netblock.Addr(), now.Add(100*365*day))
|
|
if !banned {
|
|
t.Error("a permanent ban ended")
|
|
}
|
|
}
|
|
|
|
func TestRepeatWindowRunsOut(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
// gap is the time between the end of the first ban and the second.
|
|
gap time.Duration
|
|
want time.Duration
|
|
}{
|
|
{"broken again as the window ends", day, 3 * time.Hour},
|
|
{"broken again after the window", day + time.Nanosecond, time.Hour},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
|
|
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
|
second, _ := ledger.BanForLimit(netblock, first.Expires.Add(tc.gap), bans.Notes{})
|
|
|
|
if second.Expires.Sub(second.Start) != tc.want ||
|
|
second.Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
|
t.Errorf("second ban lasts %s with earlier bans %+v, want %s and 1 for a limit",
|
|
second.Expires.Sub(second.Start), second.Notes.EarlierBans, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFirstBanLongerThanTheMaximumIsPermanent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rules := defaultRules()
|
|
rules.LimitBanDuration = rules.MaxBanDuration + time.Hour
|
|
ledger := bans.New(rules)
|
|
|
|
ban, _ := ledger.BanForLimit(netip.MustParsePrefix("203.0.113.9/32"), midnight(),
|
|
bans.Notes{})
|
|
if !ban.Permanent() {
|
|
t.Errorf("first ban ends at %s, want a permanent one", ban.Expires)
|
|
}
|
|
}
|
|
|
|
func TestLongestBanSetFarOffDoesNotOverflow(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// 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.
|
|
rules := defaultRules()
|
|
rules.MaxBanDuration = 100000 * day
|
|
ledger := bans.New(rules)
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
now := midnight()
|
|
|
|
for i := range 14 {
|
|
ban, _ := ledger.BanForLimit(netblock, now, bans.Notes{})
|
|
if !ban.Expires.After(ban.Start) {
|
|
t.Fatalf("ban %d starts at %s and ends at %s", i+1, ban.Start, ban.Expires)
|
|
}
|
|
|
|
now = ban.Expires
|
|
}
|
|
|
|
ban, _ := ledger.BanForLimit(netblock, now, bans.Notes{})
|
|
if !ban.Permanent() {
|
|
t.Errorf("15th ban ends at %s, want a permanent one", ban.Expires)
|
|
}
|
|
}
|
|
|
|
func TestBrokenLimitDuringABanMakesNoOther(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
|
|
first, made := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
|
if !made {
|
|
t.Error("the first ban was not made")
|
|
}
|
|
|
|
again, made := ledger.BanForLimit(netblock, midnight().Add(time.Minute), bans.Notes{})
|
|
|
|
if made || again != first || len(ledger.Bans(netblock)) != 1 {
|
|
t.Errorf("a limit broken during a ban gave %+v, made %t, and %d bans, "+
|
|
"want %+v, not made, and 1", again, made, len(ledger.Bans(netblock)), first)
|
|
}
|
|
|
|
again, made = ledger.BanForAttack(netblock, midnight().Add(time.Minute), bans.Notes{})
|
|
if made || again != first {
|
|
t.Errorf("an attack during a ban gave %+v, made %t, want %+v, not made",
|
|
again, made, first)
|
|
}
|
|
}
|
|
|
|
func TestCheckRefusesWhileTheBanLastsAndCountsTheRefusals(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Requests: 5})
|
|
|
|
for range 3 {
|
|
got, banned, _ := ledger.Check(netblock.Addr(), ban.Expires.Add(-time.Nanosecond))
|
|
if !banned || got.Start != ban.Start {
|
|
t.Fatalf("check during the ban gives %+v and %t", got, banned)
|
|
}
|
|
}
|
|
|
|
_, banned, _ := ledger.Check(netip.MustParseAddr("203.0.113.10"), midnight())
|
|
if banned {
|
|
t.Error("another netblock is banned")
|
|
}
|
|
|
|
_, banned, _ = ledger.Check(netblock.Addr(), ban.Expires)
|
|
if banned {
|
|
t.Error("the ban did not end")
|
|
}
|
|
|
|
// The netblock's requests went from 5 to 8 with the three refused.
|
|
notes := ledger.Bans(netblock)[0].Notes
|
|
if notes.Refused != 3 || notes.Requests != 8 {
|
|
t.Errorf("the notes count %d refused requests of %d, want 3 of 8",
|
|
notes.Refused, notes.Requests)
|
|
}
|
|
}
|
|
|
|
func TestFindCountsNothing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Requests: 5})
|
|
|
|
got, banned, _ := ledger.Find(netblock.Addr(), ban.Expires.Add(-time.Nanosecond))
|
|
if !banned || got != ban {
|
|
t.Errorf("find during the ban gives %+v and %t, want %+v", got, banned, ban)
|
|
}
|
|
|
|
_, banned, _ = ledger.Find(netblock.Addr(), ban.Expires)
|
|
if banned {
|
|
t.Error("the ban did not end")
|
|
}
|
|
|
|
if notes := ledger.Bans(netblock)[0].Notes; notes != ban.Notes {
|
|
t.Errorf("the notes are %+v, want them unchanged, %+v", notes, ban.Notes)
|
|
}
|
|
}
|
|
|
|
func TestMaxBansDropsTheEarliestBanOfTheNetblockSeenLongestAgo(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rules := defaultRules()
|
|
rules.MaxBans = 3
|
|
ledger := bans.New(rules)
|
|
a := netip.MustParsePrefix("203.0.113.1/32")
|
|
b := netip.MustParsePrefix("203.0.113.2/32")
|
|
c := netip.MustParsePrefix("203.0.113.3/32")
|
|
d := netip.MustParsePrefix("2001:db8::/64")
|
|
now := midnight()
|
|
|
|
first, _ := ledger.BanForLimit(a, now, bans.Notes{})
|
|
ledger.BanForLimit(b, now, bans.Notes{})
|
|
ledger.BanForLimit(c, now, bans.Notes{})
|
|
|
|
// A request from a makes b the netblock seen longest ago, and its ban
|
|
// goes to make room for d's.
|
|
ledger.Check(a.Addr(), now)
|
|
ledger.BanForLimit(d, now, bans.Notes{})
|
|
wantBans(t, ledger, map[netip.Prefix]int{a: 1, b: 0, c: 1, d: 1})
|
|
|
|
// a is banned again once its ban has ended; c, seen longest ago, goes.
|
|
ledger.BanForLimit(a, first.Expires, bans.Notes{})
|
|
wantBans(t, ledger, map[netip.Prefix]int{a: 2, c: 0, d: 1})
|
|
|
|
// With d seen since, a is seen longest ago, and its earlier ban goes
|
|
// first.
|
|
ledger.Check(d.Addr(), first.Expires)
|
|
ledger.BanForLimit(b, first.Expires, bans.Notes{})
|
|
wantBans(t, ledger, map[netip.Prefix]int{a: 1, b: 1, d: 1})
|
|
|
|
if !ledger.Bans(a)[0].Start.Equal(first.Expires) {
|
|
t.Errorf("a kept its ban of %s, want the later one", ledger.Bans(a)[0].Start)
|
|
}
|
|
}
|
|
|
|
func TestFullLedgerDropsTheEarlierBanOfTheNetblockBannedAgain(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// With room for one ban, the netblock's ended ban goes to make room for
|
|
// its new one, whose notes still count it.
|
|
rules := defaultRules()
|
|
rules.MaxBans = 1
|
|
ledger := bans.New(rules)
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
|
|
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
|
second, _ := ledger.BanForLimit(netblock, first.Expires, bans.Notes{})
|
|
|
|
held := ledger.Bans(netblock)
|
|
if len(held) != 1 || held[0] != second ||
|
|
held[0].Notes.EarlierBans != (bans.EarlierBans{Limit: 1}) {
|
|
t.Errorf("the ledger holds %+v, want only the second ban, "+
|
|
"with 1 earlier ban for a limit", held)
|
|
}
|
|
}
|
|
|
|
func TestRequestDuringAnAttackBanMakesItPermanent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
notes := bans.Notes{RuleID: "env-file", Target: "path"}
|
|
|
|
ban, _ := ledger.BanForAttack(netblock, midnight(), notes)
|
|
if !ban.Expires.Equal(midnight().Add(7*day)) || ban.Cause != bans.CauseAttack ||
|
|
ban.Notes.RuleID != "env-file" || ledger.Made(bans.CauseAttack) != 1 ||
|
|
ledger.Made(bans.CauseLimit) != 0 {
|
|
t.Fatalf("the ban is %+v, with %d made for an attack and %d for a limit, "+
|
|
"want one for an attack, of seven days", ban,
|
|
ledger.Made(bans.CauseAttack), ledger.Made(bans.CauseLimit))
|
|
}
|
|
|
|
wantChanged(t, ledger, true)
|
|
|
|
// In observe mode the ban refuses nothing, and stays as it is, while
|
|
// Find tells that the request would have made it permanent.
|
|
got, _, wouldMakePermanent := ledger.Find(netblock.Addr(), midnight().Add(time.Hour))
|
|
if got.Permanent() || ledger.Bans(netblock)[0].Permanent() || !wouldMakePermanent {
|
|
t.Fatalf("a request found under the ban left it %+v, would have made it "+
|
|
"permanent %t, want it as it was, and true", got, wouldMakePermanent)
|
|
}
|
|
|
|
wantChanged(t, ledger, false)
|
|
|
|
// A request it refuses makes it permanent, says so, and makes
|
|
// bans.json due.
|
|
got, _, madePermanent := ledger.Check(netblock.Addr(), midnight().Add(time.Hour))
|
|
if !madePermanent || !got.Permanent() || !ledger.Bans(netblock)[0].Permanent() {
|
|
t.Fatalf("after a request during the ban, it is %+v, made permanent %t, "+
|
|
"want it made permanent", got, madePermanent)
|
|
}
|
|
|
|
wantChanged(t, ledger, true)
|
|
|
|
// The next request finds it permanent already.
|
|
_, banned, madePermanent := ledger.Check(netblock.Addr(), midnight().Add(100*365*day))
|
|
if !banned || madePermanent {
|
|
t.Errorf("a later request is banned %t, and made the ban permanent %t, "+
|
|
"want banned by the permanent ban", banned, madePermanent)
|
|
}
|
|
}
|
|
|
|
func TestAttackAfterAnAttackBanHasEndedBansPermanently(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
|
|
// A ban for a broken limit before does not count.
|
|
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
|
second, _ := ledger.BanForAttack(netblock, first.Expires, bans.Notes{})
|
|
|
|
if second.Expires.Sub(second.Start) != 7*day {
|
|
t.Fatalf("the first ban for an attack lasts %s, want 7 days",
|
|
second.Expires.Sub(second.Start))
|
|
}
|
|
|
|
// Once that has run out without a request, the netblock is served, and
|
|
// its next clear sign of attack bans it for good.
|
|
_, banned, _ := ledger.Check(netblock.Addr(), second.Expires)
|
|
if banned {
|
|
t.Fatal("the ban did not end")
|
|
}
|
|
|
|
// Its notes show the earlier ban for an attack that makes it permanent,
|
|
// beside the one for a limit.
|
|
third, _ := ledger.BanForAttack(netblock, second.Expires.Add(30*day), bans.Notes{})
|
|
if !third.Permanent() ||
|
|
third.Notes.EarlierBans != (bans.EarlierBans{Limit: 1, Attack: 1}) {
|
|
t.Errorf("the next ban for an attack is %+v, want a permanent one, "+
|
|
"with 1 earlier ban for a limit and 1 for an attack", third)
|
|
}
|
|
}
|
|
|
|
func TestWouldBanGivesTheBanWithoutMakingIt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
first, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{})
|
|
wantChanged(t, ledger, true)
|
|
|
|
// While the first ban lasts, none would be made.
|
|
during, would := ledger.WouldBanForAttack(netblock, midnight(), bans.Notes{})
|
|
if would || during != first {
|
|
t.Errorf("during the first ban, would ban %t with %+v, want false with %+v",
|
|
would, during, first)
|
|
}
|
|
|
|
// As it ends, a clear sign of attack would ban for seven days, and a
|
|
// limit broken again for three hours, but neither is made.
|
|
limitNotes := bans.Notes{Kind: "requests", Limit: 1, Window: "minute"}
|
|
attack, wouldAttack := ledger.WouldBanForAttack(netblock, first.Expires,
|
|
bans.Notes{RuleID: "git-dir"})
|
|
limit, wouldLimit := ledger.WouldBanForLimit(netblock, first.Expires, limitNotes)
|
|
|
|
if !wouldAttack || !attack.Expires.Equal(first.Expires.Add(7*day)) ||
|
|
attack.Reason != "matched the rule git-dir" || !wouldLimit ||
|
|
!limit.Expires.Equal(first.Expires.Add(3*time.Hour)) ||
|
|
limit.Reason != "requests per minute over the limit of 1" {
|
|
t.Errorf("would ban with %+v and %+v, want seven days for the attack and "+
|
|
"three hours for the limit", attack, limit)
|
|
}
|
|
|
|
if len(ledger.Bans(netblock)) != 1 || ledger.Made(bans.CauseLimit) != 1 ||
|
|
ledger.Made(bans.CauseAttack) != 0 {
|
|
t.Errorf("the ledger holds %+v, want the first ban alone", ledger.Bans(netblock))
|
|
}
|
|
|
|
wantChanged(t, ledger, false)
|
|
|
|
// The ban made is the one that would have been.
|
|
made, _ := ledger.BanForLimit(netblock, first.Expires, limitNotes)
|
|
if made != limit {
|
|
t.Errorf("the ban made is %+v, want %+v", made, limit)
|
|
}
|
|
}
|
|
|
|
func TestWouldBePermanentAnswersAsTheBanWouldBeMade(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
now := midnight()
|
|
|
|
// Five bans for a limit in a row, of 1, 3, 9, 27 and 81 hours, are not
|
|
// permanent. The sixth, of 243 hours, would be, while a first ban for
|
|
// an attack would not.
|
|
for i := range 5 {
|
|
if ledger.WouldBePermanent(netblock, now, bans.CauseLimit) {
|
|
t.Fatalf("ban %d for a limit would be permanent", i+1)
|
|
}
|
|
|
|
ban, _ := ledger.BanForLimit(netblock, now, bans.Notes{})
|
|
now = ban.Expires
|
|
}
|
|
|
|
if !ledger.WouldBePermanent(netblock, now, bans.CauseLimit) {
|
|
t.Error("the sixth ban for a limit would not be permanent")
|
|
}
|
|
|
|
if ledger.WouldBePermanent(netblock, now, bans.CauseAttack) {
|
|
t.Error("a first ban for an attack would be permanent")
|
|
}
|
|
|
|
// Once a first ban for an attack has ended, the next would be permanent.
|
|
attack, _ := ledger.BanForAttack(netblock, now, bans.Notes{})
|
|
if !ledger.WouldBePermanent(netblock, attack.Expires, bans.CauseAttack) {
|
|
t.Error("a second ban for an attack would not be permanent")
|
|
}
|
|
}
|
|
|
|
func TestAttackBanDoesNotLengthenTheNextBanForALimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
|
|
// Three times the seven days would be permanent; a limit broken as the
|
|
// ban for an attack ends bans for an hour, as a first broken limit does.
|
|
attack, _ := ledger.BanForAttack(netblock, midnight(), bans.Notes{})
|
|
limit, _ := ledger.BanForLimit(netblock, attack.Expires, bans.Notes{})
|
|
|
|
if limit.Expires.Sub(limit.Start) != time.Hour || limit.Cause != bans.CauseLimit {
|
|
t.Errorf("the ban for a limit is %+v, want one of an hour", limit)
|
|
}
|
|
|
|
// And a request during the ban for a limit leaves it as it is.
|
|
got, _, madePermanent := ledger.Check(netblock.Addr(), limit.Start)
|
|
if got.Permanent() || madePermanent {
|
|
t.Error("a request during a ban for a limit made it permanent")
|
|
}
|
|
}
|
|
|
|
func TestRequestTextsAreCutTo256Bytes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
long := strings.Repeat("a", 300)
|
|
request := bans.Request{
|
|
Time: midnight(), Method: long, Host: long, Path: long, Status: 403, UserAgent: long,
|
|
}
|
|
|
|
ban, _ := ledger.BanForLimit(netblock, midnight(), bans.Notes{Request: request})
|
|
|
|
cut := long[:256]
|
|
want := bans.Request{
|
|
Time: midnight(), Method: cut, Host: cut, Path: cut, Status: 403, UserAgent: cut,
|
|
}
|
|
|
|
if ban.Notes.Request != want || ledger.Bans(netblock)[0].Notes.Request != want {
|
|
t.Errorf("the notes keep %+v, want each text cut to 256 bytes", ban.Notes.Request)
|
|
}
|
|
}
|
|
|
|
func TestLookupFillsTheNotesOfTheNetblocksBansWithoutOne(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ledger := bans.New(defaultRules())
|
|
netblock := netip.MustParsePrefix("203.0.113.9/32")
|
|
other := netip.MustParsePrefix("198.51.100.7/32")
|
|
|
|
// A ban made with the client's lookup, one made before it came, after
|
|
// the first ended, and one on another netblock.
|
|
ledger.BanForLimit(netblock, midnight(), bans.Notes{
|
|
ASN: "AS64497", ASName: "Other Net", Country: "FR",
|
|
})
|
|
ledger.BanForLimit(netblock, midnight().Add(time.Hour), bans.Notes{})
|
|
ledger.BanForLimit(other, midnight(), bans.Notes{})
|
|
|
|
ledger.AddLookup(netblock, "AS64496", "Example Net", "DE")
|
|
|
|
held := ledger.Bans(netblock)
|
|
if len(held) != 2 {
|
|
t.Fatalf("%s has %d bans, want 2", netblock, len(held))
|
|
}
|
|
|
|
for i, want := range []bans.Notes{
|
|
{ASN: "AS64497", ASName: "Other Net", Country: "FR"},
|
|
{ASN: "AS64496", ASName: "Example Net", Country: "DE"},
|
|
} {
|
|
got := held[i].Notes
|
|
if got.ASN != want.ASN || got.ASName != want.ASName || got.Country != want.Country {
|
|
t.Errorf("ban %d's notes give %q, %q and %q, want %q, %q and %q", i+1,
|
|
got.ASN, got.ASName, got.Country, want.ASN, want.ASName, want.Country)
|
|
}
|
|
}
|
|
|
|
if notes := ledger.Bans(other)[0].Notes; notes.ASN != "" || notes.Country != "" {
|
|
t.Errorf("the ban on %s has %q and %q, want neither",
|
|
other, notes.ASN, notes.Country)
|
|
}
|
|
}
|
|
|
|
// defaultRules are the rules at the settings' defaults.
|
|
func defaultRules() bans.Rules {
|
|
return bans.Rules{
|
|
LimitBanDuration: time.Hour,
|
|
LimitBanRepeatWindow: day,
|
|
MaxBanDuration: 7 * day,
|
|
AttackBanDuration: 7 * day,
|
|
MaxBans: 5000,
|
|
}
|
|
}
|
|
|
|
// midnight is when the tests' first bans are made.
|
|
func midnight() time.Time {
|
|
return time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
|
|
}
|
|
|
|
// wantBans checks how many bans the ledger holds on each netblock.
|
|
func wantBans(t *testing.T, ledger *bans.Ledger, want map[netip.Prefix]int) {
|
|
t.Helper()
|
|
|
|
for netblock, count := range want {
|
|
got := len(ledger.Bans(netblock))
|
|
if got != count {
|
|
t.Errorf("%s has %d bans, want %d", netblock, got, count)
|
|
}
|
|
}
|
|
}
|