Lower limits for listed AS numbers and countries (closes #21)
check / check (push) Canceled after 0s

SWWAF_ASN_LIMIT_PERCENT and SWWAF_COUNTRY_LIMIT_PERCENT give the clients
of the AS numbers and countries they list that percentage of every rate
and byte limit, rounded down; SWWAF_ASN_BYTES_PERCENT and
SWWAF_COUNTRY_BYTES_PERCENT take its place for the byte limits of those
they list; SWWAF_UNKNOWN_LIMIT_PERCENT (100) covers clients without a
country. The lowest applies. While one lowers a limit, a request waits
for its client's lookup, and SWWAF_LOOKUP_SOURCE=off stops the start. Log
lines give limit_percent and bytes_percent with their settings; ban
notes, and so alerts, give the broken limit's.

Judgement call: a client without a country is unknown, whatever its AS number.
Judgement call: bytes_percent and its setting are log fields SPEC does not name.
Rule suppressed: funlen on FromEnvironment, one line per setting.

Model: opus-5-5
This commit was merged in pull request #103.
This commit is contained in:
2026-10-07 14:22:01 +02:00
parent f35e3ddfe8
commit 2421cdc273
17 changed files with 1102 additions and 109 deletions
+109 -4
View File
@@ -50,6 +50,11 @@ const (
addLookupHeaders = "SWWAF_ADD_LOOKUP_HEADERS"
deniedCountries = "SWWAF_DENIED_COUNTRIES"
allowedCountries = "SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES"
asnLimitPercent = "SWWAF_ASN_LIMIT_PERCENT"
countryLimitPercent = "SWWAF_COUNTRY_LIMIT_PERCENT"
asnBytesPercent = "SWWAF_ASN_BYTES_PERCENT"
countryBytesPercent = "SWWAF_COUNTRY_BYTES_PERCENT"
unknownLimitPercent = "SWWAF_UNKNOWN_LIMIT_PERCENT"
banResponse = "SWWAF_BAN_RESPONSE"
limitBanDuration = "SWWAF_LIMIT_BAN_DURATION"
limitBanRepeatWindow = "SWWAF_LIMIT_BAN_REPEAT_WINDOW"
@@ -892,9 +897,14 @@ func TestSettingNeedingLookupsStopsTheStartWhileTheyAreOff(t *testing.T) {
t.Parallel()
for name, value := range map[string]string{
deniedCountries: "kp",
allowedCountries: "de",
addLookupHeaders: enabled,
deniedCountries: "kp",
allowedCountries: "de",
addLookupHeaders: enabled,
asnLimitPercent: "AS64496:50",
countryLimitPercent: "cn:25",
asnBytesPercent: "AS64496:50",
countryBytesPercent: "cn:25",
unknownLimitPercent: "99",
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
@@ -909,12 +919,94 @@ func TestSettingNeedingLookupsStopsTheStartWhileTheyAreOff(t *testing.T) {
})
}
// Set empty, the country lists need nothing looked up.
// Set empty, the lists need nothing looked up, and nor does
// SWWAF_UNKNOWN_LIMIT_PERCENT at 100, which lowers no limit.
fromEnvironment(t, environment{
lookupSource: off, deniedCountries: "", allowedCountries: "",
asnLimitPercent: "", countryLimitPercent: "", asnBytesPercent: "",
countryBytesPercent: "", unknownLimitPercent: "100",
})
}
func TestBiasedThresholdsAsSet(t *testing.T) {
t.Parallel()
cfg := fromEnvironment(t, environment{})
if len(cfg.ASNLimitPercent) != 0 || len(cfg.CountryLimitPercent) != 0 ||
len(cfg.ASNBytesPercent) != 0 || len(cfg.CountryBytesPercent) != 0 ||
cfg.UnknownLimitPercent != 100 {
t.Errorf("biased thresholds %v, %v, %v, %v and %d by default, "+
"want four empty lists and 100", cfg.ASNLimitPercent, cfg.CountryLimitPercent,
cfg.ASNBytesPercent, cfg.CountryBytesPercent, cfg.UnknownLimitPercent)
}
// AS numbers and countries in either case, an AS number with leading
// zeros, 0 and 100.
cfg = fromEnvironment(t, environment{
asnLimitPercent: "AS14061:50, as16276:0,AS045102:100",
countryLimitPercent: "cn:25,RU:50",
asnBytesPercent: "as16276:75",
countryBytesPercent: "ru:10",
unknownLimitPercent: "0",
})
for name, tc := range map[string]struct{ got, want map[string]int64 }{
asnLimitPercent: {
cfg.ASNLimitPercent,
map[string]int64{"AS14061": 50, "AS16276": 0, "AS45102": 100},
},
countryLimitPercent: {cfg.CountryLimitPercent, map[string]int64{"CN": 25, "RU": 50}},
asnBytesPercent: {cfg.ASNBytesPercent, map[string]int64{"AS16276": 75}},
countryBytesPercent: {cfg.CountryBytesPercent, map[string]int64{"RU": 10}},
} {
if !maps.Equal(tc.got, tc.want) {
t.Errorf("%s gave %v, want %v", name, tc.got, tc.want)
}
}
if cfg.UnknownLimitPercent != 0 {
t.Errorf("%s gave %d, want 0", unknownLimitPercent, cfg.UnknownLimitPercent)
}
}
func TestInvalidBiasedThresholdStopsTheStartSayingWhatIsWrong(t *testing.T) {
t.Parallel()
const (
notASN = " is not an AS number such as AS64496"
notItem = " is not a code, : and a percentage, such as AS64496:50 or cn:25"
notPercent = " is not a percentage, a whole number from 0 to 100"
)
for _, tc := range []struct{ name, value, want string }{
{asnLimitPercent, "14061:50", `"14061"` + notASN},
{asnLimitPercent, "AS4294967296:50", `"AS4294967296"` + notASN},
{asnLimitPercent, "AS14061", `"AS14061"` + notItem},
{asnLimitPercent, "AS14061:101", `"101"` + notPercent},
{asnLimitPercent, "AS14061:50,as14061:25", `"as14061" is listed twice`},
{
countryLimitPercent, "nk:25",
`"nk" is not a two-letter country code such as de or kp`,
},
{countryLimitPercent, "cn:25,CN:50", `"CN" is listed twice`},
{asnBytesPercent, "AS14061:-1", `"-1"` + notPercent},
{countryBytesPercent, "cn:50%", `"50%"` + notPercent},
{unknownLimitPercent, "101", `"101"` + notPercent},
{unknownLimitPercent, off, `"off"` + notPercent},
} {
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
t.Parallel()
_, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv)
want := tc.name + ": " + tc.want
if err == nil || err.Error() != want {
t.Errorf("error %v, want %s", err, want)
}
})
}
}
func TestSizesAndOff(t *testing.T) {
t.Parallel()
@@ -1054,6 +1146,14 @@ func TestInvalidValueStopsTheStart(t *testing.T) {
{allowedCountries, "uk"},
{allowedCountries, "zz"},
{allowedCountries, "de,germany"},
{asnLimitPercent, "AS14061:50,,AS16276:50"}, {asnLimitPercent, "ASX:50"},
{asnLimitPercent, "AS14061:"}, {asnLimitPercent, "AS14061 :50"},
{asnLimitPercent, "AS14061:1.5"}, {asnLimitPercent, "AS-1:50"},
{countryLimitPercent, "cn"}, {countryLimitPercent, "cn:"},
{countryLimitPercent, "cn:25:50"}, {countryLimitPercent, "china:25"},
{asnBytesPercent, "AS14061:101"}, {countryBytesPercent, "su:50"},
{unknownLimitPercent, ""}, {unknownLimitPercent, "-1"},
{unknownLimitPercent, "50%"},
{metricsTopN, off}, {metricsTopN, "0"}, {metricsTopN, "-1"},
{logRequestHeaders, "accept,,origin"}, {logRequestHeaders, "accept;origin"},
{logRequestHeaders, "accept language"}, {logRequestHeaders, "x-foo:"},
@@ -1325,6 +1425,11 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
addLookupHeaders: "false",
deniedCountries: "",
allowedCountries: "",
asnLimitPercent: "",
countryLimitPercent: "",
asnBytesPercent: "",
countryBytesPercent: "",
unknownLimitPercent: "100",
banResponse: "403",
limitBanDuration: "1h",
limitBanRepeatWindow: "24h",