Files
smallwebwaf/internal/waf/biased.go
clawbot 4d4ae61abf
check / check (push) Waiting to run
Rename the settings whose names do not say what they control (closes #234)
Applies the setting renames of the naming audit under the SWW_WAF_ and
SWW_RPROXY_ prefixes, with sneak's names for the IP reputation cache
time and the two time limits, which now take milliseconds. The rproxy
log field default_disposition becomes waf_backend_no_answer, and
config.Config and config.FromEnvironment become config.WAF and
config.WAFFromEnvironment. Go fields, test names and prose follow. No
aliases; nothing reads the old names.

Model: opus-5-5
2026-11-12 09:48:09 +00:00

163 lines
5.1 KiB
Go

package waf
import (
"sneak.berlin/go/smallwebwaf/internal/config"
)
// whole is the percentage of each limit a client gets when no biased
// threshold lowers its limits.
const whole = 100
// percentage is a client's limit percentage for the rate limits or for
// the byte limits, as the biased thresholds give it, and the setting that
// gave it: "" with whole when none lowers that kind of limit.
type percentage struct {
percent int64
setting string
}
// biasedThresholdsSet reports whether a biased threshold can lower a
// client's limits by its lookup: SWW_WAF_ASN_BYTES_LIMIT_PERCENT or
// SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT is not empty,
// SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT is below 100, or
// SWW_WAF_ASN_LIMIT_PERCENT_URL is set. The client's lookup is then needed
// before its request goes on.
func biasedThresholdsSet(cfg *config.WAF) bool {
return len(cfg.ASNBytesLimitPercent) > 0 ||
len(cfg.CountryBytesLimitPercent) > 0 ||
cfg.UnknownCountryLimitPercent < whole ||
cfg.ASNLimitPercentURL != ""
}
// limitPercentages returns the client's limit percentages, for the rate
// limits and for the byte limits, by its tags, its AS number and country
// as looked up, each "" when unknown, and the blocklists, DNSBL zones and
// AbuseIPDB that list it. Each is the lowest of those the settings give
// it, the first of them in the order below when several are lowest: the
// percentage SWW_WAF_TAG_LIMIT_PERCENT gives its asn= tag, the one the file
// SWW_WAF_ASN_LIMIT_PERCENT_URL names gives its AS number, the one
// SWW_WAF_TAG_LIMIT_PERCENT gives its country= tag, the lowest it gives its
// other tags, for a client without a country, unless its request skipped
// the wait for GeoJS, SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT, for a client a blocklist
// lists, the percentage of SWW_WAF_BLOCKLIST_ACTION while it is limit, and
// for a client a DNSBL zone's verdict lists, or whose AbuseIPDB score is
// a hit, the percentage of SWW_WAF_DNSBL_ABUSEIPDB_ACTION while it is limit. For
// the byte limits, SWW_WAF_ASN_BYTES_LIMIT_PERCENT and
// SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT take the place of the first three for
// an AS number or a country they list.
func (rq *request) limitPercentages() (percentage, percentage) {
cfg := rq.h.config
asn, country := rq.line.ASN, rq.line.Country
unknown := percentage{percent: whole}
if country == "" && !rq.skippedLookupWait {
unknown = percentage{
cfg.UnknownCountryLimitPercent,
"SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT",
}
}
fetched := percentage{percent: whole}
if percent, listed := rq.h.lists.ASNLimitPercent(asn); listed {
fetched = percentage{percent, "SWW_WAF_ASN_LIMIT_PERCENT_URL"}
}
blocklisted := percentage{percent: whole}
if rq.blocklisted && cfg.BlocklistAction == "limit" {
blocklisted = percentage{
cfg.BlocklistLimitPercent,
"SWW_WAF_BLOCKLIST_ACTION",
}
}
dnsblAbuseIPDBListed := percentage{percent: whole}
if (rq.dnsblListed || rq.abuseIPDBHit) &&
cfg.DNSBLAbuseIPDBAction == "limit" {
dnsblAbuseIPDBListed = percentage{
cfg.DNSBLAbuseIPDBLimitPercent,
"SWW_WAF_DNSBL_ABUSEIPDB_ACTION",
}
}
const tagSetting = "SWW_WAF_TAG_LIMIT_PERCENT"
asnRequests := lowest(
given(cfg.TagLimitPercent, asnTag(asn), tagSetting),
fetched,
)
countryRequests := given(
cfg.TagLimitPercent,
countryTag(country),
tagSetting,
)
otherTags := percentage{percent: whole}
for _, tag := range rq.line.Tags {
if tag != asnTag(asn) && tag != countryTag(country) {
otherTags = lowest(
otherTags,
given(cfg.TagLimitPercent, tag, tagSetting),
)
}
}
asnBytes, countryBytes := asnRequests, countryRequests
if _, listed := cfg.ASNBytesLimitPercent[asn]; listed {
asnBytes = given(
cfg.ASNBytesLimitPercent,
asn,
"SWW_WAF_ASN_BYTES_LIMIT_PERCENT",
)
}
if _, listed := cfg.CountryBytesLimitPercent[country]; listed {
countryBytes = given(
cfg.CountryBytesLimitPercent,
country,
"SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT",
)
}
// The limits that lower requests and bytes alike.
common := lowest(otherTags, unknown, blocklisted, dnsblAbuseIPDBListed)
return lowest(asnRequests, countryRequests, common),
lowest(asnBytes, countryBytes, common)
}
// given returns the percentage percents, the setting named setting, gives
// code, an AS number, a country or a tag, or whole when it does not list code.
func given(percents map[string]int64, code, setting string) percentage {
percent, listed := percents[code]
if !listed {
return percentage{percent: whole}
}
return percentage{percent, setting}
}
// lowest returns the lowest of percentages below whole, the first of them
// when several are lowest, or whole when none is below it.
func lowest(percentages ...percentage) percentage {
low := percentage{percent: whole}
for _, p := range percentages {
if p.percent < low.percent {
low = p
}
}
return low
}
// logged returns p as the log line and the notes of a ban give it: its
// percent and setting, or nil and "" for whole, which they leave out.
func (p percentage) logged() (*int64, string) {
if p.percent == whole {
return nil, ""
}
return &p.percent, p.setting
}