1 Commits
Author SHA1 Message Date
sneak 4a030557f3 Rename the program to sww and its deciding daemon to discriminator (closes #218)
check / check (push) Failing after 2m47s
The program is now sww, built from cmd/sww, with two modes: sww rproxy and
sww discriminator. servewaf becomes discriminator everywhere it names that
daemon: the package (internal/discriminator), its types, functions, files, the
discriminator_line log field, and its docs and tests.

smallwebwaf is left only where sneak's directive allows it: the documents'
product name, the git.eeqj.de/sneak/smallwebwaf URLs, and the Go module path
sneak.berlin/go/smallwebwaf. Everywhere else a name containing it takes the sww
form: the package (internal/sww), metric names (sww_*), the X-Sww-* headers,
the /_sww/ admin endpoints, the OS user, and the state and rules directories.
The SWWAF_ environment variables take the SWW_ form.

No logic changes. make check passes.

Model: deepseek-v4-pro
2026-11-07 05:27:58 +00:00
7 changed files with 33 additions and 33 deletions

No files matched your search

+8 -7
View File
@@ -7,8 +7,8 @@ requests of scrapers, scanners and other abusers before they reach an app.
## What it does ## What it does
A web application firewall reads each request on its way to a web app and A web application firewall reads each request on its way to a web app and
refuses the ones that look like abuse. `smallwebwaf` is one binary, shipped in refuses the ones that look like abuse. `smallwebwaf` is one program, `sww`,
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 `discriminator` about each request, passes the allowed ones to the
@@ -194,7 +194,8 @@ account, a key, a token or an alert destination stays off until you give one.
contents are the value, less one newline at the end. Setting both forms stops contents are the value, less one newline at the end. Setting both forms stops
the start. `SWW_LOG_REMOTE_TLS_CA_FILE` and the start. `SWW_LOG_REMOTE_TLS_CA_FILE` and
`SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already name a file, so they have no `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already name a file, so they have no
`_FILE` form. No `SWRP_` setting has one, since `rproxy` never touches disk. `_FILE` form. `SWW_COMMAND` cannot be given as a file. No `SWRP_` setting has
one, since `rproxy` never touches disk.
- A duration is Go's syntax, with `d` for days: `90s`, `15m`, `24h`, `7d`. - A duration is Go's syntax, with `d` for days: `90s`, `15m`, `24h`, `7d`.
- A size is a number of bytes with an optional `K`, `M` or `G`, in powers of - A size is a number of bytes with an optional `K`, `M` or `G`, in powers of
1024: `512K`, `100M`, `5G`. 1024: `512K`, `100M`, `5G`.
@@ -581,10 +582,10 @@ is set aside as `<name>.bad`, and never stops the running process (see
come from Ubuntu's archive, and nixpkgs is fixed at one release. An app's come from Ubuntu's archive, and nixpkgs is fixed at one release. An app's
`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 `smallwebwaf` as the `sww` user (uid and - runit, a service supervisor, runs `sww` as the `sww` user (uid and gid 65532)
gid 65532) and starts it again when it exits. The `run` script of and starts it again when it exits. The `run` script of `discriminator` gives
`discriminator` gives the state directory to the `sww` user at each start, so the state directory to the `sww` user at each start, so a volume needs no
a volume needs no change of owner. change of owner.
- `rproxy` and `discriminator` use no token between them. `discriminator`'s own - `rproxy` and `discriminator` use no token between them. `discriminator`'s own
endpoints, under `/_sww/`, are for your admin tools and your metrics endpoints, under `/_sww/`, are for your admin tools and your metrics
collector, at its own address on the private network; nothing routes the collector, at its own address on the private network; nothing routes the
+7 -5
View File
@@ -1,7 +1,7 @@
# smallwebwaf specification # smallwebwaf specification
`smallwebwaf` is a web application firewall for self-hosted apps. It is one `smallwebwaf` is a web application firewall for self-hosted apps. It is one
binary, and two of its commands do the work: binary, `sww`, and two of its commands do the work:
- `sww rproxy` is a reverse proxy between traefik and one app. - `sww rproxy` is a reverse proxy between traefik and one app.
- `sww discriminator` decides what happens to each request, for many sites. - `sww discriminator` decides what happens to each request, for many sites.
@@ -326,7 +326,8 @@ Conventions:
the value itself would be. Setting both forms stops the start, and so does the value itself would be. Setting both forms stops the start, and so does
naming a file that cannot be read. `SWW_LOG_REMOTE_TLS_CA_FILE` and naming a file that cannot be read. `SWW_LOG_REMOTE_TLS_CA_FILE` and
`SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already hold a file name, so they have no `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` already hold a file name, so they have no
`_FILE` form. No `SWRP_` setting has one, since `rproxy` never touches disk. `_FILE` form. `SWW_COMMAND` cannot be given as a file. No `SWRP_` setting has
one, since `rproxy` never touches disk.
- Settings, files included, are read once at start. Changing one means - Settings, files included, are read once at start. Changing one means
restarting the container. The files `discriminator` watches while it runs are restarting the container. The files `discriminator` watches while it runs are
its state files, its rule files and the lookup database. its state files, its rule files and the lookup database.
@@ -584,8 +585,8 @@ files, the limits and the bans still apply to the client that sends it.
Coraza directives. An exclusion switches one rule off for some fields or paths Coraza directives. An exclusion switches one rule off for some fields or paths
only, where `SWW_WAF_DISABLED_RULES` switches it off everywhere. The file only, where `SWW_WAF_DISABLED_RULES` switches it off everywhere. The file
holds an app's own exclusions; the eighth change below holds gitea's. holds an app's own exclusions; the eighth change below holds gitea's.
- `discriminator` loads the file after `sww`'s own setup rules and before - `discriminator` loads the file after `smallwebwaf`'s own setup rules and
the Core Rule Set's rules. The Core Rule Set loads its own before the Core Rule Set's rules. The Core Rule Set loads its own
`REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf` at the same point. `REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf` at the same point.
- So the file holds only `SecAction` and `SecRule` lines that exclude with - So the file holds only `SecAction` and `SecRule` lines that exclude with
`ctl:ruleRemoveTargetById` or `ctl:ruleRemoveById`. These run with each `ctl:ruleRemoveTargetById` or `ctl:ruleRemoveById`. These run with each
@@ -595,7 +596,8 @@ files, the limits and the bans still apply to the client that sends it.
- The start stops on a file that cannot be read or does not parse, whatever - The start stops on a file that cannot be read or does not parse, whatever
`SWW_WAF_MODE` is. `SWW_WAF_MODE` is.
- It stops too on a file that names a number from 900000 to 900999, the ids - It stops too on a file that names a number from 900000 to 900999, the ids
of `sww`'s own rules, alone or in a range. Comment lines are not checked. of `smallwebwaf`'s own rules, alone or in a range. Comment lines are not
checked.
- It stops too on a directive that removes or changes rules as the file - It stops too on a directive that removes or changes rules as the file
loads, such as `SecRuleRemoveById`, since none of the Core Rule Set's loads, such as `SecRuleRemoveById`, since none of the Core Rule Set's
rules is loaded yet. rules is loaded yet.
+5 -5
View File
@@ -1,8 +1,8 @@
// Command sww is a web application firewall. `sww rproxy` is // Command sww is a web application firewall. `sww rproxy` is the reverse
// the reverse proxy in front of one app, which asks discriminator about each request. // proxy in front of one app, which asks discriminator about each request.
// `sww discriminator` decides, for many sites, and keeps what lasts. // `sww discriminator` decides, for many sites, and keeps what lasts. `sww
// `sww healthcheck` is the container's health check. Everything it does // healthcheck` is the container's health check. Everything it does is in
// is in internal/sww. // internal/sww.
package main package main
import ( import (
+3 -4
View File
@@ -306,10 +306,9 @@ func (h *handler) refuseQuestion(w http.ResponseWriter, reason string) {
// 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 discriminator,
// with // with body, which has been read of r, and with the length the visitor
// body, which has been read of r, and with the length the visitor announced, // announced, as -1 when it announced none. The question's own length covers
// as -1 when it announced none. The question's own length covers only the part // only the part of the body it carries.
// of the body it carries.
func asTheVisitorSentIt(r *http.Request, body []byte) *http.Request { func asTheVisitorSentIt(r *http.Request, body []byte) *http.Request {
sent := r.Clone(r.Context()) sent := r.Clone(r.Context())
announced := sent.Header.Get(ContentLengthHeader) announced := sent.Header.Get(ContentLengthHeader)
+3 -3
View File
@@ -391,9 +391,9 @@ func startRProxy(
} }
// newDiscriminator makes discriminator's server, without starting it, and // newDiscriminator makes discriminator's server, without starting it, and
// returns it, what // returns it, what it writes, and the queue of the alerts it raises, as the
// it writes, and the queue of the alerts it raises, as the settings in env make // settings in env make it. No alert is sent from the queue: they wait in it,
// it. No alert is sent from the queue: they wait in it, for the test to look at. // for the test to look at.
// With no geojsURL, there is no stand-in for GeoJS to look clients up at, and // With no geojsURL, there is no stand-in for GeoJS to look clients up at, and
// SWW_LOOKUP_SOURCE is off unless env sets it. While it is file, the lookup // SWW_LOOKUP_SOURCE is off unless env sets it. While it is file, the lookup
// database SWW_LOOKUP_DB_PATH names is read. Clients are checked with AbuseIPDB // database SWW_LOOKUP_DB_PATH names is read. Clients are checked with AbuseIPDB
+3 -4
View File
@@ -146,10 +146,9 @@ func newTransport() *http.Transport {
} }
// newDiscriminatorTransport returns what carries questions and reports to // newDiscriminatorTransport returns what carries questions and reports to
// discriminator. // discriminator. It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send
// It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send a visitor's Upgrade // a visitor's Upgrade header, or a Connection header other than close or
// header, or a Connection header other than close or keep-alive, and that would // keep-alive, and that would read as discriminator giving no answer.
// read as discriminator giving no answer.
func newDiscriminatorTransport() *http.Transport { func newDiscriminatorTransport() *http.Transport {
transport := newTransport() transport := newTransport()
transport.Protocols = new(http.Protocols) transport.Protocols = new(http.Protocols)
+4 -5
View File
@@ -268,8 +268,8 @@ func (s *Discriminator) question(t *testing.T) question {
} }
// startDiscriminatorAnsweringReports starts a stand-in for discriminator that // startDiscriminatorAnsweringReports starts a stand-in for discriminator that
// answers each // answers each question as answer does and each report as report does, and
// question as answer does and each report as report does, and returns its URL. // returns its URL.
func startDiscriminatorAnsweringReports( func startDiscriminatorAnsweringReports(
t *testing.T, answer, report http.HandlerFunc, t *testing.T, answer, report http.HandlerFunc,
) string { ) string {
@@ -1033,9 +1033,8 @@ func TestAnAnswerOf1MiBGoesToTheVisitor(t *testing.T) {
} }
// startDiscriminatorWriting starts a stand-in for discriminator that reads // startDiscriminatorWriting starts a stand-in for discriminator that reads
// each question and // each question and answers it with text, as it is, and returns its URL. It
// answers it with text, as it is, and returns its URL. It can send what Go's // can send what Go's server would not.
// server would not.
func startDiscriminatorWriting(t *testing.T, text string) string { func startDiscriminatorWriting(t *testing.T, text string) string {
t.Helper() t.Helper()