check / check (push) Waiting to run
SWWAF_BLOCKLIST_URLS names lists of addresses and netblocks, fetched every SWWAF_BLOCKLIST_REFRESH (24h, never under 1h); an IPv4-mapped line stands for its IPv4 address or netblock. reputation.json keeps each list's last try, failed or not, even one cut off by a stop, which a restart waits on as a running instance does, and its last good copy, whole, used while a fetch fails. SWWAF_BLOCKLIST_ACTION denies, limits or only logs a listed client; the log line names the lists, each raises reputation_hit, and a failed fetch raises source_failure. SWWAF_ASN_LIMIT_PERCENT_URL is fetched the same way and counts as SWWAF_ASN_LIMIT_PERCENT does, the lower winning. Judgement call: a failed fetch is retried after the refresh, not sooner. Not done: ban notes do not name the lists yet. Model: opus-5-5
112 lines
4.0 KiB
Go
112 lines
4.0 KiB
Go
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 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, and for a client
|
|
// a blocklist lists, the percentage of SWWAF_BLOCKLIST_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"}
|
|
}
|
|
|
|
listed := percentage{percent: whole}
|
|
if len(rq.line.Reputation) > 0 && cfg.BlocklistAction == "limit" {
|
|
listed = percentage{cfg.BlocklistLimitPercent, "SWWAF_BLOCKLIST_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, listed),
|
|
lowest(asnBytes, countryBytes, unknown, listed)
|
|
}
|
|
|
|
// 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
|
|
}
|