rproxy, the proxy in front of each app, asks servewaf and fails open (closes #171) #181

Open
clawbot wants to merge 1 commits from issue-171-rproxy into next
Collaborator

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
clawbot added the needs-review label 2026-11-05 01:09:12 +01:00
clawbot self-assigned this 2026-11-05 01:09:12 +01:00
Author
Collaborator
  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

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
clawbot added needs-rework and removed needs-review labels 2026-11-05 01:48:48 +01:00
clawbot force-pushed issue-171-rproxy from fbb5158377 to 3490284e16 2026-11-05 02:12:45 +01:00 Compare
Author
Collaborator

What each finding of #181 (comment) 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

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
clawbot added needs-review and removed needs-rework labels 2026-11-05 02:12:51 +01:00
Author
Collaborator
  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

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
clawbot added needs-rework and removed needs-review labels 2026-11-05 02:49:28 +01:00
clawbot force-pushed issue-171-rproxy from 3490284e16 to 553bbcbe95 2026-11-05 03:13:10 +01:00 Compare
Author
Collaborator

What each finding of #181 (comment) became:

  1. 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.
  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

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
clawbot added needs-review and removed needs-rework labels 2026-11-05 03:13:20 +01:00
Owner

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.
Owner

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?
Owner

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.
Author
Collaborator

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
clawbot added needs-rework and removed needs-review labels 2026-11-05 03:53:57 +01:00
clawbot added 1 commit 2026-11-05 04:09:38 +01:00
`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
clawbot force-pushed issue-171-rproxy from 553bbcbe95 to 33b8d81be1 2026-11-05 04:09:38 +01:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-11-05 04:09:47 +01:00
Author
Collaborator
  1. #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.
  2. #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
All checks were successful
check / check (push) Successful in 4m26s
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-171-rproxy:issue-171-rproxy
git checkout issue-171-rproxy
Sign in to join this conversation.