package proxy 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, // SWWAF_UNKNOWN_LIMIT_PERCENT is below 100, or SWWAF_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 SWWAF_ASN_LIMIT_PERCENT gives its AS number, the one the file // SWWAF_ASN_LIMIT_PERCENT_URL names gives it, the one // SWWAF_COUNTRY_LIMIT_PERCENT gives its country, for a client without a // country, SWWAF_UNKNOWN_LIMIT_PERCENT, for a client a blocklist lists, // the percentage of SWWAF_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 SWWAF_REPUTATION_ACTION while it is limit. For the // byte limits, SWWAF_ASN_BYTES_PERCENT and SWWAF_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 == "" { unknown = percentage{cfg.UnknownLimitPercent, "SWWAF_UNKNOWN_LIMIT_PERCENT"} } fetched := percentage{percent: whole} if percent, listed := rq.h.lists.ASNLimitPercent(asn); listed { fetched = percentage{percent, "SWWAF_ASN_LIMIT_PERCENT_URL"} } blocklisted := percentage{percent: whole} if rq.blocklisted && cfg.BlocklistAction == "limit" { blocklisted = percentage{cfg.BlocklistLimitPercent, "SWWAF_BLOCKLIST_ACTION"} } reputationListed := percentage{percent: whole} if (rq.dnsblListed || rq.abuseIPDBHit) && cfg.ReputationAction == "limit" { reputationListed = percentage{cfg.ReputationLimitPercent, "SWWAF_REPUTATION_ACTION"} } asnRequests := lowest(given(cfg.ASNLimitPercent, asn, "SWWAF_ASN_LIMIT_PERCENT"), fetched) countryRequests := given(cfg.CountryLimitPercent, country, "SWWAF_COUNTRY_LIMIT_PERCENT") asnBytes, countryBytes := asnRequests, countryRequests if _, listed := cfg.ASNBytesPercent[asn]; listed { asnBytes = given(cfg.ASNBytesPercent, asn, "SWWAF_ASN_BYTES_PERCENT") } if _, listed := cfg.CountryBytesPercent[country]; listed { countryBytes = given(cfg.CountryBytesPercent, country, "SWWAF_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 }