check / check (push) Failing after 3m27s
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_. Every place a name appeared changed: the code, the viper keys, tests, documents, examples, the README's settings table and the image files. Model: deepseek-v4-pro
121 lines
4.5 KiB
Go
121 lines
4.5 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: one of its lists 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.ASNLimitPercent) > 0 || len(cfg.CountryLimitPercent) > 0 ||
|
|
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 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_ASN_LIMIT_PERCENT gives its AS number, the one the file
|
|
// SWW_WAF_ASN_LIMIT_PERCENT_URL names gives it, the one
|
|
// SWW_WAF_COUNTRY_LIMIT_PERCENT gives its country, 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"}
|
|
}
|
|
|
|
asnRequests := lowest(given(cfg.ASNLimitPercent, asn, "SWW_WAF_ASN_LIMIT_PERCENT"),
|
|
fetched)
|
|
countryRequests := given(cfg.CountryLimitPercent, country,
|
|
"SWW_WAF_COUNTRY_LIMIT_PERCENT")
|
|
|
|
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, unknown, blocklisted, reputationListed),
|
|
lowest(asnBytes, countryBytes, unknown, blocklisted, reputationListed)
|
|
}
|
|
|
|
// given returns the percentage percents, the setting named setting, gives
|
|
// code, an AS number or a country, 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
|
|
}
|