Caches of outside APIs live in /var/cache/sww (closes #314)
check / check (push) Waiting to run

The answers of GeoJS, the lists fetched by URL, the DNS blocklists and
AbuseIPDB each go in a cache file in SWW_CACHE_DIR (default
/var/cache/sww): lookups.json, lists.json, dnsbl.json and
abuseipdb-scores.json. reputation.json is gone. The AbuseIPDB checks
spent today are a count, not a cache, and stay in SWW_STATE_DIR as
abuseipdb-checks.json.

A cache file never stops the start: one that does not parse is set
aside as an edit is, one that cannot be read is written over, and its
cache starts empty. An unwritable SWW_CACHE_DIR stops the start, as an
unwritable SWW_STATE_DIR does. The run script makes the cache directory
and gives it to sww.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-11 05:33:55 +00:00
1 parent b89c53c640
commit 7c913d058d
23 files changed
+1049 -449

No files matched your search

+49 -31
View File
@@ -37,7 +37,7 @@ never as a time.
`SWW_`, and those of `rproxy` with `SWRP_`. The only other inputs are `waf`'s
files:
- the rule files;
- the state files;
- the state files and the cache files;
- a lookup database;
- a `SWW_` setting given as a file;
- the file `SWW_LOG_REMOTE_TLS_CA_FILE` names;
@@ -56,8 +56,8 @@ never as a time.
## Architecture
- One statically linked Go binary. Only `waf` writes to disk, and only to its
state directory. It ships in one image, which runs either command (see
"Deployment").
state directory and its cache directory. It ships in one image, which runs
either command (see "Deployment").
- No database of any kind. Nothing is read from disk while serving a request.
- Packages:
- `cmd/sww`: the binary, which only calls `internal/sww`.
@@ -96,8 +96,8 @@ never as a time.
- `internal/alerts`: holds back repeats and alerts past the hourly limit,
and sends the rest to the webhook, Slack and ntfy, each from a queue of
its own.
- `internal/state`: reads the state files at start, takes in edits while
running, and writes the files when due and at the stop.
- `internal/state`: reads the state and cache files at start, takes in edits
while running, and writes the files when due and at the stop.
- `internal/requestlog` and `internal/remotelog`: the lines on stdout, and
sending them to the syslog server.
- `internal/metrics`: the metrics, in the Prometheus text format.
@@ -391,10 +391,10 @@ below are `waf`'s settings; "`rproxy`" gives the use of `rproxy`'s. Conventions:
start. The file form keeps a token out of the container's environment (see
"Tokens" under "Deployment").
### State files
### State and cache files
- `SWW_STATE_DIR` names the directory of the state files (see "State" under
"Deployment").
- `SWW_STATE_DIR` names the directory of the state files, and `SWW_CACHE_DIR`
that of the cache files (see "State" under "Deployment").
- `bans.json` is written `SWW_STATE_WRITE_DELAY` after a ban is made, lifted or
made permanent, so a burst of changes becomes one write. Every file is written
every `SWW_STATE_COUNTER_INTERVAL`, and at an orderly stop.
@@ -1595,8 +1595,8 @@ together.
is kept whole, comment lines included, on failure and across restarts.
- Each list is fetched again `SWW_BLOCKLIST_REFRESH` after its last try, one
list at a time. `reputation.json` keeps when each was last tried, so a restart
does not fetch a list again before its time. A fetch fails when:
list at a time. `lists.json` keeps when each was last tried, so a restart does
not fetch a list again before its time. A fetch fails when:
- the server answers other than `200`;
- the fetch does not finish within a minute;
- the list is longer than 16 MiB;
@@ -1640,9 +1640,9 @@ together.
DNS blocklists made for mail servers list residential ranges, and would
block ordinary visitors, so they should not be used. Spamhaus zones need
their keyed query service, given as a zone name that contains the key. `waf`
shows the key as `********` wherever it names the zone. Only
`reputation.json` keeps the key. `SWW_DNSBL_RESOLVER` names a resolver,
since public resolvers are refused by several list operators.
shows the key as `********` wherever it names the zone. Only `dnsbl.json`
keeps the key. `SWW_DNSBL_RESOLVER` names a resolver, since public resolvers
are refused by several list operators.
- AbuseIPDB (`SWW_ABUSEIPDB_KEY`) is asked, at its check endpoint, for the abuse
confidence score of a client, from 0 to 100. Only a client that has already
@@ -1653,8 +1653,8 @@ together.
client's later requests. For an IPv6 client, AbuseIPDB is asked about the
address of the request that set off the check, and the score is used for the
whole group. A score at or over `SWW_ABUSEIPDB_MIN_SCORE` is a hit. The budget
is counted in UTC days and kept in `reputation.json`, so a restart does not
make it whole again. The check that spends the last of it is logged and
is counted in UTC days and kept in `abuseipdb-checks.json`, so a restart does
not make it whole again. The check that spends the last of it is logged and
alerted, and no client is checked until the day ends. The key is sent in the
`Key` header and nowhere else, and is never logged. A check fails, after which
no client is checked for a minute, when:
@@ -1665,9 +1665,9 @@ together.
by default. Such verdicts are less certain than a blocklist, so a listed
client gets a quarter of every request and byte limit rather than a refusal.
- Verdicts and scores are kept for `SWW_REPUTATION_CACHE_TTL`, in memory and in
`reputation.json`. A source that is down or over quota gives no verdict. The
service continues. Each failure raises a `source_failure` alert (see
"Alerting").
`dnsbl.json` and `abuseipdb-scores.json`. A source that is down or over quota
gives no verdict. The service continues. Each failure raises a
`source_failure` alert (see "Alerting").
### Alerting
@@ -1770,21 +1770,28 @@ default file.
## Persistent state
All state lives in memory, and the files in `SWW_STATE_DIR` hold a copy of it,
so an orderly stop loses nothing. The files are meant for people as well: an
admin can read or edit them at any time, and the running `waf` takes the edit
in. `docs/state-files.md` lists the files, what each holds, and their sizes.
All state lives in memory, and JSON files hold a copy of it, so an orderly stop
loses nothing. The files are meant for people as well: an admin can read or edit
them at any time, and the running `waf` takes the edit in. `docs/state-files.md`
lists the files, what each holds, and their sizes.
- The state files, in `SWW_STATE_DIR`, hold what cannot be fetched again:
`bans.json`, `clients.json`, `abuseipdb-checks.json` and `alerts.json`.
- The cache files, in `SWW_CACHE_DIR`, hold the answers of outside services:
`lookups.json`, `lists.json`, `dnsbl.json` and `abuseipdb-scores.json`. A lost
cache file costs only questions asked again, so the cache directory can be
left out of backups.
- Each file holds one kind of state, as JSON with:
- a top-level `version` number;
- entries sorted by client address;
- times in RFC 3339 UTC;
- days as `YYYY-MM-DD`;
- durations and sizes as plain numbers, with the unit in the field name.
- `bans.json`, `reputation.json` and `alerts.json` are indented, so that a
person can open `bans.json` in an editor, find an address, and remove or add
an entry. `clients.json` and `lookups.json` put each entry on one line, which
halves their size and lets `grep` show everything about one client.
- `bans.json`, `abuseipdb-checks.json`, `alerts.json` and `lists.json` are
indented, so that a person can open `bans.json` in an editor, find an address,
and remove or add an entry. `clients.json`, `lookups.json`, `dnsbl.json` and
`abuseipdb-scores.json` put each entry on one line, which halves their size
and lets `grep` show everything about one client.
- Clients that were never banned are kept too, so every client's history
survives a restart.
- Each file has a bound, so it can be rewritten whole. Writing one takes well
@@ -1798,8 +1805,8 @@ in. `docs/state-files.md` lists the files, what each holds, and their sizes.
disk, so an admin's edit is never overwritten.
- At start, the files are read back. Active bans go into an in-memory lookup by
netblock, everything else into maps. Expired cache entries are discarded. A
missing file means empty state, and is normal on first run. These stop the
process with a message naming the file and position:
missing file means empty state, and is normal on first run. In a state file,
these stop the process with a message naming the file and position:
- a file that does not parse, or has an unknown `version`;
- an entry without a field it needs;
- a ban whose cause is not one of the four;
@@ -1809,6 +1816,11 @@ in. `docs/state-files.md` lists the files, what each holds, and their sizes.
Since writes are atomic, a broken file can only come from a hand edit, which
the editor should hear about.
- A cache file never stops the start. One that does not parse is set aside as
`<name>.bad`, as an edit is below. One that cannot be read is logged and
written over at its next write. Either way its cache starts empty and fills
again.
- While running, `waf` notices a file that is edited, replaced or added. It
tells its own writes from an admin's by comparing the file with what it last
read or wrote. An edit that parses is taken in at once: what the file says
@@ -2470,8 +2482,10 @@ It sets no `ENTRYPOINT` or `USER` of its own. The container must start
`SWW_COMMAND` is `waf`, and `rproxy` when it is `rproxy`, unset or empty. It
runs either as the `sww` user (uid and gid 65532), with runit's `chpst`. For
`waf`, it first makes `SWW_STATE_DIR` and every file in it belong to that
user. For `rproxy` it changes no owner. With any other value, it starts
nothing, and stops with a message naming `SWW_COMMAND` and its value.
user. It also makes `SWW_CACHE_DIR` if it is missing, and gives it and every
file in it to that user. For `rproxy` it changes no owner. With any other
value, it starts nothing, and stops with a message naming `SWW_COMMAND` and
its value.
Every `run` script, the app's included, is a bash script. It starts with
`#!/usr/bin/env bash` and `set -euo pipefail`, and puts its code in a `main`
@@ -2494,7 +2508,7 @@ process that stops at start is started again every second, not in a tight loop.
- invalid configuration;
- a lookup database that is missing or unreadable;
- a rule file or state file that does not parse;
- an unwritable `SWW_STATE_DIR`;
- an unwritable `SWW_STATE_DIR` or `SWW_CACHE_DIR`;
- a `SWW_RULES_DIR` that does not exist or cannot be read;
- a `SWW_LISTEN_ADDR` it cannot listen on.
- Only an invalid setting, or a `SWRP_LISTEN_ADDR` it cannot listen on, stops
@@ -2563,6 +2577,10 @@ needs no backup beyond the host's own. The image declares no volume, since every
app image built on it would inherit it. `rproxy` keeps nothing, and needs no
volume.
`waf` keeps its cache files in `SWW_CACHE_DIR`, `/var/cache/sww` by default,
which the `run` script makes. It needs no volume and can be left out of backups:
a new container starts with empty caches, which fill again.
### Tokens
A token given as a file (`SWW_ADMIN_TOKEN_FILE`, `SWW_METRICS_TOKEN_FILE`) stays