Author SHA1 Message Date
sneak 42643be662 Rename the program to sww and its deciding daemon to discriminator
check / check (push) Successful in 3m13s
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 04:47:34 +00:00
11 changed files with 33 additions and 61 deletions

No files matched your search

-14
View File
@@ -124,20 +124,6 @@ RUN git checkout --quiet --detach b4b2c785308b1ce785b6155c7fe5f16879080193 \
# ubuntu 26.04, 2026-09-27 # ubuntu 26.04, 2026-09-27
FROM ubuntu@sha256:f144425ff09be612d6d9ad965196e9cdc23dae1f42110a8a11a3e9a8198759f7 FROM ubuntu@sha256:f144425ff09be612d6d9ad965196e9cdc23dae1f42110a8a11a3e9a8198759f7
# The image's own labels, in place of the ones it would inherit from
# Ubuntu's. script/docker and script/cibuild pass the build arguments;
# a build that passes none labels the image unknown rather than leave
# Ubuntu's values.
ARG VERSION=unknown
ARG CREATED=unknown
ARG REVISION=unknown
LABEL org.opencontainers.image.title="smallwebwaf" \
org.opencontainers.image.description="A web application firewall for self-hosted web apps: sww rproxy in front of an app, and sww discriminator, which decides." \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.created="${CREATED}" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.source="https://git.eeqj.de/sneak/smallwebwaf"
# runit's install creates its _runit-log user with minsysusers, which # runit's install creates its _runit-log user with minsysusers, which
# reads this file in place of runit's /usr/lib/sysusers.d/runit.conf. # reads this file in place of runit's /usr/lib/sysusers.d/runit.conf.
# runit's line leaves out the shell, and minsysusers prints a Perl # runit's line leaves out the shell, and minsysusers prints a Perl
+7 -8
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 program, `sww`, refuses the ones that look like abuse. `smallwebwaf` is one binary, shipped in
shipped in one image, with two commands that do this together: 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,8 +194,7 @@ 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. `SWW_COMMAND` cannot be given as a file. No `SWRP_` setting has `_FILE` form. No `SWRP_` setting has one, since `rproxy` never touches disk.
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`.
@@ -582,10 +581,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 `sww` as the `sww` user (uid and gid 65532) - runit, a service supervisor, runs `smallwebwaf` as the `sww` user (uid and
and starts it again when it exits. The `run` script of `discriminator` gives gid 65532) and starts it again when it exits. The `run` script of
the state directory to the `sww` user at each start, so a volume needs no `discriminator` gives the state directory to the `sww` user at each start, so
change of owner. a volume needs no 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
+5 -7
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, `sww`, and two of its commands do the work: binary, 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,8 +326,7 @@ 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. `SWW_COMMAND` cannot be given as a file. No `SWRP_` setting has `_FILE` form. No `SWRP_` setting has one, since `rproxy` never touches disk.
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.
@@ -585,8 +584,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 `smallwebwaf`'s own setup rules and - `discriminator` loads the file after `sww`'s own setup rules and before
before the Core Rule Set's rules. The Core Rule Set loads its own 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
@@ -596,8 +595,7 @@ 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 `smallwebwaf`'s own rules, alone or in a range. Comment lines are not of `sww`'s own rules, alone or in a range. Comment lines are not checked.
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 the reverse // Command sww is a web application firewall. `sww rproxy` is
// proxy in front of one app, which asks discriminator about each request. // the reverse proxy in front of one app, which asks discriminator about each request.
// `sww discriminator` decides, for many sites, and keeps what lasts. `sww // `sww discriminator` decides, for many sites, and keeps what lasts.
// healthcheck` is the container's health check. Everything it does is in // `sww healthcheck` is the container's health check. Everything it does
// internal/sww. // is in internal/sww.
package main package main
import ( import (
-4
View File
@@ -9,10 +9,6 @@
ARG SWW_IMAGE=smallwebwaf ARG SWW_IMAGE=smallwebwaf
FROM ${SWW_IMAGE} FROM ${SWW_IMAGE}
# Its own title and description, in place of the smallwebwaf image's.
LABEL org.opencontainers.image.title="smallwebwaf-example-app" \
org.opencontainers.image.description="An example app built on the smallwebwaf image."
# Packages the app needs, from the nixpkgs in the image. # Packages the app needs, from the nixpkgs in the image.
RUN nix-env -iA nixpkgs.busybox RUN nix-env -iA nixpkgs.busybox
+4 -3
View File
@@ -306,9 +306,10 @@ 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 body, which has been read of r, and with the length the visitor // with
// announced, as -1 when it announced none. The question's own length covers // body, which has been read of r, and with the length the visitor announced,
// only the part of the body it carries. // as -1 when it announced none. The question's own length covers only the part
// 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 it writes, and the queue of the alerts it raises, as the // returns it, what
// settings in env make it. No alert is sent from the queue: they wait in it, // it writes, and the queue of the alerts it raises, as the settings in env make
// for the test to look at. // it. No alert is sent from the queue: they wait in it, 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
+4 -3
View File
@@ -146,9 +146,10 @@ func newTransport() *http.Transport {
} }
// newDiscriminatorTransport returns what carries questions and reports to // newDiscriminatorTransport returns what carries questions and reports to
// discriminator. It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send // discriminator.
// a visitor's Upgrade header, or a Connection header other than close or // It speaks HTTP/1.1 only. Go's HTTP/2 client refuses to send a visitor's Upgrade
// keep-alive, and that would read as discriminator giving no answer. // header, or a Connection header other than close or keep-alive, and that would
// 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)
+5 -4
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 question as answer does and each report as report does, and // answers each
// returns its URL. // question as answer does and each report as report does, and returns its URL.
func startDiscriminatorAnsweringReports( func startDiscriminatorAnsweringReports(
t *testing.T, answer, report http.HandlerFunc, t *testing.T, answer, report http.HandlerFunc,
) string { ) string {
@@ -1033,8 +1033,9 @@ 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 answers it with text, as it is, and returns its URL. It // each question and
// can send what Go's server would not. // answers it with text, as it is, and returns its URL. It can send what Go's
// server would not.
func startDiscriminatorWriting(t *testing.T, text string) string { func startDiscriminatorWriting(t *testing.T, text string) string {
t.Helper() t.Helper()
-5
View File
@@ -20,13 +20,8 @@ main() {
# the version a build stage derives from the .git in the context. # the version a build stage derives from the .git in the context.
version="$(git describe --tags --always --dirty 2>/dev/null || true)" version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown" [ -n "$version" ] || version="unknown"
revision="$(git rev-parse HEAD 2>/dev/null || true)"
[ -n "$revision" ] || revision="unknown"
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
docker build --no-cache \ docker build --no-cache \
--build-arg VERSION="$version" \ --build-arg VERSION="$version" \
--build-arg REVISION="$revision" \
--build-arg CREATED="$created" \
-t "$("$SCRIPT_DIR/projectname")" . -t "$("$SCRIPT_DIR/projectname")" .
} }
-5
View File
@@ -16,13 +16,8 @@ main() {
# the version a build stage derives from the .git in the context. # the version a build stage derives from the .git in the context.
version="$(git describe --tags --always --dirty 2>/dev/null || true)" version="$(git describe --tags --always --dirty 2>/dev/null || true)"
[ -n "$version" ] || version="unknown" [ -n "$version" ] || version="unknown"
revision="$(git rev-parse HEAD 2>/dev/null || true)"
[ -n "$revision" ] || revision="unknown"
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
docker build --no-cache \ docker build --no-cache \
--build-arg VERSION="$version" \ --build-arg VERSION="$version" \
--build-arg REVISION="$revision" \
--build-arg CREATED="$created" \
-t "$("$SCRIPT_DIR/projectname")" . -t "$("$SCRIPT_DIR/projectname")" .
} }