Module hardening

Module hardening 

Source
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):

  1. Parser/amplification bounds on hyper’s connection builders, so a single hostile connection cannot pin unbounded memory or parse cost:

    Knob (env)DefaultAttack it bounds
    DWARA_HTTP1_MAX_HEADERS100header-count bombs (N header lines per request)
    DWARA_HTTP1_MAX_BUF_KIB64 KiBsingle-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 000SLOWLORIS: a connection that sends headers slower than this is closed before ever reaching a route
    DWARA_H2_MAX_CONCURRENT_STREAMS128stream 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.

  1. Request-body inactivity gap (DWARA_REQUEST_BODY_TIMEOUT_MS, default 30 000, 0 disables): the INBOUND request body handed to the dataplane is wrapped in InboundBody, which errors when the gap between two body frames exceeds the configured duration. Semantics deliberately mirror the DW-014 response-side write_ms wrapper (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§

DigestingBody
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 as SignatureBodyError::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.
HttpHardening
Resolved hardening configuration (see the module docs for the knob table, defaults, and the attack each bound addresses).
InboundBody
Inbound request body with an inactivity-gap timeout (the request-side mirror of crate::dataplane::upstream::UpstreamBody). A tokio::time::Sleep is 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.
LimitedBody
Request body wrapped with a route’s max_body_bytes cap (DW-027): counts streamed data frames and errors with LimitedBodyError::OverLimit the 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 (see proxy::request_limit_exceeded). None cap = thin passthrough (the same shape InboundBody takes with no gap configured), so the proxy can wrap unconditionally for proxied actions.
PrefixedStream
A stream that replays prefix (bytes already sniffed off the wire) before forwarding to the inner stream. IO traits only delegate.

Enums§

InboundBodyError
Error of the inbound request-body wrapper.
LimitedBodyError
Error of the route-limited request body (DW-027).
RouteLimitViolation
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.
SignatureBodyError
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_length is the request body’s exact size when declared (size_hint().exact() — Content-Length for h1, the content-length pseudo-header for h2), None when 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 Vary header (DW-027): appended to the existing comma-separated list when not already present (case-insensitive), created otherwise. RFC 9110 permits MULTIPLE Vary field 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 their Vary. Correct Vary is what keeps a shared cache from serving a compressed (or origin-specific) response to a client that could not have received it.