check / check (push) Waiting to run
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
163 lines
5.0 KiB
Go
163 lines
5.0 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",
|
|
}
|
|
}
|
|
|
|
reputationListed := percentage{percent: whole}
|
|
if (rq.dnsblListed || rq.abuseIPDBHit) &&
|
|
cfg.DNSBLAbuseIPDBAction == "limit" {
|
|
reputationListed = 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, reputationListed)
|
|
|
|
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
|
|
}
|