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_ASN_BYTES_PERCENT or // SWW_COUNTRY_BYTES_PERCENT is not empty, SWW_UNKNOWN_LIMIT_PERCENT is // below 100, or SWW_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_TAG_LIMIT_PERCENT gives its asn= tag, the one the file // SWW_ASN_LIMIT_PERCENT_URL names gives its AS number, the one // SWW_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_UNKNOWN_LIMIT_PERCENT, for a client a blocklist // lists, the percentage of SWW_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_REPUTATION_ACTION while it is limit. For // the byte limits, SWW_ASN_BYTES_PERCENT and SWW_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_UNKNOWN_LIMIT_PERCENT"} } fetched := percentage{percent: whole} if percent, listed := rq.h.lists.ASNLimitPercent(asn); listed { fetched = percentage{percent, "SWW_ASN_LIMIT_PERCENT_URL"} } blocklisted := percentage{percent: whole} if rq.blocklisted && cfg.BlocklistAction == "limit" { blocklisted = percentage{cfg.BlocklistLimitPercent, "SWW_BLOCKLIST_ACTION"} } reputationListed := percentage{percent: whole} if (rq.dnsblListed || rq.abuseIPDBHit) && cfg.ReputationAction == "limit" { reputationListed = percentage{cfg.ReputationLimitPercent, "SWW_REPUTATION_ACTION"} } const tagSetting = "SWW_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_ASN_BYTES_PERCENT") } if _, listed := cfg.CountryBytesPercent[country]; listed { countryBytes = given(cfg.CountryBytesPercent, country, "SWW_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 }