Author SHA1 Message Date
clawbot e3b159db07 rproxy and servewaf replace the one-process smallwebwaf (closes #172, closes #165)
check / check (push) Failing after 8m17s
`smallwebwaf` with no command now prints the commands and exits with status 2.
The image runs rproxy, or servewaf with SMALLWEBWAF_COMMAND=servewaf. The
passing-on code, its settings and its log fields and metric go. internal/proxy
keeps the checks, answers, reports and own endpoints; its tests run through the
real rproxy. The documents describe the pair.

Judgement call: clients.json keeps token_refused, since state files refuse unknown fields.
Deviation: script/gitea-requests sets SWWAF_LOG_REQUEST_HEADERS, to label requests.

Model: opus-5-5
2026-11-05 08:54:11 +00:00
3 changed files with 23 additions and 45 deletions
+15 -15
View File
@@ -1575,13 +1575,12 @@ It sets no `ENTRYPOINT` or `USER` of its own. The container must start
writes. `runsvinit` starts runit's `runsvdir`, which runs the `run` script of
each directory under `/etc/service`. Ubuntu's `sv` looks for services there
too.
- The image's `run` script is `/etc/service/smallwebwaf/run`. It starts
`servewaf` when `SMALLWEBWAF_COMMAND` is `servewaf`, and `rproxy` when it is
`rproxy`, unset or empty. It runs either as the `smallwebwaf` user (uid and
gid 65532), with runit's `chpst`. For `servewaf`, it first makes
`SWWAF_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 `SMALLWEBWAF_COMMAND` and its value.
- The image's `run` script, `/etc/service/smallwebwaf/run`, starts the command
`SMALLWEBWAF_COMMAND` names, `rproxy` when it is unset or empty, as the
`smallwebwaf` user (uid and gid 65532) with runit's `chpst`. For `servewaf`,
it first makes `SWWAF_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 `SMALLWEBWAF_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`
@@ -1701,14 +1700,15 @@ ranges traefik is on.
### upaas
upaas rebuilds and restarts an app's Docker container when its repository
changes. An app it deploys takes the shape of "`rproxy` in the app's container"
with no change to upaas. upaas builds the app's Dockerfile and runs the one
container as it runs any app. The traefik labels route to port 8080, and the
`SWRP_` settings go with the app's environment variables. `servewaf` runs in a
container of its own, on a network the app's container can reach, with its state
volume and the directory of any token file. `docs/gitea.md` records what the
defaults do in front of gitea, and `script/gitea-requests` checks those notes.
An app deployed by upaas, which rebuilds and restarts an app's Docker container
when its repository changes, takes the shape of "`rproxy` in the app's
container" with no change to upaas. upaas builds the app's Dockerfile and runs
the one container as it runs any app. The traefik labels route to port 8080, and
the `SWRP_` settings go with the app's environment variables. `servewaf` runs in
a container of its own, on a network the app's container can reach, with its
state volume and the directory of any token file. `docs/gitea.md` records what
the defaults do in front of gitea, and `script/gitea-requests` checks those
notes.
## Planned, not built
+7 -27
View File
@@ -1,9 +1,7 @@
package rproxy_test
import (
"bufio"
"bytes"
"errors"
"io"
"log/slog"
"net"
@@ -604,12 +602,10 @@ func TestAnAppTooSlowToAnswerIsAnswered504(t *testing.T) {
func TestAnAppTooSlowToFinishItsAnswerIsCutOff(t *testing.T) {
t.Parallel()
const start = "the start"
release := make(chan struct{})
servewaf := startServeWAF(t, allow)
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, start)
_, _ = io.WriteString(w, "the start")
_ = http.NewResponseController(w).Flush()
<-release
@@ -617,29 +613,17 @@ func TestAnAppTooSlowToFinishItsAnswerIsCutOff(t *testing.T) {
t.Cleanup(func() { close(release) })
// The time limit is long enough for a loaded host to pass on the start of the
// answer before it runs out.
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{
maxResponseTime: "3000",
maxResponseTime: "300",
})
conn := dial(t, addr)
send(t, conn, "GET / HTTP/1.1\r\nHost: site.example\r\n\r\n")
res, err := http.ReadResponse(bufio.NewReader(conn), nil)
// dial's deadline ends this read with an error if the connection stays open.
_, err := io.ReadAll(conn)
if err != nil {
t.Fatalf("read the answer: %v", err)
}
// The body breaks off when rproxy closes the connection. If the connection
// stays open, dial's deadline ends this read with a timeout instead.
body, err := io.ReadAll(res.Body)
_ = res.Body.Close()
if res.StatusCode != http.StatusOK || string(body) != start ||
!errors.Is(err, io.ErrUnexpectedEOF) {
t.Errorf("answered %d %q (%v), want the app's 200 %q, then the connection closed",
res.StatusCode, body, err, start)
t.Errorf("read: %v, want the connection closed", err)
}
if report := servewaf.report(t); report.Status != http.StatusOK {
@@ -667,10 +651,8 @@ func TestAVisitorTooSlowToTakeTheAnswerIsCutOff(t *testing.T) {
}
}
})
// The time limit is long enough for a loaded host to start passing on the
// answer before it runs out.
addr, out := startRProxy(t, servewaf.URL, app.URL, map[string]string{
maxResponseTime: "3000",
maxResponseTime: "500",
})
// The visitor asks, and never reads the answer.
@@ -811,10 +793,8 @@ func TestAVisitorThatStopsSendingGetsAnAnswerRProxyGivesByTheDeadline(t *testing
servewaf := startServeWAF(t, deny)
app := startApp(t, greet)
// The time limit is long enough for a loaded host to read the first part of
// the body before it runs out.
addr, _ := startRProxy(t, servewaf.URL, app.URL, map[string]string{
maxRequestTime: "3000",
maxRequestTime: "500",
})
// Go's server waits for a little of the body left before it sends an answer,
+1 -3
View File
@@ -1353,10 +1353,8 @@ func TestAReportServeWAFLeavesUnansweredIsDroppedAtTheTimeLimit(t *testing.T) {
})
app := startApp(t, greet)
// The time limit is long enough for a loaded host to answer the question before
// it runs out.
addr, out := startRProxy(t, backend, app.URL, map[string]string{
timeoutSetting: "3000",
timeoutSetting: "1000",
})
t.Cleanup(func() { close(release) })