check / check (push) Waiting to run
Every *.rules file in SWWAF_RULES_DIR is read at start and on each change, and each request is checked against the rules after the rate limits: log notes a match, block refuses with 403, ban refuses and bans the netblock for SWWAF_ATTACK_BAN_DURATION, made permanent by its next request or clear sign of attack. path, query and uri are matched as the request line sent them. bans.json gains each ban's cause, and ban notes count earlier bans by cause. The image ships 00-default.rules. Judgement call: a header sent twice is matched with its values joined by ", ". Judgement call: SWWAF_MAX_BAN_DURATION does not cap a ban for an attack. Not in this unit: offences for rule matches, with the error burst. Model: opus-5-5
123 lines
3.3 KiB
Go
123 lines
3.3 KiB
Go
package proxy
|
|
|
|
import (
|
|
"net/netip"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
|
)
|
|
|
|
// banResponse is a refusal answered with SWWAF_BAN_RESPONSE, and logged
|
|
// with action.
|
|
func (rq *request) banResponse(action string) *refusal {
|
|
return &refusal{status: rq.h.config.BanResponse, action: action}
|
|
}
|
|
|
|
// banned reports whether a ban on a netblock the client is in covers the
|
|
// request at now, and notes for the log line when that ban ends.
|
|
func (rq *request) banned(now time.Time) bool {
|
|
check := rq.h.ledger.Check
|
|
if rq.h.config.Observe {
|
|
check = rq.h.ledger.Find // in observe mode the ban refuses nothing
|
|
}
|
|
|
|
ban, banned := check(rq.client, now)
|
|
if banned {
|
|
rq.line.BanExpires = banExpires(ban)
|
|
}
|
|
|
|
return banned
|
|
}
|
|
|
|
// limitBroken counts the request for the rate limits at now, and reports
|
|
// whether it takes the client over one. In enforce mode such a request
|
|
// bans the client's netblock, and sets the client's counters back to
|
|
// zero; in observe mode it does neither.
|
|
func (rq *request) limitBroken(now time.Time) bool {
|
|
group := clientGroup(rq.client)
|
|
|
|
hit, over := rq.h.limiter.Count(group, now)
|
|
if !over {
|
|
return false
|
|
}
|
|
|
|
rq.line.LimitHit = hit.Window
|
|
rq.line.Offence = requestlog.OffenceLimit
|
|
|
|
if rq.h.config.Observe {
|
|
return true
|
|
}
|
|
|
|
netblock := rq.netblock()
|
|
ban := rq.h.ledger.BanForLimit(netblock, now, bans.Notes{
|
|
Country: rq.line.Country,
|
|
Limit: hit.Limit,
|
|
Window: hit.Window,
|
|
Count: hit.Requests,
|
|
Request: rq.noted(now),
|
|
Requests: rq.netblockRequests(netblock),
|
|
})
|
|
rq.h.limiter.Reset(group)
|
|
rq.line.BanExpires = banExpires(ban)
|
|
|
|
return true
|
|
}
|
|
|
|
// banForAttack bans the client's netblock at now for a clear sign of
|
|
// attack, the match of rule, a ban rule.
|
|
func (rq *request) banForAttack(now time.Time, rule rules.Rule) {
|
|
netblock := rq.netblock()
|
|
ban := rq.h.ledger.BanForAttack(netblock, now, bans.Notes{
|
|
Country: rq.line.Country,
|
|
RuleID: rule.ID,
|
|
Target: rule.Target,
|
|
Request: rq.noted(now),
|
|
Requests: rq.netblockRequests(netblock),
|
|
})
|
|
rq.line.BanExpires = banExpires(ban)
|
|
}
|
|
|
|
// noted is the request, refused at now with SWWAF_BAN_RESPONSE, as the
|
|
// notes of the ban it makes keep it.
|
|
func (rq *request) noted(now time.Time) bans.Request {
|
|
return bans.Request{
|
|
Time: now,
|
|
Method: rq.in.Method,
|
|
Host: rq.in.Host,
|
|
Path: rq.in.URL.RequestURI(),
|
|
Status: rq.h.config.BanResponse,
|
|
UserAgent: rq.in.UserAgent(),
|
|
}
|
|
}
|
|
|
|
// netblockRequests is how many requests netblock has sent since it was
|
|
// first seen, this one included: the histories count it only once it has
|
|
// ended.
|
|
func (rq *request) netblockRequests(netblock netip.Prefix) int64 {
|
|
return rq.h.limiter.Requests(netblock) + 1
|
|
}
|
|
|
|
// netblock is the netblock a ban on the client covers: its IPv4 address,
|
|
// widened to SWWAF_BAN_SCOPE_V4_PREFIX, or the IPv6 group clientGroup
|
|
// counts it in.
|
|
func (rq *request) netblock() netip.Prefix {
|
|
addr := rq.client.Unmap()
|
|
if addr.Is4() {
|
|
return netip.PrefixFrom(addr, rq.h.config.BanScopeV4Prefix).Masked()
|
|
}
|
|
|
|
return clientGroup(addr)
|
|
}
|
|
|
|
// banExpires is when ban ends, as the log line gives it: a time, or
|
|
// permanent.
|
|
func banExpires(ban bans.Ban) string {
|
|
if ban.Permanent() {
|
|
return "permanent"
|
|
}
|
|
|
|
return requestlog.FormatTime(ban.Expires)
|
|
}
|