HTTP/2 connection

http2-conn is Cruft's server-role HTTP/2 connection and stream driver, the layer that holds per-stream state, flow-control windows, and header reassembly across frames. It enforces the caps that stop the known HTTP/2 denial-of-service attacks: Rapid Reset, stream and CONTINUATION floods, HPACK bombs, and oversized frames.

HTTP/2 is a connection state machine: one TCP byte stream multiplexed into many logical streams, each with its own lifecycle, its own flow-control window, header blocks that arrive split across frames, and a peer who may be hostile. This is more than a Friday-afternoon codec. The question that matters is what the server does when a client opens a stream, resets it, and does that in a tight loop ten thousand times a second. Cruft's http2-conn is the crate that answers it in Cruft's stack, standing in for the connection and stream half of h2.

Alpha, mostly unproven. http2-conn (crate rusty-http2-conn, Cruft 0.0.10) is a server-role HTTP/2 (RFC 9113) connection and stream driver that replaces the connection/stream/flow-control layer of the crates.io h2 crate. It has had no external audit and no interop testing against real HTTP/2 clients. The DoS and framing defenses below are exercised by the crate's own negative tests; conformance and interop against a real peer are not yet proven. Do not put it in front of production or adversarial traffic.

What the connection layer owns

The frame bytes are a different crate (http2-codec), and the header compression is a third (http2-hpack). http2-conn is the thing that owns state across frames: which streams exist, whose turn it is to send headers, how many bytes each side may still send. Its whole surface is one feed loop:

feed(inbound: &[u8]) -> Result<Http2Feed, String>

You push whatever bytes arrived off the socket. It returns bytes to write back (Http2Feed.outbound) plus any fully-reassembled requests (Http2Feed.requests), each an Http2Request with stream_id, headers, and body. It is runtime-agnostic: no sockets, no async, same feed-driven shape as Cruft's TLS handshake machine. You respond with respond(stream_id, status, headers, body), which serializes a HEADERS frame plus flow-controlled DATA.

Its job is to turn a byte stream into a sequence of (request, response) turns while holding per-stream state correctly. The happy path (preface, SETTINGS exchange, GET extraction, POST with body, then a response that decodes back to the right headers) is covered by brings_up_connection_and_extracts_get_request and extracts_post_with_body_then_responds.

Surviving many streams under load

Here is the distinction that separates a demo from a server. Every frame the client sends is cheap for the client and can be expensive for the server. A HEADERS frame forces a full HPACK decode. An RST_STREAM forces a stream teardown. Nothing stops the client from doing both, forever, on new stream IDs. That is not a bug in one function; it is the entire threat model of HTTP/2, and it has CVE numbers.

The naive version of this crate advertised a concurrent-stream limit and never enforced it, removed a stream on RST_STREAM with no counter, and accumulated CONTINUATION fragments with no cap. That is three named denial-of-service classes at once:

  • Rapid Reset (CVE-2023-44487): HEADERS + RST_STREAM in a loop, each pass paying a full HPACK decode, the stream map never bounded by lifetime.
  • Stream flood: the map grows past the advertised MAX_CONCURRENT_STREAMS.
  • CONTINUATION flood (CVE-2024-27316 class): a header block spread across unboundedly many CONTINUATION frames, unbounded in total bytes.

Where these stand now: fixed and enforced

All three defenses are in the source you can read:

  • A concurrent-stream cap: HEADERS past SERVER_MAX_CONCURRENT_STREAMS (100) sends a GOAWAY carrying ERR_ENHANCE_YOUR_CALM.
  • A total-lifetime cap: more than SERVER_MAX_STREAM_LIFETIME (1000) streams over the connection's life also triggers ENHANCE_YOUR_CALM.
  • An RST_STREAM counter: past SERVER_MAX_RST_STREAMS (100) resets, ENHANCE_YOUR_CALM GOAWAY. This is the Rapid Reset brake.
  • A CONTINUATION cap: both a per-block frame count (SERVER_MAX_CONTINUATIONS_PER_BLOCK, 32) and a total header-block byte cap (SERVER_MAX_HEADER_BLOCK_BYTES, 65536).
  • Emitted request streams are evicted from the map (try_emit), so they do not leak for the connection lifetime.

Each has a negative test that drives the flood and asserts the ENHANCE_YOUR_CALM GOAWAY code, namely rapid_reset_flood_gets_enhance_your_calm_goaway, concurrent_stream_flood_gets_enhance_your_calm_goaway, and continuation_flood_gets_enhance_your_calm_goaway. The HPACK-bomb path is covered too (hpack_header_list_bomb_is_rejected, hpack_dynamic_table_size_update_above_advertised_limit_is_rejected), enforced through the bounded decode_header_block_limited API that finish_headers calls.

Framing and flow control as validation

The other way a server dies is not a flood but a single malformed or oversized frame. The first version of this crate trusted the codec's 24-bit length field (frames up to ~16 MB), did no receive-side flow-control accounting, and skipped a pile of RFC 9113 validity rules. That is closed now:

  • An inbound frame larger than the advertised max sends a FRAME_SIZE_ERROR GOAWAY, and (the subtle part) a partial oversized frame is rejected before its payload is buffered, so the read buffer cannot be grown unboundedly (tests inbound_frame_larger_than_advertised_max_gets_frame_size_goaway and partial_inbound_frame_larger_than_advertised_max_gets_frame_size_goaway).
  • Receive-side flow control now exists: both a connection window and per-stream windows are decremented on DATA, and exhaustion sends a FLOW_CONTROL_ERROR GOAWAY (test inbound_data_exceeding_receive_window_gets_flow_control_goaway).
  • WINDOW_UPDATE of zero is a PROTOCOL_ERROR and an increment overflowing 2^31-1 is a FLOW_CONTROL_ERROR (test window_update_zero_and_overflow_are_rejected).
  • SETTINGS shape and ranges are validated: a payload not a multiple of six fails via decode_settings_payload_checked (no silent truncation), and ENABLE_PUSH, MAX_FRAME_SIZE, and INITIAL_WINDOW_SIZE ranges are enforced (test malformed_settings_get_frame_or_protocol_goaway).
  • Stream-ID rules: even-numbered client IDs and non-monotonic IDs are rejected, and DATA on a closed/emitted stream gets STREAM_CLOSED (test data_on_closed_stream_gets_stream_closed_goaway).

Outbound DATA is genuinely flow-controlled too: respond buffers a body that exceeds the current window and flush_stream / flush_all release it on later WINDOW_UPDATEs, splitting to the peer's MAX_FRAME_SIZE. large_body_respects_frame_size_and_flow_control drives a 100 KB body, asserts it windows out at 65535 bytes, then grants window and asserts the whole body arrives in order with END_STREAM on the final frame.

What it does NOT do

Server role only. There is no client-role connection machine here. There is no server push: PUSH_PROMISE frames are ignored, and the SETTINGS handler tracks ENABLE_PUSH only to reject bad values. PRIORITY is ignored (no priority tree, no dependency handling), which is defensible (RFC 9113 deprecates priority signaling) but worth knowing. There is no active flow-control pacing: the server does not proactively send WINDOW_UPDATEs to enlarge the client's receive window as it consumes DATA, so a large upload stalls once the initial receive window is spent. Active window pacing and server push are named staged follow-ons. Trailers and half-closed-then-more-headers edge cases are not modeled. And this is one connection: no listener, no ALPN, no TLS. Something above it (see the socket layer) owns the TCP and the h2 ALPN token.

Limitations

http2-conn is a competent server-role connection machine whose known DoS classes now have real, tested brakes. The Rapid Reset, stream-flood, CONTINUATION-flood, HPACK-bomb, oversized-frame, and flow-control defenses all have negative tests in-tree, so the defensive constants are no longer decorative. That property is genuine and narrow.

The limit is this. "Has a test that drives the flood I thought of" is not the same as "is correct against the protocol." The crates.io h2 this crate replaces has years of interop against nghttp2, curl, browsers, and Go's client, an h2spec conformance history, and fuzzing. http2-conn's tests all run against input it generates itself with its own sibling codecs. h2spec conformance, fuzzing, and differential testing against h2/nghttp2 remain a high-value follow-on, not something done. There are almost certainly conformance and interop gaps that only a real client surfaces, precisely because the tests and the code-under-test share an author and a mental model. The DoS caps are hardcoded constants (100 streams, 100 resets, 1000 lifetime, 32 continuations, 64 KB blocks), tuned by reasoning, not by measuring real load. Treat "survives the three named CVEs" as covered on this corpus and "behaves like h2 against a real peer" as unproven. It is spec-plausible and CVE-hardened, not conformance-proven, and not for production traffic.