Expand description
Protocol hardening knobs (DW-023, feature analysis 4.20).
Two families of defenses, applied identically to EVERY serving surface (data-plane listeners and the admin listener):
-
Parser/amplification bounds on hyper’s connection builders, so a single hostile connection cannot pin unbounded memory or parse cost:
Knob (env) Default Attack it bounds DWARA_HTTP1_MAX_HEADERS100 header-count bombs (N header lines per request) DWARA_HTTP1_MAX_BUF_KIB64 KiB single-header/line size bombs (hyper’s read buffer cap; a header line that does not fit is a 431-class parse failure) DWARA_HTTP1_HEADER_TIMEOUT_MS10 000 SLOWLORIS: a connection that sends headers slower than this is closed before ever reaching a route DWARA_H2_MAX_CONCURRENT_STREAMS128 stream floods over one h2 connection (also advertised to the peer in SETTINGS) DWARA_H2_STREAM_WINDOW_KIB1024 (1 MiB) per-stream receive buffering a malicious h2 peer can force DWARA_H2_CONNECTION_WINDOW_KIB4096 (4 MiB) connection-wide h2 receive buffering DWARA_H2_MAX_SEND_BUF_KIB1024 (1 MiB) outbound h2 send buffer per connection (write-amplification / memory pinning by a non-reading peer)
Left deliberately at hyper’s defaults: max_frame_size (16 KiB, the
minimum legal value — hyper already refuses larger frames) and HTTP/2
max_headers (hyper-util’s h2 builder has no such knob; the h2 header
list is bounded by the flow-control windows pinned above).
A timer is installed on the HTTP/1 builder (TokioTimer) because
hyper disables header_read_timeout when no timer is configured.
- Request-body inactivity gap (
DWARA_REQUEST_BODY_TIMEOUT_MS, default 30 000,0disables): the INBOUND request body handed to the dataplane is wrapped inInboundBody, which errors when the gap between two body frames exceeds the configured duration. Semantics deliberately mirror the DW-014 response-sidewrite_mswrapper (crate::dataplane::upstream::UpstreamBody): it is a GAP timeout, not a total budget — a legitimate slow upload (large file over a slow link) that keeps making progress never trips it; a client that sends headers and then trickles body bytes forever to hold a concurrency slot and an upstream connection is cut off after the gap. When the wrapper fires the in-flight upstream attempt fails as a transport-class error (the proxy answers 502 and closes), and the request’s global concurrency slot is released with the response.
CL+TE request smuggling is NOT configured here because it needs no
configuration: hyper 1.x’s HTTP/1 parser rejects requests carrying both
Content-Length and Transfer-Encoding (400/close) outright, and the
gateway never passes raw bytes to an upstream — every forwarded request
is rebuilt from hyper-parsed parts, so a smuggled “second request” would
require hyper itself to mis-parse the first one. Both properties are
pinned by the smuggling-corpus integration tests in dwara-bin.
The pre-parse sniff’s timeout is PER-READ, not a total head budget: a
client that trickles one byte at a time, each inside the
DWARA_HTTP1_HEADER_TIMEOUT_MS bound, keeps every individual sniff
read successful, and the guard deliberately hands such connections to
hyper instead of duplicating a head deadline (preferring handoff over
racing hyper for the same bytes). The fallback defense for slow
trickles is therefore hyper’s PER-HEAD header_read_timeout — the
same knob value, installed on the connection builder together with a
TokioTimer — which bounds the total time one request head may take
and closes the connection when it elapses. The two compose as: the
sniff owns each individual read, hyper owns the head as a whole, so
neither a stalled connection nor a slow trickle can hold a listener
slot indefinitely.
All knobs are read once from the environment at startup (invalid values fall back to the documented default) and are process-wide, not per-listener: hardening is a property of the parser, not of the route.
Structs§
- Digesting
Body - Request body that enforces an HMAC-signed body digest while
STREAMING (DW-036): every data frame is folded into an incremental
SHA-256 — nothing is buffered, so a signed body of any size costs
O(1) memory beyond the frame in flight — and the final digest is
compared (constant-time,
subtle) against the digest the verified signature bound at authn time (Identity::body_digest,security::authn). A mismatch surfaces asSignatureBodyError::DigestMismatch, aborting the upstream send WITH the last frame withheld on declared-length bodies (the digest is final the moment the declared byte count is hashed) or at the terminating frame on unknown-length bodies: the tampered request never completes upstream and the proxy answers 401. - Http
Hardening - Resolved hardening configuration (see the module docs for the knob table, defaults, and the attack each bound addresses).
- Inbound
Body - Inbound request body with an inactivity-gap timeout (the request-side
mirror of
crate::dataplane::upstream::UpstreamBody). Atokio::time::Sleepis armed whenever the stream is idle-Pending and cleared on every frame, so the timeout bounds GAPS, not total streaming time. When it fires the error propagates through the dataplane’s attempt machinery: the in-flight upstream send fails as a transport-class error, the client receives a classified 5xx, and the request’s concurrency slot is released. - Limited
Body - Request body wrapped with a route’s
max_body_bytescap (DW-027): counts streamed data frames and errors withLimitedBodyError::OverLimitthe moment the running total crosses the cap — the streaming half of the body limit, for requests whose length is not declared up front (chunked, h2 without content-length). The error propagates through the dataplane’s attempt machinery; the proxy recognizes it in the failure’s source chain and answers 413 (seeproxy::request_limit_exceeded).Nonecap = thin passthrough (the same shapeInboundBodytakes with no gap configured), so the proxy can wrap unconditionally for proxied actions. - Prefixed
Stream - A stream that replays
prefix(bytes already sniffed off the wire) before forwarding to the inner stream. IO traits only delegate.
Enums§
- Inbound
Body Error - Error of the inbound request-body wrapper.
- Limited
Body Error - Error of the route-limited request body (DW-027).
- Route
Limit Violation - One route-scoped request-limit violation (DW-027): which cap was crossed, by what observed value. The proxy turns these into the 431 (headers) / 413 (body) error-envelope responses.
- Signature
Body Error - Error of the signed-request digesting body (DW-036).
Constants§
- DEFAULT_
H2_ CONNECTION_ WINDOW_ KIB - Default HTTP/2 initial connection window, in KiB (4 MiB).
- DEFAULT_
H2_ MAX_ CONCURRENT_ STREAMS - Default HTTP/2 concurrent-stream cap (also advertised in SETTINGS).
- DEFAULT_
H2_ MAX_ SEND_ BUF_ KIB - Default HTTP/2 per-connection send-buffer cap, in KiB (1 MiB).
- DEFAULT_
H2_ STREAM_ WINDOW_ KIB - Default HTTP/2 initial per-stream window, in KiB (1 MiB).
- DEFAULT_
HTTP1_ HEADER_ TIMEOUT_ MS - Default slowloris header-read timeout.
- DEFAULT_
HTTP1_ MAX_ BUF_ KIB - Default HTTP/1 read-buffer cap, in KiB (hyper’s is ~8 KiB growable; pinned to 64 KiB explicitly so the bound is a documented constant).
- DEFAULT_
HTTP1_ MAX_ HEADERS - Default HTTP/1 max header COUNT (hyper’s own default, made explicit).
- DEFAULT_
REQUEST_ BODY_ TIMEOUT_ MS - Default inbound request-body inactivity gap.
Functions§
- check_
route_ limits - Check a request against a route’s
crate::config::RequestLimits(DW-027), right after route resolution.content_lengthis the request body’s exact size when declared (size_hint().exact()— Content-Length for h1, the content-length pseudo-header for h2),Nonewhen the length is unknown (streaming). Header checks are header-count first (cheapest to report), then header bytes, then the declared body size. - merge_
vary - Merge one token into a response’s
Varyheader (DW-027): appended to the existing comma-separated list when not already present (case-insensitive), created otherwise. RFC 9110 permits MULTIPLEVaryfield lines; every line is folded into the membership check and the merged value, and the result replaces the set with one line — reading only the first line would drop the later lines’ tokens and corrupt cache keys for upstreams that split theirVary. CorrectVaryis what keeps a shared cache from serving a compressed (or origin-specific) response to a client that could not have received it.