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
This commit is contained in:
sneak committed 2026-11-07 05:27:58 +00:00
1 parent 2e2f6f7175
commit 4a030557f3
106 files changed
+3302 -3275

No files matched your search

+25 -25
View File
@@ -1,5 +1,5 @@
// Package state keeps smallwebwaf's state in JSON files in
// SWWAF_STATE_DIR, as the "Persistent state" section of SPEC.md requires
// Package state keeps sww's state in JSON files in
// SWW_STATE_DIR, as the "Persistent state" section of SPEC.md requires
// and docs/state-files.md lists them:
// bans.json holds the bans, clients.json each client's counters and
// history, lookups.json GeoJS's answers, reputation.json the last try and
@@ -8,7 +8,7 @@
// cooldowns, the hour under way, the alerts waiting for each destination
// and the anomaly counters. Load
// reads them at start, Watch takes in an admin's edit of one while
// smallwebwaf runs, and Run and WriteAll write them. The disk is read and
// sww runs, and Run and WriteAll write them. The disk is read and
// written outside the parts' locks, which are held only to take a
// snapshot or to put in what a file holds, so that no request waits on
// the disk.
@@ -44,7 +44,7 @@ import (
// version is the version of the files' format, the only one read.
const version = 1
// fileMode lets the smallwebwaf user alone read and write the files, which
// fileMode lets the sww user alone read and write the files, which
// hold visitors' addresses.
const fileMode = 0o600
@@ -72,16 +72,16 @@ var (
// Params are what Load needs.
type Params struct {
// Dir is the directory of the state files (SWWAF_STATE_DIR).
// Dir is the directory of the state files (SWW_STATE_DIR).
Dir string
// WriteDelay is how long after a ban is made bans.json is written
// (SWWAF_STATE_WRITE_DELAY), and CounterInterval how often every file
// is (SWWAF_STATE_COUNTER_INTERVAL).
// (SWW_STATE_WRITE_DELAY), and CounterInterval how often every file
// is (SWW_STATE_COUNTER_INTERVAL).
WriteDelay time.Duration
CounterInterval time.Duration
// Ledger, Limiter, GeoJS, Lists, DNSBL, AbuseIPDB, Alerts and Anomalies
// hold the state. Alerts also receive a file_error alert for an edit set
// aside, and for a write that fails while smallwebwaf runs.
// aside, and for a write that fails while sww runs.
Ledger *bans.Ledger
Limiter *ratelimit.Limiter
GeoJS *lookup.GeoJS
@@ -101,7 +101,7 @@ type Params struct {
Metrics *metrics.Metrics
}
// Files are the state files of a running smallwebwaf.
// Files are the state files of a running sww.
type Files struct {
params Params
@@ -110,7 +110,7 @@ type Files struct {
// it.
mu sync.Mutex
// sums are the SHA-256 sums of what each file held, by name, when
// smallwebwaf last read or wrote it. A file that holds anything else
// sww last read or wrote it. A file that holds anything else
// has been edited since.
sums map[string][sha256.Size]byte
}
@@ -185,7 +185,7 @@ type stateFile interface {
func Load(params Params) (*Files, error) {
err := checkWritable(params.Dir)
if err != nil {
return nil, fmt.Errorf("SWWAF_STATE_DIR cannot be written: %w", err)
return nil, fmt.Errorf("SWW_STATE_DIR cannot be written: %w", err)
}
f := &Files{params: params, sums: map[string][sha256.Size]byte{}}
@@ -241,7 +241,7 @@ func (f *Files) Run(ctx context.Context) {
}
}
// WriteAll writes every state file, as smallwebwaf stops. A file that
// WriteAll writes every state file, as sww stops. A file that
// fails does not keep the others from being written.
func (f *Files) WriteAll() error {
return errors.Join(f.writeFile(bansJSON), f.writeFile(clientsJSON),
@@ -250,7 +250,7 @@ func (f *Files) WriteAll() error {
// Watch watches Dir until ctx is done, and takes in an admin's edit of a
// state file as soon as it is saved: what the file holds replaces what
// smallwebwaf held for it. An edit that does not parse is left for the
// sww held for it. An edit that does not parse is left for the
// file's next write, which sets it aside, since a file can be read while
// an editor is still writing it. If Dir cannot be watched, that is
// logged, and an edit is taken in only before its file is written.
@@ -310,7 +310,7 @@ func (f *Files) logFailure(name string, err error) {
}
// fileChanged takes in what the state file name holds, as Watch sees it
// change, if that is an edit made since smallwebwaf last read or wrote
// change, if that is an edit made since sww last read or wrote
// the file. A file that cannot be read or does not parse is left for its
// next write.
func (f *Files) fileChanged(name string) {
@@ -326,7 +326,7 @@ func (f *Files) fileChanged(name string) {
}
// takeInEdit takes in data, an edit of the state file name, as takeIn
// does, and counts and logs it. Every edit taken in while smallwebwaf
// does, and counts and logs it. Every edit taken in while sww
// runs, by Watch or by a write, is taken in here. An edit that does not
// parse is neither counted nor logged, and takeIn's error returned.
func (f *Files) takeInEdit(name string, data []byte) error {
@@ -356,13 +356,13 @@ func (f *Files) read(name string) (int, error) {
}
// readChanged returns what the state file name holds, and whether that
// has changed since smallwebwaf last read or wrote the file, as it has
// for a file smallwebwaf never read or wrote. A missing file has not
// has changed since sww last read or wrote the file, as it has
// for a file sww never read or wrote. A missing file has not
// changed: it is written again at its next write.
func (f *Files) readChanged(name string) ([]byte, bool, error) {
path := filepath.Join(f.params.Dir, name)
data, err := os.ReadFile(path) //nolint:gosec // a state file, in SWWAF_STATE_DIR
data, err := os.ReadFile(path) //nolint:gosec // a state file, in SWW_STATE_DIR
if errors.Is(err, fs.ErrNotExist) {
return nil, false, nil
}
@@ -377,7 +377,7 @@ func (f *Files) readChanged(name string) ([]byte, bool, error) {
// takeIn parses data, what the state file name holds, puts it into the
// part that keeps that state, in place of what the part held, and returns
// how many entries the file holds. edit is whether data is an admin's
// edit taken in while smallwebwaf runs, rather than the file read at the
// edit taken in while sww runs, rather than the file read at the
// start. An error names the file and, where the JSON decoder tells it,
// the line and column, or else the entry.
func (f *Files) takeIn(name string, data []byte, edit bool) (int, error) {
@@ -491,8 +491,8 @@ func (f *Files) takeInAlerts(path string, data []byte) (int, error) {
return entries, nil
}
// writeFile writes the state file name from what smallwebwaf holds. An
// edit made since smallwebwaf last read or wrote the file is taken in
// writeFile writes the state file name from what sww holds. An
// edit made since sww last read or wrote the file is taken in
// first, so that it is not overwritten, or set aside if it does not
// parse. A file that cannot be read, or an edit that cannot be set
// aside, is left as it is, and the write given up. Every write is counted
@@ -561,7 +561,7 @@ func (f *Files) setAside(name string, parseErr error) error {
return nil
}
// encode returns the state file name as smallwebwaf writes it, from a
// encode returns the state file name as sww writes it, from a
// snapshot of the part that keeps that state.
func (f *Files) encode(name string) ([]byte, error) {
switch name {
@@ -965,7 +965,7 @@ func parse(path string, data []byte, file stateFile) error {
err := json.Unmarshal(data, &header)
if err == nil && header.Version != version {
err = fmt.Errorf("%w %d, where this smallwebwaf reads version %d",
err = fmt.Errorf("%w %d, where this sww reads version %d",
errVersion, header.Version, version)
}
@@ -1035,7 +1035,7 @@ func write(dir, name string, data []byte) error {
// syncDirectory syncs dir to the disk, so that a rename in it lasts.
func syncDirectory(dir string) error {
directory, err := os.Open(dir) //nolint:gosec // SWWAF_STATE_DIR itself
directory, err := os.Open(dir) //nolint:gosec // SWW_STATE_DIR itself
if err != nil {
return err
}
@@ -1045,7 +1045,7 @@ func syncDirectory(dir string) error {
// writeSynced writes data to the file at path, and syncs it to the disk.
func writeSynced(path string, data []byte) error {
//nolint:gosec // a state file's temporary file, in SWWAF_STATE_DIR
//nolint:gosec // a state file's temporary file, in SWW_STATE_DIR
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return err
+21 -21
View File
@@ -122,7 +122,7 @@ const filledAlertsJSON = `{
{
"event": "file_error",
"netblock": "",
"file": "/var/lib/smallwebwaf/bans.json",
"file": "/var/lib/sww/bans.json",
"sent": "2026-10-06T00:00:00Z",
"suppressed_repeats": 0
},
@@ -169,7 +169,7 @@ const filledAlertsJSON = `{
"reason": "writing the state files failed",
"detail": {
"error": "no space left on device",
"file": "/var/lib/smallwebwaf/bans.json"
"file": "/var/lib/sww/bans.json"
},
"suppressed_repeats": 0
}
@@ -931,7 +931,7 @@ func TestUnwritableDirectoryStopsTheStart(t *testing.T) {
filepath.Join(t.TempDir(), "missing"),
notADirectory,
} {
const want = "SWWAF_STATE_DIR cannot be written: "
const want = "SWW_STATE_DIR cannot be written: "
_, err := state.Load(newParams(dir))
if err == nil || !strings.HasPrefix(err.Error(), want) {
@@ -1165,13 +1165,13 @@ func TestWritesAreCountedInTheMetrics(t *testing.T) {
)
got := scrape(t, params)
wantMetric(t, got, "smallwebwaf_state_file_writes_total"+ofBans, 1)
wantMetric(t, got, "smallwebwaf_state_file_writes_total"+ofClients, 1)
wantMetric(t, got, "smallwebwaf_state_file_write_failures_total"+ofBans, 0)
wantMetric(t, got, "smallwebwaf_state_file_size_bytes"+ofBans,
wantMetric(t, got, "sww_state_file_writes_total"+ofBans, 1)
wantMetric(t, got, "sww_state_file_writes_total"+ofClients, 1)
wantMetric(t, got, "sww_state_file_write_failures_total"+ofBans, 0)
wantMetric(t, got, "sww_state_file_size_bytes"+ofBans,
float64(len(permanentBansJSON)))
written := metric(t, got, "smallwebwaf_state_file_last_write_timestamp_seconds"+ofBans)
written := metric(t, got, "sww_state_file_last_write_timestamp_seconds"+ofBans)
if written < float64(time.Now().Add(-time.Hour).Unix()) {
t.Errorf("bans.json was last written at %v, not by that write", written)
}
@@ -1192,10 +1192,10 @@ func TestWritesAreCountedInTheMetrics(t *testing.T) {
}
got = scrape(t, params)
wantMetric(t, got, "smallwebwaf_state_file_writes_total"+ofBans, 2)
wantMetric(t, got, "smallwebwaf_state_file_write_failures_total"+ofBans, 1)
wantMetric(t, got, "smallwebwaf_state_file_write_failures_total"+ofClients, 0)
wantMetric(t, got, "smallwebwaf_state_file_size_bytes"+ofBans,
wantMetric(t, got, "sww_state_file_writes_total"+ofBans, 2)
wantMetric(t, got, "sww_state_file_write_failures_total"+ofBans, 1)
wantMetric(t, got, "sww_state_file_write_failures_total"+ofClients, 0)
wantMetric(t, got, "sww_state_file_size_bytes"+ofBans,
float64(len(permanentBansJSON)))
}
@@ -1486,7 +1486,7 @@ func TestBanWithoutACauseTakenInAsAnAdmins(t *testing.T) {
"and the reason %q", written, err, reason)
}
// Taken in while smallwebwaf runs, a ban on another netblock is an
// Taken in while sww runs, a ban on another netblock is an
// admin's too, and one made since the start.
watch(t, files, lines)
edit(t, dir, bansJSON, adminsBansJSON("198.51.100.0/24"))
@@ -1549,7 +1549,7 @@ func TestBrokenEditSetAsideAtTheNextWrite(t *testing.T) {
watch(t, files, lines)
// While smallwebwaf runs, the broken edit is left as it is: an edit
// While sww runs, the broken edit is left as it is: an edit
// of clients.json, made after it and taken in, shows that it has been
// seen.
edit(t, dir, bansJSON, broken)
@@ -1558,7 +1558,7 @@ func TestBrokenEditSetAsideAtTheNextWrite(t *testing.T) {
wantFiles(t, dir, alertsJSON, bansJSON, clientsJSON, lookupsJSON, reputationJSON)
// The next write sets it aside, logged with where the error is, and
// writes bans.json again from what smallwebwaf still holds.
// writes bans.json again from what sww still holds.
err = files.WriteAll()
if err != nil {
t.Fatalf("write: %v", err)
@@ -1613,7 +1613,7 @@ func TestEditsTakenInAreCountedInTheMetrics(t *testing.T) {
wantTakenIn(t, lines, dir, bansJSON)
wantMetric(t, scrape(t, params),
`smallwebwaf_state_file_edits_taken_in_total{file="bans.json",instance="app"}`, 2)
`sww_state_file_edits_taken_in_total{file="bans.json",instance="app"}`, 2)
}
func TestEditTakenInByAWriteIsLoggedAsWatchLogsIt(t *testing.T) {
@@ -1678,7 +1678,7 @@ func TestEditsSetAsideAreCountedInTheMetrics(t *testing.T) {
}
wantMetric(t, scrape(t, params),
`smallwebwaf_state_file_edits_set_aside_total{file="bans.json",instance="app"}`, 1)
`sww_state_file_edits_set_aside_total{file="bans.json",instance="app"}`, 1)
}
func TestDirectoryThatCannotBeWatchedIsLogged(t *testing.T) {
@@ -1854,7 +1854,7 @@ func fill(params state.Params) {
params.Alerts.Raise(alerts.Alert{
Event: alerts.EventFileError, Reason: "writing the state files failed",
Detail: map[string]any{
"file": "/var/lib/smallwebwaf/bans.json", "error": "no space left on device",
"file": "/var/lib/sww/bans.json", "error": "no space left on device",
},
})
params.Alerts.Raise(alerts.Alert{
@@ -2178,7 +2178,7 @@ func scrape(t *testing.T, params state.Params) string {
}
// metric returns the value of series in text, the metrics, such as
// smallwebwaf_state_file_writes_total{file="bans.json",instance="app"}, or
// sww_state_file_writes_total{file="bans.json",instance="app"}, or
// fails the test if there is no such series.
func metric(t *testing.T, text, series string) float64 {
t.Helper()
@@ -2210,8 +2210,8 @@ func wantWriteFailed(t *testing.T, params state.Params, name string) {
got := scrape(t, params)
file := `{file="` + name + `",instance="app"}`
wantMetric(t, got, "smallwebwaf_state_file_writes_total"+file, 1)
wantMetric(t, got, "smallwebwaf_state_file_write_failures_total"+file, 1)
wantMetric(t, got, "sww_state_file_writes_total"+file, 1)
wantMetric(t, got, "sww_state_file_write_failures_total"+file, 1)
}
// wantMetric checks the value of series in text, the metrics, as metric