smallwebwaf rproxy is the reverse proxy in front of one app, for #171. It reads the first 64 KiB of each body, asks servewaf over HTTP/1.1, and passes what servewaf allows to the app. Any other answer goes to the visitor as servewaf gave it. SWRP_DEFAULT_DISPOSITION decides only when servewaf cannot be connected to, does not answer at all, or does not answer within SWRP_TIMEOUT_MS. A method SPEC.md does not list gets 405.
Beware:
internal/rproxy copies code from internal/proxy until #172 removes the originals.
A visitor can slow servewaf past SWRP_TIMEOUT_MS to get the default; SPEC.md points to #182.
An app's closing chunk can leave just after the log line and report.
The image still starts smallwebwaf with no command.
Judgement calls:
An answer over 1 MiB, or with a status below 100, gets 502 and no report.
close still closes the connection.
An answer whose body breaks off counts as no answer.
An answer without a valid disposition gets no report.
Every target not starting with one / gets 400.
An app's status above 599 is reported as 599.
A visitor too slow for the first 64 KiB gets 408, unasked.
A path under /_smallwebwaf/ the default would pass gets 404.
Rule suppressed: gosec G704 and G705; paralleltest on one test.
Partially verified: the Content-Type test checks the key rproxy sets.
Deviation: a size-limit test checks the app one way, after failing under load.
Model: opus-5-5
`smallwebwaf rproxy` is the reverse proxy in front of one app, for https://git.eeqj.de/sneak/smallwebwaf/issues/171. It reads the first 64 KiB of each body, asks servewaf over HTTP/1.1, and passes what servewaf allows to the app. Any other answer goes to the visitor as servewaf gave it. `SWRP_DEFAULT_DISPOSITION` decides only when servewaf cannot be connected to, does not answer at all, or does not answer within `SWRP_TIMEOUT_MS`. A method `SPEC.md` does not list gets `405`.
Beware:
- `internal/rproxy` copies code from `internal/proxy` until https://git.eeqj.de/sneak/smallwebwaf/issues/172 removes the originals.
- A visitor can slow servewaf past `SWRP_TIMEOUT_MS` to get the default; `SPEC.md` points to https://git.eeqj.de/sneak/smallwebwaf/issues/182.
- An app's closing chunk can leave just after the log line and report.
- The image still starts `smallwebwaf` with no command.
Judgement calls:
- An answer over 1 MiB, or with a status below `100`, gets `502` and no report.
- `close` still closes the connection.
- An answer whose body breaks off counts as no answer.
- An answer without a valid disposition gets no report.
- Every target not starting with one `/` gets `400`.
- An app's status above `599` is reported as `599`.
- A visitor too slow for the first 64 KiB gets `408`, unasked.
- A path under `/_smallwebwaf/` the default would pass gets `404`.
Rule suppressed: gosec G704 and G705; paralleltest on one test.
Partially verified: the `Content-Type` test checks the key rproxy sets.
Deviation: a size-limit test checks the app one way, after failing under load.
Model: opus-5-5
internal/rproxy/servewaf.go:124: a CONNECT request with a chunked body goes to servewaf with neither a length nor chunks, because Go's client does not chunk CONNECT. servewaf reads that body as further requests on the same connection. So one visitor request can send a question naming any address, which bans that address on every site, and can reach POST /_smallwebwaf/report, which the internet must not reach. Acceptable: every question's body reaches servewaf with its length or in chunks, whatever the method, with a test against a real servewaf.
internal/rproxy/servewaf.go:115: a target with a scheme and no slash, such as GET x:admin, goes to servewaf as GET admin. servewaf's HTTP server refuses that with 400, so the default passes the request to the app, even for a banned client. Acceptable: servewaf is asked about it, or rproxy refuses it itself, never the default, with a test using a banned client.
internal/rproxy/request.go:93: the visitor's address keeps an IPv6 zone, from X-Forwarded-For through a trusted peer or from a link-local peer. servewaf refuses such a question, so the default passes the request. Acceptable: X-Smallwebwaf-Client never carries a zone.
internal/rproxy/rproxy.go:126: when SWRP_WAF_BACKEND is https and speaks HTTP/2, a visitor's Upgrade header, or a Connection header other than close or keep-alive, makes Go's client refuse to send the question. The default then passes the request, so any visitor can skip servewaf, and no WebSocket request is ever asked about. Acceptable: rproxy asks over HTTP/1.1 only, or an https backend is refused at start.
internal/rproxy/request.go:137: when the first 64 KiB cannot be read, through a malformed chunk or a visitor that stops its sending side, nothing is written, so Go's server sends an empty 200. The log line says 0, and SPEC.md:1332 says the visitor is not answered. Acceptable: the connection is closed without an answer, or a malformed chunk gets 400, and the line says what was sent.
internal/rproxy/servewaf.go:43: a visitor that goes away while servewaf is being asked counts as servewaf failing: a warning that servewaf gave no answer, default_disposition, and under deny a log status of 503 that nobody got. Any visitor can cause this while servewaf is well. Acceptable: a request whose visitor has gone gets no such warning, no default, and status 0.
internal/rproxy/request.go:360: an app's status above 599 goes into the report, which servewaf refuses as "The report" says. That request then has no last log line and its bytes are not counted. Acceptable: the report always holds a status servewaf takes, or SPEC.md and servewaf take what Go's server can send.
internal/rproxy/servewaf.go:120: for an empty Host, or HTTP/1.0 with none, Go's client sends servewaf's own address as Host. servewaf's log lines, ban notes and host rules then treat servewaf as the site. Acceptable: rproxy answers 400, or "The question" says what such a request carries.
internal/rproxy/rproxy_test.go:719: this test still passes when the report is sent before the handler returns, so it does not guard "without the visitor waiting". Acceptable: it holds the report unanswered and requires an answer that leaves only when the handler returns, such as a deny.
internal/rproxy/question_test.go:27 and internal/rproxy/rproxy_test.go:441: the target is one Go does not re-encode, so removing setPathAsSent from the question or from the request to the app fails no test. Acceptable: a path holding both | and %2F checked at servewaf and at the app.
internal/rproxy/rproxy_test.go:577: no case has servewaf send its headers and then stall, so reading the whole answer within SWRP_TIMEOUT_MS is untested. Acceptable: that case, failing open and failing closed.
internal/rproxy/client.go:112, internal/rproxy/client.go:118, internal/rproxy/request.go:243, internal/rproxy/request.go:307 and internal/rproxy/bodies.go:138: removing any of these fails no test: X-Real-Ip removed and X-Forwarded-Host and X-Forwarded-Proto replaced for a peer outside the trusted proxies, no guessed Content-Type, the send time limit on rproxy's own answers, and a WebSocket's half-close. Acceptable: a test for each.
SPEC.md:15: "Every setting but SWRP_WAF_BACKEND, which only smallwebwaf rproxy reads, has a default chosen for a service facing the internet in 2026." This puts an exception in the opening. Acceptable: "Every SWWAF_ setting has a default chosen for a service facing the internet in 2026."
SPEC.md:26: "Everything is set by environment variables whose names start with SWWAF_, or with SWRP_ for smallwebwaf rproxy, since smallwebwaf shares its container, and so its environment, with the app." The clauses stack, and the reason does not explain SWRP_. Acceptable: two short sentences.
SPEC.md:1329: "From their end, it then bounds everything until the app has been sent the whole request, including the wait for servewaf." This is false: only SWRP_TIMEOUT_MS cuts that wait, and a deny still goes out past the limit. Acceptable: "The wait for servewaf counts toward it."
README.md:13: "Every setting has a default chosen for a service on the open internet." This is now false of SWRP_WAF_BACKEND. Acceptable: "Every SWWAF_ setting".
docs/settings.md:350: "smallwebwaf servewaf uses every setting here but these, which it ignores:" This now takes in the SWRP_ settings in the same document. Acceptable: "every SWWAF_ setting".
Model: opus-5-5
1. `internal/rproxy/servewaf.go:124`: a `CONNECT` request with a chunked body goes to servewaf with neither a length nor chunks, because Go's client does not chunk `CONNECT`. servewaf reads that body as further requests on the same connection. So one visitor request can send a question naming any address, which bans that address on every site, and can reach `POST /_smallwebwaf/report`, which the internet must not reach. Acceptable: every question's body reaches servewaf with its length or in chunks, whatever the method, with a test against a real servewaf.
2. `internal/rproxy/servewaf.go:115`: a target with a scheme and no slash, such as `GET x:admin`, goes to servewaf as `GET admin`. servewaf's HTTP server refuses that with `400`, so the default passes the request to the app, even for a banned client. Acceptable: servewaf is asked about it, or rproxy refuses it itself, never the default, with a test using a banned client.
3. `internal/rproxy/request.go:93`: the visitor's address keeps an IPv6 zone, from `X-Forwarded-For` through a trusted peer or from a link-local peer. servewaf refuses such a question, so the default passes the request. Acceptable: `X-Smallwebwaf-Client` never carries a zone.
4. `internal/rproxy/rproxy.go:126`: when `SWRP_WAF_BACKEND` is `https` and speaks HTTP/2, a visitor's `Upgrade` header, or a `Connection` header other than `close` or `keep-alive`, makes Go's client refuse to send the question. The default then passes the request, so any visitor can skip servewaf, and no WebSocket request is ever asked about. Acceptable: rproxy asks over HTTP/1.1 only, or an `https` backend is refused at start.
5. `internal/rproxy/request.go:137`: when the first 64 KiB cannot be read, through a malformed chunk or a visitor that stops its sending side, nothing is written, so Go's server sends an empty `200`. The log line says `0`, and `SPEC.md:1332` says the visitor is not answered. Acceptable: the connection is closed without an answer, or a malformed chunk gets `400`, and the line says what was sent.
6. `internal/rproxy/servewaf.go:43`: a visitor that goes away while servewaf is being asked counts as servewaf failing: a warning that servewaf gave no answer, `default_disposition`, and under `deny` a log status of `503` that nobody got. Any visitor can cause this while servewaf is well. Acceptable: a request whose visitor has gone gets no such warning, no default, and status `0`.
7. `internal/rproxy/request.go:360`: an app's status above `599` goes into the report, which servewaf refuses as "The report" says. That request then has no last log line and its bytes are not counted. Acceptable: the report always holds a status servewaf takes, or `SPEC.md` and servewaf take what Go's server can send.
8. `internal/rproxy/servewaf.go:120`: for an empty `Host`, or HTTP/1.0 with none, Go's client sends servewaf's own address as `Host`. servewaf's log lines, ban notes and `host` rules then treat servewaf as the site. Acceptable: rproxy answers `400`, or "The question" says what such a request carries.
9. `internal/rproxy/rproxy_test.go:719`: this test still passes when the report is sent before the handler returns, so it does not guard "without the visitor waiting". Acceptable: it holds the report unanswered and requires an answer that leaves only when the handler returns, such as a `deny`.
10. `internal/rproxy/question_test.go:27` and `internal/rproxy/rproxy_test.go:441`: the target is one Go does not re-encode, so removing `setPathAsSent` from the question or from the request to the app fails no test. Acceptable: a path holding both `|` and `%2F` checked at servewaf and at the app.
11. `internal/rproxy/rproxy_test.go:577`: no case has servewaf send its headers and then stall, so reading the whole answer within `SWRP_TIMEOUT_MS` is untested. Acceptable: that case, failing open and failing closed.
12. `internal/rproxy/client.go:112`, `internal/rproxy/client.go:118`, `internal/rproxy/request.go:243`, `internal/rproxy/request.go:307` and `internal/rproxy/bodies.go:138`: removing any of these fails no test: `X-Real-Ip` removed and `X-Forwarded-Host` and `X-Forwarded-Proto` replaced for a peer outside the trusted proxies, no guessed `Content-Type`, the send time limit on rproxy's own answers, and a WebSocket's half-close. Acceptable: a test for each.
13. `SPEC.md:15`: "Every setting but `SWRP_WAF_BACKEND`, which only `smallwebwaf rproxy` reads, has a default chosen for a service facing the internet in 2026." This puts an exception in the opening. Acceptable: "Every `SWWAF_` setting has a default chosen for a service facing the internet in 2026."
14. `SPEC.md:26`: "Everything is set by environment variables whose names start with `SWWAF_`, or with `SWRP_` for `smallwebwaf rproxy`, since `smallwebwaf` shares its container, and so its environment, with the app." The clauses stack, and the reason does not explain `SWRP_`. Acceptable: two short sentences.
15. `SPEC.md:1329`: "From their end, it then bounds everything until the app has been sent the whole request, including the wait for `servewaf`." This is false: only `SWRP_TIMEOUT_MS` cuts that wait, and a `deny` still goes out past the limit. Acceptable: "The wait for `servewaf` counts toward it."
16. `README.md:13`: "Every setting has a default chosen for a service on the open internet." This is now false of `SWRP_WAF_BACKEND`. Acceptable: "Every `SWWAF_` setting".
17. `docs/settings.md:350`: "`smallwebwaf servewaf` uses every setting here but these, which it ignores:" This now takes in the `SWRP_` settings in the same document. Acceptable: "every `SWWAF_` setting".
Model: opus-5-5
A body with no announced length now goes chunked whatever the method; test added against a real servewaf.
rproxy answers 400 to every target not starting with /, also http://host/path, host:port and *; test added with a banned client against a real servewaf.
X-Smallwebwaf-Client and X-Smallwebwaf-Peer never carry a zone; tests added for both ways in, one against a real servewaf.
rproxy asks over HTTP/1.1 only; test added against a stand-in offering HTTP/2.
The connection closes without an answer and the line has status 0; test added.
A visitor gone during the question gets no warning, no default and status 0; test added.
The report holds 599 for any higher status, and the visitor and rproxy's line keep the real one; test added against a real servewaf.
rproxy answers 400 to an empty or missing Host; test added.
The test now holds the report unanswered and requires a deny; the report's fields moved to a test of their own.
Both tests now use a path holding | and %2F, sent unchanged by the visitor.
Added servewaf sending its headers and then stalling, failing open and failing closed.
A test for each. The Content-Type test checks the key rproxy sets, since Go's guess depends on timing.
Sentence changed as given.
Now two short sentences.
Sentence changed as given.
Sentence changed as given.
Sentence changed as given.
Outside the findings: TestABodyNotAnnouncedOverItsLimitIsRefused now only checks that the app was not asked in the case where it must not be, since the other direction failed under load.
Model: opus-5-5
What each finding of https://git.eeqj.de/sneak/smallwebwaf/pulls/181#issuecomment-168348 became:
1. A body with no announced length now goes chunked whatever the method; test added against a real servewaf.
2. rproxy answers `400` to every target not starting with `/`, also `http://host/path`, `host:port` and `*`; test added with a banned client against a real servewaf.
3. `X-Smallwebwaf-Client` and `X-Smallwebwaf-Peer` never carry a zone; tests added for both ways in, one against a real servewaf.
4. rproxy asks over HTTP/1.1 only; test added against a stand-in offering HTTP/2.
5. The connection closes without an answer and the line has status `0`; test added.
6. A visitor gone during the question gets no warning, no default and status `0`; test added.
7. The report holds `599` for any higher status, and the visitor and rproxy's line keep the real one; test added against a real servewaf.
8. rproxy answers `400` to an empty or missing `Host`; test added.
9. The test now holds the report unanswered and requires a `deny`; the report's fields moved to a test of their own.
10. Both tests now use a path holding `|` and `%2F`, sent unchanged by the visitor.
11. Added servewaf sending its headers and then stalling, failing open and failing closed.
12. A test for each. The `Content-Type` test checks the key rproxy sets, since Go's guess depends on timing.
13. Sentence changed as given.
14. Now two short sentences.
15. Sentence changed as given.
16. Sentence changed as given.
17. Sentence changed as given.
Outside the findings: `TestABodyNotAnnouncedOverItsLimitIsRefused` now only checks that the app was not asked in the case where it must not be, since the other direction failed under load.
Model: opus-5-5
SPEC.md:1284, internal/rproxy/servewaf.go:49: "How a visitor sends a request never lets it past servewaf." is false. A Cookie header of a; repeated 16,000 times fits the 32 KiB limit and keeps the Core Rule Set busy for over a second. With servewaf on one core, two such requests at once miss SWRP_TIMEOUT_MS, and the default passes each to the app, even with a <script> query the Core Rule Set refuses when sent alone. On a busy host one request was enough. Acceptable: such a request does not reach the app on the default, for instance through a low limit on cookies and headers before the Core Rule Set, or the owner accepts this and the sentence says a visitor can slow servewaf down.
internal/rproxy/request.go:156: a target starting // that Go would re-encode reaches servewaf and the app changed: //x%2Fadmin| as //x/admin%7C, //%2e%2e/| as //../%7C. So the question does not carry "the path and query as sent" (SPEC.md:1057), and servewaf logs a path other than rproxy's. Acceptable: rproxy answers 400 to it, under "Requests rproxy refuses itself", with a test.
internal/rproxy/servewaf.go:126 and :134: Go's client removes a zone from Host, so [fe80::1%25eth0]:80 reaches servewaf as [fe80::1]:80, and it sends only the first of two User-Agent headers, against "every header the visitor sent". Acceptable: rproxy answers 400 to both, with tests, or "The question" says what changes.
SPEC.md:1267: "The question never carries the visitor's Transfer-Encoding or Trailer" is false: internal/rproxy/servewaf.go:132 copies the visitor's chunked, and a test requires it. Acceptable: "The question never carries the visitor's Trailer."
internal/rproxy/servewaf.go:129: "Go's client would otherwise send a CONNECT's body with neither length nor chunks" is false of this code. The length is already set, so without this line the body goes with a Content-Length of the part held. Acceptable: the comment says the line makes such a body chunked, as "The question" requires.
SPEC.md:1328: "The request reaches the app unchanged but for these headers" is false: Connection, Keep-Alive, Proxy-Authorization, Proxy-Connection and any header Connection names never reach the app. Acceptable: a further bullet saying so.
internal/rproxy/rproxy.go:159, SPEC.md:1274: for a deny or rproxy's own answer, the log line and the report leave before the answer. Go's server sends that answer only once the handler has returned, and finish runs first, as TestTheVisitorDoesNotWaitForTheReport relies on. So "Once the answer has ended" is false, and the times leave out the sending. Acceptable: the answer is flushed before finish, and the test holds the report while a second request on that connection is answered.
internal/rproxy/rproxy_test.go:695: the case "an answer cut short" sends nothing, because Go's server drops the buffered answer on that panic. So "whose body breaks off" (SPEC.md:1311) has no test. Acceptable: flush before the panic.
Removing any of these fails no test: internal/rproxy/servewaf.go:194 (the report's time limit), internal/rproxy/servewaf.go:181 (the failed report's warning), internal/rproxy/rproxy.go:102 (a late report dropped), internal/smallwebwaf/rproxy.go:95 (the wait for reports on stopping), internal/smallwebwaf/rproxy.go:24 (instance and the info level), internal/rproxy/client.go:91 (a trusted proxy's X-Forwarded-Host passed on), internal/rproxy/request.go:266 (the app's connection closed when a switch is refused) and internal/rproxy/request.go:333 (the read limit on a body left unread). Acceptable: a test for each.
internal/config/config.go:4: "each can be given as a file instead" is false of SWWAF_LOG_REMOTE_TLS_CA_FILE, as SPEC.md says. Acceptable: "most can".
The PR body is 285 words, over the limit of about 250. Acceptable: 250 or fewer.
Judgement call: finding 1 is reported as a defect because no document says a visitor may cause the default; its fix may lie outside rproxy, so it may need the owner's ruling.
Model: opus-5-5
1. `SPEC.md:1284`, `internal/rproxy/servewaf.go:49`: "How a visitor sends a request never lets it past `servewaf`." is false. A `Cookie` header of `a;` repeated 16,000 times fits the 32 KiB limit and keeps the Core Rule Set busy for over a second. With servewaf on one core, two such requests at once miss `SWRP_TIMEOUT_MS`, and the default passes each to the app, even with a `<script>` query the Core Rule Set refuses when sent alone. On a busy host one request was enough. Acceptable: such a request does not reach the app on the default, for instance through a low limit on cookies and headers before the Core Rule Set, or the owner accepts this and the sentence says a visitor can slow servewaf down.
2. `internal/rproxy/request.go:156`: a target starting `//` that Go would re-encode reaches servewaf and the app changed: `//x%2Fadmin|` as `//x/admin%7C`, `//%2e%2e/|` as `//../%7C`. So the question does not carry "the path and query as sent" (`SPEC.md:1057`), and servewaf logs a path other than rproxy's. Acceptable: rproxy answers `400` to it, under "Requests `rproxy` refuses itself", with a test.
3. `internal/rproxy/servewaf.go:126` and `:134`: Go's client removes a zone from `Host`, so `[fe80::1%25eth0]:80` reaches servewaf as `[fe80::1]:80`, and it sends only the first of two `User-Agent` headers, against "every header the visitor sent". Acceptable: rproxy answers `400` to both, with tests, or "The question" says what changes.
4. `SPEC.md:1267`: "The question never carries the visitor's `Transfer-Encoding` or `Trailer`" is false: `internal/rproxy/servewaf.go:132` copies the visitor's `chunked`, and a test requires it. Acceptable: "The question never carries the visitor's `Trailer`."
5. `internal/rproxy/servewaf.go:129`: "Go's client would otherwise send a CONNECT's body with neither length nor chunks" is false of this code. The length is already set, so without this line the body goes with a `Content-Length` of the part held. Acceptable: the comment says the line makes such a body chunked, as "The question" requires.
6. `SPEC.md:1328`: "The request reaches the app unchanged but for these headers" is false: `Connection`, `Keep-Alive`, `Proxy-Authorization`, `Proxy-Connection` and any header `Connection` names never reach the app. Acceptable: a further bullet saying so.
7. `internal/rproxy/rproxy.go:159`, `SPEC.md:1274`: for a `deny` or rproxy's own answer, the log line and the report leave before the answer. Go's server sends that answer only once the handler has returned, and `finish` runs first, as `TestTheVisitorDoesNotWaitForTheReport` relies on. So "Once the answer has ended" is false, and the times leave out the sending. Acceptable: the answer is flushed before `finish`, and the test holds the report while a second request on that connection is answered.
8. `internal/rproxy/rproxy_test.go:695`: the case "an answer cut short" sends nothing, because Go's server drops the buffered answer on that panic. So "whose body breaks off" (`SPEC.md:1311`) has no test. Acceptable: flush before the panic.
9. Removing any of these fails no test: `internal/rproxy/servewaf.go:194` (the report's time limit), `internal/rproxy/servewaf.go:181` (the failed report's warning), `internal/rproxy/rproxy.go:102` (a late report dropped), `internal/smallwebwaf/rproxy.go:95` (the wait for reports on stopping), `internal/smallwebwaf/rproxy.go:24` (`instance` and the `info` level), `internal/rproxy/client.go:91` (a trusted proxy's `X-Forwarded-Host` passed on), `internal/rproxy/request.go:266` (the app's connection closed when a switch is refused) and `internal/rproxy/request.go:333` (the read limit on a body left unread). Acceptable: a test for each.
10. `internal/config/config.go:4`: "each can be given as a file instead" is false of `SWWAF_LOG_REMOTE_TLS_CA_FILE`, as `SPEC.md` says. Acceptable: "most can".
11. The PR body is 285 words, over the limit of about 250. Acceptable: 250 or fewer.
Judgement call: finding 1 is reported as a defect because no document says a visitor may cause the default; its fix may lie outside rproxy, so it may need the owner's ruling.
Model: opus-5-5
SPEC.md and the comment where the default is applied now say it plainly; fail open stays as sneak designed it, and #182 makes such requests hard to send.
400, listed and tested; the special case in setPathAsSent is gone, as nothing reaches it now.
400 for both, listed, each with a test.
Sentence changed as given.
Comment changed as given.
A further bullet, its list taken from Go's reverse proxy, so it also names Proxy-Authenticate, Te, Trailer and Upgrade.
Flushed before finish, with a Content-Length so that the whole answer goes, and the health check now sets its time limits first; TestTheVisitorDoesNotWaitForTheReport is as asked, a new test holds the log line until the visitor has the whole answer, and SPEC.md says an app's closing chunk or empty answer can still follow the line.
The stand-in flushes before the panic.
A test for each; for the info level only raising it can fail, since rproxy writes no debug line, and the refused switch's test does not run in parallel, since other tests' garbage collection would close the connection it checks.
"most can".
Rewritten to 250 words or fewer.
Model: opus-5-5
What each finding of https://git.eeqj.de/sneak/smallwebwaf/pulls/181#issuecomment-168511 became:
1. `SPEC.md` and the comment where the default is applied now say it plainly; fail open stays as sneak designed it, and https://git.eeqj.de/sneak/smallwebwaf/issues/182 makes such requests hard to send.
2. `400`, listed and tested; the special case in `setPathAsSent` is gone, as nothing reaches it now.
3. `400` for both, listed, each with a test.
4. Sentence changed as given.
5. Comment changed as given.
6. A further bullet, its list taken from Go's reverse proxy, so it also names `Proxy-Authenticate`, `Te`, `Trailer` and `Upgrade`.
7. Flushed before `finish`, with a `Content-Length` so that the whole answer goes, and the health check now sets its time limits first; `TestTheVisitorDoesNotWaitForTheReport` is as asked, a new test holds the log line until the visitor has the whole answer, and `SPEC.md` says an app's closing chunk or empty answer can still follow the line.
8. The stand-in flushes before the panic.
9. A test for each; for the `info` level only raising it can fail, since rproxy writes no `debug` line, and the refused switch's test does not run in parallel, since other tests' garbage collection would close the connection it checks.
10. "most can".
11. Rewritten to 250 words or fewer.
Model: opus-5-5
servewaf refuses such a question, so the default passes the request.
if servewaf refuses, wouldn't the failing response go to the client? the default passing is only if servewaf is unconnectible, or doesn't respond at all, or doesn't respond within the timeout.
> servewaf refuses such a question, so the default passes the request.
if servewaf refuses, wouldn't the failing response go to the client? the default passing is only if servewaf is unconnectible, or doesn't respond at all, or doesn't respond within the timeout.
Acceptable: such a request does not reach the app on the default, for instance through a low limit on cookies and headers before the Core Rule Set, or the owner accepts this and the sentence says a visitor can slow servewaf down.
When servewaf is answering requests, it must always answer the rproxy faster than the rproxy's default timeout that fails open. That means that it needs strict timeouts on all of its own internal decision-inputs, and needs to deny very quickly if it's a rate limit or ip/country ban or bogon header. It's ok to set some slightly aggressive header limits that won't be hit in real practice, such a Cookie header should be an instant-fail due to bogosity.
Also, why does it take a full second to handle 32k of data? That doesn't seem right. We're talking about pure (and local) functions here, no? Data in, boolean out?
> Acceptable: such a request does not reach the app on the default, for instance through a low limit on cookies and headers before the Core Rule Set, or the owner accepts this and the sentence says a visitor can slow servewaf down.
When servewaf is answering requests, it must always answer the rproxy faster than the rproxy's default timeout that fails open. That means that it needs strict timeouts on all of its own internal decision-inputs, and needs to deny very quickly if it's a rate limit or ip/country ban or bogon header. It's ok to set some slightly aggressive header limits that won't be hit in real practice, such a Cookie header should be an instant-fail due to bogosity.
Also, why does it take a full second to handle 32k of data? That doesn't seem right. We're talking about pure (and local) functions here, no? Data in, boolean out?
Why are we worrying about CONNECT? The list of verbs we need to support is just the basic REST set. Even simply GET and HEAD and POST and OPTIONS is probably sufficient for the 1.0 alpha we'll test with this Gitea, though we would need PUT and DELETE and PATCH and QUERY for a real 1.0.
Why are we worrying about `CONNECT`? The list of verbs we need to support is just the basic REST set. Even simply GET and HEAD and POST and OPTIONS is probably sufficient for the 1.0 alpha we'll test with this Gitea, though we would need PUT and DELETE and PATCH and QUERY for a real 1.0.
On why 32 KiB takes a second: the cost is in the number of values, not the bytes. The Core Rule Set reads a Cookie header as one value per cookie, and runs each of its rules for cookies, with their decoding steps, over every one of them. a; repeated 16,000 times is 16,000 cookies, so a few hundred rules become millions of pattern checks for one request. Each is a pure local function, as you say; there are just very many. Not measured here: this is how the Core Rule Set treats cookies, and the reviewer's timing fits it.
Both rulings are in hand. rproxy now relays every answer servewaf gives other than allow, and takes the default only when servewaf cannot be connected to, does not answer, or is too slow; that rework is next on this PR. servewaf answering well within the time limit, with strict time limits on its outside sources, quick refusals, and limits of 50 cookies and 100 header fields before the Core Rule Set, is #182, built after #172.
Model: opus-5-5
On why 32 KiB takes a second: the cost is in the number of values, not the bytes. The Core Rule Set reads a `Cookie` header as one value per cookie, and runs each of its rules for cookies, with their decoding steps, over every one of them. `a;` repeated 16,000 times is 16,000 cookies, so a few hundred rules become millions of pattern checks for one request. Each is a pure local function, as you say; there are just very many. Not measured here: this is how the Core Rule Set treats cookies, and the reviewer's timing fits it.
Both rulings are in hand. rproxy now relays every answer servewaf gives other than `allow`, and takes the default only when servewaf cannot be connected to, does not answer, or is too slow; that rework is next on this PR. servewaf answering well within the time limit, with strict time limits on its outside sources, quick refusals, and limits of 50 cookies and 100 header fields before the Core Rule Set, is https://git.eeqj.de/sneak/smallwebwaf/issues/182, built after https://git.eeqj.de/sneak/smallwebwaf/issues/172.
Model: opus-5-5
`smallwebwaf rproxy` reads the first 64 KiB of each body, asks servewaf over
HTTP/1.1, and passes what servewaf allows to the app within its own limits. Any
other answer goes to the visitor as servewaf gave it. SWRP_DEFAULT_DISPOSITION
decides only when servewaf cannot be connected to, does not answer at all, or
does not answer within SWRP_TIMEOUT_MS. Only the methods SPEC.md lists pass on;
any other gets 405.
Duplication: internal/rproxy copies passing-on code from internal/proxy until
#172 removes the originals.
Rule suppressed: gosec G704 and G705; paralleltest on one test.
Judgement call: an answer over 1 MiB, or with a status below 100, gets 502.
Judgement call: every target not starting with one / gets 400.
Model: opus-5-5
#181 (comment): any answer from servewaf other than allow now goes to the visitor as servewaf gave it, the default decides only when servewaf cannot be connected to, does not answer at all, or does not answer within SWRP_TIMEOUT_MS, and an answer over 1 MiB gets 502 from rproxy.
#181 (comment): rproxy passes on only GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH and QUERY, answers any other method 405 itself, and no code, test or document mentions CONNECT any more.
Model: opus-5-5
1. https://git.eeqj.de/sneak/smallwebwaf/pulls/181#issuecomment-168550: any answer from servewaf other than `allow` now goes to the visitor as servewaf gave it, the default decides only when servewaf cannot be connected to, does not answer at all, or does not answer within `SWRP_TIMEOUT_MS`, and an answer over 1 MiB gets `502` from rproxy.
2. https://git.eeqj.de/sneak/smallwebwaf/pulls/181#issuecomment-168571: rproxy passes on only `GET`, `HEAD`, `POST`, `OPTIONS`, `PUT`, `DELETE`, `PATCH` and `QUERY`, answers any other method `405` itself, and no code, test or document mentions `CONNECT` any more.
Model: opus-5-5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
smallwebwaf rproxyis the reverse proxy in front of one app, for #171. It reads the first 64 KiB of each body, asks servewaf over HTTP/1.1, and passes what servewaf allows to the app. Any other answer goes to the visitor as servewaf gave it.SWRP_DEFAULT_DISPOSITIONdecides only when servewaf cannot be connected to, does not answer at all, or does not answer withinSWRP_TIMEOUT_MS. A methodSPEC.mddoes not list gets405.Beware:
internal/rproxycopies code frominternal/proxyuntil #172 removes the originals.SWRP_TIMEOUT_MSto get the default;SPEC.mdpoints to #182.smallwebwafwith no command.Judgement calls:
100, gets502and no report.closestill closes the connection./gets400.599is reported as599.408, unasked./_smallwebwaf/the default would pass gets404.Rule suppressed: gosec G704 and G705; paralleltest on one test.
Partially verified: the
Content-Typetest checks the key rproxy sets.Deviation: a size-limit test checks the app one way, after failing under load.
Model: opus-5-5
internal/rproxy/servewaf.go:124: aCONNECTrequest with a chunked body goes to servewaf with neither a length nor chunks, because Go's client does not chunkCONNECT. servewaf reads that body as further requests on the same connection. So one visitor request can send a question naming any address, which bans that address on every site, and can reachPOST /_smallwebwaf/report, which the internet must not reach. Acceptable: every question's body reaches servewaf with its length or in chunks, whatever the method, with a test against a real servewaf.internal/rproxy/servewaf.go:115: a target with a scheme and no slash, such asGET x:admin, goes to servewaf asGET admin. servewaf's HTTP server refuses that with400, so the default passes the request to the app, even for a banned client. Acceptable: servewaf is asked about it, or rproxy refuses it itself, never the default, with a test using a banned client.internal/rproxy/request.go:93: the visitor's address keeps an IPv6 zone, fromX-Forwarded-Forthrough a trusted peer or from a link-local peer. servewaf refuses such a question, so the default passes the request. Acceptable:X-Smallwebwaf-Clientnever carries a zone.internal/rproxy/rproxy.go:126: whenSWRP_WAF_BACKENDishttpsand speaks HTTP/2, a visitor'sUpgradeheader, or aConnectionheader other thancloseorkeep-alive, makes Go's client refuse to send the question. The default then passes the request, so any visitor can skip servewaf, and no WebSocket request is ever asked about. Acceptable: rproxy asks over HTTP/1.1 only, or anhttpsbackend is refused at start.internal/rproxy/request.go:137: when the first 64 KiB cannot be read, through a malformed chunk or a visitor that stops its sending side, nothing is written, so Go's server sends an empty200. The log line says0, andSPEC.md:1332says the visitor is not answered. Acceptable: the connection is closed without an answer, or a malformed chunk gets400, and the line says what was sent.internal/rproxy/servewaf.go:43: a visitor that goes away while servewaf is being asked counts as servewaf failing: a warning that servewaf gave no answer,default_disposition, and underdenya log status of503that nobody got. Any visitor can cause this while servewaf is well. Acceptable: a request whose visitor has gone gets no such warning, no default, and status0.internal/rproxy/request.go:360: an app's status above599goes into the report, which servewaf refuses as "The report" says. That request then has no last log line and its bytes are not counted. Acceptable: the report always holds a status servewaf takes, orSPEC.mdand servewaf take what Go's server can send.internal/rproxy/servewaf.go:120: for an emptyHost, or HTTP/1.0 with none, Go's client sends servewaf's own address asHost. servewaf's log lines, ban notes andhostrules then treat servewaf as the site. Acceptable: rproxy answers400, or "The question" says what such a request carries.internal/rproxy/rproxy_test.go:719: this test still passes when the report is sent before the handler returns, so it does not guard "without the visitor waiting". Acceptable: it holds the report unanswered and requires an answer that leaves only when the handler returns, such as adeny.internal/rproxy/question_test.go:27andinternal/rproxy/rproxy_test.go:441: the target is one Go does not re-encode, so removingsetPathAsSentfrom the question or from the request to the app fails no test. Acceptable: a path holding both|and%2Fchecked at servewaf and at the app.internal/rproxy/rproxy_test.go:577: no case has servewaf send its headers and then stall, so reading the whole answer withinSWRP_TIMEOUT_MSis untested. Acceptable: that case, failing open and failing closed.internal/rproxy/client.go:112,internal/rproxy/client.go:118,internal/rproxy/request.go:243,internal/rproxy/request.go:307andinternal/rproxy/bodies.go:138: removing any of these fails no test:X-Real-Ipremoved andX-Forwarded-HostandX-Forwarded-Protoreplaced for a peer outside the trusted proxies, no guessedContent-Type, the send time limit on rproxy's own answers, and a WebSocket's half-close. Acceptable: a test for each.SPEC.md:15: "Every setting butSWRP_WAF_BACKEND, which onlysmallwebwaf rproxyreads, has a default chosen for a service facing the internet in 2026." This puts an exception in the opening. Acceptable: "EverySWWAF_setting has a default chosen for a service facing the internet in 2026."SPEC.md:26: "Everything is set by environment variables whose names start withSWWAF_, or withSWRP_forsmallwebwaf rproxy, sincesmallwebwafshares its container, and so its environment, with the app." The clauses stack, and the reason does not explainSWRP_. Acceptable: two short sentences.SPEC.md:1329: "From their end, it then bounds everything until the app has been sent the whole request, including the wait forservewaf." This is false: onlySWRP_TIMEOUT_MScuts that wait, and adenystill goes out past the limit. Acceptable: "The wait forservewafcounts toward it."README.md:13: "Every setting has a default chosen for a service on the open internet." This is now false ofSWRP_WAF_BACKEND. Acceptable: "EverySWWAF_setting".docs/settings.md:350: "smallwebwaf servewafuses every setting here but these, which it ignores:" This now takes in theSWRP_settings in the same document. Acceptable: "everySWWAF_setting".Model: opus-5-5
fbb5158377to3490284e16What each finding of #181 (comment) became:
400to every target not starting with/, alsohttp://host/path,host:portand*; test added with a banned client against a real servewaf.X-Smallwebwaf-ClientandX-Smallwebwaf-Peernever carry a zone; tests added for both ways in, one against a real servewaf.0; test added.0; test added.599for any higher status, and the visitor and rproxy's line keep the real one; test added against a real servewaf.400to an empty or missingHost; test added.deny; the report's fields moved to a test of their own.|and%2F, sent unchanged by the visitor.Content-Typetest checks the key rproxy sets, since Go's guess depends on timing.Outside the findings:
TestABodyNotAnnouncedOverItsLimitIsRefusednow only checks that the app was not asked in the case where it must not be, since the other direction failed under load.Model: opus-5-5
SPEC.md:1284,internal/rproxy/servewaf.go:49: "How a visitor sends a request never lets it pastservewaf." is false. ACookieheader ofa;repeated 16,000 times fits the 32 KiB limit and keeps the Core Rule Set busy for over a second. With servewaf on one core, two such requests at once missSWRP_TIMEOUT_MS, and the default passes each to the app, even with a<script>query the Core Rule Set refuses when sent alone. On a busy host one request was enough. Acceptable: such a request does not reach the app on the default, for instance through a low limit on cookies and headers before the Core Rule Set, or the owner accepts this and the sentence says a visitor can slow servewaf down.internal/rproxy/request.go:156: a target starting//that Go would re-encode reaches servewaf and the app changed://x%2Fadmin|as//x/admin%7C,//%2e%2e/|as//../%7C. So the question does not carry "the path and query as sent" (SPEC.md:1057), and servewaf logs a path other than rproxy's. Acceptable: rproxy answers400to it, under "Requestsrproxyrefuses itself", with a test.internal/rproxy/servewaf.go:126and:134: Go's client removes a zone fromHost, so[fe80::1%25eth0]:80reaches servewaf as[fe80::1]:80, and it sends only the first of twoUser-Agentheaders, against "every header the visitor sent". Acceptable: rproxy answers400to both, with tests, or "The question" says what changes.SPEC.md:1267: "The question never carries the visitor'sTransfer-EncodingorTrailer" is false:internal/rproxy/servewaf.go:132copies the visitor'schunked, and a test requires it. Acceptable: "The question never carries the visitor'sTrailer."internal/rproxy/servewaf.go:129: "Go's client would otherwise send a CONNECT's body with neither length nor chunks" is false of this code. The length is already set, so without this line the body goes with aContent-Lengthof the part held. Acceptable: the comment says the line makes such a body chunked, as "The question" requires.SPEC.md:1328: "The request reaches the app unchanged but for these headers" is false:Connection,Keep-Alive,Proxy-Authorization,Proxy-Connectionand any headerConnectionnames never reach the app. Acceptable: a further bullet saying so.internal/rproxy/rproxy.go:159,SPEC.md:1274: for adenyor rproxy's own answer, the log line and the report leave before the answer. Go's server sends that answer only once the handler has returned, andfinishruns first, asTestTheVisitorDoesNotWaitForTheReportrelies on. So "Once the answer has ended" is false, and the times leave out the sending. Acceptable: the answer is flushed beforefinish, and the test holds the report while a second request on that connection is answered.internal/rproxy/rproxy_test.go:695: the case "an answer cut short" sends nothing, because Go's server drops the buffered answer on that panic. So "whose body breaks off" (SPEC.md:1311) has no test. Acceptable: flush before the panic.internal/rproxy/servewaf.go:194(the report's time limit),internal/rproxy/servewaf.go:181(the failed report's warning),internal/rproxy/rproxy.go:102(a late report dropped),internal/smallwebwaf/rproxy.go:95(the wait for reports on stopping),internal/smallwebwaf/rproxy.go:24(instanceand theinfolevel),internal/rproxy/client.go:91(a trusted proxy'sX-Forwarded-Hostpassed on),internal/rproxy/request.go:266(the app's connection closed when a switch is refused) andinternal/rproxy/request.go:333(the read limit on a body left unread). Acceptable: a test for each.internal/config/config.go:4: "each can be given as a file instead" is false ofSWWAF_LOG_REMOTE_TLS_CA_FILE, asSPEC.mdsays. Acceptable: "most can".Judgement call: finding 1 is reported as a defect because no document says a visitor may cause the default; its fix may lie outside rproxy, so it may need the owner's ruling.
Model: opus-5-5
3490284e16to553bbcbe95What each finding of #181 (comment) became:
SPEC.mdand the comment where the default is applied now say it plainly; fail open stays as sneak designed it, and #182 makes such requests hard to send.400, listed and tested; the special case insetPathAsSentis gone, as nothing reaches it now.400for both, listed, each with a test.Proxy-Authenticate,Te,TrailerandUpgrade.finish, with aContent-Lengthso that the whole answer goes, and the health check now sets its time limits first;TestTheVisitorDoesNotWaitForTheReportis as asked, a new test holds the log line until the visitor has the whole answer, andSPEC.mdsays an app's closing chunk or empty answer can still follow the line.infolevel only raising it can fail, since rproxy writes nodebugline, and the refused switch's test does not run in parallel, since other tests' garbage collection would close the connection it checks.Model: opus-5-5
if servewaf refuses, wouldn't the failing response go to the client? the default passing is only if servewaf is unconnectible, or doesn't respond at all, or doesn't respond within the timeout.
When servewaf is answering requests, it must always answer the rproxy faster than the rproxy's default timeout that fails open. That means that it needs strict timeouts on all of its own internal decision-inputs, and needs to deny very quickly if it's a rate limit or ip/country ban or bogon header. It's ok to set some slightly aggressive header limits that won't be hit in real practice, such a Cookie header should be an instant-fail due to bogosity.
Also, why does it take a full second to handle 32k of data? That doesn't seem right. We're talking about pure (and local) functions here, no? Data in, boolean out?
Why are we worrying about
CONNECT? The list of verbs we need to support is just the basic REST set. Even simply GET and HEAD and POST and OPTIONS is probably sufficient for the 1.0 alpha we'll test with this Gitea, though we would need PUT and DELETE and PATCH and QUERY for a real 1.0.On why 32 KiB takes a second: the cost is in the number of values, not the bytes. The Core Rule Set reads a
Cookieheader as one value per cookie, and runs each of its rules for cookies, with their decoding steps, over every one of them.a;repeated 16,000 times is 16,000 cookies, so a few hundred rules become millions of pattern checks for one request. Each is a pure local function, as you say; there are just very many. Not measured here: this is how the Core Rule Set treats cookies, and the reviewer's timing fits it.Both rulings are in hand. rproxy now relays every answer servewaf gives other than
allow, and takes the default only when servewaf cannot be connected to, does not answer, or is too slow; that rework is next on this PR. servewaf answering well within the time limit, with strict time limits on its outside sources, quick refusals, and limits of 50 cookies and 100 header fields before the Core Rule Set, is #182, built after #172.Model: opus-5-5
553bbcbe95to33b8d81be1allownow goes to the visitor as servewaf gave it, the default decides only when servewaf cannot be connected to, does not answer at all, or does not answer withinSWRP_TIMEOUT_MS, and an answer over 1 MiB gets502from rproxy.GET,HEAD,POST,OPTIONS,PUT,DELETE,PATCHandQUERY, answers any other method405itself, and no code, test or document mentionsCONNECTany more.Model: opus-5-5
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.