Country allow and deny lists, looked up through GeoJS (closes #44)
check / check (push) Failing after 3s
check / check (push) Failing after 3s
SWWAF_DENIED_COUNTRIES and SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES refuse a request with 403 before its body is read or rate-limited, logged as country_denied. internal/lookup asks GeoJS only while a list is set, 200 clients per request, one at a time, keeping answers 7 days. Failures, a redirect or an answer leaving an address out included, are logged without addresses; GeoJS is then left alone a second, doubling to five minutes. Private, loopback and link-local clients are never sent. Deviation, per the issue: no SWWAF_LOOKUP_SOURCE or SWWAF_LOOKUP_TIMEOUT; 403, not SWWAF_BAN_RESPONSE. Deviation: GeoJS's country endpoint, not geo.json. Judgement call: an IPv6 /64 is asked about by its first address; at most 10,000 clients wait. Judgement call: config.go lists the ISO 3166-1 codes; no widely used library holds them. Model: opus-5-5
This commit was merged in pull request #54.
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -51,6 +52,13 @@ type Config struct {
|
||||
RateLimitPerMinute int64
|
||||
RateLimitPerHour int64
|
||||
RateLimitPerDay int64
|
||||
// DeniedCountries are the countries whose clients are refused
|
||||
// (SWWAF_DENIED_COUNTRIES). ExclusivelyAllowedCountries, when not
|
||||
// empty, are the only countries whose clients are let through
|
||||
// (SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES). Both hold two-letter codes in
|
||||
// capitals, as GeoJS gives them.
|
||||
DeniedCountries []string
|
||||
ExclusivelyAllowedCountries []string
|
||||
|
||||
// settings are the values read, as given or by default, for the
|
||||
// log line at start.
|
||||
@@ -84,6 +92,9 @@ var (
|
||||
errNotUpstreamURL = errors.New(
|
||||
"is not a URL with only a scheme, a host and an optional port, " +
|
||||
"such as http://127.0.0.1:8081")
|
||||
errNotCountry = errors.New(
|
||||
"is not a two-letter country code such as de or kp")
|
||||
errOnBothLists = errors.New("is in SWWAF_DENIED_COUNTRIES too")
|
||||
)
|
||||
|
||||
// FromEnvironment reads the settings with lookupEnv, normally
|
||||
@@ -104,6 +115,16 @@ func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
|
||||
RateLimitPerMinute: env.count("SWWAF_RATE_LIMIT_PER_MINUTE", "1000"),
|
||||
RateLimitPerHour: env.count("SWWAF_RATE_LIMIT_PER_HOUR", "10000"),
|
||||
RateLimitPerDay: env.count("SWWAF_RATE_LIMIT_PER_DAY", "50000"),
|
||||
DeniedCountries: env.countries("SWWAF_DENIED_COUNTRIES", ""),
|
||||
ExclusivelyAllowedCountries: env.countries(
|
||||
"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
||||
}
|
||||
|
||||
for _, country := range cfg.ExclusivelyAllowedCountries {
|
||||
if slices.Contains(cfg.DeniedCountries, country) {
|
||||
env.check("SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES",
|
||||
fmt.Errorf("%q %w", country, errOnBothLists))
|
||||
}
|
||||
}
|
||||
|
||||
if env.err != nil {
|
||||
@@ -201,6 +222,14 @@ func (e *environment) count(name, defaultValue string) int64 {
|
||||
return count
|
||||
}
|
||||
|
||||
// countries reads a setting that is a list of countries.
|
||||
func (e *environment) countries(name, defaultValue string) []string {
|
||||
countries, err := parseCountries(e.value(name, defaultValue))
|
||||
e.check(name, err)
|
||||
|
||||
return countries
|
||||
}
|
||||
|
||||
// parseDuration reads a duration in Go's syntax, such as 90s or 15m, a
|
||||
// whole number of days such as 7d, or off.
|
||||
func parseDuration(value string) (time.Duration, error) {
|
||||
@@ -348,6 +377,62 @@ func parseNetblock(value string) (netip.Prefix, error) {
|
||||
return netip.PrefixFrom(addr, addr.BitLen()), nil
|
||||
}
|
||||
|
||||
// countryCodes are the two-letter codes ISO 3166-1 assigns today, and XK,
|
||||
// the code in common use for Kosovo. golang.org/x/text/language cannot
|
||||
// check them: it also takes withdrawn codes such as su, and reserved ones
|
||||
// such as ac, as countries.
|
||||
const countryCodes = `
|
||||
AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ
|
||||
BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ
|
||||
CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ
|
||||
DE DJ DK DM DO DZ
|
||||
EC EE EG EH ER ES ET
|
||||
FI FJ FK FM FO FR
|
||||
GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY
|
||||
HK HM HN HR HT HU
|
||||
ID IE IL IM IN IO IQ IR IS IT
|
||||
JE JM JO JP
|
||||
KE KG KH KI KM KN KP KR KW KY KZ
|
||||
LA LB LC LI LK LR LS LT LU LV LY
|
||||
MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ
|
||||
NA NC NE NF NG NI NL NO NP NR NU NZ
|
||||
OM
|
||||
PA PE PF PG PH PK PL PM PN PR PS PT PW PY
|
||||
QA
|
||||
RE RO RS RU RW
|
||||
SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ
|
||||
TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ
|
||||
UA UG UM US UY UZ
|
||||
VA VC VE VG VI VN VU
|
||||
WF WS
|
||||
XK
|
||||
YE YT
|
||||
ZA ZM ZW
|
||||
`
|
||||
|
||||
// parseCountries reads a comma-separated list of country codes in either
|
||||
// case, and returns them in capitals.
|
||||
func parseCountries(value string) ([]string, error) {
|
||||
items, err := parseList(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
known := strings.Fields(countryCodes)
|
||||
countries := make([]string, 0, len(items))
|
||||
|
||||
for _, item := range items {
|
||||
country := strings.ToUpper(item)
|
||||
if !slices.Contains(known, country) {
|
||||
return nil, fmt.Errorf("%q %w", item, errNotCountry)
|
||||
}
|
||||
|
||||
countries = append(countries, country)
|
||||
}
|
||||
|
||||
return countries, nil
|
||||
}
|
||||
|
||||
// parseListenAddr checks an address to listen on: an optional host and a
|
||||
// port number.
|
||||
func parseListenAddr(value string) (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user