Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0bb4abdce |
@@ -13,17 +13,15 @@ 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 two parts of
|
https://git.eeqj.de/sneak/smallwebwaf/issues/14), and so are the static lists,
|
||||||
milestone 3: the static lists, which come next in the build order, and the
|
which come next in the build order. `smallwebwaf` passes each request to the app
|
||||||
header size and the idle time as settings, which come last in it. `smallwebwaf`
|
and the app's answer back, unchanged, within its timeouts and size limits, works
|
||||||
passes each request to the app and the app's answer back, unchanged, within its
|
out each client's address, refuses a client that sends too many requests, comes
|
||||||
timeouts and size limits, works out each client's address, refuses a client that
|
from a country you refuse or from a network you refuse, lets the networks you
|
||||||
sends too many requests, comes from a country you refuse or from a network you
|
choose through, and writes a JSON log line for every request. It comes as the
|
||||||
refuse, lets the networks you choose through, and writes a JSON log line for
|
image the app's own image is built on. The rest of the design comes after that,
|
||||||
every request. It comes as the image the app's own image is built on. The rest
|
in the order of the build order in [`SPEC.md`](SPEC.md). The survey of existing
|
||||||
of the design comes after that, in the order of the build order in
|
tools that led to the design is in [`EVALUATION.md`](EVALUATION.md).
|
||||||
[`SPEC.md`](SPEC.md). The survey of existing tools that led to the design is in
|
|
||||||
[`EVALUATION.md`](EVALUATION.md).
|
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@@ -544,9 +542,8 @@ so that they run in minimal containers.
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- The rest of milestone 3, from the bans that broken request limits lead to
|
- The rest of milestone 3, after the static lists, and the rest of the design,
|
||||||
through the metrics endpoint, and the rest of the design, in the order of the
|
in the order of the build order in [`SPEC.md`](SPEC.md).
|
||||||
build order in [`SPEC.md`](SPEC.md).
|
|
||||||
|
|
||||||
## Documents
|
## Documents
|
||||||
|
|
||||||
|
|||||||
@@ -293,8 +293,7 @@ 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`, except
|
- Any limit or threshold can be switched off with the value `off`.
|
||||||
`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
|
||||||
@@ -414,9 +413,7 @@ 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. It must be
|
`431` and closes the connection, and nothing reaches the app.
|
||||||
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("is not a size of more than 4K, such as 32K")
|
errNotOver4K = errors.New("must be more than 4K, and cannot be off")
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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.headerSize(
|
ClientRequestHeaderMaxBytes: env.size(
|
||||||
"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,6 +142,13 @@ 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",
|
||||||
@@ -236,15 +243,6 @@ 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))
|
||||||
@@ -316,19 +314,6 @@ 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,31 +185,13 @@ 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()
|
||||||
|
|
||||||
@@ -260,6 +242,9 @@ 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