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
|
||||
(https://git.eeqj.de/sneak/smallwebwaf/issues/13 and
|
||||
https://git.eeqj.de/sneak/smallwebwaf/issues/14), and so are two parts of
|
||||
milestone 3: the static lists, which come next in the build order, and the
|
||||
header size and the idle time as settings, which come last in it. `smallwebwaf`
|
||||
passes each request to the app and the app's answer back, unchanged, within its
|
||||
timeouts and size limits, works out each client's address, refuses a client that
|
||||
sends too many requests, comes from a country you refuse or from a network you
|
||||
refuse, lets the networks you choose through, and writes a JSON log line for
|
||||
every request. It comes as the image the app's own image is built on. The rest
|
||||
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).
|
||||
https://git.eeqj.de/sneak/smallwebwaf/issues/14), and so are the static lists,
|
||||
which come next in the build order. `smallwebwaf` passes each request to the app
|
||||
and the app's answer back, unchanged, within its timeouts and size limits, works
|
||||
out each client's address, refuses a client that sends too many requests, comes
|
||||
from a country you refuse or from a network you refuse, lets the networks you
|
||||
choose through, and writes a JSON log line for every request. It comes as the
|
||||
image the app's own image is built on. The rest 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
|
||||
|
||||
@@ -544,9 +542,8 @@ so that they run in minimal containers.
|
||||
|
||||
## TODO
|
||||
|
||||
- The rest of milestone 3, from the bans that broken request limits lead to
|
||||
through the metrics endpoint, and the rest of the design, in the order of the
|
||||
build order in [`SPEC.md`](SPEC.md).
|
||||
- The rest of milestone 3, after the static lists, and the rest of the design,
|
||||
in the order of the build order in [`SPEC.md`](SPEC.md).
|
||||
|
||||
## Documents
|
||||
|
||||
|
||||
@@ -293,8 +293,7 @@ it.
|
||||
needs: an alert destination, an account key, a token.
|
||||
- Every setting's name starts with `SWWAF_`, since `smallwebwaf` shares its
|
||||
container, and so its environment variables, with the app it protects.
|
||||
- Any limit or threshold can be switched off with the value `off`, except
|
||||
`SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES`.
|
||||
- Any limit or threshold can be switched off with the value `off`.
|
||||
- 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
|
||||
setting's name with `_FILE` added, such as `SWWAF_ADMIN_TOKEN_FILE`, for
|
||||
@@ -414,9 +413,7 @@ The settings, by group:
|
||||
headers, its body.
|
||||
- `SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES` (default `32K`): the largest
|
||||
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
|
||||
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.
|
||||
`431` and closes the connection, and nothing reaches the app.
|
||||
- `SWWAF_CLIENT_IDLE_TIMEOUT` (default `120s`): how long a kept-open
|
||||
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
|
||||
|
||||
@@ -110,7 +110,7 @@ var (
|
||||
errNotCountry = errors.New(
|
||||
"is not a two-letter country code such as de or kp")
|
||||
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
|
||||
@@ -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"),
|
||||
TrustedProxies: env.netblocks("SWWAF_TRUSTED_PROXIES", privateRanges),
|
||||
ClientRequestTimeout: env.duration("SWWAF_CLIENT_REQUEST_TIMEOUT", "60s"),
|
||||
ClientRequestHeaderMaxBytes: env.headerSize(
|
||||
ClientRequestHeaderMaxBytes: env.size(
|
||||
"SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES", "32K"),
|
||||
ClientIdleTimeout: env.duration("SWWAF_CLIENT_IDLE_TIMEOUT", "120s"),
|
||||
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", ""),
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if slices.Contains(cfg.DeniedCountries, country) {
|
||||
env.check("SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES",
|
||||
@@ -236,15 +243,6 @@ func (e *environment) size(name, defaultValue string) int64 {
|
||||
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.
|
||||
func (e *environment) count(name, defaultValue string) int64 {
|
||||
count, err := parseCount(e.value(name, defaultValue))
|
||||
@@ -316,19 +314,6 @@ func parseSize(value string) (int64, error) {
|
||||
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
|
||||
// stands for.
|
||||
func splitUnit(value string) (string, int64) {
|
||||
|
||||
@@ -185,31 +185,13 @@ func TestSizesAndOff(t *testing.T) {
|
||||
func TestRequestHeaderMaxBytesJustOver4K(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// 4K and off stop the start, as TestInvalidValueStopsTheStart shows.
|
||||
cfg := fromEnvironment(t, environment{clientHeaderMaxBytes: "4097"})
|
||||
if cfg.ClientRequestHeaderMaxBytes != 4097 {
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -260,6 +242,9 @@ func TestInvalidValueStopsTheStart(t *testing.T) {
|
||||
{denyNets, "198.51.100.0/24,"},
|
||||
{clientRequestTimeout, "60"},
|
||||
{clientRequestTimeout, ""},
|
||||
{clientHeaderMaxBytes, "4K"},
|
||||
{clientHeaderMaxBytes, off},
|
||||
{clientHeaderMaxBytes, "32KB"},
|
||||
{clientIdleTimeout, "0s"},
|
||||
{clientIdleTimeout, "2 minutes"},
|
||||
{clientResponseTimeout, "1y"},
|
||||
|
||||
Reference in New Issue
Block a user