1 Commits
Author SHA1 Message Date
sneak 872172e958 servewaf answers rproxy's question about each request and takes its report (closes #170)
check / check (push) Successful in 10m27s
`smallwebwaf servewaf` runs today's checks on the visitor's request each
question carries, answers rproxy, and passes nothing to an app. rproxy reports
every request servewaf answered. A report counts an allowed request's bytes and
writes servewaf's second log line. The table holds a copy of what a report
needs, never the request, so a report cannot reach a request being handled.

Judgement call: a question's body is read before any check; over 64 KiB gets 400.
Judgement call: a report's bytes and durations have ceilings, so they cannot overflow.
Judgement call: a `close` disposition for SWWAF_BAN_RESPONSE=close.

Model: opus-5-5
2026-11-04 23:07:20 +00:00
4 changed files with 20 additions and 6 deletions
+2 -1
View File
@@ -1198,7 +1198,8 @@ two lines a line is. `docs/request-log.md` gives the fields.
### `servewaf`'s own endpoints
A request without `X-Smallwebwaf-Client` is for `servewaf` itself, at its own
address on the private network. Nothing routes the internet there. These are:
address on the private network. The internet must not be able to reach that
address. Its endpoints are:
- the health check, the metrics, and the ban and client endpoints, as
`docs/admin-endpoints.md` lists them, with the same tokens;
+3 -2
View File
@@ -1,6 +1,7 @@
// Command smallwebwaf is a web application firewall for one app: it runs
// between traefik and the app, and passes requests through within its
// limits. Everything it does is in internal/smallwebwaf.
// between traefik and the app and passes requests through within its limits,
// or, as `smallwebwaf servewaf`, answers rproxy's questions about them.
// Everything it does is in internal/smallwebwaf.
package main
import (
+4 -3
View File
@@ -153,7 +153,8 @@ It has the fields above, with these meanings where they differ:
- `action`: `forward` for `allow`, and `admin` for the `404` for a path under
`/_smallwebwaf/`.
- `aborted`: true when `rproxy` went away before the answer.
- `duration_total`: from the question's headers being read to the line.
- `duration_total`: from `servewaf` having read the question's body to the line.
- `time`: when `servewaf` had read the question's body.
The line leaves out `upstream_status`, `websocket` and the
`duration_upstream_...` fields, since `servewaf` passes nothing to the app.
@@ -163,8 +164,8 @@ holds the client's bytes without this request's.
### The `report` line
It has every field of the `decision` line, and `time` stays when the request
arrived. These fields say what the report says:
It has every field of the `decision` line, and `time` stays as the `decision`
line gives it. These fields say what the report says:
- `status`: what the visitor got, `0` if nothing;
- `request_bytes` and `response_bytes`: the body bytes each way, with what a
+11
View File
@@ -954,6 +954,17 @@ func TestMetricsCountAnAllowedRequestFromItsReport(t *testing.T) {
wantMetric(t, metrics, "smallwebwaf_response_bytes_total"+series, answerBytes)
wantMetric(t, metrics, `smallwebwaf_upstream_duration_seconds_sum{instance="`+
alertInstance+`"}`, 0.004)
// A scrape adds its own time to the request duration when it ends, so this
// check needs a server that has not been scraped yet.
unscraped := startServeWAF(t, "", map[string]string{metricsToken: token})
got = unscraped.report(proxy.Report{
RequestID: unscraped.allowed(client), Status: http.StatusBadGateway, DurationTotal: 5,
})
wantStatus(t, got, http.StatusNoContent)
wantMetric(t, unscraped.metrics(),
`smallwebwaf_request_duration_seconds_sum{instance="`+alertInstance+`"}`, 0.005)
}
func TestReportForARequestNotHeldIsIgnored(t *testing.T) {