Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d7733bfaf |
@@ -13,15 +13,17 @@ JSON log line for every request.
|
|||||||
|
|
||||||
Status: the first two milestones are built
|
Status: the first two milestones are built
|
||||||
(https://git.eeqj.de/sneak/smallwebwaf/issues/13 and
|
(https://git.eeqj.de/sneak/smallwebwaf/issues/13 and
|
||||||
https://git.eeqj.de/sneak/smallwebwaf/issues/14), and so are the static lists,
|
https://git.eeqj.de/sneak/smallwebwaf/issues/14), and so are two parts of
|
||||||
which come next in the build order. `smallwebwaf` passes each request to the app
|
milestone 3: the static lists, which come next in the build order, and the
|
||||||
and the app's answer back, unchanged, within its timeouts and size limits, works
|
header size and the idle time as settings, which come last in it. `smallwebwaf`
|
||||||
out each client's address, refuses a client that sends too many requests, comes
|
passes each request to the app and the app's answer back, unchanged, within its
|
||||||
from a country you refuse or from a network you refuse, lets the networks you
|
timeouts and size limits, works out each client's address, refuses a client that
|
||||||
choose through, and writes a JSON log line for every request. It comes as the
|
sends too many requests, comes from a country you refuse or from a network you
|
||||||
image the app's own image is built on. The rest of the design comes after that,
|
refuse, lets the networks you choose through, and writes a JSON log line for
|
||||||
in the order of the build order in [`SPEC.md`](SPEC.md). The survey of existing
|
every request. It comes as the image the app's own image is built on. The rest
|
||||||
tools that led to the design is in [`EVALUATION.md`](EVALUATION.md).
|
of the design comes after that, in the order of the build order in
|
||||||
|
[`SPEC.md`](SPEC.md). The survey of existing tools that led to the design is in
|
||||||
|
[`EVALUATION.md`](EVALUATION.md).
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@@ -542,8 +544,9 @@ so that they run in minimal containers.
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- The rest of milestone 3, after the static lists, and the rest of the design,
|
- The rest of milestone 3, from the bans that broken request limits lead to
|
||||||
in the order of the build order in [`SPEC.md`](SPEC.md).
|
through the metrics endpoint, and the rest of the design, in the order of the
|
||||||
|
build order in [`SPEC.md`](SPEC.md).
|
||||||
|
|
||||||
## Documents
|
## Documents
|
||||||
|
|
||||||
|
|||||||
@@ -293,7 +293,8 @@ it.
|
|||||||
needs: an alert destination, an account key, a token.
|
needs: an alert destination, an account key, a token.
|
||||||
- Every setting's name starts with `SWWAF_`, since `smallwebwaf` shares its
|
- Every setting's name starts with `SWWAF_`, since `smallwebwaf` shares its
|
||||||
container, and so its environment variables, with the app it protects.
|
container, and so its environment variables, with the app it protects.
|
||||||
- Any limit or threshold can be switched off with the value `off`.
|
- Any limit or threshold can be switched off with the value `off`, except
|
||||||
|
`SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES`.
|
||||||
- A list set to an empty value is an empty list, and replaces the default.
|
- A list set to an empty value is an empty list, and replaces the default.
|
||||||
- Every setting may instead be given as a file holding the value, named by the
|
- Every setting may instead be given as a file holding the value, named by the
|
||||||
setting's name with `_FILE` added, such as `SWWAF_ADMIN_TOKEN_FILE`, for
|
setting's name with `_FILE` added, such as `SWWAF_ADMIN_TOKEN_FILE`, for
|
||||||
@@ -413,7 +414,9 @@ The settings, by group:
|
|||||||
headers, its body.
|
headers, its body.
|
||||||
- `SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES` (default `32K`): the largest
|
- `SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES` (default `32K`): the largest
|
||||||
request line and headers a client may send. Over it, `smallwebwaf` answers
|
request line and headers a client may send. Over it, `smallwebwaf` answers
|
||||||
`431` and closes the connection, and nothing reaches the app.
|
`431` and closes the connection, and nothing reaches the app. It must be
|
||||||
|
more than `4K`, and cannot be `off`: Go's HTTP server always has such a
|
||||||
|
limit, and reads 4 KiB past the one it is given before it refuses.
|
||||||
- `SWWAF_CLIENT_IDLE_TIMEOUT` (default `120s`): how long a kept-open
|
- `SWWAF_CLIENT_IDLE_TIMEOUT` (default `120s`): how long a kept-open
|
||||||
connection may wait for its next request before `smallwebwaf` closes it.
|
connection may wait for its next request before `smallwebwaf` closes it.
|
||||||
It is longer than the 90 seconds after which traefik, by default, closes a
|
It is longer than the 90 seconds after which traefik, by default, closes a
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ var (
|
|||||||
errNotCountry = errors.New(
|
errNotCountry = errors.New(
|
||||||
"is not a two-letter country code such as de or kp")
|
"is not a two-letter country code such as de or kp")
|
||||||
errOnBothLists = errors.New("is in SWWAF_DENIED_COUNTRIES too")
|
errOnBothLists = errors.New("is in SWWAF_DENIED_COUNTRIES too")
|
||||||
errNotOver4K = errors.New("must be more than 4K, and cannot be off")
|
errNotOver4K = errors.New("is not a size of more than 4K, such as 32K")
|
||||||
)
|
)
|
||||||
|
|
||||||
// FromEnvironment reads the settings with lookupEnv, normally
|
// FromEnvironment reads the settings with lookupEnv, normally
|
||||||
@@ -123,7 +123,7 @@ func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
|
|||||||
UpstreamURL: env.appURL("SWWAF_UPSTREAM_URL", "http://127.0.0.1:8081"),
|
UpstreamURL: env.appURL("SWWAF_UPSTREAM_URL", "http://127.0.0.1:8081"),
|
||||||
TrustedProxies: env.netblocks("SWWAF_TRUSTED_PROXIES", privateRanges),
|
TrustedProxies: env.netblocks("SWWAF_TRUSTED_PROXIES", privateRanges),
|
||||||
ClientRequestTimeout: env.duration("SWWAF_CLIENT_REQUEST_TIMEOUT", "60s"),
|
ClientRequestTimeout: env.duration("SWWAF_CLIENT_REQUEST_TIMEOUT", "60s"),
|
||||||
ClientRequestHeaderMaxBytes: env.size(
|
ClientRequestHeaderMaxBytes: env.headerSize(
|
||||||
"SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES", "32K"),
|
"SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES", "32K"),
|
||||||
ClientIdleTimeout: env.duration("SWWAF_CLIENT_IDLE_TIMEOUT", "120s"),
|
ClientIdleTimeout: env.duration("SWWAF_CLIENT_IDLE_TIMEOUT", "120s"),
|
||||||
ClientResponseTimeout: env.duration("SWWAF_CLIENT_RESPONSE_TIMEOUT", "30m"),
|
ClientResponseTimeout: env.duration("SWWAF_CLIENT_RESPONSE_TIMEOUT", "30m"),
|
||||||
@@ -142,13 +142,6 @@ func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
|
|||||||
"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
"SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go's server reads 4K past the limit it is given on the request line
|
|
||||||
// and headers before it refuses them, so proxy.New gives it this
|
|
||||||
// setting less 4K, which must leave a limit.
|
|
||||||
if cfg.ClientRequestHeaderMaxBytes <= 4*kibibyte {
|
|
||||||
env.check("SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES", errNotOver4K)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, country := range cfg.ExclusivelyAllowedCountries {
|
for _, country := range cfg.ExclusivelyAllowedCountries {
|
||||||
if slices.Contains(cfg.DeniedCountries, country) {
|
if slices.Contains(cfg.DeniedCountries, country) {
|
||||||
env.check("SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES",
|
env.check("SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES",
|
||||||
@@ -243,6 +236,15 @@ func (e *environment) size(name, defaultValue string) int64 {
|
|||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// headerSize reads the setting that is the largest request line and
|
||||||
|
// headers.
|
||||||
|
func (e *environment) headerSize(name, defaultValue string) int64 {
|
||||||
|
size, err := parseHeaderSize(e.value(name, defaultValue))
|
||||||
|
e.check(name, err)
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
// count reads a setting that is a number of requests.
|
// count reads a setting that is a number of requests.
|
||||||
func (e *environment) count(name, defaultValue string) int64 {
|
func (e *environment) count(name, defaultValue string) int64 {
|
||||||
count, err := parseCount(e.value(name, defaultValue))
|
count, err := parseCount(e.value(name, defaultValue))
|
||||||
@@ -314,6 +316,19 @@ func parseSize(value string) (int64, error) {
|
|||||||
return n * unit, nil
|
return n * unit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseHeaderSize reads the largest request line and headers: a size as
|
||||||
|
// parseSize reads it, but more than 4K and never off. Go's server reads 4K
|
||||||
|
// past the limit it is given before it refuses, so proxy.New gives it this
|
||||||
|
// size less 4K, which must leave a limit.
|
||||||
|
func parseHeaderSize(value string) (int64, error) {
|
||||||
|
size, err := parseSize(value)
|
||||||
|
if err != nil || size <= 4*kibibyte {
|
||||||
|
return 0, fmt.Errorf("%q %w", value, errNotOver4K)
|
||||||
|
}
|
||||||
|
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
|
|
||||||
// splitUnit splits a size into its number and the bytes its suffix
|
// splitUnit splits a size into its number and the bytes its suffix
|
||||||
// stands for.
|
// stands for.
|
||||||
func splitUnit(value string) (string, int64) {
|
func splitUnit(value string) (string, int64) {
|
||||||
|
|||||||
@@ -185,13 +185,31 @@ func TestSizesAndOff(t *testing.T) {
|
|||||||
func TestRequestHeaderMaxBytesJustOver4K(t *testing.T) {
|
func TestRequestHeaderMaxBytesJustOver4K(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// 4K and off stop the start, as TestInvalidValueStopsTheStart shows.
|
|
||||||
cfg := fromEnvironment(t, environment{clientHeaderMaxBytes: "4097"})
|
cfg := fromEnvironment(t, environment{clientHeaderMaxBytes: "4097"})
|
||||||
if cfg.ClientRequestHeaderMaxBytes != 4097 {
|
if cfg.ClientRequestHeaderMaxBytes != 4097 {
|
||||||
t.Errorf("4097 read as %d", cfg.ClientRequestHeaderMaxBytes)
|
t.Errorf("4097 read as %d", cfg.ClientRequestHeaderMaxBytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRequestHeaderMaxBytesRefusalNeverOffersOff(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, value := range []string{"32KB", "0", "4K", off} {
|
||||||
|
t.Run(value, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := config.FromEnvironment(
|
||||||
|
environment{clientHeaderMaxBytes: value}.lookupEnv)
|
||||||
|
|
||||||
|
want := clientHeaderMaxBytes + `: "` + value +
|
||||||
|
`" is not a size of more than 4K, such as 32K`
|
||||||
|
if err == nil || err.Error() != want {
|
||||||
|
t.Errorf("error %v, want %s", err, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRateLimitsOff(t *testing.T) {
|
func TestRateLimitsOff(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -242,9 +260,6 @@ func TestInvalidValueStopsTheStart(t *testing.T) {
|
|||||||
{denyNets, "198.51.100.0/24,"},
|
{denyNets, "198.51.100.0/24,"},
|
||||||
{clientRequestTimeout, "60"},
|
{clientRequestTimeout, "60"},
|
||||||
{clientRequestTimeout, ""},
|
{clientRequestTimeout, ""},
|
||||||
{clientHeaderMaxBytes, "4K"},
|
|
||||||
{clientHeaderMaxBytes, off},
|
|
||||||
{clientHeaderMaxBytes, "32KB"},
|
|
||||||
{clientIdleTimeout, "0s"},
|
{clientIdleTimeout, "0s"},
|
||||||
{clientIdleTimeout, "2 minutes"},
|
{clientIdleTimeout, "2 minutes"},
|
||||||
{clientResponseTimeout, "1y"},
|
{clientResponseTimeout, "1y"},
|
||||||
|
|||||||
Reference in New Issue
Block a user