check / check (push) Successful in 4m46s
Every setting of discriminator and rproxy is read through viper, in place of the os.LookupEnv function Main passed down. NewViper turns AutomaticEnv and AllowEmptyEnv on, so a setting that is set but empty reads as set. A setting may also be given as a file, the _FILE form, which rproxy's settings now have too; rproxy still writes nothing to disk. The settings are renamed. Discriminator's take the prefix SWW_WAF_, rproxy's take SWW_RPROXY_, and the shared SWW_COMMAND keeps SWW_. The Core Rule Set's settings take SWW_WAF_CORE_RULE_SET_ rather than SWW_WAF_WAF_. Model: deepseek-v4-pro (first version); opus-5-5 (rework)
131 lines
4.8 KiB
Go
131 lines
4.8 KiB
Go
package discriminator
|
|
|
|
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_PERCENT or
|
|
// SWW_WAF_COUNTRY_BYTES_PERCENT is not empty, SWW_WAF_UNKNOWN_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.Config) bool {
|
|
return len(cfg.ASNBytesPercent) > 0 || len(cfg.CountryBytesPercent) > 0 ||
|
|
cfg.UnknownLimitPercent < 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_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_REPUTATION_ACTION while it is limit. For
|
|
// the byte limits, SWW_WAF_ASN_BYTES_PERCENT and SWW_WAF_COUNTRY_BYTES_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.UnknownLimitPercent, "SWW_WAF_UNKNOWN_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.ReputationAction == "limit" {
|
|
reputationListed = percentage{cfg.ReputationLimitPercent, "SWW_WAF_REPUTATION_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.ASNBytesPercent[asn]; listed {
|
|
asnBytes = given(cfg.ASNBytesPercent, asn, "SWW_WAF_ASN_BYTES_PERCENT")
|
|
}
|
|
|
|
if _, listed := cfg.CountryBytesPercent[country]; listed {
|
|
countryBytes = given(cfg.CountryBytesPercent, country,
|
|
"SWW_WAF_COUNTRY_BYTES_PERCENT")
|
|
}
|
|
|
|
return lowest(asnRequests, countryRequests, otherTags, unknown, blocklisted,
|
|
reputationListed),
|
|
lowest(asnBytes, countryBytes, otherTags, unknown, blocklisted, reputationListed)
|
|
}
|
|
|
|
// 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
|
|
}
|