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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+62
-62
@@ -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"},
|
||||||
)
|
)
|
||||||
@@ -542,7 +542,7 @@ 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.limit {
|
cfg.RefusalsLimitPerMinute != tc.limit {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
@@ -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{
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -844,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)
|
||||||
|
|||||||
@@ -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