1 Commits
Author SHA1 Message Date
sneak c35c204e93 A ban's earlier_bans counts a ban dropped after an admin's ban (closes #128)
check / check (push) Canceled after 0s
earlierBans took the counts in the notes of the netblock's first ban
held, so a ban dropped after an admin's ban, which came first, went
uncounted. Each ban held now gives a count, its notes' counts plus one
for it and each ban held after it, and each cause takes the largest.
Counting from the newest ban alone would lose the bans before an
admin's ban added to bans.json without notes.

Model: opus-5-5
2026-10-20 09:45:46 +00:00
2 changed files with 48 additions and 10 deletions
+29
View File
@@ -61,6 +61,35 @@ func TestAdminsBansAreNeverDroppedAndDoNotCountTowardMaxBans(t *testing.T) {
wantBans(t, ledger, map[netip.Prefix]int{adminsOnly: 2, both: 1, second: 0, third: 1}) wantBans(t, ledger, map[netip.Prefix]int{adminsOnly: 2, both: 1, second: 0, third: 1})
} }
func TestEarlierBansCountABanDroppedAfterAnAdminsBan(t *testing.T) {
t.Parallel()
rules := defaultRules()
rules.MaxBans = 2
ledger := bans.New(rules)
netblock := netip.MustParsePrefix("203.0.113.1/32")
other := netip.MustParsePrefix("203.0.113.2/32")
// An admin bans the netblock for a minute, and it then breaks a limit
// twice, each time after its last ban has ended.
ledger.BanForAdmin(netblock, midnight(), midnight().Add(time.Minute),
"probes for logins")
first, _ := ledger.BanForLimit(netblock, midnight().Add(time.Minute), bans.Notes{})
second, _ := ledger.BanForLimit(netblock, first.Expires, bans.Notes{})
// A ban on another netblock fills the ledger, and the netblock's first
// ban for a limit, its earliest that is not an admin's, goes.
ledger.BanForLimit(other, second.Start, bans.Notes{})
wantBans(t, ledger, map[netip.Prefix]int{netblock: 2, other: 1})
// The netblock's next ban still counts it.
third, _ := ledger.BanForLimit(netblock, second.Expires, bans.Notes{})
if third.Notes.EarlierBans != (bans.EarlierBans{Limit: 2, Admin: 1}) {
t.Errorf("the third ban for a limit has earlier bans %+v, "+
"want 2 for a limit and 1 an admin's", third.Notes.EarlierBans)
}
}
func TestReasonOfTheBansSmallwebwafMakes(t *testing.T) { func TestReasonOfTheBansSmallwebwafMakes(t *testing.T) {
t.Parallel() t.Parallel()
+19 -10
View File
@@ -687,26 +687,35 @@ func (l *Ledger) ban(
} }
// earlierBans returns how many bans a netblock with the bans held, oldest // earlierBans returns how many bans a netblock with the bans held, oldest
// first, has had, by cause: the first ban held counts the bans the // first, has had, by cause. Each ban held gives a count: the bans its
// netblock had before that one, since dropped to make room, and each ban // notes count before it, and one for it and each ban held after it. A
// held adds one. // ban's count misses a ban dropped after it to make room, and that of an
// admin's ban added to bans.json without notes misses the bans before it,
// so each cause gets the largest of these counts.
func earlierBans(held []Ban) EarlierBans { func earlierBans(held []Ban) EarlierBans {
earlier := held[0].Notes.EarlierBans // fromHere counts ban and the bans held after it.
var most, fromHere EarlierBans
for _, ban := range held { for _, ban := range slices.Backward(held) {
switch ban.Cause { switch ban.Cause {
case CauseLimit: case CauseLimit:
earlier.Limit++ fromHere.Limit++
case CauseAttack: case CauseAttack:
earlier.Attack++ fromHere.Attack++
case CauseAdmin: case CauseAdmin:
earlier.Admin++ fromHere.Admin++
case CauseCrowdSec: case CauseCrowdSec:
earlier.CrowdSec++ fromHere.CrowdSec++
} }
notes := ban.Notes.EarlierBans
most.Limit = max(most.Limit, notes.Limit+fromHere.Limit)
most.Attack = max(most.Attack, notes.Attack+fromHere.Attack)
most.Admin = max(most.Admin, notes.Admin+fromHere.Admin)
most.CrowdSec = max(most.CrowdSec, notes.CrowdSec+fromHere.CrowdSec)
} }
return earlier return most
} }
// markChanged has Changed receive a value, unless one is waiting already. // markChanged has Changed receive a value, unless one is waiting already.