servewaf answers rproxy's question about each request and takes its report (closes #170)
check / check (push) Successful in 4m27s

`smallwebwaf servewaf` runs today's checks on the visitor's request a
question carries and answers allow, deny or close, instead of passing it
to an app. rproxy's report then counts the bytes toward the byte limits
and adds the request to the client's history. SPEC.md gives the exact
question, answer and report, which rproxy follows. `smallwebwaf` with no
command is unchanged.

Judgement call: a third disposition, close, since closing the question's
connection would read as servewaf down and let the request through.
Judgement call: allow can answer a new request id, since a visitor behind
a trusted proxy can choose its own.

Model: opus-5-5
This commit is contained in:
2026-11-04 19:41:12 +00:00
parent fdabf4974b
commit ed6ba53747
25 changed files with 1673 additions and 85 deletions
+29 -9
View File
@@ -53,6 +53,11 @@ type request struct {
client netip.Addr
peer netip.Addr
peerTrusted bool
// site is, in servewaf, the host the visitor asked for, which the
// alerts name, since one servewaf serves many sites. It is "" otherwise.
site string
// allowed is true for a question answered with DispositionAllow.
allowed bool
// lookedUp is true once the client's AS number and country have been
// looked up, whether or not an answer was there, and lookupAnswer is
// what the lookup gave then, the zero Answer while GeoJS had given none.
@@ -108,18 +113,27 @@ type request struct {
answerStarted time.Time
}
// newRequest starts handling r: it notes the time, counts the request as
// under way, works out the client, and starts the log line with what is
// known of the request.
// newRequest starts handling r: it works out the client from the TCP peer
// and X-Forwarded-For, and goes on as startRequest does.
func (h *handler) newRequest(w http.ResponseWriter, r *http.Request) *request {
peer := peerAddress(r)
trusted := h.config.TrustedProxies
client := clientAddress(peer, r.Header.Values("X-Forwarded-For"), trusted)
return h.startRequest(w, r, client, peer, isInside(peer, trusted))
}
// startRequest starts handling r from client, which came through peer: it
// notes the time, counts the request as under way, and starts the log line
// with what is known of the request. peerTrusted says whether to believe
// the request's id and scheme, which r's headers give.
func (h *handler) startRequest(
w http.ResponseWriter, r *http.Request, client, peer netip.Addr, peerTrusted bool,
) *request {
h.metrics.RequestStarted()
start := time.Now()
peer := peerAddress(r)
trusted := h.config.TrustedProxies
peerTrusted := isInside(peer, trusted)
forwardedFor := r.Header.Values("X-Forwarded-For")
client := clientAddress(peer, forwardedFor, trusted)
rq := &request{
h: h,
@@ -569,8 +583,13 @@ func (rq *request) finish() {
}
// Counted before the log line is written, so that the metrics count
// every request whose line is out.
rq.h.metrics.RequestEnded(line, limit, duration, upstreamDuration)
// every request whose line is out. An allowed request is the exception:
// its report gives the status, bytes and times the metrics count.
if rq.allowed {
rq.h.metrics.RequestAllowed()
} else {
rq.h.metrics.RequestEnded(line, limit, duration, upstreamDuration)
}
err := requestlog.Write(rq.h.requestLog, line)
if err != nil {
@@ -634,6 +653,7 @@ func (rq *request) countAnomalies() {
answer, _ := rq.answerAtTheEnd()
rq.h.anomalies.Count(rq.h.now(), anomaly.Request{
Host: rq.site,
Client: rq.client,
ClientGroup: rq.h.clientGroup(rq.client),
ASN: answer.ASN,