Rename discriminator to waf everywhere (closes #312)
check / check (push) Successful in 5m7s
check / check (push) Successful in 5m7s
The command is now `sww waf`, and the package is `internal/waf`. Types, functions, files, tests, comments, docs, the Dockerfile, the runit script, `script/`, compose examples, the log field `waf_line` and the `X-From-Waf` test header follow. No behaviour changes, and no setting changes name: rproxy already finds the waf through `SWRP_WAF_BACKEND`. Done with one `sed -i` (Discriminator and DISCRIMINATOR to WAF, discriminator to waf) over the files `git grep -i discriminator` listed, as sneak allowed for this rename, then `git mv` for the package and files. Model: opus-5-5
This commit is contained in:
1 parent
ad36dd1775
commit
3fc63a1e40
89 files changed
+1454
-1489
No files matched your search
+2
-2
@@ -155,7 +155,7 @@ RUN git checkout --quiet --detach b4b2c785308b1ce785b6155c7fe5f16879080193 \
|
|||||||
-o /usr/local/bin/runsvinit .
|
-o /usr/local/bin/runsvinit .
|
||||||
|
|
||||||
# The image, described under "Deployment" in SPEC.md. It runs rproxy unless
|
# The image, described under "Deployment" in SPEC.md. It runs rproxy unless
|
||||||
# SWW_COMMAND says discriminator, and an app's Dockerfile can build FROM it.
|
# SWW_COMMAND says waf, and an app's Dockerfile can build FROM it.
|
||||||
# It is the last stage, so a plain `docker build .` builds it.
|
# It is the last stage, so a plain `docker build .` builds it.
|
||||||
#
|
#
|
||||||
# ubuntu 26.04, 2026-09-27
|
# ubuntu 26.04, 2026-09-27
|
||||||
@@ -223,7 +223,7 @@ RUN groupadd --system --gid 65532 sww \
|
|||||||
|
|
||||||
# The state files' directory, SWW_STATE_DIR by default, where a volume
|
# The state files' directory, SWW_STATE_DIR by default, where a volume
|
||||||
# is mounted to keep them across deploys. The run script gives it to the
|
# is mounted to keep them across deploys. The run script gives it to the
|
||||||
# sww user at each start of discriminator.
|
# sww user at each start of waf.
|
||||||
RUN mkdir /var/lib/sww
|
RUN mkdir /var/lib/sww
|
||||||
|
|
||||||
# The default rule file, in SWW_RULES_DIR by default, where a Dockerfile
|
# The default rule file, in SWW_RULES_DIR by default, where a Dockerfile
|
||||||
|
|||||||
@@ -11,16 +11,15 @@ refuses the ones that look like abuse. `smallwebwaf` is one program, `sww`,
|
|||||||
shipped in one image, with two commands that do this together:
|
shipped in one image, with two commands that do this together:
|
||||||
|
|
||||||
- `sww rproxy` is a small reverse proxy in front of one app. traefik routes to
|
- `sww rproxy` is a small reverse proxy in front of one app. traefik routes to
|
||||||
it. It asks `discriminator` about each request, passes the allowed ones to the
|
it. It asks `waf` about each request, passes the allowed ones to the app, and
|
||||||
app, and passes the app's answers back. It keeps nothing and never touches
|
passes the app's answers back. It keeps nothing and never touches disk.
|
||||||
disk.
|
- `sww waf` decides. It runs the checks and keeps the bans, the counters and
|
||||||
- `sww discriminator` decides. It runs the checks and keeps the bans, the
|
each client's history. One `waf` serves many sites: a client banned through
|
||||||
counters and each client's history. One `discriminator` serves many sites: a
|
one site is refused on all of them, and its requests on all of them count
|
||||||
client banned through one site is refused on all of them, and its requests on
|
toward one set of limits.
|
||||||
all of them count toward one set of limits.
|
|
||||||
|
|
||||||
The site keeps answering while `discriminator` is down, slow or restarting:
|
The site keeps answering while `waf` is down, slow or restarting: without an
|
||||||
without an answer in time, `rproxy` passes the request to the app.
|
answer in time, `rproxy` passes the request to the app.
|
||||||
|
|
||||||
It protects against:
|
It protects against:
|
||||||
|
|
||||||
@@ -80,11 +79,11 @@ make docker
|
|||||||
|
|
||||||
Push the image to a registry your hosts pull from, and name it there by digest.
|
Push the image to a registry your hosts pull from, and name it there by digest.
|
||||||
|
|
||||||
The image runs `rproxy` by default. To run `discriminator` from the same image,
|
The image runs `rproxy` by default. To run `waf` from the same image, set the
|
||||||
set the container's `SWW_COMMAND` to `discriminator`. The image's health check
|
container's `SWW_COMMAND` to `waf`. The image's health check follows the
|
||||||
follows the command: in `rproxy`'s container it passes once `rproxy` answers and
|
command: in `rproxy`'s container it passes once `rproxy` answers and the app
|
||||||
the app accepts connections, whether or not `discriminator` answers; in
|
accepts connections, whether or not `waf` answers; in `waf`'s container, once
|
||||||
`discriminator`'s container, once `discriminator` answers.
|
`waf` answers.
|
||||||
|
|
||||||
### The usual setup: three containers
|
### The usual setup: three containers
|
||||||
|
|
||||||
@@ -92,24 +91,24 @@ The usual setup is three containers on one private network:
|
|||||||
|
|
||||||
- the app;
|
- the app;
|
||||||
- `rproxy`, in front of the app, which traefik routes to: `SWRP_WAF_BACKEND`
|
- `rproxy`, in front of the app, which traefik routes to: `SWRP_WAF_BACKEND`
|
||||||
names `discriminator` and `SWRP_UPSTREAM_URL` names the app;
|
names `waf` and `SWRP_UPSTREAM_URL` names the app;
|
||||||
- `discriminator`, with `SWW_COMMAND=discriminator` and a volume at
|
- `waf`, with `SWW_COMMAND=waf` and a volume at `/var/lib/sww` for its state
|
||||||
`/var/lib/sww` for its state files.
|
files.
|
||||||
|
|
||||||
Point traefik's labels at port 8080 of `rproxy`
|
Point traefik's labels at port 8080 of `rproxy`
|
||||||
(`traefik.http.services.<name>.loadbalancer.server.port=8080`). Publish no port
|
(`traefik.http.services.<name>.loadbalancer.server.port=8080`). Publish no port
|
||||||
of the app or of `discriminator`. Anything on that network can reach the app
|
of the app or of `waf`. Anything on that network can reach the app around
|
||||||
around `rproxy`, and can ask `discriminator` questions, so the network holds
|
`rproxy`, and can ask `waf` questions, so the network holds only what you trust.
|
||||||
only what you trust. Each further app adds its own container and its own
|
Each further app adds its own container and its own `rproxy`, whose
|
||||||
`rproxy`, whose `SWRP_WAF_BACKEND` names the same `discriminator`. The "Smallest
|
`SWRP_WAF_BACKEND` names the same `waf`. The "Smallest setup" example below
|
||||||
setup" example below shows this shape.
|
shows this shape.
|
||||||
|
|
||||||
### `rproxy` in the app's container
|
### `rproxy` in the app's container
|
||||||
|
|
||||||
An app's image can instead be built on the `smallwebwaf` image, so that `rproxy`
|
An app's image can instead be built on the `smallwebwaf` image, so that `rproxy`
|
||||||
runs beside the app in one container. `discriminator` still runs in a container
|
runs beside the app in one container. `waf` still runs in a container of its
|
||||||
of its own. The app's `Dockerfile` starts `FROM` the `smallwebwaf` image, named
|
own. The app's `Dockerfile` starts `FROM` the `smallwebwaf` image, named by
|
||||||
by digest, and adds:
|
digest, and adds:
|
||||||
|
|
||||||
- the app's binary;
|
- the app's binary;
|
||||||
- the packages the app needs, from the nixpkgs in the image;
|
- the packages the app needs, from the nixpkgs in the image;
|
||||||
@@ -169,26 +168,25 @@ does.
|
|||||||
- A log line with `"msg":"starting"` gives the version, the address and every
|
- A log line with `"msg":"starting"` gives the version, the address and every
|
||||||
setting in effect, with secrets masked.
|
setting in effect, with secrets masked.
|
||||||
- A request to the site through traefik gets the app's answer.
|
- A request to the site through traefik gets the app's answer.
|
||||||
`https://<site>/_sww/healthz` answers `ok`. `discriminator`'s log lines about
|
`https://<site>/_sww/healthz` answers `ok`. `waf`'s log lines about the
|
||||||
the request have `"action":"forward"`.
|
request have `"action":"forward"`.
|
||||||
- To see a refusal without banning yourself, run `discriminator` with
|
- To see a refusal without banning yourself, run `waf` with `SWW_MODE=observe`
|
||||||
`SWW_MODE=observe` and ask for `/.env`. The request reaches the app.
|
and ask for `/.env`. The request reaches the app. `waf`'s log lines have
|
||||||
`discriminator`'s log lines have `"would_action":"temporarily_banned"` and
|
`"would_action":"temporarily_banned"` and `"rule_ids":["env-file"]`. A ban you
|
||||||
`"rule_ids":["env-file"]`. A ban you get in `enforce` mode is lifted as
|
get in `enforce` mode is lifted as "Clients and bans" below describes.
|
||||||
"Clients and bans" below describes.
|
|
||||||
- With `SWW_METRICS_TOKEN` set, this answers with the metrics, from a host on
|
- With `SWW_METRICS_TOKEN` set, this answers with the metrics, from a host on
|
||||||
`discriminator`'s network:
|
`waf`'s network:
|
||||||
`curl -H "Authorization: Bearer $TOKEN" http://discriminator:8080/_sww/metrics`.
|
`curl -H "Authorization: Bearer $TOKEN" http://waf:8080/_sww/metrics`.
|
||||||
- `make example-app` builds the image and an app on it, runs them, and checks
|
- `make example-app` builds the image and an app on it, runs them, and checks
|
||||||
the whole exchange (see "Entrypoints").
|
the whole exchange (see "Entrypoints").
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Every setting is an environment variable, read once at start; changing one means
|
Every setting is an environment variable, read once at start; changing one means
|
||||||
restarting the container. `rproxy` reads only the `SWRP_` settings, and
|
restarting the container. `rproxy` reads only the `SWRP_` settings, and `waf`
|
||||||
`discriminator` only the `SWW_` ones. Every setting but `SWRP_WAF_BACKEND` has a
|
only the `SWW_` ones. Every setting but `SWRP_WAF_BACKEND` has a default, chosen
|
||||||
default, chosen for a service on the open internet. A setting that needs an
|
for a service on the open internet. A setting that needs an account, a key, a
|
||||||
account, a key, a token or an alert destination stays off until you give one.
|
token or an alert destination stays off until you give one.
|
||||||
|
|
||||||
- Any `SWW_` setting can be given as a file instead, for secrets and long lists:
|
- Any `SWW_` setting can be given as a file instead, for secrets and long lists:
|
||||||
`SWW_ADMIN_TOKEN_FILE` names the file that holds `SWW_ADMIN_TOKEN`. The file's
|
`SWW_ADMIN_TOKEN_FILE` names the file that holds `SWW_ADMIN_TOKEN`. The file's
|
||||||
@@ -222,23 +220,23 @@ Every setting the code reads, with its default:
|
|||||||
|
|
||||||
| Variable | Default | What it does |
|
| Variable | Default | What it does |
|
||||||
| ---------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `SWW_COMMAND` | `rproxy` | Which command the image runs, and its health check checks: `rproxy` or `discriminator`. |
|
| `SWW_COMMAND` | `rproxy` | Which command the image runs, and its health check checks: `rproxy` or `waf`. |
|
||||||
| `SWRP_WAF_BACKEND` | required | `discriminator`'s URL, such as `http://discriminator:8080`. |
|
| `SWRP_WAF_BACKEND` | required | `waf`'s URL, such as `http://waf:8080`. |
|
||||||
| `SWRP_UPSTREAM_URL` | `http://127.0.0.1:8081` | The app's URL: a scheme, a host and an optional port, nothing more. |
|
| `SWRP_UPSTREAM_URL` | `http://127.0.0.1:8081` | The app's URL: a scheme, a host and an optional port, nothing more. |
|
||||||
| `SWRP_LISTEN_ADDR` | `:8080` | Where `rproxy` listens; leave the host part empty. |
|
| `SWRP_LISTEN_ADDR` | `:8080` | Where `rproxy` listens; leave the host part empty. |
|
||||||
| `SWRP_TRUSTED_PROXIES` | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | The netblocks whose `X-Forwarded-For` `rproxy` believes; set but empty, it believes none. |
|
| `SWRP_TRUSTED_PROXIES` | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | The netblocks whose `X-Forwarded-For` `rproxy` believes; set but empty, it believes none. |
|
||||||
| `SWRP_TIMEOUT_MS` | `2000` | How long `rproxy` waits for `discriminator`'s answer to a question and to a report, in milliseconds. |
|
| `SWRP_TIMEOUT_MS` | `2000` | How long `rproxy` waits for `waf`'s answer to a question and to a report, in milliseconds. |
|
||||||
| `SWRP_DEFAULT_DISPOSITION` | `allow` | What `rproxy` does with a request when `discriminator` cannot be reached or does not answer in time: `allow` passes it to the app, `deny` answers `503`. |
|
| `SWRP_DEFAULT_DISPOSITION` | `allow` | What `rproxy` does with a request when `waf` cannot be reached or does not answer in time: `allow` passes it to the app, `deny` answers `503`. |
|
||||||
| `SWRP_MAX_REQUEST_SIZE` | `100M` | The largest request body, a size or `off`; a larger one is answered `413`. |
|
| `SWRP_MAX_REQUEST_SIZE` | `100M` | The largest request body, a size or `off`; a larger one is answered `413`. |
|
||||||
| `SWRP_MAX_RESPONSE_SIZE` | `1G` | The largest response body, a size or `off`. The visitor gets `502` in place of a larger one, or it is cut off if it has already started. |
|
| `SWRP_MAX_RESPONSE_SIZE` | `1G` | The largest response body, a size or `off`. The visitor gets `502` in place of a larger one, or it is cut off if it has already started. |
|
||||||
| `SWRP_MAX_REQUEST_TIME_MS` | `60000` | Two time limits, each this long, in milliseconds or `off`: one for the visitor to send its request line and headers, then one for the whole request to reach the app. |
|
| `SWRP_MAX_REQUEST_TIME_MS` | `60000` | Two time limits, each this long, in milliseconds or `off`: one for the visitor to send its request line and headers, then one for the whole request to reach the app. |
|
||||||
| `SWRP_MAX_RESPONSE_TIME_MS` | `1800000` | How long the answer may take, from the end of the request to its last byte reaching the visitor, in milliseconds or `off`. |
|
| `SWRP_MAX_RESPONSE_TIME_MS` | `1800000` | How long the answer may take, from the end of the request to its last byte reaching the visitor, in milliseconds or `off`. |
|
||||||
| `SWW_LISTEN_ADDR` | `:8080` | Where `discriminator` listens; leave the host part empty. |
|
| `SWW_LISTEN_ADDR` | `:8080` | Where `waf` listens; leave the host part empty. |
|
||||||
| `SWW_INSTANCE_NAME` | the container's host name | The name in every line `discriminator` logs, and in every alert and metric, such as `fsn1/discriminator`. |
|
| `SWW_INSTANCE_NAME` | the container's host name | The name in every line `waf` logs, and in every alert and metric, such as `fsn1/waf`. |
|
||||||
| `SWW_MODE` | `enforce` | `observe` refuses nothing and makes no ban; each log line says what `enforce` would have done. |
|
| `SWW_MODE` | `enforce` | `observe` refuses nothing and makes no ban; each log line says what `enforce` would have done. |
|
||||||
| `SWW_IPV6_GROUP_PREFIX` | `64` | The prefix length of the IPv6 netblock that counts as one client, from 32 to 128. |
|
| `SWW_IPV6_GROUP_PREFIX` | `64` | The prefix length of the IPv6 netblock that counts as one client, from 32 to 128. |
|
||||||
| `SWW_MAX_TRACKED_CLIENTS` | `20000` | The most clients kept, in memory and in `clients.json`; the least recently seen is dropped first. |
|
| `SWW_MAX_TRACKED_CLIENTS` | `20000` | The most clients kept, in memory and in `clients.json`; the least recently seen is dropped first. |
|
||||||
| `SWW_MAX_BANS` | `5000` | The most bans `discriminator` keeps of its own; bans an admin made do not count. |
|
| `SWW_MAX_BANS` | `5000` | The most bans `waf` keeps of its own; bans an admin made do not count. |
|
||||||
| `SWW_ALLOW_NETS` | empty | Netblocks whose clients skip every check, such as your monitoring. |
|
| `SWW_ALLOW_NETS` | empty | Netblocks whose clients skip every check, such as your monitoring. |
|
||||||
| `SWW_RATE_LIMIT_EXEMPT_NETS` | empty | Netblocks whose clients are exempt from rate limiting and the byte limits. |
|
| `SWW_RATE_LIMIT_EXEMPT_NETS` | empty | Netblocks whose clients are exempt from rate limiting and the byte limits. |
|
||||||
| `SWW_DENY_NETS` | empty | Netblocks whose clients are refused. |
|
| `SWW_DENY_NETS` | empty | Netblocks whose clients are refused. |
|
||||||
@@ -296,9 +294,9 @@ Every setting the code reads, with its default:
|
|||||||
| `SWW_STATE_DIR` | `/var/lib/sww` | The directory of the state files, an absolute path. |
|
| `SWW_STATE_DIR` | `/var/lib/sww` | The directory of the state files, an absolute path. |
|
||||||
| `SWW_STATE_WRITE_DELAY` | `10s` | How long `bans.json` waits to be written after a ban is made, lifted or made permanent. |
|
| `SWW_STATE_WRITE_DELAY` | `10s` | How long `bans.json` waits to be written after a ban is made, lifted or made permanent. |
|
||||||
| `SWW_STATE_COUNTER_INTERVAL` | `15m` | How often every state file is written. |
|
| `SWW_STATE_COUNTER_INTERVAL` | `15m` | How often every state file is written. |
|
||||||
| `SWW_LOG_LEVEL` | `info` | The least severe of `discriminator`'s own messages that are written: `debug`, `info`, `warn` or `error`; request lines are always written. |
|
| `SWW_LOG_LEVEL` | `info` | The least severe of `waf`'s own messages that are written: `debug`, `info`, `warn` or `error`; request lines are always written. |
|
||||||
| `SWW_LOG_REQUEST_HEADERS` | `accept,accept-language,accept-encoding,content-type,origin,range` | The request headers whose values the request log records; `Authorization` and `Cookie` are never recorded. |
|
| `SWW_LOG_REQUEST_HEADERS` | `accept,accept-language,accept-encoding,content-type,origin,range` | The request headers whose values the request log records; `Authorization` and `Cookie` are never recorded. |
|
||||||
| `SWW_LOG_REMOTE_URL` | unset | A syslog server that also gets every line `discriminator` writes: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://` with a host and a port. |
|
| `SWW_LOG_REMOTE_URL` | unset | A syslog server that also gets every line `waf` writes: `syslog+udp://`, `syslog+tcp://` or `syslog+tls://` with a host and a port. |
|
||||||
| `SWW_LOG_REMOTE_TLS_CA_FILE` | unset | A file of PEM certificates a `syslog+tls` server's certificate must chain to, instead of the host's own. |
|
| `SWW_LOG_REMOTE_TLS_CA_FILE` | unset | A file of PEM certificates a `syslog+tls` server's certificate must chain to, instead of the host's own. |
|
||||||
| `SWW_LOG_REMOTE_BUFFER` | `10000` | The most lines held while they wait to be sent; past it, the oldest is dropped. |
|
| `SWW_LOG_REMOTE_BUFFER` | `10000` | The most lines held while they wait to be sent; past it, the oldest is dropped. |
|
||||||
| `SWW_LOG_REMOTE_FACILITY` | `local0` | The syslog facility of each record sent. |
|
| `SWW_LOG_REMOTE_FACILITY` | `local0` | The syslog facility of each record sent. |
|
||||||
@@ -351,8 +349,8 @@ own, where each reaches the others by service name. Each example publishes
|
|||||||
|
|
||||||
### Smallest setup
|
### Smallest setup
|
||||||
|
|
||||||
One app, `rproxy` in front of it, and `discriminator` with a volume for its
|
One app, `rproxy` in front of it, and `waf` with a volume for its state files,
|
||||||
state files, every setting at its default.
|
every setting at its default.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
@@ -363,20 +361,20 @@ services:
|
|||||||
rproxy:
|
rproxy:
|
||||||
image: smallwebwaf
|
image: smallwebwaf
|
||||||
environment:
|
environment:
|
||||||
SWRP_WAF_BACKEND: http://discriminator:8080
|
SWRP_WAF_BACKEND: http://waf:8080
|
||||||
SWRP_UPSTREAM_URL: http://app:8081
|
SWRP_UPSTREAM_URL: http://app:8081
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|
||||||
discriminator:
|
waf:
|
||||||
image: smallwebwaf
|
image: smallwebwaf
|
||||||
environment:
|
environment:
|
||||||
SWW_COMMAND: discriminator
|
SWW_COMMAND: waf
|
||||||
volumes:
|
volumes:
|
||||||
- discriminator-state:/var/lib/sww
|
- waf-state:/var/lib/sww
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
discriminator-state:
|
waf-state:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom per-address limits
|
### Custom per-address limits
|
||||||
@@ -399,15 +397,15 @@ services:
|
|||||||
rproxy:
|
rproxy:
|
||||||
image: smallwebwaf
|
image: smallwebwaf
|
||||||
environment:
|
environment:
|
||||||
SWRP_WAF_BACKEND: http://discriminator:8080
|
SWRP_WAF_BACKEND: http://waf:8080
|
||||||
SWRP_UPSTREAM_URL: http://app:8081
|
SWRP_UPSTREAM_URL: http://app:8081
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|
||||||
discriminator:
|
waf:
|
||||||
image: smallwebwaf
|
image: smallwebwaf
|
||||||
environment:
|
environment:
|
||||||
SWW_COMMAND: discriminator
|
SWW_COMMAND: waf
|
||||||
# Monitoring skips every check.
|
# Monitoring skips every check.
|
||||||
SWW_ALLOW_NETS: 192.0.2.10
|
SWW_ALLOW_NETS: 192.0.2.10
|
||||||
# The office and the automation host are never rate limited.
|
# The office and the automation host are never rate limited.
|
||||||
@@ -426,18 +424,18 @@ services:
|
|||||||
SWW_BAN_RESPONSE: "429"
|
SWW_BAN_RESPONSE: "429"
|
||||||
SWW_LIMIT_BAN_DURATION: 30m
|
SWW_LIMIT_BAN_DURATION: 30m
|
||||||
volumes:
|
volumes:
|
||||||
- discriminator-state:/var/lib/sww
|
- waf-state:/var/lib/sww
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
discriminator-state:
|
waf-state:
|
||||||
```
|
```
|
||||||
|
|
||||||
### `rproxy` beside the app
|
### `rproxy` beside the app
|
||||||
|
|
||||||
The app is built on the `smallwebwaf` image, with `rproxy` inside its container.
|
The app is built on the `smallwebwaf` image, with `rproxy` inside its container.
|
||||||
`app/` holds the `Dockerfile` and `app.run` from "`rproxy` in the app's
|
`app/` holds the `Dockerfile` and `app.run` from "`rproxy` in the app's
|
||||||
container". `rproxy` answers `503` to every request while `discriminator` is
|
container". `rproxy` answers `503` to every request while `waf` is down, and
|
||||||
down, and `discriminator`:
|
`waf`:
|
||||||
|
|
||||||
- observes before it enforces;
|
- observes before it enforces;
|
||||||
- looks addresses up in a local database rather than GeoJS;
|
- looks addresses up in a local database rather than GeoJS;
|
||||||
@@ -450,17 +448,17 @@ services:
|
|||||||
# A Dockerfile built FROM the smallwebwaf image, with rproxy inside.
|
# A Dockerfile built FROM the smallwebwaf image, with rproxy inside.
|
||||||
build: ./app
|
build: ./app
|
||||||
environment:
|
environment:
|
||||||
SWRP_WAF_BACKEND: http://discriminator:8080
|
SWRP_WAF_BACKEND: http://waf:8080
|
||||||
# Answer 503 rather than pass a request on while discriminator is down.
|
# Answer 503 rather than pass a request on while waf is down.
|
||||||
SWRP_DEFAULT_DISPOSITION: deny
|
SWRP_DEFAULT_DISPOSITION: deny
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|
||||||
discriminator:
|
waf:
|
||||||
image: smallwebwaf
|
image: smallwebwaf
|
||||||
environment:
|
environment:
|
||||||
SWW_COMMAND: discriminator
|
SWW_COMMAND: waf
|
||||||
SWW_INSTANCE_NAME: fsn1/discriminator
|
SWW_INSTANCE_NAME: fsn1/waf
|
||||||
# Refuse nothing yet; log what enforce mode would have done.
|
# Refuse nothing yet; log what enforce mode would have done.
|
||||||
SWW_MODE: observe
|
SWW_MODE: observe
|
||||||
# Look addresses up in a local IPinfo Lite database, never GeoJS.
|
# Look addresses up in a local IPinfo Lite database, never GeoJS.
|
||||||
@@ -471,12 +469,12 @@ services:
|
|||||||
SWW_ALERT_NTFY_TOKEN_FILE: /etc/sww/tokens/ntfy
|
SWW_ALERT_NTFY_TOKEN_FILE: /etc/sww/tokens/ntfy
|
||||||
SWW_ALERT_EVENTS: ban,permanent_ban,core_rule_set_match,source_failure,file_error
|
SWW_ALERT_EVENTS: ban,permanent_ban,core_rule_set_match,source_failure,file_error
|
||||||
volumes:
|
volumes:
|
||||||
- discriminator-state:/var/lib/sww
|
- waf-state:/var/lib/sww
|
||||||
- /srv/ipinfo:/var/lib/ipinfo:ro
|
- /srv/ipinfo:/var/lib/ipinfo:ro
|
||||||
- /srv/sww/tokens:/etc/sww/tokens:ro
|
- /srv/sww/tokens:/etc/sww/tokens:ro
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
discriminator-state:
|
waf-state:
|
||||||
```
|
```
|
||||||
|
|
||||||
The database is mounted by its directory, not by file, so that a replaced
|
The database is mounted by its directory, not by file, so that a replaced
|
||||||
@@ -498,18 +496,18 @@ How it works inside. [`SPEC.md`](SPEC.md) gives every rule in full, and why.
|
|||||||
|
|
||||||
### What happens to a request
|
### What happens to a request
|
||||||
|
|
||||||
A request goes through these steps, in this order. Once `discriminator` refuses
|
A request goes through these steps, in this order. Once `waf` refuses a request,
|
||||||
a request, the steps after that are skipped, apart from the report, and nothing
|
the steps after that are skipped, apart from the report, and nothing of the
|
||||||
of the request reaches the app.
|
request reaches the app.
|
||||||
|
|
||||||
`rproxy` takes the first two:
|
`rproxy` takes the first two:
|
||||||
|
|
||||||
1. The health check, `GET /_sww/healthz`, is answered at once.
|
1. The health check, `GET /_sww/healthz`, is answered at once.
|
||||||
2. The visitor's address is worked out from `X-Forwarded-For`, believed only
|
2. The visitor's address is worked out from `X-Forwarded-For`, believed only
|
||||||
from `SWRP_TRUSTED_PROXIES`. `rproxy` sends `discriminator` the request as
|
from `SWRP_TRUSTED_PROXIES`. `rproxy` sends `waf` the request as the visitor
|
||||||
the visitor sent it, with the first 64 KiB of its body.
|
sent it, with the first 64 KiB of its body.
|
||||||
|
|
||||||
`discriminator` takes the checks:
|
`waf` takes the checks:
|
||||||
|
|
||||||
3. The tag lists.
|
3. The tag lists.
|
||||||
4. The static lists, `SWW_ALLOW_NETS` and `SWW_DENY_NETS`.
|
4. The static lists, `SWW_ALLOW_NETS` and `SWW_DENY_NETS`.
|
||||||
@@ -528,13 +526,13 @@ of the request reaches the app.
|
|||||||
16. A path under `/_sww/` is answered `404`, so that the app never sees that
|
16. A path under `/_sww/` is answered `404`, so that the app never sees that
|
||||||
prefix.
|
prefix.
|
||||||
|
|
||||||
`rproxy` takes the rest, with `discriminator` counting at the end:
|
`rproxy` takes the rest, with `waf` counting at the end:
|
||||||
|
|
||||||
17. The request is passed to the app, streaming.
|
17. The request is passed to the app, streaming.
|
||||||
18. After the answer, `rproxy` sends `discriminator` a report with the status
|
18. After the answer, `rproxy` sends `waf` a report with the status and the body
|
||||||
and the body bytes each way. For an allowed request, `discriminator` counts
|
bytes each way. For an allowed request, `waf` counts the bytes
|
||||||
the bytes `SWW_BYTES_COUNT` names toward the byte limits, bans the client if
|
`SWW_BYTES_COUNT` names toward the byte limits, bans the client if they take
|
||||||
they take it past one, and adds the request to the client's history.
|
it past one, and adds the request to the client's history.
|
||||||
|
|
||||||
A refused visitor is answered with `SWW_BAN_RESPONSE`. A `block` rule or a Core
|
A refused visitor is answered with `SWW_BAN_RESPONSE`. A `block` rule or a Core
|
||||||
Rule Set match answers `403` and bans nobody, since the Core Rule Set has false
|
Rule Set match answers `403` and bans nobody, since the Core Rule Set has false
|
||||||
@@ -560,8 +558,8 @@ status for a size or time limit, such as `413` for a body too large.
|
|||||||
|
|
||||||
### Request tags
|
### Request tags
|
||||||
|
|
||||||
Each request gets tags from what `discriminator` knows of its client, written in
|
Each request gets tags from what `waf` knows of its client, written in the
|
||||||
the `tags` field of its log line:
|
`tags` field of its log line:
|
||||||
|
|
||||||
- `asn=<number>` and `country=<code>`, from the lookup;
|
- `asn=<number>` and `country=<code>`, from the lookup;
|
||||||
- one tag per list of `SWW_TAG_LISTS` that holds the client, such as `torexit`
|
- one tag per list of `SWW_TAG_LISTS` that holds the client, such as `torexit`
|
||||||
@@ -612,17 +610,17 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
|
|||||||
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's own
|
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's own
|
||||||
fields or paths.
|
fields or paths.
|
||||||
- Rule files are plain text, one rule to a line, edited by hand and read again
|
- Rule files are plain text, one rule to a line, edited by hand and read again
|
||||||
while `discriminator` runs. The image ships one file of probes no real visitor
|
while `waf` runs. The image ships one file of probes no real visitor sends
|
||||||
sends (see [`docs/rules.md`](docs/rules.md)).
|
(see [`docs/rules.md`](docs/rules.md)).
|
||||||
|
|
||||||
### State files
|
### State files
|
||||||
|
|
||||||
`discriminator` keeps everything in memory, and the five JSON files in
|
`waf` keeps everything in memory, and the five JSON files in `SWW_STATE_DIR`
|
||||||
`SWW_STATE_DIR` hold a copy: `bans.json`, `clients.json`, `lookups.json`,
|
hold a copy: `bans.json`, `clients.json`, `lookups.json`, `reputation.json` and
|
||||||
`reputation.json` and `alerts.json`. Each is written whole, to a temporary file
|
`alerts.json`. Each is written whole, to a temporary file renamed over the real
|
||||||
renamed over the real one, so a crash leaves a complete file. `discriminator`
|
one, so a crash leaves a complete file. `waf` notices a file edited on disk and
|
||||||
notices a file edited on disk and takes the edit in; an edit that does not parse
|
takes the edit in; an edit that does not parse is set aside as `<name>.bad`, and
|
||||||
is set aside as `<name>.bad`, and never stops the running process (see
|
never stops the running process (see
|
||||||
[`docs/state-files.md`](docs/state-files.md)).
|
[`docs/state-files.md`](docs/state-files.md)).
|
||||||
|
|
||||||
### The image and its processes
|
### The image and its processes
|
||||||
@@ -632,20 +630,19 @@ is set aside as `<name>.bad`, and never stops the running process (see
|
|||||||
`Dockerfile` installs a package with `nix-env -iA nixpkgs.<name>` and gets the
|
`Dockerfile` installs a package with `nix-env -iA nixpkgs.<name>` and gets the
|
||||||
same package each time it is built (see [`docs/image.md`](docs/image.md)).
|
same package each time it is built (see [`docs/image.md`](docs/image.md)).
|
||||||
- runit, a service supervisor, runs `sww` as the `sww` user (uid and gid 65532)
|
- runit, a service supervisor, runs `sww` as the `sww` user (uid and gid 65532)
|
||||||
and starts it again when it exits. The `run` script of `discriminator` gives
|
and starts it again when it exits. The `run` script of `waf` gives the state
|
||||||
the state directory to the `sww` user at each start, so a volume needs no
|
directory to the `sww` user at each start, so a volume needs no change of
|
||||||
change of owner.
|
owner.
|
||||||
- `rproxy` and `discriminator` use no token between them. `discriminator`'s own
|
- `rproxy` and `waf` use no token between them. `waf`'s own endpoints, under
|
||||||
endpoints, under `/_sww/`, are for your admin tools and your metrics
|
`/_sww/`, are for your admin tools and your metrics collector, at its own
|
||||||
collector, at its own address on the private network; nothing routes the
|
address on the private network; nothing routes the internet there.
|
||||||
internet there.
|
|
||||||
- `docker stop` gives requests in progress five seconds to finish. With `rproxy`
|
- `docker stop` gives requests in progress five seconds to finish. With `rproxy`
|
||||||
in the app's container, the app must keep answering for five seconds after
|
in the app's container, the app must keep answering for five seconds after
|
||||||
`SIGTERM` for that to help them.
|
`SIGTERM` for that to help them.
|
||||||
- Only faults found at start stop a process: an invalid setting, a rule file or
|
- Only faults found at start stop a process: an invalid setting, a rule file or
|
||||||
state file that does not parse, a lookup database that cannot be read, or an
|
state file that does not parse, a lookup database that cannot be read, or an
|
||||||
address it cannot listen on. Once running, a failing lookup service,
|
address it cannot listen on. Once running, a failing lookup service,
|
||||||
reputation source or alert destination never takes `discriminator` down.
|
reputation source or alert destination never takes `waf` down.
|
||||||
|
|
||||||
### Packages
|
### Packages
|
||||||
|
|
||||||
@@ -653,8 +650,8 @@ is set aside as `<name>.bad`, and never stops the running process (see
|
|||||||
- `internal/sww`: the process, as the command it is given, and the health check.
|
- `internal/sww`: the process, as the command it is given, and the health check.
|
||||||
- `internal/config`: reads the settings, the one place they are read.
|
- `internal/config`: reads the settings, the one place they are read.
|
||||||
- `internal/rproxy`: the reverse proxy in front of one app.
|
- `internal/rproxy`: the reverse proxy in front of one app.
|
||||||
- `internal/discriminator`: `discriminator`'s server, which runs the checks and
|
- `internal/waf`: `waf`'s server, which runs the checks and answers its own
|
||||||
answers its own endpoints.
|
endpoints.
|
||||||
- `internal/ratelimit`, `internal/bans`, `internal/rules`,
|
- `internal/ratelimit`, `internal/bans`, `internal/rules`,
|
||||||
`internal/coreruleset`, `internal/lookup`, `internal/reputation`,
|
`internal/coreruleset`, `internal/lookup`, `internal/reputation`,
|
||||||
`internal/routewatch`, `internal/anomaly` and `internal/alerts`: one check or
|
`internal/routewatch`, `internal/anomaly` and `internal/alerts`: one check or
|
||||||
@@ -734,25 +731,25 @@ on the host.
|
|||||||
- `script/precommit`: run by the git pre-commit hook; runs `script/check`.
|
- `script/precommit`: run by the git pre-commit hook; runs `script/check`.
|
||||||
- `script/install-precommit`: installs that hook. `make hooks`.
|
- `script/install-precommit`: installs that hook. `make hooks`.
|
||||||
- `script/build`: builds `bin/sww` with the Go on your host. `make build`.
|
- `script/build`: builds `bin/sww` with the Go on your host. `make build`.
|
||||||
- `script/run`: builds `bin/sww` and runs `discriminator` and `rproxy` on the
|
- `script/run`: builds `bin/sww` and runs `waf` and `rproxy` on the host, with
|
||||||
host, with the settings in your environment. `make run`. Unless your settings
|
the settings in your environment. `make run`. Unless your settings say
|
||||||
say otherwise:
|
otherwise:
|
||||||
- `rproxy` listens on port 8080, in front of an app at
|
- `rproxy` listens on port 8080, in front of an app at
|
||||||
`http://127.0.0.1:8081`;
|
`http://127.0.0.1:8081`;
|
||||||
- `discriminator` listens on port 8090;
|
- `waf` listens on port 8090;
|
||||||
- its state files go in `bin/state`;
|
- its state files go in `bin/state`;
|
||||||
- its rule files are those of `share/rules.d`.
|
- its rule files are those of `share/rules.d`.
|
||||||
- `script/example-app`: builds the image and the example app
|
- `script/example-app`: builds the image and the example app
|
||||||
(`deploy/example-app`) on it, runs the app with `rproxy` in its container and
|
(`deploy/example-app`) on it, runs the app with `rproxy` in its container and
|
||||||
`discriminator` in another, and checks them. Then it removes the containers,
|
`waf` in another, and checks them. Then it removes the containers, the
|
||||||
the network, the volumes and the images. Needs network access.
|
network, the volumes and the images. Needs network access. `make example-app`.
|
||||||
`make example-app`. It checks:
|
It checks:
|
||||||
- the health checks;
|
- the health checks;
|
||||||
- a request passed to the app;
|
- a request passed to the app;
|
||||||
- a ban for a broken limit, and a ban for a probe made permanent;
|
- a ban for a broken limit, and a ban for a probe made permanent;
|
||||||
- that only `discriminator`'s container changes the state directory's owner;
|
- that only `waf`'s container changes the state directory's owner;
|
||||||
- with `discriminator` stopped, the app still answering through `rproxy`;
|
- with `waf` stopped, the app still answering through `rproxy`;
|
||||||
- with `discriminator` started again, bans applying again;
|
- with `waf` started again, bans applying again;
|
||||||
- orderly stops with `sv stop` and `docker stop`;
|
- orderly stops with `sv stop` and `docker stop`;
|
||||||
- bans surviving a new container on the same volume;
|
- bans surviving a new container on the same volume;
|
||||||
- a setting given as a file.
|
- a setting given as a file.
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
// Command sww is a web application firewall. `sww rproxy` is the reverse
|
// Command sww is a web application firewall. `sww rproxy` is the reverse
|
||||||
// proxy in front of one app, which asks discriminator about each request.
|
// proxy in front of one app, which asks waf about each request.
|
||||||
// `sww discriminator` decides, for many sites, and keeps what lasts. `sww
|
// `sww waf` decides, for many sites, and keeps what lasts. `sww
|
||||||
// healthcheck` is the container's health check. Everything it does is in
|
// healthcheck` is the container's health check. Everything it does is in
|
||||||
// internal/sww.
|
// internal/sww.
|
||||||
package main
|
package main
|
||||||
|
|||||||
+17
-17
@@ -1,8 +1,8 @@
|
|||||||
# Admin endpoints
|
# Admin endpoints
|
||||||
|
|
||||||
The endpoints under `/_sww/`, what each takes and what each answers.
|
The endpoints under `/_sww/`, what each takes and what each answers. `sww waf`
|
||||||
`sww discriminator` answers them at its own address, on the private network.
|
answers them at its own address, on the private network. "Admin endpoints" in
|
||||||
"Admin endpoints" in [`SPEC.md`](../SPEC.md) says how their tokens work.
|
[`SPEC.md`](../SPEC.md) says how their tokens work.
|
||||||
|
|
||||||
- `GET /_sww/healthz`: `200` and `ok`, to anyone.
|
- `GET /_sww/healthz`: `200` and `ok`, to anyone.
|
||||||
- `GET /_sww/metrics`: the metrics, for `SWW_METRICS_TOKEN` (see
|
- `GET /_sww/metrics`: the metrics, for `SWW_METRICS_TOKEN` (see
|
||||||
@@ -20,10 +20,10 @@ The endpoints under `/_sww/`, what each takes and what each answers.
|
|||||||
- one that names an IPv4-mapped netblock, or an address with a zone.
|
- one that names an IPv4-mapped netblock, or an address with a zone.
|
||||||
- `DELETE /_sww/bans/<client>`: lifts every active ban covering the address
|
- `DELETE /_sww/bans/<client>`: lifts every active ban covering the address
|
||||||
`<client>`. It answers `404` when none is active.
|
`<client>`. It answers `404` when none is active.
|
||||||
- `GET /_sww/clients/<ip>`: what `discriminator` knows of the client. Under
|
- `GET /_sww/clients/<ip>`: what `waf` knows of the client. Under `client`, its
|
||||||
`client`, its counters and history as `clients.json` holds them, or `null`
|
counters and history as `clients.json` holds them, or `null` when
|
||||||
when `clients.json` does not hold it. Under `bans`, every ban covering the
|
`clients.json` does not hold it. Under `bans`, every ban covering the address,
|
||||||
address, with its notes.
|
with its notes.
|
||||||
- `POST /_sww/report`: takes `rproxy`'s report, without a token. It answers
|
- `POST /_sww/report`: takes `rproxy`'s report, without a token. It answers
|
||||||
`204`, or `400` to a body that is no report (see "The report" in
|
`204`, or `400` to a body that is no report (see "The report" in
|
||||||
[`SPEC.md`](../SPEC.md)).
|
[`SPEC.md`](../SPEC.md)).
|
||||||
@@ -32,29 +32,29 @@ No check runs on these requests. A missing or wrong token is answered `401`. An
|
|||||||
endpoint whose token is unset answers `404`, as does any other path.
|
endpoint whose token is unset answers `404`, as does any other path.
|
||||||
|
|
||||||
`sww rproxy` answers only the health check of these, at its own address. It asks
|
`sww rproxy` answers only the health check of these, at its own address. It asks
|
||||||
`discriminator` about any other request under `/_sww/`. One that passes the
|
`waf` about any other request under `/_sww/`. One that passes the checks is
|
||||||
checks is answered `404`, so that the app never sees that prefix (see "`rproxy`"
|
answered `404`, so that the app never sees that prefix (see "`rproxy`" in
|
||||||
in [`SPEC.md`](../SPEC.md)).
|
[`SPEC.md`](../SPEC.md)).
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
With `SWW_ADMIN_TOKEN` set, the token in `$TOKEN`, and `discriminator` reached
|
With `SWW_ADMIN_TOKEN` set, the token in `$TOKEN`, and `waf` reached as
|
||||||
as `discriminator:8080` on its network:
|
`waf:8080` on its network:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Every ban.
|
# Every ban.
|
||||||
curl -H "Authorization: Bearer $TOKEN" http://discriminator:8080/_sww/bans
|
curl -H "Authorization: Bearer $TOKEN" http://waf:8080/_sww/bans
|
||||||
|
|
||||||
# Ban 203.0.113.0/24 for seven days.
|
# Ban 203.0.113.0/24 for seven days.
|
||||||
curl -H "Authorization: Bearer $TOKEN" \
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
--json '{"netblock": "203.0.113.0/24", "duration": "7d", "reason": "probes for logins"}' \
|
--json '{"netblock": "203.0.113.0/24", "duration": "7d", "reason": "probes for logins"}' \
|
||||||
http://discriminator:8080/_sww/bans
|
http://waf:8080/_sww/bans
|
||||||
|
|
||||||
# Lift the bans on 203.0.113.9.
|
# Lift the bans on 203.0.113.9.
|
||||||
curl -H "Authorization: Bearer $TOKEN" -X DELETE \
|
curl -H "Authorization: Bearer $TOKEN" -X DELETE \
|
||||||
http://discriminator:8080/_sww/bans/203.0.113.9
|
http://waf:8080/_sww/bans/203.0.113.9
|
||||||
|
|
||||||
# What discriminator knows of 203.0.113.9.
|
# What waf knows of 203.0.113.9.
|
||||||
curl -H "Authorization: Bearer $TOKEN" \
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
http://discriminator:8080/_sww/clients/203.0.113.9
|
http://waf:8080/_sww/clients/203.0.113.9
|
||||||
```
|
```
|
||||||
+6
-7
@@ -6,7 +6,7 @@ back or dropped.
|
|||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
- `ban`: a ban `discriminator` made.
|
- `ban`: a ban `waf` made.
|
||||||
- `permanent_ban`: a permanent ban it made, or an existing ban made permanent by
|
- `permanent_ban`: a permanent ban it made, or an existing ban made permanent by
|
||||||
a request.
|
a request.
|
||||||
- `core_rule_set_match`: the Core Rule Set scored a request at or over the
|
- `core_rule_set_match`: the Core Rule Set scored a request at or over the
|
||||||
@@ -34,7 +34,7 @@ on one line:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"instance": "fsn1/discriminator",
|
"instance": "fsn1/waf",
|
||||||
"time": "2026-10-06T12:00:50.123461Z",
|
"time": "2026-10-06T12:00:50.123461Z",
|
||||||
"event": "ban",
|
"event": "ban",
|
||||||
"host": "app.example",
|
"host": "app.example",
|
||||||
@@ -79,9 +79,8 @@ on one line:
|
|||||||
|
|
||||||
- `instance`: `SWW_INSTANCE_NAME`. `time`: when the alert was raised. `event`:
|
- `instance`: `SWW_INSTANCE_NAME`. `time`: when the alert was raised. `event`:
|
||||||
one of the events above.
|
one of the events above.
|
||||||
- `host`: the site of the request that raised the alert, since one
|
- `host`: the site of the request that raised the alert, since one `waf` serves
|
||||||
`discriminator` serves many sites. It is left out of `source_failure`,
|
many sites. It is left out of `source_failure`, `file_error` and `summary`.
|
||||||
`file_error` and `summary`.
|
|
||||||
- `client`: the address whose request raised the alert. Empty for
|
- `client`: the address whose request raised the alert. Empty for
|
||||||
`source_failure`, `file_error` and `summary`.
|
`source_failure`, `file_error` and `summary`.
|
||||||
- `netblock`: empty for `source_failure`, `file_error` and `summary`; otherwise
|
- `netblock`: empty for `source_failure`, `file_error` and `summary`; otherwise
|
||||||
@@ -111,8 +110,8 @@ on one line:
|
|||||||
## Slack and ntfy
|
## Slack and ntfy
|
||||||
|
|
||||||
Slack and ntfy get a message with a title and a text. The title is the instance
|
Slack and ntfy get a message with a title and a text. The title is the instance
|
||||||
and the event, such as `fsn1/discriminator: ban`. The text is the `reason`, then
|
and the event, such as `fsn1/waf: ban`. The text is the `reason`, then one line
|
||||||
one line for each of these that the alert has:
|
for each of these that the alert has:
|
||||||
|
|
||||||
- the host, the client, the netblock and the country;
|
- the host, the client, the netblock and the country;
|
||||||
- the file, the source, the error and the mode from `detail`;
|
- the file, the source, the error and the mode from `detail`;
|
||||||
|
|||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
# What the image holds
|
# What the image holds
|
||||||
|
|
||||||
The contents of the `smallwebwaf` image, and how each part is pinned. The image
|
The contents of the `smallwebwaf` image, and how each part is pinned. The image
|
||||||
runs `rproxy` by default, or `discriminator` with `SWW_COMMAND=discriminator`.
|
runs `rproxy` by default, or `waf` with `SWW_COMMAND=waf`. "Deployment" in
|
||||||
"Deployment" in [`SPEC.md`](../SPEC.md) says why each part is there and why it
|
[`SPEC.md`](../SPEC.md) says why each part is there and why it is pinned that
|
||||||
is pinned that way.
|
way.
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
@@ -18,8 +18,8 @@ is pinned that way.
|
|||||||
- The `sww` binary, and the user `sww` (uid and gid 65532).
|
- The `sww` binary, and the user `sww` (uid and gid 65532).
|
||||||
- The service directory `/etc/service/sww`, with the `run` script
|
- The service directory `/etc/service/sww`, with the `run` script
|
||||||
`share/sww.run`.
|
`share/sww.run`.
|
||||||
- `/var/lib/sww` for `discriminator`'s state files, and `/etc/sww/rules.d` with
|
- `/var/lib/sww` for `waf`'s state files, and `/etc/sww/rules.d` with the
|
||||||
the default rule file `share/rules.d/00-default.rules`.
|
default rule file `share/rules.d/00-default.rules`.
|
||||||
- `EXPOSE 8080`, and the `HEALTHCHECK` that runs `sww healthcheck`.
|
- `EXPOSE 8080`, and the `HEALTHCHECK` that runs `sww healthcheck`.
|
||||||
|
|
||||||
## Pins
|
## Pins
|
||||||
|
|||||||
+6
-6
@@ -1,8 +1,8 @@
|
|||||||
# The metrics
|
# The metrics
|
||||||
|
|
||||||
What `GET /_sww/metrics` answers with, at `discriminator`'s own address.
|
What `GET /_sww/metrics` answers with, at `waf`'s own address. "Metrics
|
||||||
"Metrics endpoint" in [`SPEC.md`](../SPEC.md) describes the endpoint. Prometheus
|
endpoint" in [`SPEC.md`](../SPEC.md) describes the endpoint. Prometheus keeps
|
||||||
keeps the `instance` label as `exported_instance` unless the scrape sets
|
the `instance` label as `exported_instance` unless the scrape sets
|
||||||
`honor_labels: true`. `rproxy` has no metrics.
|
`honor_labels: true`. `rproxy` has no metrics.
|
||||||
|
|
||||||
Every `sww_*request_bytes_total` and `sww_*response_bytes_total` series counts
|
Every `sww_*request_bytes_total` and `sww_*response_bytes_total` series counts
|
||||||
@@ -15,7 +15,7 @@ body bytes, never headers, whatever `SWW_BYTES_COUNT` says.
|
|||||||
- `sww_request_duration_seconds` and `sww_upstream_duration_seconds`, as
|
- `sww_request_duration_seconds` and `sww_upstream_duration_seconds`, as
|
||||||
histograms;
|
histograms;
|
||||||
- `sww_requests_in_flight`: the questions and requests to its own endpoints that
|
- `sww_requests_in_flight`: the questions and requests to its own endpoints that
|
||||||
`discriminator` is handling.
|
`waf` is handling.
|
||||||
|
|
||||||
## Limits and bans
|
## Limits and bans
|
||||||
|
|
||||||
@@ -78,8 +78,8 @@ By `file`:
|
|||||||
|
|
||||||
## When a request is counted
|
## When a request is counted
|
||||||
|
|
||||||
`discriminator` counts a denied request at its decision, and a request to its
|
`waf` counts a denied request at its decision, and a request to its own
|
||||||
own endpoints when it is answered. It counts an allowed request when its report
|
endpoints when it is answered. It counts an allowed request when its report
|
||||||
comes, with the status, bytes and times the report gives. These are not counted:
|
comes, with the status, bytes and times the report gives. These are not counted:
|
||||||
|
|
||||||
- a report itself;
|
- a report itself;
|
||||||
|
|||||||
+34
-37
@@ -1,32 +1,32 @@
|
|||||||
# The request log's fields
|
# The request log's fields
|
||||||
|
|
||||||
The fields of the JSON objects `rproxy` and `discriminator` write about each
|
The fields of the JSON objects `rproxy` and `waf` write about each request.
|
||||||
request. "Request log" and "The log" in [`SPEC.md`](../SPEC.md) say when a line
|
"Request log" and "The log" in [`SPEC.md`](../SPEC.md) say when a line is
|
||||||
is written and how the log is sent to a syslog server. A field that does not
|
written and how the log is sent to a syslog server. A field that does not apply
|
||||||
apply to a request is left out.
|
to a request is left out.
|
||||||
|
|
||||||
## `discriminator`'s lines
|
## `waf`'s lines
|
||||||
|
|
||||||
`sww discriminator` writes two lines for each question it answers, joined by
|
`sww waf` writes two lines for each question it answers, joined by `request_id`.
|
||||||
`request_id`. The field `discriminator_line` says which of the two a line is:
|
The field `waf_line` says which of the two a line is:
|
||||||
|
|
||||||
- `decision`: the line it writes once it has answered the question;
|
- `decision`: the line it writes once it has answered the question;
|
||||||
- `report`: the line it writes when `rproxy`'s report comes, with everything
|
- `report`: the line it writes when `rproxy`'s report comes, with everything
|
||||||
known. The repetition is intended.
|
known. The repetition is intended.
|
||||||
|
|
||||||
A request to its own endpoints writes one line, without `discriminator_line`. A
|
A request to its own endpoints writes one line, without `waf_line`. A report
|
||||||
report writes no line but the `report` line of the request it names.
|
writes no line but the `report` line of the request it names.
|
||||||
|
|
||||||
These questions write no line:
|
These questions write no line:
|
||||||
|
|
||||||
- a question refused with `400` (see "The question" under "`discriminator`" in
|
- a question refused with `400` (see "The question" under "`waf`" in
|
||||||
[`SPEC.md`](../SPEC.md));
|
[`SPEC.md`](../SPEC.md));
|
||||||
- a question whose body stopped arriving, which gets no answer.
|
- a question whose body stopped arriving, which gets no answer.
|
||||||
|
|
||||||
A `report` line:
|
A `report` line:
|
||||||
|
|
||||||
```
|
```
|
||||||
{"type":"request","discriminator_line":"report","time":"2026-10-03T12:00:00.123Z","instance":"fsn1/discriminator","client_ip":"203.0.113.9","method":"GET","scheme":"https","host":"app.example","path":"/","query":"","protocol":"HTTP/1.1","status":200,"request_bytes":0,"response_bytes":5120,"referer":"","user_agent":"curl/8.9.1","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","peer_ip":"172.18.0.2","forwarded_for":"203.0.113.9","client_group":"203.0.113.9/32","asn":"AS64496","as_name":"Example Net","country":"DE","request_headers":{"accept":"*/*"},"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"core_rule_set_score":0,"duration_total_ms":3.217,"duration_checks_ms":0.041,"duration_core_rule_set_ms":0.029,"duration_upstream_total_ms":3.104}
|
{"type":"request","waf_line":"report","time":"2026-10-03T12:00:00.123Z","instance":"fsn1/waf","client_ip":"203.0.113.9","method":"GET","scheme":"https","host":"app.example","path":"/","query":"","protocol":"HTTP/1.1","status":200,"request_bytes":0,"response_bytes":5120,"referer":"","user_agent":"curl/8.9.1","request_id":"7Q2NHZ4KJ3VXW5YB6R3MEFTD2A","peer_ip":"172.18.0.2","forwarded_for":"203.0.113.9","client_group":"203.0.113.9/32","asn":"AS64496","as_name":"Example Net","country":"DE","request_headers":{"accept":"*/*"},"action":"forward","counts":{"minute":1,"hour":12,"day":40,"minute_bytes":5120,"hour_bytes":61440,"day_bytes":204800},"core_rule_set_score":0,"duration_total_ms":3.217,"duration_checks_ms":0.041,"duration_core_rule_set_ms":0.029,"duration_upstream_total_ms":3.104}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Every line
|
### Every line
|
||||||
@@ -41,9 +41,8 @@ Every one of these lines has:
|
|||||||
|
|
||||||
### Standard web log fields
|
### Standard web log fields
|
||||||
|
|
||||||
- `time`: when `discriminator` had read the question's body, RFC 3339 with
|
- `time`: when `waf` had read the question's body, RFC 3339 with milliseconds,
|
||||||
milliseconds, in UTC. The `report` line gives the same time as the `decision`
|
in UTC. The `report` line gives the same time as the `decision` line.
|
||||||
line.
|
|
||||||
- `instance`: `SWW_INSTANCE_NAME`.
|
- `instance`: `SWW_INSTANCE_NAME`.
|
||||||
- `client_ip`: the visitor's address, as `rproxy` worked it out.
|
- `client_ip`: the visitor's address, as `rproxy` worked it out.
|
||||||
- `method`, `path` and `query`: the visitor's request.
|
- `method`, `path` and `query`: the visitor's request.
|
||||||
@@ -52,9 +51,8 @@ Every one of these lines has:
|
|||||||
- `host`: the site the visitor asked for.
|
- `host`: the site the visitor asked for.
|
||||||
- `protocol`: the question's own, `HTTP/1.1`.
|
- `protocol`: the question's own, `HTTP/1.1`.
|
||||||
- `status`:
|
- `status`:
|
||||||
- on the `decision` line, what `discriminator` answered: `200` for `allow`,
|
- on the `decision` line, what `waf` answered: `200` for `allow`, the status
|
||||||
the status of `deny`, or `0` for `close`, as the visitor is then sent
|
of `deny`, or `0` for `close`, as the visitor is then sent nothing;
|
||||||
nothing;
|
|
||||||
- on the `report` line, what the visitor got, `0` if nothing.
|
- on the `report` line, what the visitor got, `0` if nothing.
|
||||||
- `request_bytes`:
|
- `request_bytes`:
|
||||||
- on the `decision` line, how much of the question's body the Core Rule Set
|
- on the `decision` line, how much of the question's body the Core Rule Set
|
||||||
@@ -62,7 +60,7 @@ Every one of these lines has:
|
|||||||
- on the `report` line, the bytes of the visitor's body that `rproxy` read,
|
- on the `report` line, the bytes of the visitor's body that `rproxy` read,
|
||||||
with what a WebSocket carried from the visitor.
|
with what a WebSocket carried from the visitor.
|
||||||
- `response_bytes`:
|
- `response_bytes`:
|
||||||
- on the `decision` line, the body of `discriminator`'s answer;
|
- on the `decision` line, the body of `waf`'s answer;
|
||||||
- on the `report` line, the bytes of the answer's body, with what a
|
- on the `report` line, the bytes of the answer's body, with what a
|
||||||
WebSocket carried from the app.
|
WebSocket carried from the app.
|
||||||
- `referer` and `user_agent`.
|
- `referer` and `user_agent`.
|
||||||
@@ -79,7 +77,7 @@ Every one of these lines has:
|
|||||||
- lookups are off;
|
- lookups are off;
|
||||||
- the client is not looked up;
|
- the client is not looked up;
|
||||||
- the source cannot place the client;
|
- the source cannot place the client;
|
||||||
- GeoJS had not answered by the time `discriminator` answered.
|
- GeoJS had not answered by the time `waf` answered.
|
||||||
- `content_type`: the visitor's `Content-Type`.
|
- `content_type`: the visitor's `Content-Type`.
|
||||||
- `content_length`: the body length the visitor announced.
|
- `content_length`: the body length the visitor announced.
|
||||||
- `request_headers`: the headers listed in `SWW_LOG_REQUEST_HEADERS`, by name in
|
- `request_headers`: the headers listed in `SWW_LOG_REQUEST_HEADERS`, by name in
|
||||||
@@ -89,7 +87,7 @@ Every one of these lines has:
|
|||||||
|
|
||||||
### Response detail
|
### Response detail
|
||||||
|
|
||||||
- `response_content_type`: the type of `discriminator`'s own answer.
|
- `response_content_type`: the type of `waf`'s own answer.
|
||||||
- `aborted`: true when `rproxy` went away before the answer.
|
- `aborted`: true when `rproxy` went away before the answer.
|
||||||
|
|
||||||
### Decision
|
### Decision
|
||||||
@@ -105,8 +103,8 @@ Every one of these lines has:
|
|||||||
- `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and
|
- `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and
|
||||||
`blocked_by_core_rule_set`: refused by that check;
|
`blocked_by_core_rule_set`: refused by that check;
|
||||||
- `request_header_too_large`: over a header limit;
|
- `request_header_too_large`: over a header limit;
|
||||||
- `own_endpoint`: answered at one of `discriminator`'s own endpoints, or
|
- `own_endpoint`: answered at one of `waf`'s own endpoints, or `404` for a
|
||||||
`404` for a path under `/_sww/`.
|
path under `/_sww/`.
|
||||||
- `would_action`: in `observe` mode, what `enforce` mode would have done with a
|
- `would_action`: in `observe` mode, what `enforce` mode would have done with a
|
||||||
request it would have refused. `action` then names what was done, normally
|
request it would have refused. `action` then names what was done, normally
|
||||||
`forward`.
|
`forward`.
|
||||||
@@ -131,9 +129,8 @@ Every one of these lines has:
|
|||||||
- `core_rule_set`: `score`, and `threshold`, `SWW_WAF_ANOMALY_THRESHOLD`;
|
- `core_rule_set`: `score`, and `threshold`, `SWW_WAF_ANOMALY_THRESHOLD`;
|
||||||
- `refusal_limit`: `window`, `count` and `limit`, of the error burst;
|
- `refusal_limit`: `window`, `count` and `limit`, of the error burst;
|
||||||
- `byte_limits`: `window`, `count` and `limit`, the count in bytes.
|
- `byte_limits`: `window`, `count` and `limit`, the count in bytes.
|
||||||
- `after_answer_heuristic`: the heuristic that banned the client after
|
- `after_answer_heuristic`: the heuristic that banned the client after `waf`
|
||||||
`discriminator` answered the request, in `observe` mode too: `refusal_limit`
|
answered the request, in `observe` mode too: `refusal_limit` or `byte_limits`.
|
||||||
or `byte_limits`.
|
|
||||||
- `after_answer_heuristic_figures`: an object of the figures that heuristic
|
- `after_answer_heuristic_figures`: an object of the figures that heuristic
|
||||||
acted on, as listed above.
|
acted on, as listed above.
|
||||||
|
|
||||||
@@ -191,8 +188,8 @@ Every one of these lines has:
|
|||||||
All in milliseconds:
|
All in milliseconds:
|
||||||
|
|
||||||
- `duration_total_ms`:
|
- `duration_total_ms`:
|
||||||
- on the `decision` line, from `discriminator` having read the question's
|
- on the `decision` line, from `waf` having read the question's body to the
|
||||||
body to the line;
|
line;
|
||||||
- on the `report` line, from `rproxy` having read the request's headers to
|
- on the `report` line, from `rproxy` having read the request's headers to
|
||||||
the end of the answer.
|
the end of the answer.
|
||||||
- `duration_checks_ms`: to the end of the checks;
|
- `duration_checks_ms`: to the end of the checks;
|
||||||
@@ -202,8 +199,8 @@ All in milliseconds:
|
|||||||
|
|
||||||
### Requests to its own endpoints
|
### Requests to its own endpoints
|
||||||
|
|
||||||
The line of a request to `discriminator`'s own endpoints has these fields, with
|
The line of a request to `waf`'s own endpoints has these fields, with these
|
||||||
these meanings where they differ:
|
meanings where they differ:
|
||||||
|
|
||||||
- `client_ip` and `peer_ip`: the request's TCP peer.
|
- `client_ip` and `peer_ip`: the request's TCP peer.
|
||||||
- `request_id`: a new random one of 26 letters and digits.
|
- `request_id`: a new random one of 26 letters and digits.
|
||||||
@@ -216,7 +213,7 @@ these meanings where they differ:
|
|||||||
## `rproxy`'s line
|
## `rproxy`'s line
|
||||||
|
|
||||||
`sww rproxy` writes one short line for each request. `request_id` joins it to
|
`sww rproxy` writes one short line for each request. `request_id` joins it to
|
||||||
`discriminator`'s two. It has these fields and no others:
|
`waf`'s two. It has these fields and no others:
|
||||||
|
|
||||||
- `type`: `request`.
|
- `type`: `request`.
|
||||||
- `time`: when the request arrived, RFC 3339 with milliseconds, in UTC.
|
- `time`: when the request arrived, RFC 3339 with milliseconds, in UTC.
|
||||||
@@ -231,15 +228,15 @@ these meanings where they differ:
|
|||||||
carried from the app.
|
carried from the app.
|
||||||
- `duration_request_ms`: milliseconds from the request's headers being read
|
- `duration_request_ms`: milliseconds from the request's headers being read
|
||||||
until the app had been sent the whole request, or the answer started if that
|
until the app had been sent the whole request, or the answer started if that
|
||||||
was sooner. It includes the wait for `discriminator`.
|
was sooner. It includes the wait for `waf`.
|
||||||
- `duration_response_ms`: milliseconds from then until the end of the answer.
|
- `duration_response_ms`: milliseconds from then until the end of the answer.
|
||||||
- `disposition`: what was done with the request: `allow`, `deny` or `close`. It
|
- `disposition`: what was done with the request: `allow`, `deny` or `close`. It
|
||||||
is `discriminator`'s answer, or the value of `SWRP_DEFAULT_DISPOSITION`. It is
|
is `waf`'s answer, or the value of `SWRP_DEFAULT_DISPOSITION`. It is left out
|
||||||
left out when:
|
when:
|
||||||
- `discriminator` was not asked, as for the health check;
|
- `waf` was not asked, as for the health check;
|
||||||
- the visitor went away while `discriminator` was being asked;
|
- the visitor went away while `waf` was being asked;
|
||||||
- `discriminator`'s answer had no valid `X-Sww-Disposition`;
|
- `waf`'s answer had no valid `X-Sww-Disposition`;
|
||||||
- `rproxy` could not pass on `discriminator`'s answer, and answered `502`.
|
- `rproxy` could not pass on `waf`'s answer, and answered `502`.
|
||||||
- `default_disposition`: `true` when `SWRP_DEFAULT_DISPOSITION` gave
|
- `default_disposition`: `true` when `SWRP_DEFAULT_DISPOSITION` gave
|
||||||
`disposition`, and otherwise left out.
|
`disposition`, and otherwise left out.
|
||||||
|
|
||||||
|
|||||||
+4
-6
@@ -54,20 +54,18 @@ empty-agent user_agent log ^$
|
|||||||
|
|
||||||
## A file of your own
|
## A file of your own
|
||||||
|
|
||||||
`discriminator` reads the rule files in its own container. Rules of your own go
|
`waf` reads the rule files in its own container. Rules of your own go in a file
|
||||||
in a file named to sort after the default, such as `50-site.rules`:
|
named to sort after the default, such as `50-site.rules`:
|
||||||
|
|
||||||
```
|
```
|
||||||
# 50-site.rules: WordPress probes, which this site never serves
|
# 50-site.rules: WordPress probes, which this site never serves
|
||||||
wp-probe path ban (?i)^/(wp-login\.php|xmlrpc\.php|wp-admin/)
|
wp-probe path ban (?i)^/(wp-login\.php|xmlrpc\.php|wp-admin/)
|
||||||
```
|
```
|
||||||
|
|
||||||
A `Dockerfile` built on the `smallwebwaf` image, for `discriminator` to run
|
A `Dockerfile` built on the `smallwebwaf` image, for `waf` to run from, adds it:
|
||||||
from, adds it:
|
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
COPY 50-site.rules /etc/sww/rules.d/50-site.rules
|
COPY 50-site.rules /etc/sww/rules.d/50-site.rules
|
||||||
```
|
```
|
||||||
|
|
||||||
Or mount it there. A `path` rule applies on every site behind that
|
Or mount it there. A `path` rule applies on every site behind that `waf`.
|
||||||
`discriminator`.
|
|
||||||
+37
-40
@@ -4,9 +4,9 @@ Every setting, its default and the values it takes, in the groups of "Settings"
|
|||||||
in [`SPEC.md`](../SPEC.md). That section gives the conventions, the settings
|
in [`SPEC.md`](../SPEC.md). That section gives the conventions, the settings
|
||||||
that take `off`, the `_FILE` form, and what each group does and why.
|
that take `off`, the `_FILE` form, and what each group does and why.
|
||||||
|
|
||||||
The settings that start with `SWW_` are `discriminator`'s. Those that start with
|
The settings that start with `SWW_` are `waf`'s. Those that start with `SWRP_`
|
||||||
`SWRP_` are `rproxy`'s, and are under "`rproxy`" below. `SWW_COMMAND` chooses
|
are `rproxy`'s, and are under "`rproxy`" below. `SWW_COMMAND` chooses between
|
||||||
between the two (see "The image's command" below).
|
the two (see "The image's command" below).
|
||||||
|
|
||||||
## Forms of values
|
## Forms of values
|
||||||
|
|
||||||
@@ -28,21 +28,20 @@ between the two (see "The image's command" below).
|
|||||||
|
|
||||||
## Core
|
## Core
|
||||||
|
|
||||||
- `SWW_LISTEN_ADDR` (default `:8080`): where `discriminator` listens: `:` and a
|
- `SWW_LISTEN_ADDR` (default `:8080`): where `waf` listens: `:` and a port, such
|
||||||
port, such as `:9000`. Leave the host out (see "Ports" in
|
as `:9000`. Leave the host out (see "Ports" in [`SPEC.md`](../SPEC.md)).
|
||||||
[`SPEC.md`](../SPEC.md)).
|
|
||||||
- `SWW_INSTANCE_NAME` (default: the container's host name): the `instance` name
|
- `SWW_INSTANCE_NAME` (default: the container's host name): the `instance` name
|
||||||
in each line `discriminator` logs, and in each alert and metric, such as
|
in each line `waf` logs, and in each alert and metric, such as `fsn1/waf`.
|
||||||
`fsn1/discriminator`. Valid UTF-8. No control character while
|
Valid UTF-8. No control character while `SWW_ALERT_NTFY_URL` is set. 1 to 48
|
||||||
`SWW_ALERT_NTFY_URL` is set. 1 to 48 printable ASCII characters without a
|
printable ASCII characters without a space while `SWW_LOG_REMOTE_URL` is set
|
||||||
space while `SWW_LOG_REMOTE_URL` is set and `SWW_LOG_REMOTE_APP_NAME` is not.
|
and `SWW_LOG_REMOTE_APP_NAME` is not.
|
||||||
- `SWW_MODE` (default `enforce`): `enforce` or `observe`.
|
- `SWW_MODE` (default `enforce`): `enforce` or `observe`.
|
||||||
- `SWW_IPV6_GROUP_PREFIX` (default `64`): the prefix length of an IPv6 client's
|
- `SWW_IPV6_GROUP_PREFIX` (default `64`): the prefix length of an IPv6 client's
|
||||||
group, from 32 to 128.
|
group, from 32 to 128.
|
||||||
- `SWW_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a whole
|
- `SWW_MAX_TRACKED_CLIENTS` (default `20000`): the most clients kept, a whole
|
||||||
number above zero.
|
number above zero.
|
||||||
- `SWW_MAX_BANS` (default `5000`): how many of its own bans `discriminator`
|
- `SWW_MAX_BANS` (default `5000`): how many of its own bans `waf` keeps, a whole
|
||||||
keeps, a whole number above zero.
|
number above zero.
|
||||||
|
|
||||||
## Tokens
|
## Tokens
|
||||||
|
|
||||||
@@ -62,8 +61,8 @@ between the two (see "The image's command" below).
|
|||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
- `SWW_LOG_LEVEL` (default `info`): the least severe of `discriminator`'s own
|
- `SWW_LOG_LEVEL` (default `info`): the least severe of `waf`'s own messages
|
||||||
messages that are written: `debug`, `info`, `warn` or `error`.
|
that are written: `debug`, `info`, `warn` or `error`.
|
||||||
- `SWW_LOG_REQUEST_HEADERS` (default
|
- `SWW_LOG_REQUEST_HEADERS` (default
|
||||||
`accept,accept-language,accept-encoding,content-type,origin,range`): the
|
`accept,accept-language,accept-encoding,content-type,origin,range`): the
|
||||||
request headers recorded in the log line, in either case. `Host` and
|
request headers recorded in the log line, in either case. `Host` and
|
||||||
@@ -167,8 +166,8 @@ cut off; the client is banned.
|
|||||||
|
|
||||||
## Request tags
|
## Request tags
|
||||||
|
|
||||||
Every request gets tags from what `discriminator` knows of its client. See
|
Every request gets tags from what `waf` knows of its client. See "Request tags"
|
||||||
"Request tags" in [`SPEC.md`](../SPEC.md).
|
in [`SPEC.md`](../SPEC.md).
|
||||||
|
|
||||||
- `SWW_TAG_LISTS` (default empty): address lists that each add a tag, as
|
- `SWW_TAG_LISTS` (default empty): address lists that each add a tag, as
|
||||||
`name=URL` pairs, such as `torexit=https://example.com/tor.txt`. A name is the
|
`name=URL` pairs, such as `torexit=https://example.com/tor.txt`. A name is the
|
||||||
@@ -188,10 +187,10 @@ Every request gets tags from what `discriminator` knows of its client. See
|
|||||||
|
|
||||||
- `SWW_ROUTEWATCH_URL` (default unset): the `http` or `https` URL of a
|
- `SWW_ROUTEWATCH_URL` (default unset): the `http` or `https` URL of a
|
||||||
routewatch instance. [routewatch](https://git.eeqj.de/sneak/routewatch) is a
|
routewatch instance. [routewatch](https://git.eeqj.de/sneak/routewatch) is a
|
||||||
service that gives an address's announced netblock and origin AS.
|
service that gives an address's announced netblock and origin AS. `waf` asks
|
||||||
`discriminator` asks it about each client, in the background, and adds the
|
it about each client, in the background, and adds the tags `asn=<number>` and
|
||||||
tags `asn=<number>` and `prefix=<netblock>` from its answer. While it is
|
`prefix=<netblock>` from its answer. While it is unset, nothing asks
|
||||||
unset, nothing asks routewatch and no request gets a `prefix=` tag.
|
routewatch and no request gets a `prefix=` tag.
|
||||||
|
|
||||||
`SWW_TAG_DENY` and `SWW_TAG_LIMIT_PERCENT` accept tags in either case. A tag
|
`SWW_TAG_DENY` and `SWW_TAG_LIMIT_PERCENT` accept tags in either case. A tag
|
||||||
that no request can get stops the start. A request can get only these:
|
that no request can get stops the start. A request can get only these:
|
||||||
@@ -377,7 +376,7 @@ Then mount its directory into the container read-only, and name the file:
|
|||||||
```sh
|
```sh
|
||||||
docker run \
|
docker run \
|
||||||
-v /srv/app/tokens:/etc/sww/tokens:ro \
|
-v /srv/app/tokens:/etc/sww/tokens:ro \
|
||||||
-e SWW_COMMAND=discriminator \
|
-e SWW_COMMAND=waf \
|
||||||
-e SWW_METRICS_TOKEN_FILE=/etc/sww/tokens/metrics \
|
-e SWW_METRICS_TOKEN_FILE=/etc/sww/tokens/metrics \
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
@@ -391,21 +390,20 @@ A few limits are fixed rather than settings:
|
|||||||
- at most 1,000 DNS blocklist queries are under way at once;
|
- at most 1,000 DNS blocklist queries are under way at once;
|
||||||
- at most 20,000 anomaly counters are kept;
|
- at most 20,000 anomaly counters are kept;
|
||||||
- each alert destination's queue holds at most 1,000 alerts;
|
- each alert destination's queue holds at most 1,000 alerts;
|
||||||
- `discriminator` holds at most 10,000 answered requests awaiting their reports;
|
- `waf` holds at most 10,000 answered requests awaiting their reports;
|
||||||
- `discriminator` refuses a request with more than 50 cookies or more than 100
|
- `waf` refuses a request with more than 50 cookies or more than 100 header
|
||||||
header fields (see "Answering in time" under "`discriminator`" in
|
fields (see "Answering in time" under "`waf`" in [`SPEC.md`](../SPEC.md));
|
||||||
[`SPEC.md`](../SPEC.md));
|
|
||||||
- `rproxy` takes a request line and headers of at most 32 KiB, and closes a
|
- `rproxy` takes a request line and headers of at most 32 KiB, and closes a
|
||||||
kept-open connection idle for 120 seconds;
|
kept-open connection idle for 120 seconds;
|
||||||
- `rproxy` reads at most 1 MiB of the body of `discriminator`'s answer, and
|
- `rproxy` reads at most 1 MiB of the body of `waf`'s answer, and answers `502`
|
||||||
answers `502` to a longer one.
|
to a longer one.
|
||||||
|
|
||||||
## `discriminator`'s answer and `rproxy`'s wait
|
## `waf`'s answer and `rproxy`'s wait
|
||||||
|
|
||||||
`SWW_LOOKUP_TIMEOUT` is the longest a question waits on an outside source. Keep
|
`SWW_LOOKUP_TIMEOUT` is the longest a question waits on an outside source. Keep
|
||||||
it well under `rproxy`'s `SWRP_TIMEOUT_MS`, as the default is. No question waits
|
it well under `rproxy`'s `SWRP_TIMEOUT_MS`, as the default is. No question waits
|
||||||
for `SWW_REPUTATION_TIMEOUT`, since the DNS blocklists and AbuseIPDB are asked
|
for `SWW_REPUTATION_TIMEOUT`, since the DNS blocklists and AbuseIPDB are asked
|
||||||
in the background (see "Answering in time" under "`discriminator`" in
|
in the background (see "Answering in time" under "`waf`" in
|
||||||
[`SPEC.md`](../SPEC.md)).
|
[`SPEC.md`](../SPEC.md)).
|
||||||
|
|
||||||
## `rproxy`
|
## `rproxy`
|
||||||
@@ -417,16 +415,15 @@ they do.
|
|||||||
|
|
||||||
A time here is a whole number of milliseconds above zero, such as `2000`.
|
A time here is a whole number of milliseconds above zero, such as `2000`.
|
||||||
|
|
||||||
- `SWRP_WAF_BACKEND` (required, no default): `discriminator`'s URL. An `http` or
|
- `SWRP_WAF_BACKEND` (required, no default): `waf`'s URL. An `http` or `https`
|
||||||
`https` URL with a host and an optional port, and nothing more. `rproxy`
|
URL with a host and an optional port, and nothing more. `rproxy` speaks
|
||||||
speaks HTTP/1.1 to it.
|
HTTP/1.1 to it.
|
||||||
- `SWRP_TIMEOUT_MS` (default `2000`): how long `rproxy` waits for
|
- `SWRP_TIMEOUT_MS` (default `2000`): how long `rproxy` waits for `waf`'s
|
||||||
`discriminator`'s answer, to a question and to a report. A time; never `off`.
|
answer, to a question and to a report. A time; never `off`.
|
||||||
- `SWRP_DEFAULT_DISPOSITION` (default `allow`): what is done with a request when
|
- `SWRP_DEFAULT_DISPOSITION` (default `allow`): what is done with a request when
|
||||||
`discriminator` cannot be connected to, does not answer at all, or does not
|
`waf` cannot be connected to, does not answer at all, or does not answer
|
||||||
answer within `SWRP_TIMEOUT_MS`. `allow` passes it to the app, and `deny`
|
within `SWRP_TIMEOUT_MS`. `allow` passes it to the app, and `deny` answers
|
||||||
answers `503`. It does not apply to an answer `discriminator` gives, such as
|
`503`. It does not apply to an answer `waf` gives, such as an error.
|
||||||
an error.
|
|
||||||
- `SWRP_MAX_REQUEST_SIZE` (default `100M`): the largest request body. A size or
|
- `SWRP_MAX_REQUEST_SIZE` (default `100M`): the largest request body. A size or
|
||||||
`off`.
|
`off`.
|
||||||
- `SWRP_MAX_RESPONSE_SIZE` (default `1G`): the largest response body. A size or
|
- `SWRP_MAX_RESPONSE_SIZE` (default `1G`): the largest response body. A size or
|
||||||
@@ -452,8 +449,8 @@ A time here is a whole number of milliseconds above zero, such as `2000`.
|
|||||||
## The image's command
|
## The image's command
|
||||||
|
|
||||||
`SWW_COMMAND` (default `rproxy`) chooses what the image runs and what its health
|
`SWW_COMMAND` (default `rproxy`) chooses what the image runs and what its health
|
||||||
check checks: `rproxy` or `discriminator` (see "Deployment" in
|
check checks: `rproxy` or `waf` (see "Deployment" in [`SPEC.md`](../SPEC.md)).
|
||||||
[`SPEC.md`](../SPEC.md)). It cannot be given as a file.
|
It cannot be given as a file.
|
||||||
|
|
||||||
## Where earlier settings went
|
## Where earlier settings went
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ a top-level `version` of 1.
|
|||||||
## `bans.json`
|
## `bans.json`
|
||||||
|
|
||||||
Active and past bans: every ban an admin made, and up to `SWW_MAX_BANS` of those
|
Active and past bans: every ban an admin made, and up to `SWW_MAX_BANS` of those
|
||||||
`discriminator` made. Per entry:
|
`waf` made. Per entry:
|
||||||
|
|
||||||
- `netblock`, `start` and `expires` (`null` for a permanent ban);
|
- `netblock`, `start` and `expires` (`null` for a permanent ban);
|
||||||
- `cause`: `attack`, `limit`, `admin` or `crowdsec`;
|
- `cause`: `attack`, `limit`, `admin` or `crowdsec`;
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ type Params struct {
|
|||||||
// Alert is one alert, as the webhook is sent it and alerts.json holds it,
|
// Alert is one alert, as the webhook is sent it and alerts.json holds it,
|
||||||
// with the fields of "The JSON object" in docs/alerts.md. ASN, ASName and
|
// with the fields of "The JSON object" in docs/alerts.md. ASN, ASName and
|
||||||
// Country are, for a ban, the client's as the ban's notes give them. Host is
|
// Country are, for a ban, the client's as the ban's notes give them. Host is
|
||||||
// the site of the request that raised the alert, while one discriminator serves
|
// the site of the request that raised the alert, while one waf serves
|
||||||
// many sites, and "" otherwise.
|
// many sites, and "" otherwise.
|
||||||
//
|
//
|
||||||
//nolint:tagliatelle // docs/alerts.md names the alert's fields in snake_case
|
//nolint:tagliatelle // docs/alerts.md names the alert's fields in snake_case
|
||||||
|
|||||||
+13
-13
@@ -1,6 +1,6 @@
|
|||||||
// Package config reads sww's settings, and is the one place they are
|
// Package config reads sww's settings, and is the one place they are
|
||||||
// read. Every setting is an environment variable. The settings of `sww
|
// read. Every setting is an environment variable. The settings of `sww
|
||||||
// discriminator` start with SWW_, every one of them has a default, and most can be
|
// waf` start with SWW_, every one of them has a default, and most can be
|
||||||
// given as a file instead. Those of `sww rproxy` start with SWRP_. Each
|
// given as a file instead. Those of `sww rproxy` start with SWRP_. Each
|
||||||
// of them but SWRP_WAF_BACKEND has a default, and none can be given as a file,
|
// of them but SWRP_WAF_BACKEND has a default, and none can be given as a file,
|
||||||
// since rproxy never touches disk. SWW_COMMAND chooses which of the two
|
// since rproxy never touches disk. SWW_COMMAND chooses which of the two
|
||||||
@@ -32,10 +32,10 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/remotelog"
|
"sneak.berlin/go/smallwebwaf/internal/remotelog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the settings of `sww discriminator`. A time or size limit of
|
// Config is the settings of `sww waf`. A time or size limit of
|
||||||
// zero is off.
|
// zero is off.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// ListenAddr is where discriminator listens (SWW_LISTEN_ADDR).
|
// ListenAddr is where waf listens (SWW_LISTEN_ADDR).
|
||||||
ListenAddr string
|
ListenAddr string
|
||||||
// InstanceName is the name every log line and alert gives as instance,
|
// InstanceName is the name every log line and alert gives as instance,
|
||||||
// and every metric carries as its label instance (SWW_INSTANCE_NAME),
|
// and every metric carries as its label instance (SWW_INSTANCE_NAME),
|
||||||
@@ -239,7 +239,7 @@ type Config struct {
|
|||||||
// attack (SWW_TRAP_PATHS), each starting with / and without a ?.
|
// attack (SWW_TRAP_PATHS), each starting with / and without a ?.
|
||||||
TrapPaths []string
|
TrapPaths []string
|
||||||
// ErrorBurstThreshold is the most requests of a client within a minute
|
// ErrorBurstThreshold is the most requests of a client within a minute
|
||||||
// that discriminator may refuse after a rule file or Core Rule Set match; one
|
// that waf may refuse after a rule file or Core Rule Set match; one
|
||||||
// more breaks a limit (SWW_ERROR_BURST_THRESHOLD). 0 is off.
|
// more breaks a limit (SWW_ERROR_BURST_THRESHOLD). 0 is off.
|
||||||
ErrorBurstThreshold int64
|
ErrorBurstThreshold int64
|
||||||
// LogRemoteURL is where every line on stdout is also sent
|
// LogRemoteURL is where every line on stdout is also sent
|
||||||
@@ -378,8 +378,8 @@ var (
|
|||||||
errNeedsDBPath = errors.New("it names the file to look clients up in")
|
errNeedsDBPath = errors.New("it names the file to look clients up in")
|
||||||
errDBPathUnused = errors.New("only file reads it")
|
errDBPathUnused = errors.New("only file reads it")
|
||||||
errOver64K = errors.New(
|
errOver64K = errors.New(
|
||||||
"is more than 64K, the most discriminator gets of a body")
|
"is more than 64K, the most waf gets of a body")
|
||||||
errNotCommand = errors.New("is not rproxy or discriminator")
|
errNotCommand = errors.New("is not rproxy or waf")
|
||||||
errNotDurationAboveZero = errors.New(
|
errNotDurationAboveZero = errors.New(
|
||||||
"is not a duration above zero, such as 1h or 7d")
|
"is not a duration above zero, such as 1h or 7d")
|
||||||
errNotNumberAboveZero = errors.New(
|
errNotNumberAboveZero = errors.New(
|
||||||
@@ -627,7 +627,7 @@ func FromEnvironment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Command reads only SWW_COMMAND, the command the image runs and its
|
// Command reads only SWW_COMMAND, the command the image runs and its
|
||||||
// health check checks: rproxy when it is unset or empty, or discriminator. It cannot
|
// health check checks: rproxy when it is unset or empty, or waf. It cannot
|
||||||
// be given as a file. Any other value is an error that names the setting.
|
// be given as a file. Any other value is an error that names the setting.
|
||||||
func Command(lookupEnv func(string) (string, bool)) (string, error) {
|
func Command(lookupEnv func(string) (string, bool)) (string, error) {
|
||||||
command, _ := lookupEnv("SWW_COMMAND")
|
command, _ := lookupEnv("SWW_COMMAND")
|
||||||
@@ -635,17 +635,17 @@ func Command(lookupEnv func(string) (string, bool)) (string, error) {
|
|||||||
switch command {
|
switch command {
|
||||||
case "", "rproxy":
|
case "", "rproxy":
|
||||||
return "rproxy", nil
|
return "rproxy", nil
|
||||||
case "discriminator":
|
case "waf":
|
||||||
return command, nil
|
return command, nil
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("SWW_COMMAND: %q %w", command, errNotCommand)
|
return "", fmt.Errorf("SWW_COMMAND: %q %w", command, errNotCommand)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiscriminatorListenAddr reads only SWW_LISTEN_ADDR, which may be given as a
|
// WAFListenAddr reads only SWW_LISTEN_ADDR, which may be given as a
|
||||||
// file, as FromEnvironment does. The health check in discriminator's container needs
|
// file, as FromEnvironment does. The health check in waf's container needs
|
||||||
// no other setting, so it reads no other, nor a file that another names.
|
// no other setting, so it reads no other, nor a file that another names.
|
||||||
func DiscriminatorListenAddr(lookupEnv func(string) (string, bool)) (string, error) {
|
func WAFListenAddr(lookupEnv func(string) (string, bool)) (string, error) {
|
||||||
env := &environment{lookupEnv: lookupEnv}
|
env := &environment{lookupEnv: lookupEnv}
|
||||||
listenAddr := env.address("SWW_LISTEN_ADDR")
|
listenAddr := env.address("SWW_LISTEN_ADDR")
|
||||||
|
|
||||||
@@ -1610,7 +1610,7 @@ func parseSize(value string) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseWAFBodyLimit reads the most of a request body the Core Rule Set
|
// parseWAFBodyLimit reads the most of a request body the Core Rule Set
|
||||||
// reads: a size as parseSize reads it, or off, but at most 64K, since discriminator
|
// reads: a size as parseSize reads it, or off, but at most 64K, since waf
|
||||||
// gets no more of a body than that.
|
// gets no more of a body than that.
|
||||||
func parseWAFBodyLimit(value string) (int64, error) {
|
func parseWAFBodyLimit(value string) (int64, error) {
|
||||||
limit, err := parseSize(value)
|
limit, err := parseSize(value)
|
||||||
@@ -1955,7 +1955,7 @@ func ParseASN(value string) (string, error) {
|
|||||||
return "AS" + strconv.FormatUint(number, 10), nil
|
return "AS" + strconv.FormatUint(number, 10), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// The tags a request can get that the settings can name, as discriminator
|
// The tags a request can get that the settings can name, as waf
|
||||||
// writes them. The prefixes are followed by a value.
|
// writes them. The prefixes are followed by a value.
|
||||||
const (
|
const (
|
||||||
ASNTagPrefix = "asn="
|
ASNTagPrefix = "asn="
|
||||||
|
|||||||
@@ -636,11 +636,11 @@ func TestInvalidCoreRuleSetSettingStopsTheStart(t *testing.T) {
|
|||||||
wafBodyLimit, "128KB",
|
wafBodyLimit, "128KB",
|
||||||
`"128KB" is not a size such as 512K, 100M or 5G, or off`,
|
`"128KB" is not a size such as 512K, 100M or 5G, or off`,
|
||||||
},
|
},
|
||||||
{wafBodyLimit, "65K", `"65K" is more than 64K, the most discriminator ` +
|
{wafBodyLimit, "65K", `"65K" is more than 64K, the most waf ` +
|
||||||
`gets of a body`},
|
`gets of a body`},
|
||||||
{
|
{
|
||||||
wafBodyLimit, "65537",
|
wafBodyLimit, "65537",
|
||||||
`"65537" is more than 64K, the most discriminator gets of a body`,
|
`"65537" is more than 64K, the most waf gets of a body`,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ type RProxy struct {
|
|||||||
// TrustedProxies are the netblocks whose X-Forwarded-For is believed
|
// TrustedProxies are the netblocks whose X-Forwarded-For is believed
|
||||||
// (SWRP_TRUSTED_PROXIES).
|
// (SWRP_TRUSTED_PROXIES).
|
||||||
TrustedProxies []netip.Prefix
|
TrustedProxies []netip.Prefix
|
||||||
// WAFBackend is discriminator's URL (SWRP_WAF_BACKEND).
|
// WAFBackend is waf's URL (SWRP_WAF_BACKEND).
|
||||||
WAFBackend *url.URL
|
WAFBackend *url.URL
|
||||||
// Timeout is how long rproxy waits for discriminator's answer to a question,
|
// Timeout is how long rproxy waits for waf's answer to a question,
|
||||||
// and to a report (SWRP_TIMEOUT_MS). It cannot be off.
|
// and to a report (SWRP_TIMEOUT_MS). It cannot be off.
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
// DefaultDisposition is what is done with a request when discriminator cannot be
|
// DefaultDisposition is what is done with a request when waf cannot be
|
||||||
// connected to, does not answer at all, or does not answer within Timeout:
|
// connected to, does not answer at all, or does not answer within Timeout:
|
||||||
// allow or deny (SWRP_DEFAULT_DISPOSITION).
|
// allow or deny (SWRP_DEFAULT_DISPOSITION).
|
||||||
DefaultDisposition string
|
DefaultDisposition string
|
||||||
@@ -50,7 +50,7 @@ type RProxy struct {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
errNeedsWAFBackend = errors.New(
|
errNeedsWAFBackend = errors.New(
|
||||||
"is required: it is discriminator's URL, such as http://discriminator:8080")
|
"is required: it is waf's URL, such as http://waf:8080")
|
||||||
errNotDisposition = errors.New("is not allow or deny")
|
errNotDisposition = errors.New("is not allow or deny")
|
||||||
errNotMilliseconds = errors.New(
|
errNotMilliseconds = errors.New(
|
||||||
"is not a whole number of milliseconds above zero, such as 2000")
|
"is not a whole number of milliseconds above zero, such as 2000")
|
||||||
@@ -124,7 +124,7 @@ func (c *RProxy) LogValue() slog.Value {
|
|||||||
return slog.GroupValue(c.settings...)
|
return slog.GroupValue(c.settings...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// wafBackend reads the setting that is discriminator's URL, which must be set.
|
// wafBackend reads the setting that is waf's URL, which must be set.
|
||||||
func (e *environment) wafBackend(name string) *url.URL {
|
func (e *environment) wafBackend(name string) *url.URL {
|
||||||
value := e.value(name, "")
|
value := e.value(name, "")
|
||||||
if value == "" {
|
if value == "" {
|
||||||
@@ -139,7 +139,7 @@ func (e *environment) wafBackend(name string) *url.URL {
|
|||||||
return backend
|
return backend
|
||||||
}
|
}
|
||||||
|
|
||||||
// disposition reads the setting that is what is done with a request when discriminator
|
// disposition reads the setting that is what is done with a request when waf
|
||||||
// cannot be connected to, does not answer at all, or does not answer in time:
|
// cannot be connected to, does not answer at all, or does not answer in time:
|
||||||
// allow or deny.
|
// allow or deny.
|
||||||
func (e *environment) disposition(name, defaultValue string) string {
|
func (e *environment) disposition(name, defaultValue string) string {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const (
|
|||||||
rproxyMaxResponse = "SWRP_MAX_RESPONSE_SIZE"
|
rproxyMaxResponse = "SWRP_MAX_RESPONSE_SIZE"
|
||||||
rproxyRequestTime = "SWRP_MAX_REQUEST_TIME_MS"
|
rproxyRequestTime = "SWRP_MAX_REQUEST_TIME_MS"
|
||||||
rproxyAnswerTime = "SWRP_MAX_RESPONSE_TIME_MS"
|
rproxyAnswerTime = "SWRP_MAX_RESPONSE_TIME_MS"
|
||||||
backendURL = "http://discriminator:8080"
|
backendURL = "http://waf:8080"
|
||||||
rproxyDefaultAddr = ":8080"
|
rproxyDefaultAddr = ":8080"
|
||||||
rproxyDefaultApp = "http://127.0.0.1:8081"
|
rproxyDefaultApp = "http://127.0.0.1:8081"
|
||||||
notASize = "a lot"
|
notASize = "a lot"
|
||||||
@@ -114,8 +114,8 @@ func TestRProxyInvalidSettingsAreRefused(t *testing.T) {
|
|||||||
name, value string
|
name, value string
|
||||||
}{
|
}{
|
||||||
{wafBackend, ""},
|
{wafBackend, ""},
|
||||||
{wafBackend, "discriminator:8080"},
|
{wafBackend, "waf:8080"},
|
||||||
{wafBackend, "http://discriminator:8080/path"},
|
{wafBackend, "http://waf:8080/path"},
|
||||||
{rproxyTimeout, "0"},
|
{rproxyTimeout, "0"},
|
||||||
{rproxyTimeout, "-1"},
|
{rproxyTimeout, "-1"},
|
||||||
{rproxyTimeout, off},
|
{rproxyTimeout, off},
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ type History struct {
|
|||||||
ASName string `json:"as_name,omitempty"`
|
ASName string `json:"as_name,omitempty"`
|
||||||
Country string `json:"country,omitempty"`
|
Country string `json:"country,omitempty"`
|
||||||
LookedUp time.Time `json:"looked_up,omitzero"`
|
LookedUp time.Time `json:"looked_up,omitzero"`
|
||||||
// Requests are all the client's requests: Forwarded those discriminator allowed,
|
// Requests are all the client's requests: Forwarded those waf allowed,
|
||||||
// Refused those it refused, and neither those for a path under
|
// Refused those it refused, and neither those for a path under
|
||||||
// /_sww/.
|
// /_sww/.
|
||||||
Requests int64 `json:"requests"`
|
Requests int64 `json:"requests"`
|
||||||
@@ -136,7 +136,7 @@ type Offences struct {
|
|||||||
|
|
||||||
// Request is what a client's history keeps of one of its requests.
|
// Request is what a client's history keeps of one of its requests.
|
||||||
type Request struct {
|
type Request struct {
|
||||||
// Forwarded is true for a request discriminator allowed, and Refused for one it
|
// Forwarded is true for a request waf allowed, and Refused for one it
|
||||||
// refused. Both are false for a request for a path under /_sww/.
|
// refused. Both are false for a request for a path under /_sww/.
|
||||||
Forwarded bool
|
Forwarded bool
|
||||||
Refused bool
|
Refused bool
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package requestlog writes the lines sww prints on stdout: one
|
// Package requestlog writes the lines sww prints on stdout: one
|
||||||
// JSON object per request, two from discriminator and a short one from rproxy,
|
// JSON object per request, two from waf and a short one from rproxy,
|
||||||
// marked "type":"request", and the process's own messages as JSON lines marked
|
// marked "type":"request", and the process's own messages as JSON lines marked
|
||||||
// "type":"process".
|
// "type":"process".
|
||||||
package requestlog
|
package requestlog
|
||||||
@@ -14,9 +14,9 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The action a request line names: what discriminator did with the request.
|
// The action a request line names: what waf did with the request.
|
||||||
const (
|
const (
|
||||||
// ActionForward is a request discriminator allowed, which rproxy passes to
|
// ActionForward is a request waf allowed, which rproxy passes to
|
||||||
// the app.
|
// the app.
|
||||||
ActionForward = "forward"
|
ActionForward = "forward"
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ const (
|
|||||||
// ActionCountryDenied is a request refused because its client's country
|
// ActionCountryDenied is a request refused because its client's country
|
||||||
// is not in SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
// is not in SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
||||||
ActionCountryDenied = "country_denied"
|
ActionCountryDenied = "country_denied"
|
||||||
// ActionOwnEndpoint is a request discriminator answered at one of its own
|
// ActionOwnEndpoint is a request waf answered at one of its own
|
||||||
// endpoints, or a question about a path under /_sww/.
|
// endpoints, or a question about a path under /_sww/.
|
||||||
ActionOwnEndpoint = "own_endpoint"
|
ActionOwnEndpoint = "own_endpoint"
|
||||||
)
|
)
|
||||||
@@ -69,14 +69,14 @@ const OffenceLimit = "limit"
|
|||||||
// that broke the error burst.
|
// that broke the error burst.
|
||||||
const LimitHitMinuteRefusals = "minute_refusals"
|
const LimitHitMinuteRefusals = "minute_refusals"
|
||||||
|
|
||||||
// The discriminator_line a request line names, which says which of
|
// The waf_line a request line names, which says which of
|
||||||
// discriminator's two lines about a question it is.
|
// waf's two lines about a question it is.
|
||||||
const (
|
const (
|
||||||
// DiscriminatorLineDecision is discriminator's line at its decision.
|
// WAFLineDecision is waf's line at its decision.
|
||||||
DiscriminatorLineDecision = "decision"
|
WAFLineDecision = "decision"
|
||||||
// DiscriminatorLineReport is discriminator's line when rproxy's report
|
// WAFLineReport is waf's line when rproxy's report
|
||||||
// comes, with the decision's fields and the report's together.
|
// comes, with the decision's fields and the report's together.
|
||||||
DiscriminatorLineReport = "report"
|
WAFLineReport = "report"
|
||||||
)
|
)
|
||||||
|
|
||||||
// timeLayout is RFC 3339 with milliseconds.
|
// timeLayout is RFC 3339 with milliseconds.
|
||||||
@@ -89,9 +89,9 @@ const timeLayout = "2006-01-02T15:04:05.000Z07:00"
|
|||||||
//nolint:tagliatelle // docs/request-log.md names the fields in snake_case
|
//nolint:tagliatelle // docs/request-log.md names the fields in snake_case
|
||||||
type Line struct {
|
type Line struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
// DiscriminatorLine is DiscriminatorLineDecision or DiscriminatorLineReport
|
// WAFLine is WAFLineDecision or WAFLineReport
|
||||||
// on discriminator's two lines about a question.
|
// on waf's two lines about a question.
|
||||||
DiscriminatorLine string `json:"discriminator_line,omitempty"`
|
WAFLine string `json:"waf_line,omitempty"`
|
||||||
|
|
||||||
// The standard web log fields. Scheme is how the client reached rproxy, or
|
// The standard web log fields. Scheme is how the client reached rproxy, or
|
||||||
// the trusted proxy in front of it.
|
// the trusted proxy in front of it.
|
||||||
@@ -111,7 +111,7 @@ type Line struct {
|
|||||||
UserAgent string `json:"user_agent"`
|
UserAgent string `json:"user_agent"`
|
||||||
|
|
||||||
// Request detail. RequestID is the id rproxy made for the request, or for a
|
// Request detail. RequestID is the id rproxy made for the request, or for a
|
||||||
// request to discriminator's own endpoints a new one. ForwardedFor is the
|
// request to waf's own endpoints a new one. ForwardedFor is the
|
||||||
// X-Forwarded-For header as received. ClientGroup is the netblock the
|
// X-Forwarded-For header as received. ClientGroup is the netblock the
|
||||||
// client is counted as. ASN, ASName and Country are the client's AS
|
// client is counted as. ASN, ASName and Country are the client's AS
|
||||||
// number, AS name and country, as looked up.
|
// number, AS name and country, as looked up.
|
||||||
@@ -131,7 +131,7 @@ type Line struct {
|
|||||||
HasAuthorization bool `json:"has_authorization,omitempty"`
|
HasAuthorization bool `json:"has_authorization,omitempty"`
|
||||||
HasCookie bool `json:"has_cookie,omitempty"`
|
HasCookie bool `json:"has_cookie,omitempty"`
|
||||||
|
|
||||||
// Response detail, of discriminator's own answer. Aborted is true when
|
// Response detail, of waf's own answer. Aborted is true when
|
||||||
// rproxy, or the admin's tool, went away before the answer.
|
// rproxy, or the admin's tool, went away before the answer.
|
||||||
ResponseContentType string `json:"response_content_type,omitempty"`
|
ResponseContentType string `json:"response_content_type,omitempty"`
|
||||||
Aborted bool `json:"aborted,omitempty"`
|
Aborted bool `json:"aborted,omitempty"`
|
||||||
@@ -142,7 +142,7 @@ type Line struct {
|
|||||||
// taken with a request it would have refused: ActionClientDenied,
|
// taken with a request it would have refused: ActionClientDenied,
|
||||||
// ActionTemporarilyBanned, ActionPermanentlyBanned, ActionCountryDenied,
|
// ActionTemporarilyBanned, ActionPermanentlyBanned, ActionCountryDenied,
|
||||||
// ActionClientIsRateLimited, ActionBlockedByBlockRule,
|
// ActionClientIsRateLimited, ActionBlockedByBlockRule,
|
||||||
// ActionBlockedByCoreRuleSet, or, in discriminator,
|
// ActionBlockedByCoreRuleSet, or, in waf,
|
||||||
// ActionRequestHeaderTooLarge for the header limits.
|
// ActionRequestHeaderTooLarge for the header limits.
|
||||||
WouldAction string `json:"would_action,omitempty"`
|
WouldAction string `json:"would_action,omitempty"`
|
||||||
// Heuristic is the name of the heuristic that refused the request, in
|
// Heuristic is the name of the heuristic that refused the request, in
|
||||||
@@ -151,7 +151,7 @@ type Line struct {
|
|||||||
Heuristic string `json:"heuristic,omitempty"`
|
Heuristic string `json:"heuristic,omitempty"`
|
||||||
HeuristicFigures map[string]any `json:"heuristic_figures,omitempty"`
|
HeuristicFigures map[string]any `json:"heuristic_figures,omitempty"`
|
||||||
// AfterAnswerHeuristic and AfterAnswerHeuristicFigures are the same for
|
// AfterAnswerHeuristic and AfterAnswerHeuristicFigures are the same for
|
||||||
// the heuristic that banned the client after discriminator answered the
|
// the heuristic that banned the client after waf answered the
|
||||||
// request.
|
// request.
|
||||||
AfterAnswerHeuristic string `json:"after_answer_heuristic,omitempty"`
|
AfterAnswerHeuristic string `json:"after_answer_heuristic,omitempty"`
|
||||||
AfterAnswerHeuristicFigures map[string]any `json:"after_answer_heuristic_figures,omitempty"` //nolint:lll // a struct tag cannot be split
|
AfterAnswerHeuristicFigures map[string]any `json:"after_answer_heuristic_figures,omitempty"` //nolint:lll // a struct tag cannot be split
|
||||||
@@ -231,7 +231,7 @@ type RProxyLine struct {
|
|||||||
// sooner. DurationResponseMS runs from then until the answer ends.
|
// sooner. DurationResponseMS runs from then until the answer ends.
|
||||||
DurationRequestMS float64 `json:"duration_request_ms"`
|
DurationRequestMS float64 `json:"duration_request_ms"`
|
||||||
DurationResponseMS float64 `json:"duration_response_ms"`
|
DurationResponseMS float64 `json:"duration_response_ms"`
|
||||||
// Disposition is the valid disposition discriminator answered or, while
|
// Disposition is the valid disposition waf answered or, while
|
||||||
// DefaultDisposition is true, the value of SWRP_DEFAULT_DISPOSITION. It is ""
|
// DefaultDisposition is true, the value of SWRP_DEFAULT_DISPOSITION. It is ""
|
||||||
// when neither gave one.
|
// when neither gave one.
|
||||||
Disposition string `json:"disposition,omitempty"`
|
Disposition string `json:"disposition,omitempty"`
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// peerAddress is the address of the request's TCP peer, normally traefik, without
|
// peerAddress is the address of the request's TCP peer, normally traefik, without
|
||||||
// any IPv6 zone, which discriminator refuses.
|
// any IPv6 zone, which waf refuses.
|
||||||
func peerAddress(r *http.Request) netip.Addr {
|
func peerAddress(r *http.Request) netip.Addr {
|
||||||
addrPort, err := netip.ParseAddrPort(r.RemoteAddr)
|
addrPort, err := netip.ParseAddrPort(r.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -34,7 +34,7 @@ func peerAddress(r *http.Request) netip.Addr {
|
|||||||
// address outside them is the visitor; if every address in it is inside, the
|
// address outside them is the visitor; if every address in it is inside, the
|
||||||
// leftmost is, and with no header, the peer. An entry that is not an address
|
// leftmost is, and with no header, the peer. An entry that is not an address
|
||||||
// ends the reading, since nothing to its left can be believed. The visitor's
|
// ends the reading, since nothing to its left can be believed. The visitor's
|
||||||
// address never has an IPv6 zone, which discriminator refuses.
|
// address never has an IPv6 zone, which waf refuses.
|
||||||
func clientAddress(
|
func clientAddress(
|
||||||
peer netip.Addr, forwardedFor []string, trusted []netip.Prefix,
|
peer netip.Addr, forwardedFor []string, trusted []netip.Prefix,
|
||||||
) netip.Addr {
|
) netip.Addr {
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/rproxy"
|
"sneak.berlin/go/smallwebwaf/internal/rproxy"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -59,7 +59,7 @@ func TestServerHasTheFixedLimits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestABodyAnnouncedOverItsLimitIsRefusedOnceDiscriminatorAllowsIt(t *testing.T) {
|
func TestABodyAnnouncedOverItsLimitIsRefusedOnceWAFAllowsIt(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -72,7 +72,7 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceDiscriminatorAllowsIt(t *testing
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||||
t.Error("the app was asked")
|
t.Error("the app was asked")
|
||||||
})
|
})
|
||||||
@@ -101,7 +101,7 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceDiscriminatorAllowsIt(t *testing
|
|||||||
func TestADenyOutranksTheBodySizeLimit(t *testing.T) {
|
func TestADenyOutranksTheBodySizeLimit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, deny)
|
standIn := startWAF(t, deny)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
maxRequestSize: "1K",
|
maxRequestSize: "1K",
|
||||||
@@ -110,7 +110,7 @@ func TestADenyOutranksTheBodySizeLimit(t *testing.T) {
|
|||||||
got := visit(t, newRequest(t, http.MethodPost, addr, "/",
|
got := visit(t, newRequest(t, http.MethodPost, addr, "/",
|
||||||
bytes.NewReader(make([]byte, 1025))))
|
bytes.NewReader(make([]byte, 1025))))
|
||||||
if got.status != http.StatusForbidden {
|
if got.status != http.StatusForbidden {
|
||||||
t.Errorf("answered %+v, want discriminator's 403", got)
|
t.Errorf("answered %+v, want waf's 403", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ func TestABodyNotAnnouncedOverItsLimitIsRefused(t *testing.T) {
|
|||||||
|
|
||||||
var asked atomic.Bool
|
var asked atomic.Bool
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
asked.Store(true)
|
asked.Store(true)
|
||||||
greet(w, r)
|
greet(w, r)
|
||||||
@@ -166,7 +166,7 @@ func TestAResponseOverItsLimit(t *testing.T) {
|
|||||||
t.Run("announced, is refused", func(t *testing.T) {
|
t.Run("announced, is refused", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set(contentLengthHeader, strconv.Itoa(len(large)))
|
w.Header().Set(contentLengthHeader, strconv.Itoa(len(large)))
|
||||||
_, _ = io.WriteString(w, large)
|
_, _ = io.WriteString(w, large)
|
||||||
@@ -188,7 +188,7 @@ func TestAResponseOverItsLimit(t *testing.T) {
|
|||||||
t.Run("not announced, is cut off", func(t *testing.T) {
|
t.Run("not announced, is cut off", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
_, _ = io.WriteString(w, large[:1])
|
_, _ = io.WriteString(w, large[:1])
|
||||||
_ = http.NewResponseController(w).Flush()
|
_ = http.NewResponseController(w).Flush()
|
||||||
@@ -218,7 +218,7 @@ func TestAHeadWhoseAnswerAnnouncesMoreThanTheResponseLimitIsPassedOn(t *testing.
|
|||||||
|
|
||||||
const announced = "2048"
|
const announced = "2048"
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set(contentLengthHeader, announced)
|
w.Header().Set(contentLengthHeader, announced)
|
||||||
})
|
})
|
||||||
@@ -241,7 +241,7 @@ func TestASettingOfOffSetsNoLimit(t *testing.T) {
|
|||||||
t.Run(setting, func(t *testing.T) {
|
t.Run(setting, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
setting: "off",
|
setting: "off",
|
||||||
@@ -252,7 +252,7 @@ func TestASettingOfOffSetsNoLimit(t *testing.T) {
|
|||||||
wantGreeting(t, visit(t, newRequest(t, http.MethodPost, addr, "/",
|
wantGreeting(t, visit(t, newRequest(t, http.MethodPost, addr, "/",
|
||||||
strings.NewReader("a body"))))
|
strings.NewReader("a body"))))
|
||||||
|
|
||||||
if got := get(t, addr, discriminator.HealthPath); got.status != http.StatusOK {
|
if got := get(t, addr, waf.HealthPath); got.status != http.StatusOK {
|
||||||
t.Errorf("answered %+v, want rproxy's own 200", got)
|
t.Errorf("answered %+v, want rproxy's own 200", got)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -271,7 +271,7 @@ func TestAVisitorTooSlowToSendTheFirstPartIsAnswered408(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
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{
|
||||||
maxRequestTime: "500",
|
maxRequestTime: "500",
|
||||||
@@ -290,7 +290,7 @@ func TestAVisitorTooSlowToSendTheFirstPartIsAnswered408(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(standIn.questions) != 0 {
|
if len(standIn.questions) != 0 {
|
||||||
t.Error("discriminator was asked")
|
t.Error("waf was asked")
|
||||||
}
|
}
|
||||||
|
|
||||||
standIn.wantNoReport(t)
|
standIn.wantNoReport(t)
|
||||||
@@ -301,7 +301,7 @@ func TestAVisitorTooSlowToSendTheFirstPartIsAnswered408(t *testing.T) {
|
|||||||
func TestAVisitorThatGoesAwayBeforeTheFirstPartIsNotAsked(t *testing.T) {
|
func TestAVisitorThatGoesAwayBeforeTheFirstPartIsNotAsked(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
@@ -315,7 +315,7 @@ func TestAVisitorThatGoesAwayBeforeTheFirstPartIsNotAsked(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(standIn.questions) != 0 {
|
if len(standIn.questions) != 0 {
|
||||||
t.Error("discriminator was asked")
|
t.Error("waf was asked")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ func TestABodyThatCannotBeReadClosesTheConnectionWithoutAnAnswer(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ func TestABodyThatCannotBeReadClosesTheConnectionWithoutAnAnswer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(standIn.questions) != 0 {
|
if len(standIn.questions) != 0 {
|
||||||
t.Error("discriminator was asked")
|
t.Error("waf was asked")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -412,7 +412,7 @@ func TestABodyLeftUnreadIsReadOnlyUntilTheRequestTimeLimit(t *testing.T) {
|
|||||||
announced = 100 * questionBodyMaxBytes
|
announced = 100 * questionBodyMaxBytes
|
||||||
)
|
)
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
})
|
})
|
||||||
@@ -456,13 +456,13 @@ func TestAnAnswerRProxyGivesItselfHasTheResponseTimeToBeSent(t *testing.T) {
|
|||||||
name, target string
|
name, target string
|
||||||
status int
|
status int
|
||||||
}{
|
}{
|
||||||
{"discriminator's deny", "/", http.StatusForbidden},
|
{"waf's deny", "/", http.StatusForbidden},
|
||||||
{"rproxy's own 400", "x:admin", http.StatusBadRequest},
|
{"rproxy's own 400", "x:admin", http.StatusBadRequest},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, deny)
|
standIn := startWAF(t, deny)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
_, _, server := startServer(t, standIn.URL, app.URL, map[string]string{
|
_, _, server := startServer(t, standIn.URL, app.URL, map[string]string{
|
||||||
maxResponseTime: strconv.FormatInt(limit.Milliseconds(), 10),
|
maxResponseTime: strconv.FormatInt(limit.Milliseconds(), 10),
|
||||||
@@ -494,7 +494,7 @@ func TestAnAnswerRProxyGivesItselfHasTenSecondsToBeSentAtLeast(t *testing.T) {
|
|||||||
|
|
||||||
const least = 10 * time.Second
|
const least = 10 * time.Second
|
||||||
|
|
||||||
standIn := startDiscriminator(t, deny)
|
standIn := startWAF(t, deny)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
_, _, server := startServer(t, standIn.URL, app.URL, map[string]string{
|
_, _, server := startServer(t, standIn.URL, app.URL, map[string]string{
|
||||||
maxResponseTime: "300",
|
maxResponseTime: "300",
|
||||||
@@ -516,7 +516,7 @@ func TestAnAnswerRProxyGivesItselfHasTenSecondsToBeSentAtLeast(t *testing.T) {
|
|||||||
func TestAVisitorTooSlowToSendTheRestIsAnswered408(t *testing.T) {
|
func TestAVisitorTooSlowToSendTheRestIsAnswered408(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
maxRequestTime: "3000",
|
maxRequestTime: "3000",
|
||||||
@@ -585,7 +585,7 @@ func TestAnAppTooSlowToTakeTheRequestIsAnswered504(t *testing.T) {
|
|||||||
writeSize = 32 << 10
|
writeSize = 32 << 10
|
||||||
)
|
)
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
addr, _ := startRProxy(t, standIn.URL, startAppThatTakesNothing(t),
|
addr, _ := startRProxy(t, standIn.URL, startAppThatTakesNothing(t),
|
||||||
map[string]string{maxRequestTime: "3000"})
|
map[string]string{maxRequestTime: "3000"})
|
||||||
|
|
||||||
@@ -612,7 +612,7 @@ func TestAnAppTooSlowToAnswerIsAnswered504(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
release := make(chan struct{})
|
release := make(chan struct{})
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||||
<-release
|
<-release
|
||||||
})
|
})
|
||||||
@@ -658,7 +658,7 @@ func visitAsTheTimeRunsOut(t *testing.T) {
|
|||||||
// answerAfter is how long the app waits before it answers.
|
// answerAfter is how long the app waits before it answers.
|
||||||
var answerAfter atomic.Int64
|
var answerAfter atomic.Int64
|
||||||
|
|
||||||
backend := startDiscriminatorAnsweringReports(t, allow,
|
backend := startWAFAnsweringReports(t, allow,
|
||||||
func(w http.ResponseWriter, _ *http.Request) {
|
func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
})
|
})
|
||||||
@@ -701,7 +701,7 @@ func TestAnAppTooSlowToFinishItsAnswerIsCutOff(t *testing.T) {
|
|||||||
const start = "the start"
|
const start = "the start"
|
||||||
|
|
||||||
release := make(chan struct{})
|
release := make(chan struct{})
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
_, _ = io.WriteString(w, start)
|
_, _ = io.WriteString(w, start)
|
||||||
_ = http.NewResponseController(w).Flush()
|
_ = http.NewResponseController(w).Flush()
|
||||||
@@ -750,7 +750,7 @@ func TestAVisitorTooSlowToTakeTheAnswerIsCutOff(t *testing.T) {
|
|||||||
writeSize = 32 << 10
|
writeSize = 32 << 10
|
||||||
)
|
)
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
part := make([]byte, writeSize)
|
part := make([]byte, writeSize)
|
||||||
|
|
||||||
@@ -787,7 +787,7 @@ func TestAVisitorTooSlowToTakeTheAnswerIsCutOff(t *testing.T) {
|
|||||||
func TestAnAppThatCannotBeReachedIsAnswered502WithAReport(t *testing.T) {
|
func TestAnAppThatCannotBeReachedIsAnswered502WithAReport(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
addr, _ := startRProxy(t, standIn.URL, nowhere, nil)
|
addr, _ := startRProxy(t, standIn.URL, nowhere, nil)
|
||||||
|
|
||||||
got := get(t, addr, "/")
|
got := get(t, addr, "/")
|
||||||
@@ -803,7 +803,7 @@ func TestAnAppThatCannotBeReachedIsAnswered502WithAReport(t *testing.T) {
|
|||||||
func TestAVisitorThatStopsSendingAfterTheAppHasAnsweredGetsTheAnswer(t *testing.T) {
|
func TestAVisitorThatStopsSendingAfterTheAppHasAnsweredGetsTheAnswer(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
_, _ = io.WriteString(w, greeting)
|
_, _ = io.WriteString(w, greeting)
|
||||||
})
|
})
|
||||||
@@ -867,7 +867,7 @@ func readAnswer(t *testing.T, conn net.Conn) answered {
|
|||||||
func TestAVisitorThatStopsALittleShortGetsTheAppsAnswerAtOnce(t *testing.T) {
|
func TestAVisitorThatStopsALittleShortGetsTheAppsAnswerAtOnce(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, answerBeforeTheBody)
|
app := startApp(t, answerBeforeTheBody)
|
||||||
// The request time limit is longer than the 10 seconds the status and headers
|
// The request time limit is longer than the 10 seconds the status and headers
|
||||||
// of an answer have to be sent, and than dial's deadline.
|
// of an answer have to be sent, and than dial's deadline.
|
||||||
@@ -887,7 +887,7 @@ func TestAVisitorThatStopsALittleShortIsReadUntilTheRequestTimeLimit(t *testing.
|
|||||||
|
|
||||||
const limit = 2 * time.Second
|
const limit = 2 * time.Second
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, answerBeforeTheBody)
|
app := startApp(t, answerBeforeTheBody)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
maxRequestTime: strconv.FormatInt(limit.Milliseconds(), 10),
|
maxRequestTime: strconv.FormatInt(limit.Milliseconds(), 10),
|
||||||
@@ -914,7 +914,7 @@ func TestAVisitorThatStopsSendingWhileTheAppWorksGetsNoAnswer(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
working := make(chan struct{})
|
working := make(chan struct{})
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(_ http.ResponseWriter, r *http.Request) {
|
app := startApp(t, func(_ http.ResponseWriter, r *http.Request) {
|
||||||
close(working)
|
close(working)
|
||||||
<-r.Context().Done()
|
<-r.Context().Done()
|
||||||
@@ -955,7 +955,7 @@ func TestTheRequestTimeLimitDoesNotCutAnAnswerOnceTheAppHasTheRequest(t *testing
|
|||||||
|
|
||||||
const answer = "the end of a long answer"
|
const answer = "the end of a long answer"
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = io.Copy(io.Discard, r.Body)
|
_, _ = io.Copy(io.Discard, r.Body)
|
||||||
_ = http.NewResponseController(w).Flush()
|
_ = http.NewResponseController(w).Flush()
|
||||||
@@ -984,14 +984,14 @@ func TestAVisitorThatStopsALittleShortGetsAnAnswerRProxyGivesAtOnce(t *testing.T
|
|||||||
{"a deny", post, http.StatusForbidden},
|
{"a deny", post, http.StatusForbidden},
|
||||||
{
|
{
|
||||||
"the health check",
|
"the health check",
|
||||||
"GET " + discriminator.HealthPath + " HTTP/1.1\r\nHost: site.example\r\n",
|
"GET " + waf.HealthPath + " HTTP/1.1\r\nHost: site.example\r\n",
|
||||||
http.StatusOK,
|
http.StatusOK,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, deny)
|
standIn := startWAF(t, deny)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
// The request time limit is longer than the 10 seconds an answer rproxy
|
// The request time limit is longer than the 10 seconds an answer rproxy
|
||||||
// gives itself has to be sent, and than dial's deadline.
|
// gives itself has to be sent, and than dial's deadline.
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// questionBodyMaxBytes is the most of a body a question carries.
|
// questionBodyMaxBytes is the most of a body a question carries.
|
||||||
const questionBodyMaxBytes = discriminator.QuestionBodyMaxBytes
|
const questionBodyMaxBytes = waf.QuestionBodyMaxBytes
|
||||||
|
|
||||||
// chunked is the encoding of a body whose length the visitor did not announce.
|
// chunked is the encoding of a body whose length the visitor did not announce.
|
||||||
const chunked = "chunked"
|
const chunked = "chunked"
|
||||||
@@ -29,7 +29,7 @@ const pathGoReencodes = "/some%2Fpath/with%20space|pipe"
|
|||||||
func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
|
func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
@@ -42,9 +42,9 @@ func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
|
|||||||
req.Header.Set(forwardedForHeader, "198.51.100.1")
|
req.Header.Set(forwardedForHeader, "198.51.100.1")
|
||||||
req.Header.Set(forwardedProtoHeader, "gopher")
|
req.Header.Set(forwardedProtoHeader, "gopher")
|
||||||
req.Header.Set(requestIDHeader, "the-visitor's")
|
req.Header.Set(requestIDHeader, "the-visitor's")
|
||||||
req.Header.Set(discriminator.ClientHeader, "198.51.100.2")
|
req.Header.Set(waf.ClientHeader, "198.51.100.2")
|
||||||
req.Header.Set(discriminator.PeerHeader, "198.51.100.3")
|
req.Header.Set(waf.PeerHeader, "198.51.100.3")
|
||||||
req.Header.Set(discriminator.ContentLengthHeader, "12345")
|
req.Header.Set(waf.ContentLengthHeader, "12345")
|
||||||
// Go's client then sends no User-Agent, so that one in the question would be
|
// Go's client then sends no User-Agent, so that one in the question would be
|
||||||
// Go's own.
|
// Go's own.
|
||||||
req.Header["User-Agent"] = nil
|
req.Header["User-Agent"] = nil
|
||||||
@@ -68,8 +68,8 @@ func TestQuestionIsTheVisitorsRequestWithRProxysHeaders(t *testing.T) {
|
|||||||
want.Set(forwardedForHeader, "198.51.100.1")
|
want.Set(forwardedForHeader, "198.51.100.1")
|
||||||
want.Set(forwardedProtoHeader, "http")
|
want.Set(forwardedProtoHeader, "http")
|
||||||
want.Set(requestIDHeader, asked.header.Get(requestIDHeader))
|
want.Set(requestIDHeader, asked.header.Get(requestIDHeader))
|
||||||
want.Set(discriminator.ClientHeader, localhost)
|
want.Set(waf.ClientHeader, localhost)
|
||||||
want.Set(discriminator.PeerHeader, localhost)
|
want.Set(waf.PeerHeader, localhost)
|
||||||
|
|
||||||
if !maps.EqualFunc(asked.header, want, slices.Equal) {
|
if !maps.EqualFunc(asked.header, want, slices.Equal) {
|
||||||
t.Errorf("the question's headers are %v, want %v", asked.header, want)
|
t.Errorf("the question's headers are %v, want %v", asked.header, want)
|
||||||
@@ -93,7 +93,7 @@ func TestQuestionNamesTheVisitorATrustedProxySpeaksFor(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
forwarded := make(chan http.Header, 1)
|
forwarded := make(chan http.Header, 1)
|
||||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
forwarded <- r.Header.Clone()
|
forwarded <- r.Header.Clone()
|
||||||
@@ -110,8 +110,8 @@ func TestQuestionNamesTheVisitorATrustedProxySpeaksFor(t *testing.T) {
|
|||||||
wantGreeting(t, visit(t, req))
|
wantGreeting(t, visit(t, req))
|
||||||
|
|
||||||
asked := standIn.question(t)
|
asked := standIn.question(t)
|
||||||
if asked.header.Get(discriminator.ClientHeader) != tc.client ||
|
if asked.header.Get(waf.ClientHeader) != tc.client ||
|
||||||
asked.header.Get(discriminator.PeerHeader) != localhost ||
|
asked.header.Get(waf.PeerHeader) != localhost ||
|
||||||
asked.header.Get(forwardedForHeader) != visitor ||
|
asked.header.Get(forwardedForHeader) != visitor ||
|
||||||
asked.header.Get(forwardedProtoHeader) != tc.proto {
|
asked.header.Get(forwardedProtoHeader) != tc.proto {
|
||||||
t.Errorf("the question's headers are %v", asked.header)
|
t.Errorf("the question's headers are %v", asked.header)
|
||||||
@@ -157,7 +157,7 @@ func TestQuestionNamesTheClientAsTheForwardedAddressesSay(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
trustedProxies: localhost + "/32,10.0.0.0/8",
|
trustedProxies: localhost + "/32,10.0.0.0/8",
|
||||||
@@ -167,7 +167,7 @@ func TestQuestionNamesTheClientAsTheForwardedAddressesSay(t *testing.T) {
|
|||||||
req.Header[forwardedForHeader] = tc.header
|
req.Header[forwardedForHeader] = tc.header
|
||||||
wantGreeting(t, visit(t, req))
|
wantGreeting(t, visit(t, req))
|
||||||
|
|
||||||
if got := standIn.question(t).header.Get(discriminator.ClientHeader); got !=
|
if got := standIn.question(t).header.Get(waf.ClientHeader); got !=
|
||||||
tc.client {
|
tc.client {
|
||||||
t.Errorf("the question names %s, want %s", got, tc.client)
|
t.Errorf("the question names %s, want %s", got, tc.client)
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ func randomBytes(n int) []byte {
|
|||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFirst64KiBReachDiscriminatorAndTheWholeBodyTheApp(t *testing.T) {
|
func TestFirst64KiBReachWAFAndTheWholeBodyTheApp(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, size := range []int{
|
for _, size := range []int{
|
||||||
@@ -198,7 +198,7 @@ func TestFirst64KiBReachDiscriminatorAndTheWholeBodyTheApp(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
body := randomBytes(size)
|
body := randomBytes(size)
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
received := make(chan []byte, 1)
|
received := make(chan []byte, 1)
|
||||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
whole, _ := io.ReadAll(r.Body)
|
whole, _ := io.ReadAll(r.Body)
|
||||||
@@ -244,7 +244,7 @@ func wantQuestionBody(
|
|||||||
len(want))
|
len(want))
|
||||||
}
|
}
|
||||||
|
|
||||||
values := asked.header.Values(discriminator.ContentLengthHeader)
|
values := asked.header.Values(waf.ContentLengthHeader)
|
||||||
|
|
||||||
if announced {
|
if announced {
|
||||||
if !slices.Equal(values, []string{strconv.Itoa(size)}) ||
|
if !slices.Equal(values, []string{strconv.Itoa(size)}) ||
|
||||||
@@ -265,7 +265,7 @@ func wantQuestionBody(
|
|||||||
func TestDenyNeedsOnlyTheFirstPartOfALargeBody(t *testing.T) {
|
func TestDenyNeedsOnlyTheFirstPartOfALargeBody(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, deny)
|
standIn := startWAF(t, deny)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ func TestDenyNeedsOnlyTheFirstPartOfALargeBody(t *testing.T) {
|
|||||||
strings.Repeat("x", questionBodyMaxBytes))
|
strings.Repeat("x", questionBodyMaxBytes))
|
||||||
|
|
||||||
if status := readStatus(t, conn); status != http.StatusForbidden {
|
if status := readStatus(t, conn); status != http.StatusForbidden {
|
||||||
t.Errorf("status %d, want discriminator's 403", status)
|
t.Errorf("status %d, want waf's 403", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
if report := standIn.report(t); report.RequestBytes != questionBodyMaxBytes {
|
if report := standIn.report(t); report.RequestBytes != questionBodyMaxBytes {
|
||||||
@@ -307,14 +307,14 @@ func TestAQuestionCarriesItsBodyWhateverTheMethod(t *testing.T) {
|
|||||||
t.Run(method+" "+tc.name, func(t *testing.T) {
|
t.Run(method+" "+tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, deny)
|
standIn := startWAF(t, deny)
|
||||||
addr, _ := startRProxy(t, standIn.URL, nowhere, nil)
|
addr, _ := startRProxy(t, standIn.URL, nowhere, nil)
|
||||||
|
|
||||||
conn := dial(t, addr)
|
conn := dial(t, addr)
|
||||||
send(t, conn, method+" /x HTTP/1.1\r\nHost: site.example\r\n"+tc.framing)
|
send(t, conn, method+" /x HTTP/1.1\r\nHost: site.example\r\n"+tc.framing)
|
||||||
|
|
||||||
if status := readStatus(t, conn); status != http.StatusForbidden {
|
if status := readStatus(t, conn); status != http.StatusForbidden {
|
||||||
t.Errorf("status %d, want discriminator's 403", status)
|
t.Errorf("status %d, want waf's 403", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
asked := standIn.question(t)
|
asked := standIn.question(t)
|
||||||
@@ -335,7 +335,7 @@ func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
|
|||||||
t.Run(method, func(t *testing.T) {
|
t.Run(method, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||||
t.Error("the app was asked")
|
t.Error("the app was asked")
|
||||||
})
|
})
|
||||||
@@ -356,7 +356,7 @@ func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(standIn.questions) != 0 {
|
if len(standIn.questions) != 0 {
|
||||||
t.Error("discriminator was asked")
|
t.Error("waf was asked")
|
||||||
}
|
}
|
||||||
|
|
||||||
standIn.wantNoReport(t)
|
standIn.wantNoReport(t)
|
||||||
@@ -364,7 +364,7 @@ func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEachMethodPassedOnReachesDiscriminatorAndTheApp(t *testing.T) {
|
func TestEachMethodPassedOnReachesWAFAndTheApp(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, method := range []string{
|
for _, method := range []string{
|
||||||
@@ -377,7 +377,7 @@ func TestEachMethodPassedOnReachesDiscriminatorAndTheApp(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
reached := make(chan string, 1)
|
reached := make(chan string, 1)
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(_ http.ResponseWriter, r *http.Request) {
|
app := startApp(t, func(_ http.ResponseWriter, r *http.Request) {
|
||||||
reached <- r.Method
|
reached <- r.Method
|
||||||
})
|
})
|
||||||
@@ -389,7 +389,7 @@ func TestEachMethodPassedOnReachesDiscriminatorAndTheApp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if asked := standIn.question(t); asked.method != method {
|
if asked := standIn.question(t); asked.method != method {
|
||||||
t.Errorf("discriminator was asked about %s", asked.method)
|
t.Errorf("waf was asked about %s", asked.method)
|
||||||
}
|
}
|
||||||
|
|
||||||
if atApp := <-reached; atApp != method {
|
if atApp := <-reached; atApp != method {
|
||||||
@@ -399,7 +399,7 @@ func TestEachMethodPassedOnReachesDiscriminatorAndTheApp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestARequestThatCannotGoToDiscriminatorAsSentIsRefused(t *testing.T) {
|
func TestARequestThatCannotGoToWAFAsSentIsRefused(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const host = "Host: site.example\r\n\r\n"
|
const host = "Host: site.example\r\n\r\n"
|
||||||
@@ -421,7 +421,7 @@ func TestARequestThatCannotGoToDiscriminatorAsSentIsRefused(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
@@ -439,7 +439,7 @@ func TestARequestThatCannotGoToDiscriminatorAsSentIsRefused(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(standIn.questions) != 0 {
|
if len(standIn.questions) != 0 {
|
||||||
t.Error("discriminator was asked")
|
t.Error("waf was asked")
|
||||||
}
|
}
|
||||||
|
|
||||||
standIn.wantNoReport(t)
|
standIn.wantNoReport(t)
|
||||||
@@ -447,12 +447,12 @@ func TestARequestThatCannotGoToDiscriminatorAsSentIsRefused(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAHostWithAPercentSignButNoBracketGoesToDiscriminator(t *testing.T) {
|
func TestAHostWithAPercentSignButNoBracketGoesToWAF(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const host = "a%b"
|
const host = "a%b"
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
@@ -473,7 +473,7 @@ func TestATrustedPeerWithNoForwardedProtoIsAskedAboutAsHTTP(t *testing.T) {
|
|||||||
|
|
||||||
const want = "http"
|
const want = "http"
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
trustedProxies: localhost + "/32",
|
trustedProxies: localhost + "/32",
|
||||||
@@ -502,7 +502,7 @@ func TestAQuestionNamesNoZone(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, deny)
|
standIn := startWAF(t, deny)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
_, out, server := startServer(t, standIn.URL, app.URL, map[string]string{
|
_, out, server := startServer(t, standIn.URL, app.URL, map[string]string{
|
||||||
trustedProxies: localhost + "/32",
|
trustedProxies: localhost + "/32",
|
||||||
@@ -515,8 +515,8 @@ func TestAQuestionNamesNoZone(t *testing.T) {
|
|||||||
server.Handler.ServeHTTP(httptest.NewRecorder(), req)
|
server.Handler.ServeHTTP(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
asked := standIn.question(t)
|
asked := standIn.question(t)
|
||||||
if asked.header.Get(discriminator.ClientHeader) != tc.client ||
|
if asked.header.Get(waf.ClientHeader) != tc.client ||
|
||||||
asked.header.Get(discriminator.PeerHeader) != tc.peer {
|
asked.header.Get(waf.PeerHeader) != tc.peer {
|
||||||
t.Errorf("the question's headers are %v", asked.header)
|
t.Errorf("the question's headers are %v", asked.header)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -16,16 +16,16 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// flushAfterEachWrite has ReverseProxy pass on each part of the app's answer as
|
// flushAfterEachWrite has ReverseProxy pass on each part of the app's answer as
|
||||||
// soon as it arrives.
|
// soon as it arrives.
|
||||||
const flushAfterEachWrite time.Duration = -1
|
const flushAfterEachWrite time.Duration = -1
|
||||||
|
|
||||||
// reportStatusMax is the highest status discriminator takes in a report. An app can
|
// reportStatusMax is the highest status waf takes in a report. An app can
|
||||||
// send a higher one, up to 999, and the visitor gets that as it is.
|
// send a higher one, up to 999, and the visitor gets that as it is.
|
||||||
const reportStatusMax = 599
|
const reportStatusMax = 599
|
||||||
|
|
||||||
@@ -60,14 +60,14 @@ type request struct {
|
|||||||
peerTrusted bool
|
peerTrusted bool
|
||||||
start time.Time
|
start time.Time
|
||||||
|
|
||||||
// disposition is what is done with the request: discriminator's valid disposition,
|
// disposition is what is done with the request: waf's valid disposition,
|
||||||
// or SWRP_DEFAULT_DISPOSITION. It is "" when neither gave one.
|
// or SWRP_DEFAULT_DISPOSITION. It is "" when neither gave one.
|
||||||
disposition string
|
disposition string
|
||||||
// answered is true when discriminator gave disposition, which makes a report due,
|
// answered is true when waf gave disposition, which makes a report due,
|
||||||
// and byDefault when SWRP_DEFAULT_DISPOSITION did.
|
// and byDefault when SWRP_DEFAULT_DISPOSITION did.
|
||||||
answered bool
|
answered bool
|
||||||
byDefault bool
|
byDefault bool
|
||||||
// asn and country are discriminator's X-Client-ASN and X-Client-Country, for the
|
// asn and country are waf's X-Client-ASN and X-Client-Country, for the
|
||||||
// app.
|
// app.
|
||||||
asn, country string
|
asn, country string
|
||||||
|
|
||||||
@@ -131,14 +131,14 @@ func (rq *request) answerHealth() {
|
|||||||
// visitor too slow to send it is answered 408. For a malformed chunk, or a
|
// visitor too slow to send it is answered 408. For a malformed chunk, or a
|
||||||
// visitor that went away or stopped its sending side, the connection is closed
|
// visitor that went away or stopped its sending side, the connection is closed
|
||||||
// without an answer. A body over SWRP_MAX_REQUEST_SIZE goes on with the part
|
// without an answer. A body over SWRP_MAX_REQUEST_SIZE goes on with the part
|
||||||
// read, so that discriminator is asked first; passToApp then refuses it.
|
// read, so that waf is asked first; passToApp then refuses it.
|
||||||
func (rq *request) readFirstPartOfBody() bool {
|
func (rq *request) readFirstPartOfBody() bool {
|
||||||
if rq.body == nil {
|
if rq.body == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = rq.rc.SetReadDeadline(rq.requestDeadline())
|
_ = rq.rc.SetReadDeadline(rq.requestDeadline())
|
||||||
held, err := io.ReadAll(io.LimitReader(rq.body, discriminator.QuestionBodyMaxBytes))
|
held, err := io.ReadAll(io.LimitReader(rq.body, waf.QuestionBodyMaxBytes))
|
||||||
// From here on, the request timer ends a read of the rest of the body, so that
|
// From here on, the request timer ends a read of the rest of the body, so that
|
||||||
// the answer can name the side rproxy was waiting on.
|
// the answer can name the side rproxy was waiting on.
|
||||||
_ = rq.rc.SetReadDeadline(time.Time{})
|
_ = rq.rc.SetReadDeadline(time.Time{})
|
||||||
@@ -200,7 +200,7 @@ func (rq *request) forward(ctx context.Context) {
|
|||||||
// rewrite makes the request the app receives: the visitor's request, unchanged,
|
// rewrite makes the request the app receives: the visitor's request, unchanged,
|
||||||
// sent to SWRP_UPSTREAM_URL, with the forwarded headers and the request's id set,
|
// sent to SWRP_UPSTREAM_URL, with the forwarded headers and the request's id set,
|
||||||
// without any X-Client-ASN or X-Client-Country the visitor sent, and with
|
// without any X-Client-ASN or X-Client-Country the visitor sent, and with
|
||||||
// discriminator's.
|
// waf's.
|
||||||
func (rq *request) rewrite(pr *httputil.ProxyRequest) {
|
func (rq *request) rewrite(pr *httputil.ProxyRequest) {
|
||||||
upstream := rq.h.config.UpstreamURL
|
upstream := rq.h.config.UpstreamURL
|
||||||
pr.Out.URL.Scheme = upstream.Scheme
|
pr.Out.URL.Scheme = upstream.Scheme
|
||||||
@@ -333,7 +333,7 @@ func (rq *request) answer(status int) {
|
|||||||
// handler has returned, after finish has written the log line and sent the report.
|
// handler has returned, after finish has written the log line and sent the report.
|
||||||
// Without Content-Length, the flush would send the body in chunks, the last of
|
// Without Content-Length, the flush would send the body in chunks, the last of
|
||||||
// which would still wait for the handler. A HEAD's answer has no body, so any
|
// which would still wait for the handler. A HEAD's answer has no body, so any
|
||||||
// Content-Length it has stays as discriminator gave it.
|
// Content-Length it has stays as waf gave it.
|
||||||
func (rq *request) send(status int, body []byte) {
|
func (rq *request) send(status int, body []byte) {
|
||||||
if rq.in.Method != http.MethodHead {
|
if rq.in.Method != http.MethodHead {
|
||||||
rq.out.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
rq.out.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||||
@@ -379,7 +379,7 @@ func (rq *request) refuse(status int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// finish ends the request's time limits, writes its log line and, if discriminator
|
// finish ends the request's time limits, writes its log line and, if waf
|
||||||
// answered the question, sends the report. ctx is the request's own context.
|
// answered the question, sends the report. ctx is the request's own context.
|
||||||
func (rq *request) finish(ctx context.Context) {
|
func (rq *request) finish(ctx context.Context) {
|
||||||
rq.stopTimers()
|
rq.stopTimers()
|
||||||
@@ -416,7 +416,7 @@ func (rq *request) finish(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
report := &discriminator.Report{
|
report := &waf.Report{
|
||||||
RequestID: rq.id,
|
RequestID: rq.id,
|
||||||
Status: min(rq.out.status, reportStatusMax),
|
Status: min(rq.out.status, reportStatusMax),
|
||||||
RequestBytes: requestBytes,
|
RequestBytes: requestBytes,
|
||||||
|
|||||||
+28
-28
@@ -1,10 +1,10 @@
|
|||||||
// Package rproxy is the reverse proxy in front of one app that asks discriminator
|
// Package rproxy is the reverse proxy in front of one app that asks waf
|
||||||
// about each request. It passes a request discriminator allows to the app and the
|
// about each request. It passes a request waf allows to the app and the
|
||||||
// app's answer back, within the size and time limits. Any other answer from
|
// app's answer back, within the size and time limits. Any other answer from
|
||||||
// discriminator goes to the visitor as discriminator gave it. SWRP_DEFAULT_DISPOSITION
|
// waf goes to the visitor as waf gave it. SWRP_DEFAULT_DISPOSITION
|
||||||
// decides only when discriminator cannot be connected to, does not answer at all, or
|
// decides only when waf cannot be connected to, does not answer at all, or
|
||||||
// does not answer within SWRP_TIMEOUT_MS. rproxy itself refuses a request with a
|
// does not answer within SWRP_TIMEOUT_MS. rproxy itself refuses a request with a
|
||||||
// method it does not pass on, or one it cannot put to discriminator as the visitor
|
// method it does not pass on, or one it cannot put to waf as the visitor
|
||||||
// sent it. It keeps nothing between requests and never touches disk. "rproxy" in
|
// sent it. It keeps nothing between requests and never touches disk. "rproxy" in
|
||||||
// SPEC.md says what it does.
|
// SPEC.md says what it does.
|
||||||
package rproxy
|
package rproxy
|
||||||
@@ -22,10 +22,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// How rproxy keeps connections to the app and to discriminator open between
|
// How rproxy keeps connections to the app and to waf open between
|
||||||
// requests.
|
// requests.
|
||||||
const (
|
const (
|
||||||
idleConns = 100
|
idleConns = 100
|
||||||
@@ -80,14 +80,14 @@ func New(params Params) *Server {
|
|||||||
backend := params.Config.WAFBackend
|
backend := params.Config.WAFBackend
|
||||||
errorLog := slog.NewLogLogger(params.ProcessLog.Handler(), slog.LevelWarn)
|
errorLog := slog.NewLogLogger(params.ProcessLog.Handler(), slog.LevelWarn)
|
||||||
h := &handler{
|
h := &handler{
|
||||||
config: params.Config,
|
config: params.Config,
|
||||||
requestLog: params.RequestLog,
|
requestLog: params.RequestLog,
|
||||||
processLog: params.ProcessLog,
|
processLog: params.ProcessLog,
|
||||||
errorLog: errorLog,
|
errorLog: errorLog,
|
||||||
transport: newTransport(),
|
transport: newTransport(),
|
||||||
discriminatorTransport: newDiscriminatorTransport(),
|
wafTransport: newWAFTransport(),
|
||||||
reportURL: (&url.URL{
|
reportURL: (&url.URL{
|
||||||
Scheme: backend.Scheme, Host: backend.Host, Path: discriminator.ReportPath,
|
Scheme: backend.Scheme, Host: backend.Host, Path: waf.ReportPath,
|
||||||
}).String(),
|
}).String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,10 +122,10 @@ type handler struct {
|
|||||||
requestLog io.Writer
|
requestLog io.Writer
|
||||||
processLog *slog.Logger
|
processLog *slog.Logger
|
||||||
errorLog *log.Logger
|
errorLog *log.Logger
|
||||||
// transport carries requests to the app, and discriminatorTransport questions and
|
// transport carries requests to the app, and wafTransport questions and
|
||||||
// reports to discriminator.
|
// reports to waf.
|
||||||
transport http.RoundTripper
|
transport http.RoundTripper
|
||||||
discriminatorTransport http.RoundTripper
|
wafTransport http.RoundTripper
|
||||||
// reportURL is where the reports go.
|
// reportURL is where the reports go.
|
||||||
reportURL string
|
reportURL string
|
||||||
// reports waits for the reports being sent. reportsMu guards starting one
|
// reports waits for the reports being sent. reportsMu guards starting one
|
||||||
@@ -147,11 +147,11 @@ func newTransport() *http.Transport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newDiscriminatorTransport returns what carries questions and reports to
|
// newWAFTransport returns what carries questions and reports to
|
||||||
// discriminator. It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send
|
// waf. It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send
|
||||||
// a visitor's Upgrade header, or a Connection header other than close or
|
// a visitor's Upgrade header, or a Connection header other than close or
|
||||||
// keep-alive, and that would read as discriminator giving no answer.
|
// keep-alive, and that would read as waf giving no answer.
|
||||||
func newDiscriminatorTransport() *http.Transport {
|
func newWAFTransport() *http.Transport {
|
||||||
transport := newTransport()
|
transport := newTransport()
|
||||||
transport.Protocols = new(http.Protocols)
|
transport.Protocols = new(http.Protocols)
|
||||||
transport.Protocols.SetHTTP1(true)
|
transport.Protocols.SetHTTP1(true)
|
||||||
@@ -160,15 +160,15 @@ func newDiscriminatorTransport() *http.Transport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP handles one request: it answers the health check itself, refuses a
|
// ServeHTTP handles one request: it answers the health check itself, refuses a
|
||||||
// request with a method it does not pass on or one it cannot put to discriminator as
|
// request with a method it does not pass on or one it cannot put to waf as
|
||||||
// the visitor sent it, and for any other request reads the first part of the
|
// the visitor sent it, and for any other request reads the first part of the
|
||||||
// body, asks discriminator, and does what the answer says. Once the answer has ended,
|
// body, asks waf, and does what the answer says. Once the answer has ended,
|
||||||
// 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.finish(r.Context())
|
defer rq.finish(r.Context())
|
||||||
|
|
||||||
if r.Method == http.MethodGet && r.URL.Path == discriminator.HealthPath {
|
if r.Method == http.MethodGet && r.URL.Path == waf.HealthPath {
|
||||||
rq.answerHealth()
|
rq.answerHealth()
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -201,9 +201,9 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
rq.noteAnswer(answer)
|
rq.noteAnswer(answer)
|
||||||
|
|
||||||
switch rq.disposition {
|
switch rq.disposition {
|
||||||
case discriminator.DispositionAllow:
|
case waf.DispositionAllow:
|
||||||
rq.passToApp(r.Context())
|
rq.passToApp(r.Context())
|
||||||
case discriminator.DispositionClose:
|
case waf.DispositionClose:
|
||||||
// This panic has Go's server close the connection without an answer,
|
// This panic has Go's server close the connection without an answer,
|
||||||
// and log nothing; the deferred finish still runs.
|
// and log nothing; the deferred finish still runs.
|
||||||
panic(http.ErrAbortHandler)
|
panic(http.ErrAbortHandler)
|
||||||
@@ -213,7 +213,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// passToApp passes the request on to the app, unless a limit or its path keeps
|
// passToApp passes the request on to the app, unless a limit or its path keeps
|
||||||
// it from the app. discriminator answers a path under ownEndpointPrefix itself, so
|
// it from the app. waf answers a path under ownEndpointPrefix itself, so
|
||||||
// such a request gets here only when SWRP_DEFAULT_DISPOSITION allowed it. ctx is the
|
// such a request gets here only when SWRP_DEFAULT_DISPOSITION allowed it. ctx is the
|
||||||
// request's own context.
|
// request's own context.
|
||||||
func (rq *request) passToApp(ctx context.Context) {
|
func (rq *request) passToApp(ctx context.Context) {
|
||||||
|
|||||||
+101
-101
@@ -19,9 +19,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/rproxy"
|
"sneak.berlin/go/smallwebwaf/internal/rproxy"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -54,27 +54,27 @@ const (
|
|||||||
|
|
||||||
// The headers the tests set and look at.
|
// The headers the tests set and look at.
|
||||||
const (
|
const (
|
||||||
dispositionHeader = discriminator.DispositionHeader
|
dispositionHeader = waf.DispositionHeader
|
||||||
forwardedForHeader = "X-Forwarded-For"
|
forwardedForHeader = "X-Forwarded-For"
|
||||||
forwardedProtoHeader = "X-Forwarded-Proto"
|
forwardedProtoHeader = "X-Forwarded-Proto"
|
||||||
requestIDHeader = "X-Request-ID"
|
requestIDHeader = "X-Request-ID"
|
||||||
asnHeader = "X-Client-Asn"
|
asnHeader = "X-Client-Asn"
|
||||||
countryHeader = "X-Client-Country"
|
countryHeader = "X-Client-Country"
|
||||||
contentLengthHeader = "Content-Length"
|
contentLengthHeader = "Content-Length"
|
||||||
// fromDiscriminatorHeader is a header the stand-in for discriminator adds to a deny.
|
// fromWAFHeader is a header the stand-in for waf adds to a deny.
|
||||||
fromDiscriminatorHeader = "X-From-Discriminator"
|
fromWAFHeader = "X-From-Waf"
|
||||||
// addedHeaderValue is the value of a header a test adds to an answer, to see it
|
// addedHeaderValue is the value of a header a test adds to an answer, to see it
|
||||||
// reach the visitor.
|
// reach the visitor.
|
||||||
addedHeaderValue = "yes"
|
addedHeaderValue = "yes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// What the stand-in for discriminator answers.
|
// What the stand-in for waf answers.
|
||||||
const (
|
const (
|
||||||
dispositionAllow = discriminator.DispositionAllow
|
dispositionAllow = waf.DispositionAllow
|
||||||
dispositionDeny = discriminator.DispositionDeny
|
dispositionDeny = waf.DispositionDeny
|
||||||
dispositionClose = discriminator.DispositionClose
|
dispositionClose = waf.DispositionClose
|
||||||
// deniedBody is the body of a deny.
|
// deniedBody is the body of a deny.
|
||||||
deniedBody = "refused by discriminator"
|
deniedBody = "refused by waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// output collects what rproxy writes on stdout.
|
// output collects what rproxy writes on stdout.
|
||||||
@@ -158,7 +158,7 @@ func (l heldLog) Write(p []byte) (int, error) {
|
|||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// question is what the stand-in for discriminator received as a question.
|
// question is what the stand-in for waf received as a question.
|
||||||
type question struct {
|
type question struct {
|
||||||
method, target, host string
|
method, target, host string
|
||||||
header http.Header
|
header http.Header
|
||||||
@@ -167,26 +167,26 @@ type question struct {
|
|||||||
transferEncoding []string
|
transferEncoding []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discriminator is a stand-in for discriminator that answers each question as
|
// WAF is a stand-in for waf that answers each question as
|
||||||
// answer does, and keeps the questions and the reports it gets.
|
// answer does, and keeps the questions and the reports it gets.
|
||||||
type Discriminator struct {
|
type WAF struct {
|
||||||
*httptest.Server
|
*httptest.Server
|
||||||
|
|
||||||
questions chan question
|
questions chan question
|
||||||
reports chan discriminator.Report
|
reports chan waf.Report
|
||||||
}
|
}
|
||||||
|
|
||||||
// startDiscriminator starts a stand-in for discriminator.
|
// startWAF starts a stand-in for waf.
|
||||||
func startDiscriminator(t *testing.T, answer http.HandlerFunc) *Discriminator {
|
func startWAF(t *testing.T, answer http.HandlerFunc) *WAF {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
s := &Discriminator{
|
s := &WAF{
|
||||||
questions: make(chan question, 10),
|
questions: make(chan question, 10),
|
||||||
reports: make(chan discriminator.Report, 10),
|
reports: make(chan waf.Report, 10),
|
||||||
}
|
}
|
||||||
s.Server = httptest.NewServer(http.HandlerFunc(
|
s.Server = httptest.NewServer(http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Header.Get(discriminator.ClientHeader) == "" {
|
if r.Header.Get(waf.ClientHeader) == "" {
|
||||||
s.takeReport(t, w, r)
|
s.takeReport(t, w, r)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -209,17 +209,17 @@ func startDiscriminator(t *testing.T, answer http.HandlerFunc) *Discriminator {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// takeReport keeps a report and answers it as discriminator does.
|
// takeReport keeps a report and answers it as waf does.
|
||||||
func (s *Discriminator) takeReport(
|
func (s *WAF) takeReport(
|
||||||
t *testing.T, w http.ResponseWriter, r *http.Request,
|
t *testing.T, w http.ResponseWriter, r *http.Request,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
var report discriminator.Report
|
var report waf.Report
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&report)
|
err := json.NewDecoder(r.Body).Decode(&report)
|
||||||
if err != nil || r.Method != http.MethodPost ||
|
if err != nil || r.Method != http.MethodPost ||
|
||||||
r.URL.Path != discriminator.ReportPath {
|
r.URL.Path != waf.ReportPath {
|
||||||
t.Errorf("%s %s is no report: %v", r.Method, r.URL.Path, err)
|
t.Errorf("%s %s is no report: %v", r.Method, r.URL.Path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ func (s *Discriminator) takeReport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wantNoReport checks that no report comes for a moment.
|
// wantNoReport checks that no report comes for a moment.
|
||||||
func (s *Discriminator) wantNoReport(t *testing.T) {
|
func (s *WAF) wantNoReport(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -240,7 +240,7 @@ func (s *Discriminator) wantNoReport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// report waits for a report.
|
// report waits for a report.
|
||||||
func (s *Discriminator) report(t *testing.T) discriminator.Report {
|
func (s *WAF) report(t *testing.T) waf.Report {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -249,12 +249,12 @@ func (s *Discriminator) report(t *testing.T) discriminator.Report {
|
|||||||
case <-time.After(waitLimit):
|
case <-time.After(waitLimit):
|
||||||
t.Fatal("no report")
|
t.Fatal("no report")
|
||||||
|
|
||||||
return discriminator.Report{}
|
return waf.Report{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// question waits for a question.
|
// question waits for a question.
|
||||||
func (s *Discriminator) question(t *testing.T) question {
|
func (s *WAF) question(t *testing.T) question {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -267,17 +267,17 @@ func (s *Discriminator) question(t *testing.T) question {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// startDiscriminatorAnsweringReports starts a stand-in for discriminator that
|
// startWAFAnsweringReports starts a stand-in for waf that
|
||||||
// answers each question as answer does and each report as report does, and
|
// answers each question as answer does and each report as report does, and
|
||||||
// returns its URL.
|
// returns its URL.
|
||||||
func startDiscriminatorAnsweringReports(
|
func startWAFAnsweringReports(
|
||||||
t *testing.T, answer, report http.HandlerFunc,
|
t *testing.T, answer, report http.HandlerFunc,
|
||||||
) string {
|
) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
backend := httptest.NewServer(http.HandlerFunc(
|
backend := httptest.NewServer(http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Header.Get(discriminator.ClientHeader) != "" {
|
if r.Header.Get(waf.ClientHeader) != "" {
|
||||||
answer(w, r)
|
answer(w, r)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -299,7 +299,7 @@ func allow(w http.ResponseWriter, _ *http.Request) {
|
|||||||
// deny answers a question with deny, status 403 and a body of its own.
|
// deny answers a question with deny, status 403 and a body of its own.
|
||||||
func deny(w http.ResponseWriter, _ *http.Request) {
|
func deny(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set(dispositionHeader, dispositionDeny)
|
w.Header().Set(dispositionHeader, dispositionDeny)
|
||||||
w.Header().Set(fromDiscriminatorHeader, addedHeaderValue)
|
w.Header().Set(fromWAFHeader, addedHeaderValue)
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
_, _ = io.WriteString(w, deniedBody)
|
_, _ = io.WriteString(w, deniedBody)
|
||||||
}
|
}
|
||||||
@@ -320,7 +320,7 @@ func greet(w http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = io.WriteString(w, greeting)
|
_, _ = io.WriteString(w, greeting)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startRProxy starts rproxy with discriminator at backend and the app at appURL, and
|
// startRProxy starts rproxy with waf at backend and the app at appURL, and
|
||||||
// the settings in env on top of the defaults. It returns where rproxy listens
|
// the settings in env on top of the defaults. It returns where rproxy listens
|
||||||
// and what it writes.
|
// and what it writes.
|
||||||
func startRProxy(
|
func startRProxy(
|
||||||
@@ -502,7 +502,7 @@ func readStatus(t *testing.T, conn net.Conn) int {
|
|||||||
func TestPassesAnAllowedRequestToTheAppAndItsAnswerBack(t *testing.T) {
|
func TestPassesAnAllowedRequestToTheAppAndItsAnswerBack(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, r *http.Request) {
|
standIn := startWAF(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set(asnHeader, "AS64496")
|
w.Header().Set(asnHeader, "AS64496")
|
||||||
w.Header().Set(countryHeader, "DE")
|
w.Header().Set(countryHeader, "DE")
|
||||||
allow(w, r)
|
allow(w, r)
|
||||||
@@ -561,7 +561,7 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) {
|
|||||||
"X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip",
|
"X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip",
|
||||||
}
|
}
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
seen := make(chan http.Header, 1)
|
seen := make(chan http.Header, 1)
|
||||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
seen <- r.Header.Clone()
|
seen <- r.Header.Clone()
|
||||||
@@ -606,7 +606,7 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) {
|
|||||||
func TestATrustedProxysForwardedHeadersPassOn(t *testing.T) {
|
func TestATrustedProxysForwardedHeadersPassOn(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
seen := make(chan http.Header, 1)
|
seen := make(chan http.Header, 1)
|
||||||
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
seen <- r.Header.Clone()
|
seen <- r.Header.Clone()
|
||||||
@@ -645,7 +645,7 @@ func TestATrustedProxysForwardedHeadersPassOn(t *testing.T) {
|
|||||||
func TestAnAnswerWithoutAContentTypeGetsNone(t *testing.T) {
|
func TestAnAnswerWithoutAContentTypeGetsNone(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header()["Content-Type"] = nil
|
w.Header()["Content-Type"] = nil
|
||||||
_, _ = io.WriteString(w, "<html>")
|
_, _ = io.WriteString(w, "<html>")
|
||||||
@@ -667,19 +667,19 @@ func TestAnAnswerWithoutAContentTypeGetsNone(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemovesTheVisitorsLookupHeadersWhenDiscriminatorGivesNone(t *testing.T) {
|
func TestRemovesTheVisitorsLookupHeadersWhenWAFGivesNone(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
backend func(t *testing.T) string
|
backend func(t *testing.T) string
|
||||||
}{
|
}{
|
||||||
{"discriminator allows", func(t *testing.T) string {
|
{"waf allows", func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return startDiscriminator(t, allow).URL
|
return startWAF(t, allow).URL
|
||||||
}},
|
}},
|
||||||
{"discriminator is down", func(*testing.T) string { return nowhere }},
|
{"waf is down", func(*testing.T) string { return nowhere }},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -705,10 +705,10 @@ func TestRemovesTheVisitorsLookupHeadersWhenDiscriminatorGivesNone(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDenyGivesTheVisitorDiscriminatorsAnswer(t *testing.T) {
|
func TestDenyGivesTheVisitorWAFsAnswer(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, deny)
|
standIn := startWAF(t, deny)
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||||
t.Error("the app was asked")
|
t.Error("the app was asked")
|
||||||
})
|
})
|
||||||
@@ -716,8 +716,8 @@ func TestDenyGivesTheVisitorDiscriminatorsAnswer(t *testing.T) {
|
|||||||
|
|
||||||
got := get(t, addr, "/")
|
got := get(t, addr, "/")
|
||||||
if got.status != http.StatusForbidden || got.body != deniedBody ||
|
if got.status != http.StatusForbidden || got.body != deniedBody ||
|
||||||
got.header.Get(fromDiscriminatorHeader) != addedHeaderValue {
|
got.header.Get(fromWAFHeader) != addedHeaderValue {
|
||||||
t.Errorf("answered %+v, want discriminator's answer", got)
|
t.Errorf("answered %+v, want waf's answer", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(got.header.Values(dispositionHeader)) != 0 {
|
if len(got.header.Values(dispositionHeader)) != 0 {
|
||||||
@@ -733,21 +733,21 @@ func TestDenyGivesTheVisitorDiscriminatorsAnswer(t *testing.T) {
|
|||||||
|
|
||||||
line := out.requestLine(t)
|
line := out.requestLine(t)
|
||||||
if line["disposition"] != dispositionDeny || line["default_disposition"] != nil {
|
if line["disposition"] != dispositionDeny || line["default_disposition"] != nil {
|
||||||
t.Errorf("line %v, want discriminator's deny", line)
|
t.Errorf("line %v, want waf's deny", line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestADeniedHeadKeepsDiscriminatorsContentLength(t *testing.T) {
|
func TestADeniedHeadKeepsWAFsContentLength(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, deny)
|
standIn := startWAF(t, deny)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
got := visit(t, newRequest(t, http.MethodHead, addr, "/", http.NoBody))
|
got := visit(t, newRequest(t, http.MethodHead, addr, "/", http.NoBody))
|
||||||
if got.status != http.StatusForbidden ||
|
if got.status != http.StatusForbidden ||
|
||||||
got.header.Get(contentLengthHeader) != strconv.Itoa(len(deniedBody)) {
|
got.header.Get(contentLengthHeader) != strconv.Itoa(len(deniedBody)) {
|
||||||
t.Errorf("answered %+v, want discriminator's 403 and its length", got)
|
t.Errorf("answered %+v, want waf's 403 and its length", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,13 +762,13 @@ func TestAnAnswerRProxyHoldsWholeIsSentBeforeItsLogLine(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"a deny", "GET /" + host, deniedBody, http.StatusForbidden},
|
{"a deny", "GET /" + host, deniedBody, http.StatusForbidden},
|
||||||
{"rproxy's own answer", "GET x:admin" + host, "Bad Request\n", http.StatusBadRequest},
|
{"rproxy's own answer", "GET x:admin" + host, "Bad Request\n", http.StatusBadRequest},
|
||||||
{"the health check", "GET " + discriminator.HealthPath + host, "ok\n", http.StatusOK},
|
{"the health check", "GET " + waf.HealthPath + host, "ok\n", http.StatusOK},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
log := heldLog{release: make(chan struct{})}
|
log := heldLog{release: make(chan struct{})}
|
||||||
standIn := startDiscriminator(t, deny)
|
standIn := startWAF(t, deny)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startServerLogging(t, standIn.URL, app.URL, nil, log, io.Discard)
|
addr, _ := startServerLogging(t, standIn.URL, app.URL, nil, log, io.Discard)
|
||||||
|
|
||||||
@@ -798,7 +798,7 @@ func TestAnAnswerRProxyHoldsWholeIsSentBeforeItsLogLine(t *testing.T) {
|
|||||||
func TestRProxysOwnAnswerIsPlainText(t *testing.T) {
|
func TestRProxysOwnAnswerIsPlainText(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
addr, _ := startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
@@ -821,7 +821,7 @@ func TestRProxysOwnAnswerIsPlainText(t *testing.T) {
|
|||||||
func TestCloseClosesTheConnectionWithoutAnAnswer(t *testing.T) {
|
func TestCloseClosesTheConnectionWithoutAnAnswer(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, _ *http.Request) {
|
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set(dispositionHeader, dispositionClose)
|
w.Header().Set(dispositionHeader, dispositionClose)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
@@ -853,7 +853,7 @@ func TestCloseClosesTheConnectionWithoutAnAnswer(t *testing.T) {
|
|||||||
func TestALineForARequestClosedWithoutAnAnswerHasNoNegativeTime(t *testing.T) {
|
func TestALineForARequestClosedWithoutAnAnswerHasNoNegativeTime(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
backend := startDiscriminatorAnsweringReports(t,
|
backend := startWAFAnsweringReports(t,
|
||||||
func(w http.ResponseWriter, _ *http.Request) {
|
func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set(dispositionHeader, dispositionClose)
|
w.Header().Set(dispositionHeader, dispositionClose)
|
||||||
},
|
},
|
||||||
@@ -878,17 +878,17 @@ func TestALineForARequestClosedWithoutAnAnswerHasNoNegativeTime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// answeringWith returns what starts a stand-in for discriminator that answers each
|
// answeringWith returns what starts a stand-in for waf that answers each
|
||||||
// question as answer does, and returns its URL.
|
// question as answer does, and returns its URL.
|
||||||
func answeringWith(answer http.HandlerFunc) func(t *testing.T) string {
|
func answeringWith(answer http.HandlerFunc) func(t *testing.T) string {
|
||||||
return func(t *testing.T) string {
|
return func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return startDiscriminator(t, answer).URL
|
return startWAF(t, answer).URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// noAnswers are the ways discriminator can leave the decision to the default.
|
// noAnswers are the ways waf can leave the decision to the default.
|
||||||
func noAnswers() map[string]func(t *testing.T) string {
|
func noAnswers() map[string]func(t *testing.T) string {
|
||||||
return map[string]func(t *testing.T) string{
|
return map[string]func(t *testing.T) string{
|
||||||
"no connection": func(*testing.T) string { return nowhere },
|
"no connection": func(*testing.T) string { return nowhere },
|
||||||
@@ -920,7 +920,7 @@ func noAnswers() map[string]func(t *testing.T) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.T) {
|
func TestAnAnswerOtherThanAllowGoesToTheVisitorAsWAFGaveIt(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -929,7 +929,7 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.
|
|||||||
}{
|
}{
|
||||||
{"a refusal", dispositionDeny, deniedBody, http.StatusForbidden},
|
{"a refusal", dispositionDeny, deniedBody, http.StatusForbidden},
|
||||||
{"a refusal, 429", dispositionDeny, deniedBody, http.StatusTooManyRequests},
|
{"a refusal, 429", dispositionDeny, deniedBody, http.StatusTooManyRequests},
|
||||||
{"discriminator's 400", "", "refused\n", http.StatusBadRequest},
|
{"waf's 400", "", "refused\n", http.StatusBadRequest},
|
||||||
{"a 500", "", "broken\n", http.StatusInternalServerError},
|
{"a 500", "", "broken\n", http.StatusInternalServerError},
|
||||||
{"a 503 denied", dispositionDeny, "busy\n", http.StatusServiceUnavailable},
|
{"a 503 denied", dispositionDeny, "busy\n", http.StatusServiceUnavailable},
|
||||||
{"no header", "", "who knows\n", http.StatusOK},
|
{"no header", "", "who knows\n", http.StatusOK},
|
||||||
@@ -939,12 +939,12 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.
|
|||||||
t.Run(tc.name+" with the default "+setting, func(t *testing.T) {
|
t.Run(tc.name+" with the default "+setting, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, _ *http.Request) {
|
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
if tc.disposition != "" {
|
if tc.disposition != "" {
|
||||||
w.Header().Set(dispositionHeader, tc.disposition)
|
w.Header().Set(dispositionHeader, tc.disposition)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set(fromDiscriminatorHeader, addedHeaderValue)
|
w.Header().Set(fromWAFHeader, addedHeaderValue)
|
||||||
w.WriteHeader(tc.status)
|
w.WriteHeader(tc.status)
|
||||||
_, _ = io.WriteString(w, tc.body)
|
_, _ = io.WriteString(w, tc.body)
|
||||||
})
|
})
|
||||||
@@ -957,8 +957,8 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.
|
|||||||
|
|
||||||
got := get(t, addr, "/")
|
got := get(t, addr, "/")
|
||||||
if got.status != tc.status || got.body != tc.body ||
|
if got.status != tc.status || got.body != tc.body ||
|
||||||
got.header.Get(fromDiscriminatorHeader) != addedHeaderValue {
|
got.header.Get(fromWAFHeader) != addedHeaderValue {
|
||||||
t.Errorf("answered %+v, want discriminator's answer", got)
|
t.Errorf("answered %+v, want waf's answer", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(got.header.Values(dispositionHeader)) != 0 {
|
if len(got.header.Values(dispositionHeader)) != 0 {
|
||||||
@@ -970,8 +970,8 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.
|
|||||||
t.Errorf("line %v, want status %d and no default", line, tc.status)
|
t.Errorf("line %v, want status %d and no default", line, tc.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
if out.has("discriminator gave no answer") {
|
if out.has("waf gave no answer") {
|
||||||
t.Error("rproxy warned that discriminator gave no answer")
|
t.Error("rproxy warned that waf gave no answer")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -981,14 +981,14 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsDiscriminatorGaveIt(t *testing.
|
|||||||
func TestAnAnswerWithoutAValidDispositionGetsNoReport(t *testing.T) {
|
func TestAnAnswerWithoutAValidDispositionGetsNoReport(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, _ *http.Request) {
|
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
http.Error(w, "refused", http.StatusBadRequest)
|
http.Error(w, "refused", http.StatusBadRequest)
|
||||||
})
|
})
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
if got := get(t, addr, "/"); got.status != http.StatusBadRequest {
|
if got := get(t, addr, "/"); got.status != http.StatusBadRequest {
|
||||||
t.Errorf("answered %+v, want discriminator's 400", got)
|
t.Errorf("answered %+v, want waf's 400", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
if line := out.requestLine(t); line["disposition"] != nil {
|
if line := out.requestLine(t); line["disposition"] != nil {
|
||||||
@@ -1001,7 +1001,7 @@ func TestAnAnswerWithoutAValidDispositionGetsNoReport(t *testing.T) {
|
|||||||
func TestAnAnswerWithoutAContentTypeGoesToTheVisitorWithNone(t *testing.T) {
|
func TestAnAnswerWithoutAContentTypeGoesToTheVisitorWithNone(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, _ *http.Request) {
|
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header()["Content-Type"] = nil
|
w.Header()["Content-Type"] = nil
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
_, _ = io.WriteString(w, "<html>broken")
|
_, _ = io.WriteString(w, "<html>broken")
|
||||||
@@ -1019,7 +1019,7 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
|
|||||||
|
|
||||||
const size = 1 << 20
|
const size = 1 << 20
|
||||||
|
|
||||||
standIn := startDiscriminator(t, func(w http.ResponseWriter, _ *http.Request) {
|
standIn := startWAF(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
_, _ = w.Write(make([]byte, size))
|
_, _ = w.Write(make([]byte, size))
|
||||||
})
|
})
|
||||||
@@ -1027,15 +1027,15 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
|
|||||||
|
|
||||||
got := get(t, addr, "/")
|
got := get(t, addr, "/")
|
||||||
if got.status != http.StatusInternalServerError || len(got.body) != size {
|
if got.status != http.StatusInternalServerError || len(got.body) != size {
|
||||||
t.Errorf("answered %d with %d bytes, want discriminator's 500 with %d",
|
t.Errorf("answered %d with %d bytes, want waf's 500 with %d",
|
||||||
got.status, len(got.body), size)
|
got.status, len(got.body), size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// startDiscriminatorWriting starts a stand-in for discriminator that reads
|
// startWAFWriting starts a stand-in for waf that reads
|
||||||
// each question and answers it with text, as it is, and returns its URL. It
|
// each question and answers it with text, as it is, and returns its URL. It
|
||||||
// can send what Go's server would not.
|
// can send what Go's server would not.
|
||||||
func startDiscriminatorWriting(t *testing.T, text string) string {
|
func startWAFWriting(t *testing.T, text string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
|
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
|
||||||
@@ -1105,7 +1105,7 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
|
|||||||
"a status below 100": func(t *testing.T) string {
|
"a status below 100": func(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return startDiscriminatorWriting(t, "HTTP/1.1 099 Odd\r\nContent-Length: 0\r\n\r\n")
|
return startWAFWriting(t, "HTTP/1.1 099 Odd\r\nContent-Length: 0\r\n\r\n")
|
||||||
},
|
},
|
||||||
"a 101 that holds its connection open": answeringWith(switchAndHold),
|
"a 101 that holds its connection open": answeringWith(switchAndHold),
|
||||||
} {
|
} {
|
||||||
@@ -1130,7 +1130,7 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
|
|||||||
t.Errorf("line %v, want status 502 and no disposition", line)
|
t.Errorf("line %v, want status 502 and no disposition", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
out.waitFor(t, "discriminator's answer is dropped")
|
out.waitFor(t, "waf's answer is dropped")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1139,7 +1139,7 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
|
|||||||
func TestAnAnswerThatCannotBePassedOnGetsNoReport(t *testing.T) {
|
func TestAnAnswerThatCannotBePassedOnGetsNoReport(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, overOneMiB(dispositionDeny))
|
standIn := startWAF(t, overOneMiB(dispositionDeny))
|
||||||
addr, out := startRProxy(t, standIn.URL, nowhere, nil)
|
addr, out := startRProxy(t, standIn.URL, nowhere, nil)
|
||||||
|
|
||||||
if got := get(t, addr, "/"); got.status != http.StatusBadGateway {
|
if got := get(t, addr, "/"); got.status != http.StatusBadGateway {
|
||||||
@@ -1196,14 +1196,14 @@ func TestWithoutAnAnswerTheDefaultDispositionDecides(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAVisitorThatGoesAwayWhileDiscriminatorIsAskedGetsNoDefault(t *testing.T) {
|
func TestAVisitorThatGoesAwayWhileWAFIsAskedGetsNoDefault(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, disposition := range []string{dispositionAllow, dispositionDeny} {
|
for _, disposition := range []string{dispositionAllow, dispositionDeny} {
|
||||||
t.Run("with the default "+disposition, func(t *testing.T) {
|
t.Run("with the default "+disposition, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, func(_ http.ResponseWriter, r *http.Request) {
|
standIn := startWAF(t, func(_ http.ResponseWriter, r *http.Request) {
|
||||||
<-r.Context().Done()
|
<-r.Context().Done()
|
||||||
})
|
})
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||||
@@ -1226,8 +1226,8 @@ func TestAVisitorThatGoesAwayWhileDiscriminatorIsAskedGetsNoDefault(t *testing.T
|
|||||||
t.Errorf("line %v, want status 0 and no disposition", line)
|
t.Errorf("line %v, want status 0 and no disposition", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
if out.has("discriminator gave no answer") {
|
if out.has("waf gave no answer") {
|
||||||
t.Error("rproxy warned that discriminator gave no answer")
|
t.Error("rproxy warned that waf gave no answer")
|
||||||
}
|
}
|
||||||
|
|
||||||
standIn.wantNoReport(t)
|
standIn.wantNoReport(t)
|
||||||
@@ -1242,13 +1242,13 @@ func TestRProxyWarnsWhenTheDefaultDecides(t *testing.T) {
|
|||||||
addr, out := startRProxy(t, nowhere, app.URL, nil)
|
addr, out := startRProxy(t, nowhere, app.URL, nil)
|
||||||
|
|
||||||
wantGreeting(t, get(t, addr, "/"))
|
wantGreeting(t, get(t, addr, "/"))
|
||||||
out.waitFor(t, "discriminator gave no answer")
|
out.waitFor(t, "waf gave no answer")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendsNoReportWithoutAnAnswer(t *testing.T) {
|
func TestSendsNoReportWithoutAnAnswer(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, func(_ http.ResponseWriter, r *http.Request) {
|
standIn := startWAF(t, func(_ http.ResponseWriter, r *http.Request) {
|
||||||
<-r.Context().Done()
|
<-r.Context().Done()
|
||||||
})
|
})
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
@@ -1275,29 +1275,29 @@ func TestAPathUnderOwnPrefixNeverReachesTheApp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHealthCheckIsAnsweredWhileDiscriminatorIsDown(t *testing.T) {
|
func TestHealthCheckIsAnsweredWhileWAFIsDown(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, out := startRProxy(t, nowhere, app.URL, nil)
|
addr, out := startRProxy(t, nowhere, app.URL, nil)
|
||||||
|
|
||||||
got := get(t, addr, discriminator.HealthPath)
|
got := get(t, addr, waf.HealthPath)
|
||||||
if got.status != http.StatusOK || got.body != "ok\n" {
|
if got.status != http.StatusOK || got.body != "ok\n" {
|
||||||
t.Errorf("answered %+v, want ok", got)
|
t.Errorf("answered %+v, want ok", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
line := out.requestLine(t)
|
line := out.requestLine(t)
|
||||||
if line["path"] != discriminator.HealthPath || line["disposition"] != nil {
|
if line["path"] != waf.HealthPath || line["disposition"] != nil {
|
||||||
t.Errorf("line %v, want one with no disposition", line)
|
t.Errorf("line %v, want one with no disposition", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// With discriminator up too, it is not asked.
|
// With waf up too, it is not asked.
|
||||||
addr, _ = startRProxy(t, standIn.URL, app.URL, nil)
|
addr, _ = startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
get(t, addr, discriminator.HealthPath)
|
get(t, addr, waf.HealthPath)
|
||||||
|
|
||||||
if len(standIn.questions) != 0 {
|
if len(standIn.questions) != 0 {
|
||||||
t.Error("discriminator was asked about the health check")
|
t.Error("waf was asked about the health check")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1305,7 +1305,7 @@ func TestTheVisitorDoesNotWaitForTheReport(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
release := make(chan struct{})
|
release := make(chan struct{})
|
||||||
backend := startDiscriminatorAnsweringReports(t, deny,
|
backend := startWAFAnsweringReports(t, deny,
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
select {
|
select {
|
||||||
case <-release:
|
case <-release:
|
||||||
@@ -1340,16 +1340,16 @@ func TestTheVisitorDoesNotWaitForTheReport(t *testing.T) {
|
|||||||
_ = res.Body.Close()
|
_ = res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusForbidden {
|
if res.StatusCode != http.StatusForbidden {
|
||||||
t.Errorf("status %d, want discriminator's 403", res.StatusCode)
|
t.Errorf("status %d, want waf's 403", res.StatusCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAReportDiscriminatorLeavesUnansweredIsDroppedAtTheTimeLimit(t *testing.T) {
|
func TestAReportWAFLeavesUnansweredIsDroppedAtTheTimeLimit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
release := make(chan struct{})
|
release := make(chan struct{})
|
||||||
backend := startDiscriminatorAnsweringReports(t, allow,
|
backend := startWAFAnsweringReports(t, allow,
|
||||||
func(w http.ResponseWriter, _ *http.Request) {
|
func(w http.ResponseWriter, _ *http.Request) {
|
||||||
<-release
|
<-release
|
||||||
|
|
||||||
@@ -1369,10 +1369,10 @@ func TestAReportDiscriminatorLeavesUnansweredIsDroppedAtTheTimeLimit(t *testing.
|
|||||||
out.waitFor(t, "a report is dropped")
|
out.waitFor(t, "a report is dropped")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAReportDiscriminatorRefusesIsDroppedWithAWarning(t *testing.T) {
|
func TestAReportWAFRefusesIsDroppedWithAWarning(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
backend := startDiscriminatorAnsweringReports(t, allow,
|
backend := startWAFAnsweringReports(t, allow,
|
||||||
func(w http.ResponseWriter, _ *http.Request) {
|
func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
})
|
})
|
||||||
@@ -1387,7 +1387,7 @@ func TestAReportDiscriminatorRefusesIsDroppedWithAWarning(t *testing.T) {
|
|||||||
func TestAReportAnswered101IsDroppedAtOnce(t *testing.T) {
|
func TestAReportAnswered101IsDroppedAtOnce(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
backend := startDiscriminatorAnsweringReports(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",
|
timeoutSetting: "60000",
|
||||||
@@ -1400,7 +1400,7 @@ func TestAReportAnswered101IsDroppedAtOnce(t *testing.T) {
|
|||||||
func TestAReportDueAfterWaitForReportsIsDropped(t *testing.T) {
|
func TestAReportDueAfterWaitForReportsIsDropped(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, out, server := startServer(t, standIn.URL, app.URL, nil)
|
addr, out, server := startServer(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
@@ -1416,7 +1416,7 @@ func TestReportGivesWhatHappened(t *testing.T) {
|
|||||||
|
|
||||||
const body = "the visitor's body"
|
const body = "the visitor's body"
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
@@ -1446,7 +1446,7 @@ func TestWaitForReportsWaitsForAReportBeingSent(t *testing.T) {
|
|||||||
release := make(chan struct{})
|
release := make(chan struct{})
|
||||||
backend := httptest.NewServer(http.HandlerFunc(
|
backend := httptest.NewServer(http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Header.Get(discriminator.ClientHeader) != "" {
|
if r.Header.Get(waf.ClientHeader) != "" {
|
||||||
allow(w, r)
|
allow(w, r)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -1501,7 +1501,7 @@ func TestWaitForReportsWaitsForAReportBeingSent(t *testing.T) {
|
|||||||
func TestReportOfAWebSocketCountsWhatItCarried(t *testing.T) {
|
func TestReportOfAWebSocketCountsWhatItCarried(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, echoAfterUpgrade)
|
app := startApp(t, echoAfterUpgrade)
|
||||||
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
maxRequestTime: "2000",
|
maxRequestTime: "2000",
|
||||||
@@ -1556,7 +1556,7 @@ func TestAWebSocketPassesOnThatTheVisitorStoppedSending(t *testing.T) {
|
|||||||
|
|
||||||
const goodbye = "sent after the visitor stopped sending\n"
|
const goodbye = "sent after the visitor stopped sending\n"
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
conn, buffered, err := http.NewResponseController(w).Hijack()
|
conn, buffered, err := http.NewResponseController(w).Hijack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1614,7 +1614,7 @@ func TestAWebSocketPassesOnThatTheVisitorStoppedSending(t *testing.T) {
|
|||||||
//nolint:paralleltest // see above
|
//nolint:paralleltest // see above
|
||||||
func TestASwitchNotPassedOnClosesTheAppsConnection(t *testing.T) {
|
func TestASwitchNotPassedOnClosesTheAppsConnection(t *testing.T) {
|
||||||
closed := make(chan error, 1)
|
closed := make(chan error, 1)
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||||
conn, buffered, err := http.NewResponseController(w).Hijack()
|
conn, buffered, err := http.NewResponseController(w).Hijack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1685,7 +1685,7 @@ func TestEachRequestGetsAFreshID(t *testing.T) {
|
|||||||
|
|
||||||
const fromTraefik = "an-id-from-traefik"
|
const fromTraefik = "an-id-from-traefik"
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
trustedProxies: localhost + "/32",
|
trustedProxies: localhost + "/32",
|
||||||
@@ -1715,7 +1715,7 @@ func TestLogLineGivesTheRequestAsSent(t *testing.T) {
|
|||||||
body = "the visitor's body"
|
body = "the visitor's body"
|
||||||
)
|
)
|
||||||
|
|
||||||
standIn := startDiscriminator(t, allow)
|
standIn := startWAF(t, allow)
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
addr, out := startRProxy(t, standIn.URL, app.URL, nil)
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// answerBodyMaxBytes is the most of the body of discriminator's answer that is read.
|
// answerBodyMaxBytes is the most of the body of waf's answer that is read.
|
||||||
// It is far above any answer's.
|
// It is far above any answer's.
|
||||||
const answerBodyMaxBytes = 1 << 20
|
const answerBodyMaxBytes = 1 << 20
|
||||||
|
|
||||||
// reportAnswerMaxBytes is the most of the answer to a report that is read,
|
// reportAnswerMaxBytes is the most of the answer to a report that is read,
|
||||||
// which has no body when discriminator takes the report.
|
// which has no body when waf takes the report.
|
||||||
const reportAnswerMaxBytes = 4 << 10
|
const reportAnswerMaxBytes = 4 << 10
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -27,22 +27,22 @@ var (
|
|||||||
errReportRefused = errors.New("the report was answered")
|
errReportRefused = errors.New("the report was answered")
|
||||||
)
|
)
|
||||||
|
|
||||||
// discriminatorAnswer is what discriminator answered to a question.
|
// wafAnswer is what waf answered to a question.
|
||||||
type discriminatorAnswer struct {
|
type wafAnswer struct {
|
||||||
status int
|
status int
|
||||||
header http.Header
|
header http.Header
|
||||||
body []byte
|
body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// noteAnswer notes what discriminator answered. An answer without a valid disposition
|
// noteAnswer notes what waf answered. An answer without a valid disposition
|
||||||
// notes nothing, and no report is due for it: discriminator holds no decision to report
|
// notes nothing, and no report is due for it: waf holds no decision to report
|
||||||
// on.
|
// on.
|
||||||
func (rq *request) noteAnswer(answer *discriminatorAnswer) {
|
func (rq *request) noteAnswer(answer *wafAnswer) {
|
||||||
disposition := answer.header.Get(discriminator.DispositionHeader)
|
disposition := answer.header.Get(waf.DispositionHeader)
|
||||||
|
|
||||||
switch disposition {
|
switch disposition {
|
||||||
case discriminator.DispositionAllow, discriminator.DispositionDeny,
|
case waf.DispositionAllow, waf.DispositionDeny,
|
||||||
discriminator.DispositionClose:
|
waf.DispositionClose:
|
||||||
rq.disposition = disposition
|
rq.disposition = disposition
|
||||||
rq.answered = true
|
rq.answered = true
|
||||||
rq.asn = answer.header.Get(asnHeader)
|
rq.asn = answer.header.Get(asnHeader)
|
||||||
@@ -55,27 +55,27 @@ func (rq *request) noteAnswer(answer *discriminatorAnswer) {
|
|||||||
func (rq *request) handleNoUsableAnswer(ctx context.Context, err error) {
|
func (rq *request) handleNoUsableAnswer(ctx context.Context, err error) {
|
||||||
switch {
|
switch {
|
||||||
case ctx.Err() != nil:
|
case ctx.Err() != nil:
|
||||||
// The visitor went away while discriminator was being asked. discriminator has not
|
// The visitor went away while waf was being asked. waf has not
|
||||||
// failed, and nothing is noted. This panic has Go's server close the
|
// failed, and nothing is noted. This panic has Go's server close the
|
||||||
// connection without an answer, and log nothing; the deferred finish still
|
// connection without an answer, and log nothing; the deferred finish still
|
||||||
// runs.
|
// runs.
|
||||||
panic(http.ErrAbortHandler)
|
panic(http.ErrAbortHandler)
|
||||||
case errors.Is(err, errCannotPassOn):
|
case errors.Is(err, errCannotPassOn):
|
||||||
rq.h.processLog.Warn("discriminator's answer is dropped", "error", err.Error())
|
rq.h.processLog.Warn("waf's answer is dropped", "error", err.Error())
|
||||||
rq.answer(http.StatusBadGateway)
|
rq.answer(http.StatusBadGateway)
|
||||||
default:
|
default:
|
||||||
// discriminator cannot be connected to, does not answer at all, or does not answer
|
// waf cannot be connected to, does not answer at all, or does not answer
|
||||||
// within SWRP_TIMEOUT_MS. A request that keeps discriminator busy past
|
// within SWRP_TIMEOUT_MS. A request that keeps waf busy past
|
||||||
// SWRP_TIMEOUT_MS, like any request while discriminator is overloaded, gets
|
// SWRP_TIMEOUT_MS, like any request while waf is overloaded, gets
|
||||||
// SWRP_DEFAULT_DISPOSITION here. deny closes this, at the cost of answering 503
|
// SWRP_DEFAULT_DISPOSITION here. deny closes this, at the cost of answering 503
|
||||||
// whenever discriminator is slow. Making such requests hard to send is
|
// whenever waf is slow. Making such requests hard to send is
|
||||||
// https://git.eeqj.de/sneak/smallwebwaf/issues/182.
|
// https://git.eeqj.de/sneak/smallwebwaf/issues/182.
|
||||||
rq.h.processLog.Warn("discriminator gave no answer", "error", err.Error())
|
rq.h.processLog.Warn("waf gave no answer", "error", err.Error())
|
||||||
|
|
||||||
rq.disposition = rq.h.config.DefaultDisposition
|
rq.disposition = rq.h.config.DefaultDisposition
|
||||||
rq.byDefault = true
|
rq.byDefault = true
|
||||||
|
|
||||||
if rq.disposition == discriminator.DispositionAllow {
|
if rq.disposition == waf.DispositionAllow {
|
||||||
rq.passToApp(ctx)
|
rq.passToApp(ctx)
|
||||||
} else {
|
} else {
|
||||||
rq.answer(http.StatusServiceUnavailable)
|
rq.answer(http.StatusServiceUnavailable)
|
||||||
@@ -83,10 +83,10 @@ func (rq *request) handleNoUsableAnswer(ctx context.Context, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ask sends discriminator the question, and returns its answer, which must have come
|
// ask sends waf the question, and returns its answer, which must have come
|
||||||
// whole within SWRP_TIMEOUT_MS. It ends when ctx does, as when the visitor goes
|
// whole within SWRP_TIMEOUT_MS. It ends when ctx does, as when the visitor goes
|
||||||
// away.
|
// away.
|
||||||
func (rq *request) ask(ctx context.Context) (*discriminatorAnswer, error) {
|
func (rq *request) ask(ctx context.Context) (*wafAnswer, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, rq.h.config.Timeout)
|
ctx, cancel := context.WithTimeout(ctx, rq.h.config.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ func (rq *request) ask(ctx context.Context) (*discriminatorAnswer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := rq.h.discriminatorTransport.RoundTrip(question)
|
res, err := rq.h.wafTransport.RoundTrip(question)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("send the question: %w", err)
|
return nil, fmt.Errorf("send the question: %w", err)
|
||||||
}
|
}
|
||||||
@@ -120,17 +120,17 @@ func (rq *request) ask(ctx context.Context) (*discriminatorAnswer, error) {
|
|||||||
return nil, fmt.Errorf("%w: its body is over 1 MiB", errCannotPassOn)
|
return nil, fmt.Errorf("%w: its body is over 1 MiB", errCannotPassOn)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &discriminatorAnswer{res.StatusCode, res.Header, body}, nil
|
return &wafAnswer{res.StatusCode, res.Header, body}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// canBeAsked reports whether Go's client can send discriminator r as the visitor sent
|
// canBeAsked reports whether Go's client can send waf r as the visitor sent
|
||||||
// it. It cannot send these as they came:
|
// it. It cannot send these as they came:
|
||||||
// - a target that does not start with /, or that starts with //;
|
// - a target that does not start with /, or that starts with //;
|
||||||
// - an empty Host, for which it sends discriminator's own address;
|
// - an empty Host, for which it sends waf's own address;
|
||||||
// - a Host holding an IPv6 zone, which it removes;
|
// - a Host holding an IPv6 zone, which it removes;
|
||||||
// - more than one User-Agent, of which it sends the first;
|
// - more than one User-Agent, of which it sends the first;
|
||||||
// - an empty User-Agent, which it leaves out, the one case that still goes to
|
// - an empty User-Agent, which it leaves out, the one case that still goes to
|
||||||
// discriminator.
|
// waf.
|
||||||
func canBeAsked(r *http.Request) bool {
|
func canBeAsked(r *http.Request) bool {
|
||||||
target, host := r.RequestURI, r.Host
|
target, host := r.RequestURI, r.Host
|
||||||
hostHasZone := strings.HasPrefix(host, "[") && strings.Contains(host, "%")
|
hostHasZone := strings.HasPrefix(host, "[") && strings.Contains(host, "%")
|
||||||
@@ -170,13 +170,13 @@ func (rq *request) newQuestion(ctx context.Context) (*http.Request, error) {
|
|||||||
question.TransferEncoding = in.TransferEncoding
|
question.TransferEncoding = in.TransferEncoding
|
||||||
|
|
||||||
question.Header = in.Header.Clone()
|
question.Header = in.Header.Clone()
|
||||||
question.Header.Set(discriminator.ClientHeader, rq.client.String())
|
question.Header.Set(waf.ClientHeader, rq.client.String())
|
||||||
question.Header.Set(discriminator.PeerHeader, rq.peer.String())
|
question.Header.Set(waf.PeerHeader, rq.peer.String())
|
||||||
question.Header.Del(discriminator.ContentLengthHeader)
|
question.Header.Del(waf.ContentLengthHeader)
|
||||||
|
|
||||||
announced := in.Header.Get("Content-Length")
|
announced := in.Header.Get("Content-Length")
|
||||||
if announced != "" {
|
if announced != "" {
|
||||||
question.Header.Set(discriminator.ContentLengthHeader, announced)
|
question.Header.Set(waf.ContentLengthHeader, announced)
|
||||||
}
|
}
|
||||||
|
|
||||||
question.Header.Set(requestIDHeader, rq.id)
|
question.Header.Set(requestIDHeader, rq.id)
|
||||||
@@ -191,13 +191,13 @@ func (rq *request) newQuestion(ctx context.Context) (*http.Request, error) {
|
|||||||
return question, nil
|
return question, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// answerWith gives the visitor discriminator's answer, less its disposition.
|
// answerWith gives the visitor waf's answer, less its disposition.
|
||||||
func (rq *request) answerWith(answer *discriminatorAnswer) {
|
func (rq *request) answerWith(answer *wafAnswer) {
|
||||||
rq.prepareOwnAnswer(rq.requestDeadline())
|
rq.prepareOwnAnswer(rq.requestDeadline())
|
||||||
|
|
||||||
header := rq.out.Header()
|
header := rq.out.Header()
|
||||||
maps.Copy(header, answer.header)
|
maps.Copy(header, answer.header)
|
||||||
header.Del(discriminator.DispositionHeader)
|
header.Del(waf.DispositionHeader)
|
||||||
|
|
||||||
// Go's server adds a Content-Type it guesses from the body to a response whose
|
// Go's server adds a Content-Type it guesses from the body to a response whose
|
||||||
// headers have no Content-Type key. The key with no value sends none.
|
// headers have no Content-Type key. The key with no value sends none.
|
||||||
@@ -208,10 +208,10 @@ func (rq *request) answerWith(answer *discriminatorAnswer) {
|
|||||||
rq.send(answer.status, answer.body)
|
rq.send(answer.status, answer.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendReport sends discriminator report on a goroutine of its own, so that the
|
// sendReport sends waf report on a goroutine of its own, so that the
|
||||||
// visitor does not wait for it. The report goes on after ctx, the request's own
|
// visitor does not wait for it. The report goes on after ctx, the request's own
|
||||||
// context, has ended.
|
// context, has ended.
|
||||||
func (h *handler) sendReport(ctx context.Context, report *discriminator.Report) {
|
func (h *handler) sendReport(ctx context.Context, report *waf.Report) {
|
||||||
h.reportsMu.Lock()
|
h.reportsMu.Lock()
|
||||||
defer h.reportsMu.Unlock()
|
defer h.reportsMu.Unlock()
|
||||||
|
|
||||||
@@ -227,9 +227,9 @@ func (h *handler) sendReport(ctx context.Context, report *discriminator.Report)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// postReport posts report to discriminator, and waits for its answer for up to
|
// postReport posts report to waf, and waits for its answer for up to
|
||||||
// SWRP_TIMEOUT_MS.
|
// SWRP_TIMEOUT_MS.
|
||||||
func (h *handler) postReport(ctx context.Context, report *discriminator.Report) error {
|
func (h *handler) postReport(ctx context.Context, report *waf.Report) error {
|
||||||
body, err := json.Marshal(report)
|
body, err := json.Marshal(report)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encode the report: %w", err)
|
return fmt.Errorf("encode the report: %w", err)
|
||||||
@@ -246,7 +246,7 @@ func (h *handler) postReport(ctx context.Context, report *discriminator.Report)
|
|||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
res, err := h.discriminatorTransport.RoundTrip(req)
|
res, err := h.wafTransport.RoundTrip(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("send the report: %w", err)
|
return fmt.Errorf("send the report: %w", err)
|
||||||
}
|
}
|
||||||
+10
-10
@@ -11,7 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// healthCheckTimeout bounds the whole health check.
|
// healthCheckTimeout bounds the whole health check.
|
||||||
@@ -19,8 +19,8 @@ const healthCheckTimeout = 5 * time.Second
|
|||||||
|
|
||||||
var errHealthEndpoint = errors.New("the health endpoint answered")
|
var errHealthEndpoint = errors.New("the health endpoint answered")
|
||||||
|
|
||||||
// HealthCheck is the container's health check. In discriminator's container, where
|
// HealthCheck is the container's health check. In waf's container, where
|
||||||
// SWW_COMMAND is discriminator, it returns 0 while discriminator answers its health
|
// SWW_COMMAND is waf, it returns 0 while waf answers its health
|
||||||
// endpoint on 127.0.0.1, at the port in SWW_LISTEN_ADDR. In rproxy's container,
|
// endpoint on 127.0.0.1, at the port in SWW_LISTEN_ADDR. In rproxy's container,
|
||||||
// where SWW_COMMAND is rproxy, unset or empty, it returns 0 while rproxy
|
// where SWW_COMMAND is rproxy, unset or empty, it returns 0 while rproxy
|
||||||
// answers its health endpoint on 127.0.0.1, at the port in SWRP_LISTEN_ADDR, and
|
// answers its health endpoint on 127.0.0.1, at the port in SWRP_LISTEN_ADDR, and
|
||||||
@@ -49,8 +49,8 @@ func HealthCheck(
|
|||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
err = fmt.Errorf("invalid setting: %w", err)
|
err = fmt.Errorf("invalid setting: %w", err)
|
||||||
case command == "discriminator":
|
case command == "waf":
|
||||||
err = checkDiscriminator(ctx, lookupEnv)
|
err = checkWAF(ctx, lookupEnv)
|
||||||
default:
|
default:
|
||||||
err = checkRProxyAndApp(ctx, lookupEnv)
|
err = checkRProxyAndApp(ctx, lookupEnv)
|
||||||
}
|
}
|
||||||
@@ -64,18 +64,18 @@ func HealthCheck(
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDiscriminator asks discriminator's health endpoint.
|
// checkWAF asks waf's health endpoint.
|
||||||
func checkDiscriminator(
|
func checkWAF(
|
||||||
ctx context.Context, lookupEnv func(string) (string, bool),
|
ctx context.Context, lookupEnv func(string) (string, bool),
|
||||||
) error {
|
) error {
|
||||||
listenAddr, err := config.DiscriminatorListenAddr(lookupEnv)
|
listenAddr, err := config.WAFListenAddr(lookupEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid setting: %w", err)
|
return fmt.Errorf("invalid setting: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = askHealthEndpoint(ctx, listenAddr)
|
err = askHealthEndpoint(ctx, listenAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ask discriminator: %w", err)
|
return fmt.Errorf("ask waf: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -109,7 +109,7 @@ func checkRProxyAndApp(
|
|||||||
// listenAddr, which the settings have checked has one.
|
// listenAddr, which the settings have checked has one.
|
||||||
func askHealthEndpoint(ctx context.Context, listenAddr string) error {
|
func askHealthEndpoint(ctx context.Context, listenAddr string) error {
|
||||||
_, port, _ := net.SplitHostPort(listenAddr)
|
_, port, _ := net.SplitHostPort(listenAddr)
|
||||||
health := "http://" + net.JoinHostPort("127.0.0.1", port) + discriminator.HealthPath
|
health := "http://" + net.JoinHostPort("127.0.0.1", port) + waf.HealthPath
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, health, http.NoBody)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, health, http.NoBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ const (
|
|||||||
// commandSetting says which command a container runs, and which the health
|
// commandSetting says which command a container runs, and which the health
|
||||||
// check checks.
|
// check checks.
|
||||||
commandSetting = "SWW_COMMAND"
|
commandSetting = "SWW_COMMAND"
|
||||||
// discriminatorCommand is the command that runs discriminator.
|
// wafCommand is the command that runs waf.
|
||||||
discriminatorCommand = "discriminator"
|
wafCommand = "waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHealthCheckInDiscriminatorsContainer(t *testing.T) {
|
func TestHealthCheckInWAFsContainer(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, stop := context.WithCancel(t.Context())
|
ctx, stop := context.WithCancel(t.Context())
|
||||||
@@ -47,7 +47,7 @@ func TestHealthCheckInDiscriminatorsContainer(t *testing.T) {
|
|||||||
// The container's settings: an address to listen on with an empty
|
// The container's settings: an address to listen on with an empty
|
||||||
// host part, which the health check asks at 127.0.0.1.
|
// host part, which the health check asks at 127.0.0.1.
|
||||||
wantHealthCheck(t, map[string]string{
|
wantHealthCheck(t, map[string]string{
|
||||||
commandSetting: discriminatorCommand, listenAddr: ":" + port,
|
commandSetting: wafCommand, listenAddr: ":" + port,
|
||||||
}, 0, "")
|
}, 0, "")
|
||||||
|
|
||||||
// The health check reads that setting alone, here given as a file: a removed
|
// The health check reads that setting alone, here given as a file: a removed
|
||||||
@@ -59,7 +59,7 @@ func TestHealthCheckInDiscriminatorsContainer(t *testing.T) {
|
|||||||
{"SWW_MODE", "neither"},
|
{"SWW_MODE", "neither"},
|
||||||
} {
|
} {
|
||||||
wantHealthCheck(t, map[string]string{
|
wantHealthCheck(t, map[string]string{
|
||||||
commandSetting: discriminatorCommand,
|
commandSetting: wafCommand,
|
||||||
listenAddr + "_FILE": writeFile(t, ":"+port+"\n"),
|
listenAddr + "_FILE": writeFile(t, ":"+port+"\n"),
|
||||||
other.name: other.value,
|
other.name: other.value,
|
||||||
}, 0, "")
|
}, 0, "")
|
||||||
@@ -68,10 +68,10 @@ func TestHealthCheckInDiscriminatorsContainer(t *testing.T) {
|
|||||||
// Listening on port 0 takes any free port, so nothing listens on port 0
|
// Listening on port 0 takes any free port, so nothing listens on port 0
|
||||||
// itself.
|
// itself.
|
||||||
wantHealthCheck(t, map[string]string{
|
wantHealthCheck(t, map[string]string{
|
||||||
commandSetting: discriminatorCommand, listenAddr: ":0",
|
commandSetting: wafCommand, listenAddr: ":0",
|
||||||
}, 1, "unhealthy: ask discriminator: ")
|
}, 1, "unhealthy: ask waf: ")
|
||||||
wantHealthCheck(t, map[string]string{
|
wantHealthCheck(t, map[string]string{
|
||||||
commandSetting: discriminatorCommand, listenAddr: "8080",
|
commandSetting: wafCommand, listenAddr: "8080",
|
||||||
}, 1, "unhealthy: invalid setting: SWW_LISTEN_ADDR: ")
|
}, 1, "unhealthy: invalid setting: SWW_LISTEN_ADDR: ")
|
||||||
|
|
||||||
stop()
|
stop()
|
||||||
@@ -90,7 +90,7 @@ func TestHealthCheckInRProxysContainer(t *testing.T) {
|
|||||||
defer app.Close()
|
defer app.Close()
|
||||||
|
|
||||||
// Nothing listens at the backend. The health check asks rproxy and the app,
|
// Nothing listens at the backend. The health check asks rproxy and the app,
|
||||||
// whether or not discriminator answers.
|
// whether or not waf answers.
|
||||||
site, _ := startRProxy(t, "http://127.0.0.1:9", app.URL)
|
site, _ := startRProxy(t, "http://127.0.0.1:9", app.URL)
|
||||||
_, port, _ := net.SplitHostPort(strings.TrimPrefix(site, "http://"))
|
_, port, _ := net.SplitHostPort(strings.TrimPrefix(site, "http://"))
|
||||||
settings := func(more ...string) map[string]string {
|
settings := func(more ...string) map[string]string {
|
||||||
@@ -107,7 +107,7 @@ func TestHealthCheckInRProxysContainer(t *testing.T) {
|
|||||||
wantHealthCheck(t, settings(commandSetting, "rproxy"), 0, "")
|
wantHealthCheck(t, settings(commandSetting, "rproxy"), 0, "")
|
||||||
|
|
||||||
// SWW_COMMAND cannot be given as a file.
|
// SWW_COMMAND cannot be given as a file.
|
||||||
wantHealthCheck(t, settings(commandSetting+"_FILE", writeFile(t, "discriminator\n")),
|
wantHealthCheck(t, settings(commandSetting+"_FILE", writeFile(t, "waf\n")),
|
||||||
0, "")
|
0, "")
|
||||||
|
|
||||||
// Another setting does not fail it, even an invalid one, and neither does
|
// Another setting does not fail it, even an invalid one, and neither does
|
||||||
@@ -137,7 +137,7 @@ func TestHealthCheckFailsWithAnyOtherCommand(t *testing.T) {
|
|||||||
|
|
||||||
wantHealthCheck(t, map[string]string{commandSetting: "healthcheck"}, 1,
|
wantHealthCheck(t, map[string]string{commandSetting: "healthcheck"}, 1,
|
||||||
"unhealthy: invalid setting: SWW_COMMAND: "+
|
"unhealthy: invalid setting: SWW_COMMAND: "+
|
||||||
"\"healthcheck\" is not rproxy or discriminator\n")
|
"\"healthcheck\" is not rproxy or waf\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHealthCheckRefusesAnArgument(t *testing.T) {
|
func TestHealthCheckRefusesAnArgument(t *testing.T) {
|
||||||
|
|||||||
+53
-53
@@ -19,18 +19,18 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/sww"
|
"sneak.berlin/go/smallwebwaf/internal/sww"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// wafBackend is the setting that names discriminator to rproxy.
|
// wafBackend is the setting that names waf to rproxy.
|
||||||
wafBackend = "SWRP_WAF_BACKEND"
|
wafBackend = "SWRP_WAF_BACKEND"
|
||||||
// rproxyListenAddr is the setting that says where rproxy listens.
|
// rproxyListenAddr is the setting that says where rproxy listens.
|
||||||
rproxyListenAddr = "SWRP_LISTEN_ADDR"
|
rproxyListenAddr = "SWRP_LISTEN_ADDR"
|
||||||
// rproxyUpstreamURL is the setting that says where the app is.
|
// rproxyUpstreamURL is the setting that says where the app is.
|
||||||
rproxyUpstreamURL = "SWRP_UPSTREAM_URL"
|
rproxyUpstreamURL = "SWRP_UPSTREAM_URL"
|
||||||
// rproxyTimeout is the setting that says how long rproxy waits for discriminator.
|
// rproxyTimeout is the setting that says how long rproxy waits for waf.
|
||||||
rproxyTimeout = "SWRP_TIMEOUT_MS"
|
rproxyTimeout = "SWRP_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"
|
||||||
@@ -52,7 +52,7 @@ func runRProxy(ctx context.Context, env map[string]string, out *output) int {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// startRProxy runs rproxy with discriminator at backend and the app at appURL, and
|
// startRProxy runs rproxy with waf at backend and the app at appURL, and
|
||||||
// returns its URL and what it writes. It believes X-Forwarded-For from the
|
// returns its URL and what it writes. It believes X-Forwarded-For from the
|
||||||
// tests. It stops as the test ends, and that it stopped in order is checked.
|
// tests. It stops as the test ends, and that it stopped in order is checked.
|
||||||
func startRProxy(t *testing.T, backend, appURL string) (string, *output) {
|
func startRProxy(t *testing.T, backend, appURL string) (string, *output) {
|
||||||
@@ -91,10 +91,10 @@ func startRProxy(t *testing.T, backend, appURL string) (string, *output) {
|
|||||||
return "http://" + addr, out
|
return "http://" + addr, out
|
||||||
}
|
}
|
||||||
|
|
||||||
// startDiscriminator runs discriminator with the settings in env, and returns
|
// startWAF runs waf with the settings in env, and returns
|
||||||
// its address and URL, what it writes, and what stops it, which checks that it
|
// its address and URL, what it writes, and what stops it, which checks that it
|
||||||
// stopped in order.
|
// stopped in order.
|
||||||
func startDiscriminator(t *testing.T, env map[string]string) (string, *output, func()) {
|
func startWAF(t *testing.T, env map[string]string) (string, *output, func()) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
ctx, stop := context.WithCancel(t.Context())
|
ctx, stop := context.WithCancel(t.Context())
|
||||||
@@ -113,17 +113,17 @@ func startDiscriminator(t *testing.T, env map[string]string) (string, *output, f
|
|||||||
select {
|
select {
|
||||||
case status := <-exited:
|
case status := <-exited:
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
t.Fatalf("discriminator's exit status %d, want 0; output:\n%s", status, out.text())
|
t.Fatalf("waf's exit status %d, want 0; output:\n%s", status, out.text())
|
||||||
}
|
}
|
||||||
case <-time.After(waitLimit):
|
case <-time.After(waitLimit):
|
||||||
t.Fatal("discriminator still running after being told to stop")
|
t.Fatal("waf still running after being told to stop")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// discriminatorSettings are discriminator's settings with the default rule
|
// wafSettings are waf's settings with the default rule
|
||||||
// file, to listen at any free port.
|
// file, to listen at any free port.
|
||||||
func discriminatorSettings(t *testing.T) map[string]string {
|
func wafSettings(t *testing.T) map[string]string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
@@ -136,7 +136,7 @@ func discriminatorSettings(t *testing.T) map[string]string {
|
|||||||
func TestABanThroughOneSiteRefusesTheClientOnTheOther(t *testing.T) {
|
func TestABanThroughOneSiteRefusesTheClientOnTheOther(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
addr, discriminatorOut, stop := startDiscriminator(t, discriminatorSettings(t))
|
addr, wafOut, stop := startWAF(t, wafSettings(t))
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
backend := "http://" + addr
|
backend := "http://" + addr
|
||||||
@@ -151,32 +151,32 @@ func TestABanThroughOneSiteRefusesTheClientOnTheOther(t *testing.T) {
|
|||||||
wantStatus(t, second+"/", placed, http.StatusForbidden)
|
wantStatus(t, second+"/", placed, http.StatusForbidden)
|
||||||
wantStatus(t, second+"/", otherClient, http.StatusOK)
|
wantStatus(t, second+"/", otherClient, http.StatusOK)
|
||||||
|
|
||||||
// One request's three lines carry one id, rproxy's with discriminator's answer.
|
// One request's three lines carry one id, rproxy's with waf's answer.
|
||||||
line := firstOut.line(t, "path", bannedProbe)
|
line := firstOut.line(t, "path", bannedProbe)
|
||||||
if line["disposition"] != discriminator.DispositionDeny ||
|
if line["disposition"] != waf.DispositionDeny ||
|
||||||
line["client_ip"] != placed {
|
line["client_ip"] != placed {
|
||||||
t.Errorf("rproxy's line is %v", line)
|
t.Errorf("rproxy's line is %v", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
id, _ := line["request_id"].(string)
|
id, _ := line["request_id"].(string)
|
||||||
decision := discriminatorLine(t, discriminatorOut, id, "decision")
|
decision := wafLine(t, wafOut, id, "decision")
|
||||||
report := discriminatorLine(t, discriminatorOut, id, "report")
|
report := wafLine(t, wafOut, id, "report")
|
||||||
|
|
||||||
if decision["host"] != line["host"] || decision["client_ip"] != placed ||
|
if decision["host"] != line["host"] || decision["client_ip"] != placed ||
|
||||||
report["status"] != float64(http.StatusForbidden) {
|
report["status"] != float64(http.StatusForbidden) {
|
||||||
t.Errorf("discriminator's lines are %v and %v", decision, report)
|
t.Errorf("waf's lines are %v and %v", decision, report)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// discriminatorLine waits for discriminator's line with request id and
|
// wafLine waits for waf's line with request id and
|
||||||
// discriminator_line kind.
|
// waf_line kind.
|
||||||
func discriminatorLine(t *testing.T, out *output, id, kind string) map[string]any {
|
func wafLine(t *testing.T, out *output, id, kind string) map[string]any {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
deadline := time.Now().Add(waitLimit)
|
deadline := time.Now().Add(waitLimit)
|
||||||
for time.Now().Before(deadline) {
|
for time.Now().Before(deadline) {
|
||||||
line := out.findLine(t, func(fields map[string]any) bool {
|
line := out.findLine(t, func(fields map[string]any) bool {
|
||||||
return fields["request_id"] == id && fields["discriminator_line"] == kind
|
return fields["request_id"] == id && fields["waf_line"] == kind
|
||||||
})
|
})
|
||||||
if line != nil {
|
if line != nil {
|
||||||
return line
|
return line
|
||||||
@@ -190,14 +190,14 @@ func discriminatorLine(t *testing.T, out *output, id, kind string) map[string]an
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTheSiteAnswersWhileDiscriminatorIsStoppedAndStartedAgain(t *testing.T) {
|
func TestTheSiteAnswersWhileWAFIsStoppedAndStartedAgain(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
env := discriminatorSettings(t)
|
env := wafSettings(t)
|
||||||
addr, _, stop := startDiscriminator(t, env)
|
addr, _, stop := startWAF(t, env)
|
||||||
|
|
||||||
// rproxy asks discriminator through an address the test holds throughout,
|
// rproxy asks waf through an address the test holds throughout,
|
||||||
// so no other test can take it while discriminator is stopped.
|
// so no other test can take it while waf is stopped.
|
||||||
backend, forwardTo := startForwarder(t)
|
backend, forwardTo := startForwarder(t)
|
||||||
forwardTo(addr)
|
forwardTo(addr)
|
||||||
|
|
||||||
@@ -228,22 +228,22 @@ func TestTheSiteAnswersWhileDiscriminatorIsStoppedAndStartedAgain(t *testing.T)
|
|||||||
wantStatus(t, site+bannedProbe, placed, http.StatusForbidden)
|
wantStatus(t, site+bannedProbe, placed, http.StatusForbidden)
|
||||||
wantStatus(t, site+"/", placed, http.StatusForbidden)
|
wantStatus(t, site+"/", placed, http.StatusForbidden)
|
||||||
|
|
||||||
// rproxy's connections are closed at once from here, before discriminator
|
// rproxy's connections are closed at once from here, before waf
|
||||||
// stops and frees its port.
|
// stops and frees its port.
|
||||||
forwardTo("")
|
forwardTo("")
|
||||||
stop()
|
stop()
|
||||||
|
|
||||||
// With discriminator down, the site answers, and rproxy says the default did it.
|
// With waf down, the site answers, and rproxy says the default did it.
|
||||||
wantStatus(t, site+"/after-stop", placed, http.StatusOK)
|
wantStatus(t, site+"/after-stop", placed, http.StatusOK)
|
||||||
|
|
||||||
line := out.line(t, "path", "/after-stop")
|
line := out.line(t, "path", "/after-stop")
|
||||||
if line["disposition"] != discriminator.DispositionAllow ||
|
if line["disposition"] != waf.DispositionAllow ||
|
||||||
line["default_disposition"] != true {
|
line["default_disposition"] != true {
|
||||||
t.Errorf("rproxy's line is %v", line)
|
t.Errorf("rproxy's line is %v", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Started again with its state files, it applies the ban again.
|
// Started again with its state files, it applies the ban again.
|
||||||
addr, _, stop = startDiscriminator(t, env)
|
addr, _, stop = startWAF(t, env)
|
||||||
forwardTo(addr)
|
forwardTo(addr)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -397,10 +397,10 @@ func sendRaw(t *testing.T, site, request string) int {
|
|||||||
return res.StatusCode
|
return res.StatusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAChunkedBodyStaysABodyAtDiscriminator(t *testing.T) {
|
func TestAChunkedBodyStaysABodyAtWAF(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
addr, discriminatorOut, stop := startDiscriminator(t, discriminatorSettings(t))
|
addr, wafOut, stop := startWAF(t, wafSettings(t))
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
site, _ := startRProxy(t, "http://"+addr, startApp(t))
|
site, _ := startRProxy(t, "http://"+addr, startApp(t))
|
||||||
@@ -409,7 +409,7 @@ func TestAChunkedBodyStaysABodyAtDiscriminator(t *testing.T) {
|
|||||||
const smuggledID = "smuggled-request"
|
const smuggledID = "smuggled-request"
|
||||||
|
|
||||||
smuggled := "GET " + bannedProbe + " HTTP/1.1\r\nHost: site.example\r\n" +
|
smuggled := "GET " + bannedProbe + " HTTP/1.1\r\nHost: site.example\r\n" +
|
||||||
discriminator.ClientHeader + ": " + otherClient + "\r\n" +
|
waf.ClientHeader + ": " + otherClient + "\r\n" +
|
||||||
"X-Request-ID: " + smuggledID + "\r\n\r\n"
|
"X-Request-ID: " + smuggledID + "\r\n\r\n"
|
||||||
|
|
||||||
status := sendRaw(t, site, "POST /upload HTTP/1.1\r\nHost: site.example\r\n"+
|
status := sendRaw(t, site, "POST /upload HTTP/1.1\r\nHost: site.example\r\n"+
|
||||||
@@ -425,18 +425,18 @@ func TestAChunkedBodyStaysABodyAtDiscriminator(t *testing.T) {
|
|||||||
time.Sleep(pollInterval)
|
time.Sleep(pollInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
line := discriminatorOut.findLine(t, func(fields map[string]any) bool {
|
line := wafOut.findLine(t, func(fields map[string]any) bool {
|
||||||
return fields["request_id"] == smuggledID
|
return fields["request_id"] == smuggledID
|
||||||
})
|
})
|
||||||
if line != nil {
|
if line != nil {
|
||||||
t.Errorf("discriminator read the body as a request: %v", line)
|
t.Errorf("waf read the body as a request: %v", line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestATargetThatCannotGoToDiscriminatorIsRefusedForABannedClient(t *testing.T) {
|
func TestATargetThatCannotGoToWAFIsRefusedForABannedClient(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
addr, _, stop := startDiscriminator(t, discriminatorSettings(t))
|
addr, _, stop := startWAF(t, wafSettings(t))
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
site, out := startRProxy(t, "http://"+addr, startApp(t))
|
site, out := startRProxy(t, "http://"+addr, startApp(t))
|
||||||
@@ -460,7 +460,7 @@ func TestAClientWithAZoneIsTheClientWithout(t *testing.T) {
|
|||||||
|
|
||||||
const zoned = "2001:db8::1%eth0"
|
const zoned = "2001:db8::1%eth0"
|
||||||
|
|
||||||
addr, _, stop := startDiscriminator(t, discriminatorSettings(t))
|
addr, _, stop := startWAF(t, wafSettings(t))
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
site, out := startRProxy(t, "http://"+addr, startApp(t))
|
site, out := startRProxy(t, "http://"+addr, startApp(t))
|
||||||
@@ -475,7 +475,7 @@ func TestAClientWithAZoneIsTheClientWithout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAStatusDiscriminatorDoesNotTakeIsReportedAs599(t *testing.T) {
|
func TestAStatusWAFDoesNotTakeIsReportedAs599(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const status = 799
|
const status = 799
|
||||||
@@ -486,7 +486,7 @@ func TestAStatusDiscriminatorDoesNotTakeIsReportedAs599(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
t.Cleanup(app.Close)
|
t.Cleanup(app.Close)
|
||||||
|
|
||||||
addr, discriminatorOut, stop := startDiscriminator(t, discriminatorSettings(t))
|
addr, wafOut, stop := startWAF(t, wafSettings(t))
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
site, out := startRProxy(t, "http://"+addr, app.URL)
|
site, out := startRProxy(t, "http://"+addr, app.URL)
|
||||||
@@ -500,9 +500,9 @@ func TestAStatusDiscriminatorDoesNotTakeIsReportedAs599(t *testing.T) {
|
|||||||
|
|
||||||
id, _ := line["request_id"].(string)
|
id, _ := line["request_id"].(string)
|
||||||
|
|
||||||
report := discriminatorLine(t, discriminatorOut, id, "report")
|
report := wafLine(t, wafOut, id, "report")
|
||||||
if report["status"] != 599.0 {
|
if report["status"] != 599.0 {
|
||||||
t.Errorf("discriminator's line is %v, want status 599", report)
|
t.Errorf("waf's line is %v, want status 599", report)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,16 +544,16 @@ func startRProxyProcess(t *testing.T, env ...string) (string, *exec.Cmd, *output
|
|||||||
return addr, cmd, out
|
return addr, cmd, out
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRProxyAsksADiscriminatorThatSpeaksHTTP2OverHTTP1(t *testing.T) {
|
func TestRProxyAsksAWAFThatSpeaksHTTP2OverHTTP1(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
protocols := make(chan string, 10)
|
protocols := make(chan string, 10)
|
||||||
backend := httptest.NewUnstartedServer(http.HandlerFunc(
|
backend := httptest.NewUnstartedServer(http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Header.Get(discriminator.ClientHeader) != "" {
|
if r.Header.Get(waf.ClientHeader) != "" {
|
||||||
protocols <- r.Proto
|
protocols <- r.Proto
|
||||||
|
|
||||||
w.Header().Set(discriminator.DispositionHeader, discriminator.DispositionDeny)
|
w.Header().Set(waf.DispositionHeader, waf.DispositionDeny)
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -563,7 +563,7 @@ func TestRProxyAsksADiscriminatorThatSpeaksHTTP2OverHTTP1(t *testing.T) {
|
|||||||
|
|
||||||
// rproxy has no setting for the certificates it trusts, so it is told of this
|
// rproxy has no setting for the certificates it trusts, so it is told of this
|
||||||
// one as Go's client is told of any, in a process of its own.
|
// one as Go's client is told of any, in a process of its own.
|
||||||
certificates := filepath.Join(t.TempDir(), "discriminator.pem")
|
certificates := filepath.Join(t.TempDir(), "waf.pem")
|
||||||
|
|
||||||
err := os.WriteFile(certificates, pem.EncodeToMemory(&pem.Block{
|
err := os.WriteFile(certificates, pem.EncodeToMemory(&pem.Block{
|
||||||
Type: "CERTIFICATE", Bytes: backend.Certificate().Raw,
|
Type: "CERTIFICATE", Bytes: backend.Certificate().Raw,
|
||||||
@@ -585,7 +585,7 @@ func TestRProxyAsksADiscriminatorThatSpeaksHTTP2OverHTTP1(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
status := sendRaw(t, addr, "GET / HTTP/1.1\r\nHost: site.example\r\n"+headers+"\r\n")
|
status := sendRaw(t, addr, "GET / HTTP/1.1\r\nHost: site.example\r\n"+headers+"\r\n")
|
||||||
if status != http.StatusForbidden {
|
if status != http.StatusForbidden {
|
||||||
t.Errorf("with %s: status %d, want discriminator's 403", name, status)
|
t.Errorf("with %s: status %d, want waf's 403", name, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -594,7 +594,7 @@ func TestRProxyAsksADiscriminatorThatSpeaksHTTP2OverHTTP1(t *testing.T) {
|
|||||||
t.Errorf("with %s: asked over %s, want HTTP/1.1", name, protocol)
|
t.Errorf("with %s: asked over %s, want HTTP/1.1", name, protocol)
|
||||||
}
|
}
|
||||||
case <-time.After(waitLimit):
|
case <-time.After(waitLimit):
|
||||||
t.Errorf("with %s: discriminator was not asked", name)
|
t.Errorf("with %s: waf was not asked", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -623,8 +623,8 @@ func TestRProxyStoppingWaitsForAReportBeingSent(t *testing.T) {
|
|||||||
release := make(chan struct{})
|
release := make(chan struct{})
|
||||||
backend := httptest.NewServer(http.HandlerFunc(
|
backend := httptest.NewServer(http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Header.Get(discriminator.ClientHeader) != "" {
|
if r.Header.Get(waf.ClientHeader) != "" {
|
||||||
w.Header().Set(discriminator.DispositionHeader, discriminator.DispositionAllow)
|
w.Header().Set(waf.DispositionHeader, waf.DispositionAllow)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -731,7 +731,7 @@ func TestRProxyStoppingCutsOffARequestAfterFiveSeconds(t *testing.T) {
|
|||||||
|
|
||||||
backend := httptest.NewServer(http.HandlerFunc(
|
backend := httptest.NewServer(http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, _ *http.Request) {
|
func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set(discriminator.DispositionHeader, discriminator.DispositionAllow)
|
w.Header().Set(waf.DispositionHeader, waf.DispositionAllow)
|
||||||
}))
|
}))
|
||||||
t.Cleanup(backend.Close)
|
t.Cleanup(backend.Close)
|
||||||
|
|
||||||
@@ -794,7 +794,7 @@ func TestRProxyInvalidSettingStopsTheStart(t *testing.T) {
|
|||||||
{"no backend", wafBackend, nil},
|
{"no backend", wafBackend, nil},
|
||||||
{
|
{
|
||||||
"another invalid", rproxyTimeout,
|
"another invalid", rproxyTimeout,
|
||||||
map[string]string{wafBackend: "http://discriminator:8080", rproxyTimeout: "soon"},
|
map[string]string{wafBackend: "http://waf:8080", rproxyTimeout: "soon"},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@@ -831,7 +831,7 @@ func TestRProxyAddressInUseStopsTheStart(t *testing.T) {
|
|||||||
out := &output{}
|
out := &output{}
|
||||||
|
|
||||||
status := runRProxy(t.Context(), map[string]string{
|
status := runRProxy(t.Context(), map[string]string{
|
||||||
wafBackend: "http://discriminator:8080",
|
wafBackend: "http://waf:8080",
|
||||||
rproxyListenAddr: taken.Addr().String(),
|
rproxyListenAddr: taken.Addr().String(),
|
||||||
}, out)
|
}, out)
|
||||||
if status != 1 {
|
if status != 1 {
|
||||||
@@ -848,7 +848,7 @@ func TestMainRunsRProxy(t *testing.T) {
|
|||||||
addr, cmd, out := startRProxyProcess(t, wafBackend+"=http://127.0.0.1:9")
|
addr, cmd, out := startRProxyProcess(t, wafBackend+"=http://127.0.0.1:9")
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet,
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet,
|
||||||
"http://"+addr+discriminator.HealthPath, http.NoBody)
|
"http://"+addr+waf.HealthPath, http.NoBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("new request: %v", err)
|
t.Fatalf("new request: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-18
@@ -1,9 +1,9 @@
|
|||||||
// Package sww runs the sww process, as the command it is given.
|
// Package sww runs the sww process, as the command it is given.
|
||||||
// `sww discriminator` reads the settings, the rule files, the lookup database
|
// `sww waf` reads the settings, the rule files, the lookup database
|
||||||
// and the state files, answers rproxy's questions until it is told to stop, and
|
// and the state files, answers rproxy's questions until it is told to stop, and
|
||||||
// then stops in an orderly way, writing the state files. `sww rproxy`
|
// then stops in an orderly way, writing the state files. `sww rproxy`
|
||||||
// reads rproxy's settings alone, and runs the reverse proxy in front of one app
|
// reads rproxy's settings alone, and runs the reverse proxy in front of one app
|
||||||
// that asks discriminator about each request. `sww healthcheck` is the
|
// that asks waf about each request. `sww healthcheck` is the
|
||||||
// container's health check.
|
// container's health check.
|
||||||
package sww
|
package sww
|
||||||
|
|
||||||
@@ -23,13 +23,13 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/remotelog"
|
"sneak.berlin/go/smallwebwaf/internal/remotelog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/state"
|
"sneak.berlin/go/smallwebwaf/internal/state"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// shutdownTimeout is how long requests in progress may take to finish
|
// shutdownTimeout is how long requests in progress may take to finish
|
||||||
@@ -37,7 +37,7 @@ import (
|
|||||||
// runit and docker wait a little longer before they kill the process.
|
// runit and docker wait a little longer before they kill the process.
|
||||||
const shutdownTimeout = 5 * time.Second
|
const shutdownTimeout = 5 * time.Second
|
||||||
|
|
||||||
// remoteLogStopTimeout is how long, as discriminator stops, the log lines still
|
// remoteLogStopTimeout is how long, as waf stops, the log lines still
|
||||||
// waiting are sent to SWW_LOG_REMOTE_URL before they are given up. stdout has
|
// waiting are sent to SWW_LOG_REMOTE_URL before they are given up. stdout has
|
||||||
// carried them.
|
// carried them.
|
||||||
const remoteLogStopTimeout = 2 * time.Second
|
const remoteLogStopTimeout = 2 * time.Second
|
||||||
@@ -51,11 +51,11 @@ const usage = `usage: sww <command>
|
|||||||
|
|
||||||
commands:
|
commands:
|
||||||
rproxy the reverse proxy in front of one app
|
rproxy the reverse proxy in front of one app
|
||||||
discriminator decides for rproxy, and keeps bans, counters and history
|
waf decides for rproxy, and keeps bans, counters and history
|
||||||
healthcheck the container's health check
|
healthcheck the container's health check
|
||||||
`
|
`
|
||||||
|
|
||||||
// Params are what RunDiscriminator and RunRProxy need from the process.
|
// Params are what RunWAF and RunRProxy need from the process.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
// Version is the version of the binary, set when it is built.
|
// Version is the version of the binary, set when it is built.
|
||||||
Version string
|
Version string
|
||||||
@@ -66,7 +66,7 @@ type Params struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Main runs the command os.Args names, and returns the process's exit status.
|
// Main runs the command os.Args names, and returns the process's exit status.
|
||||||
// `sww rproxy` and `sww discriminator` run until SIGTERM or SIGINT.
|
// `sww rproxy` and `sww waf` run until SIGTERM or SIGINT.
|
||||||
// With no command, or one it does not know, it prints the commands and returns
|
// With no command, or one it does not know, it prints the commands and returns
|
||||||
// usageStatus.
|
// usageStatus.
|
||||||
func Main(version string) int {
|
func Main(version string) int {
|
||||||
@@ -84,8 +84,8 @@ func Main(version string) int {
|
|||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "rproxy":
|
case "rproxy":
|
||||||
return RunRProxy(ctx, params)
|
return RunRProxy(ctx, params)
|
||||||
case "discriminator":
|
case "waf":
|
||||||
return RunDiscriminator(ctx, params)
|
return RunWAF(ctx, params)
|
||||||
case "healthcheck":
|
case "healthcheck":
|
||||||
return HealthCheck(ctx, args[1:], os.LookupEnv, os.Stderr)
|
return HealthCheck(ctx, args[1:], os.LookupEnv, os.Stderr)
|
||||||
default:
|
default:
|
||||||
@@ -100,10 +100,10 @@ func printUsage(w io.Writer) int {
|
|||||||
return usageStatus
|
return usageStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunDiscriminator is `sww discriminator`: it reads the settings, the rule files,
|
// RunWAF is `sww waf`: it reads the settings, the rule files,
|
||||||
// the lookup database and the state files, then serves requests until ctx is
|
// the lookup database and the state files, then serves requests until ctx is
|
||||||
// done. It returns the process's exit status, 1 when discriminator cannot start.
|
// done. It returns the process's exit status, 1 when waf cannot start.
|
||||||
func RunDiscriminator(ctx context.Context, params Params) int {
|
func RunWAF(ctx context.Context, params Params) int {
|
||||||
// Until the settings are read, the one message is an invalid setting's
|
// Until the settings are read, the one message is an invalid setting's
|
||||||
// error, which every SWW_LOG_LEVEL lets through.
|
// error, which every SWW_LOG_LEVEL lets through.
|
||||||
processLog := requestlog.NewProcessLogger(params.Stdout,
|
processLog := requestlog.NewProcessLogger(params.Stdout,
|
||||||
@@ -151,7 +151,7 @@ func RunDiscriminator(ctx context.Context, params Params) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
crs, err := discriminator.NewCoreRuleSet(cfg)
|
crs, err := waf.NewCoreRuleSet(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
processLog.Error("cannot use the Core Rule Set", "error", err.Error())
|
processLog.Error("cannot use the Core Rule Set", "error", err.Error())
|
||||||
|
|
||||||
@@ -191,14 +191,14 @@ func RunDiscriminator(ctx context.Context, params Params) int {
|
|||||||
return serve(ctx, server, listener, files, ruleFiles, alertQueue, processLog)
|
return serve(ctx, server, listener, files, ruleFiles, alertQueue, processLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newServer returns the server discriminator runs, with the metrics of the alerts,
|
// newServer returns the server waf runs, with the metrics of the alerts,
|
||||||
// after reading the lookup database while SWW_LOOKUP_SOURCE is file. A lookup
|
// after reading the lookup database while SWW_LOOKUP_SOURCE is file. A lookup
|
||||||
// database that cannot be read is an error.
|
// database that cannot be read is an error.
|
||||||
func newServer(
|
func newServer(
|
||||||
cfg *config.Config, stdout io.Writer, processLog *slog.Logger,
|
cfg *config.Config, stdout io.Writer, processLog *slog.Logger,
|
||||||
now func() time.Time, ruleFiles *rules.Files, alertQueue *alerts.Queue,
|
now func() time.Time, ruleFiles *rules.Files, alertQueue *alerts.Queue,
|
||||||
crs *coreruleset.CoreRuleSet,
|
crs *coreruleset.CoreRuleSet,
|
||||||
) (*discriminator.Server, error) {
|
) (*waf.Server, error) {
|
||||||
var lookupFile *lookup.File
|
var lookupFile *lookup.File
|
||||||
|
|
||||||
if cfg.LookupSource == "file" {
|
if cfg.LookupSource == "file" {
|
||||||
@@ -215,7 +215,7 @@ func newServer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server := discriminator.New(discriminator.Params{
|
server := waf.New(waf.Params{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
RequestLog: stdout,
|
RequestLog: stdout,
|
||||||
ProcessLog: processLog,
|
ProcessLog: processLog,
|
||||||
@@ -239,7 +239,7 @@ func newServer(
|
|||||||
// loadStateFiles reads the state files into the parts of server and into
|
// loadStateFiles reads the state files into the parts of server and into
|
||||||
// alertQueue, as state.Load does.
|
// alertQueue, as state.Load does.
|
||||||
func loadStateFiles(
|
func loadStateFiles(
|
||||||
cfg *config.Config, server *discriminator.Server, alertQueue *alerts.Queue,
|
cfg *config.Config, server *waf.Server, alertQueue *alerts.Queue,
|
||||||
now func() time.Time, processLog *slog.Logger,
|
now func() time.Time, processLog *slog.Logger,
|
||||||
) (*state.Files, error) {
|
) (*state.Files, error) {
|
||||||
return state.Load(state.Params{
|
return state.Load(state.Params{
|
||||||
@@ -325,7 +325,7 @@ func startSending(
|
|||||||
// shutdownTimeout to finish, and writes every state file, alerts.json with
|
// shutdownTimeout to finish, and writes every state file, alerts.json with
|
||||||
// the alerts still waiting.
|
// the alerts still waiting.
|
||||||
func serve(
|
func serve(
|
||||||
ctx context.Context, server *discriminator.Server, listener net.Listener,
|
ctx context.Context, server *waf.Server, listener net.Listener,
|
||||||
files *state.Files, ruleFiles *rules.Files, alertQueue *alerts.Queue,
|
files *state.Files, ruleFiles *rules.Files, alertQueue *alerts.Queue,
|
||||||
processLog *slog.Logger,
|
processLog *slog.Logger,
|
||||||
) int {
|
) int {
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
|
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/sww"
|
"sneak.berlin/go/smallwebwaf/internal/sww"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -123,11 +123,11 @@ func (o *output) text() string {
|
|||||||
return o.buf.String()
|
return o.buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// run runs `sww discriminator` with the settings in env until ctx is done,
|
// run runs `sww waf` with the settings in env until ctx is done,
|
||||||
// and returns its exit status. SWW_LOOKUP_SOURCE is off unless env sets it,
|
// and returns its exit status. SWW_LOOKUP_SOURCE is off unless env sets it,
|
||||||
// so that no test sends GeoJS its clients' addresses.
|
// so that no test sends GeoJS its clients' addresses.
|
||||||
func run(ctx context.Context, env map[string]string, out *output) int {
|
func run(ctx context.Context, env map[string]string, out *output) int {
|
||||||
return sww.RunDiscriminator(ctx, sww.Params{
|
return sww.RunWAF(ctx, sww.Params{
|
||||||
Version: testVersion,
|
Version: testVersion,
|
||||||
LookupEnv: func(name string) (string, bool) {
|
LookupEnv: func(name string) (string, bool) {
|
||||||
value, ok := env[name]
|
value, ok := env[name]
|
||||||
@@ -1245,7 +1245,7 @@ func startApp(t *testing.T) string {
|
|||||||
return app.URL
|
return app.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// runUntilStopped runs `sww discriminator` with the settings in env, has use
|
// runUntilStopped runs `sww waf` with the settings in env, has use
|
||||||
// send it requests at url, then stops it as SIGTERM does, checks that it
|
// send it requests at url, then stops it as SIGTERM does, checks that it
|
||||||
// stopped in order, and returns its output.
|
// stopped in order, and returns its output.
|
||||||
func runUntilStopped(
|
func runUntilStopped(
|
||||||
@@ -1319,7 +1319,7 @@ func wantStartingLine(t *testing.T, line map[string]any, dir string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wantAllowed checks that discriminator at url allows a request from localhost.
|
// wantAllowed checks that waf at url allows a request from localhost.
|
||||||
func wantAllowed(t *testing.T, url string) {
|
func wantAllowed(t *testing.T, url string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -1504,7 +1504,7 @@ func onlyBan(t *testing.T, dir string) map[string]any {
|
|||||||
return file.Bans[0]
|
return file.Bans[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// wantRefused checks that discriminator at url refuses a request from localhost with
|
// wantRefused checks that waf at url refuses a request from localhost with
|
||||||
// 403, the default SWW_BAN_RESPONSE.
|
// 403, the default SWW_BAN_RESPONSE.
|
||||||
func wantRefused(t *testing.T, url string) {
|
func wantRefused(t *testing.T, url string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -1681,8 +1681,8 @@ func (w *webhook) waitFor(t *testing.T, event string, answered bool) webhookPost
|
|||||||
}
|
}
|
||||||
|
|
||||||
// statusFrom returns the status a request to url from the client at from is
|
// statusFrom returns the status a request to url from the client at from is
|
||||||
// answered with. The request serves rproxy and discriminator alike. rproxy reads the
|
// answered with. The request serves rproxy and waf alike. rproxy reads the
|
||||||
// client from X-Forwarded-For, and replaces the other two headers. To discriminator,
|
// client from X-Forwarded-For, and replaces the other two headers. To waf,
|
||||||
// they make it a question about that client.
|
// they make it a question about that client.
|
||||||
func statusFrom(t *testing.T, url, from string) int {
|
func statusFrom(t *testing.T, url, from string) int {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -1694,7 +1694,7 @@ func statusFrom(t *testing.T, url, from string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("X-Forwarded-For", from)
|
req.Header.Set("X-Forwarded-For", from)
|
||||||
req.Header.Set(discriminator.ClientHeader, from)
|
req.Header.Set(waf.ClientHeader, from)
|
||||||
req.Header.Set("X-Request-ID", rand.Text())
|
req.Header.Set("X-Request-ID", rand.Text())
|
||||||
|
|
||||||
transport := &http.Transport{}
|
transport := &http.Transport{}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/sww"
|
"sneak.berlin/go/smallwebwaf/internal/sww"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// commandForMain, while set in the environment, has this test binary run Main
|
// commandForMain, while set in the environment, has this test binary run Main
|
||||||
@@ -34,7 +34,7 @@ func TestMain(m *testing.M) {
|
|||||||
os.Exit(sww.Main(testVersion))
|
os.Exit(sww.Main(testVersion))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMainRunsDiscriminator(t *testing.T) {
|
func TestMainRunsWAF(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
binary, err := os.Executable()
|
binary, err := os.Executable()
|
||||||
@@ -50,7 +50,7 @@ func TestMainRunsDiscriminator(t *testing.T) {
|
|||||||
cmd.Stdout = out
|
cmd.Stdout = out
|
||||||
|
|
||||||
cmd.Env = append(os.Environ(),
|
cmd.Env = append(os.Environ(),
|
||||||
commandForMain+"="+discriminatorCommand,
|
commandForMain+"="+wafCommand,
|
||||||
listenAddr+"="+localhost+":0",
|
listenAddr+"="+localhost+":0",
|
||||||
stateDir+"="+t.TempDir(),
|
stateDir+"="+t.TempDir(),
|
||||||
rulesDir+"="+t.TempDir(),
|
rulesDir+"="+t.TempDir(),
|
||||||
@@ -67,7 +67,7 @@ func TestMainRunsDiscriminator(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
addr, _ := out.line(t, "msg", "starting")["address"].(string)
|
addr, _ := out.line(t, "msg", "starting")["address"].(string)
|
||||||
ask(t, addr, "/", http.StatusOK, discriminator.DispositionAllow)
|
ask(t, addr, "/", http.StatusOK, waf.DispositionAllow)
|
||||||
|
|
||||||
err = cmd.Process.Signal(syscall.SIGTERM)
|
err = cmd.Process.Signal(syscall.SIGTERM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -108,7 +108,7 @@ func TestMainWithoutACommandItKnowsPrintsTheCommands(t *testing.T) {
|
|||||||
t.Errorf("exit status %d (%v), want 2", code, err)
|
t.Errorf("exit status %d (%v), want 2", code, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range []string{"rproxy", discriminatorCommand, "healthcheck"} {
|
for _, name := range []string{"rproxy", wafCommand, "healthcheck"} {
|
||||||
if !strings.Contains(stderr.String(), name) {
|
if !strings.Contains(stderr.String(), name) {
|
||||||
t.Errorf("stderr %q does not name %s", stderr.String(), name)
|
t.Errorf("stderr %q does not name %s", stderr.String(), name)
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ func TestMainWithoutACommandItKnowsPrintsTheCommands(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ask sends discriminator at addr rproxy's question about a request for path from
|
// ask sends waf at addr rproxy's question about a request for path from
|
||||||
// placed, checks the answer's status and disposition, and closes it.
|
// placed, checks the answer's status and disposition, and closes it.
|
||||||
func ask(t *testing.T, addr, path string, status int, disposition string) {
|
func ask(t *testing.T, addr, path string, status int, disposition string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -132,7 +132,7 @@ func ask(t *testing.T, addr, path string, status int, disposition string) {
|
|||||||
t.Fatalf("new request: %v", err)
|
t.Fatalf("new request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set(discriminator.ClientHeader, placed)
|
req.Header.Set(waf.ClientHeader, placed)
|
||||||
req.Header.Set("X-Request-ID", rand.Text())
|
req.Header.Set("X-Request-ID", rand.Text())
|
||||||
|
|
||||||
transport := &http.Transport{}
|
transport := &http.Transport{}
|
||||||
@@ -147,16 +147,16 @@ func ask(t *testing.T, addr, path string, status int, disposition string) {
|
|||||||
_ = res.Body.Close()
|
_ = res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != status ||
|
if res.StatusCode != status ||
|
||||||
res.Header.Get(discriminator.DispositionHeader) != disposition {
|
res.Header.Get(waf.DispositionHeader) != disposition {
|
||||||
t.Errorf("answered %d with disposition %q, want %d with %q", res.StatusCode,
|
t.Errorf("answered %d with disposition %q, want %d with %q", res.StatusCode,
|
||||||
res.Header.Get(discriminator.DispositionHeader), status, disposition)
|
res.Header.Get(waf.DispositionHeader), status, disposition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runDiscriminator runs `sww discriminator` with the settings in env, has use
|
// runWAF runs `sww waf` with the settings in env, has use
|
||||||
// send it requests at the address it listens on, stops it as SIGTERM does, and
|
// send it requests at the address it listens on, stops it as SIGTERM does, and
|
||||||
// checks that it stopped in order.
|
// checks that it stopped in order.
|
||||||
func runDiscriminator(t *testing.T, env map[string]string, use func(addr string)) {
|
func runWAF(t *testing.T, env map[string]string, use func(addr string)) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
ctx, stop := context.WithCancel(t.Context())
|
ctx, stop := context.WithCancel(t.Context())
|
||||||
@@ -183,7 +183,7 @@ func runDiscriminator(t *testing.T, env map[string]string, use func(addr string)
|
|||||||
out.line(t, "msg", "stopped")
|
out.line(t, "msg", "stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDiscriminatorKeepsItsBansAcrossARestartAndNeedsNoApp(t *testing.T) {
|
func TestWAFKeepsItsBansAcrossARestartAndNeedsNoApp(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
@@ -192,13 +192,13 @@ func TestDiscriminatorKeepsItsBansAcrossARestartAndNeedsNoApp(t *testing.T) {
|
|||||||
rulesDir: filepath.Join("..", "..", "share", "rules.d"),
|
rulesDir: filepath.Join("..", "..", "share", "rules.d"),
|
||||||
}
|
}
|
||||||
|
|
||||||
runDiscriminator(t, env, func(addr string) {
|
runWAF(t, env, func(addr string) {
|
||||||
ask(t, addr, "/", http.StatusOK, discriminator.DispositionAllow)
|
ask(t, addr, "/", http.StatusOK, waf.DispositionAllow)
|
||||||
// The default rule file bans a probe for this.
|
// The default rule file bans a probe for this.
|
||||||
ask(t, addr, "/.env", http.StatusForbidden, discriminator.DispositionDeny)
|
ask(t, addr, "/.env", http.StatusForbidden, waf.DispositionDeny)
|
||||||
})
|
})
|
||||||
|
|
||||||
runDiscriminator(t, env, func(addr string) {
|
runWAF(t, env, func(addr string) {
|
||||||
ask(t, addr, "/", http.StatusForbidden, discriminator.DispositionDeny)
|
ask(t, addr, "/", http.StatusForbidden, waf.DispositionDeny)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic abuseipdb. It reads SWW_ABUSEIPDB_KEY,
|
// The heuristic abuseipdb. It reads SWW_ABUSEIPDB_KEY,
|
||||||
// SWW_ABUSEIPDB_MIN_SCORE and SWW_REPUTATION_ACTION.
|
// SWW_ABUSEIPDB_MIN_SCORE and SWW_REPUTATION_ACTION.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -49,7 +49,7 @@ func (rq *request) answerHealth() {
|
|||||||
_, _ = io.WriteString(rq.out, "ok\n")
|
_, _ = io.WriteString(rq.out, "ok\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// answerAdmin answers a request for one of discriminator's own endpoints. Each
|
// answerAdmin answers a request for one of waf's own endpoints. Each
|
||||||
// needs a token, sent as Authorization: Bearer <token>: the metrics
|
// needs a token, sent as Authorization: Bearer <token>: the metrics
|
||||||
// SWW_METRICS_TOKEN, the others SWW_ADMIN_TOKEN. A request without it is
|
// SWW_METRICS_TOKEN, the others SWW_ADMIN_TOKEN. A request without it is
|
||||||
// refused with 401. An endpoint whose token is unset answers 404, as any other
|
// refused with 401. An endpoint whose token is unset answers 404, as any other
|
||||||
@@ -250,7 +250,7 @@ type clientAnswer struct {
|
|||||||
Bans []state.BanEntry `json:"bans"`
|
Bans []state.BanEntry `json:"bans"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// showClient answers GET ClientsPath<ip> with what discriminator knows of the client:
|
// showClient answers GET ClientsPath<ip> with what waf knows of the client:
|
||||||
// its counters, its history, which holds its country as last looked up and its
|
// its counters, its history, which holds its country as last looked up and its
|
||||||
// offences, and its bans with their notes.
|
// offences, and its bans with their notes.
|
||||||
func (rq *request) showClient() {
|
func (rq *request) showClient() {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/state"
|
"sneak.berlin/go/smallwebwaf/internal/state"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -88,11 +88,11 @@ func TestAdminEndpointsNeedTheAdminToken(t *testing.T) {
|
|||||||
|
|
||||||
// Any other request under /_sww/ is not found.
|
// Any other request under /_sww/ is not found.
|
||||||
for _, e := range []adminEndpoint{
|
for _, e := range []adminEndpoint{
|
||||||
{http.MethodPut, discriminator.BansPath, banOtherClient},
|
{http.MethodPut, waf.BansPath, banOtherClient},
|
||||||
{http.MethodDelete, discriminator.BansPath, ""},
|
{http.MethodDelete, waf.BansPath, ""},
|
||||||
{http.MethodGet, discriminator.BansPath + "/" + otherClient, ""},
|
{http.MethodGet, waf.BansPath + "/" + otherClient, ""},
|
||||||
{http.MethodPost, discriminator.ClientsPath + otherClient, ""},
|
{http.MethodPost, waf.ClientsPath + otherClient, ""},
|
||||||
{http.MethodGet, strings.TrimSuffix(discriminator.ClientsPath, "/"), ""},
|
{http.MethodGet, strings.TrimSuffix(waf.ClientsPath, "/"), ""},
|
||||||
} {
|
} {
|
||||||
s.admin(e.method, e.path, e.body, http.StatusNotFound)
|
s.admin(e.method, e.path, e.body, http.StatusNotFound)
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ func TestBanAddedListedAndLiftedThroughTheEndpoints(t *testing.T) {
|
|||||||
Reason: "probes for logins",
|
Reason: "probes for logins",
|
||||||
}
|
}
|
||||||
|
|
||||||
wantBans(t, s.admin(http.MethodPost, discriminator.BansPath, banOtherClient,
|
wantBans(t, s.admin(http.MethodPost, waf.BansPath, banOtherClient,
|
||||||
http.StatusOK), want)
|
http.StatusOK), want)
|
||||||
|
|
||||||
line := s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
line := s.get(client, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
|
||||||
@@ -129,7 +129,7 @@ func TestBanAddedListedAndLiftedThroughTheEndpoints(t *testing.T) {
|
|||||||
// Its notes count the request it refused.
|
// Its notes count the request it refused.
|
||||||
want.Notes.Requests, want.Notes.Refused = 1, 1
|
want.Notes.Requests, want.Notes.Refused = 1, 1
|
||||||
|
|
||||||
wantBans(t, s.admin(http.MethodGet, discriminator.BansPath, "", http.StatusOK), want)
|
wantBans(t, s.admin(http.MethodGet, waf.BansPath, "", http.StatusOK), want)
|
||||||
|
|
||||||
// Ten minutes on, lifting the bans on client lifts that one, which is
|
// Ten minutes on, lifting the bans on client lifts that one, which is
|
||||||
// kept, marked lifted.
|
// kept, marked lifted.
|
||||||
@@ -138,13 +138,13 @@ func TestBanAddedListedAndLiftedThroughTheEndpoints(t *testing.T) {
|
|||||||
lifted := clk.Now()
|
lifted := clk.Now()
|
||||||
want.Lifted = &lifted
|
want.Lifted = &lifted
|
||||||
|
|
||||||
wantBans(t, s.admin(http.MethodDelete, discriminator.BansPath+"/"+client, "",
|
wantBans(t, s.admin(http.MethodDelete, waf.BansPath+"/"+client, "",
|
||||||
http.StatusOK), want)
|
http.StatusOK), want)
|
||||||
s.get(client, http.StatusOK, requestlog.ActionForward)
|
s.get(client, http.StatusOK, requestlog.ActionForward)
|
||||||
wantBans(t, s.admin(http.MethodGet, discriminator.BansPath, "", http.StatusOK), want)
|
wantBans(t, s.admin(http.MethodGet, waf.BansPath, "", http.StatusOK), want)
|
||||||
|
|
||||||
// No ban on it is active any more.
|
// No ban on it is active any more.
|
||||||
s.admin(http.MethodDelete, discriminator.BansPath+"/"+client, "", http.StatusNotFound)
|
s.admin(http.MethodDelete, waf.BansPath+"/"+client, "", http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBanToAddGivesItsNetblockAndDuration(t *testing.T) {
|
func TestBanToAddGivesItsNetblockAndDuration(t *testing.T) {
|
||||||
@@ -181,7 +181,7 @@ func TestBanToAddGivesItsNetblockAndDuration(t *testing.T) {
|
|||||||
want.Expires = &expires
|
want.Expires = &expires
|
||||||
}
|
}
|
||||||
|
|
||||||
wantBans(t, s.admin(http.MethodPost, discriminator.BansPath, body, http.StatusOK),
|
wantBans(t, s.admin(http.MethodPost, waf.BansPath, body, http.StatusOK),
|
||||||
want)
|
want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +251,7 @@ func TestBanToAddThatCannotBeReadIsRefused(t *testing.T) {
|
|||||||
"request body too large",
|
"request body too large",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
got := s.admin(http.MethodPost, discriminator.BansPath, tc.body,
|
got := s.admin(http.MethodPost, waf.BansPath, tc.body,
|
||||||
http.StatusBadRequest)
|
http.StatusBadRequest)
|
||||||
if !strings.Contains(string(got.body), tc.want) {
|
if !strings.Contains(string(got.body), tc.want) {
|
||||||
t.Errorf("%.80s was answered %q, want it to say %q", tc.body, got.body, tc.want)
|
t.Errorf("%.80s was answered %q, want it to say %q", tc.body, got.body, tc.want)
|
||||||
@@ -285,7 +285,7 @@ func TestClientEndpointShowsTheClientAndItsBans(t *testing.T) {
|
|||||||
Bans []state.BanEntry `json:"bans"`
|
Bans []state.BanEntry `json:"bans"`
|
||||||
}
|
}
|
||||||
|
|
||||||
decode(t, s.admin(http.MethodGet, discriminator.ClientsPath+addr, "",
|
decode(t, s.admin(http.MethodGet, waf.ClientsPath+addr, "",
|
||||||
http.StatusOK), &got)
|
http.StatusOK), &got)
|
||||||
|
|
||||||
if got.Client == nil {
|
if got.Client == nil {
|
||||||
@@ -308,15 +308,15 @@ func TestClientEndpointShowsTheClientAndItsBans(t *testing.T) {
|
|||||||
|
|
||||||
// Of an address no request came from and no ban covers, nothing is
|
// Of an address no request came from and no ban covers, nothing is
|
||||||
// known.
|
// known.
|
||||||
got := s.admin(http.MethodGet, discriminator.ClientsPath+"198.51.100.99", "",
|
got := s.admin(http.MethodGet, waf.ClientsPath+"198.51.100.99", "",
|
||||||
http.StatusOK)
|
http.StatusOK)
|
||||||
if string(got.body) != "{\n \"client\": null,\n \"bans\": []\n}\n" {
|
if string(got.body) != "{\n \"client\": null,\n \"bans\": []\n}\n" {
|
||||||
t.Errorf("an unknown client is answered\n%s", got.body)
|
t.Errorf("an unknown client is answered\n%s", got.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.admin(http.MethodGet, discriminator.ClientsPath+"203.0.113", "",
|
s.admin(http.MethodGet, waf.ClientsPath+"203.0.113", "",
|
||||||
http.StatusBadRequest)
|
http.StatusBadRequest)
|
||||||
s.admin(http.MethodDelete, discriminator.BansPath+"/203.0.113.0/24", "",
|
s.admin(http.MethodDelete, waf.BansPath+"/203.0.113.0/24", "",
|
||||||
http.StatusBadRequest)
|
http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,14 +347,14 @@ type adminEndpoint struct {
|
|||||||
// bans on otherClient, and asking about otherClient.
|
// bans on otherClient, and asking about otherClient.
|
||||||
func adminEndpoints() []adminEndpoint {
|
func adminEndpoints() []adminEndpoint {
|
||||||
return []adminEndpoint{
|
return []adminEndpoint{
|
||||||
{http.MethodGet, discriminator.BansPath, ""},
|
{http.MethodGet, waf.BansPath, ""},
|
||||||
{http.MethodPost, discriminator.BansPath, banOtherClient},
|
{http.MethodPost, waf.BansPath, banOtherClient},
|
||||||
{http.MethodDelete, discriminator.BansPath + "/" + otherClient, ""},
|
{http.MethodDelete, waf.BansPath + "/" + otherClient, ""},
|
||||||
{http.MethodGet, discriminator.ClientsPath + otherClient, ""},
|
{http.MethodGet, waf.ClientsPath + otherClient, ""},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// admin sends a request with method for path, with body, to discriminator's own
|
// admin sends a request with method for path, with body, to waf's own
|
||||||
// address, with the admin token, and checks that it is answered with status, its
|
// address, with the admin token, and checks that it is answered with status, its
|
||||||
// log line's action own_endpoint. It returns the answer.
|
// log line's action own_endpoint. It returns the answer.
|
||||||
func (s *sender) admin(method, path, body string, status int) answer {
|
func (s *sender) admin(method, path, body string, status int) answer {
|
||||||
@@ -370,7 +370,7 @@ func (s *sender) ownEndpointRequest(
|
|||||||
) answer {
|
) answer {
|
||||||
s.t.Helper()
|
s.t.Helper()
|
||||||
|
|
||||||
req := newRequest(s.t, method, s.discriminatorAddr, path, strings.NewReader(body))
|
req := newRequest(s.t, method, s.wafAddr, path, strings.NewReader(body))
|
||||||
if authorization != "" {
|
if authorization != "" {
|
||||||
req.Header.Set("Authorization", authorization)
|
req.Header.Set("Authorization", authorization)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -203,7 +203,7 @@ func TestObserveModeWorksOutABanOnlyWhenItsAlertWouldBeSent(t *testing.T) {
|
|||||||
// never sent them, and returns the queue they wait in as well.
|
// never sent them, and returns the queue they wait in as well.
|
||||||
func startWithAlerts(
|
func startWithAlerts(
|
||||||
t *testing.T, env map[string]string,
|
t *testing.T, env map[string]string,
|
||||||
) (*sender, *clock, *discriminator.Server, *alerts.Queue) {
|
) (*sender, *clock, *waf.Server, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return startAppWithAlerts(t, func(http.ResponseWriter, *http.Request) {}, env)
|
return startAppWithAlerts(t, func(http.ResponseWriter, *http.Request) {}, env)
|
||||||
@@ -212,7 +212,7 @@ func startWithAlerts(
|
|||||||
// startAppWithAlerts is startWithAlerts in front of the app handler.
|
// startAppWithAlerts is startWithAlerts in front of the app handler.
|
||||||
func startAppWithAlerts(
|
func startAppWithAlerts(
|
||||||
t *testing.T, handler http.HandlerFunc, env map[string]string,
|
t *testing.T, handler http.HandlerFunc, env map[string]string,
|
||||||
) (*sender, *clock, *discriminator.Server, *alerts.Queue) {
|
) (*sender, *clock, *waf.Server, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
app := startApp(t, handler)
|
app := startApp(t, handler)
|
||||||
@@ -226,7 +226,7 @@ func startAppWithAlerts(
|
|||||||
|
|
||||||
addr, out, server, queue := startProxyWithAlerts(t, app.URL, "", clk.Now, settings)
|
addr, out, server, queue := startProxyWithAlerts(t, app.URL, "", clk.Now, settings)
|
||||||
|
|
||||||
s := &sender{t: t, addr: addr, discriminatorAddr: server.Addr, out: out}
|
s := &sender{t: t, addr: addr, wafAddr: server.Addr, out: out}
|
||||||
|
|
||||||
return s, clk, server, queue
|
return s, clk, server, queue
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
@@ -13,9 +13,9 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/anomaly"
|
"sneak.berlin/go/smallwebwaf/internal/anomaly"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The anomaly thresholds: the prefix of a scope followed by the end of a
|
// The anomaly thresholds: the prefix of a scope followed by the end of a
|
||||||
@@ -319,7 +319,7 @@ func TestRequestCountsForTheASNumberGeoJSGivesBeforeItEnds(t *testing.T) {
|
|||||||
// kept.
|
// kept.
|
||||||
geojsURL, _, release := startHeldGeoJS(t)
|
geojsURL, _, release := startHeldGeoJS(t)
|
||||||
|
|
||||||
var server atomic.Pointer[discriminator.Server]
|
var server atomic.Pointer[waf.Server]
|
||||||
|
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {
|
||||||
release()
|
release()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -15,16 +15,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// answerTimeLimit is the default of rproxy's SWRP_TIMEOUT_MS, the time
|
// answerTimeLimit is the default of rproxy's SWRP_TIMEOUT_MS, the time
|
||||||
// discriminator has to answer.
|
// waf has to answer.
|
||||||
answerTimeLimit = 2 * time.Second
|
answerTimeLimit = 2 * time.Second
|
||||||
// defaultLookupWait is the default of SWW_LOOKUP_TIMEOUT.
|
// defaultLookupWait is the default of SWW_LOOKUP_TIMEOUT.
|
||||||
defaultLookupWait = 800 * time.Millisecond
|
defaultLookupWait = 800 * time.Millisecond
|
||||||
@@ -33,7 +33,7 @@ const (
|
|||||||
headerFieldLimit = 100
|
headerFieldLimit = 100
|
||||||
// cookieHeader is the header the cookie limit counts.
|
// cookieHeader is the header the cookie limit counts.
|
||||||
cookieHeader = "Cookie"
|
cookieHeader = "Cookie"
|
||||||
// tooLarge is how discriminator refuses a request over the header limits.
|
// tooLarge is how waf refuses a request over the header limits.
|
||||||
tooLarge = http.StatusRequestHeaderFieldsTooLarge
|
tooLarge = http.StatusRequestHeaderFieldsTooLarge
|
||||||
// silentAbuseIPDBURL is where AbuseIPDB never answers: at the stand-in
|
// silentAbuseIPDBURL is where AbuseIPDB never answers: at the stand-in
|
||||||
// that silentGeoJSURL names too.
|
// that silentGeoJSURL names too.
|
||||||
@@ -56,7 +56,7 @@ func cookies(count int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fillHeaders adds headers to a question until the checks see total header
|
// fillHeaders adds headers to a question until the checks see total header
|
||||||
// fields: all but ClientHeader and PeerHeader, which discriminator removes. Go's
|
// fields: all but ClientHeader and PeerHeader, which waf removes. Go's
|
||||||
// client sends nothing else but Host, which the limit does not count, once
|
// client sends nothing else but Host, which the limit does not count, once
|
||||||
// User-Agent is set.
|
// User-Agent is set.
|
||||||
func fillHeaders(req *http.Request, total int) {
|
func fillHeaders(req *http.Request, total int) {
|
||||||
@@ -70,13 +70,13 @@ func fillHeaders(req *http.Request, total int) {
|
|||||||
func TestSixteenThousandCookiesAreRefusedBeforeTheCoreRuleSetReadsThem(t *testing.T) {
|
func TestSixteenThousandCookiesAreRefusedBeforeTheCoreRuleSetReadsThem(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s := startDiscriminator(t, "", map[string]string{wafMode: block})
|
s := startWAF(t, "", map[string]string{wafMode: block})
|
||||||
req := s.question(siteA, client, "/")
|
req := s.question(siteA, client, "/")
|
||||||
req.Header.Set(cookieHeader, strings.Repeat("a;", 16000))
|
req.Header.Set(cookieHeader, strings.Repeat("a;", 16000))
|
||||||
|
|
||||||
got := do(t, req)
|
got := do(t, req)
|
||||||
|
|
||||||
wantDisposition(t, got, tooLarge, discriminator.DispositionDeny)
|
wantDisposition(t, got, tooLarge, waf.DispositionDeny)
|
||||||
|
|
||||||
line := s.out.requestLine(t)
|
line := s.out.requestLine(t)
|
||||||
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
||||||
@@ -89,7 +89,7 @@ func TestSixteenThousandCookiesAreRefusedBeforeTheCoreRuleSetReadsThem(t *testin
|
|||||||
func TestHeaderLimitRefusalNamesItsHeuristicCountsAndLimits(t *testing.T) {
|
func TestHeaderLimitRefusalNamesItsHeuristicCountsAndLimits(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s := startDiscriminator(t, "", nil)
|
s := startWAF(t, "", nil)
|
||||||
req := s.question(siteA, client, "/")
|
req := s.question(siteA, client, "/")
|
||||||
fillHeaders(req, headerFieldLimit+1)
|
fillHeaders(req, headerFieldLimit+1)
|
||||||
do(t, req)
|
do(t, req)
|
||||||
@@ -143,7 +143,7 @@ func TestEachHeaderLimitRefusesARequestOverIt(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s := startDiscriminator(t, "", map[string]string{wafMode: block})
|
s := startWAF(t, "", map[string]string{wafMode: block})
|
||||||
req := s.question(siteA, client, "/")
|
req := s.question(siteA, client, "/")
|
||||||
tc.make(req)
|
tc.make(req)
|
||||||
|
|
||||||
@@ -151,13 +151,13 @@ func TestEachHeaderLimitRefusesARequestOverIt(t *testing.T) {
|
|||||||
line := s.out.requestLine(t)
|
line := s.out.requestLine(t)
|
||||||
|
|
||||||
if tc.refused {
|
if tc.refused {
|
||||||
wantDisposition(t, got, tooLarge, discriminator.DispositionDeny)
|
wantDisposition(t, got, tooLarge, waf.DispositionDeny)
|
||||||
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wantDisposition(t, got, http.StatusOK, discriminator.DispositionAllow)
|
wantDisposition(t, got, http.StatusOK, waf.DispositionAllow)
|
||||||
wantLine(t, line, http.StatusOK, requestlog.ActionForward)
|
wantLine(t, line, http.StatusOK, requestlog.ActionForward)
|
||||||
|
|
||||||
if line.DurationCoreRuleSetMS == nil {
|
if line.DurationCoreRuleSetMS == nil {
|
||||||
@@ -178,11 +178,11 @@ func TestHeaderLimitsHoldWhateverTheCoreRuleSetSettingsSay(t *testing.T) {
|
|||||||
t.Run(fmt.Sprint(env), func(t *testing.T) {
|
t.Run(fmt.Sprint(env), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s := startDiscriminator(t, "", env)
|
s := startWAF(t, "", env)
|
||||||
req := s.question(siteA, client, "/")
|
req := s.question(siteA, client, "/")
|
||||||
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
wantDisposition(t, do(t, req), tooLarge, discriminator.DispositionDeny)
|
wantDisposition(t, do(t, req), tooLarge, waf.DispositionDeny)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,23 +190,23 @@ func TestHeaderLimitsHoldWhateverTheCoreRuleSetSettingsSay(t *testing.T) {
|
|||||||
func TestClientInTheAllowNetsSkipsTheHeaderLimits(t *testing.T) {
|
func TestClientInTheAllowNetsSkipsTheHeaderLimits(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s := startDiscriminator(t, "", map[string]string{wafMode: block, allowNets: client})
|
s := startWAF(t, "", map[string]string{wafMode: block, allowNets: client})
|
||||||
req := s.question(siteA, client, "/")
|
req := s.question(siteA, client, "/")
|
||||||
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
wantDisposition(t, do(t, req), http.StatusOK, discriminator.DispositionAllow)
|
wantDisposition(t, do(t, req), http.StatusOK, waf.DispositionAllow)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObserveModeAllowsARequestOverTheHeaderLimitsUnread(t *testing.T) {
|
func TestObserveModeAllowsARequestOverTheHeaderLimitsUnread(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s := startDiscriminator(t, "", map[string]string{mode: observe, wafMode: block})
|
s := startWAF(t, "", map[string]string{mode: observe, wafMode: block})
|
||||||
req := s.question(siteA, client, "/")
|
req := s.question(siteA, client, "/")
|
||||||
req.Header.Set(cookieHeader, strings.Repeat("a;", 16000))
|
req.Header.Set(cookieHeader, strings.Repeat("a;", 16000))
|
||||||
|
|
||||||
got := do(t, req)
|
got := do(t, req)
|
||||||
|
|
||||||
wantDisposition(t, got, http.StatusOK, discriminator.DispositionAllow)
|
wantDisposition(t, got, http.StatusOK, waf.DispositionAllow)
|
||||||
|
|
||||||
line := s.out.requestLine(t)
|
line := s.out.requestLine(t)
|
||||||
if line.WouldAction != requestlog.ActionRequestHeaderTooLarge ||
|
if line.WouldAction != requestlog.ActionRequestHeaderTooLarge ||
|
||||||
@@ -219,7 +219,7 @@ func TestObserveModeAllowsARequestOverTheHeaderLimitsUnread(t *testing.T) {
|
|||||||
func TestRequestsOverTheHeaderLimitsCountTowardTheRateLimits(t *testing.T) {
|
func TestRequestsOverTheHeaderLimitsCountTowardTheRateLimits(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s := startDiscriminator(t, "", map[string]string{rateLimitPerMinute: "1"})
|
s := startWAF(t, "", map[string]string{rateLimitPerMinute: "1"})
|
||||||
ask := func() answer {
|
ask := func() answer {
|
||||||
req := s.question(siteA, client, "/")
|
req := s.question(siteA, client, "/")
|
||||||
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
@@ -227,8 +227,8 @@ func TestRequestsOverTheHeaderLimitsCountTowardTheRateLimits(t *testing.T) {
|
|||||||
return do(t, req)
|
return do(t, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantDisposition(t, ask(), tooLarge, discriminator.DispositionDeny)
|
wantDisposition(t, ask(), tooLarge, waf.DispositionDeny)
|
||||||
wantDisposition(t, ask(), http.StatusForbidden, discriminator.DispositionDeny)
|
wantDisposition(t, ask(), http.StatusForbidden, waf.DispositionDeny)
|
||||||
|
|
||||||
lines := s.out.requestLines(t, 2)
|
lines := s.out.requestLines(t, 2)
|
||||||
wantLine(t, lines[1], http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
wantLine(t, lines[1], http.StatusForbidden, requestlog.ActionClientIsRateLimited)
|
||||||
@@ -246,13 +246,13 @@ func TestTheHeaderLimitsComeAfterTheTrapPathsAndTheRuleFiles(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
s := startDiscriminator(t, "", map[string]string{
|
s := startWAF(t, "", map[string]string{
|
||||||
trapPaths: trapPathList, rulesDir: writeRules(t, testRules),
|
trapPaths: trapPathList, rulesDir: writeRules(t, testRules),
|
||||||
})
|
})
|
||||||
req := s.question(siteA, client, tc.path)
|
req := s.question(siteA, client, tc.path)
|
||||||
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
wantDisposition(t, do(t, req), http.StatusForbidden, discriminator.DispositionDeny)
|
wantDisposition(t, do(t, req), http.StatusForbidden, waf.DispositionDeny)
|
||||||
wantLine(t, s.out.requestLine(t), http.StatusForbidden, tc.action)
|
wantLine(t, s.out.requestLine(t), http.StatusForbidden, tc.action)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -262,13 +262,13 @@ func TestNotWaitingForGeoJSNeverLowersAClientsLimit(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
geojsURL, _ := startGeoJS(t)
|
geojsURL, _ := startGeoJS(t)
|
||||||
s := startDiscriminator(t, geojsURL, map[string]string{
|
s := startWAF(t, geojsURL, map[string]string{
|
||||||
unknownLimitPercent: "0", lookupTimeout: "1h",
|
unknownLimitPercent: "0", lookupTimeout: "1h",
|
||||||
})
|
})
|
||||||
req := s.question(siteA, fromDE, "/")
|
req := s.question(siteA, fromDE, "/")
|
||||||
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
wantDisposition(t, do(t, req), tooLarge, discriminator.DispositionDeny)
|
wantDisposition(t, do(t, req), tooLarge, waf.DispositionDeny)
|
||||||
|
|
||||||
line := s.out.requestLine(t)
|
line := s.out.requestLine(t)
|
||||||
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
wantLine(t, line, tooLarge, requestlog.ActionRequestHeaderTooLarge)
|
||||||
@@ -278,14 +278,14 @@ func TestNotWaitingForGeoJSNeverLowersAClientsLimit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The client is under no ban, and GeoJS places it in a country.
|
// The client is under no ban, and GeoJS places it in a country.
|
||||||
wantDisposition(t, s.ask(fromDE, "/"), http.StatusOK, discriminator.DispositionAllow)
|
wantDisposition(t, s.ask(fromDE, "/"), http.StatusOK, waf.DispositionAllow)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAClientGeoJSPlacedInNoCountryCountsAsUnknownWithoutWaiting(t *testing.T) {
|
func TestAClientGeoJSPlacedInNoCountryCountsAsUnknownWithoutWaiting(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
geojsURL, _ := startGeoJS(t)
|
geojsURL, _ := startGeoJS(t)
|
||||||
s := startDiscriminator(t, geojsURL, map[string]string{unknownLimitPercent: "0"})
|
s := startWAF(t, geojsURL, map[string]string{unknownLimitPercent: "0"})
|
||||||
s.server.GeoJS.Load([]lookup.Answer{{
|
s.server.GeoJS.Load([]lookup.Answer{{
|
||||||
Client: netip.MustParsePrefix(client + "/32"), ASN: "AS64512",
|
Client: netip.MustParsePrefix(client + "/32"), ASN: "AS64512",
|
||||||
Answered: s.clock.Now(), Used: s.clock.Now(),
|
Answered: s.clock.Now(), Used: s.clock.Now(),
|
||||||
@@ -294,7 +294,7 @@ func TestAClientGeoJSPlacedInNoCountryCountsAsUnknownWithoutWaiting(t *testing.T
|
|||||||
req := s.question(siteA, client, "/")
|
req := s.question(siteA, client, "/")
|
||||||
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
wantDisposition(t, do(t, req), http.StatusForbidden, discriminator.DispositionDeny)
|
wantDisposition(t, do(t, req), http.StatusForbidden, waf.DispositionDeny)
|
||||||
wantLine(t, s.out.requestLine(t), http.StatusForbidden,
|
wantLine(t, s.out.requestLine(t), http.StatusForbidden,
|
||||||
requestlog.ActionClientIsRateLimited)
|
requestlog.ActionClientIsRateLimited)
|
||||||
}
|
}
|
||||||
@@ -303,14 +303,14 @@ func TestObserveModeWaitsForGeoJSForARequestOverTheHeaderLimits(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
geojsURL, _ := startGeoJS(t)
|
geojsURL, _ := startGeoJS(t)
|
||||||
s := startDiscriminator(t, geojsURL, map[string]string{
|
s := startWAF(t, geojsURL, map[string]string{
|
||||||
mode: observe, addLookupHeaders: on, lookupTimeout: "1h",
|
mode: observe, addLookupHeaders: on, lookupTimeout: "1h",
|
||||||
})
|
})
|
||||||
req := s.question(siteA, fromDE, "/")
|
req := s.question(siteA, fromDE, "/")
|
||||||
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
req.Header.Set(cookieHeader, cookies(cookieLimit+1))
|
||||||
|
|
||||||
got := do(t, req)
|
got := do(t, req)
|
||||||
wantDisposition(t, got, http.StatusOK, discriminator.DispositionAllow)
|
wantDisposition(t, got, http.StatusOK, waf.DispositionAllow)
|
||||||
|
|
||||||
asn, country := got.header.Get("X-Client-Asn"), got.header.Get("X-Client-Country")
|
asn, country := got.header.Get("X-Client-Asn"), got.header.Get("X-Client-Country")
|
||||||
if asn != asnDE || country != "DE" {
|
if asn != asnDE || country != "DE" {
|
||||||
@@ -319,18 +319,18 @@ func TestObserveModeWaitsForGeoJSForARequestOverTheHeaderLimits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newDiscriminatorInBubble makes a discriminator for a synctest bubble: GeoJS never
|
// newWAFInBubble makes a waf for a synctest bubble: GeoJS never
|
||||||
// answers, a setting needs its answer, and everything is at its default but
|
// answers, a setting needs its answer, and everything is at its default but
|
||||||
// for env. No request to it may use the network.
|
// for env. No request to it may use the network.
|
||||||
func newDiscriminatorInBubble(
|
func newWAFInBubble(
|
||||||
t *testing.T, env map[string]string,
|
t *testing.T, env map[string]string,
|
||||||
) (*discriminator.Server, *output) {
|
) (*waf.Server, *output) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
settings := map[string]string{tagDeny: "country=KP"}
|
settings := map[string]string{tagDeny: "country=KP"}
|
||||||
maps.Copy(settings, env)
|
maps.Copy(settings, env)
|
||||||
|
|
||||||
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now, settings)
|
server, out, _ := newWAF(t, silentGeoJSURL, time.Now, settings)
|
||||||
|
|
||||||
return server, out
|
return server, out
|
||||||
}
|
}
|
||||||
@@ -338,13 +338,13 @@ func newDiscriminatorInBubble(
|
|||||||
// askDirectly hands a question about a request from client straight to server,
|
// askDirectly hands a question about a request from client straight to server,
|
||||||
// as change leaves it, and returns the answer's status and how long it took.
|
// as change leaves it, and returns the answer's status and how long it took.
|
||||||
func askDirectly(
|
func askDirectly(
|
||||||
t *testing.T, server *discriminator.Server, change func(*http.Request),
|
t *testing.T, server *waf.Server, change func(*http.Request),
|
||||||
) (int, time.Duration) {
|
) (int, time.Duration) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
|
||||||
req.RemoteAddr = net.JoinHostPort(rproxyPeer, "1234")
|
req.RemoteAddr = net.JoinHostPort(rproxyPeer, "1234")
|
||||||
req.Header.Set(discriminator.ClientHeader, client)
|
req.Header.Set(waf.ClientHeader, client)
|
||||||
req.Header.Set(requestIDHeader, "the-id")
|
req.Header.Set(requestIDHeader, "the-id")
|
||||||
change(req)
|
change(req)
|
||||||
|
|
||||||
@@ -357,17 +357,17 @@ func askDirectly(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// banClient bans client at now.
|
// banClient bans client at now.
|
||||||
func banClient(_ *testing.T, server *discriminator.Server, now time.Time) {
|
func banClient(_ *testing.T, server *waf.Server, now time.Time) {
|
||||||
server.Ledger.BanForLimit(netip.MustParsePrefix(client+"/32"), now, bans.Notes{})
|
server.Ledger.BanForLimit(netip.MustParsePrefix(client+"/32"), now, bans.Notes{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// countClient counts one request from client at now for the rate limits.
|
// countClient counts one request from client at now for the rate limits.
|
||||||
func countClient(_ *testing.T, server *discriminator.Server, now time.Time) {
|
func countClient(_ *testing.T, server *waf.Server, now time.Time) {
|
||||||
server.Limiter.Count(netip.MustParsePrefix(client+"/32"), now, wholeLimit)
|
server.Limiter.Count(netip.MustParsePrefix(client+"/32"), now, wholeLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// listClient puts client on the blocklist at dropURL.
|
// listClient puts client on the blocklist at dropURL.
|
||||||
func listClient(t *testing.T, server *discriminator.Server, now time.Time) {
|
func listClient(t *testing.T, server *waf.Server, now time.Time) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
err := server.Lists.Load([]reputation.List{{
|
err := server.Lists.Load([]reputation.List{{
|
||||||
@@ -379,13 +379,13 @@ func listClient(t *testing.T, server *discriminator.Server, now time.Time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// decideOnClient puts client on the CrowdSec decision list.
|
// decideOnClient puts client on the CrowdSec decision list.
|
||||||
func decideOnClient(t *testing.T, server *discriminator.Server, now time.Time) {
|
func decideOnClient(t *testing.T, server *waf.Server, now time.Time) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
loadDecisions(t, server, now, clientsDecision)
|
loadDecisions(t, server, now, clientsDecision)
|
||||||
}
|
}
|
||||||
|
|
||||||
// leaveAsItIs prepares nothing.
|
// leaveAsItIs prepares nothing.
|
||||||
func leaveAsItIs(*testing.T, *discriminator.Server, time.Time) {}
|
func leaveAsItIs(*testing.T, *waf.Server, time.Time) {}
|
||||||
|
|
||||||
func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
|
func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -396,7 +396,7 @@ func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
env map[string]string
|
env map[string]string
|
||||||
// prepare gets the server ready, at now, before the question.
|
// prepare gets the server ready, at now, before the question.
|
||||||
prepare func(t *testing.T, server *discriminator.Server, now time.Time)
|
prepare func(t *testing.T, server *waf.Server, now time.Time)
|
||||||
// cookie is the request's Cookie header.
|
// cookie is the request's Cookie header.
|
||||||
cookie string
|
cookie string
|
||||||
status int
|
status int
|
||||||
@@ -443,7 +443,7 @@ func TestARefusedRequestDoesNotWaitForASlowGeoJS(t *testing.T) {
|
|||||||
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
||||||
// says, so that a request that does not wait takes no time at all.
|
// 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 := newDiscriminatorInBubble(t, tc.env)
|
server, out := newWAFInBubble(t, tc.env)
|
||||||
tc.prepare(t, server, time.Now())
|
tc.prepare(t, server, time.Now())
|
||||||
|
|
||||||
status, took := askDirectly(t, server, func(req *http.Request) {
|
status, took := askDirectly(t, server, func(req *http.Request) {
|
||||||
@@ -471,7 +471,7 @@ func TestAQuestionWaitsForASlowGeoJSOnlyAsLongAsTheDefaultSays(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
env map[string]string
|
env map[string]string
|
||||||
// prepare gets the server ready, at now, before the question.
|
// prepare gets the server ready, at now, before the question.
|
||||||
prepare func(t *testing.T, server *discriminator.Server, now time.Time)
|
prepare func(t *testing.T, server *waf.Server, now time.Time)
|
||||||
status int
|
status int
|
||||||
action string
|
action string
|
||||||
}{
|
}{
|
||||||
@@ -500,7 +500,7 @@ func TestAQuestionWaitsForASlowGeoJSOnlyAsLongAsTheDefaultSays(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
server, out := newDiscriminatorInBubble(t, tc.env)
|
server, out := newWAFInBubble(t, tc.env)
|
||||||
tc.prepare(t, server, time.Now())
|
tc.prepare(t, server, time.Now())
|
||||||
|
|
||||||
status, took := askDirectly(t, server, func(*http.Request) {})
|
status, took := askDirectly(t, server, func(*http.Request) {})
|
||||||
@@ -516,7 +516,7 @@ func TestAQuestionWaitsForASlowGeoJSOnlyAsLongAsTheDefaultSays(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDiscriminatorAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
|
func TestWAFAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
) {
|
) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -531,12 +531,12 @@ func TestDiscriminatorAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
|
|||||||
|
|
||||||
t.Cleanup(func() { _ = resolver.Close() })
|
t.Cleanup(func() { _ = resolver.Close() })
|
||||||
|
|
||||||
server, out, _ := newDiscriminatorWith(t, func(
|
server, out, _ := newWAFWith(t, func(
|
||||||
params discriminator.Params,
|
params waf.Params,
|
||||||
) *discriminator.Server {
|
) *waf.Server {
|
||||||
params.AbuseIPDBURL = silentAbuseIPDBURL
|
params.AbuseIPDBURL = silentAbuseIPDBURL
|
||||||
|
|
||||||
return discriminator.New(params)
|
return waf.New(params)
|
||||||
}, held, time.Now, map[string]string{
|
}, held, time.Now, map[string]string{
|
||||||
tagDeny: "country=KP", wafMode: block,
|
tagDeny: "country=KP", wafMode: block,
|
||||||
dnsblZones: dnsblZone, dnsblResolver: resolver.LocalAddr().String(),
|
dnsblZones: dnsblZone, dnsblResolver: resolver.LocalAddr().String(),
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic active_ban, and the code that makes bans, which several
|
// The heuristic active_ban, and the code that makes bans, which several
|
||||||
// heuristics share. active_ban reads SWW_MODE; the bans are the ban ledger's.
|
// heuristics share. active_ban reads SWW_MODE; the bans are the ban ledger's.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -393,14 +393,14 @@ func (c *clock) advance(d time.Duration) {
|
|||||||
c.now = c.now.Add(d)
|
c.now = c.now.Add(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startWithClock starts discriminator, and rproxy in front of an app that answers 200,
|
// startWithClock starts waf, and rproxy in front of an app that answers 200,
|
||||||
// with the settings in env on top of trusting localhost's
|
// with the settings in env on top of trusting localhost's
|
||||||
// X-Forwarded-For, clients' AS numbers and countries looked up at
|
// X-Forwarded-For, clients' AS numbers and countries looked up at
|
||||||
// geojsURL, and a clock set to midnight, the start of a bucket in every
|
// geojsURL, and a clock set to midnight, the start of a bucket in every
|
||||||
// window.
|
// window.
|
||||||
func startWithClock(
|
func startWithClock(
|
||||||
t *testing.T, geojsURL string, env map[string]string,
|
t *testing.T, geojsURL string, env map[string]string,
|
||||||
) (*sender, *clock, *discriminator.Server) {
|
) (*sender, *clock, *waf.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
app := startApp(t, func(http.ResponseWriter, *http.Request) {})
|
||||||
@@ -410,20 +410,20 @@ func startWithClock(
|
|||||||
|
|
||||||
addr, out, server := startProxyWithClock(t, app.URL, geojsURL, clk.Now, settings)
|
addr, out, server := startProxyWithClock(t, app.URL, geojsURL, clk.Now, settings)
|
||||||
|
|
||||||
return &sender{t: t, addr: addr, discriminatorAddr: server.Addr, out: out}, clk, server
|
return &sender{t: t, addr: addr, wafAddr: server.Addr, out: out}, clk, server
|
||||||
}
|
}
|
||||||
|
|
||||||
// sender sends requests to rproxy one after another, each on a connection of its
|
// sender sends requests to rproxy one after another, each on a connection of its
|
||||||
// own, and checks each one's answer and log line. discriminatorAddr is where
|
// own, and checks each one's answer and log line. wafAddr is where
|
||||||
// discriminator's own endpoints are, which ownEndpointRequest asks. Every request
|
// waf's own endpoints are, which ownEndpointRequest asks. Every request
|
||||||
// discriminator gets must come through the sender, since the log lines are
|
// waf gets must come through the sender, since the log lines are
|
||||||
// matched to them in order.
|
// matched to them in order.
|
||||||
type sender struct {
|
type sender struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
addr string
|
addr string
|
||||||
discriminatorAddr string
|
wafAddr string
|
||||||
out *output
|
out *output
|
||||||
sent int
|
sent int
|
||||||
}
|
}
|
||||||
|
|
||||||
// get sends a GET request for / from the client at from.
|
// get sends a GET request for / from the client at from.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -15,9 +15,9 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
|
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The biased thresholds.
|
// The biased thresholds.
|
||||||
@@ -291,7 +291,7 @@ func TestClientWithoutAnAnswerInTimeHasTheUnknownLimitPercent(t *testing.T) {
|
|||||||
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
||||||
// says, with a GeoJS that never answers.
|
// says, with a GeoJS that never answers.
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
|
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
||||||
map[string]string{unknownLimitPercent: "0"})
|
map[string]string{unknownLimitPercent: "0"})
|
||||||
|
|
||||||
// Once the time the request waits for its answer is up, the client
|
// Once the time the request waits for its answer is up, the client
|
||||||
@@ -335,7 +335,7 @@ func TestRequestWaitsForItsLookupWhileABiasedThresholdIsSet(t *testing.T) {
|
|||||||
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
|
||||||
// says, with a GeoJS that never answers.
|
// says, with a GeoJS that never answers.
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
|
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
lookupTimeout: timeout.String(), tagLists: "vpn=" + vpnList, tc.setting: tc.value,
|
lookupTimeout: timeout.String(), tagLists: "vpn=" + vpnList, tc.setting: tc.value,
|
||||||
})
|
})
|
||||||
@@ -437,7 +437,7 @@ func TestBanForALoweredLimitGivesThePercentageInItsNotesAndItsAlert(t *testing.T
|
|||||||
// clock.
|
// clock.
|
||||||
func startWithLookups(
|
func startWithLookups(
|
||||||
t *testing.T, env map[string]string,
|
t *testing.T, env map[string]string,
|
||||||
) (*sender, *discriminator.Server, *alerts.Queue) {
|
) (*sender, *waf.Server, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
s, _, server, queue := startWithLookupsAndClock(t, env)
|
s, _, server, queue := startWithLookupsAndClock(t, env)
|
||||||
@@ -452,7 +452,7 @@ func startWithLookups(
|
|||||||
// country, and no other address.
|
// country, and no other address.
|
||||||
func startWithLookupsAndClock(
|
func startWithLookupsAndClock(
|
||||||
t *testing.T, env map[string]string,
|
t *testing.T, env map[string]string,
|
||||||
) (*sender, *clock, *discriminator.Server, *alerts.Queue) {
|
) (*sender, *clock, *waf.Server, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "ipinfo_lite.mmdb")
|
path := filepath.Join(t.TempDir(), "ipinfo_lite.mmdb")
|
||||||
@@ -482,12 +482,12 @@ func (s *sender) uploadFrom(from string) logLine {
|
|||||||
// straight to server's handler, without the network, and returns once it is
|
// straight to server's handler, without the network, and returns once it is
|
||||||
// answered.
|
// answered.
|
||||||
func serveFromDE(
|
func serveFromDE(
|
||||||
t *testing.T, server *discriminator.Server, method string, body io.Reader,
|
t *testing.T, server *waf.Server, method string, body io.Reader,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
req := httptest.NewRequestWithContext(t.Context(), method, "/", body)
|
req := httptest.NewRequestWithContext(t.Context(), method, "/", body)
|
||||||
req.Header.Set(discriminator.ClientHeader, fromDE)
|
req.Header.Set(waf.ClientHeader, fromDE)
|
||||||
req.Header.Set(requestIDHeader, "the-id")
|
req.Header.Set(requestIDHeader, "the-id")
|
||||||
|
|
||||||
server.Handler.ServeHTTP(httptest.NewRecorder(), req)
|
server.Handler.ServeHTTP(httptest.NewRecorder(), req)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic blocklists. It reads SWW_BLOCKLIST_ACTION, and the blocklists
|
// The heuristic blocklists. It reads SWW_BLOCKLIST_ACTION, and the blocklists
|
||||||
// through the reputation lists.
|
// through the reputation lists.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic byte_limits. It reads SWW_BYTES_COUNT, and the byte limits
|
// The heuristic byte_limits. It reads SWW_BYTES_COUNT, and the byte limits
|
||||||
// and biased thresholds through the limiter and limitPercentages.
|
// and biased thresholds through the limiter and limitPercentages.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic core_rule_set. It reads SWW_WAF_MODE, SWW_WAF_EXEMPT_PATHS
|
// The heuristic core_rule_set. It reads SWW_WAF_MODE, SWW_WAF_EXEMPT_PATHS
|
||||||
// and SWW_WAF_ANOMALY_THRESHOLD.
|
// and SWW_WAF_ANOMALY_THRESHOLD.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic countries. It reads SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
// The heuristic countries. It reads SWW_EXCLUSIVELY_ALLOWED_COUNTRIES.
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic crowdsec. It reads SWW_CROWDSEC_LAPI_URL, and the decisions
|
// The heuristic crowdsec. It reads SWW_CROWDSEC_LAPI_URL, and the decisions
|
||||||
// through the reputation lists.
|
// through the reputation lists.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The CrowdSec settings, and the tests' engine, which is never asked: each
|
// The CrowdSec settings, and the tests' engine, which is never asked: each
|
||||||
@@ -184,7 +184,7 @@ func TestObserveModeForwardsAClientTheCrowdSecDecisionListListsAndAlertsTheBan(
|
|||||||
// decisionsURL, answer, the engine's answer, fetched at fetched, as
|
// decisionsURL, answer, the engine's answer, fetched at fetched, as
|
||||||
// reputation.json would at start.
|
// reputation.json would at start.
|
||||||
func loadDecisions(
|
func loadDecisions(
|
||||||
t *testing.T, server *discriminator.Server, fetched time.Time, answer string,
|
t *testing.T, server *waf.Server, fetched time.Time, answer string,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic deny_nets. It reads SWW_DENY_NETS.
|
// The heuristic deny_nets. It reads SWW_DENY_NETS.
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic dnsbl. It reads SWW_REPUTATION_ACTION, and the DNSBL zones'
|
// The heuristic dnsbl. It reads SWW_REPUTATION_ACTION, and the DNSBL zones'
|
||||||
// verdicts through the DNSBL checker.
|
// verdicts through the DNSBL checker.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic header_limits. It reads no setting: its limits are fixed.
|
// The heuristic header_limits. It reads no setting: its limits are fixed.
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHealthEndpointCountsTowardNoLimit(t *testing.T) {
|
func TestHealthEndpointCountsTowardNoLimit(t *testing.T) {
|
||||||
@@ -21,7 +21,7 @@ func TestHealthEndpointCountsTowardNoLimit(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
for range healthChecks {
|
for range healthChecks {
|
||||||
got := s.ownEndpointRequest("", http.MethodGet, discriminator.HealthPath, "",
|
got := s.ownEndpointRequest("", http.MethodGet, waf.HealthPath, "",
|
||||||
http.StatusOK)
|
http.StatusOK)
|
||||||
|
|
||||||
if string(got.body) != "ok\n" || got.header.Get("Content-Type") != contentType {
|
if string(got.body) != "ok\n" || got.header.Get("Content-Type") != contentType {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
// and add one line for it to one of the lists below. Its settings go in
|
// and add one line for it to one of the lists below. Its settings go in
|
||||||
// config.Config, and the comment at the top of its file names them.
|
// config.Config, and the comment at the top of its file names them.
|
||||||
|
|
||||||
// heuristic is one of the checks that decide what discriminator does with a
|
// heuristic is one of the checks that decide what waf does with a
|
||||||
// request.
|
// request.
|
||||||
type heuristic interface {
|
type heuristic interface {
|
||||||
// name is the heuristic's name, which the log line gives.
|
// name is the heuristic's name, which the log line gives.
|
||||||
@@ -70,7 +70,7 @@ func afterLookup() []heuristic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// afterAnswer are the heuristics that run on a request once discriminator has
|
// afterAnswer are the heuristics that run on a request once waf has
|
||||||
// answered it, before its decision line is written. They ban the client, and do
|
// answered it, before its decision line is written. They ban the client, and do
|
||||||
// not change what the request got: their verdict's action is not used.
|
// not change what the request got: their verdict's action is not used.
|
||||||
func afterAnswer() []heuristic {
|
func afterAnswer() []heuristic {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHistoryKeepsEachRequestOfTheClient(t *testing.T) {
|
func TestHistoryKeepsEachRequestOfTheClient(t *testing.T) {
|
||||||
@@ -100,8 +100,8 @@ func TestOwnEndpointsAreNotInTheHistory(t *testing.T) {
|
|||||||
|
|
||||||
s, _, server := startWithClock(t, "", map[string]string{metricsToken: token})
|
s, _, server := startWithClock(t, "", map[string]string{metricsToken: token})
|
||||||
|
|
||||||
s.ownEndpointRequest("", http.MethodGet, discriminator.HealthPath, "", http.StatusOK)
|
s.ownEndpointRequest("", http.MethodGet, waf.HealthPath, "", http.StatusOK)
|
||||||
s.ownEndpointRequest("", http.MethodGet, discriminator.MetricsPath, "",
|
s.ownEndpointRequest("", http.MethodGet, waf.MetricsPath, "",
|
||||||
http.StatusUnauthorized)
|
http.StatusUnauthorized)
|
||||||
s.scrape()
|
s.scrape()
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ func TestPathUnderTheOwnPrefixIsNeitherForwardedNorRefused(t *testing.T) {
|
|||||||
|
|
||||||
// historyOf returns the history of the client at addr.
|
// historyOf returns the history of the client at addr.
|
||||||
func historyOf(
|
func historyOf(
|
||||||
t *testing.T, server *discriminator.Server, addr string,
|
t *testing.T, server *waf.Server, addr string,
|
||||||
) ratelimit.History {
|
) ratelimit.History {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -379,13 +379,13 @@ func TestRequestWaitsAsLongAsTheLookupTimeoutSays(t *testing.T) {
|
|||||||
// clock of the test's own: the wait lasts exactly as long as it should,
|
// clock of the test's own: the wait lasts exactly as long as it should,
|
||||||
// however slowly the test process runs. Nothing in it may wait on the
|
// however slowly the test process runs. Nothing in it may wait on the
|
||||||
// network, which would keep that clock from moving on: the request is
|
// network, which would keep that clock from moving on: the request is
|
||||||
// handed to discriminator's handler, and GeoJS is one that never answers.
|
// handed to waf's handler, and GeoJS is one that never answers.
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
// Not the default. The exclusive list needs the answer, and the app
|
// Not the default. The exclusive list needs the answer, and the app
|
||||||
// is never reached.
|
// is never reached.
|
||||||
const timeout = 3 * time.Second
|
const timeout = 3 * time.Second
|
||||||
|
|
||||||
server, out, _ := newDiscriminator(t, silentGeoJSURL, time.Now,
|
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
lookupTimeout: timeout.String(),
|
lookupTimeout: timeout.String(),
|
||||||
allowedCountries: "DE",
|
allowedCountries: "DE",
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -32,7 +32,7 @@ func TestMetricsAreOffWhileTheTokenIsUnset(t *testing.T) {
|
|||||||
|
|
||||||
// An empty token does not match the unset one either.
|
// An empty token does not match the unset one either.
|
||||||
for _, authorization := range []string{bearer, "Bearer ", ""} {
|
for _, authorization := range []string{bearer, "Bearer ", ""} {
|
||||||
s.ownEndpointRequest(authorization, http.MethodGet, discriminator.MetricsPath, "",
|
s.ownEndpointRequest(authorization, http.MethodGet, waf.MetricsPath, "",
|
||||||
http.StatusNotFound)
|
http.StatusNotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,19 +46,19 @@ func TestMetricsNeedTheTokenAndOtherPathsAreNotFound(t *testing.T) {
|
|||||||
method, path, authorization string
|
method, path, authorization string
|
||||||
status int
|
status int
|
||||||
}{
|
}{
|
||||||
{http.MethodGet, discriminator.MetricsPath, "", http.StatusUnauthorized},
|
{http.MethodGet, waf.MetricsPath, "", http.StatusUnauthorized},
|
||||||
{
|
{
|
||||||
http.MethodGet, discriminator.MetricsPath, "Bearer " + strings.ToUpper(token),
|
http.MethodGet, waf.MetricsPath, "Bearer " + strings.ToUpper(token),
|
||||||
http.StatusUnauthorized,
|
http.StatusUnauthorized,
|
||||||
},
|
},
|
||||||
{http.MethodGet, discriminator.MetricsPath, "Basic " + token,
|
{http.MethodGet, waf.MetricsPath, "Basic " + token,
|
||||||
http.StatusUnauthorized},
|
http.StatusUnauthorized},
|
||||||
{http.MethodGet, discriminator.MetricsPath, bearer, http.StatusOK},
|
{http.MethodGet, waf.MetricsPath, bearer, http.StatusOK},
|
||||||
{http.MethodGet, discriminator.MetricsPath, "bearer " + token, http.StatusOK},
|
{http.MethodGet, waf.MetricsPath, "bearer " + token, http.StatusOK},
|
||||||
{http.MethodPost, discriminator.MetricsPath, bearer, http.StatusNotFound},
|
{http.MethodPost, waf.MetricsPath, bearer, http.StatusNotFound},
|
||||||
{http.MethodGet, discriminator.MetricsPath + "/", bearer, http.StatusNotFound},
|
{http.MethodGet, waf.MetricsPath + "/", bearer, http.StatusNotFound},
|
||||||
{http.MethodGet, "/_sww/bans", bearer, http.StatusNotFound},
|
{http.MethodGet, "/_sww/bans", bearer, http.StatusNotFound},
|
||||||
{http.MethodPost, discriminator.HealthPath, "", http.StatusNotFound},
|
{http.MethodPost, waf.HealthPath, "", http.StatusNotFound},
|
||||||
} {
|
} {
|
||||||
got := s.ownEndpointRequest(tc.authorization, tc.method, tc.path, "", tc.status)
|
got := s.ownEndpointRequest(tc.authorization, tc.method, tc.path, "", tc.status)
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ func TestMetricsCountTheBansAnAdminMakes(t *testing.T) {
|
|||||||
wantMetric(t, s.scrape(), admins, 1)
|
wantMetric(t, s.scrape(), admins, 1)
|
||||||
|
|
||||||
// And a ban made through the endpoint.
|
// And a ban made through the endpoint.
|
||||||
s.admin(http.MethodPost, discriminator.BansPath, banOtherClient, http.StatusOK)
|
s.admin(http.MethodPost, waf.BansPath, banOtherClient, http.StatusOK)
|
||||||
wantMetric(t, s.scrape(), admins, 2)
|
wantMetric(t, s.scrape(), admins, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,12 +361,12 @@ func keptAnswer(addr, country string) lookup.Answer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// scrape asks discriminator at addr for the metrics, with the token, and returns
|
// scrape asks waf at addr for the metrics, with the token, and returns
|
||||||
// them.
|
// them.
|
||||||
func scrape(t *testing.T, addr string) string {
|
func scrape(t *testing.T, addr string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
req := newRequest(t, http.MethodGet, addr, discriminator.MetricsPath, http.NoBody)
|
req := newRequest(t, http.MethodGet, addr, waf.MetricsPath, http.NoBody)
|
||||||
req.Header.Set("Authorization", bearer)
|
req.Header.Set("Authorization", bearer)
|
||||||
|
|
||||||
got := do(t, req)
|
got := do(t, req)
|
||||||
@@ -381,7 +381,7 @@ func scrape(t *testing.T, addr string) string {
|
|||||||
func (s *sender) scrape() string {
|
func (s *sender) scrape() string {
|
||||||
s.t.Helper()
|
s.t.Helper()
|
||||||
|
|
||||||
return string(s.ownEndpointRequest(bearer, http.MethodGet, discriminator.MetricsPath,
|
return string(s.ownEndpointRequest(bearer, http.MethodGet, waf.MetricsPath,
|
||||||
"", http.StatusOK).body)
|
"", http.StatusOK).body)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Package discriminator is the server the discriminator command runs. It answers
|
// Package waf is the server the waf command runs. It answers
|
||||||
// rproxy's questions, takes rproxy's reports and answers its own endpoints, and
|
// rproxy's questions, takes rproxy's reports and answers its own endpoints, and
|
||||||
// writes a log line for each.
|
// writes a log line for each.
|
||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -25,11 +25,11 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ownEndpointPrefix starts the path of each of discriminator's own endpoints. The app
|
// ownEndpointPrefix starts the path of each of waf's own endpoints. The app
|
||||||
// never sees a path under it.
|
// never sees a path under it.
|
||||||
const ownEndpointPrefix = "/_sww/"
|
const ownEndpointPrefix = "/_sww/"
|
||||||
|
|
||||||
// HealthPath is the health endpoint of discriminator and of rproxy, which the
|
// HealthPath is the health endpoint of waf and of rproxy, which the
|
||||||
// container's health check asks.
|
// container's health check asks.
|
||||||
const HealthPath = "/_sww/healthz"
|
const HealthPath = "/_sww/healthz"
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ const MetricsPath = "/_sww/metrics"
|
|||||||
// a client's address, lifts them, with SWW_ADMIN_TOKEN.
|
// a client's address, lifts them, with SWW_ADMIN_TOKEN.
|
||||||
const BansPath = "/_sww/bans"
|
const BansPath = "/_sww/bans"
|
||||||
|
|
||||||
// ClientsPath is where an admin asks what discriminator knows of a client, by
|
// ClientsPath is where an admin asks what waf knows of a client, by
|
||||||
// the client's address after it, with SWW_ADMIN_TOKEN.
|
// the client's address after it, with SWW_ADMIN_TOKEN.
|
||||||
const ClientsPath = "/_sww/clients/"
|
const ClientsPath = "/_sww/clients/"
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ const ClientsPath = "/_sww/clients/"
|
|||||||
type Params struct {
|
type Params struct {
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
// RequestLog receives two JSON lines per question, one at its decision and
|
// RequestLog receives two JSON lines per question, one at its decision and
|
||||||
// one when its report comes, and one per request to discriminator's own
|
// one when its report comes, and one per request to waf's own
|
||||||
// endpoints.
|
// endpoints.
|
||||||
RequestLog io.Writer
|
RequestLog io.Writer
|
||||||
// ProcessLog receives the process's own messages.
|
// ProcessLog receives the process's own messages.
|
||||||
@@ -73,7 +73,7 @@ type Params struct {
|
|||||||
// Rules are the rule files' rules, which each request is checked
|
// Rules are the rule files' rules, which each request is checked
|
||||||
// against.
|
// against.
|
||||||
Rules *rules.Files
|
Rules *rules.Files
|
||||||
// Alerts receive the alert for each ban discriminator makes or makes
|
// Alerts receive the alert for each ban waf makes or makes
|
||||||
// permanent, for each count over an anomaly threshold, for each request
|
// permanent, for each count over an anomaly threshold, for each request
|
||||||
// whose client a blocklist, the CrowdSec decision list, a DNSBL zone or
|
// whose client a blocklist, the CrowdSec decision list, a DNSBL zone or
|
||||||
// AbuseIPDB lists, for each request the Core Rule Set scores at or over
|
// AbuseIPDB lists, for each request the Core Rule Set scores at or over
|
||||||
@@ -83,7 +83,7 @@ type Params struct {
|
|||||||
Alerts *alerts.Queue
|
Alerts *alerts.Queue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server is the server discriminator runs, with the parts whose state the state
|
// Server is the server waf runs, with the parts whose state the state
|
||||||
// files keep, the lookup database, nil unless SWW_LOOKUP_SOURCE is file, the
|
// files keep, the lookup database, nil unless SWW_LOOKUP_SOURCE is file, the
|
||||||
// lists fetched from URLs, which its Run fetches, the DNSBL zones' verdicts,
|
// lists fetched from URLs, which its Run fetches, the DNSBL zones' verdicts,
|
||||||
// AbuseIPDB's scores and checks spent, and the metrics. RouteWatch, nil while
|
// AbuseIPDB's scores and checks spent, and the metrics. RouteWatch, nil while
|
||||||
@@ -248,7 +248,7 @@ func NewCoreRuleSet(cfg *config.Config) (*coreruleset.CoreRuleSet, error) {
|
|||||||
return coreRuleSet, nil
|
return coreRuleSet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handler is discriminator's handler. It holds what every request shares; what
|
// handler is waf's handler. It holds what every request shares; what
|
||||||
// belongs to one request is in a request.
|
// belongs to one request is in a request.
|
||||||
type handler struct {
|
type handler struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -17,11 +17,11 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/rproxy"
|
"sneak.berlin/go/smallwebwaf/internal/rproxy"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -177,9 +177,9 @@ func (o *output) requestLine(t *testing.T) logLine {
|
|||||||
return o.requestLines(t, 1)[0]
|
return o.requestLines(t, 1)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// endedLines waits for count requests that went through rproxy, or to discriminator's
|
// endedLines waits for count requests that went through rproxy, or to waf's
|
||||||
// own endpoints, to have ended, and returns the last log line of each, in the
|
// own endpoints, to have ended, and returns the last log line of each, in the
|
||||||
// order discriminator took them. A question's last line is the one its report writes.
|
// order waf took them. A question's last line is the one its report writes.
|
||||||
func (o *output) endedLines(t *testing.T, count int) []logLine {
|
func (o *output) endedLines(t *testing.T, count int) []logLine {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ func (o *output) endedLines(t *testing.T, count int) []logLine {
|
|||||||
|
|
||||||
ended := len(found) >= count
|
ended := len(found) >= count
|
||||||
for _, line := range found[:min(count, len(found))] {
|
for _, line := range found[:min(count, len(found))] {
|
||||||
if line.DiscriminatorLine == requestlog.DiscriminatorLineDecision {
|
if line.WAFLine == requestlog.WAFLineDecision {
|
||||||
ended = false
|
ended = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,7 +207,7 @@ func (o *output) endedLines(t *testing.T, count int) []logLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// lastLines returns the last request log line written so far of each request,
|
// lastLines returns the last request log line written so far of each request,
|
||||||
// in the order discriminator took them.
|
// in the order waf took them.
|
||||||
func (o *output) lastLines(t *testing.T) []logLine {
|
func (o *output) lastLines(t *testing.T) []logLine {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -273,9 +273,9 @@ func startApp(t *testing.T, app http.HandlerFunc) *httptest.Server {
|
|||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
// startProxy starts discriminator, and rproxy in front of the app at appURL, each with
|
// startProxy starts waf, and rproxy in front of the app at appURL, each with
|
||||||
// the settings in env on top of the defaults, and returns where rproxy listens
|
// the settings in env on top of the defaults, and returns where rproxy listens
|
||||||
// and what discriminator writes.
|
// and what waf writes.
|
||||||
func startProxy(t *testing.T, appURL string, env map[string]string) (string, *output) {
|
func startProxy(t *testing.T, appURL string, env map[string]string) (string, *output) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -295,14 +295,14 @@ func startProxyWithGeoJS(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startProxyWithClock is startProxyWithGeoJS with requests counted and
|
// startProxyWithClock is startProxyWithGeoJS with requests counted and
|
||||||
// bans made by the time now tells, and returns discriminator's server as well, whose
|
// bans made by the time now tells, and returns waf's server as well, whose
|
||||||
// Addr is where discriminator listens. Unless env sets SWW_RULES_DIR, it is an empty
|
// Addr is where waf listens. Unless env sets SWW_RULES_DIR, it is an empty
|
||||||
// directory, of no rules, and unless it sets SWW_INSTANCE_NAME, that is app,
|
// directory, of no rules, and unless it sets SWW_INSTANCE_NAME, that is app,
|
||||||
// the label instance of every metric.
|
// the label instance of every metric.
|
||||||
func startProxyWithClock(
|
func startProxyWithClock(
|
||||||
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
||||||
env map[string]string,
|
env map[string]string,
|
||||||
) (string, *output, *discriminator.Server) {
|
) (string, *output, *waf.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
addr, out, server, _ := startProxyWithAlerts(t, appURL, geojsURL, now, env)
|
addr, out, server, _ := startProxyWithAlerts(t, appURL, geojsURL, now, env)
|
||||||
@@ -311,14 +311,14 @@ func startProxyWithClock(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startProxyWithAlerts is startProxyWithClock, and returns the queue of
|
// startProxyWithAlerts is startProxyWithClock, and returns the queue of
|
||||||
// the alerts discriminator raises as well, as newDiscriminator makes them.
|
// the alerts waf raises as well, as newWAF makes them.
|
||||||
func startProxyWithAlerts(
|
func startProxyWithAlerts(
|
||||||
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
t *testing.T, appURL, geojsURL string, now func() time.Time,
|
||||||
env map[string]string,
|
env map[string]string,
|
||||||
) (string, *output, *discriminator.Server, *alerts.Queue) {
|
) (string, *output, *waf.Server, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
server, out, alertQueue := newDiscriminator(t, geojsURL, now, env)
|
server, out, alertQueue := newWAF(t, geojsURL, now, env)
|
||||||
serve(t, server)
|
serve(t, server)
|
||||||
|
|
||||||
return startRProxy(t, server.Addr, appURL, env), out, server, alertQueue
|
return startRProxy(t, server.Addr, appURL, env), out, server, alertQueue
|
||||||
@@ -326,7 +326,7 @@ func startProxyWithAlerts(
|
|||||||
|
|
||||||
// serve serves server on a port of its own until the test ends, and sets its
|
// serve serves server on a port of its own until the test ends, and sets its
|
||||||
// Addr to where it listens.
|
// Addr to where it listens.
|
||||||
func serve(t *testing.T, server *discriminator.Server) {
|
func serve(t *testing.T, server *waf.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
|
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", localhost+":0")
|
||||||
@@ -345,17 +345,17 @@ func serve(t *testing.T, server *discriminator.Server) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// startRProxy starts rproxy in front of the app at appURL, asking the discriminator
|
// startRProxy starts rproxy in front of the app at appURL, asking the waf
|
||||||
// at discriminatorAddr, with the settings in env on top of the defaults, and returns
|
// at wafAddr, with the settings in env on top of the defaults, and returns
|
||||||
// where it listens. What it writes is dropped. SWRP_TIMEOUT_MS is a minute unless
|
// where it listens. What it writes is dropped. SWRP_TIMEOUT_MS is a minute unless
|
||||||
// env sets it, so that on a busy host SWRP_DEFAULT_DISPOSITION does not decide.
|
// env sets it, so that on a busy host SWRP_DEFAULT_DISPOSITION does not decide.
|
||||||
func startRProxy(
|
func startRProxy(
|
||||||
t *testing.T, discriminatorAddr, appURL string, env map[string]string,
|
t *testing.T, wafAddr, appURL string, env map[string]string,
|
||||||
) string {
|
) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
settings := map[string]string{
|
settings := map[string]string{
|
||||||
"SWRP_WAF_BACKEND": "http://" + discriminatorAddr,
|
"SWRP_WAF_BACKEND": "http://" + wafAddr,
|
||||||
"SWRP_UPSTREAM_URL": appURL,
|
"SWRP_UPSTREAM_URL": appURL,
|
||||||
"SWRP_TIMEOUT_MS": "60000",
|
"SWRP_TIMEOUT_MS": "60000",
|
||||||
}
|
}
|
||||||
@@ -392,7 +392,7 @@ func startRProxy(
|
|||||||
return listener.Addr().String()
|
return listener.Addr().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// newDiscriminator makes discriminator's server, without starting it, and
|
// newWAF makes waf's server, without starting it, and
|
||||||
// returns it, what it writes, and the queue of the alerts it raises, as the
|
// returns it, what it writes, and the queue of the alerts it raises, as the
|
||||||
// settings in env make it. No alert is sent from the queue: they wait in it,
|
// settings in env make it. No alert is sent from the queue: they wait in it,
|
||||||
// for the test to look at.
|
// for the test to look at.
|
||||||
@@ -402,19 +402,19 @@ func startRProxy(
|
|||||||
// at abuseIPDBURL while env sets SWW_ABUSEIPDB_KEY. SWW_WAF_MODE is off unless
|
// at abuseIPDBURL while env sets SWW_ABUSEIPDB_KEY. SWW_WAF_MODE is off unless
|
||||||
// env sets it, so that only the tests of the Core Rule Set have their requests
|
// env sets it, so that only the tests of the Core Rule Set have their requests
|
||||||
// inspected by it.
|
// inspected by it.
|
||||||
func newDiscriminator(
|
func newWAF(
|
||||||
t *testing.T, geojsURL string, now func() time.Time, env map[string]string,
|
t *testing.T, geojsURL string, now func() time.Time, env map[string]string,
|
||||||
) (*discriminator.Server, *output, *alerts.Queue) {
|
) (*waf.Server, *output, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
return newDiscriminatorWith(t, discriminator.New, geojsURL, now, env)
|
return newWAFWith(t, waf.New, geojsURL, now, env)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newDiscriminatorWith is newDiscriminator for the server build makes.
|
// newWAFWith is newWAF for the server build makes.
|
||||||
func newDiscriminatorWith(
|
func newWAFWith(
|
||||||
t *testing.T, build func(discriminator.Params) *discriminator.Server,
|
t *testing.T, build func(waf.Params) *waf.Server,
|
||||||
geojsURL string, now func() time.Time, env map[string]string,
|
geojsURL string, now func() time.Time, env map[string]string,
|
||||||
) (*discriminator.Server, *output, *alerts.Queue) {
|
) (*waf.Server, *output, *alerts.Queue) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
settings := map[string]string{
|
settings := map[string]string{
|
||||||
@@ -466,12 +466,12 @@ func newDiscriminatorWith(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coreRuleSet, err := discriminator.NewCoreRuleSet(cfg)
|
coreRuleSet, err := waf.NewCoreRuleSet(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Core Rule Set: %v", err)
|
t.Fatalf("Core Rule Set: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
server := build(discriminator.Params{
|
server := build(waf.Params{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
RequestLog: out,
|
RequestLog: out,
|
||||||
ProcessLog: processLog,
|
ProcessLog: processLog,
|
||||||
@@ -516,7 +516,7 @@ func readAnswer(res *http.Response) answer {
|
|||||||
return answer{status: res.StatusCode, header: res.Header, body: body, err: err}
|
return answer{status: res.StatusCode, header: res.Header, body: body, err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newRequest makes a request for path to addr, where rproxy or discriminator listens.
|
// newRequest makes a request for path to addr, where rproxy or waf listens.
|
||||||
func newRequest(t *testing.T, method, addr, path string, body io.Reader) *http.Request {
|
func newRequest(t *testing.T, method, addr, path string, body io.Reader) *http.Request {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic rate_limits. It reads SWW_RATE_LIMIT_EXEMPT_NETS,
|
// The heuristic rate_limits. It reads SWW_RATE_LIMIT_EXEMPT_NETS,
|
||||||
// SWW_RATE_LIMIT_EXEMPT_PATHS and SWW_BAN_RESPONSE, and the rate limits and
|
// SWW_RATE_LIMIT_EXEMPT_PATHS and SWW_BAN_RESPONSE, and the rate limits and
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic refusal_limit. It reads SWW_ERROR_BURST_THRESHOLD and
|
// The heuristic refusal_limit. It reads SWW_ERROR_BURST_THRESHOLD and
|
||||||
// SWW_BAN_RESPONSE.
|
// SWW_BAN_RESPONSE.
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// refusalLimit counts the request once it has been answered, if discriminator
|
// refusalLimit counts the request once it has been answered, if waf
|
||||||
// refused it after a rule file match, a trap path or a Core Rule Set match, and
|
// refused it after a rule file match, a trap path or a Core Rule Set match, and
|
||||||
// in observe mode if enforce mode would have: more than
|
// in observe mode if enforce mode would have: more than
|
||||||
// SWW_ERROR_BURST_THRESHOLD such refusals of the client within a minute break
|
// SWW_ERROR_BURST_THRESHOLD such refusals of the client within a minute break
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
@@ -10,14 +10,14 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
|
const errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
|
||||||
|
|
||||||
// refused is a request the tests here send, which discriminator refuses after a rule
|
// refused is a request the tests here send, which waf refuses after a rule
|
||||||
// file match.
|
// file match.
|
||||||
type refused int
|
type refused int
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ func (r refused) send(s *sender, from string) logLine {
|
|||||||
// SWW_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
|
// SWW_ERROR_BURST_THRESHOLD at threshold, and the settings in env.
|
||||||
func startForErrorBurst(
|
func startForErrorBurst(
|
||||||
t *testing.T, threshold string, env map[string]string,
|
t *testing.T, threshold string, env map[string]string,
|
||||||
) (*sender, *clock, *discriminator.Server) {
|
) (*sender, *clock, *waf.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
settings := map[string]string{
|
settings := map[string]string{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// What the reputation heuristics share: blocklists, crowdsec, dnsbl and
|
// What the reputation heuristics share: blocklists, crowdsec, dnsbl and
|
||||||
// abuseipdb.
|
// abuseipdb.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -15,10 +15,10 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
"sneak.berlin/go/smallwebwaf/internal/reputation"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The reputation settings.
|
// The reputation settings.
|
||||||
@@ -125,13 +125,13 @@ func TestReputationRefusalNamesItsHeuristicAndWhatListedTheClient(t *testing.T)
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
heuristic string
|
heuristic string
|
||||||
env map[string]string
|
env map[string]string
|
||||||
load func(*testing.T, *discriminator.Server)
|
load func(*testing.T, *waf.Server)
|
||||||
figures string
|
figures string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"blocklists",
|
"blocklists",
|
||||||
map[string]string{blocklistURLs: dropURL, blocklistAction: actionDeny},
|
map[string]string{blocklistURLs: dropURL, blocklistAction: actionDeny},
|
||||||
func(t *testing.T, server *discriminator.Server) {
|
func(t *testing.T, server *waf.Server) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
loadLists(t, server, map[string][]string{dropURL: {client}})
|
loadLists(t, server, map[string][]string{dropURL: {client}})
|
||||||
},
|
},
|
||||||
@@ -143,7 +143,7 @@ func TestReputationRefusalNamesItsHeuristicAndWhatListedTheClient(t *testing.T)
|
|||||||
dnsblZones: dnsblZone, dnsblResolver: noResolver,
|
dnsblZones: dnsblZone, dnsblResolver: noResolver,
|
||||||
reputationAction: actionDeny,
|
reputationAction: actionDeny,
|
||||||
},
|
},
|
||||||
func(_ *testing.T, server *discriminator.Server) {
|
func(_ *testing.T, server *waf.Server) {
|
||||||
loadVerdicts(server, map[string][]string{client: {dnsblZone}})
|
loadVerdicts(server, map[string][]string{client: {dnsblZone}})
|
||||||
},
|
},
|
||||||
`{"zones": ["` + dnsblZone + `"]}`,
|
`{"zones": ["` + dnsblZone + `"]}`,
|
||||||
@@ -151,7 +151,7 @@ func TestReputationRefusalNamesItsHeuristicAndWhatListedTheClient(t *testing.T)
|
|||||||
{
|
{
|
||||||
"abuseipdb",
|
"abuseipdb",
|
||||||
map[string]string{abuseIPDBKey: accountKey, reputationAction: actionDeny},
|
map[string]string{abuseIPDBKey: accountKey, reputationAction: actionDeny},
|
||||||
func(_ *testing.T, server *discriminator.Server) {
|
func(_ *testing.T, server *waf.Server) {
|
||||||
loadScores(server, map[string]int64{client: 80})
|
loadScores(server, map[string]int64{client: 80})
|
||||||
},
|
},
|
||||||
`{"score": 80, "min_score": 75}`,
|
`{"score": 80, "min_score": 75}`,
|
||||||
@@ -494,7 +494,7 @@ func TestReputationSourcesTagTheRequestWhateverTheirAction(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
env map[string]string
|
env map[string]string
|
||||||
load func(*discriminator.Server)
|
load func(*waf.Server)
|
||||||
want []string
|
want []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -502,7 +502,7 @@ func TestReputationSourcesTagTheRequestWhateverTheirAction(t *testing.T) {
|
|||||||
map[string]string{
|
map[string]string{
|
||||||
dnsblZones: dnsblZone, dnsblResolver: noResolver, reputationAction: actionLog,
|
dnsblZones: dnsblZone, dnsblResolver: noResolver, reputationAction: actionLog,
|
||||||
},
|
},
|
||||||
func(server *discriminator.Server) {
|
func(server *waf.Server) {
|
||||||
loadVerdicts(server, map[string][]string{fromDE: {dnsblZone}})
|
loadVerdicts(server, map[string][]string{fromDE: {dnsblZone}})
|
||||||
},
|
},
|
||||||
[]string{"asn=64496", "country=DE", "dnsbl=" + dnsblZone},
|
[]string{"asn=64496", "country=DE", "dnsbl=" + dnsblZone},
|
||||||
@@ -510,7 +510,7 @@ func TestReputationSourcesTagTheRequestWhateverTheirAction(t *testing.T) {
|
|||||||
{
|
{
|
||||||
abuseipdb,
|
abuseipdb,
|
||||||
map[string]string{abuseIPDBKey: accountKey, reputationAction: actionLog},
|
map[string]string{abuseIPDBKey: accountKey, reputationAction: actionLog},
|
||||||
func(server *discriminator.Server) {
|
func(server *waf.Server) {
|
||||||
loadScores(server, map[string]int64{fromDE: 80})
|
loadScores(server, map[string]int64{fromDE: 80})
|
||||||
},
|
},
|
||||||
[]string{abuseipdb, "asn=64496", "country=DE"},
|
[]string{abuseipdb, "asn=64496", "country=DE"},
|
||||||
@@ -715,7 +715,7 @@ const (
|
|||||||
// AbuseIPDB.
|
// AbuseIPDB.
|
||||||
const abuseipdb = reputation.AbuseIPDBSource
|
const abuseipdb = reputation.AbuseIPDBSource
|
||||||
|
|
||||||
// abuseIPDBURL is where newDiscriminator has clients checked with AbuseIPDB: at
|
// abuseIPDBURL is where newWAF has clients checked with AbuseIPDB: at
|
||||||
// abuseIPDBStandIn, which TestMain registers with Go's default transport,
|
// abuseIPDBStandIn, which TestMain registers with Go's default transport,
|
||||||
// through which AbuseIPDB is asked.
|
// through which AbuseIPDB is asked.
|
||||||
const abuseIPDBURL = "abuseipdb://stand-in/api/v2/check"
|
const abuseIPDBURL = "abuseipdb://stand-in/api/v2/check"
|
||||||
@@ -1075,7 +1075,7 @@ func verdictsFetched() time.Time {
|
|||||||
// verdictsFetched, as reputation.json would at start: one that lists the
|
// verdictsFetched, as reputation.json would at start: one that lists the
|
||||||
// client from each zone listedBy gives for it, and one that does not from
|
// client from each zone listedBy gives for it, and one that does not from
|
||||||
// each other zone.
|
// each other zone.
|
||||||
func loadVerdicts(server *discriminator.Server, listedBy map[string][]string) {
|
func loadVerdicts(server *waf.Server, listedBy map[string][]string) {
|
||||||
verdicts := make([]reputation.Verdict, 0, len(listedBy)*len(server.DNSBL.Zones()))
|
verdicts := make([]reputation.Verdict, 0, len(listedBy)*len(server.DNSBL.Zones()))
|
||||||
|
|
||||||
for client, zones := range listedBy {
|
for client, zones := range listedBy {
|
||||||
@@ -1093,7 +1093,7 @@ func loadVerdicts(server *discriminator.Server, listedBy map[string][]string) {
|
|||||||
// loadScores puts into server's AbuseIPDB the score scores gives each
|
// loadScores puts into server's AbuseIPDB the score scores gives each
|
||||||
// client, an IPv4 address, fetched at verdictsFetched, as reputation.json
|
// client, an IPv4 address, fetched at verdictsFetched, as reputation.json
|
||||||
// would at start.
|
// would at start.
|
||||||
func loadScores(server *discriminator.Server, scores map[string]int64) {
|
func loadScores(server *waf.Server, scores map[string]int64) {
|
||||||
kept := make([]reputation.Score, 0, len(scores))
|
kept := make([]reputation.Score, 0, len(scores))
|
||||||
for client, score := range scores {
|
for client, score := range scores {
|
||||||
kept = append(kept, reputation.Score{
|
kept = append(kept, reputation.Score{
|
||||||
@@ -1107,7 +1107,7 @@ func loadScores(server *discriminator.Server, scores map[string]int64) {
|
|||||||
|
|
||||||
// wantAbuseIPDBChecks checks how many clients server has checked with
|
// wantAbuseIPDBChecks checks how many clients server has checked with
|
||||||
// AbuseIPDB.
|
// AbuseIPDB.
|
||||||
func wantAbuseIPDBChecks(t *testing.T, server *discriminator.Server, want int) {
|
func wantAbuseIPDBChecks(t *testing.T, server *waf.Server, want int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if got := server.AbuseIPDB.Checked(); got != want {
|
if got := server.AbuseIPDB.Checked(); got != want {
|
||||||
@@ -1117,7 +1117,7 @@ func wantAbuseIPDBChecks(t *testing.T, server *discriminator.Server, want int) {
|
|||||||
|
|
||||||
// loadLists puts copies of lists into server's lists, by URL, each with
|
// loadLists puts copies of lists into server's lists, by URL, each with
|
||||||
// its lines, fetched at listsFetched, as reputation.json would at start.
|
// its lines, fetched at listsFetched, as reputation.json would at start.
|
||||||
func loadLists(t *testing.T, server *discriminator.Server, copies map[string][]string) {
|
func loadLists(t *testing.T, server *waf.Server, copies map[string][]string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
lists := make([]reputation.List, 0, len(copies))
|
lists := make([]reputation.List, 0, len(copies))
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
// refusal is discriminator refusing a request: the status the visitor is to get, 0
|
// refusal is waf refusing a request: the status the visitor is to get, 0
|
||||||
// to close its connection without an answer, and the action the log line names.
|
// to close its connection without an answer, and the action the log line names.
|
||||||
type refusal struct {
|
type refusal struct {
|
||||||
status int
|
status int
|
||||||
@@ -25,7 +25,7 @@ type refusal struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// decision is what the checks note of a request: its log line, and what
|
// decision is what the checks note of a request: its log line, and what
|
||||||
// counting the request needs once it has ended. discriminator keeps a copy of it
|
// counting the request needs once it has ended. waf keeps a copy of it
|
||||||
// until the request's report comes. It holds no credentials, since the log line
|
// until the request's report comes. It holds no credentials, since the log line
|
||||||
// never does, and neither the request's objects nor their bodies.
|
// never does, and neither the request's objects nor their bodies.
|
||||||
type decision struct {
|
type decision struct {
|
||||||
@@ -34,7 +34,7 @@ type decision struct {
|
|||||||
// requestURI is the path with the query, as a ban's notes keep it.
|
// requestURI is the path with the query, as a ban's notes keep it.
|
||||||
requestURI string
|
requestURI string
|
||||||
// site is, for a question, the host the visitor asked for, which the
|
// site is, for a question, the host the visitor asked for, which the
|
||||||
// alerts name, since one discriminator serves many sites.
|
// alerts name, since one waf serves many sites.
|
||||||
site string
|
site string
|
||||||
// allowed is true for a question answered with DispositionAllow.
|
// allowed is true for a question answered with DispositionAllow.
|
||||||
allowed bool
|
allowed bool
|
||||||
@@ -63,7 +63,7 @@ type decision struct {
|
|||||||
reputation []bans.ReputationHit
|
reputation []bans.ReputationHit
|
||||||
}
|
}
|
||||||
|
|
||||||
// request is one request to discriminator, from the moment its headers have been
|
// request is one request to waf, from the moment its headers have been
|
||||||
// read to its log line.
|
// read to its log line.
|
||||||
type request struct {
|
type request struct {
|
||||||
decision
|
decision
|
||||||
@@ -373,7 +373,7 @@ func (rq *request) answerAtTheEnd() (lookup.Answer, bool) {
|
|||||||
return rq.h.geojs.Kept(rq.h.clientGroup(rq.client))
|
return rq.h.geojs.Kept(rq.h.clientGroup(rq.client))
|
||||||
}
|
}
|
||||||
|
|
||||||
// responseWriter is discriminator's answer to a request. It notes the status and
|
// responseWriter is waf's answer to a request. It notes the status and
|
||||||
// size for the log line, and the first error writing the answer.
|
// size for the log line, and the first error writing the answer.
|
||||||
type responseWriter struct {
|
type responseWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -105,7 +105,7 @@ func wantFullLine(t *testing.T, line logLine) {
|
|||||||
headers := map[string]string{"accept": "text/html", "x-custom": "one, two"}
|
headers := map[string]string{"accept": "text/html", "x-custom": "one, two"}
|
||||||
|
|
||||||
want := withTimings(line, requestlog.Line{
|
want := withTimings(line, requestlog.Line{
|
||||||
Type: requestType, DiscriminatorLine: requestlog.DiscriminatorLineReport,
|
Type: requestType, WAFLine: requestlog.WAFLineReport,
|
||||||
Time: line.Time, Instance: instance,
|
Time: line.Time, Instance: instance,
|
||||||
ClientIP: ipv6Client, Method: http.MethodPost, Scheme: secure,
|
ClientIP: ipv6Client, Method: http.MethodPost, Scheme: secure,
|
||||||
Host: appHost, Path: "/full", Protocol: protocol,
|
Host: appHost, Path: "/full", Protocol: protocol,
|
||||||
@@ -131,7 +131,7 @@ func wantBareLine(t *testing.T, line logLine) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
want := withTimings(line, requestlog.Line{
|
want := withTimings(line, requestlog.Line{
|
||||||
Type: requestType, DiscriminatorLine: requestlog.DiscriminatorLineReport,
|
Type: requestType, WAFLine: requestlog.WAFLineReport,
|
||||||
Time: line.Time, Instance: instance,
|
Time: line.Time, Instance: instance,
|
||||||
ClientIP: localhost, Method: http.MethodPost, Scheme: plain,
|
ClientIP: localhost, Method: http.MethodPost, Scheme: plain,
|
||||||
Host: appHost, Path: "/bare", Protocol: protocol,
|
Host: appHost, Path: "/bare", Protocol: protocol,
|
||||||
@@ -221,7 +221,7 @@ func TestTimingsAreInOrder(t *testing.T) {
|
|||||||
wantStatus(t, do(t, req), http.StatusForbidden)
|
wantStatus(t, do(t, req), http.StatusForbidden)
|
||||||
refused := out.endedLines(t, 2)[1]
|
refused := out.endedLines(t, 2)[1]
|
||||||
|
|
||||||
wantStatus(t, get(t, server.Addr, discriminator.HealthPath), http.StatusOK)
|
wantStatus(t, get(t, server.Addr, waf.HealthPath), http.StatusOK)
|
||||||
health := out.endedLines(t, 3)[2]
|
health := out.endedLines(t, 3)[2]
|
||||||
|
|
||||||
// A request passed to the app has every timing; one refused, none of
|
// A request passed to the app has every timing; one refused, none of
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic routewatch_tags. It reads SWW_ROUTEWATCH_URL.
|
// The heuristic routewatch_tags. It reads SWW_ROUTEWATCH_URL.
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// routeWatchURL is SWW_ROUTEWATCH_URL, and tagsMissing the log line's field
|
// routeWatchURL is SWW_ROUTEWATCH_URL, and tagsMissing the log line's field
|
||||||
@@ -58,7 +58,7 @@ func startRouteWatch(t *testing.T, routes map[string]standInRoute) string {
|
|||||||
// waitForRouteWatch waits until routewatch's answer about from, in the client
|
// waitForRouteWatch waits until routewatch's answer about from, in the client
|
||||||
// group of the given length, is kept, so that the next request gets its tags.
|
// group of the given length, is kept, so that the next request gets its tags.
|
||||||
func waitForRouteWatch(
|
func waitForRouteWatch(
|
||||||
t *testing.T, server *discriminator.Server, from string, bits int,
|
t *testing.T, server *waf.Server, from string, bits int,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic rule_files. It reads the rules of the rule files, which
|
// The heuristic rule_files. It reads the rules of the rule files, which
|
||||||
// SWW_RULES_DIR and SWW_RULES_ENABLED name.
|
// SWW_RULES_DIR and SWW_RULES_ENABLED name.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic tag_deny. It reads SWW_TAG_DENY.
|
// The heuristic tag_deny. It reads SWW_TAG_DENY.
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic tag_lists. It reads the address lists of SWW_TAG_LISTS through
|
// The heuristic tag_lists. It reads the address lists of SWW_TAG_LISTS through
|
||||||
// the reputation lists.
|
// the reputation lists.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
// The heuristic trap_paths. It reads SWW_TRAP_PATHS.
|
// The heuristic trap_paths. It reads SWW_TRAP_PATHS.
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator_test
|
package waf_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package discriminator
|
package waf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -18,8 +18,8 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The headers of rproxy's question and of discriminator's answer, as
|
// The headers of rproxy's question and of waf's answer, as
|
||||||
// "discriminator" in SPEC.md gives them.
|
// "waf" in SPEC.md gives them.
|
||||||
const (
|
const (
|
||||||
// ClientHeader marks a request as rproxy's question, and gives the
|
// ClientHeader marks a request as rproxy's question, and gives the
|
||||||
// visitor's address.
|
// visitor's address.
|
||||||
@@ -28,7 +28,7 @@ const (
|
|||||||
PeerHeader = "X-Sww-Peer"
|
PeerHeader = "X-Sww-Peer"
|
||||||
// ContentLengthHeader gives the body length the visitor announced.
|
// ContentLengthHeader gives the body length the visitor announced.
|
||||||
ContentLengthHeader = "X-Sww-Content-Length"
|
ContentLengthHeader = "X-Sww-Content-Length"
|
||||||
// DispositionHeader gives discriminator's answer.
|
// DispositionHeader gives waf's answer.
|
||||||
DispositionHeader = "X-Sww-Disposition"
|
DispositionHeader = "X-Sww-Disposition"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,12 +41,12 @@ const (
|
|||||||
DispositionDeny = "deny"
|
DispositionDeny = "deny"
|
||||||
// DispositionClose tells rproxy to close the visitor's connection
|
// DispositionClose tells rproxy to close the visitor's connection
|
||||||
// without an answer, which SWW_BAN_RESPONSE asks for when it is close.
|
// without an answer, which SWW_BAN_RESPONSE asks for when it is close.
|
||||||
// discriminator closing the question's connection instead would read as
|
// waf closing the question's connection instead would read as
|
||||||
// discriminator being down, and rproxy would then let the request through.
|
// waf being down, and rproxy would then let the request through.
|
||||||
DispositionClose = "close"
|
DispositionClose = "close"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReportPath is where rproxy sends the report of each request discriminator
|
// ReportPath is where rproxy sends the report of each request waf
|
||||||
// answered.
|
// answered.
|
||||||
const ReportPath = "/_sww/report"
|
const ReportPath = "/_sww/report"
|
||||||
|
|
||||||
@@ -70,11 +70,11 @@ const (
|
|||||||
// maxReportedMilliseconds is the longest duration a report may give,
|
// maxReportedMilliseconds is the longest duration a report may give,
|
||||||
// about 31 years, so that it fits a time.Duration.
|
// about 31 years, so that it fits a time.Duration.
|
||||||
maxReportedMilliseconds = 1e12
|
maxReportedMilliseconds = 1e12
|
||||||
// discriminatorTimeout bounds how long a request to discriminator takes to
|
// wafTimeout bounds how long a request to waf takes to
|
||||||
// arrive, and its answer to leave, and discriminatorIdleTimeout how long a
|
// arrive, and its answer to leave, and wafIdleTimeout how long a
|
||||||
// kept-open connection waits for its next request.
|
// kept-open connection waits for its next request.
|
||||||
discriminatorTimeout = 60 * time.Second
|
wafTimeout = 60 * time.Second
|
||||||
discriminatorIdleTimeout = 120 * time.Second
|
wafIdleTimeout = 120 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -97,7 +97,7 @@ type Report struct {
|
|||||||
DurationUpstreamTotalMS float64 `json:"duration_upstream_total_ms"`
|
DurationUpstreamTotalMS float64 `json:"duration_upstream_total_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns the server `sww discriminator` runs. It answers
|
// New returns the server `sww waf` runs. It answers
|
||||||
// rproxy's questions, takes rproxy's reports and answers its own endpoints. It
|
// rproxy's questions, takes rproxy's reports and answers its own endpoints. It
|
||||||
// passes nothing to an app. rproxy applies the size and time limits to the
|
// passes nothing to an app. rproxy applies the size and time limits to the
|
||||||
// visitor. Go's default limit on a request's line and headers, 1 MiB, stays: it
|
// visitor. Go's default limit on a request's line and headers, 1 MiB, stays: it
|
||||||
@@ -111,9 +111,9 @@ func New(params Params) *Server {
|
|||||||
Addr: params.Config.ListenAddr,
|
Addr: params.Config.ListenAddr,
|
||||||
Handler: h,
|
Handler: h,
|
||||||
ErrorLog: slog.NewLogLogger(params.ProcessLog.Handler(), slog.LevelWarn),
|
ErrorLog: slog.NewLogLogger(params.ProcessLog.Handler(), slog.LevelWarn),
|
||||||
ReadTimeout: discriminatorTimeout,
|
ReadTimeout: wafTimeout,
|
||||||
WriteTimeout: discriminatorTimeout,
|
WriteTimeout: wafTimeout,
|
||||||
IdleTimeout: discriminatorIdleTimeout,
|
IdleTimeout: wafIdleTimeout,
|
||||||
},
|
},
|
||||||
Ledger: h.ledger,
|
Ledger: h.ledger,
|
||||||
Limiter: h.limiter,
|
Limiter: h.limiter,
|
||||||
@@ -128,8 +128,8 @@ func New(params Params) *Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP handles one request to discriminator: a request carrying
|
// ServeHTTP handles one request to waf: a request carrying
|
||||||
// ClientHeader is rproxy's question, and any other is for discriminator
|
// ClientHeader is rproxy's question, and any other is for waf
|
||||||
// itself.
|
// itself.
|
||||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if len(r.Header.Values(ClientHeader)) > 0 {
|
if len(r.Header.Values(ClientHeader)) > 0 {
|
||||||
@@ -146,7 +146,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// its client: the heuristic crowdsec, header_limits, blocklists or rate_limits
|
// its client: the heuristic crowdsec, header_limits, blocklists or rate_limits
|
||||||
// will refuse it, as a function in that heuristic's file says. In observe mode,
|
// will refuse it, as a function in that heuristic's file says. In observe mode,
|
||||||
// which refuses nothing, none is. rproxy lets a request through when
|
// which refuses nothing, none is. rproxy lets a request through when
|
||||||
// discriminator answers too late.
|
// waf answers too late.
|
||||||
func (rq *request) refusedWhateverTheAnswer(now time.Time) bool {
|
func (rq *request) refusedWhateverTheAnswer(now time.Time) bool {
|
||||||
if rq.h.config.Observe {
|
if rq.h.config.Observe {
|
||||||
return false
|
return false
|
||||||
@@ -180,7 +180,7 @@ func (h *handler) answerQuestion(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer rq.finish()
|
defer rq.finish()
|
||||||
|
|
||||||
rq.site = r.Host
|
rq.site = r.Host
|
||||||
rq.line.DiscriminatorLine = requestlog.DiscriminatorLineDecision
|
rq.line.WAFLine = requestlog.WAFLineDecision
|
||||||
refused := rq.check(r.Context())
|
refused := rq.check(r.Context())
|
||||||
rq.checked = time.Now()
|
rq.checked = time.Now()
|
||||||
underOwnPrefix := strings.HasPrefix(r.URL.Path, ownEndpointPrefix)
|
underOwnPrefix := strings.HasPrefix(r.URL.Path, ownEndpointPrefix)
|
||||||
@@ -206,7 +206,7 @@ func (h *handler) answerQuestion(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// The app never sees this prefix. discriminator's own endpoints are at its
|
// The app never sees this prefix. waf's own endpoints are at its
|
||||||
// own address, so this answers as a path with no endpoint does.
|
// own address, so this answers as a path with no endpoint does.
|
||||||
rq.line.Action = requestlog.ActionOwnEndpoint
|
rq.line.Action = requestlog.ActionOwnEndpoint
|
||||||
rq.out.Header().Set(DispositionHeader, DispositionDeny)
|
rq.out.Header().Set(DispositionHeader, DispositionDeny)
|
||||||
@@ -214,7 +214,7 @@ func (h *handler) answerQuestion(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// readQuestion returns the visitor's address and the body a question carries,
|
// readQuestion returns the visitor's address and the body a question carries,
|
||||||
// and whether discriminator takes the question. It answers a question it does not
|
// and whether waf takes the question. It answers a question it does not
|
||||||
// take with 400 and no disposition, which rproxy treats as it treats no
|
// take with 400 and no disposition, which rproxy treats as it treats no
|
||||||
// answer. The id of a question it takes is held from here on, without a
|
// answer. The id of a question it takes is held from here on, without a
|
||||||
// decision yet, so that another question with that id is refused while this
|
// decision yet, so that another question with that id is refused while this
|
||||||
@@ -257,7 +257,7 @@ func (h *handler) readQuestion(
|
|||||||
|
|
||||||
held, _ := h.awaitingReport.ContainsOrAdd(id, nil)
|
held, _ := h.awaitingReport.ContainsOrAdd(id, nil)
|
||||||
if held {
|
if held {
|
||||||
h.refuseQuestion(w, requestIDHeader+" is the id of a request discriminator holds")
|
h.refuseQuestion(w, requestIDHeader+" is the id of a request waf holds")
|
||||||
|
|
||||||
return netip.Addr{}, nil, false
|
return netip.Addr{}, nil, false
|
||||||
}
|
}
|
||||||
@@ -265,14 +265,14 @@ func (h *handler) readQuestion(
|
|||||||
return client, body, true
|
return client, body, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// refuseQuestion answers a question discriminator does not take, for reason.
|
// refuseQuestion answers a question waf does not take, for reason.
|
||||||
func (h *handler) refuseQuestion(w http.ResponseWriter, reason string) {
|
func (h *handler) refuseQuestion(w http.ResponseWriter, reason string) {
|
||||||
h.processLog.Warn("a question is refused", "reason", reason)
|
h.processLog.Warn("a question is refused", "reason", reason)
|
||||||
http.Error(w, reason, http.StatusBadRequest)
|
http.Error(w, reason, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// asTheVisitorSentIt returns a copy of r, a question, as the checks and the
|
// asTheVisitorSentIt returns a copy of r, a question, as the checks and the
|
||||||
// log line are to see it: without the headers rproxy adds for discriminator,
|
// log line are to see it: without the headers rproxy adds for waf,
|
||||||
// with body, which has been read of r, and with the length the visitor
|
// with body, which has been read of r, and with the length the visitor
|
||||||
// announced, as -1 when it announced none. The question's own length covers
|
// announced, as -1 when it announced none. The question's own length covers
|
||||||
// only the part of the body it carries.
|
// only the part of the body it carries.
|
||||||
@@ -340,7 +340,7 @@ func (rq *request) deny(r refusal) {
|
|||||||
http.Error(rq.out, http.StatusText(r.status), r.status)
|
http.Error(rq.out, http.StatusText(r.status), r.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// answerOwnEndpoint answers a request to discriminator's own address. No check
|
// answerOwnEndpoint answers a request to waf's own address. No check
|
||||||
// runs on it: the address is on the private network, where the peers are
|
// runs on it: the address is on the private network, where the peers are
|
||||||
// rproxy and the admin's tools, not visitors.
|
// rproxy and the admin's tools, not visitors.
|
||||||
func (h *handler) answerOwnEndpoint(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) answerOwnEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -455,7 +455,7 @@ func (h *handler) finishWith(ctx context.Context, held *decision, report Report)
|
|||||||
requestBytes: report.RequestBytes,
|
requestBytes: report.RequestBytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
rq.line.DiscriminatorLine = requestlog.DiscriminatorLineReport
|
rq.line.WAFLine = requestlog.WAFLineReport
|
||||||
rq.line.Status = report.Status
|
rq.line.Status = report.Status
|
||||||
rq.line.RequestBytes = report.RequestBytes
|
rq.line.RequestBytes = report.RequestBytes
|
||||||
rq.line.ResponseBytes = report.ResponseBytes
|
rq.line.ResponseBytes = report.ResponseBytes
|
||||||
File diff suppressed because it is too large.
Load diff
+60
-60
@@ -1,25 +1,25 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/example-app: build the image, and on it the example app in
|
# script/example-app: build the image, and on it the example app in
|
||||||
# deploy/example-app, then run the app's container, with rproxy in it, and
|
# deploy/example-app, then run the app's container, with rproxy in it, and
|
||||||
# discriminator in a container of its own with a volume for the state files. It
|
# waf in a container of its own with a volume for the state files. It
|
||||||
# checks:
|
# checks:
|
||||||
# - that the health checks pass;
|
# - that the health checks pass;
|
||||||
# - that a request is served through rproxy, and a second one in a minute
|
# - that a request is served through rproxy, and a second one in a minute
|
||||||
# bans the client;
|
# bans the client;
|
||||||
# - that a probe for /.env bans another client, which its next request bans
|
# - that a probe for /.env bans another client, which its next request bans
|
||||||
# for good;
|
# for good;
|
||||||
# - that only discriminator's container changes the state directory's owner;
|
# - that only waf's container changes the state directory's owner;
|
||||||
# - that the app still answers through rproxy with discriminator stopped, and
|
# - that the app still answers through rproxy with waf stopped, and
|
||||||
# that once discriminator has started again, bans apply again;
|
# that once waf has started again, bans apply again;
|
||||||
# - that `sv stop` stops rproxy in order, and `docker stop` stops each
|
# - that `sv stop` stops rproxy in order, and `docker stop` stops each
|
||||||
# container without having to kill it;
|
# container without having to kill it;
|
||||||
# - that a new discriminator container on the same volume still refuses the banned
|
# - that a new waf container on the same volume still refuses the banned
|
||||||
# client;
|
# client;
|
||||||
# - that a discriminator container with its state files on a new volume mounted at
|
# - that a waf container with its state files on a new volume mounted at
|
||||||
# the directory SWW_STATE_DIR_FILE names becomes healthy;
|
# the directory SWW_STATE_DIR_FILE names becomes healthy;
|
||||||
# - that one whose SWW_STATE_DIR does not exist logs discriminator's message
|
# - that one whose SWW_STATE_DIR does not exist logs waf's message
|
||||||
# about it.
|
# about it.
|
||||||
# discriminator runs with SWW_LOOKUP_SOURCE=off, so that no address is sent to
|
# waf runs with SWW_LOOKUP_SOURCE=off, so that no address is sent to
|
||||||
# GeoJS. The containers, the network, the volumes and both images are removed
|
# GeoJS. The containers, the network, the volumes and both images are removed
|
||||||
# however the script ends. Building the app needs network access, for nixpkgs'
|
# however the script ends. Building the app needs network access, for nixpkgs'
|
||||||
# binary cache. script/check does not run this.
|
# binary cache. script/check does not run this.
|
||||||
@@ -35,7 +35,7 @@ IMAGE="$NAME-base"
|
|||||||
APP_IMAGE="$NAME-app"
|
APP_IMAGE="$NAME-app"
|
||||||
NETWORK="$NAME"
|
NETWORK="$NAME"
|
||||||
APP="$NAME-app-container"
|
APP="$NAME-app-container"
|
||||||
DISCRIMINATOR="$NAME-discriminator"
|
WAF="$NAME-waf"
|
||||||
VOLUME="$NAME-state"
|
VOLUME="$NAME-state"
|
||||||
# The volumes of the container whose state directory SWW_STATE_DIR_FILE
|
# The volumes of the container whose state directory SWW_STATE_DIR_FILE
|
||||||
# names: its state files, and the file that names their directory.
|
# names: its state files, and the file that names their directory.
|
||||||
@@ -43,7 +43,7 @@ MOVED_VOLUME="$NAME-moved-state"
|
|||||||
SETTINGS_VOLUME="$NAME-settings"
|
SETTINGS_VOLUME="$NAME-settings"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
docker rm --force "$APP" "$DISCRIMINATOR" >/dev/null 2>&1 || true
|
docker rm --force "$APP" "$WAF" >/dev/null 2>&1 || true
|
||||||
docker network rm "$NETWORK" >/dev/null 2>&1 || true
|
docker network rm "$NETWORK" >/dev/null 2>&1 || true
|
||||||
docker volume rm --force "$VOLUME" "$MOVED_VOLUME" "$SETTINGS_VOLUME" \
|
docker volume rm --force "$VOLUME" "$MOVED_VOLUME" "$SETTINGS_VOLUME" \
|
||||||
>/dev/null 2>&1 || true
|
>/dev/null 2>&1 || true
|
||||||
@@ -53,8 +53,8 @@ cleanup() {
|
|||||||
fail() {
|
fail() {
|
||||||
echo "example-app: $*; the app container's output:" >&2
|
echo "example-app: $*; the app container's output:" >&2
|
||||||
docker logs "$APP" >&2 || true
|
docker logs "$APP" >&2 || true
|
||||||
echo "example-app: discriminator's output:" >&2
|
echo "example-app: waf's output:" >&2
|
||||||
docker logs "$DISCRIMINATOR" >&2 || true
|
docker logs "$WAF" >&2 || true
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,49 +87,49 @@ logged() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# start_discriminator: run discriminator from the image, with the state files on the
|
# start_waf: run waf from the image, with the state files on the
|
||||||
# volume, a rate limit of one request a minute and no client looked up, and wait
|
# volume, a rate limit of one request a minute and no client looked up, and wait
|
||||||
# until it is healthy.
|
# until it is healthy.
|
||||||
start_discriminator() {
|
start_waf() {
|
||||||
docker run --detach --name "$DISCRIMINATOR" --network "$NETWORK" \
|
docker run --detach --name "$WAF" --network "$NETWORK" \
|
||||||
--volume "$VOLUME:/var/lib/sww" \
|
--volume "$VOLUME:/var/lib/sww" \
|
||||||
--env SWW_COMMAND=discriminator \
|
--env SWW_COMMAND=waf \
|
||||||
--env SWW_RATE_LIMIT_PER_MINUTE=1 \
|
--env SWW_RATE_LIMIT_PER_MINUTE=1 \
|
||||||
--env SWW_LOOKUP_SOURCE=off \
|
--env SWW_LOOKUP_SOURCE=off \
|
||||||
"$IMAGE" >/dev/null
|
"$IMAGE" >/dev/null
|
||||||
wait_for "discriminator's health check did not pass" healthy "$DISCRIMINATOR"
|
wait_for "waf's health check did not pass" healthy "$WAF"
|
||||||
}
|
}
|
||||||
|
|
||||||
# start_app: run the app's container, with rproxy in it, and wait until it is
|
# start_app: run the app's container, with rproxy in it, and wait until it is
|
||||||
# healthy.
|
# healthy.
|
||||||
start_app() {
|
start_app() {
|
||||||
docker run --detach --name "$APP" --network "$NETWORK" --publish 127.0.0.1::8080 \
|
docker run --detach --name "$APP" --network "$NETWORK" --publish 127.0.0.1::8080 \
|
||||||
--env SWRP_WAF_BACKEND="http://$DISCRIMINATOR:8080" \
|
--env SWRP_WAF_BACKEND="http://$WAF:8080" \
|
||||||
"$APP_IMAGE" >/dev/null
|
"$APP_IMAGE" >/dev/null
|
||||||
wait_for "the app's health check did not pass" healthy "$APP"
|
wait_for "the app's health check did not pass" healthy "$APP"
|
||||||
address="$(docker port "$APP" 8080/tcp)"
|
address="$(docker port "$APP" 8080/tcp)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# start_moved_discriminator: run discriminator with the state files on a new volume at
|
# start_moved_waf: run waf with the state files on a new volume at
|
||||||
# /srv/state, which SWW_STATE_DIR_FILE names in a file only the sww
|
# /srv/state, which SWW_STATE_DIR_FILE names in a file only the sww
|
||||||
# user can read, as under "Settings given as files" in docs/settings.md, and
|
# user can read, as under "Settings given as files" in docs/settings.md, and
|
||||||
# wait until it is healthy. discriminator can write the new volume only once the run
|
# wait until it is healthy. waf can write the new volume only once the run
|
||||||
# script has given it to that user.
|
# script has given it to that user.
|
||||||
start_moved_discriminator() {
|
start_moved_waf() {
|
||||||
docker volume create "$MOVED_VOLUME" >/dev/null
|
docker volume create "$MOVED_VOLUME" >/dev/null
|
||||||
docker volume create "$SETTINGS_VOLUME" >/dev/null
|
docker volume create "$SETTINGS_VOLUME" >/dev/null
|
||||||
docker run --rm --volume "$SETTINGS_VOLUME:/settings" \
|
docker run --rm --volume "$SETTINGS_VOLUME:/settings" \
|
||||||
--entrypoint bash "$IMAGE" -c 'echo /srv/state > /settings/state-dir &&
|
--entrypoint bash "$IMAGE" -c 'echo /srv/state > /settings/state-dir &&
|
||||||
chown sww:sww /settings/state-dir &&
|
chown sww:sww /settings/state-dir &&
|
||||||
chmod 0400 /settings/state-dir'
|
chmod 0400 /settings/state-dir'
|
||||||
docker run --detach --name "$DISCRIMINATOR" --network "$NETWORK" \
|
docker run --detach --name "$WAF" --network "$NETWORK" \
|
||||||
--volume "$MOVED_VOLUME:/srv/state" \
|
--volume "$MOVED_VOLUME:/srv/state" \
|
||||||
--volume "$SETTINGS_VOLUME:/etc/sww/settings:ro" \
|
--volume "$SETTINGS_VOLUME:/etc/sww/settings:ro" \
|
||||||
--env SWW_COMMAND=discriminator \
|
--env SWW_COMMAND=waf \
|
||||||
--env SWW_STATE_DIR_FILE=/etc/sww/settings/state-dir \
|
--env SWW_STATE_DIR_FILE=/etc/sww/settings/state-dir \
|
||||||
--env SWW_LOOKUP_SOURCE=off \
|
--env SWW_LOOKUP_SOURCE=off \
|
||||||
"$IMAGE" >/dev/null
|
"$IMAGE" >/dev/null
|
||||||
wait_for "discriminator's health check did not pass" healthy "$DISCRIMINATOR"
|
wait_for "waf's health check did not pass" healthy "$WAF"
|
||||||
}
|
}
|
||||||
|
|
||||||
# status_from <client> <path>: print the status a request for path from client,
|
# status_from <client> <path>: print the status a request for path from client,
|
||||||
@@ -169,87 +169,87 @@ main() {
|
|||||||
|
|
||||||
docker network create "$NETWORK" >/dev/null
|
docker network create "$NETWORK" >/dev/null
|
||||||
docker volume create "$VOLUME" >/dev/null
|
docker volume create "$VOLUME" >/dev/null
|
||||||
start_discriminator
|
start_waf
|
||||||
start_app
|
start_app
|
||||||
echo "example-app: the health checks pass"
|
echo "example-app: the health checks pass"
|
||||||
|
|
||||||
[ "$(owner_of_state_dir "$DISCRIMINATOR")" = sww ] ||
|
[ "$(owner_of_state_dir "$WAF")" = sww ] ||
|
||||||
fail "discriminator's container did not give the state directory to sww"
|
fail "waf's container did not give the state directory to sww"
|
||||||
[ "$(owner_of_state_dir "$APP")" = root ] ||
|
[ "$(owner_of_state_dir "$APP")" = root ] ||
|
||||||
fail "the app's container changed the state directory's owner"
|
fail "the app's container changed the state directory's owner"
|
||||||
echo "example-app: only discriminator's container changes the state directory's owner"
|
echo "example-app: only waf's container changes the state directory's owner"
|
||||||
|
|
||||||
page="$(curl --fail --silent --show-error --max-time 10 "http://$address/")" ||
|
page="$(curl --fail --silent --show-error --max-time 10 "http://$address/")" ||
|
||||||
fail "no answer on port 8080"
|
fail "no answer on port 8080"
|
||||||
[ "$page" = "hello from the example app" ] || fail "port 8080 answered $page"
|
[ "$page" = "hello from the example app" ] || fail "port 8080 answered $page"
|
||||||
wait_for "discriminator logged no request it allowed" \
|
wait_for "waf logged no request it allowed" \
|
||||||
logged "$DISCRIMINATOR" '"action":"forward"' '"discriminator_line":"report"'
|
logged "$WAF" '"action":"forward"' '"waf_line":"report"'
|
||||||
echo "example-app: rproxy passes a request to the app and its answer back"
|
echo "example-app: rproxy passes a request to the app and its answer back"
|
||||||
|
|
||||||
refused || fail "a second request in a minute was not refused"
|
refused || fail "a second request in a minute was not refused"
|
||||||
wait_for "discriminator logged no ban" \
|
wait_for "waf logged no ban" \
|
||||||
logged "$DISCRIMINATOR" '"action":"client_is_rate_limited"'
|
logged "$WAF" '"action":"client_is_rate_limited"'
|
||||||
echo "example-app: a second request in a minute bans the client"
|
echo "example-app: a second request in a minute bans the client"
|
||||||
|
|
||||||
refused_from 203.0.113.9 /.env || fail "a probe for /.env was not refused"
|
refused_from 203.0.113.9 /.env || fail "a probe for /.env was not refused"
|
||||||
wait_for "discriminator logged no ban for the probe" \
|
wait_for "waf logged no ban for the probe" \
|
||||||
logged "$DISCRIMINATOR" '"action":"temporarily_banned"' '"rule_ids":["env-file"]'
|
logged "$WAF" '"action":"temporarily_banned"' '"rule_ids":["env-file"]'
|
||||||
refused_from 203.0.113.9 / || fail "the client of the probe was let through"
|
refused_from 203.0.113.9 / || fail "the client of the probe was let through"
|
||||||
wait_for "the client's next request did not make its ban permanent" \
|
wait_for "the client's next request did not make its ban permanent" \
|
||||||
logged "$DISCRIMINATOR" '"action":"permanently_banned"' '"ban_expires":"permanent"'
|
logged "$WAF" '"action":"permanently_banned"' '"ban_expires":"permanent"'
|
||||||
echo "example-app: a probe for /.env bans the client, its next request for good"
|
echo "example-app: a probe for /.env bans the client, its next request for good"
|
||||||
|
|
||||||
docker exec "$DISCRIMINATOR" sv stop sww >/dev/null ||
|
docker exec "$WAF" sv stop sww >/dev/null ||
|
||||||
fail "sv stop sww failed in discriminator's container"
|
fail "sv stop sww failed in waf's container"
|
||||||
wait_for "discriminator did not stop in order" logged "$DISCRIMINATOR" '"msg":"stopped"'
|
wait_for "waf did not stop in order" logged "$WAF" '"msg":"stopped"'
|
||||||
[ "$(status_from 203.0.113.9 /)" = 200 ] ||
|
[ "$(status_from 203.0.113.9 /)" = 200 ] ||
|
||||||
fail "with discriminator stopped, the app did not answer through rproxy"
|
fail "with waf stopped, the app did not answer through rproxy"
|
||||||
wait_for "rproxy logged no request SWRP_DEFAULT_DISPOSITION decided" \
|
wait_for "rproxy logged no request SWRP_DEFAULT_DISPOSITION decided" \
|
||||||
logged "$APP" '"default_disposition":true'
|
logged "$APP" '"default_disposition":true'
|
||||||
docker exec "$APP" /usr/local/bin/sww healthcheck ||
|
docker exec "$APP" /usr/local/bin/sww healthcheck ||
|
||||||
fail "with discriminator stopped, the app's health check failed"
|
fail "with waf stopped, the app's health check failed"
|
||||||
echo "example-app: with discriminator stopped, the app answers through rproxy"
|
echo "example-app: with waf stopped, the app answers through rproxy"
|
||||||
|
|
||||||
docker exec "$DISCRIMINATOR" sv start sww >/dev/null ||
|
docker exec "$WAF" sv start sww >/dev/null ||
|
||||||
fail "sv start sww failed in discriminator's container"
|
fail "sv start sww failed in waf's container"
|
||||||
wait_for "once discriminator had started again, the ban did not apply" \
|
wait_for "once waf had started again, the ban did not apply" \
|
||||||
refused_from 203.0.113.9 /
|
refused_from 203.0.113.9 /
|
||||||
echo "example-app: once discriminator has started again, bans apply again"
|
echo "example-app: once waf has started again, bans apply again"
|
||||||
|
|
||||||
docker exec "$APP" sv stop sww >/dev/null ||
|
docker exec "$APP" sv stop sww >/dev/null ||
|
||||||
fail "sv stop sww failed in the app's container"
|
fail "sv stop sww failed in the app's container"
|
||||||
wait_for "rproxy did not stop in order" logged "$APP" '"msg":"stopped"'
|
wait_for "rproxy did not stop in order" logged "$APP" '"msg":"stopped"'
|
||||||
echo "example-app: sv stop stops rproxy in order"
|
echo "example-app: sv stop stops rproxy in order"
|
||||||
|
|
||||||
for container in "$APP" "$DISCRIMINATOR"; do
|
for container in "$APP" "$WAF"; do
|
||||||
docker stop "$container" >/dev/null
|
docker stop "$container" >/dev/null
|
||||||
status="$(docker inspect --format '{{.State.ExitCode}}' "$container")"
|
status="$(docker inspect --format '{{.State.ExitCode}}' "$container")"
|
||||||
[ "$status" = 0 ] || fail "docker stop left exit status $status"
|
[ "$status" = 0 ] || fail "docker stop left exit status $status"
|
||||||
done
|
done
|
||||||
echo "example-app: docker stop stops each container in order"
|
echo "example-app: docker stop stops each container in order"
|
||||||
|
|
||||||
docker rm "$APP" "$DISCRIMINATOR" >/dev/null
|
docker rm "$APP" "$WAF" >/dev/null
|
||||||
start_discriminator
|
start_waf
|
||||||
start_app
|
start_app
|
||||||
refused || fail "the new discriminator container let the banned client through"
|
refused || fail "the new waf container let the banned client through"
|
||||||
wait_for "discriminator logged no request refused under the ban" \
|
wait_for "waf logged no request refused under the ban" \
|
||||||
logged "$DISCRIMINATOR" '"action":"temporarily_banned"'
|
logged "$WAF" '"action":"temporarily_banned"'
|
||||||
echo "example-app: a new discriminator container on the same volume keeps the ban"
|
echo "example-app: a new waf container on the same volume keeps the ban"
|
||||||
|
|
||||||
docker rm --force "$DISCRIMINATOR" >/dev/null
|
docker rm --force "$WAF" >/dev/null
|
||||||
start_moved_discriminator
|
start_moved_waf
|
||||||
echo "example-app: the run script gives the state files' directory named" \
|
echo "example-app: the run script gives the state files' directory named" \
|
||||||
"by SWW_STATE_DIR_FILE to the sww user"
|
"by SWW_STATE_DIR_FILE to the sww user"
|
||||||
|
|
||||||
docker rm --force "$DISCRIMINATOR" >/dev/null
|
docker rm --force "$WAF" >/dev/null
|
||||||
docker run --detach --name "$DISCRIMINATOR" --network "$NETWORK" \
|
docker run --detach --name "$WAF" --network "$NETWORK" \
|
||||||
--env SWW_COMMAND=discriminator \
|
--env SWW_COMMAND=waf \
|
||||||
--env SWW_STATE_DIR=/srv/missing \
|
--env SWW_STATE_DIR=/srv/missing \
|
||||||
--env SWW_LOOKUP_SOURCE=off \
|
--env SWW_LOOKUP_SOURCE=off \
|
||||||
"$IMAGE" >/dev/null
|
"$IMAGE" >/dev/null
|
||||||
wait_for "discriminator did not report the missing state directory" \
|
wait_for "waf did not report the missing state directory" \
|
||||||
logged "$DISCRIMINATOR" "SWW_STATE_DIR cannot be written"
|
logged "$WAF" "SWW_STATE_DIR cannot be written"
|
||||||
echo "example-app: discriminator reports a state directory that does not exist"
|
echo "example-app: waf reports a state directory that does not exist"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
+7
-7
@@ -1,9 +1,9 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/run: build bin/sww with script/build and run discriminator and
|
# script/run: build bin/sww with script/build and run waf and
|
||||||
# rproxy on the host, with the settings in the environment, until rproxy
|
# rproxy on the host, with the settings in the environment, until rproxy
|
||||||
# exits or the script is told to stop. rproxy listens on port 8080 unless
|
# exits or the script is told to stop. rproxy listens on port 8080 unless
|
||||||
# SWRP_LISTEN_ADDR says otherwise, in front of the app at SWRP_UPSTREAM_URL.
|
# SWRP_LISTEN_ADDR says otherwise, in front of the app at SWRP_UPSTREAM_URL.
|
||||||
# discriminator listens on port 8090 unless SWW_LISTEN_ADDR says otherwise, and
|
# waf listens on port 8090 unless SWW_LISTEN_ADDR says otherwise, and
|
||||||
# rproxy asks it there unless SWRP_WAF_BACKEND says otherwise. Unless
|
# rproxy asks it there unless SWRP_WAF_BACKEND says otherwise. Unless
|
||||||
# SWW_STATE_DIR is set, the state files go in bin/state, beside the binary,
|
# SWW_STATE_DIR is set, the state files go in bin/state, beside the binary,
|
||||||
# and unless SWW_RULES_DIR is set, the rule files are those of share/rules.d,
|
# and unless SWW_RULES_DIR is set, the rule files are those of share/rules.d,
|
||||||
@@ -13,14 +13,14 @@ set -eu
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
DISCRIMINATOR=""
|
WAF=""
|
||||||
RPROXY=""
|
RPROXY=""
|
||||||
|
|
||||||
# stop_both: stop what runs, and wait for it, so that discriminator writes its state
|
# stop_both: stop what runs, and wait for it, so that waf writes its state
|
||||||
# files.
|
# files.
|
||||||
stop_both() {
|
stop_both() {
|
||||||
trap '' HUP INT TERM
|
trap '' HUP INT TERM
|
||||||
kill $DISCRIMINATOR $RPROXY 2>/dev/null || true
|
kill $WAF $RPROXY 2>/dev/null || true
|
||||||
wait || true
|
wait || true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ main() {
|
|||||||
trap stop_both EXIT
|
trap stop_both EXIT
|
||||||
trap 'exit 1' HUP INT TERM
|
trap 'exit 1' HUP INT TERM
|
||||||
|
|
||||||
"$ROOT/bin/sww" discriminator &
|
"$ROOT/bin/sww" waf &
|
||||||
DISCRIMINATOR=$!
|
WAF=$!
|
||||||
"$ROOT/bin/sww" rproxy &
|
"$ROOT/bin/sww" rproxy &
|
||||||
RPROXY=$!
|
RPROXY=$!
|
||||||
wait "$RPROXY"
|
wait "$RPROXY"
|
||||||
|
|||||||
+5
-5
@@ -3,7 +3,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
# runit's run script for sww, run again whenever sww
|
# runit's run script for sww, run again whenever sww
|
||||||
# exits; the wait spaces out the restarts. SWW_COMMAND names the
|
# exits; the wait spaces out the restarts. SWW_COMMAND names the
|
||||||
# command to run: rproxy when it is unset or empty, or discriminator. With any
|
# command to run: rproxy when it is unset or empty, or waf. With any
|
||||||
# other value the script starts nothing and stops with a message naming it.
|
# other value the script starts nothing and stops with a message naming it.
|
||||||
# exec, so that the signal `sv stop` sends reaches sww itself.
|
# exec, so that the signal `sv stop` sends reaches sww itself.
|
||||||
main() {
|
main() {
|
||||||
@@ -11,9 +11,9 @@ main() {
|
|||||||
local command="${SWW_COMMAND:-rproxy}"
|
local command="${SWW_COMMAND:-rproxy}"
|
||||||
case "$command" in
|
case "$command" in
|
||||||
rproxy) ;;
|
rproxy) ;;
|
||||||
discriminator) give_state_dir_to_sww ;;
|
waf) give_state_dir_to_sww ;;
|
||||||
*)
|
*)
|
||||||
printf 'SWW_COMMAND: "%s" is not rproxy or discriminator\n' \
|
printf 'SWW_COMMAND: "%s" is not rproxy or waf\n' \
|
||||||
"$command" >&2
|
"$command" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
@@ -25,9 +25,9 @@ main() {
|
|||||||
# it to the sww user, so that a volume mounted there needs no change
|
# it to the sww user, so that a volume mounted there needs no change
|
||||||
# of owner; chown -R changes a symbolic link itself, never what it points
|
# of owner; chown -R changes a symbolic link itself, never what it points
|
||||||
# to. When SWW_STATE_DIR, in either form, cannot be read or is not the
|
# to. When SWW_STATE_DIR, in either form, cannot be read or is not the
|
||||||
# absolute path of a directory, no owner is changed, and discriminator starts and
|
# absolute path of a directory, no owner is changed, and waf starts and
|
||||||
# stops with a message naming the setting. A change of owner that fails, as
|
# stops with a message naming the setting. A change of owner that fails, as
|
||||||
# on a volume mounted read-only, does not end the script either: discriminator
|
# on a volume mounted read-only, does not end the script either: waf
|
||||||
# reports a directory it cannot write.
|
# reports a directory it cannot write.
|
||||||
give_state_dir_to_sww() {
|
give_state_dir_to_sww() {
|
||||||
local dir
|
local dir
|
||||||
|
|||||||
Reference in new issue
Block a user