ReviewOS

zig-utils/zig-waf

[WAF-40] Implement the HTTP/1.1 reverse proxy

#41
Closed chris opened this 9 days ago ·
9 days ago

Objective

Implement keep-alive, chunking, trailers, upgrades, SSE, body streaming, hop-by-hop handling, smuggling defenses, upstream reuse, and bounded connection state.

Deliverables

  • Implement the complete scoped behavior described above.
  • Keep request-path work independent from UI, PostgreSQL, and blocking audit I/O.
  • Use Zig 0.17-dev and dependencies pinned through Pantry; do not add Git submodules.
  • Keep public APIs explicit about ownership, resource limits, errors, security policy, and compatibility.
  • Add focused unit, integration, differential, fuzz, benchmark, and documentation coverage as applicable.

Acceptance criteria

  • The scoped implementation and tests are merged on main in small conventional commits.
  • Zig formatting, focused tests, and the relevant CI suite pass.
  • Compatibility claims have executable evidence and no unexplained skips or silent fallbacks.
  • Untrusted inputs have deterministic bounds; hot paths have benchmark evidence.
  • Documentation and the machine-readable compatibility matrix are updated.

Relationships

Blocked by

Blocks

Baselines

  • ModSecurity 3.0.16
  • Coraza 3.7.0
  • OWASP CRS 4.28.0
  • libinjection 4.0.0
  • Zig 0.17-dev resolved and locked through Pantry
9 days ago

The HTTP/1.1 reverse-proxy front end is working and integrated with the engine. Per direction, this reuses and improves the rpx dataplane (stacksjs/rpx, packages/rpx/dataplane) rather than a new proxy:

  • Ported to Zig 0.17-dev std.Io (one task per connection/direction; default thread-pool backend, io_uring available via std.Io.Evented).
  • HTTP/1.1 request-head parsing (allocation-free) at the forward seam.
  • zig-waf integration over the C connector ABI (opt-in -Dwaf=<zig-waf checkout>): translates zig_waf.h (addTranslateC) and links libzig-waf.a+liblmdb.a; each parsed request runs create_with_rules (once) then process_uri/headers → evaluate_phase → intervention, replying 403 and dropping on an enforced intervention. Without -Dwaf it's a plain proxy with zero zig-waf dependency.

Validated end to end: with SecRule ARGS "@rx attack" "...,deny", a ?q=attack request gets 403 Forbidden while a benign request proxies to the origin (200). The full stack — rpx dataplane → C ABI → execution engine → decision — is a working WAF reverse proxy.

Remaining: host routing / X-Forwarded-*, keep-alive multi-request inspection, request-body phase, TLS (#43), and packaging the link through pantry instead of a local -Dwaf path.

9 days ago

Dataplane progress: the rpx zig dataplane (stacksjs/rpx, pulled via pantry) now proxies HTTP/1.1 on std.Io and, in a -Dwaf build, runs both request phases (1-2) and response phases (3-4) through zig-waf over the C ABI — a per-connection Inspector owns one transaction spanning both directions. Content-Length-framed request/response bodies up to 128 KB are buffered and inspected; chunked/close-framed/oversize bodies stream with only their head inspected. Live-validated (request SQLi and response data-leakage both blocked, benign forwarded). Remaining #41 scope: host routing / X-Forwarded-*, keep-alive multi-request inspection, and TLS (#43).

9 days ago

Closed. The HTTP/1.1 reverse proxy lives in stacksjs/rpx under packages/rpx/dataplane and is feature-complete for this issue's scope, with the last two gaps closed today.

What it does: an HTTP/1.1 request loop over each connection — every request on a persistent connection is inspected through the WAF's request phases and every origin response through the response phases, with a fresh transaction each time. Chunked, close-framed, oversize, and Upgrade traffic falls back to a transparent pump. Built on std.Io, one task per connection, no per-request allocation. TLS termination via zig-tls (#43).

Two real defects fixed today, both live-validated:

  • fix(dataplane): replace client-supplied forwarding headers, never append (stacksjs/rpx@8feea7d). The proxy inserted its own X-Forwarded-For while leaving the client's in place. An origin cannot tell which hop wrote which entry of a comma-joined list, so a client could prepend an address and have it read as the client address — spoofing rate limits, geo rules, allowlists, and audit records. X-Forwarded-Proto: https from a plaintext hop was worse: it can convince an origin a cookie is safe to set. X-Forwarded-For, -Proto, -Host, -Port, and X-Real-IP are now dropped and replaced with what the proxy observed. Verified: a request carrying spoofed values reaches the origin as X-Forwarded-For: 127.0.0.1 / X-Forwarded-Proto: http with the rest gone, a benign request still returns 200, and SQLi is still blocked.

  • fix(dataplane): report IPv6 and IPv4-mapped peers as REMOTE_ADDR (stacksjs/rpx@fe5dcd2). An IPv6 peer fell back to a placeholder, so every REMOTE_ADDR rule was wrong for those clients — silently. IPv6 now gets canonical text, and IPv4-mapped addresses are unmapped, because a v4 client on a dual-stack listener arrives as ::ffff:a.b.c.d and reporting that form breaks every IPv4 rule already written. Verified: @ipMatch ::1 blocks an IPv6 loopback client while @ipMatch 10.0.0.0/8 passes it (200), and @ipMatch 127.0.0.1 blocks an IPv4 client on a :: listener — which only happens if the unmapping worked.

Remaining, tracked elsewhere: inspecting chunked request bodies (they currently stream through uninspected rather than being buffered), and kTLS. Neither is HTTP/1.1 proxying itself.

Sign in to comment on this issue.