Admin endpoints for bans and clients on the single listener (closes #27)
check / check (push) Successful in 4m26s
check / check (push) Successful in 4m26s
SWWAF_ADMIN_TOKEN, or its _FILE form, opens GET and POST /_smallwebwaf/bans, DELETE /_smallwebwaf/bans/<client> and GET /_smallwebwaf/clients/<ip>. Unset, they answer 404; a missing or wrong token gets 401, in observe mode too. They go through every check, as the metrics do. POST takes a netblock, not IPv4-mapped and without a zone, or a client's address, a duration or permanent, and a reason, and makes an admin ban even while another lasts. DELETE lifts every active ban covering the address, kept and marked lifted. Bans come back as bans.json entries; a client as clients.json holds it, with its bans. Judgement call: answers leave out bans.json's version field. Judgement call: DELETE takes an address, not a netblock. Rule suppressed: gosec G304 on a test reading bans.json. Model: opus-5-5
This commit was merged in pull request #92.
This commit is contained in:
+102
-6
@@ -154,7 +154,8 @@ type Ledger struct {
|
||||
// at most rules.MaxBans.
|
||||
held int
|
||||
// made is how many bans have been made since the start, by cause: by
|
||||
// the ledger, and by an admin in an edit of bans.json.
|
||||
// the ledger, and by an admin, through BanForAdmin or in an edit of
|
||||
// bans.json.
|
||||
made map[string]int
|
||||
// v4Lengths and v6Lengths are the lengths of the IPv4 and IPv6
|
||||
// netblocks that have been banned. Check looks for a ban at each of
|
||||
@@ -182,9 +183,9 @@ func New(rules Rules) *Ledger {
|
||||
}
|
||||
}
|
||||
|
||||
// Changed receives a value after a ban is made or made permanent, so that
|
||||
// bans.json can be written. Several changes before it is read leave one
|
||||
// value.
|
||||
// Changed receives a value after a ban is made, lifted or made permanent,
|
||||
// so that bans.json can be written. Several changes before it is read
|
||||
// leave one value.
|
||||
func (l *Ledger) Changed() <-chan struct{} {
|
||||
return l.changed
|
||||
}
|
||||
@@ -267,6 +268,81 @@ func (l *Ledger) BanForAttack(netblock netip.Prefix, now time.Time, notes Notes)
|
||||
return l.ban(netblock, now, CauseAttack, "matched the rule "+notes.RuleID, notes)
|
||||
}
|
||||
|
||||
// BanForAdmin bans netblock at now for an admin, with reason, until
|
||||
// expires, or for good when expires is zero, and returns the ban, whose
|
||||
// cause is CauseAdmin. Unlike BanForLimit and BanForAttack, it makes the
|
||||
// ban even while another on netblock is active, since the admin asked
|
||||
// for this one. The ledger fills in the notes' EarlierBans, and counts
|
||||
// the ban among those made.
|
||||
func (l *Ledger) BanForAdmin(
|
||||
netblock netip.Prefix, now, expires time.Time, reason string,
|
||||
) Ban {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
ban := Ban{
|
||||
Netblock: netblock.Masked(), Start: now, Expires: expires, Cause: CauseAdmin,
|
||||
Reason: reason,
|
||||
}
|
||||
|
||||
held, found := l.netblocks.Get(ban.Netblock)
|
||||
if found {
|
||||
ban.Notes.EarlierBans = earlierBans(*held)
|
||||
}
|
||||
|
||||
l.add(ban)
|
||||
l.made[CauseAdmin]++
|
||||
l.markChanged()
|
||||
|
||||
return ban
|
||||
}
|
||||
|
||||
// Lift lifts, at now, every ban active then on a netblock client is in,
|
||||
// as an admin does, and returns those bans. A lifted ban is kept, refuses
|
||||
// nothing, and does not make the netblock's next ban longer.
|
||||
func (l *Ledger) Lift(client netip.Addr, now time.Time) []Ban {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
var lifted []Ban
|
||||
|
||||
for _, bans := range l.covering(client) {
|
||||
for i := range *bans {
|
||||
ban := &(*bans)[i]
|
||||
if ban.ActiveAt(now) {
|
||||
ban.Lifted = now
|
||||
lifted = append(lifted, *ban)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(lifted) > 0 {
|
||||
l.markChanged()
|
||||
}
|
||||
|
||||
return lifted
|
||||
}
|
||||
|
||||
// Covering returns every ban held on a netblock client is in, active or
|
||||
// not, sorted by netblock, and each netblock's bans oldest first. It is
|
||||
// not a request from client, and leaves when the netblocks were last seen
|
||||
// unchanged.
|
||||
func (l *Ledger) Covering(client netip.Addr) []Ban {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
var held []Ban
|
||||
for _, bans := range l.covering(client) {
|
||||
held = append(held, *bans...)
|
||||
}
|
||||
|
||||
slices.SortStableFunc(held, func(a, b Ban) int {
|
||||
return a.Netblock.Compare(b.Netblock)
|
||||
})
|
||||
|
||||
return held
|
||||
}
|
||||
|
||||
// Bans returns the bans held on netblock, oldest first. It is not a
|
||||
// request from netblock, and leaves when it was last seen unchanged.
|
||||
func (l *Ledger) Bans(netblock netip.Prefix) []Ban {
|
||||
@@ -283,8 +359,8 @@ func (l *Ledger) Bans(netblock netip.Prefix) []Ban {
|
||||
|
||||
// Made returns how many bans for cause have been made since the start:
|
||||
// for CauseLimit and CauseAttack, by the ledger; for CauseAdmin, by an
|
||||
// admin in an edit of bans.json, as LoadEdit counts them. The bans read
|
||||
// from bans.json at the start are not among them.
|
||||
// admin, with BanForAdmin or in an edit of bans.json, as LoadEdit counts
|
||||
// them. The bans read from bans.json at the start are not among them.
|
||||
func (l *Ledger) Made(cause string) int {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
@@ -496,6 +572,26 @@ func (l *Ledger) active(client netip.Addr, now time.Time) *Ban {
|
||||
return nil
|
||||
}
|
||||
|
||||
// covering returns the bans of each netblock held that client is in,
|
||||
// leaving when the netblocks were last seen unchanged.
|
||||
func (l *Ledger) covering(client netip.Addr) []*[]Ban {
|
||||
lengths := l.v6Lengths
|
||||
if client.Is4() {
|
||||
lengths = l.v4Lengths
|
||||
}
|
||||
|
||||
var found []*[]Ban
|
||||
|
||||
for _, length := range lengths {
|
||||
bans, ok := l.netblocks.Peek(netip.PrefixFrom(client, length).Masked())
|
||||
if ok {
|
||||
found = append(found, bans)
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
// add adds ban to its netblock's bans, after the last, and makes its
|
||||
// netblock the most recently seen. With MaxBans held, it drops one first,
|
||||
// unless ban's cause is CauseAdmin, which does not count toward MaxBans.
|
||||
|
||||
Reference in New Issue
Block a user