sozu_lib/protocol/mux/h2.rs
1//! H2 mux connection wrapper (RFC 9113).
2//!
3//! Owns wire-side connection state: HPACK encoder/decoder, peer settings,
4//! flow window, GOAWAY/RST attribution, and the [`H2FloodDetector`] backing
5//! the CVE-2023-44487 / CVE-2024-27316 / CVE-2025-8671 mitigations. Stream
6//! storage lives in the sibling `Context<L>` (`mux/mod.rs`); this module is
7//! the canonical home for the edge-trigger discipline — paths that queue
8//! bytes for a later event-loop pass must arm writable / signal pending
9//! write (cf. `arm_writable()` at the deferred-control-frame sites and
10//! `lib/src/lib.rs:1006`-`1010`).
11
12use std::{
13 cmp::min,
14 collections::{HashMap, HashSet},
15 io::{IoSlice, Write as _},
16 time::{Duration, Instant},
17};
18
19/// Compile-time guard: `payload_len as usize` casts in the H2 parser assume at
20/// least 32-bit pointer width. This prevents silent truncation on platforms
21/// with smaller pointers (e.g. 16-bit embedded targets).
22const _: () = assert!(
23 std::mem::size_of::<usize>() >= 4,
24 "sozu requires at least 32-bit pointers"
25);
26
27use rusty_ulid::Ulid;
28use sozu_command::{logging::ansi_palette, ready::Ready};
29
30use crate::metrics::names;
31use crate::{
32 L7ListenerHandler, ListenerHandler, Protocol, Readiness, SessionMetrics,
33 protocol::mux::{
34 BackendStatus, Context, DebugEvent, DebugHistory, Endpoint, GenericHttpStream,
35 GlobalStreamId, MuxResult, Position, Stream, StreamId, StreamState, converter,
36 forcefully_terminate_answer,
37 parser::{self, Frame, FrameHeader, FrameType, H2Error, Headers, WindowUpdate},
38 pkawa, remove_backend_stream, serializer, set_default_answer,
39 shared::{EndStreamAction, drain_tls_close_notify, end_stream_decision},
40 update_readiness_after_read, update_readiness_after_write,
41 },
42 socket::{SocketHandler, SocketResult, stats::socket_rtt},
43 timer::TimeoutContainer,
44};
45
46/// Protocol label + session descriptor used as a prefix on every
47/// [`ConnectionH2`] log line. Matches the RUSTLS log-context convention:
48/// `MUX-H2\tSession(...)\t >>>`. When colored output is enabled (via
49/// [`ansi_palette`]) the label is wrapped in bold bright-white ANSI (uniform
50/// across every protocol) and the session detail is rendered in light grey.
51///
52/// Fields included in the session block (chosen to surface the most common
53/// H2 troubleshooting axes — flow stall, leaked stream, draining state,
54/// peer-side gap, reset-flood exposure):
55/// - `peer` — peer address (or `None` if the socket is gone)
56/// - `position` — `Server` / `Client(...)` orientation
57/// - `state` — current [`H2State`]
58/// - `streams` — number of in-flight streams on this connection
59/// - `last_peer_id` — `highest_peer_stream_id` (gap to the peer's view)
60/// - `window` — connection-level send window (RFC 9113 §6.9)
61/// - `draining` — set after the first GOAWAY of a graceful shutdown
62/// - `total_rst_streams_emitted_lifetime` — MadeYouReset counter (CVE-2025-8671)
63/// - `total_rst_received_lifetime` — Rapid Reset counter (CVE-2023-44487)
64/// - `readiness` — connection-level mio readiness snapshot
65///
66/// Computed lazily on each callsite — the helper only materialises when the
67/// log level is enabled, so uncolored hot paths keep a single thread-local
68/// read (the colored check) and one `format!` allocation.
69macro_rules! log_context {
70 ($self:expr) => {{
71 let (open, reset, grey, gray, white) = ansi_palette();
72 format!(
73 "[{ulid} - - -]\t{open}MUX-H2{reset}\t{grey}Session{reset}({gray}peer{reset}={white}{peer:?}{reset}, {gray}position{reset}={white}{position:?}{reset}, {gray}state{reset}={white}{state:?}{reset}, {gray}streams{reset}={white}{streams}{reset}, {gray}last_peer_id{reset}={white}{last_peer_id}{reset}, {gray}window{reset}={white}{window}{reset}, {gray}draining{reset}={white}{draining}{reset}, {gray}total_rst_streams_emitted_lifetime{reset}={white}{total_rst_streams_emitted_lifetime}{reset}, {gray}total_rst_received_lifetime{reset}={white}{total_rst_received_lifetime}{reset}, {gray}readiness{reset}={white}{readiness}{reset})\t >>>",
74 open = open,
75 reset = reset,
76 grey = grey,
77 gray = gray,
78 white = white,
79 ulid = $self.session_ulid,
80 peer = $self.socket.socket_ref().peer_addr().ok(),
81 position = $self.position,
82 state = $self.state,
83 streams = $self.streams.len(),
84 last_peer_id = $self.highest_peer_stream_id,
85 window = $self.flow_control.window,
86 draining = $self.drain.draining,
87 total_rst_streams_emitted_lifetime = $self.flood_detector.total_rst_streams_emitted_lifetime,
88 total_rst_received_lifetime = $self.flood_detector.total_rst_received_lifetime,
89 readiness = $self.readiness,
90 )
91 }};
92}
93
94/// Per-stream variant of [`log_context!`] used when a [`Stream`]'s
95/// [`HttpContext`](crate::protocol::kawa_h1::editor::HttpContext) is in
96/// scope. Populates the `request_id`, `cluster_id` and `backend_id` slots of
97/// the bracket so the log line can be filtered by the specific H2 stream it
98/// belongs to.
99#[allow(unused_macros)]
100macro_rules! log_context_stream {
101 ($self:expr, $http_context:expr) => {{
102 let (open, reset, grey, gray, white) = ansi_palette();
103 format!(
104 "[{ulid} {req} {cluster} {backend}]\t{open}MUX-H2{reset}\t{grey}Session{reset}({gray}peer{reset}={white}{peer:?}{reset}, {gray}position{reset}={white}{position:?}{reset}, {gray}state{reset}={white}{state:?}{reset}, {gray}streams{reset}={white}{streams}{reset}, {gray}last_peer_id{reset}={white}{last_peer_id}{reset}, {gray}window{reset}={white}{window}{reset}, {gray}draining{reset}={white}{draining}{reset}, {gray}total_rst_streams_emitted_lifetime{reset}={white}{total_rst_streams_emitted_lifetime}{reset}, {gray}total_rst_received_lifetime{reset}={white}{total_rst_received_lifetime}{reset}, {gray}readiness{reset}={white}{readiness}{reset})\t >>>",
105 open = open,
106 reset = reset,
107 grey = grey,
108 gray = gray,
109 white = white,
110 ulid = $self.session_ulid,
111 req = $http_context.id,
112 cluster = $http_context.cluster_id.as_deref().unwrap_or("-"),
113 backend = $http_context.backend_id.as_deref().unwrap_or("-"),
114 peer = $self.socket.socket_ref().peer_addr().ok(),
115 position = $self.position,
116 state = $self.state,
117 streams = $self.streams.len(),
118 last_peer_id = $self.highest_peer_stream_id,
119 window = $self.flow_control.window,
120 draining = $self.drain.draining,
121 total_rst_streams_emitted_lifetime = $self.flood_detector.total_rst_streams_emitted_lifetime,
122 total_rst_received_lifetime = $self.flood_detector.total_rst_received_lifetime,
123 readiness = $self.readiness,
124 )
125 }};
126}
127
128/// Module-level prefix without session context, for logs emitted from
129/// free functions, `H2ConnectionConfig` validation and other sites where no
130/// `ConnectionH2` is in scope. Keeps the `MUX-H2` label consistent with
131/// connection logs and honours the colored flag.
132macro_rules! log_module_context {
133 () => {{
134 let (open, reset, _, _, _) = ansi_palette();
135 format!("{open}MUX-H2{reset}\t >>>", open = open, reset = reset)
136 }};
137}
138
139/// `if let Some(violation) = self.flood_detector.check_flood() { return self.handle_flood_violation(violation); }`
140/// pattern wrapped as a single statement. Pure dispatch — the actual flood
141/// thresholds and counters live inside `H2FloodDetector::check_flood` and
142/// `ConnectionH2::handle_flood_violation`, which the macro does not touch.
143/// Use this at every per-frame counter bump site so the wrapper stays
144/// uniform and a future grep for "flood-check forgot to return" finds zero.
145macro_rules! check_flood_or_return {
146 ($self:expr) => {
147 if let Some(violation) = $self.flood_detector.check_flood() {
148 return $self.handle_flood_violation(violation);
149 }
150 };
151}
152
153/// Outcome of a single-stream write flush in write_streams.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155enum FlushOutcome {
156 /// All queued bytes were drained to the socket.
157 Drained,
158 /// The socket blocked before the queue was drained. The caller must
159 /// arrange to resume (set expect_write or return from write_streams).
160 Stalled,
161}
162
163// ── RFC 9113 §6.5.2 Settings Defaults ───────────────────────────────────────
164
165const DEFAULT_HEADER_TABLE_SIZE: u32 = 4096;
166const DEFAULT_MAX_CONCURRENT_STREAMS: u32 = 100;
167pub(super) const DEFAULT_INITIAL_WINDOW_SIZE: u32 = (1 << 16) - 1; // 65535
168const DEFAULT_MAX_FRAME_SIZE: u32 = 1 << 14; // 16384
169
170// RFC 9113 §6.5.2: SETTINGS_MAX_FRAME_SIZE valid range [2^14, 2^24)
171const MIN_MAX_FRAME_SIZE: u32 = 1 << 14; // 16384
172const MAX_MAX_FRAME_SIZE: u32 = 1 << 24; // 16777216 (exclusive upper bound)
173
174// RFC 9113 §6.9: maximum flow control window size (2^31 - 1)
175const FLOW_CONTROL_MAX_WINDOW: u32 = (1 << 31) - 1;
176// RFC 9113 §5.1.1: stream identifiers are 31-bit unsigned integers (2^31 - 1).
177const STREAM_ID_MAX: u32 = 0x7FFF_FFFF;
178
179/// Allocate the next locally-initiated stream identifier given the current
180/// `last_stream_id` watermark, returning `(issued_id, next_last_stream_id)`
181/// or `None` when the 31-bit space is exhausted.
182///
183/// RFC 9113 §5.1.1 reserves odd identifiers for clients and even identifiers
184/// for servers. Sōzu never server-pushes, so in practice this helper is
185/// called on the backend (client) side via [`ConnectionH2::new_stream_id`].
186/// The server branch is kept symmetrical so the behaviour is exercised by
187/// the unit tests and remains correct if push is ever enabled.
188///
189/// `last_stream_id` tracks the even "watermark" (2, 4, 6, ...). A client call
190/// issues `watermark - 1` (odd), a server call issues `watermark - 2` (even).
191/// The helper enforces two invariants:
192/// - the issued identifier never exceeds `STREAM_ID_MAX` (2³¹ - 1); and
193/// - the returned watermark is a valid starting point for the next call.
194///
195/// Exhaustion is reported with `None` to the caller, which must emit
196/// GOAWAY(NO_ERROR) and stop issuing new streams on this connection
197/// (see `start_stream` for the client-side drain path).
198pub(super) fn next_stream_id(
199 last_stream_id: StreamId,
200 is_client: bool,
201) -> Option<(StreamId, StreamId)> {
202 let next = last_stream_id.checked_add(2)?;
203 let issued = if is_client {
204 next.checked_sub(1)?
205 } else {
206 next.checked_sub(2)?
207 };
208 // RFC 9113 §5.1.1: stream identifiers are 31-bit. Reject any allocation
209 // whose issued value would exceed `STREAM_ID_MAX`; the watermark itself
210 // is allowed to sit at `STREAM_ID_MAX + 1` (the sentinel that fails the
211 // next call).
212 if issued > STREAM_ID_MAX {
213 return None;
214 }
215 // Post-conditions (RFC 9113 §5.1.1):
216 // - the issued id fits the 31-bit space;
217 // - the returned watermark is strictly greater than the id we issued, so a
218 // subsequent call cannot re-issue or regress;
219 // - role-parity: client ids are odd, server ids even. This holds ONLY when
220 // `last_stream_id` is an even watermark, which is the helper's documented
221 // contract and what production always maintains (`create_stream` rounds to
222 // `(stream_id + 2) & !1`; the connection initialises it to 0). The unit
223 // tests deliberately feed odd `last` values at the saturation boundary, so
224 // the parity check is gated on the watermark being even — a parity slip
225 // from an *even* watermark would let two roles collide on one id.
226 debug_assert!(
227 issued <= STREAM_ID_MAX,
228 "issued stream id must fit the 31-bit space"
229 );
230 debug_assert!(
231 next > issued,
232 "the next watermark must advance strictly past the issued id"
233 );
234 debug_assert!(
235 last_stream_id & 1 != 0 || (issued & 1 == 1) == is_client,
236 "from an even watermark, client ids must be odd and server ids even (RFC 9113 §5.1.1)"
237 );
238 Some((issued, next))
239}
240
241/// Enlarged connection-level receive window (1 MB).
242/// The RFC 9113 default is 65 535 bytes, which is too small for high-throughput
243/// proxying and causes excessive WINDOW_UPDATE round-trips. 1 MB matches the
244/// initial window used by HAProxy, the h2 crate, and other production proxies.
245const ENLARGED_CONNECTION_WINDOW: u32 = 1_048_576;
246
247/// H2 client connection preface size: 24-byte magic + 9-byte SETTINGS frame header
248pub(super) const CLIENT_PREFACE_SIZE: usize = 24 + parser::FRAME_HEADER_SIZE;
249
250// ── Flood Detection Thresholds (CVE mitigations) ────────────────────────────
251
252/// Default maximum RST_STREAM frames per window (CVE-2023-44487 Rapid Reset + CVE-2019-9514)
253const DEFAULT_MAX_RST_STREAM_PER_WINDOW: u32 = 100;
254/// Hard lifetime cap on total RST_STREAM frames received on a single
255/// connection (CVE-2023-44487 Rapid Reset).
256///
257/// The per-window counter half-decays, which allows a patient attacker to
258/// sustain ~50 RST/sec indefinitely — each one costs the backend a request
259/// that will be cancelled before any response work is produced. A lifetime
260/// counter that never decays puts an absolute ceiling on that amplification
261/// per connection. 10 000 is generous for legitimate traffic (months of
262/// occasional client-side cancellations) but rapidly trips on the ~30/sec
263/// abusive pace reported in the CVE-2023-44487 advisory (~5 minutes).
264pub(super) const DEFAULT_MAX_RST_STREAM_LIFETIME: u64 = 10_000;
265/// Hard lifetime cap on RST_STREAM frames received BEFORE the corresponding
266/// backend response has started. These are the cheap-for-client /
267/// expensive-for-us resets that characterise Rapid Reset: the client pays
268/// one RST frame, we pay a round-trip to the backend plus request parsing.
269/// A much lower ceiling kills the attack well before 10 000 lifetime total.
270pub(super) const DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME: u64 = 50;
271/// Absolute lifetime cap on **server-emitted** RST_STREAM frames on a single
272/// connection (CVE-2025-8671 — "MadeYouReset"). Distinct from
273/// [`DEFAULT_MAX_RST_STREAM_LIFETIME`] which caps *received* RSTs
274/// (CVE-2023-44487 Rapid Reset).
275///
276/// MadeYouReset has the server talk itself into flooding: the attacker sends
277/// legitimate-looking frames that force the server to emit RST_STREAM (content
278/// -length mismatch, header parse error, rejected priority, zero-increment
279/// `WINDOW_UPDATE` on an open stream, …). Each forced RST costs the server a
280/// header-decode, kawa buffer setup and frame serialisation; uncapped, it
281/// becomes the same class of DoS as Rapid Reset but with a flipped emission
282/// direction.
283///
284/// 500 is conservative: legitimate traffic very rarely triggers a
285/// server-initiated RST (aside from graceful `NoError` cancels which are not
286/// counted), so crossing 500 on a single connection is a strong abuse signal.
287pub(super) const DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME: u64 = 500;
288/// Default maximum PING frames per window (CVE-2019-9512 Ping Flood)
289const DEFAULT_MAX_PING_PER_WINDOW: u32 = 100;
290/// Absolute lifetime cap on PING frames received on a single connection.
291/// Mirrors DEFAULT_MAX_RST_STREAM_LIFETIME — generous for legitimate
292/// keep-alives but trips on sustained low-rate abuse (CVE-2019-9512).
293const DEFAULT_MAX_PING_LIFETIME: u32 = 10_000;
294/// Default maximum SETTINGS frames per window (CVE-2019-9515 Settings Flood)
295const DEFAULT_MAX_SETTINGS_PER_WINDOW: u32 = 50;
296/// Absolute lifetime cap on SETTINGS frames received on a single connection.
297/// Mirrors DEFAULT_MAX_RST_STREAM_LIFETIME — generous for legitimate
298/// renegotiations but trips on sustained low-rate abuse (CVE-2019-9515).
299const DEFAULT_MAX_SETTINGS_LIFETIME: u32 = 10_000;
300/// Default maximum empty DATA frames per window (CVE-2019-9518 Empty Frames)
301const DEFAULT_MAX_EMPTY_DATA_PER_WINDOW: u32 = 100;
302/// Default maximum connection-level (stream 0) WINDOW_UPDATE frames per
303/// sliding window. Non-zero stream-0 WINDOW_UPDATE frames are otherwise
304/// uncounted by the generic glitch detector — a peer could burn proxy CPU by
305/// sending millions of legal-looking stream-0 WINDOW_UPDATEs. Value mirrors
306/// [`DEFAULT_MAX_EMPTY_DATA_PER_WINDOW`] / [`DEFAULT_MAX_PING_PER_WINDOW`] —
307/// legitimate proxies only need a handful per second.
308const DEFAULT_MAX_WINDOW_UPDATE_STREAM0_PER_WINDOW: u32 = 100;
309/// Default maximum CONTINUATION frames per header block (CVE-2024-27316)
310const DEFAULT_MAX_CONTINUATION_FRAMES: u32 = 20;
311/// Maximum accumulated header block size across CONTINUATION frames (64KB)
312pub(super) const MAX_HEADER_LIST_SIZE: usize = 65536;
313/// Default maximum HPACK dynamic table size (SETTINGS_HEADER_TABLE_SIZE)
314/// accepted from the peer. 64 KB is well above the RFC default of 4 KB
315/// while preventing a malicious peer from advertising up to 4 GB.
316const DEFAULT_MAX_HEADER_TABLE_SIZE: u32 = 65536;
317/// Default maximum number of materialized header fields per request/response —
318/// HPACK fields plus expanded cookie crumbs (RFC 9113 §8.2.3). Bounds the HPACK
319/// indexed-reference "header bomb": each 1-byte indexed reference materializes a
320/// `Pair` of per-entry bookkeeping, so an attacker amplifies wire bytes into
321/// allocation. RFC 9113 §6.5.2's +32-octet/field accounting alone caps this at
322/// ~2048 fields for a 64 KB list; this explicit count cap is the tighter,
323/// upstream-matching defense (cf. nginx `max_headers`, Apache `LimitRequestFields`).
324const DEFAULT_MAX_HEADER_FIELDS: u32 = 128;
325/// Cumulative outbound progress (bytes) a window-stalled stream must drain to
326/// clear its flow-control-stall deadline (M2 cumulative-stall budget). Below
327/// this, a `WINDOW_UPDATE(+1)` drip that trickles a few bytes per idle period
328/// cannot keep the slot alive: the deadline ages out and the reaper
329/// RST(CANCEL)s the stream. Chosen as one max H2 DATA frame payload (16 KiB) —
330/// a legitimate slow-but-steady transfer drains at least one frame per idle
331/// period at any realistic bandwidth, while a drip attacker grants far less. A
332/// `const`, not a config knob: `h2_stream_idle_timeout_seconds` is already the
333/// operator dial for slow-link tolerance, and coupling a second knob invites
334/// misconfiguration (high floor + low deadline = mass false reaps).
335const FC_STALL_CLEAR_FLOOR: usize = 16 * 1024;
336/// RFC 9113 §6.5.2: the size accounted against `SETTINGS_MAX_HEADER_LIST_SIZE`
337/// is the uncompressed name + value octets PLUS a 32-octet overhead per field.
338/// The per-field overhead is what bounds the field count under a fixed byte
339/// budget — omitting it lets a peer materialize ~33× more fields than intended.
340pub(super) const HEADER_FIELD_SIZE_OVERHEAD: usize = 32;
341/// Duration of the sliding window for rate-based flood counters
342const FLOOD_WINDOW_DURATION: std::time::Duration = std::time::Duration::from_secs(1);
343/// Default maximum general anomaly count before triggering ENHANCE_YOUR_CALM
344const DEFAULT_MAX_GLITCH_COUNT: u32 = 100;
345
346/// RFC 9113 §5.1.2: threshold of `REFUSED_STREAM` emissions per
347/// [`BACKPRESSURE_WINDOW_DURATION`] that triggers back-pressure — at this
348/// point we halve the advertised `SETTINGS_MAX_CONCURRENT_STREAMS` so the
349/// peer throttles its request rate instead of paying the RST round-trip for
350/// every new stream.
351const BACKPRESSURE_REFUSAL_THRESHOLD: u32 = 50;
352/// Sliding window used to detect refusal bursts for SETTINGS back-pressure.
353const BACKPRESSURE_WINDOW_DURATION: std::time::Duration = std::time::Duration::from_secs(60);
354
355/// Configurable thresholds for H2 flood detection.
356///
357/// All values have safe defaults matching the compile-time constants.
358/// When configured via listener config, `None` values fall back to these defaults.
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
360pub struct H2FloodConfig {
361 /// Maximum RST_STREAM frames per second window (CVE-2023-44487, CVE-2019-9514)
362 pub max_rst_stream_per_window: u32,
363 /// Maximum PING frames per second window (CVE-2019-9512)
364 pub max_ping_per_window: u32,
365 /// Maximum SETTINGS frames per second window (CVE-2019-9515)
366 pub max_settings_per_window: u32,
367 /// Maximum empty DATA frames per second window (CVE-2019-9518)
368 pub max_empty_data_per_window: u32,
369 /// Maximum connection-level (stream 0) WINDOW_UPDATE frames per sliding
370 /// window. Caps the CPU cost of a peer sending a flood of non-zero
371 /// stream-0 WINDOW_UPDATEs — each is individually legal so the generic
372 /// glitch counter does not trip, yet millions per connection still burn
373 /// server CPU parsing and updating the flow window.
374 pub max_window_update_stream0_per_window: u32,
375 /// Maximum CONTINUATION frames per header block (CVE-2024-27316)
376 pub max_continuation_frames: u32,
377 /// Maximum accumulated protocol anomalies before ENHANCE_YOUR_CALM
378 pub max_glitch_count: u32,
379 /// Absolute lifetime cap on RST_STREAM frames received on a single
380 /// connection (CVE-2023-44487). Never decays — provides a ceiling the
381 /// per-window counter cannot.
382 pub max_rst_stream_lifetime: u64,
383 /// Lifetime cap on "abusive" (pre-response-start) RST_STREAM frames —
384 /// the Rapid Reset signature (CVE-2023-44487).
385 pub max_rst_stream_abusive_lifetime: u64,
386 /// Absolute lifetime cap on **server-emitted** RST_STREAM frames for this
387 /// connection (CVE-2025-8671 "MadeYouReset"). Only non-`NoError` resets
388 /// count — graceful cancels are exempt.
389 pub max_rst_stream_emitted_lifetime: u64,
390 /// Maximum accumulated HPACK-decoded header list size per request
391 /// (SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113 §6.5.2).
392 pub max_header_list_size: u32,
393 /// Maximum HPACK dynamic table size (SETTINGS_HEADER_TABLE_SIZE) accepted
394 /// from the peer. Caps the value the peer advertises in SETTINGS frames to
395 /// prevent unbounded HPACK encoder memory growth.
396 pub max_header_table_size: u32,
397 /// Maximum number of materialized header fields, enforced per HEADERS block
398 /// and (independently) per trailers block — HPACK fields plus expanded
399 /// cookie crumbs (RFC 9113 §8.2.3). Bounds the HPACK indexed-reference
400 /// header bomb, where many 1-byte indexed references each materialize a
401 /// `Pair` of per-entry bookkeeping.
402 pub max_header_fields: u32,
403}
404
405impl Default for H2FloodConfig {
406 fn default() -> Self {
407 Self {
408 max_rst_stream_per_window: DEFAULT_MAX_RST_STREAM_PER_WINDOW,
409 max_ping_per_window: DEFAULT_MAX_PING_PER_WINDOW,
410 max_settings_per_window: DEFAULT_MAX_SETTINGS_PER_WINDOW,
411 max_empty_data_per_window: DEFAULT_MAX_EMPTY_DATA_PER_WINDOW,
412 max_window_update_stream0_per_window: DEFAULT_MAX_WINDOW_UPDATE_STREAM0_PER_WINDOW,
413 max_continuation_frames: DEFAULT_MAX_CONTINUATION_FRAMES,
414 max_glitch_count: DEFAULT_MAX_GLITCH_COUNT,
415 max_rst_stream_lifetime: DEFAULT_MAX_RST_STREAM_LIFETIME,
416 max_rst_stream_abusive_lifetime: DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME,
417 max_rst_stream_emitted_lifetime: DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME,
418 max_header_list_size: MAX_HEADER_LIST_SIZE as u32,
419 max_header_table_size: DEFAULT_MAX_HEADER_TABLE_SIZE,
420 max_header_fields: DEFAULT_MAX_HEADER_FIELDS,
421 }
422 }
423}
424
425impl H2FloodConfig {
426 /// Create a validated config, clamping all thresholds to at least 1.
427 /// Zero thresholds would cause immediate flood detection on any frame.
428 #[allow(clippy::too_many_arguments)]
429 pub fn new(
430 max_rst_stream_per_window: u32,
431 max_ping_per_window: u32,
432 max_settings_per_window: u32,
433 max_empty_data_per_window: u32,
434 max_window_update_stream0_per_window: u32,
435 max_continuation_frames: u32,
436 max_glitch_count: u32,
437 max_rst_stream_lifetime: u64,
438 max_rst_stream_abusive_lifetime: u64,
439 max_rst_stream_emitted_lifetime: u64,
440 max_header_list_size: u32,
441 max_header_table_size: u32,
442 max_header_fields: u32,
443 ) -> Self {
444 let config = Self {
445 max_rst_stream_per_window: max_rst_stream_per_window.max(1),
446 max_ping_per_window: max_ping_per_window.max(1),
447 max_settings_per_window: max_settings_per_window.max(1),
448 max_empty_data_per_window: max_empty_data_per_window.max(1),
449 max_window_update_stream0_per_window: max_window_update_stream0_per_window.max(1),
450 max_continuation_frames: max_continuation_frames.max(1),
451 max_glitch_count: max_glitch_count.max(1),
452 max_rst_stream_lifetime: max_rst_stream_lifetime.max(1),
453 max_rst_stream_abusive_lifetime: max_rst_stream_abusive_lifetime.max(1),
454 max_rst_stream_emitted_lifetime: max_rst_stream_emitted_lifetime.max(1),
455 max_header_list_size: max_header_list_size.max(1),
456 max_header_table_size: max_header_table_size.max(1),
457 max_header_fields: max_header_fields.max(1),
458 };
459 // Post-condition: every threshold is clamped to at least 1. A zero
460 // threshold would make `check_flood`/`record_rst_*` trip on the very
461 // first frame (count > 0 > threshold), turning a legitimate connection
462 // into an immediate GOAWAY. This is the central invariant the clamps
463 // above exist to enforce — assert it rather than trusting the `.max(1)`
464 // chain stays correct under future edits.
465 debug_assert!(
466 config.max_rst_stream_per_window >= 1
467 && config.max_ping_per_window >= 1
468 && config.max_settings_per_window >= 1
469 && config.max_empty_data_per_window >= 1
470 && config.max_window_update_stream0_per_window >= 1
471 && config.max_continuation_frames >= 1
472 && config.max_glitch_count >= 1,
473 "every u32 flood threshold must be clamped to >= 1"
474 );
475 debug_assert!(
476 config.max_rst_stream_lifetime >= 1
477 && config.max_rst_stream_abusive_lifetime >= 1
478 && config.max_rst_stream_emitted_lifetime >= 1
479 && config.max_header_list_size >= 1
480 && config.max_header_table_size >= 1
481 && config.max_header_fields >= 1,
482 "every lifetime/size flood threshold must be clamped to >= 1"
483 );
484 config
485 }
486}
487
488/// Default stream Vec shrink ratio: shrink when total > active * ratio.
489const DEFAULT_STREAM_SHRINK_RATIO: u32 = 2;
490
491/// Configurable H2 connection tuning parameters.
492///
493/// All values have safe defaults. When configured via listener config,
494/// absent values fall back to compile-time defaults.
495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
496pub struct H2ConnectionConfig {
497 /// Connection-level receive window size in bytes (RFC 9113 §6.9.2).
498 pub initial_connection_window: u32,
499 /// Maximum concurrent streams (SETTINGS_MAX_CONCURRENT_STREAMS).
500 pub max_concurrent_streams: u32,
501 /// Shrink threshold ratio for recycled stream slots.
502 pub stream_shrink_ratio: u32,
503}
504
505impl Default for H2ConnectionConfig {
506 fn default() -> Self {
507 Self {
508 initial_connection_window: ENLARGED_CONNECTION_WINDOW,
509 max_concurrent_streams: DEFAULT_MAX_CONCURRENT_STREAMS,
510 stream_shrink_ratio: DEFAULT_STREAM_SHRINK_RATIO,
511 }
512 }
513}
514
515impl H2ConnectionConfig {
516 /// Create a validated config, clamping to safe bounds.
517 ///
518 /// - `initial_connection_window`: clamped to \[65535, 2^31-1\] per RFC 9113 §6.9
519 /// - `max_concurrent_streams`: minimum 1
520 /// - `stream_shrink_ratio`: minimum 2 (1 would defeat slot recycling)
521 pub fn new(
522 initial_connection_window: u32,
523 max_concurrent_streams: u32,
524 stream_shrink_ratio: u32,
525 ) -> Self {
526 let clamped_window =
527 initial_connection_window.clamp(DEFAULT_INITIAL_WINDOW_SIZE, FLOW_CONTROL_MAX_WINDOW);
528 if clamped_window != initial_connection_window {
529 warn!(
530 "{} h2_initial_connection_window {} clamped to [{}, {}]",
531 log_module_context!(),
532 initial_connection_window,
533 DEFAULT_INITIAL_WINDOW_SIZE,
534 FLOW_CONTROL_MAX_WINDOW
535 );
536 }
537 const MAX_SAFE_CONCURRENT_STREAMS: u32 = 10_000;
538 let clamped_streams = max_concurrent_streams.clamp(1, MAX_SAFE_CONCURRENT_STREAMS);
539 if max_concurrent_streams > MAX_SAFE_CONCURRENT_STREAMS {
540 error!(
541 "{} h2_max_concurrent_streams={} exceeds safe limit, clamped to {}",
542 log_module_context!(),
543 max_concurrent_streams,
544 MAX_SAFE_CONCURRENT_STREAMS
545 );
546 }
547 if clamped_streams != max_concurrent_streams
548 && max_concurrent_streams <= MAX_SAFE_CONCURRENT_STREAMS
549 {
550 warn!(
551 "{} h2_max_concurrent_streams {} clamped to minimum 1",
552 log_module_context!(),
553 max_concurrent_streams
554 );
555 }
556 let clamped_ratio = stream_shrink_ratio.max(2);
557 if clamped_ratio != stream_shrink_ratio {
558 warn!(
559 "{} h2_stream_shrink_ratio {} clamped to minimum 2",
560 log_module_context!(),
561 stream_shrink_ratio
562 );
563 }
564 let config = Self {
565 initial_connection_window: clamped_window,
566 max_concurrent_streams: clamped_streams,
567 stream_shrink_ratio: clamped_ratio,
568 };
569 // Post-conditions matching the documented clamp ranges. The window must
570 // stay within RFC 9113 §6.9's [65535, 2^31-1] (a window outside this
571 // band desynchronises flow control with the peer); max_concurrent_streams
572 // must be >= 1 (zero would refuse every stream); shrink_ratio must be
573 // >= 2 (1 defeats slot recycling, the whole point of the knob).
574 debug_assert!(
575 (DEFAULT_INITIAL_WINDOW_SIZE..=FLOW_CONTROL_MAX_WINDOW)
576 .contains(&config.initial_connection_window),
577 "clamped connection window must lie within RFC 9113 §6.9 bounds"
578 );
579 debug_assert!(
580 config.max_concurrent_streams >= 1,
581 "clamped max_concurrent_streams must be >= 1"
582 );
583 debug_assert!(
584 config.stream_shrink_ratio >= 2,
585 "clamped stream_shrink_ratio must be >= 2 to keep slot recycling effective"
586 );
587 config
588 }
589
590 /// Create from optional config values, falling back to compile-time defaults.
591 /// Combines unwrap-or-default with validation clamping.
592 pub fn from_optional(
593 window: Option<u32>,
594 max_streams: Option<u32>,
595 shrink_ratio: Option<u32>,
596 ) -> Self {
597 let defaults = Self::default();
598 Self::new(
599 window.unwrap_or(defaults.initial_connection_window),
600 max_streams.unwrap_or(defaults.max_concurrent_streams),
601 shrink_ratio.unwrap_or(defaults.stream_shrink_ratio),
602 )
603 }
604}
605
606/// Default pending WINDOW_UPDATE capacity (used in tests).
607/// The actual per-connection cap is computed from `connection_config.max_concurrent_streams`.
608#[cfg(test)]
609const DEFAULT_MAX_PENDING_WINDOW_UPDATES: usize = 1 + DEFAULT_MAX_CONCURRENT_STREAMS as usize * 4;
610
611/// Maximum number of pending RST_STREAM frames before triggering GOAWAY.
612/// When a peer causes excessive RST_STREAM queueing (e.g. rapid stream creation
613/// beyond MAX_CONCURRENT_STREAMS), this cap prevents unbounded memory growth
614/// and triggers an ENHANCE_YOUR_CALM connection error.
615const MAX_PENDING_RST_STREAMS: usize = 200;
616
617/// RFC 9113 §6.5: maximum time (in seconds) to wait for SETTINGS ACK before
618/// sending GOAWAY with SETTINGS_TIMEOUT error code.
619const SETTINGS_ACK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
620
621#[inline(always)]
622fn error_nom_to_h2(error: nom::Err<parser::ParserError>) -> H2Error {
623 match error {
624 nom::Err::Error(parser::ParserError {
625 kind: parser::ParserErrorKind::H2(e),
626 ..
627 }) => e,
628 nom::Err::Failure(parser::ParserError {
629 kind: parser::ParserErrorKind::H2(e),
630 ..
631 }) => e,
632 _ => H2Error::ProtocolError,
633 }
634}
635
636/// Distribute connection-level byte overhead proportionally to a single stream.
637///
638/// Overhead is distributed in proportion to the bytes this stream transferred
639/// relative to the total across all active streams. A stream that transferred
640/// 60% of total bytes gets 60% of the overhead.
641///
642/// `stream_bytes` and `total_bytes` are `(bytes_in, bytes_out)` tuples.
643/// Falls back to even distribution (1/active_streams) when no stream has
644/// transferred any bytes yet (total is zero).
645///
646/// Extracted as a free function to avoid borrow conflicts when `self` fields
647/// (e.g. `encoder`) are borrowed by the converter while we need to update
648/// per-stream metrics and connection overhead counters.
649fn distribute_overhead(
650 metrics: &mut SessionMetrics,
651 overhead_bin: &mut usize,
652 overhead_bout: &mut usize,
653 stream_bytes: (usize, usize),
654 total_bytes: (usize, usize),
655 active_streams: usize,
656 is_last_stream: bool,
657) {
658 let share_in = if is_last_stream {
659 // Last stream gets all remaining overhead to avoid losing remainder bytes
660 // from integer division across earlier streams.
661 *overhead_bin
662 } else if total_bytes.0 > 0 {
663 // Clamp to remaining overhead — integer division rounding across multiple
664 // streams can cause accumulated shares to exceed the total.
665 (*overhead_bin * stream_bytes.0 / total_bytes.0).min(*overhead_bin)
666 } else {
667 // No stream has transferred any inbound bytes — fall back to even split.
668 *overhead_bin / active_streams.max(1)
669 };
670 let share_out = if is_last_stream {
671 *overhead_bout
672 } else if total_bytes.1 > 0 {
673 (*overhead_bout * stream_bytes.1 / total_bytes.1).min(*overhead_bout)
674 } else {
675 // No stream has transferred any outbound bytes — fall back to even split.
676 *overhead_bout / active_streams.max(1)
677 };
678 // Pre-condition: a stream can never be credited more overhead than remains
679 // in the pool — otherwise the `*overhead_b* -= share_*` below underflows
680 // (usize wraps to a huge value, corrupting connection-overhead accounting).
681 // Every branch above either takes the whole pool (last stream) or `.min`s
682 // against it, so this must hold.
683 debug_assert!(
684 share_in <= *overhead_bin,
685 "overhead-in share must not exceed the remaining overhead pool"
686 );
687 debug_assert!(
688 share_out <= *overhead_bout,
689 "overhead-out share must not exceed the remaining overhead pool"
690 );
691 let before_bin = *overhead_bin;
692 let before_bout = *overhead_bout;
693 metrics.bin += share_in;
694 metrics.bout += share_out;
695 *overhead_bin -= share_in;
696 *overhead_bout -= share_out;
697 // Post-condition: the pool shrinks by exactly the credited share (overhead
698 // is conserved, neither created nor lost). The last stream drains it to 0.
699 debug_assert_eq!(
700 *overhead_bin,
701 before_bin - share_in,
702 "overhead-in pool must decrease by exactly the credited share"
703 );
704 debug_assert_eq!(
705 *overhead_bout,
706 before_bout - share_out,
707 "overhead-out pool must decrease by exactly the credited share"
708 );
709 debug_assert!(
710 !is_last_stream || (*overhead_bin == 0 && *overhead_bout == 0),
711 "the last stream must drain the overhead pool to zero (no lost remainder)"
712 );
713}
714
715/// LIFECYCLE §9 invariant 16 probe: returns `true` if any open stream still
716/// has outbound kawa bytes queued (`back.out` non-empty or `back.blocks`
717/// non-drained).
718///
719/// Used by `finalize_write` to preserve `Ready::WRITABLE` across a voluntary
720/// scheduler yield, and by `has_pending_write_full` to block shutdown-drain
721/// while bytes are still owed to the frontend.
722///
723/// `.get()` rather than direct indexing: an unknown `GlobalStreamId` is
724/// treated as "no pending bytes" rather than panicking — defence-in-depth
725/// against a stream-removal race during shutdown.
726fn any_stream_has_pending_back(
727 streams: &HashMap<StreamId, GlobalStreamId>,
728 context_streams: &[Stream],
729) -> bool {
730 any_stream_id_matches(streams, |gid| {
731 context_streams
732 .get(gid)
733 .is_some_and(|s| !s.back.out.is_empty() || !s.back.blocks.is_empty())
734 })
735}
736
737/// Iteration core of [`any_stream_has_pending_back`], split out so the
738/// invariant-16 dispatch is unit-testable without a full [`Stream`] fixture
739/// (the existing test module only covers `H2FloodDetector`).
740fn any_stream_id_matches<F>(streams: &HashMap<StreamId, GlobalStreamId>, mut probe: F) -> bool
741where
742 F: FnMut(GlobalStreamId) -> bool,
743{
744 streams.values().any(|gid| probe(*gid))
745}
746
747/// Collect the live streams that have exceeded `deadline` under either
748/// per-stream reap guard, deduped so a stream tripping both is reaped (and
749/// access-logged) exactly once. Split out from
750/// [`ConnectionH2::cancel_timed_out_streams`] so the two-guard union is
751/// unit-testable without a full `ConnectionH2` fixture (the existing test
752/// module only fixtures `H2FloodDetector` and `Stream`):
753///
754/// - `last_activity` — bidirectional-silence guard: no DATA/HEADERS in either
755/// direction (the slow-multiplex Slowloris timer).
756/// - `fc_stalled` — outbound-flow-control-starvation guard: a buffered response
757/// that cannot drain because the peer keeps its receive window shut (the
758/// HTTP/2 window-stall / WINDOW_UPDATE-drip vector). This guard is what the
759/// liveness timer misses: an inbound 1-byte DATA drip keeps `last_activity`
760/// warm, but never touches `fc_stalled`.
761///
762/// Streams not in `live_streams` or already in `rst_sent` are skipped. The
763/// returned reason string is the access-log tag for the guard that tripped
764/// first (idle takes precedence on a tie, purely for a stable label).
765fn collect_timed_out_streams(
766 last_activity: &HashMap<StreamId, Instant>,
767 fc_stalled: &HashMap<StreamId, Instant>,
768 live_streams: &HashMap<StreamId, GlobalStreamId>,
769 rst_sent: &HashSet<StreamId>,
770 now: Instant,
771 deadline: std::time::Duration,
772) -> Vec<(StreamId, &'static str)> {
773 let eligible = |sid: StreamId| live_streams.contains_key(&sid) && !rst_sent.contains(&sid);
774 let expired = |t: Instant| now.saturating_duration_since(t) > deadline;
775 let mut seen: HashSet<StreamId> = HashSet::new();
776 let mut out: Vec<(StreamId, &'static str)> = Vec::new();
777 for (&sid, &t) in last_activity {
778 if eligible(sid) && expired(t) && seen.insert(sid) {
779 out.push((sid, "H2::IdleTimeout"));
780 }
781 }
782 for (&sid, &t) in fc_stalled {
783 if eligible(sid) && expired(t) && seen.insert(sid) {
784 out.push((sid, "H2::WindowStall"));
785 }
786 }
787 out
788}
789
790/// True when a stream still has response/upload bytes that could be put on the
791/// wire — headers/body in flight, or a terminated-but-not-fully-flushed buffer.
792/// Deliberately EXCLUDES `is_error()`/`rst_sent`: that disjunct is specific to
793/// the priority-eligibility and write-loop gates (`write_streams`) and must stay
794/// inline there; this 2-clause helper backs ONLY the window-stall arm.
795fn has_sendable_response(kawa: &GenericHttpStream) -> bool {
796 kawa.is_main_phase() || (kawa.is_terminated() && !kawa.is_completed())
797}
798
799/// Outcome of the M2 cumulative-stall budget decision for one `write_streams`
800/// pass on a window-stalled stream. Extracted from the `write_streams` arm so
801/// the budget logic is unit-testable without a full `ConnectionH2` fixture
802/// (mirrors the [`collect_timed_out_streams`] extraction).
803#[derive(Debug, Clone, Copy, PartialEq, Eq)]
804enum FcStallAction {
805 /// Clear both the deadline (`stream_fc_stalled_since`) and the progress
806 /// accumulator (`stream_fc_stalled_progress`) for this stream.
807 Clear,
808 /// Ensure the deadline is armed (WITHOUT refreshing an existing `Instant`)
809 /// and set the progress accumulator to `progress`.
810 Arm { progress: usize },
811}
812
813/// Decide what to do with a stream's flow-control-stall deadline + cumulative
814/// progress accumulator on one write pass (M2 cumulative-stall budget).
815///
816/// - A genuinely open send window (`!outbound_window_blocked`) is a real
817/// un-stall → [`FcStallAction::Clear`].
818/// - While the window stays blocked, accumulate this pass's outbound drain
819/// (`consumed`, clamped to `>= 0`) onto `prior_progress`. Once the cumulative
820/// total reaches [`FC_STALL_CLEAR_FLOOR`] (a full DATA frame of real delivery)
821/// → `Clear`; otherwise `Arm` with the running total. A `WINDOW_UPDATE(+1)`
822/// drip adds ~1 byte/pass and never reaches the floor, so the deadline keeps
823/// aging and the reaper eventually fires.
824fn fc_stall_budget_decision(
825 outbound_window_blocked: bool,
826 consumed: i32,
827 prior_progress: Option<usize>,
828) -> FcStallAction {
829 if !outbound_window_blocked {
830 return FcStallAction::Clear;
831 }
832 let progressed = prior_progress
833 .unwrap_or(0)
834 .saturating_add(consumed.max(0) as usize);
835 if progressed >= FC_STALL_CLEAR_FLOOR {
836 FcStallAction::Clear
837 } else {
838 FcStallAction::Arm {
839 progress: progressed,
840 }
841 }
842}
843
844/// Core of [`ConnectionH2::enqueue_rst`], extracted so the RST-queueing
845/// semantics (dedupe, queued-cap counter bump, invariant-15 readiness rearm)
846/// can be unit-tested without building a full `ConnectionH2<Front>` fixture.
847///
848/// Invariants enforced:
849/// - **Dedupe** via `rst_sent`: at most one queued RST per wire stream id.
850/// `HashSet::insert` returns `false` when the id is already present; we
851/// short-circuit on that branch to keep `pending_rst_streams`,
852/// `total_rst_streams_queued` and the wire counts consistent.
853/// - **MadeYouReset queued cap** (`MAX_PENDING_RST_STREAMS`): each freshly
854/// queued RST bumps `total_rst_streams_queued`, which
855/// `flush_pending_control_frames` polices to escalate to
856/// `GOAWAY(ENHANCE_YOUR_CALM)` when exceeded.
857/// - **Invariant 15** (edge-triggered epoll): pair `Ready::WRITABLE` interest
858/// with the event bit so `writable()` is scheduled on the next tick.
859///
860/// Returns `true` when the RST was freshly queued, `false` when the
861/// stream was already in `rst_sent` (the caller asked to RST the same
862/// stream twice — a benign re-entrant idempotency, NOT a new wire
863/// emission). The boolean lets [`ConnectionH2::enqueue_rst`] account
864/// the RST only on the freshly-queued path so duplicate calls do not
865/// inflate the per-error counter or trip the MadeYouReset flood cap
866/// for frames that never reach the wire.
867fn enqueue_rst_into(
868 pending: &mut Vec<(StreamId, H2Error)>,
869 total: &mut usize,
870 rst_sent: &mut HashSet<StreamId>,
871 readiness: &mut Readiness,
872 wire_stream_id: StreamId,
873 error: H2Error,
874) -> bool {
875 let pending_before = pending.len();
876 let total_before = *total;
877 if !rst_sent.insert(wire_stream_id) {
878 // Dedupe short-circuit: the id was already queued/flushed. We must NOT
879 // touch any of the wire-count state, otherwise duplicate calls inflate
880 // the MadeYouReset (CVE-2025-8671) lifetime cap with frames that never
881 // reach the wire.
882 debug_assert!(
883 rst_sent.contains(&wire_stream_id),
884 "dedupe path requires the id to already be present in rst_sent"
885 );
886 debug_assert_eq!(
887 pending.len(),
888 pending_before,
889 "dedupe path must not enqueue a new pending RST"
890 );
891 debug_assert_eq!(
892 *total, total_before,
893 "dedupe path must not bump the queued-RST lifetime counter"
894 );
895 return false;
896 }
897 pending.push((wire_stream_id, error));
898 *total += 1;
899 readiness.arm_writable();
900 // Post-condition: a freshly-queued RST advances both the pending Vec and the
901 // lifetime counter by exactly one, and the id is now tracked for dedupe.
902 debug_assert!(
903 rst_sent.contains(&wire_stream_id),
904 "freshly-queued RST must be recorded in rst_sent for future dedupe"
905 );
906 debug_assert_eq!(
907 pending.len(),
908 pending_before + 1,
909 "a freshly-queued RST must push exactly one pending entry"
910 );
911 debug_assert_eq!(
912 *total,
913 total_before + 1,
914 "a freshly-queued RST must bump the queued-RST lifetime counter by one"
915 );
916 debug_assert_eq!(
917 pending.last().map(|(id, _)| *id),
918 Some(wire_stream_id),
919 "the just-pushed entry must be the requested wire stream id"
920 );
921 true
922}
923
924/// Detail of a flood-threshold violation returned by
925/// [`H2FloodDetector::check_flood`] and [`H2FloodDetector::record_rst_lifetime`].
926///
927/// Carrying `(reason, count, threshold)` lets the caller emit a session-scoped
928/// log line with full context — the detector itself is connection-agnostic and
929/// never logs.
930#[derive(Debug, Clone, PartialEq)]
931pub struct H2FloodViolation {
932 /// HTTP/2 error code to emit on the GOAWAY.
933 pub error: H2Error,
934 /// Human-readable name of the counter that tripped (e.g. `"RST_STREAM"`).
935 pub reason: &'static str,
936 /// Statsd metric key emitted by [`ConnectionH2::handle_flood_violation`].
937 /// Carried alongside `reason` so a single field maps to both the log line
938 /// and the dashboard counter — adding a new violation kind requires
939 /// choosing both at the construction site, preventing drift.
940 pub metric_key: &'static str,
941 /// Observed counter value at the moment of detection.
942 pub count: u64,
943 /// Configured ceiling that was crossed.
944 pub threshold: u64,
945}
946
947/// Tracks per-connection frame rates to detect and mitigate H2 flood attacks.
948///
949/// Monitors RST_STREAM (CVE-2023-44487), PING (CVE-2019-9512), SETTINGS (CVE-2019-9515),
950/// empty DATA (CVE-2019-9518), and CONTINUATION (CVE-2024-27316) flood patterns.
951/// When any counter exceeds its threshold, `check_flood()` returns the violation
952/// detail so callers can log with connection context before sending GOAWAY.
953///
954/// Thresholds are configurable via [`H2FloodConfig`], with safe defaults matching
955/// the original compile-time constants.
956#[derive(Debug)]
957pub struct H2FloodDetector {
958 /// RST_STREAM frames received in current window (CVE-2023-44487 + CVE-2019-9514)
959 pub(super) rst_stream_count: u32,
960 /// Lifetime RST_STREAM frames received on this connection.
961 ///
962 /// Never decays — provides an absolute ceiling that the half-decaying
963 /// per-window counter cannot, preventing a sustained ~50 RST/sec burst
964 /// from running forever.
965 pub(super) total_rst_received_lifetime: u64,
966 /// Lifetime RST_STREAM frames received that targeted a stream whose
967 /// backend response had not yet started. These are the "Rapid Reset"
968 /// signature — cheap for the attacker, expensive for the proxy — and
969 /// trip on a much lower ceiling than the generic lifetime counter.
970 pub(super) total_abusive_rst_received_lifetime: u64,
971 /// Lifetime RST_STREAM frames **emitted by the server** on this
972 /// connection (CVE-2025-8671 "MadeYouReset" mitigation). Incremented
973 /// inside [`ConnectionH2::reset_stream`] whenever a non-`NoError` reset
974 /// is triggered by an attacker-crafted frame (content-length mismatch,
975 /// header parse error, priority rejection, zero-increment WINDOW_UPDATE
976 /// on an open stream). Never decays — provides an absolute ceiling that
977 /// short-circuits patient-attacker patterns that stay under any windowed
978 /// counter.
979 pub(super) total_rst_streams_emitted_lifetime: u64,
980 /// PING frames received in current window (CVE-2019-9512)
981 pub(super) ping_count: u32,
982 /// Lifetime PING frames received on this connection.
983 ///
984 /// Never decays — provides an absolute ceiling that the half-decaying
985 /// per-window counter cannot, preventing sustained low-rate PING abuse.
986 pub(super) total_ping_received_lifetime: u32,
987 /// SETTINGS frames received in current window (CVE-2019-9515)
988 pub(super) settings_count: u32,
989 /// Lifetime SETTINGS frames received on this connection.
990 ///
991 /// Never decays — provides an absolute ceiling that the half-decaying
992 /// per-window counter cannot, preventing sustained low-rate SETTINGS abuse.
993 pub(super) total_settings_received_lifetime: u32,
994 /// Empty DATA frames received in current window (CVE-2019-9518)
995 pub(super) empty_data_count: u32,
996 /// Connection-level (stream 0) WINDOW_UPDATE frames received in current
997 /// sliding window. Half-decays with [`maybe_reset_window`] like other
998 /// rate counters. Increments on non-zero stream-0 WINDOW_UPDATEs only —
999 /// zero-increment frames short-circuit into GOAWAY(PROTOCOL_ERROR) per
1000 /// RFC 9113 §6.9 before reaching this counter.
1001 pub(super) window_update_stream0_count: u32,
1002 /// CONTINUATION frames received for current header block (CVE-2024-27316)
1003 pub(super) continuation_count: u32,
1004 /// Total accumulated header block size across CONTINUATION frames
1005 pub(super) accumulated_header_size: u32,
1006 /// General anomaly counter
1007 pub(super) glitch_count: u32,
1008 /// Window start for rate-based counters
1009 pub(super) window_start: Instant,
1010 /// Configurable thresholds for flood detection
1011 pub(super) config: H2FloodConfig,
1012}
1013
1014impl Default for H2FloodDetector {
1015 fn default() -> Self {
1016 Self::new(H2FloodConfig::default())
1017 }
1018}
1019
1020impl H2FloodDetector {
1021 pub fn new(config: H2FloodConfig) -> Self {
1022 // Pre-condition: thresholds are already validated (clamped to >= 1 by
1023 // `H2FloodConfig::new`). A zero per-window threshold would trip on the
1024 // first counted frame; assert it here so a config that bypassed `new`
1025 // (raw struct literal in a future caller) is caught in debug.
1026 debug_assert!(
1027 config.max_rst_stream_per_window >= 1
1028 && config.max_ping_per_window >= 1
1029 && config.max_settings_per_window >= 1
1030 && config.max_continuation_frames >= 1
1031 && config.max_glitch_count >= 1,
1032 "flood detector must be constructed with validated (>= 1) thresholds"
1033 );
1034 Self {
1035 rst_stream_count: 0,
1036 total_rst_received_lifetime: 0,
1037 total_abusive_rst_received_lifetime: 0,
1038 total_rst_streams_emitted_lifetime: 0,
1039 ping_count: 0,
1040 total_ping_received_lifetime: 0,
1041 settings_count: 0,
1042 total_settings_received_lifetime: 0,
1043 empty_data_count: 0,
1044 window_update_stream0_count: 0,
1045 continuation_count: 0,
1046 accumulated_header_size: 0,
1047 glitch_count: 0,
1048 window_start: Instant::now(),
1049 config,
1050 }
1051 }
1052
1053 /// Increment the lifetime RST_STREAM counters and return a
1054 /// [`H2FloodViolation`] if either the global or the abusive
1055 /// (pre-response-start) lifetime cap has been exceeded.
1056 ///
1057 /// `response_started` indicates whether the backend response had already
1058 /// begun when the RST arrived; `false` is the cheap-for-client /
1059 /// expensive-for-us Rapid Reset signature (CVE-2023-44487).
1060 pub fn record_rst_lifetime(&mut self, response_started: bool) -> Option<H2FloodViolation> {
1061 let total_before = self.total_rst_received_lifetime;
1062 let abusive_before = self.total_abusive_rst_received_lifetime;
1063 self.total_rst_received_lifetime = self.total_rst_received_lifetime.saturating_add(1);
1064 if !response_started {
1065 self.total_abusive_rst_received_lifetime =
1066 self.total_abusive_rst_received_lifetime.saturating_add(1);
1067 }
1068 // Monotonicity: the global lifetime counter advances by one per call
1069 // (until saturation), and the abusive sub-counter advances iff the RST
1070 // arrived before the backend response started. The abusive counter can
1071 // never exceed the global one — every abusive RST is also a received RST.
1072 debug_assert!(
1073 self.total_rst_received_lifetime >= total_before,
1074 "lifetime RST counter must be monotonic non-decreasing"
1075 );
1076 debug_assert_eq!(
1077 self.total_abusive_rst_received_lifetime > abusive_before,
1078 !response_started,
1079 "abusive RST counter advances iff the RST is pre-response-start"
1080 );
1081 debug_assert!(
1082 self.total_abusive_rst_received_lifetime <= self.total_rst_received_lifetime,
1083 "abusive RST count is a subset of total received RST count"
1084 );
1085 if self.total_rst_received_lifetime > self.config.max_rst_stream_lifetime {
1086 return Some(H2FloodViolation {
1087 error: H2Error::EnhanceYourCalm,
1088 reason: "Rapid Reset: lifetime RST_STREAM",
1089 metric_key: "h2.flood.violation.rst_stream_lifetime",
1090 count: self.total_rst_received_lifetime,
1091 threshold: self.config.max_rst_stream_lifetime,
1092 });
1093 }
1094 if self.total_abusive_rst_received_lifetime > self.config.max_rst_stream_abusive_lifetime {
1095 return Some(H2FloodViolation {
1096 error: H2Error::EnhanceYourCalm,
1097 reason: "Rapid Reset: lifetime pre-response RST_STREAM",
1098 metric_key: "h2.flood.violation.rst_stream_pre_response_lifetime",
1099 count: self.total_abusive_rst_received_lifetime,
1100 threshold: self.config.max_rst_stream_abusive_lifetime,
1101 });
1102 }
1103 None
1104 }
1105
1106 /// Increment the lifetime **server-emitted** RST_STREAM counter and
1107 /// return a [`H2FloodViolation`] once the configured ceiling is exceeded.
1108 ///
1109 /// Call sites are the error paths inside [`ConnectionH2::reset_stream`]
1110 /// where an attacker-crafted frame coerces the server into emitting a
1111 /// RST_STREAM (CVE-2025-8671 "MadeYouReset"). Only non-`NoError` resets
1112 /// are reported — callers must exclude graceful cancels.
1113 pub fn record_rst_emitted(&mut self) -> Option<H2FloodViolation> {
1114 let before = self.total_rst_streams_emitted_lifetime;
1115 self.total_rst_streams_emitted_lifetime =
1116 self.total_rst_streams_emitted_lifetime.saturating_add(1);
1117 // Monotonic: the emitted-RST counter never decays (it is the absolute
1118 // MadeYouReset ceiling, CVE-2025-8671), so each call strictly advances
1119 // it until u64 saturation.
1120 debug_assert!(
1121 self.total_rst_streams_emitted_lifetime > before || before == u64::MAX,
1122 "emitted-RST lifetime counter must advance (or already be saturated)"
1123 );
1124 if self.total_rst_streams_emitted_lifetime > self.config.max_rst_stream_emitted_lifetime {
1125 return Some(H2FloodViolation {
1126 error: H2Error::EnhanceYourCalm,
1127 reason: "MadeYouReset: lifetime server-emitted RST_STREAM",
1128 metric_key: "h2.flood.violation.rst_stream_emitted_lifetime",
1129 count: self.total_rst_streams_emitted_lifetime,
1130 threshold: self.config.max_rst_stream_emitted_lifetime,
1131 });
1132 }
1133 None
1134 }
1135
1136 /// Half-decay rate-based counters if the current window has expired.
1137 /// Uses half-window decay instead of full reset to catch burst-then-wait attacks.
1138 fn maybe_reset_window(&mut self) {
1139 if self.window_start.elapsed() >= FLOOD_WINDOW_DURATION {
1140 let (rst_before, ping_before, settings_before) =
1141 (self.rst_stream_count, self.ping_count, self.settings_count);
1142 let (empty_before, wu0_before, glitch_before) = (
1143 self.empty_data_count,
1144 self.window_update_stream0_count,
1145 self.glitch_count,
1146 );
1147 self.rst_stream_count /= 2;
1148 self.ping_count /= 2;
1149 self.settings_count /= 2;
1150 self.empty_data_count /= 2;
1151 self.window_update_stream0_count /= 2;
1152 self.glitch_count /= 2;
1153 self.window_start = Instant::now();
1154 // Half-decay invariant: each rate-based counter is exactly halved
1155 // (integer division), never increased. Catching burst-then-wait
1156 // attacks relies on the counter shrinking but not vanishing — a
1157 // full reset would let a patient attacker reset to zero each window.
1158 debug_assert_eq!(self.rst_stream_count, rst_before / 2, "RST count halves");
1159 debug_assert_eq!(self.ping_count, ping_before / 2, "PING count halves");
1160 debug_assert_eq!(
1161 self.settings_count,
1162 settings_before / 2,
1163 "SETTINGS count halves"
1164 );
1165 debug_assert_eq!(
1166 self.empty_data_count,
1167 empty_before / 2,
1168 "empty-DATA count halves"
1169 );
1170 debug_assert_eq!(
1171 self.window_update_stream0_count,
1172 wu0_before / 2,
1173 "stream-0 WINDOW_UPDATE count halves"
1174 );
1175 debug_assert_eq!(self.glitch_count, glitch_before / 2, "glitch count halves");
1176 // The lifetime counters are deliberately NOT touched here — they are
1177 // the never-decaying ceilings. Guard against a future edit decaying
1178 // them by accident.
1179 debug_assert!(
1180 self.window_start.elapsed() < FLOOD_WINDOW_DURATION,
1181 "window_start must be refreshed to (approximately) now after decay"
1182 );
1183 }
1184 }
1185
1186 /// Check all flood counters. Returns a [`H2FloodViolation`] when a threshold
1187 /// is exceeded; the caller is responsible for logging with session context
1188 /// and escalating to GOAWAY.
1189 pub fn check_flood(&mut self) -> Option<H2FloodViolation> {
1190 self.maybe_reset_window();
1191
1192 fn flag(
1193 reason: &'static str,
1194 metric_key: &'static str,
1195 count: u32,
1196 threshold: u32,
1197 ) -> Option<H2FloodViolation> {
1198 if count > threshold {
1199 Some(H2FloodViolation {
1200 error: H2Error::EnhanceYourCalm,
1201 reason,
1202 metric_key,
1203 count: count as u64,
1204 threshold: threshold as u64,
1205 })
1206 } else {
1207 None
1208 }
1209 }
1210
1211 let violation = flag(
1212 "RST_STREAM",
1213 "h2.flood.violation.rst_stream_window",
1214 self.rst_stream_count,
1215 self.config.max_rst_stream_per_window,
1216 )
1217 .or_else(|| {
1218 flag(
1219 "PING",
1220 "h2.flood.violation.ping_window",
1221 self.ping_count,
1222 self.config.max_ping_per_window,
1223 )
1224 })
1225 .or_else(|| {
1226 flag(
1227 "PING lifetime",
1228 "h2.flood.violation.ping_lifetime",
1229 self.total_ping_received_lifetime,
1230 DEFAULT_MAX_PING_LIFETIME,
1231 )
1232 })
1233 .or_else(|| {
1234 flag(
1235 "SETTINGS",
1236 "h2.flood.violation.settings_window",
1237 self.settings_count,
1238 self.config.max_settings_per_window,
1239 )
1240 })
1241 .or_else(|| {
1242 flag(
1243 "SETTINGS lifetime",
1244 "h2.flood.violation.settings_lifetime",
1245 self.total_settings_received_lifetime,
1246 DEFAULT_MAX_SETTINGS_LIFETIME,
1247 )
1248 })
1249 .or_else(|| {
1250 flag(
1251 "empty DATA",
1252 "h2.flood.violation.empty_data_window",
1253 self.empty_data_count,
1254 self.config.max_empty_data_per_window,
1255 )
1256 })
1257 .or_else(|| {
1258 flag(
1259 "CONTINUATION",
1260 "h2.flood.violation.continuation_per_block",
1261 self.continuation_count,
1262 self.config.max_continuation_frames,
1263 )
1264 })
1265 .or_else(|| {
1266 flag(
1267 "WINDOW_UPDATE stream 0",
1268 "h2.flood.violation.window_update_stream0_window",
1269 self.window_update_stream0_count,
1270 self.config.max_window_update_stream0_per_window,
1271 )
1272 })
1273 .or_else(|| {
1274 flag(
1275 "accumulated header size",
1276 "h2.flood.violation.header_size_per_block",
1277 self.accumulated_header_size,
1278 self.config.max_header_list_size,
1279 )
1280 })
1281 .or_else(|| {
1282 flag(
1283 "glitch",
1284 "h2.flood.violation.glitch_window",
1285 self.glitch_count,
1286 self.config.max_glitch_count,
1287 )
1288 });
1289 // Post-condition: any reported violation is well-formed — every H2
1290 // flood escalation is an ENHANCE_YOUR_CALM connection error, and the
1291 // observed count strictly exceeds the threshold it tripped (the `flag`
1292 // helper and the lifetime checks all use strict `>`). A violation whose
1293 // count <= threshold would be a false positive terminating a healthy
1294 // connection.
1295 debug_assert!(
1296 violation
1297 .as_ref()
1298 .is_none_or(|v| v.error == H2Error::EnhanceYourCalm && v.count > v.threshold),
1299 "a flood violation must be EnhanceYourCalm with count strictly above threshold"
1300 );
1301 violation
1302 }
1303
1304 /// Reset CONTINUATION-specific counters when a header block is complete.
1305 pub fn reset_continuation(&mut self) {
1306 self.continuation_count = 0;
1307 self.accumulated_header_size = 0;
1308 // Post-condition: both CONTINUATION-block accumulators are cleared so
1309 // the next header block starts from zero (CVE-2024-27316 per-block
1310 // accounting must not leak across blocks).
1311 debug_assert_eq!(
1312 self.continuation_count, 0,
1313 "continuation_count must be zero after a block completes"
1314 );
1315 debug_assert_eq!(
1316 self.accumulated_header_size, 0,
1317 "accumulated_header_size must be zero after a block completes"
1318 );
1319 }
1320}
1321
1322#[derive(Debug)]
1323pub enum H2State {
1324 ClientPreface,
1325 ClientSettings,
1326 ServerSettings,
1327 Header,
1328 Frame(FrameHeader),
1329 ContinuationHeader(Headers),
1330 ContinuationFrame(Headers),
1331 GoAway,
1332 Error,
1333 Discard,
1334}
1335
1336#[derive(Debug, Clone, Copy)]
1337pub struct H2Settings {
1338 pub settings_header_table_size: u32,
1339 pub settings_enable_push: bool,
1340 pub settings_max_concurrent_streams: u32,
1341 pub settings_initial_window_size: u32,
1342 pub settings_max_frame_size: u32,
1343 pub settings_max_header_list_size: u32,
1344 /// RFC 8441
1345 pub settings_enable_connect_protocol: bool,
1346 /// RFC 9218
1347 pub settings_no_rfc7540_priorities: bool,
1348}
1349
1350impl Default for H2Settings {
1351 fn default() -> Self {
1352 Self {
1353 settings_header_table_size: DEFAULT_HEADER_TABLE_SIZE,
1354 settings_enable_push: false,
1355 settings_max_concurrent_streams: DEFAULT_MAX_CONCURRENT_STREAMS,
1356 settings_initial_window_size: DEFAULT_INITIAL_WINDOW_SIZE,
1357 settings_max_frame_size: DEFAULT_MAX_FRAME_SIZE,
1358 settings_max_header_list_size: MAX_HEADER_LIST_SIZE as u32,
1359 settings_enable_connect_protocol: false,
1360 settings_no_rfc7540_priorities: true,
1361 }
1362 }
1363}
1364
1365/// RFC 9218 Extensible Priorities for HTTP stream scheduling.
1366///
1367/// Stores per-stream urgency (0-7, lower = more important) and incremental
1368/// flag. Used by `writable()` to sort streams: lower urgency first, then
1369/// stream ID for stability among same-urgency non-incremental streams.
1370///
1371/// Within a same-urgency bucket the scheduler (see
1372/// [`ConnectionH2::write_streams`]) drains non-incremental streams
1373/// sequentially, then applies RFC 9218 §4 round-robin to the incremental
1374/// streams starting from [`Self::incremental_cursor`], so multiple concurrent
1375/// downloads at the same urgency interleave their DATA frames fairly.
1376///
1377/// Streams without an explicit `priority` header get the RFC 9218 defaults:
1378/// urgency 3, incremental false.
1379#[derive(Default)]
1380pub struct Prioriser {
1381 /// Per-stream priority: stream_id -> (urgency 0-7, incremental flag)
1382 priorities: HashMap<StreamId, (u8, bool)>,
1383 /// RFC 9218 §4 round-robin cursor: stream ID that fired first in the
1384 /// last write pass over the incremental tail of the lowest-urgency
1385 /// bucket that contained at least one incremental stream. The next pass
1386 /// starts from the stream immediately after this ID (wrapping around),
1387 /// so a single slow-draining stream cannot hog the connection.
1388 ///
1389 /// `0` is the "no cursor yet" sentinel and means "start from the
1390 /// smallest ID in the bucket" — H2 stream IDs are always > 0.
1391 incremental_cursor: StreamId,
1392}
1393
1394/// RFC 9218 §4 default urgency value.
1395const DEFAULT_URGENCY: u8 = 3;
1396
1397/// Maximum entries in the priority map to prevent flooding via PRIORITY frames.
1398const MAX_PRIORITIES: usize = 4096;
1399
1400/// Small look-ahead window (in stream IDs) for PRIORITY frames that arrive
1401/// slightly before the peer opens the corresponding stream. RFC 9218 allows
1402/// PRIORITY to be sent for an idle stream that the peer intends to open
1403/// soon. Past this budget we assume the ID will never be used and drop the
1404/// entry, preventing flooding with far-future stream IDs.
1405const PRIORITY_IDLE_LOOKAHEAD: u32 = 64;
1406
1407impl Prioriser {
1408 /// Record or update the priority for a stream that we know exists or are
1409 /// currently processing (used from pkawa's header-handling path where the
1410 /// owning stream's HEADERS frame is being decoded).
1411 ///
1412 /// Returns `true` if the priority is invalid (self-dependency for RFC 7540),
1413 /// signalling the caller should reset the stream with a protocol error.
1414 pub fn push_priority(&mut self, stream_id: StreamId, priority: parser::PriorityPart) -> bool {
1415 trace!(
1416 "{} PRIORITY REQUEST FOR {}: {:?}",
1417 log_module_context!(),
1418 stream_id,
1419 priority
1420 );
1421 // Pre-condition: the priority map never grows past MAX_PRIORITIES.
1422 // The cap is the only thing standing between a PRIORITY flood and
1423 // unbounded memory; assert it holds on entry (each insert path below
1424 // either updates an existing key or is gated by this check).
1425 debug_assert!(
1426 self.priorities.len() <= MAX_PRIORITIES,
1427 "priority map must never exceed MAX_PRIORITIES entries"
1428 );
1429 // Cap the priority map to prevent flooding via PRIORITY frames
1430 if !self.priorities.contains_key(&stream_id) && self.priorities.len() >= MAX_PRIORITIES {
1431 return false;
1432 }
1433 match priority {
1434 parser::PriorityPart::Rfc7540 {
1435 stream_dependency,
1436 weight: _,
1437 } => {
1438 // RFC 9113 §5.3.1: a stream cannot depend on itself; signal
1439 // the caller to RST_STREAM with PROTOCOL_ERROR. Otherwise the
1440 // RFC 7540 priority tree is deprecated and silently ignored.
1441 stream_dependency.stream_id == stream_id
1442 }
1443 parser::PriorityPart::Rfc9218 {
1444 urgency,
1445 incremental,
1446 } => {
1447 // RFC 9218 §7.1: a malformed or out-of-range priority field
1448 // MUST be "treated as absent", NOT as a stream error. Clamping
1449 // an urgency > 7 to 7 is the policy-correct interpretation:
1450 // the field is still present (so defaulting would lose
1451 // information) but its value is normalised to the RFC's
1452 // allowed range [0..=7]. Intentionally not PROTOCOL_ERROR.
1453 self.priorities
1454 .insert(stream_id, (urgency.min(7), incremental));
1455 // Post-conditions: the entry now exists with a clamped urgency
1456 // in [0, 7] (the writable scheduler buckets by urgency and would
1457 // mis-order on a value above 7), and the map stays within its
1458 // memory cap.
1459 debug_assert!(
1460 self.priorities
1461 .get(&stream_id)
1462 .is_some_and(|(u, _)| *u <= 7),
1463 "stored RFC 9218 urgency must be clamped to [0, 7]"
1464 );
1465 debug_assert!(
1466 self.priorities.len() <= MAX_PRIORITIES,
1467 "priority map must stay within MAX_PRIORITIES after insert"
1468 );
1469 false
1470 }
1471 }
1472 }
1473
1474 /// Record or update the priority for a stream ID that arrived via a
1475 /// standalone PRIORITY frame.
1476 ///
1477 /// Pass 3 Medium #4: without this guard, a peer could send PRIORITY for
1478 /// arbitrary stream IDs (e.g. 2^31 ever-increasing IDs) and pin up to
1479 /// `MAX_PRIORITIES` entries of memory. Accept only:
1480 /// - an ID that corresponds to a currently-open stream (`open_streams`);
1481 /// - an idle ID slightly ahead of `last_stream_id` (within
1482 /// [`PRIORITY_IDLE_LOOKAHEAD`]), matching RFC 9218's "set priority for
1483 /// a stream about to be opened" pattern.
1484 ///
1485 /// IDs in the past that we do not currently track (already closed) and
1486 /// IDs too far in the future are silently dropped. The `MAX_PRIORITIES`
1487 /// ceiling is preserved as a defensive backstop if both filters are ever
1488 /// circumvented.
1489 ///
1490 /// Returns the same value semantics as [`Self::push_priority`].
1491 pub fn push_priority_guarded(
1492 &mut self,
1493 stream_id: StreamId,
1494 priority: parser::PriorityPart,
1495 last_stream_id: StreamId,
1496 open_streams: &HashMap<StreamId, GlobalStreamId>,
1497 ) -> bool {
1498 if !self.is_acceptable(stream_id, last_stream_id, open_streams) {
1499 trace!(
1500 "{} PRIORITY dropped for unknown/far stream {} (last_stream_id={})",
1501 log_module_context!(),
1502 stream_id,
1503 last_stream_id
1504 );
1505 return false;
1506 }
1507 self.push_priority(stream_id, priority)
1508 }
1509
1510 fn is_acceptable(
1511 &self,
1512 stream_id: StreamId,
1513 last_stream_id: StreamId,
1514 open_streams: &HashMap<StreamId, GlobalStreamId>,
1515 ) -> bool {
1516 if open_streams.contains_key(&stream_id) {
1517 return true;
1518 }
1519 // Idle stream ahead of the current counter: accept a small look-ahead.
1520 // Past IDs that are NOT in `open_streams` are closed — drop them.
1521 let upper = last_stream_id.saturating_add(PRIORITY_IDLE_LOOKAHEAD);
1522 stream_id > last_stream_id && stream_id <= upper
1523 }
1524
1525 /// Remove a stream's priority entry (called when the stream is recycled).
1526 pub fn remove(&mut self, stream_id: &StreamId) {
1527 let had = self.priorities.contains_key(stream_id);
1528 let before = self.priorities.len();
1529 self.priorities.remove(stream_id);
1530 // Post-conditions: the entry is truly gone, and the map shrinks by
1531 // exactly one iff it was present. A leak here re-introduces the
1532 // PRIORITY-flood memory exposure the cap defends against.
1533 debug_assert!(
1534 !self.priorities.contains_key(stream_id),
1535 "remove must evict the priority entry"
1536 );
1537 debug_assert_eq!(
1538 self.priorities.len(),
1539 before - had as usize,
1540 "priority map length drops by exactly one iff the id was present"
1541 );
1542 }
1543
1544 /// Look up the priority for a stream, returning RFC 9218 defaults if absent.
1545 #[inline]
1546 pub fn get(&self, stream_id: &StreamId) -> (u8, bool) {
1547 self.priorities
1548 .get(stream_id)
1549 .copied()
1550 .unwrap_or((DEFAULT_URGENCY, false))
1551 }
1552
1553 /// Reorder a pre-sorted slice of writable stream IDs so that inside each
1554 /// urgency bucket, incremental streams appear after non-incremental ones,
1555 /// and the incremental tail is rotated by [`Self::incremental_cursor`]
1556 /// (RFC 9218 §4).
1557 ///
1558 /// The input `buf` must already be sorted by `(urgency, stream_id)`:
1559 /// this routine only partitions and rotates inside same-urgency
1560 /// contiguous runs, it does not re-sort.
1561 ///
1562 /// Returns the total number of incremental streams seen, so callers that
1563 /// need to update the cursor at the end of the write pass can early-exit
1564 /// when the count is zero.
1565 pub fn apply_incremental_rotation(&self, buf: &mut [StreamId]) -> usize {
1566 // Pre-condition: callers must hand a slice already sorted by urgency so
1567 // same-urgency runs are contiguous (this routine only partitions/rotates
1568 // within a run, it does not re-sort across urgencies). A non-monotonic
1569 // urgency sequence would split one logical bucket into several and
1570 // mis-schedule the round-robin. `windows(2)` over a slice of size N is
1571 // dead code in release.
1572 #[cfg(debug_assertions)]
1573 debug_assert!(
1574 buf.windows(2)
1575 .all(|w| self.get(&w[0]).0 <= self.get(&w[1]).0),
1576 "apply_incremental_rotation requires input pre-sorted by urgency"
1577 );
1578 let len_before = buf.len();
1579 #[cfg(debug_assertions)]
1580 let expected_incremental = buf.iter().filter(|id| self.get(id).1).count();
1581 let mut total_incremental = 0usize;
1582 let mut i = 0;
1583 while i < buf.len() {
1584 let (urgency_i, _) = self.get(&buf[i]);
1585 let mut j = i + 1;
1586 while j < buf.len() {
1587 let (urgency_j, _) = self.get(&buf[j]);
1588 if urgency_j != urgency_i {
1589 break;
1590 }
1591 j += 1;
1592 }
1593 // `buf[i..j]` is a contiguous run of same-urgency stream IDs.
1594 let bucket = &mut buf[i..j];
1595 if bucket.len() > 1 {
1596 // Stable partition: non-incremental first, incremental last,
1597 // each subrange staying in ascending stream-id order.
1598 bucket.sort_by_key(|id| self.get(id).1);
1599 let split = bucket.partition_point(|id| !self.get(id).1);
1600 let incremental_tail = &mut bucket[split..];
1601 if incremental_tail.len() > 1 {
1602 // Rotate so the pass starts right after the stream that
1603 // fired first previously. `partition_point` returns the
1604 // first index whose stream ID > cursor (so cursor itself
1605 // is still drained, but after the streams ahead of it).
1606 let start =
1607 incremental_tail.partition_point(|id| *id <= self.incremental_cursor);
1608 incremental_tail.rotate_left(start);
1609 }
1610 total_incremental += incremental_tail.len();
1611 } else if bucket.len() == 1 && self.get(&bucket[0]).1 {
1612 total_incremental += 1;
1613 }
1614 i = j;
1615 }
1616 // Post-conditions: the routine is a permutation — it reorders in place
1617 // and never drops a stream id (len unchanged), and the returned count is
1618 // exactly the number of incremental streams present (the cursor-advance
1619 // callers rely on this being the true incremental-tail size).
1620 debug_assert_eq!(
1621 buf.len(),
1622 len_before,
1623 "rotation must preserve the slice (no streams dropped or added)"
1624 );
1625 #[cfg(debug_assertions)]
1626 debug_assert_eq!(
1627 total_incremental, expected_incremental,
1628 "reported incremental count must equal the incremental streams in buf"
1629 );
1630 total_incremental
1631 }
1632
1633 /// Advance the RFC 9218 §4 round-robin cursor after a write pass.
1634 ///
1635 /// `first_incremental_fired` is the stream ID that headed the incremental
1636 /// tail we just drained; the next pass will start at the next stream
1637 /// after that ID. Callers may pass `None` when no incremental streams
1638 /// were eligible, leaving the cursor where it was.
1639 pub fn advance_incremental_cursor(&mut self, first_incremental_fired: Option<StreamId>) {
1640 if let Some(id) = first_incremental_fired {
1641 self.incremental_cursor = id;
1642 }
1643 }
1644}
1645
1646/// Connection-level flow control state (RFC 9113 §6.9).
1647pub struct H2FlowControl {
1648 /// Connection-level send window (can go negative per RFC 9113 §6.9.2).
1649 pub window: i32,
1650 /// Bytes received since last connection-level WINDOW_UPDATE.
1651 pub received_bytes_since_update: u32,
1652 /// Queued stream_id -> accumulated increment for WINDOW_UPDATE frames (O(1) coalescing).
1653 pub pending_window_updates: HashMap<u32, u32>,
1654}
1655
1656/// Byte accounting for connection overhead attribution.
1657pub struct H2ByteAccounting {
1658 /// Bytes read on the zero stream not yet attributed to a stream.
1659 pub zero_bytes_read: usize,
1660 /// Overhead bytes received (connection-level frames).
1661 pub overhead_bin: usize,
1662 /// Overhead bytes sent (connection-level frames).
1663 pub overhead_bout: usize,
1664}
1665
1666/// Connection draining state for graceful shutdown.
1667pub struct H2DrainState {
1668 /// True when we've sent GOAWAY and are draining.
1669 pub draining: bool,
1670 /// Last stream ID from peer's GOAWAY (for retry decisions).
1671 pub peer_last_stream_id: Option<StreamId>,
1672 /// Wall-clock timestamp captured the first time this connection entered
1673 /// `draining` during soft-stop. Used together with
1674 /// [`Self::graceful_shutdown_deadline`] to decide when to force-close.
1675 /// Remains `None` until the proxy-initiated drain begins (peer-initiated
1676 /// drains via `handle_goaway_frame` don't arm the forced-close timer —
1677 /// the caller in `Mux::shutting_down` is the only writer).
1678 pub started_at: Option<Instant>,
1679 /// Wall-clock budget granted to in-flight streams after the initial
1680 /// `GOAWAY(NO_ERROR)`. `None` means "wait indefinitely" (knob value `0`).
1681 /// Default when unset upstream: 5 s (see `L7ListenerHandler`).
1682 pub graceful_shutdown_deadline: Option<std::time::Duration>,
1683}
1684
1685pub struct ConnectionH2<Front: SocketHandler> {
1686 /// Connection/session ULID propagated from the parent [`Mux`]. Used to
1687 /// stamp the session slot of the `[session req cluster backend]` log
1688 /// prefix emitted by this module's `log_context!` / `log_context_stream!`
1689 /// macros.
1690 pub session_ulid: Ulid,
1691 pub decoder: loona_hpack::Decoder<'static>,
1692 pub encoder: loona_hpack::Encoder<'static>,
1693 pub expect_read: Option<(H2StreamId, usize)>,
1694 pub expect_write: Option<H2StreamId>,
1695 pub last_stream_id: StreamId,
1696 pub local_settings: H2Settings,
1697 pub peer_settings: H2Settings,
1698 pub position: Position,
1699 pub prioriser: Prioriser,
1700 pub readiness: Readiness,
1701 pub socket: Front,
1702 pub state: H2State,
1703 pub streams: HashMap<StreamId, GlobalStreamId>,
1704 pub timeout_container: TimeoutContainer,
1705 /// Connection-level flow control state (send window, receive tracking, pending updates).
1706 pub flow_control: H2FlowControl,
1707 /// Highest stream ID accepted from the peer (used for GoAway last_stream_id).
1708 pub highest_peer_stream_id: StreamId,
1709 /// RFC 7541 §4.2 / §6.3 pending dynamic-table-size-update signal.
1710 ///
1711 /// `Some(new_size)` when a peer SETTINGS frame adjusted
1712 /// `SETTINGS_HEADER_TABLE_SIZE` and we have not yet prepended the
1713 /// matching `001xxxxx` HPACK directive to a header block. Consumed and
1714 /// cleared by [`H2BlockConverter::emit_pending_size_update_if_new_block`]
1715 /// on the next `Block::StatusLine` or `Block::Header` encoded for the
1716 /// connection. Until then the peer's decoder still has its previous
1717 /// (possibly larger) table cap, so emitting is a correctness
1718 /// requirement, not a nicety — see the RFC 9113 encoder-decoder
1719 /// synchronisation contract (§6.5.2).
1720 pub pending_table_size_update: Option<u32>,
1721 /// Reusable buffer for HPACK-encoded headers in the H2 block converter.
1722 pub converter_buf: Vec<u8>,
1723 /// Reusable buffer for lowercasing header keys in the H2 block converter.
1724 pub lowercase_buf: Vec<u8>,
1725 /// Reusable buffer for assembling cookie values in the H2 block converter.
1726 pub cookie_buf: Vec<u8>,
1727 /// Connection draining state for graceful shutdown.
1728 pub drain: H2DrainState,
1729 pub zero: GenericHttpStream,
1730 /// Byte accounting for connection overhead attribution.
1731 pub bytes: H2ByteAccounting,
1732 /// Flood detector for CVE mitigations (Rapid Reset, CONTINUATION, Ping, Settings floods).
1733 pub flood_detector: H2FloodDetector,
1734 /// RFC 9113 §6.5: timestamp when we sent SETTINGS and are awaiting ACK.
1735 /// If the peer does not ACK within SETTINGS_ACK_TIMEOUT, we send GOAWAY
1736 /// with SettingsTimeout error.
1737 pub settings_sent_at: Option<Instant>,
1738 /// Queued RST_STREAM frames to send: Vec<(stream_id, error_code)>.
1739 /// Used when refusing streams (MAX_CONCURRENT_STREAMS, buffer exhaustion)
1740 /// during readable — the actual write happens in the writable preamble
1741 /// to avoid conflicting with kawa.storage usage for frame payload discard.
1742 pub pending_rst_streams: Vec<(StreamId, H2Error)>,
1743 /// RFC 9113 §6.8: tracks stream IDs for which RST_STREAM has already been sent,
1744 /// preventing duplicate RST_STREAM frames on the wire.
1745 pub rst_sent: HashSet<StreamId>,
1746 /// Lifetime counter of RST_STREAM frames queued (pending + already flushed).
1747 /// Used to detect sustained misbehavior even when writable() drains the
1748 /// pending queue between readable() calls.
1749 pub total_rst_streams_queued: usize,
1750 /// Reusable buffer for priority-sorted stream IDs in write_streams().
1751 /// Cleared and reused each call to avoid per-frame allocation.
1752 priorities_buf: Vec<StreamId>,
1753 /// True once we've asked rustls to emit TLS close_notify for this frontend.
1754 close_notify_sent: bool,
1755 /// Per-listener H2 connection tuning (window size, max streams, shrink ratio).
1756 pub connection_config: H2ConnectionConfig,
1757 /// Maximum pending WINDOW_UPDATE entries before dropping.
1758 /// Derived from `connection_config.max_concurrent_streams` at construction.
1759 max_pending_window_updates: usize,
1760 /// Last `(connection_window, active_streams, pending_window_updates)` snapshot
1761 /// emitted by [`Self::gauge_connection_state`]. The snapshot represents this
1762 /// connection's *contribution* to the three `h2.connection.*` aggregate
1763 /// gauges; each call emits the signed delta against this snapshot via
1764 /// [`gauge_add!`] so the gauge sums across connections.
1765 ///
1766 /// Stays `None` until the first emission. [`Drop`] applies the negative of
1767 /// this snapshot so the connection's contribution is always rebalanced to
1768 /// zero on teardown — independent of which close path runs.
1769 last_gauge_snapshot: Option<(usize, usize, usize)>,
1770 /// Per-stream wall-clock timestamp of last meaningful activity (DATA or
1771 /// HEADERS frame receipt). Used to cancel streams that make no forward
1772 /// progress within [`Self::stream_idle_timeout`] — mitigates slow-multiplex
1773 /// Slowloris: connection-level idle timers reset on every frame, so a
1774 /// misbehaving peer can otherwise pin up to `max_concurrent_streams` slots
1775 /// for the full nominal connection timeout.
1776 ///
1777 /// Initialized when the stream is created and refreshed on each non-empty
1778 /// inbound DATA frame and on HEADERS for an existing stream (trailers).
1779 /// Empty DATA frames (CVE-2019-9518 vector) do NOT refresh the timer.
1780 pub stream_last_activity_at: HashMap<StreamId, Instant>,
1781 /// Per-stream timestamp of when the stream first became flow-control-stalled
1782 /// on the OUTBOUND (response) side — it holds buffered response data it
1783 /// cannot drain because its effective send window `min(stream.window,
1784 /// connection.window)` is exhausted (the HTTP/2 window-stall /
1785 /// WINDOW_UPDATE-drip vector). Distinct from [`Self::stream_last_activity_at`]:
1786 /// this map is armed/cleared ONLY by outbound flow-control progress and is
1787 /// NEVER refreshed by inbound DATA/HEADERS or connection-level frames, so a
1788 /// peer dribbling 1-byte DATA on a stalled stream cannot keep it warm (the
1789 /// liveness timer alone misses this because inbound drips refresh it). Reaped
1790 /// by [`Self::cancel_timed_out_streams`] after [`Self::stream_idle_timeout`].
1791 pub stream_fc_stalled_since: HashMap<StreamId, Instant>,
1792 /// Cumulative outbound flow-control bytes drained on a window-stalled stream
1793 /// SINCE its [`Self::stream_fc_stalled_since`] deadline was armed (M2
1794 /// cumulative-stall budget). An entry exists IFF `stream_fc_stalled_since`
1795 /// has one for the stream; the two maps are kept in lockstep at every
1796 /// arm/clear/evict site. Closes the `WINDOW_UPDATE(+1)`-drip residual: a
1797 /// 1-byte drain no longer clears the deadline — only cumulative progress
1798 /// reaching [`FC_STALL_CLEAR_FLOOR`] does.
1799 pub stream_fc_stalled_progress: HashMap<StreamId, usize>,
1800 /// Per-stream idle cap. Streams with no activity for longer than this are
1801 /// RST_STREAM(CANCEL)'d by [`Self::cancel_timed_out_streams`].
1802 pub stream_idle_timeout: std::time::Duration,
1803 /// RFC 9113 §5.1.2 back-pressure: count of stream refusals
1804 /// (REFUSED_STREAM emitted via [`Self::refuse_stream_and_discard`]) within
1805 /// the current back-pressure window. When the count exceeds
1806 /// [`BACKPRESSURE_REFUSAL_THRESHOLD`] inside one
1807 /// [`BACKPRESSURE_WINDOW_DURATION`] we halve the advertised
1808 /// `SETTINGS_MAX_CONCURRENT_STREAMS` to signal the peer to slow down.
1809 refuse_count_window: u32,
1810 /// Start timestamp for the current back-pressure window.
1811 refuse_window_start: Instant,
1812 /// Set once we have halved `local_settings.settings_max_concurrent_streams`
1813 /// in response to a refusal burst. Prevents the cap from collapsing to 0
1814 /// on sustained abuse — a single halving per connection is sufficient to
1815 /// signal back-pressure; further bursts trigger `EnhanceYourCalm`.
1816 mcs_backpressure_applied: bool,
1817}
1818impl<Front: SocketHandler> std::fmt::Debug for ConnectionH2<Front> {
1819 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1820 f.debug_struct("ConnectionH2")
1821 .field("position", &self.position)
1822 .field("state", &self.state)
1823 .field("expect", &self.expect_read)
1824 .field("readiness", &self.readiness)
1825 .field("local_settings", &self.local_settings)
1826 .field("peer_settings", &self.peer_settings)
1827 .field("socket", &self.socket.socket_ref())
1828 .field("streams", &self.streams)
1829 .field("zero", &self.zero.storage.meter(20))
1830 .field("window", &self.flow_control.window)
1831 .field("total_rst_streams_queued", &self.total_rst_streams_queued)
1832 .finish()
1833 }
1834}
1835
1836/// Symmetric tear-down for the three `h2.connection.*` aggregate gauges:
1837/// whatever positive contribution this connection made via
1838/// [`ConnectionH2::gauge_connection_state`] is subtracted back out when the
1839/// connection is dropped.
1840///
1841/// Using `Drop` (rather than wiring decrements into every close path —
1842/// `graceful_goaway`, `force_disconnect`, `handle_goaway_frame`, `Mux::close`,
1843/// stream-id exhaustion, panic-unwind) is what guarantees the gauge is
1844/// arithmetically symmetric regardless of which path teardown took. Past
1845/// underflow incidents (commits a650ad69, d2f01ed4) have all been
1846/// missing-decrement bugs that `Drop` makes structurally impossible.
1847impl<Front: SocketHandler> Drop for ConnectionH2<Front> {
1848 fn drop(&mut self) {
1849 self.release_connection_gauges();
1850 }
1851}
1852
1853#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1854pub enum H2StreamId {
1855 Zero,
1856 Other { id: StreamId, gid: GlobalStreamId },
1857}
1858
1859impl<Front: SocketHandler> ConnectionH2<Front> {
1860 fn frontend_hung_up_while_draining(&self) -> bool {
1861 matches!(self.position, Position::Server)
1862 && self.drain.draining
1863 && (self.readiness.event.is_hup() || self.readiness.event.is_error())
1864 }
1865
1866 /// Once the final GOAWAY has been queued and all streams/control frames are
1867 /// gone, a peer-side HUP/ERR means any remaining rustls backlog is no
1868 /// longer deliverable. Waiting on `socket_wants_write()` in that state can
1869 /// deadlock shutdown forever because GOAWAY disables further frame reads.
1870 fn peer_gone_after_final_goaway(&self) -> bool {
1871 self.frontend_hung_up_while_draining()
1872 && matches!(self.state, H2State::GoAway | H2State::Error)
1873 && self.streams.is_empty()
1874 && self.expect_write.is_none()
1875 && self.zero.storage.is_empty()
1876 }
1877
1878 /// Shared constructor for both server and client H2 connections.
1879 ///
1880 /// Differences between server and client are captured by the caller-provided
1881 /// `position`, `expect_read`, and `readiness_interest` parameters.
1882 #[allow(clippy::too_many_arguments)]
1883 pub(super) fn new(
1884 session_ulid: Ulid,
1885 socket: Front,
1886 position: super::Position,
1887 pool: std::rc::Weak<std::cell::RefCell<crate::pool::Pool>>,
1888 flood_config: H2FloodConfig,
1889 connection_config: H2ConnectionConfig,
1890 stream_idle_timeout: std::time::Duration,
1891 graceful_shutdown_deadline: Option<std::time::Duration>,
1892 timeout_container: crate::timer::TimeoutContainer,
1893 expect_read: Option<(H2StreamId, usize)>,
1894 readiness_interest: sozu_command::ready::Ready,
1895 ) -> Option<Self> {
1896 let buffer = pool
1897 .upgrade()
1898 .and_then(|pool| pool.borrow_mut().checkout())?;
1899 let local_settings = H2Settings {
1900 settings_max_concurrent_streams: connection_config.max_concurrent_streams,
1901 ..H2Settings::default()
1902 };
1903 let mut decoder = loona_hpack::Decoder::new();
1904 // RFC 7541 §4.2: enforce SETTINGS_HEADER_TABLE_SIZE as the upper bound
1905 // for dynamic table size updates from the peer
1906 decoder.set_max_allowed_table_size(local_settings.settings_header_table_size as usize);
1907 Some(ConnectionH2 {
1908 session_ulid,
1909 decoder,
1910 encoder: loona_hpack::Encoder::new(),
1911 expect_read,
1912 expect_write: None,
1913 last_stream_id: 0,
1914 local_settings,
1915 peer_settings: H2Settings::default(),
1916 position,
1917 prioriser: Prioriser::default(),
1918 readiness: crate::Readiness {
1919 interest: readiness_interest,
1920 event: Ready::EMPTY,
1921 },
1922 socket,
1923 state: H2State::ClientPreface,
1924 streams: std::collections::HashMap::with_capacity(8),
1925 timeout_container,
1926 flow_control: H2FlowControl {
1927 window: DEFAULT_INITIAL_WINDOW_SIZE as i32,
1928 received_bytes_since_update: 0,
1929 pending_window_updates: HashMap::new(),
1930 },
1931 highest_peer_stream_id: 0,
1932 pending_table_size_update: None,
1933 converter_buf: Vec::new(),
1934 lowercase_buf: Vec::new(),
1935 cookie_buf: Vec::new(),
1936 drain: H2DrainState {
1937 draining: false,
1938 peer_last_stream_id: None,
1939 started_at: None,
1940 graceful_shutdown_deadline,
1941 },
1942 zero: kawa::Kawa::new(kawa::Kind::Request, kawa::Buffer::new(buffer)),
1943 bytes: H2ByteAccounting {
1944 zero_bytes_read: 0,
1945 overhead_bin: 0,
1946 overhead_bout: 0,
1947 },
1948 flood_detector: H2FloodDetector::new(flood_config),
1949 settings_sent_at: None,
1950 pending_rst_streams: Vec::new(),
1951 rst_sent: std::collections::HashSet::new(),
1952 total_rst_streams_queued: 0,
1953 priorities_buf: Vec::new(),
1954 close_notify_sent: false,
1955 max_pending_window_updates: 1 + connection_config.max_concurrent_streams as usize * 4,
1956 connection_config,
1957 last_gauge_snapshot: None,
1958 stream_last_activity_at: HashMap::new(),
1959 stream_fc_stalled_since: HashMap::new(),
1960 stream_fc_stalled_progress: HashMap::new(),
1961 stream_idle_timeout,
1962 refuse_count_window: 0,
1963 refuse_window_start: Instant::now(),
1964 mcs_backpressure_applied: false,
1965 })
1966 }
1967
1968 /// Start TLS close_notify on the frontend and keep the session alive until
1969 /// rustls has flushed the generated records.
1970 pub fn initiate_close_notify(&mut self) -> bool {
1971 if !self.position.is_server()
1972 || matches!(
1973 self.state,
1974 H2State::ClientPreface | H2State::ClientSettings | H2State::ServerSettings
1975 )
1976 {
1977 return false;
1978 }
1979 if !self.close_notify_sent {
1980 trace!("{} H2 initiating CLOSE_NOTIFY", log_context!(self));
1981 self.socket.socket_close();
1982 self.close_notify_sent = true;
1983 }
1984 if self.socket.socket_wants_write() {
1985 self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
1986 self.ensure_tls_flushed();
1987 true
1988 } else {
1989 false
1990 }
1991 }
1992
1993 fn expect_header(&mut self) {
1994 self.state = H2State::Header;
1995 self.expect_read = Some((H2StreamId::Zero, 9));
1996 }
1997
1998 /// Process the `H2State::Header` state: parse a 9-byte frame header from
1999 /// `self.zero`, validate the stream, create new streams if needed, and
2000 /// transition to `H2State::Frame` for the payload.
2001 ///
2002 /// Returns `MuxResult` — the caller should propagate the result directly.
2003 fn handle_header_state<L>(&mut self, context: &mut Context<L>) -> MuxResult
2004 where
2005 L: ListenerHandler + L7ListenerHandler,
2006 {
2007 let i = self.zero.storage.data();
2008 trace!("{} header: {:?}", log_context!(self), i);
2009 match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
2010 Ok((_, header)) => {
2011 trace!("{} {:#?}", log_context!(self), header);
2012 self.zero.storage.clear();
2013 let stream_id = header.stream_id;
2014 // RFC 9113 §6.10: CONTINUATION frames MUST be preceded by a
2015 // HEADERS or PUSH_PROMISE frame without END_HEADERS. When we
2016 // reach `handle_header_state`, we are between frames and no
2017 // header block is in progress (otherwise the state would be
2018 // `H2State::ContinuationHeader`). A CONTINUATION frame arriving
2019 // here is therefore standalone and MUST be treated as a
2020 // connection error of type PROTOCOL_ERROR.
2021 if header.frame_type == FrameType::Continuation {
2022 error!(
2023 "{} standalone CONTINUATION frame on stream {} without preceding HEADERS",
2024 log_context!(self),
2025 stream_id
2026 );
2027 return self.goaway(H2Error::ProtocolError);
2028 }
2029 // RFC 9113 §5.5: unknown frame types MUST be ignored and discarded.
2030 // Route unknown frames (and any stream_id == 0 control frame)
2031 // through stream 0 (the connection-level buffer) so
2032 // `handle_frame` can drop them without touching stream state.
2033 let read_stream = if stream_id == 0
2034 || matches!(header.frame_type, FrameType::Unknown(_))
2035 {
2036 H2StreamId::Zero
2037 } else if let Some(global_stream_id) = self.streams.get(&stream_id) {
2038 let allowed_on_half_closed = header.frame_type == FrameType::WindowUpdate
2039 || header.frame_type == FrameType::Priority
2040 || header.frame_type == FrameType::RstStream;
2041 let stream = &context.streams[*global_stream_id];
2042 // Use the position-aware end_of_stream flag:
2043 // - Server reads from front (client requests)
2044 // - Client reads from back (backend responses)
2045 let received_eos = if self.position.is_server() {
2046 stream.front_received_end_of_stream
2047 } else {
2048 stream.back_received_end_of_stream
2049 };
2050 trace!(
2051 "{} REQUESTING EXISTING STREAM {}: {}/{:?}",
2052 log_context!(self),
2053 stream_id,
2054 received_eos,
2055 stream.state
2056 );
2057 if !allowed_on_half_closed && (received_eos || !stream.state.is_open()) {
2058 error!(
2059 "{} CANNOT RECEIVE {:?} ON THIS STREAM {:?}",
2060 log_context!(self),
2061 header.frame_type,
2062 stream.state
2063 );
2064 return self.goaway(H2Error::StreamClosed);
2065 }
2066 // RFC 9113 §8.1: a HEADERS frame received in the body
2067 // phase is a trailer block and MUST carry END_STREAM. This
2068 // closes the request-smuggling primitive where a peer sends
2069 // HEADERS, DATA, HEADERS (no END_STREAM) to chain header
2070 // blocks on the same stream ID.
2071 //
2072 // Discriminate from the read-side Kawa parsing phase rather
2073 // than stream existence: on Position::Client the stream is
2074 // created when we send the request to the backend, so the
2075 // initial backend response HEADERS legitimately arrives on
2076 // an existing stream. Similarly, 1xx→final transitions on
2077 // either side may yield multiple HEADERS frames before the
2078 // body begins (kawa clears back to initial / terminated on
2079 // 1xx; neither is main_phase). Only HEADERS arriving once
2080 // the read side has transitioned to Body/Chunks parsing —
2081 // i.e. after headers were fully consumed and body framing
2082 // is in progress — may be a trailer.
2083 let read_in_body = if self.position.is_server() {
2084 stream.front.is_main_phase()
2085 } else {
2086 stream.back.is_main_phase()
2087 };
2088 if header.frame_type == FrameType::Headers
2089 && read_in_body
2090 && header.flags & parser::FLAG_END_STREAM == 0
2091 {
2092 error!(
2093 "{} HEADERS without END_STREAM on open stream {} in body phase: trailers MUST carry END_STREAM",
2094 log_context!(self),
2095 stream_id
2096 );
2097 return self.goaway(H2Error::ProtocolError);
2098 }
2099 if header.frame_type == FrameType::Data {
2100 H2StreamId::Other {
2101 id: stream_id,
2102 gid: *global_stream_id,
2103 }
2104 } else {
2105 H2StreamId::Zero
2106 }
2107 } else {
2108 // RFC 9113 §5.1.1: stream identifiers MUST be strictly
2109 // increasing. Tightened from `>=` to `>` so that a peer
2110 // cannot re-use `self.last_stream_id` (which would
2111 // conflict with our own server-pushed streams if we
2112 // ever enable push in the future). For the first
2113 // request on a fresh connection `last_stream_id == 0`
2114 // and any client-initiated odd stream still passes.
2115 if header.frame_type == FrameType::Headers
2116 && self.position.is_server()
2117 && stream_id & 1 == 1
2118 && stream_id > self.last_stream_id
2119 {
2120 // RFC 9113 §6.8: after sending a GOAWAY, the proxy
2121 // MUST NOT accept new streams.
2122 // `graceful_goaway` sets `drain.draining = true`
2123 // and sends an initial GOAWAY with last_stream_id =
2124 // STREAM_ID_MAX (so in-flight requests are still
2125 // accepted), but the contract for *new* peer-
2126 // initiated streams is that they must be refused.
2127 // Without this check, a peer racing the drain
2128 // window could open arbitrary new streams between
2129 // the initial and final GOAWAY emission.
2130 if self.drain.draining {
2131 if stream_id > self.highest_peer_stream_id {
2132 self.highest_peer_stream_id = stream_id;
2133 }
2134 return self.refuse_stream_and_discard(
2135 stream_id,
2136 H2Error::RefusedStream,
2137 header.payload_len,
2138 );
2139 }
2140 if self.streams.len()
2141 >= self.local_settings.settings_max_concurrent_streams as usize
2142 {
2143 error!(
2144 "{} MAX CONCURRENT STREAMS: limit={}, current={}",
2145 log_context!(self),
2146 self.local_settings.settings_max_concurrent_streams,
2147 self.streams.len()
2148 );
2149 // RFC 9113 §6.8: update highest_peer_stream_id BEFORE
2150 // queueing RST_STREAM so GOAWAY reports the correct
2151 // last_stream_id if the connection closes later.
2152 if stream_id > self.highest_peer_stream_id {
2153 self.highest_peer_stream_id = stream_id;
2154 }
2155 return self.refuse_stream_and_discard(
2156 stream_id,
2157 H2Error::RefusedStream,
2158 header.payload_len,
2159 );
2160 }
2161 match self.create_stream(stream_id, context) {
2162 Some(_) => {}
2163 None => {
2164 // Buffer pool exhaustion is transient — refuse
2165 // this stream but keep the connection alive so
2166 // existing streams can complete and free buffers.
2167 error!(
2168 "{} Could not create stream {}: buffer pool exhausted",
2169 log_context!(self),
2170 stream_id
2171 );
2172 // RFC 9113 §6.8: update highest_peer_stream_id BEFORE
2173 // queueing RST_STREAM so GOAWAY reports the correct
2174 // last_stream_id if the connection closes later.
2175 if stream_id > self.highest_peer_stream_id {
2176 self.highest_peer_stream_id = stream_id;
2177 }
2178 return self.refuse_stream_and_discard(
2179 stream_id,
2180 H2Error::RefusedStream,
2181 header.payload_len,
2182 );
2183 }
2184 }
2185 } else if header.frame_type != FrameType::Priority {
2186 // Distinguish closed vs idle: check whether the stream
2187 // was previously opened. For Server position, compare
2188 // against highest_peer_stream_id (client-initiated).
2189 // For Client position, compare against last_stream_id
2190 // (our own initiated streams) since the peer never
2191 // initiates streams on a backend connection.
2192 let is_closed_stream = if self.position.is_server() {
2193 header.stream_id <= self.highest_peer_stream_id
2194 } else {
2195 header.stream_id < self.last_stream_id
2196 };
2197 if is_closed_stream {
2198 match header.frame_type {
2199 FrameType::RstStream | FrameType::WindowUpdate => {
2200 // RFC 9113 §5.1: RST_STREAM and WINDOW_UPDATE
2201 // on a closed stream can arrive due to race
2202 // conditions and should be consumed/discarded.
2203 debug!(
2204 "{} Ignoring {:?} on closed stream {}",
2205 log_context!(self),
2206 header.frame_type,
2207 header.stream_id
2208 );
2209 self.flood_detector.glitch_count += 1;
2210 check_flood_or_return!(self);
2211 }
2212 FrameType::Data => {
2213 // RFC 9113 §5.1: DATA on a closed stream is a
2214 // stream error of type STREAM_CLOSED. Queue
2215 // RST_STREAM (not GOAWAY) to preserve the
2216 // connection for other streams. The payload is
2217 // still routed through stream 0 so handle_frame
2218 // can do connection-level flow control accounting.
2219 debug!(
2220 "{} DATA on closed stream {}, sending RST_STREAM(STREAM_CLOSED)",
2221 log_context!(self),
2222 header.stream_id
2223 );
2224 self.flood_detector.glitch_count += 1;
2225 check_flood_or_return!(self);
2226 if let Some(result) =
2227 self.enqueue_rst(header.stream_id, H2Error::StreamClosed)
2228 {
2229 return result;
2230 }
2231 }
2232 _ => {
2233 // RFC 9113 §5.1: HEADERS or other frames on a
2234 // closed stream → connection error STREAM_CLOSED.
2235 error!(
2236 "{} Received {:?} on closed stream {}, sending GOAWAY(STREAM_CLOSED)",
2237 log_context!(self),
2238 header.frame_type,
2239 header.stream_id
2240 );
2241 return self.goaway(H2Error::StreamClosed);
2242 }
2243 }
2244 } else {
2245 error!(
2246 "{} Received {:?} on idle stream {}, sending GOAWAY(PROTOCOL_ERROR)",
2247 log_context!(self),
2248 header.frame_type,
2249 header.stream_id
2250 );
2251 return self.goaway(H2Error::ProtocolError);
2252 }
2253 }
2254 H2StreamId::Zero
2255 };
2256 trace!(
2257 "{} {} {:?} {:#?}",
2258 log_context!(self),
2259 header.stream_id,
2260 stream_id,
2261 self.streams
2262 );
2263 self.expect_read = Some((read_stream, header.payload_len as usize));
2264 self.state = H2State::Frame(header);
2265 }
2266 Err(error) => {
2267 let error = error_nom_to_h2(error);
2268 error!("{} COULD NOT PARSE FRAME HEADER", log_context!(self));
2269 return self.goaway(error);
2270 }
2271 };
2272 MuxResult::Continue
2273 }
2274
2275 /// Process the `H2State::ContinuationHeader` state: parse a CONTINUATION
2276 /// frame header from `self.zero`, validate stream ID continuity, track
2277 /// flood detection counters, and transition to `ContinuationFrame`.
2278 ///
2279 /// The `headers` parameter is the accumulated HEADERS context from the
2280 /// initial HEADERS frame (cloned from the state enum to avoid borrow
2281 /// conflicts).
2282 fn handle_continuation_header_state(&mut self, headers: &Headers) -> MuxResult {
2283 let i = self.zero.storage.unparsed_data();
2284 trace!("{} continuation header: {:?}", log_context!(self), i);
2285 match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
2286 Ok((
2287 _,
2288 FrameHeader {
2289 payload_len,
2290 frame_type: FrameType::Continuation,
2291 flags,
2292 stream_id,
2293 },
2294 )) => {
2295 if self.zero.storage.end < 9 {
2296 error!(
2297 "{} CONTINUATION header: storage.end ({}) too small to remove frame header",
2298 log_context!(self),
2299 self.zero.storage.end
2300 );
2301 return self.goaway(H2Error::InternalError);
2302 }
2303 self.zero.storage.end -= 9;
2304 if stream_id != headers.stream_id {
2305 error!(
2306 "{} CONTINUATION stream_id {} does not match HEADERS stream_id {}",
2307 log_context!(self),
2308 stream_id,
2309 headers.stream_id
2310 );
2311 return self.goaway(H2Error::ProtocolError);
2312 }
2313 // CVE-2024-27316: track CONTINUATION frame count and accumulated size
2314 let cont_count_before = self.flood_detector.continuation_count;
2315 let acc_size_before = self.flood_detector.accumulated_header_size;
2316 self.flood_detector.continuation_count += 1;
2317 self.flood_detector.accumulated_header_size = self
2318 .flood_detector
2319 .accumulated_header_size
2320 .saturating_add(payload_len);
2321 // Per-block CONTINUATION accounting must grow monotonically
2322 // within a header block: each frame bumps the count by one and
2323 // the accumulated size by the frame's payload (never shrinks
2324 // mid-block). `reset_continuation` is the only thing allowed to
2325 // zero these — and only once the block is complete.
2326 debug_assert_eq!(
2327 self.flood_detector.continuation_count,
2328 cont_count_before + 1,
2329 "CONTINUATION per-block counter must advance by one per frame"
2330 );
2331 debug_assert!(
2332 self.flood_detector.accumulated_header_size >= acc_size_before,
2333 "accumulated header size must not shrink within a header block"
2334 );
2335 check_flood_or_return!(self);
2336 // RFC 9113 §10.5.1: reject header blocks that cannot be
2337 // buffered. Previously we silently removed READABLE interest
2338 // when amount > available_space, stalling the connection.
2339 // If the payload still fits in our zero buffer we can refuse
2340 // just this stream (RST_STREAM + drain); if not, the
2341 // connection can no longer decode header blocks safely and we
2342 // escalate to GOAWAY(EnhanceYourCalm).
2343 if self.flood_detector.accumulated_header_size
2344 > self.flood_detector.config.max_header_list_size
2345 {
2346 error!(
2347 "{} CONTINUATION accumulated header size {} exceeds {}",
2348 log_context!(self),
2349 self.flood_detector.accumulated_header_size,
2350 self.flood_detector.config.max_header_list_size
2351 );
2352 if (payload_len as usize) > self.zero.storage.available_space() {
2353 return self.goaway(H2Error::EnhanceYourCalm);
2354 }
2355 // Remove the already-created stream slot before refusing,
2356 // so it does not leak against MAX_CONCURRENT_STREAMS. Route
2357 // through `remove_dead_stream` so the expect_write/read
2358 // invariant (§LIFECYCLE.md 5.4) holds on this path too.
2359 if let Some(global_stream_id) = self.streams.get(&stream_id).copied() {
2360 self.remove_dead_stream(stream_id, global_stream_id);
2361 }
2362 return self.refuse_stream_and_discard(
2363 stream_id,
2364 H2Error::RefusedStream,
2365 payload_len,
2366 );
2367 }
2368 if (payload_len as usize) > self.zero.storage.available_space() {
2369 error!(
2370 "{} CONTINUATION payload {} exceeds buffer space {}",
2371 log_context!(self),
2372 payload_len,
2373 self.zero.storage.available_space()
2374 );
2375 return self.goaway(H2Error::EnhanceYourCalm);
2376 }
2377 self.expect_read = Some((H2StreamId::Zero, payload_len as usize));
2378 let mut headers = headers.clone();
2379 headers.end_headers = flags & parser::FLAG_END_HEADERS != 0;
2380 headers.header_block_fragment.len = headers
2381 .header_block_fragment
2382 .len
2383 .saturating_add(payload_len);
2384 self.state = H2State::ContinuationFrame(headers);
2385 }
2386 Err(error) => {
2387 let error = error_nom_to_h2(error);
2388 error!("{} COULD NOT PARSE CONTINUATION HEADER", log_context!(self));
2389 return self.goaway(error);
2390 }
2391 other => {
2392 error!(
2393 "{} UNEXPECTED {:?} WHILE PARSING CONTINUATION HEADER",
2394 log_context!(self),
2395 other
2396 );
2397 return self.goaway(H2Error::ProtocolError);
2398 }
2399 };
2400 MuxResult::Continue
2401 }
2402
2403 pub fn readable<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E) -> MuxResult
2404 where
2405 E: Endpoint,
2406 L: ListenerHandler + L7ListenerHandler,
2407 {
2408 self.prune_inactive_streams_while_closing(context);
2409 // Pass 4 Medium #3: per-stream idle guard. Slow-multiplex Slowloris
2410 // sends one byte or a control frame per stream just often enough to
2411 // reset the connection-level timer; per-stream deadlines catch it.
2412 self.cancel_timed_out_streams(context, &mut endpoint);
2413
2414 // RFC 9113 §6.5: check if peer has timed out on SETTINGS ACK
2415 if let Some(sent_at) = self.settings_sent_at
2416 && sent_at.elapsed() >= SETTINGS_ACK_TIMEOUT
2417 {
2418 warn!(
2419 "{} SETTINGS ACK timeout: no SETTINGS ACK observed within {:?}",
2420 log_context!(self),
2421 SETTINGS_ACK_TIMEOUT
2422 );
2423 return self.goaway(H2Error::SettingsTimeout);
2424 }
2425
2426 // Don't reset the timeout unconditionally here. Only application data
2427 // (DATA/HEADERS frames) should reset the timeout. H2 control frames
2428 // (PING, WINDOW_UPDATE, SETTINGS) must NOT reset it, otherwise a peer
2429 // sending periodic PINGs prevents timeout detection on stuck sessions.
2430 // The timeout is reset:
2431 // - Below, when reading DATA payload (H2StreamId::Other)
2432 // - In handle_frame(), when processing HEADERS frames
2433 let (stream_id, kawa) = if let Some((stream_id, amount)) = self.expect_read {
2434 let (kawa, did) = match stream_id {
2435 H2StreamId::Zero => (&mut self.zero, usize::MAX),
2436 H2StreamId::Other {
2437 gid: global_stream_id,
2438 ..
2439 } => {
2440 // Reading DATA frame payload for an application stream.
2441 // This is real application activity — reset the timeout.
2442 self.timeout_container.reset();
2443 (
2444 context.streams[global_stream_id]
2445 .split(&self.position)
2446 .rbuffer,
2447 global_stream_id,
2448 )
2449 }
2450 };
2451 trace!(
2452 "{} {:?}({:?}, {})",
2453 log_context!(self),
2454 self.state,
2455 stream_id,
2456 amount
2457 );
2458 if amount > 0 {
2459 if amount > kawa.storage.available_space() {
2460 self.readiness.interest.remove(Ready::READABLE);
2461 return MuxResult::Continue;
2462 }
2463 let (size, status) = self.socket.socket_read(&mut kawa.storage.space()[..amount]);
2464 context.debug.push(DebugEvent::SocketIO(0, did, size));
2465 kawa.storage.fill(size);
2466 self.position.count_bytes_in_counter(size);
2467 self.bytes.zero_bytes_read += size;
2468 if update_readiness_after_read(size, status, &mut self.readiness) {
2469 if matches!(self.position, Position::Server)
2470 && self.drain.draining
2471 && matches!(status, SocketResult::Closed | SocketResult::Error)
2472 {
2473 // During graceful drain, a frontend EOF/HUP means no
2474 // further frame headers or payload bytes can arrive.
2475 // Keeping expect_read here strands the connection in
2476 // Header/Frame forever even after the peer is gone.
2477 self.expect_read = None;
2478 }
2479 return MuxResult::Continue;
2480 } else if size == amount {
2481 self.expect_read = None;
2482 } else {
2483 self.expect_read = Some((stream_id, amount - size));
2484 if let (H2State::ClientPreface, Position::Server) =
2485 (&self.state, &self.position)
2486 {
2487 let i = kawa.storage.data();
2488 if !b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".starts_with(i) {
2489 debug!("{} EARLY INVALID PREFACE: {:?}", log_context!(self), i);
2490 return self.force_disconnect();
2491 }
2492 }
2493 return MuxResult::Continue;
2494 }
2495 } else {
2496 self.expect_read = None;
2497 }
2498 (stream_id, kawa)
2499 } else {
2500 self.readiness.event.remove(Ready::READABLE);
2501 return MuxResult::Continue;
2502 };
2503 match (&self.state, &self.position) {
2504 (H2State::Error, _)
2505 | (H2State::GoAway, _)
2506 | (H2State::ServerSettings, Position::Server)
2507 | (H2State::ClientPreface, Position::Client(..))
2508 | (H2State::ClientSettings, Position::Client(..)) => {
2509 error!(
2510 "{} Unexpected combination: (Readable, {:?}, {:?})",
2511 log_context!(self),
2512 self.state,
2513 self.position
2514 );
2515 return self.force_disconnect();
2516 }
2517 (H2State::Discard, _) => {
2518 let _i = kawa.storage.data();
2519 trace!("{} DISCARDING: {:?}", log_context!(self), _i);
2520 kawa.storage.clear();
2521 self.attribute_bytes_to_overhead();
2522 self.expect_header();
2523 }
2524 (H2State::ClientPreface, Position::Server) => {
2525 let i = kawa.storage.data();
2526 let i = match parser::preface(i) {
2527 Ok((i, _)) => i,
2528 Err(_) => return self.force_disconnect(),
2529 };
2530 match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
2531 Ok((
2532 _,
2533 FrameHeader {
2534 payload_len,
2535 frame_type: FrameType::Settings,
2536 flags: 0,
2537 stream_id: 0,
2538 },
2539 )) => {
2540 kawa.storage.clear();
2541 self.state = H2State::ClientSettings;
2542 self.expect_read = Some((H2StreamId::Zero, payload_len as usize));
2543 }
2544 _ => return self.force_disconnect(),
2545 };
2546 }
2547 (H2State::ClientSettings, Position::Server) => {
2548 let i = kawa.storage.data();
2549 let settings = match parser::settings_frame(
2550 i,
2551 &FrameHeader {
2552 payload_len: i.len() as u32,
2553 frame_type: FrameType::Settings,
2554 flags: 0,
2555 stream_id: 0,
2556 },
2557 ) {
2558 Ok((_, settings)) => {
2559 kawa.storage.clear();
2560 settings
2561 }
2562 Err(_) => return self.force_disconnect(),
2563 };
2564 let kawa = &mut self.zero;
2565 match serializer::gen_settings(kawa.storage.space(), &self.local_settings) {
2566 Ok((_, size)) => {
2567 kawa.storage.fill(size);
2568 incr!(names::h2::FRAMES_TX_SETTINGS);
2569 // RFC 9113 §6.5: start tracking SETTINGS ACK timeout
2570 self.settings_sent_at = Some(Instant::now());
2571 }
2572 Err(error) => {
2573 error!(
2574 "{} Could not serialize SettingsFrame: {:?}",
2575 log_context!(self),
2576 error
2577 );
2578 return self.force_disconnect();
2579 }
2580 };
2581
2582 self.state = H2State::ServerSettings;
2583 self.expect_write = Some(H2StreamId::Zero);
2584 self.readiness.signal_pending_write();
2585 return self.handle_frame(settings, 0, context, endpoint);
2586 }
2587 (H2State::ServerSettings, Position::Client(..)) => {
2588 let i = kawa.storage.data();
2589 match parser::frame_header(i, self.local_settings.settings_max_frame_size) {
2590 Ok((
2591 _,
2592 header @ FrameHeader {
2593 payload_len,
2594 frame_type: FrameType::Settings,
2595 flags: 0,
2596 stream_id: 0,
2597 },
2598 )) => {
2599 kawa.storage.clear();
2600 self.expect_read = Some((H2StreamId::Zero, payload_len as usize));
2601 self.state = H2State::Frame(header)
2602 }
2603 _ => return self.force_disconnect(),
2604 };
2605 }
2606 (H2State::Header, _) => {
2607 return self.handle_header_state(context);
2608 }
2609 (H2State::ContinuationHeader(headers), _) => {
2610 let headers = headers.clone();
2611 return self.handle_continuation_header_state(&headers);
2612 }
2613 (H2State::Frame(header), _) => {
2614 let i = kawa.storage.unparsed_data();
2615 trace!("{} data: {:?}", log_context!(self), i);
2616 let wire_payload_len = header.payload_len;
2617 let frame = match parser::frame_body(i, header) {
2618 Ok((_, frame)) => frame,
2619 Err(error) => {
2620 let error = error_nom_to_h2(error);
2621 error!("{} COULD NOT PARSE FRAME BODY", log_context!(self));
2622 return self.goaway(error);
2623 }
2624 };
2625 if let H2StreamId::Zero = stream_id {
2626 if header.frame_type == FrameType::Headers {
2627 kawa.storage.head = kawa.storage.end;
2628 } else {
2629 kawa.storage.end = kawa.storage.head;
2630 }
2631 }
2632 self.expect_header();
2633 return self.handle_frame(frame, wire_payload_len, context, endpoint);
2634 }
2635 (H2State::ContinuationFrame(headers), _) => {
2636 kawa.storage.head = kawa.storage.end;
2637 let i = kawa.storage.data();
2638 trace!("{} data: {:?}", log_context!(self), i);
2639 let headers = headers.clone();
2640 self.expect_header();
2641 return self.handle_frame(Frame::Headers(headers), 0, context, endpoint);
2642 }
2643 }
2644 MuxResult::Continue
2645 }
2646
2647 /// Update the H2 connection-level *aggregate* gauges with this connection's
2648 /// current contribution, expressed as a signed delta against the last
2649 /// snapshot we emitted.
2650 ///
2651 /// The three metrics are emitted via [`gauge_add!`] (lifecycle deltas) so
2652 /// that the dashboard sees the **sum across all live H2 connections**:
2653 ///
2654 /// - `h2.connection.window_bytes` — sum of available connection-level
2655 /// send-window bytes. Negative per-connection windows clamp to 0 so the
2656 /// aggregate represents only available capacity, not deficit.
2657 /// - `h2.connection.active_streams` — sum of in-flight streams across
2658 /// every H2 connection.
2659 /// - `h2.connection.pending_window_updates` — sum of queued (un-flushed)
2660 /// per-stream WINDOW_UPDATE entries across every H2 connection.
2661 ///
2662 /// Called from the write hot path; emits nothing when the snapshot is
2663 /// unchanged so the steady state stays cheap. The paired decrement for
2664 /// every increment is provided by [`Drop`], which subtracts the final
2665 /// snapshot when the connection is dropped — keeping the aggregate
2666 /// arithmetically symmetric independent of which close path runs
2667 /// (`graceful_goaway`, `force_disconnect`, `handle_goaway_frame`,
2668 /// `Mux::close`, panic-unwind, …).
2669 fn gauge_connection_state(&mut self) {
2670 let snapshot = (
2671 self.flow_control.window.max(0) as usize,
2672 self.streams.len(),
2673 self.flow_control.pending_window_updates.len(),
2674 );
2675 if self.last_gauge_snapshot == Some(snapshot) {
2676 return;
2677 }
2678 let prev = self.last_gauge_snapshot.unwrap_or((0, 0, 0));
2679 // Diff in i64 — usize cannot represent the negative side of the delta.
2680 let dw = snapshot.0 as i64 - prev.0 as i64;
2681 let ds = snapshot.1 as i64 - prev.1 as i64;
2682 let du = snapshot.2 as i64 - prev.2 as i64;
2683 if dw != 0 {
2684 gauge_add!(names::h2::CONNECTION_WINDOW_BYTES, dw);
2685 }
2686 if ds != 0 {
2687 gauge_add!(names::h2::CONNECTION_ACTIVE_STREAMS, ds);
2688 }
2689 if du != 0 {
2690 gauge_add!(names::h2::CONNECTION_PENDING_WINDOW_UPDATES, du);
2691 }
2692 self.last_gauge_snapshot = Some(snapshot);
2693 }
2694
2695 /// Subtract this connection's contribution from the three aggregate
2696 /// `h2.connection.*` gauges. Idempotent: clears `last_gauge_snapshot` so a
2697 /// second call (or a [`Drop`] on top of an explicit reset) is a no-op.
2698 ///
2699 /// Pairs with every prior call to [`Self::gauge_connection_state`]; called
2700 /// from [`Drop`] so the symmetry is guaranteed regardless of the close
2701 /// path.
2702 fn release_connection_gauges(&mut self) {
2703 if let Some((w, s, u)) = self.last_gauge_snapshot.take() {
2704 if w != 0 {
2705 gauge_add!(names::h2::CONNECTION_WINDOW_BYTES, -(w as i64));
2706 }
2707 if s != 0 {
2708 gauge_add!(names::h2::CONNECTION_ACTIVE_STREAMS, -(s as i64));
2709 }
2710 if u != 0 {
2711 gauge_add!(names::h2::CONNECTION_PENDING_WINDOW_UPDATES, -(u as i64));
2712 }
2713 }
2714 }
2715
2716 /// Write application data (request/response bodies, headers) across all
2717 /// active streams, respecting priority ordering and flow control.
2718 ///
2719 /// This is the main data-plane write path: it resumes any partially-written
2720 /// stream, prepares new frames via the H2 block converter, flushes them to
2721 /// the socket, and recycles completed streams.
2722 ///
2723 /// NOTE: The priority iteration loop and converter setup remain inline here
2724 /// because the converter borrows `self.encoder`, preventing further
2725 /// decomposition into `&mut self` methods within the loop body.
2726 fn write_streams<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E) -> MuxResult
2727 where
2728 E: Endpoint,
2729 L: ListenerHandler + L7ListenerHandler,
2730 {
2731 self.timeout_container.reset();
2732 // Pre-compute byte totals for proportional overhead distribution.
2733 let byte_totals = self.compute_stream_byte_totals(context);
2734 let mut io_slices: Vec<IoSlice<'static>> = Vec::new();
2735
2736 if let Some(
2737 write_stream @ H2StreamId::Other {
2738 id: stream_id,
2739 gid: global_stream_id,
2740 },
2741 ) = self.expect_write
2742 {
2743 let stream = &mut context.streams[global_stream_id];
2744 let stream_state = stream.state;
2745 let parts = stream.split(&self.position);
2746 let kawa = parts.wbuffer;
2747 // Resume path: if the same stream is parked waiting for buffer
2748 // space (expect_read matches write_stream), pass the amount so
2749 // flush_stream_out can re-enable READABLE as soon as we drain.
2750 let cross_read_amount = match self.expect_read {
2751 Some((read_stream, amount)) if write_stream == read_stream => Some(amount),
2752 _ => None,
2753 };
2754 let mut resume_bytes: usize = 0;
2755 let outcome = Self::flush_stream_out(
2756 &mut self.socket,
2757 kawa,
2758 parts.metrics,
2759 &self.position,
2760 &mut self.readiness,
2761 &mut context.debug,
2762 2,
2763 global_stream_id,
2764 None,
2765 cross_read_amount,
2766 &mut io_slices,
2767 Some(&mut resume_bytes),
2768 );
2769 // Refresh the per-stream idle timer when outbound bytes move: a
2770 // large response delivered at low bandwidth is "active", not idle,
2771 // even when the peer sends no inbound frames.
2772 if resume_bytes > 0 {
2773 if let Some(t) = self.stream_last_activity_at.get_mut(&stream_id) {
2774 *t = Instant::now();
2775 }
2776 // Clear the flow-control-stall deadline ONLY when the effective
2777 // send window is genuinely open — that alone is a real un-stall.
2778 // A window-stalled stream can flush a `WINDOW_UPDATE(+1)`-drip
2779 // byte HERE via socket-backpressure resume; clearing on that
2780 // would reset the deadline at 1-byte granularity and re-open the
2781 // drip the M2 cumulative-stall budget closes. While still blocked,
2782 // leave the deadline (and its progress accumulator) for the main
2783 // write loop's budget to govern — keeping the two maps in lockstep.
2784 if min(*parts.window, self.flow_control.window) > 0 {
2785 self.stream_fc_stalled_since.remove(&stream_id);
2786 self.stream_fc_stalled_progress.remove(&stream_id);
2787 }
2788 }
2789 if outcome == FlushOutcome::Stalled {
2790 return MuxResult::Continue;
2791 }
2792 self.expect_write = None;
2793 if (kawa.is_terminated() || kawa.is_error())
2794 && kawa.is_completed()
2795 && !Self::handle_1xx_reset(kawa, stream_state, &mut endpoint)
2796 {
2797 let (client_rtt, server_rtt) = Self::snapshot_rtts(
2798 &self.position,
2799 &self.socket,
2800 &endpoint,
2801 stream.linked_token(),
2802 );
2803
2804 if let Some((dead_id, token)) = Self::try_recycle_server_stream(
2805 &self.position,
2806 &mut self.bytes,
2807 &self.streams,
2808 stream,
2809 global_stream_id,
2810 stream_id,
2811 byte_totals,
2812 &mut context.debug,
2813 context.listener.clone(),
2814 client_rtt,
2815 server_rtt,
2816 ) {
2817 // Remove the recycled stream from the connection maps
2818 // before endpoint.end_stream() can trigger teardown.
2819 // Otherwise session close can observe a stale `Recycle`
2820 // entry in self.streams and mis-handle the connection as
2821 // if it still had an active H2 stream.
2822 self.remove_dead_stream(dead_id, global_stream_id);
2823 if let Some(token) = token {
2824 remove_backend_stream(
2825 &mut context.backend_streams,
2826 token,
2827 global_stream_id,
2828 );
2829 endpoint.end_stream(token, global_stream_id, context);
2830 }
2831 }
2832 }
2833 }
2834
2835 self.gauge_connection_state();
2836
2837 let scheme: &'static [u8] = if context.listener.borrow().protocol() == Protocol::HTTPS {
2838 b"https"
2839 } else {
2840 b"http"
2841 };
2842 let mut completed_streams = Vec::new();
2843 let mut converter_buf = std::mem::take(&mut self.converter_buf);
2844 converter_buf.clear();
2845 let mut converter = converter::H2BlockConverter {
2846 max_frame_size: self.peer_settings.settings_max_frame_size as usize,
2847 window: 0,
2848 stream_id: 0,
2849 encoder: &mut self.encoder,
2850 out: converter_buf,
2851 scheme,
2852 lowercase_buf: std::mem::take(&mut self.lowercase_buf),
2853 cookie_buf: std::mem::take(&mut self.cookie_buf),
2854 // When this connection is a backend client we are writing
2855 // toward the upstream backend — flow-control stalls in that
2856 // direction are scoped to `backend.flow_control.paused` (in
2857 // addition to the existing direction-agnostic
2858 // `h2.flow_control_stall`).
2859 position_is_client: self.position.is_client(),
2860 // RFC 9218 §4: toggled per-stream in the loop below, driven by
2861 // `Prioriser::get(stream_id).1`. Non-incremental by default so
2862 // unit tests and non-scheduled callers (e.g. the resume path
2863 // above) keep the sequential semantics.
2864 incremental_mode: false,
2865 // Populated once per write pass from `apply_incremental_rotation`
2866 // below. The converter uses `incremental_peer_count <= 1` to skip
2867 // the RFC 9218 yield-after-one-DATA behaviour when there is no
2868 // peer to interleave with (solo-bucket fast path).
2869 incremental_peer_count: 0,
2870 // RFC 7541 §6.3: move the pending size-update onto the converter
2871 // so the first header block of this pass prepends the signal.
2872 // We clear the connection-side mirror only AFTER the write pass
2873 // confirms emission via `converter.size_update_emitted`, so a
2874 // DATA-only write pass (no header block) does not drop the
2875 // signal.
2876 pending_table_size_update: self.pending_table_size_update,
2877 size_update_emitted: false,
2878 // Reset on every write pass; `check_header_capacity` flips it
2879 // mid-call and `finalize` commits the abort by flipping
2880 // `kawa.parsing_phase` to Error so the next pass emits
2881 // RST_STREAM(InternalError).
2882 pending_oversized_abort: false,
2883 };
2884 self.priorities_buf.clear();
2885 self.priorities_buf.extend(self.streams.keys().copied());
2886 // RFC 9218 §4 primary sort: ascending urgency, then stream ID for
2887 // stability. The incremental flag is handled by
2888 // `apply_incremental_rotation` below so it does not perturb the
2889 // non-incremental fast path.
2890 self.priorities_buf.sort_by_cached_key(|id| {
2891 let (urgency, _) = self.prioriser.get(id);
2892 (urgency, *id)
2893 });
2894 // RFC 9218 §4: inside each urgency bucket, move incremental streams
2895 // to the tail and rotate them by the per-connection round-robin
2896 // cursor so no single slow-draining stream can starve its
2897 // same-urgency incremental peers.
2898 let incremental_count = self
2899 .prioriser
2900 .apply_incremental_rotation(&mut self.priorities_buf);
2901
2902 // RFC 9218 §4 refinement (Tier 3a): the connection-global
2903 // `incremental_count` is too coarse for `converter.incremental_peer_count`.
2904 // A solo `u=0, i` stream with an unrelated `u=7, i` peer in a
2905 // different urgency bucket would still see `incremental_peer_count > 1`
2906 // and voluntarily yield — stranding bytes the invariant-15/16 guards
2907 // were meant to prevent. Scope the count to same-urgency streams that
2908 // are actually ready to emit this pass (eligibility mirrors the check
2909 // in the write loop below).
2910 let mut ready_incremental_by_urgency: HashMap<u8, usize> = HashMap::new();
2911 for &sid in self.priorities_buf.iter() {
2912 let (urgency, is_incremental) = self.prioriser.get(&sid);
2913 if !is_incremental {
2914 continue;
2915 }
2916 let Some(&gid) = self.streams.get(&sid) else {
2917 continue;
2918 };
2919 let wbuffer = match self.position {
2920 Position::Server => &context.streams[gid].back,
2921 Position::Client(..) => &context.streams[gid].front,
2922 };
2923 if wbuffer.is_main_phase()
2924 || (wbuffer.is_terminated() && !wbuffer.is_completed())
2925 || (wbuffer.is_error() && !self.rst_sent.contains(&sid))
2926 {
2927 *ready_incremental_by_urgency.entry(urgency).or_insert(0) += 1;
2928 }
2929 }
2930
2931 trace!(
2932 "{} PRIORITIES: {:?} (incremental_count={}, per_bucket={:?})",
2933 log_context!(self),
2934 self.priorities_buf,
2935 incremental_count,
2936 ready_incremental_by_urgency
2937 );
2938 let mut socket_write = false;
2939 // RFC 9218 §4 round-robin: remember the first incremental stream we
2940 // served this pass so we can advance `Prioriser::incremental_cursor`
2941 // to it, causing the next pass to start with the stream just after.
2942 let mut first_incremental_fired: Option<StreamId> = None;
2943 // Total outbound bytes emitted across all stream flushes this pass —
2944 // `finalize_write` uses this to distinguish a voluntary scheduler
2945 // yield (progress + pending back-buffer, LIFECYCLE §9 invariant 16)
2946 // from a no-progress wait state (e.g. flow-control starvation).
2947 let mut total_bytes_written: usize = 0;
2948 // Collect every fresh RST_STREAM emitted via the converter
2949 // (`initialize` chokepoint or the HPACK over-budget abort path)
2950 // so we can run `account_emitted_rst` for each one AFTER the
2951 // converter is dropped — the converter holds `&mut self.encoder`
2952 // for the loop body so we cannot take `&mut self` until then.
2953 let mut freshly_emitted_rsts: Vec<H2Error> = Vec::new();
2954 'outer: for idx in 0..self.priorities_buf.len() {
2955 let stream_id = self.priorities_buf[idx];
2956 let Some(&global_stream_id) = self.streams.get(&stream_id) else {
2957 error!(
2958 "{} stream_id {} from sorted keys missing in streams map",
2959 log_context!(self),
2960 stream_id
2961 );
2962 continue;
2963 };
2964 let (urgency, is_incremental) = self.prioriser.get(&stream_id);
2965 let stream = &mut context.streams[global_stream_id];
2966 let stream_state = stream.state;
2967 let parts = stream.split(&self.position);
2968 let kawa = parts.wbuffer;
2969 // Hoisted out of the gate below so the post-flush flow-control-stall
2970 // classification can see how many flow-control bytes this pass moved.
2971 let mut consumed: i32 = 0;
2972 if kawa.is_main_phase()
2973 || (kawa.is_terminated() && !kawa.is_completed())
2974 || (kawa.is_error() && !self.rst_sent.contains(&stream_id))
2975 {
2976 let window = min(*parts.window, self.flow_control.window);
2977 converter.window = window;
2978 converter.stream_id = stream_id;
2979 // RFC 9218 §4: incremental streams yield the converter after
2980 // a single DATA frame so same-urgency peers interleave.
2981 converter.incremental_mode = is_incremental;
2982 // Same-urgency-bucket ready-peer count (Tier 3a, LIFECYCLE §9
2983 // invariant 17). The converter skips the yield when there is
2984 // no peer in the same bucket to interleave with — prevents
2985 // the `finalize_write` WRITABLE-withdrawal strand (see
2986 // `test_h2_solo_incremental_drains_fully`). A connection-wide
2987 // count would wrongly yield for a solo incremental stream
2988 // when another urgency bucket happens to contain an
2989 // incremental peer.
2990 converter.incremental_peer_count = ready_incremental_by_urgency
2991 .get(&urgency)
2992 .copied()
2993 .unwrap_or(0);
2994 // Track RST_STREAM dedup: if kawa is in error state, the converter
2995 // will generate a RST_STREAM frame via `initialize`. Mark it so we
2996 // don't send a duplicate on the next writable cycle.
2997 if kawa.is_error() {
2998 let freshly_rst = self.rst_sent.insert(stream_id);
2999 // LIFECYCLE §9 invariant 17: any transition to ineligible
3000 // mid-pass MUST decrement ready_incremental_by_urgency so
3001 // later streams in the same 'outer iteration see the live
3002 // count, not the snapshot. Missing this costs one voluntary
3003 // yield per same-urgency peer that trails the RST.
3004 if freshly_rst
3005 && is_incremental
3006 && let Some(c) = ready_incremental_by_urgency.get_mut(&urgency)
3007 {
3008 *c = c.saturating_sub(1);
3009 }
3010 // Account for the RST that `initialize` is about to emit
3011 // for this stream. Without this the MadeYouReset lifetime
3012 // cap is evadable: any path that flips `parsing_phase` to
3013 // Error before reaching this gate (oversized inbound
3014 // trailers, malformed bodies, etc.) would land an
3015 // unaccounted RST on the wire. We defer the actual
3016 // accounting call until after `drop(converter)` — the
3017 // converter holds `&mut self.encoder` here.
3018 if freshly_rst {
3019 freshly_emitted_rsts.push(rst_error_from_kawa(kawa));
3020 }
3021 }
3022 // Apply per-frontend response-side header edits
3023 // (set/replace/delete) stashed by the routing layer at
3024 // request time. H2 frontends always run as Server
3025 // position; the back-side H2 client (when sozu speaks
3026 // H2 to a backend) is a request emission and was
3027 // already mutated by Router::route_from_request.
3028 //
3029 // The snapshot is **drained** via `mem::take` so the
3030 // injection runs exactly once per response. Without
3031 // this, a re-entry of `write_streams` for the same
3032 // stream (multi-frame body, flow-control yield, or
3033 // RFC 9218 same-urgency round-robin) would re-call
3034 // `apply_response_header_edits` after `kawa.prepare`
3035 // had already consumed the `Block::Flags{end_header}`
3036 // anchor — the helper falls back to
3037 // `kawa.blocks.len()` and appends the edit AFTER all
3038 // remaining DATA blocks. The next prepare cycle then
3039 // encodes that orphan `Block::Header` into
3040 // `H2BlockConverter.out` with no closing
3041 // `Block::Flags{end_header}` to flush it as a HEADERS
3042 // frame, and `H2BlockConverter::finalize` trips the
3043 // "out buffer not empty (38 bytes remaining), clearing"
3044 // defense-in-depth log on every re-entry. 38 bytes is
3045 // the static-table HPACK encoding of a typical HSTS
3046 // header, which is how the symptom surfaces in
3047 // production once the listener-default HSTS reaches a
3048 // non-trivial share of frontends.
3049 if matches!(self.position, super::Position::Server)
3050 && !parts.context.headers_response.is_empty()
3051 {
3052 let edits = std::mem::take(&mut parts.context.headers_response);
3053 super::shared::apply_response_header_edits(kawa, &edits);
3054 }
3055 kawa.prepare(&mut converter);
3056 // The pre-prepare gate at line 2483 only inserts into
3057 // `rst_sent` when `kawa.is_error()` is already true on
3058 // entry. The HPACK over-budget abort path
3059 // (`H2BlockConverter::check_header_capacity` →
3060 // `finalize`) flips `parsing_phase` to Error AND pushes
3061 // its own RST_STREAM frame inside this same prepare
3062 // pass; without a post-prepare insert here the next
3063 // writable cycle would gate-pass and double-emit a
3064 // RST_STREAM via the existing `initialize` chokepoint.
3065 //
3066 // Per Codex P2: the converter's direct RST emission
3067 // bypasses the metric/flood accounting that
3068 // `Self::reset_stream` performs. Mirror it here so a
3069 // peer that drives oversized headers across many
3070 // streams cannot escape the MadeYouReset emitted-RST
3071 // lifetime cap and so dashboards see the per-error
3072 // counter and the global tx counter.
3073 //
3074 // Per Codex P3: when an incremental stream flips to
3075 // Error mid-prepare, the RFC 9218 §4 yield-after-one
3076 // accounting must drop this stream from the
3077 // same-urgency ready bucket so trailing peers see the
3078 // live count.
3079 let freshly_rst_post_prepare = kawa.is_error() && self.rst_sent.insert(stream_id);
3080 if freshly_rst_post_prepare {
3081 // Defer accounting until after `drop(converter)`; same
3082 // reason as the pre-prepare collector above.
3083 freshly_emitted_rsts.push(rst_error_from_kawa(kawa));
3084 if is_incremental
3085 && let Some(c) = ready_incremental_by_urgency.get_mut(&urgency)
3086 {
3087 *c = c.saturating_sub(1);
3088 }
3089 }
3090 consumed = window - converter.window;
3091 *parts.window = parts.window.saturating_sub(consumed);
3092 self.flow_control.window = self.flow_control.window.saturating_sub(consumed);
3093 if is_incremental && consumed > 0 && first_incremental_fired.is_none() {
3094 first_incremental_fired = Some(stream_id);
3095 }
3096 }
3097 context.debug.push(DebugEvent::S(
3098 stream_id,
3099 global_stream_id,
3100 kawa.parsing_phase,
3101 kawa.blocks.len(),
3102 kawa.out.len(),
3103 ));
3104 let mut stream_bytes: usize = 0;
3105 let outcome = Self::flush_stream_out(
3106 &mut self.socket,
3107 kawa,
3108 parts.metrics,
3109 &self.position,
3110 &mut self.readiness,
3111 &mut context.debug,
3112 3,
3113 global_stream_id,
3114 Some(&mut socket_write),
3115 None,
3116 &mut io_slices,
3117 Some(&mut stream_bytes),
3118 );
3119 // Refresh the per-stream idle timer on outbound bytes. Without
3120 // this, a long-running response trickled at low bandwidth would
3121 // be killed by `cancel_timed_out_streams` mid-delivery — the
3122 // inbound-only refresh at h2.rs:3887-3895 / 4026-4031 never
3123 // fires while the peer is idle.
3124 if stream_bytes > 0
3125 && let Some(t) = self.stream_last_activity_at.get_mut(&stream_id)
3126 {
3127 *t = Instant::now();
3128 }
3129 // Arm/age the dedicated flow-control-stall deadline that catches a
3130 // window-stalled stream — a buffered RESPONSE to a slow frontend
3131 // (`Position::Server`) OR a buffered request UPLOAD to a slow H2
3132 // backend (`Position::Client`): window-stall reaping is bidirectional
3133 // by design (M4), so there is no position gate here. Set only when the
3134 // stream holds sendable buffered data it cannot send because its
3135 // effective send window is exhausted; unlike `stream_last_activity_at`
3136 // it is NEVER refreshed by inbound DATA/HEADERS, so a peer dribbling
3137 // 1-byte DATA cannot keep it warm.
3138 //
3139 // M2 cumulative-stall budget: a genuinely OPEN window clears the
3140 // deadline immediately (real un-stall). While the window stays
3141 // blocked, accumulate this pass's outbound drain; only cumulative
3142 // progress reaching `FC_STALL_CLEAR_FLOOR` (a full frame of real
3143 // delivery) clears it. A `WINDOW_UPDATE(+1)` drip drains ~1 byte/pass
3144 // straight back to a zero window, so it never reaches the floor — the
3145 // deadline ages out and `cancel_timed_out_streams` RST(CANCEL)s the
3146 // slot-pinning stream after `stream_idle_timeout`.
3147 let outbound_window_blocked = has_sendable_response(kawa)
3148 && min(*parts.window, self.flow_control.window) <= 0
3149 && (!kawa.blocks.is_empty() || !kawa.out.is_empty());
3150 match fc_stall_budget_decision(
3151 outbound_window_blocked,
3152 consumed,
3153 self.stream_fc_stalled_progress.get(&stream_id).copied(),
3154 ) {
3155 FcStallAction::Clear => {
3156 self.stream_fc_stalled_since.remove(&stream_id);
3157 self.stream_fc_stalled_progress.remove(&stream_id);
3158 }
3159 FcStallAction::Arm { progress } => {
3160 self.stream_fc_stalled_since
3161 .entry(stream_id)
3162 .or_insert_with(Instant::now);
3163 self.stream_fc_stalled_progress.insert(stream_id, progress);
3164 }
3165 }
3166 total_bytes_written = total_bytes_written.saturating_add(stream_bytes);
3167 if outcome == FlushOutcome::Stalled {
3168 self.expect_write = Some(H2StreamId::Other {
3169 id: stream_id,
3170 gid: global_stream_id,
3171 });
3172 break 'outer;
3173 }
3174 self.expect_write = None;
3175 if (kawa.is_terminated() || kawa.is_error())
3176 && kawa.is_completed()
3177 && !Self::handle_1xx_reset(kawa, stream_state, &mut endpoint)
3178 {
3179 let close_frontend =
3180 matches!(self.position, Position::Server) && !parts.context.keep_alive_frontend;
3181 let (client_rtt, server_rtt) = Self::snapshot_rtts(
3182 &self.position,
3183 &self.socket,
3184 &endpoint,
3185 stream.linked_token(),
3186 );
3187
3188 if let Some((dead_id, token)) = Self::try_recycle_server_stream(
3189 &self.position,
3190 &mut self.bytes,
3191 &self.streams,
3192 stream,
3193 global_stream_id,
3194 stream_id,
3195 byte_totals,
3196 &mut context.debug,
3197 context.listener.clone(),
3198 client_rtt,
3199 server_rtt,
3200 ) {
3201 completed_streams.push((dead_id, global_stream_id, token, close_frontend));
3202 // LIFECYCLE §9 invariant 17: decrement INSIDE 'outer so
3203 // later iterations see the reduced count. The post-loop
3204 // retirement at remove_dead_stream is too late.
3205 if is_incremental
3206 && let Some(c) = ready_incremental_by_urgency.get_mut(&urgency)
3207 {
3208 *c = c.saturating_sub(1);
3209 }
3210 }
3211 }
3212 }
3213 gauge!(
3214 "h2.streams.ready_incremental.by_urgency",
3215 ready_incremental_by_urgency
3216 .values()
3217 .copied()
3218 .sum::<usize>()
3219 );
3220 // Reclaim the converter's reusable buffers before any &mut self calls,
3221 // since the converter borrows self.encoder.
3222 let converter_out = std::mem::take(&mut converter.out);
3223 let lowercase_buf = std::mem::take(&mut converter.lowercase_buf);
3224 let cookie_buf = std::mem::take(&mut converter.cookie_buf);
3225 // RFC 7541 §6.3: clear our mirror of the pending size-update only
3226 // AFTER the converter confirmed the signal was emitted to its
3227 // output buffer. A DATA-only pass leaves `size_update_emitted` as
3228 // `false` so the signal stays queued for the next pass with a
3229 // header block.
3230 let size_update_emitted = converter.size_update_emitted;
3231 drop(converter);
3232 if size_update_emitted {
3233 self.pending_table_size_update = None;
3234 }
3235 // Account every RST that the converter emitted during this pass
3236 // (pre-prepare gate + post-prepare HPACK over-budget abort) so
3237 // the global tx counter, the per-error breakdown, and the
3238 // MadeYouReset emitted-RST lifetime cap stay in step. If the
3239 // cap trips, propagate the GOAWAY result.
3240 for error in freshly_emitted_rsts {
3241 if let Some(result) = self.account_emitted_rst(error) {
3242 return result;
3243 }
3244 }
3245 self.converter_buf = converter_out;
3246 self.lowercase_buf = lowercase_buf;
3247 self.cookie_buf = cookie_buf;
3248 self.shrink_converter_buffers();
3249 // RFC 9218 §4: commit the round-robin cursor so the next writable
3250 // cycle begins with the stream immediately after the one we fired
3251 // first this pass.
3252 self.prioriser
3253 .advance_incremental_cursor(first_incremental_fired);
3254 let mut close_frontend_after_completed_stream = false;
3255 for (dead_id, global_stream_id, token, close_frontend) in completed_streams {
3256 // The main write loop borrows self.encoder, so we can't mutate the
3257 // H2 maps inline. Retire the recycled stream immediately after the
3258 // converter borrow ends, before endpoint.end_stream() can trigger
3259 // teardown and observe a stale `Recycle` entry in self.streams.
3260 self.remove_dead_stream(dead_id, global_stream_id);
3261 close_frontend_after_completed_stream |= close_frontend;
3262 if let Some(token) = token {
3263 remove_backend_stream(&mut context.backend_streams, token, global_stream_id);
3264 endpoint.end_stream(token, global_stream_id, context);
3265 }
3266 }
3267 if close_frontend_after_completed_stream && !self.drain.draining {
3268 return if self.streams.is_empty() {
3269 self.goaway(H2Error::NoError)
3270 } else {
3271 self.graceful_goaway()
3272 };
3273 }
3274 self.finalize_write(socket_write, total_bytes_written, context)
3275 }
3276
3277 /// Remove streams that completed their lifecycle from all tracking maps.
3278 /// After forwarding a 1xx informational response (100 Continue, 103 Early Hints),
3279 /// reset the back buffer and re-enable backend readable so the final response
3280 /// can arrive on the same stream. Returns true if the response was 1xx.
3281 #[allow(clippy::too_many_arguments)]
3282 fn flush_stream_out(
3283 socket: &mut Front,
3284 kawa: &mut GenericHttpStream,
3285 metrics: &mut SessionMetrics,
3286 position: &Position,
3287 readiness: &mut Readiness,
3288 debug: &mut DebugHistory,
3289 debug_site: usize,
3290 global_stream_id: GlobalStreamId,
3291 mut wrote: Option<&mut bool>,
3292 cross_read_amount: Option<usize>,
3293 io_slices: &mut Vec<IoSlice<'static>>,
3294 mut bytes_written: Option<&mut usize>,
3295 ) -> FlushOutcome {
3296 while !kawa.out.is_empty() {
3297 if let Some(flag) = wrote.as_deref_mut() {
3298 *flag = true;
3299 }
3300 io_slices.clear();
3301 let buffer = kawa.storage.buffer();
3302 for block in kawa.out.iter() {
3303 match block {
3304 kawa::OutBlock::Delimiter => break,
3305 kawa::OutBlock::Store(store) => {
3306 let data = store.data(buffer);
3307 // SAFETY: the IoSlice references point into kawa's
3308 // storage buffer. They are used only for the
3309 // socket_write_vectored call below and cleared
3310 // immediately after, before kawa.consume() which may
3311 // relocate the buffer via ptr::copy (shift). No
3312 // dangling 'static refs exist during consume().
3313 let data: &'static [u8] =
3314 unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
3315 io_slices.push(IoSlice::new(data));
3316 }
3317 }
3318 }
3319 let (size, status) = socket.socket_write_vectored(io_slices);
3320 io_slices.clear();
3321 debug_assert!(
3322 io_slices.is_empty(),
3323 "IoSlice refs must be cleared before consume"
3324 );
3325 debug.push(DebugEvent::SocketIO(debug_site, global_stream_id, size));
3326 kawa.consume(size);
3327 position.count_bytes_out_counter(size);
3328 position.count_bytes_out(metrics, size);
3329 if let Some(counter) = bytes_written.as_deref_mut() {
3330 *counter = counter.saturating_add(size);
3331 }
3332 if let Some(amount) = cross_read_amount {
3333 // Resume path: same stream is parked waiting for buffer space.
3334 // Re-enable READABLE once the write freed enough room.
3335 if kawa.storage.available_space() >= amount {
3336 readiness.interest.insert(Ready::READABLE);
3337 }
3338 }
3339 if update_readiness_after_write(size, status, readiness) {
3340 return FlushOutcome::Stalled;
3341 }
3342 }
3343 FlushOutcome::Drained
3344 }
3345
3346 fn handle_1xx_reset<E: Endpoint>(
3347 kawa: &mut GenericHttpStream,
3348 stream_state: StreamState,
3349 endpoint: &mut E,
3350 ) -> bool {
3351 let is_1xx = matches!(
3352 kawa.detached.status_line,
3353 kawa::StatusLine::Response { code, .. } if (100..200).contains(&code)
3354 );
3355 if !is_1xx {
3356 return false;
3357 }
3358 debug!(
3359 "{} H2 write_streams: 1xx informational forwarded, resetting back buffer",
3360 log_module_context!()
3361 );
3362 kawa.clear();
3363 if let StreamState::Linked(token) = stream_state {
3364 let readiness = endpoint.readiness_mut(token);
3365 readiness.interest.insert(Ready::READABLE);
3366 readiness.signal_pending_read();
3367 }
3368 true
3369 }
3370
3371 /// Re-arm edge-triggered WRITABLE event if rustls still has buffered TLS data.
3372 fn ensure_tls_flushed(&mut self) {
3373 if self.socket.socket_wants_write() {
3374 self.readiness.signal_pending_write();
3375 }
3376 }
3377
3378 /// Evict every per-stream piece of state carried by this `ConnectionH2`.
3379 ///
3380 /// **Invariant**: `rst_sent`, `stream_last_activity_at`,
3381 /// `stream_fc_stalled_since`, `stream_fc_stalled_progress` and `prioriser`
3382 /// MUST be emptied of `stream_id` here — they are the only five per-stream
3383 /// caches that are not stored in the slab-allocated
3384 /// `Context.streams[]`. Forgetting any of them causes unbounded memory
3385 /// growth on long-lived connections with many cancelled streams. The
3386 /// `debug_assert`s below fail loudly in test builds if someone adds a
3387 /// new per-stream cache without updating this function.
3388 fn remove_dead_stream(&mut self, stream_id: StreamId, global_stream_id: GlobalStreamId) {
3389 if self.streams.remove(&stream_id).is_none() {
3390 error!(
3391 "{} dead stream_id {} missing from streams map",
3392 log_context!(self),
3393 stream_id
3394 );
3395 }
3396 self.rst_sent.remove(&stream_id);
3397 self.stream_last_activity_at.remove(&stream_id);
3398 self.stream_fc_stalled_since.remove(&stream_id);
3399 self.stream_fc_stalled_progress.remove(&stream_id);
3400 self.prioriser.remove(&stream_id);
3401 debug_assert!(
3402 !self.rst_sent.contains(&stream_id),
3403 "rst_sent still contains stream_id {stream_id} after eviction"
3404 );
3405 debug_assert!(
3406 !self.stream_last_activity_at.contains_key(&stream_id),
3407 "stream_last_activity_at still contains stream_id {stream_id} after eviction"
3408 );
3409 debug_assert!(
3410 !self.stream_fc_stalled_since.contains_key(&stream_id),
3411 "stream_fc_stalled_since still contains stream_id {stream_id} after eviction"
3412 );
3413 debug_assert!(
3414 !self.stream_fc_stalled_progress.contains_key(&stream_id),
3415 "stream_fc_stalled_progress still contains stream_id {stream_id} after eviction"
3416 );
3417 // Invariant: expect_write/expect_read must not reference a gid whose
3418 // context slot may be popped by shrink_trailing_recycle after eviction.
3419 if matches!(self.expect_write, Some(H2StreamId::Other { gid, .. }) if gid == global_stream_id)
3420 {
3421 self.expect_write = None;
3422 }
3423 if matches!(
3424 self.expect_read,
3425 Some((H2StreamId::Other { gid, .. }, _)) if gid == global_stream_id
3426 ) {
3427 self.expect_read = None;
3428 }
3429 }
3430
3431 /// Drop stream-id mappings for streams that never became active before a
3432 /// connection-level close. This happens on incomplete/oversized header
3433 /// blocks: the stream slot is created on the initial HEADERS frame, then a
3434 /// GOAWAY closes the connection before the request is fully materialized.
3435 fn prune_inactive_streams_while_closing<L>(&mut self, context: &mut Context<L>)
3436 where
3437 L: ListenerHandler + L7ListenerHandler,
3438 {
3439 if !self.drain.draining || !matches!(self.state, H2State::GoAway | H2State::Error) {
3440 return;
3441 }
3442
3443 let stale_streams = self
3444 .streams
3445 .iter()
3446 .filter_map(|(&stream_id, &global_stream_id)| {
3447 (!context.streams[global_stream_id].state.is_open())
3448 .then_some((stream_id, global_stream_id))
3449 })
3450 .collect::<Vec<_>>();
3451
3452 for (stream_id, global_stream_id) in stale_streams {
3453 let stream = &mut context.streams[global_stream_id];
3454 if stream.state == StreamState::Idle {
3455 stream.front.clear();
3456 stream.front.storage.clear();
3457 stream.back.clear();
3458 stream.back.storage.clear();
3459 stream.metrics.reset();
3460 stream.state = StreamState::Recycle;
3461 }
3462 self.remove_dead_stream(stream_id, global_stream_id);
3463 }
3464 }
3465
3466 /// Shrink reusable converter buffers when they grow beyond 16 KB to avoid
3467 /// holding memory after a burst of large headers.
3468 fn shrink_converter_buffers(&mut self) {
3469 if self.converter_buf.capacity() > 16_384 {
3470 self.converter_buf.shrink_to(4096);
3471 }
3472 if self.lowercase_buf.capacity() > 16_384 {
3473 self.lowercase_buf.shrink_to(4096);
3474 }
3475 if self.cookie_buf.capacity() > 16_384 {
3476 self.cookie_buf.shrink_to(4096);
3477 }
3478 }
3479
3480 /// Post-write phase: check drain completion, flush TLS, and update readiness.
3481 ///
3482 /// `bytes_written_this_pass` reports the total outbound bytes `write_streams`
3483 /// pushed to the socket (across every stream), and is used to distinguish
3484 /// two very different "no `expect_write`" states:
3485 ///
3486 /// - **Voluntary yield with progress**: at least one DATA/HEADERS frame
3487 /// emitted, but a stream left non-empty `back.out`/`back.blocks` because
3488 /// the converter yielded (e.g. RFC 9218 incremental rotation). LIFECYCLE
3489 /// §9 invariant 16: keep `Ready::WRITABLE` armed so the session loop can
3490 /// resume flushing on the next tick without waiting for an external
3491 /// wake-up that edge-triggered epoll will not deliver.
3492 /// - **No progress at all**: converter pushed every block back (e.g. flow
3493 /// window exhausted, no HEADERS ready yet). Strip `Ready::WRITABLE` —
3494 /// forward progress must come from an external trigger
3495 /// (`WINDOW_UPDATE`, new request), not from looping writable().
3496 ///
3497 /// Returns `MuxResult::Continue` in the normal case, or triggers a graceful
3498 /// GOAWAY when draining and all streams have completed.
3499 fn finalize_write<L>(
3500 &mut self,
3501 socket_write: bool,
3502 bytes_written_this_pass: usize,
3503 context: &mut Context<L>,
3504 ) -> MuxResult
3505 where
3506 L: ListenerHandler + L7ListenerHandler,
3507 {
3508 // RFC 9113 §6.8: if draining and all streams have completed,
3509 // send the final GOAWAY with the actual last_stream_id
3510 if self.drain.draining && self.streams.is_empty() {
3511 return self.graceful_goaway();
3512 }
3513
3514 if self.socket.socket_wants_write() {
3515 if !socket_write {
3516 self.socket.socket_write(&[]);
3517 }
3518 // Edge-triggered epoll: re-arm WRITABLE if rustls still has
3519 // pending encrypted data (first check triggers flush, second re-checks).
3520 self.ensure_tls_flushed();
3521 } else if self.expect_write.is_none() {
3522 // LIFECYCLE §9 invariant 16: retain `Ready::WRITABLE` when a
3523 // voluntary scheduler yield leaves stranded bytes in a stream's
3524 // `back.out`/`back.blocks` *after* the pass made forward
3525 // progress. Requiring progress avoids the degenerate no-progress
3526 // loop (e.g. flow-control-starved streams) that would otherwise
3527 // busy-spin against the session dispatcher.
3528 if bytes_written_this_pass > 0
3529 && any_stream_has_pending_back(&self.streams, &context.streams)
3530 {
3531 #[cfg(debug_assertions)]
3532 context.debug.push(DebugEvent::Str(
3533 "finalize_write: invariant 16 retained WRITABLE (pending back-buffer)"
3534 .to_owned(),
3535 ));
3536 } else if !self.pending_rst_streams.is_empty()
3537 || !self.flow_control.pending_window_updates.is_empty()
3538 {
3539 // Control-frame liveness: `flush_pending_control_frames` is
3540 // gated on `expect_write.is_none()`, so when a prior partial
3541 // write deferred the flush the RST / WINDOW_UPDATE queues
3542 // stay non-empty after `expect_write` finally drains. Without
3543 // this rearm the next tick would drop `Ready::WRITABLE` and
3544 // the queued RST would stall until an unrelated event
3545 // re-triggered writable — which is exactly the scenario
3546 // h2spec trips by sending back-to-back malformed streams.
3547 #[cfg(debug_assertions)]
3548 context.debug.push(DebugEvent::Str(
3549 "finalize_write: retained WRITABLE (control queue non-empty)".to_owned(),
3550 ));
3551 self.readiness.arm_writable();
3552 incr!(names::h2::SIGNAL_WRITABLE_REARMED_CONTROL_QUEUE);
3553 } else {
3554 // We wrote everything
3555 #[cfg(debug_assertions)]
3556 context.debug.push(DebugEvent::Str(format!(
3557 "Wrote everything: {:?}",
3558 self.streams
3559 )));
3560 self.readiness.interest.remove(Ready::WRITABLE);
3561 }
3562 }
3563 MuxResult::Continue
3564 }
3565
3566 /// Flush pending control frames (zero-buffer resume, WINDOW_UPDATEs, RST_STREAMs)
3567 /// before entering the main writable state machine.
3568 ///
3569 /// Returns `Some(result)` if the caller should return early (e.g. socket would
3570 /// block, GOAWAY triggered), or `None` if writable() should proceed normally.
3571 fn flush_pending_control_frames(&mut self) -> Option<MuxResult> {
3572 if self.frontend_hung_up_while_draining() {
3573 self.expect_write = None;
3574 self.zero.storage.clear();
3575 self.flow_control.pending_window_updates.clear();
3576 self.pending_rst_streams.clear();
3577 }
3578
3579 // RFC 9113 §6.5: check if peer has timed out on SETTINGS ACK
3580 if let Some(sent_at) = self.settings_sent_at
3581 && sent_at.elapsed() >= SETTINGS_ACK_TIMEOUT
3582 {
3583 warn!(
3584 "{} SETTINGS ACK timeout: no SETTINGS ACK observed within {:?}",
3585 log_context!(self),
3586 SETTINGS_ACK_TIMEOUT
3587 );
3588 return Some(self.goaway(H2Error::SettingsTimeout));
3589 }
3590
3591 // Stage — resume zero-buffer flush.
3592 // If a previous write was partial, finish it before serialising any
3593 // new control frames. Don't reset the timeout for control frame
3594 // writes (SETTINGS ACK, PING response, WINDOW_UPDATE) — only
3595 // application-data writes should reset it.
3596 if let Some(H2StreamId::Zero) = self.expect_write {
3597 if self.flush_zero_to_socket() {
3598 self.ensure_tls_flushed();
3599 return Some(MuxResult::Continue);
3600 }
3601 // When H2StreamId::Zero is used to write, READABLE is disabled —
3602 // re-enable it now that the flush is complete.
3603 self.readiness.interest.insert(Ready::READABLE);
3604 self.expect_write = None;
3605 }
3606
3607 // Stage — drain pending WINDOW_UPDATE frames.
3608 // Serialize and flush them inline to avoid extra event loop
3609 // iterations that could cause response data to be sent before
3610 // subsequent frames are validated.
3611 if !self.flow_control.pending_window_updates.is_empty() && self.expect_write.is_none() {
3612 let kawa = &mut self.zero;
3613 kawa.storage.clear();
3614 let buf = kawa.storage.space();
3615 let mut offset = 0;
3616 // Track which entries we successfully serialized so we can remove them.
3617 // Each WINDOW_UPDATE frame is 13 bytes (9-byte header + 4-byte payload).
3618 let mut written_ids = Vec::new();
3619 for (&stream_id, &increment) in &self.flow_control.pending_window_updates {
3620 if increment == 0 {
3621 written_ids.push(stream_id);
3622 continue;
3623 }
3624 match serializer::gen_window_update(&mut buf[offset..], stream_id, increment) {
3625 Ok((_, size)) => {
3626 offset += size;
3627 written_ids.push(stream_id);
3628 incr!(names::h2::FRAMES_TX_WINDOW_UPDATE);
3629 }
3630 Err(_) => {
3631 // Buffer full — stop here, remaining entries stay in the map
3632 break;
3633 }
3634 }
3635 }
3636 // Remove only the entries we successfully wrote (or skipped)
3637 for id in written_ids {
3638 self.flow_control.pending_window_updates.remove(&id);
3639 }
3640 if offset > 0 {
3641 kawa.storage.fill(offset);
3642 if self.flush_zero_to_socket() {
3643 self.expect_write = Some(H2StreamId::Zero);
3644 // Edge-triggered epoll: ensure pending TLS data gets flushed
3645 if self.socket.socket_wants_write() {
3646 self.readiness.event.insert(Ready::WRITABLE);
3647 }
3648 return Some(MuxResult::Continue);
3649 }
3650 }
3651 }
3652
3653 // Stage — RST_STREAM cap check + drain.
3654 // Check the lifetime total (not just pending queue length) because
3655 // writable() drains the queue between readable() calls, so the
3656 // pending count alone may never reach the cap even under sustained
3657 // misbehavior.
3658 if !matches!(self.state, H2State::GoAway | H2State::Error)
3659 && self.total_rst_streams_queued >= MAX_PENDING_RST_STREAMS
3660 {
3661 error!(
3662 "{} total RST_STREAM count {} exceeds cap {}, sending GOAWAY(ENHANCE_YOUR_CALM)",
3663 log_context!(self),
3664 self.total_rst_streams_queued,
3665 MAX_PENDING_RST_STREAMS
3666 );
3667 return Some(self.goaway(H2Error::EnhanceYourCalm));
3668 }
3669
3670 // Flush pending RST_STREAM frames (queued when refusing streams).
3671 // Accounting happens at queue-time inside `Self::enqueue_rst`, so
3672 // this drain only serialises and flushes — no metric/flood calls
3673 // here would double-count.
3674 if !self.pending_rst_streams.is_empty() && self.expect_write.is_none() {
3675 let kawa = &mut self.zero;
3676 kawa.storage.clear();
3677 let buf = kawa.storage.space();
3678 let mut offset = 0;
3679 let mut written_count = 0;
3680 for &(stream_id, ref error) in &self.pending_rst_streams {
3681 let frame_size =
3682 parser::FRAME_HEADER_SIZE + parser::RST_STREAM_PAYLOAD_SIZE as usize;
3683 if offset + frame_size > buf.len() {
3684 break;
3685 }
3686 match serializer::gen_rst_stream(&mut buf[offset..], stream_id, error.to_owned()) {
3687 Ok((_, _)) => {
3688 offset += frame_size;
3689 written_count += 1;
3690 }
3691 Err(_) => break,
3692 }
3693 }
3694 self.pending_rst_streams.drain(..written_count);
3695 if offset > 0 {
3696 kawa.storage.fill(offset);
3697 if self.flush_zero_to_socket() {
3698 self.expect_write = Some(H2StreamId::Zero);
3699 // Edge-triggered epoll: ensure pending TLS data gets flushed
3700 if self.socket.socket_wants_write() {
3701 self.readiness.event.insert(Ready::WRITABLE);
3702 }
3703 return Some(MuxResult::Continue);
3704 }
3705 }
3706 }
3707
3708 None
3709 }
3710
3711 pub fn writable<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
3712 where
3713 E: Endpoint,
3714 L: ListenerHandler + L7ListenerHandler,
3715 {
3716 self.prune_inactive_streams_while_closing(context);
3717
3718 if let Some(result) = self.flush_pending_control_frames() {
3719 return result;
3720 }
3721
3722 // Flush any pending TLS records before state-specific processing.
3723 // This ensures response DATA frames that were accepted by rustls
3724 // (via socket_write_vectored in write_streams) are pushed to the
3725 // TCP socket even when the connection is in GoAway or Error state.
3726 // Without this, the state-specific handlers may call force_disconnect()
3727 // before the response data reaches the kernel's TCP send buffer.
3728 if self.socket.socket_wants_write() {
3729 self.socket.socket_write(&[]);
3730 }
3731
3732 match (&self.state, &self.position) {
3733 (H2State::Error, Position::Server) => {
3734 if self.socket.socket_wants_write() {
3735 self.ensure_tls_flushed();
3736 MuxResult::Continue
3737 } else {
3738 MuxResult::CloseSession
3739 }
3740 }
3741 (H2State::Error, _)
3742 | (H2State::ClientSettings, Position::Server)
3743 | (H2State::ServerSettings, Position::Client(..)) => {
3744 error!(
3745 "{} Unexpected combination: (Writable, {:?}, {:?})",
3746 log_context!(self),
3747 self.state,
3748 self.position
3749 );
3750 self.force_disconnect()
3751 }
3752 (H2State::ClientPreface, Position::Server) => MuxResult::Continue,
3753 // Discard state: pending data (e.g. RST_STREAM) was already
3754 // written in the preamble above; let the readable path consume
3755 // the remaining frame payload.
3756 (H2State::Discard, _) => MuxResult::Continue,
3757 (H2State::GoAway, _) => {
3758 if self.peer_gone_after_final_goaway() {
3759 return MuxResult::CloseSession;
3760 }
3761 // Flush any remaining TLS response data before disconnecting.
3762 // The GoAway state only enters after control frames (our GOAWAY
3763 // response) are flushed above, but response DATA frames may still
3764 // be in rustls's TLS output buffer — accepted by socket_write_vectored
3765 // during write_streams() but not yet flushed to TCP. Under TCP
3766 // backpressure (HAProxy chain), this is the primary truncation vector.
3767 if self.socket.socket_wants_write() {
3768 self.socket.socket_write(&[]);
3769 if self.socket.socket_wants_write() {
3770 // TLS data still pending (TCP backpressure) — don't disconnect
3771 // yet. Re-arm WRITABLE so the event loop retries the flush.
3772 self.ensure_tls_flushed();
3773 return MuxResult::Continue;
3774 }
3775 }
3776 self.force_disconnect()
3777 }
3778 (H2State::ClientPreface, Position::Client(..)) => {
3779 trace!("{} Preparing preface and settings", log_context!(self));
3780 let pri = serializer::H2_PRI.as_bytes();
3781 let kawa = &mut self.zero;
3782
3783 kawa.storage.space()[0..pri.len()].copy_from_slice(pri);
3784 kawa.storage.fill(pri.len());
3785 match serializer::gen_settings(kawa.storage.space(), &self.local_settings) {
3786 Ok((_, size)) => {
3787 kawa.storage.fill(size);
3788 incr!(names::h2::FRAMES_TX_SETTINGS);
3789 // RFC 9113 §6.5: start tracking SETTINGS ACK timeout
3790 self.settings_sent_at = Some(Instant::now());
3791 }
3792 Err(error) => {
3793 error!(
3794 "{} Could not serialize SettingsFrame: {:?}",
3795 log_context!(self),
3796 error
3797 );
3798 return self.force_disconnect();
3799 }
3800 };
3801
3802 self.state = H2State::ClientSettings;
3803 self.expect_write = Some(H2StreamId::Zero);
3804 MuxResult::Continue
3805 }
3806 (H2State::ClientSettings, Position::Client(..)) => {
3807 trace!("{} Sent preface and settings", log_context!(self));
3808 self.state = H2State::ServerSettings;
3809 self.expect_read = Some((H2StreamId::Zero, 9));
3810 self.readiness.interest.remove(Ready::WRITABLE);
3811 MuxResult::Continue
3812 }
3813 (H2State::ServerSettings, Position::Server) => {
3814 // Enlarge the connection-level receive window beyond the RFC default
3815 // of 65 535 bytes. The configured window size is too small for
3816 // high-throughput proxying and causes excessive WINDOW_UPDATE
3817 // round-trips. Use additive increment rather than unconditional
3818 // assignment to preserve any window changes that occurred during
3819 // setup. Skip if the configured window equals the default (no
3820 // enlargement needed), since a zero-increment WINDOW_UPDATE
3821 // violates RFC 9113 §6.9.
3822 let increment = self
3823 .connection_config
3824 .initial_connection_window
3825 .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
3826 if increment > 0 {
3827 self.queue_window_update(0, increment);
3828 }
3829 // Do NOT increment flow_control.window here: sending our own
3830 // WINDOW_UPDATE enlarges the peer's send allowance, not ours.
3831 // Our send window is only updated by WINDOW_UPDATEs we receive
3832 // from the peer (RFC 9113 §6.9).
3833 self.expect_header();
3834 // Keep WRITABLE so the queued WINDOW_UPDATE gets flushed.
3835 MuxResult::Continue
3836 }
3837 // Proxying states — writing application data (request/response).
3838 // Reset the timeout here, not at the top of writable(), so that
3839 // control frame writes (PING, WINDOW_UPDATE) don't reset it.
3840 (H2State::Header, _)
3841 | (H2State::Frame(_), _)
3842 | (H2State::ContinuationFrame(_), _)
3843 | (H2State::ContinuationHeader(_), _) => self.write_streams(context, endpoint),
3844 }
3845 }
3846
3847 /// Snapshot the access-log RTTs for the local frontend and the linked backend.
3848 ///
3849 /// `Position::Server`-only. On a backend H2 connection (`Position::Client`)
3850 /// the snapshot would write swapped values onto the shared `Stream.metrics`:
3851 /// the connection's `socket` is the upstream and the corresponding
3852 /// `EndpointServer::socket` returns the frontend, so the per-stream
3853 /// `client_rtt`/`server_rtt` cells would be populated with mislabelled
3854 /// values. Gating keeps backend H2 from poisoning the access-log metric
3855 /// for the matching frontend stream.
3856 ///
3857 /// Callers must invoke this BEFORE `endpoint.end_stream(...)` on reset
3858 /// paths so the backend lookup does not depend on
3859 /// `EndpointClient::end_stream` continuing to leave entries in
3860 /// `Router.backends`.
3861 ///
3862 /// Takes individual field references (not `&self`) for the same reason
3863 /// `try_recycle_server_stream` does — to avoid borrow conflicts with the
3864 /// `H2BlockConverter` that holds `&mut self.encoder` during the per-stream
3865 /// write loop.
3866 fn snapshot_rtts<E: Endpoint>(
3867 position: &Position,
3868 socket: &Front,
3869 endpoint: &E,
3870 linked_token: Option<mio::Token>,
3871 ) -> (Option<Duration>, Option<Duration>) {
3872 if !position.is_server() {
3873 return (None, None);
3874 }
3875 (
3876 socket_rtt(socket.socket_ref()),
3877 linked_token
3878 .and_then(|t| endpoint.socket(t))
3879 .and_then(socket_rtt),
3880 )
3881 }
3882
3883 /// Try to recycle a completed server-side stream by distributing overhead,
3884 /// generating access logs, and transitioning the stream to `Recycle` state.
3885 ///
3886 /// Returns `Some((stream_id, Option<token>))` if the stream was recycled, so the
3887 /// caller can add `stream_id` to the dead-streams list and call `endpoint.end_stream()`
3888 /// if a token was returned. Returns `None` if recycling was deferred or not applicable.
3889 ///
3890 /// Takes individual field references instead of `&mut self` to avoid borrow
3891 /// conflicts when the H2 block converter holds `&mut self.encoder`.
3892 /// `client_rtt`/`server_rtt` are snapshotted by the caller (which still
3893 /// owns `&self.socket` and `&endpoint`) and forwarded into the access log.
3894 #[allow(clippy::too_many_arguments)]
3895 fn try_recycle_server_stream<L>(
3896 position: &Position,
3897 bytes: &mut H2ByteAccounting,
3898 streams: &HashMap<StreamId, GlobalStreamId>,
3899 stream: &mut crate::protocol::mux::Stream,
3900 global_stream_id: GlobalStreamId,
3901 stream_id: StreamId,
3902 byte_totals: (usize, usize),
3903 debug: &mut DebugHistory,
3904 listener: std::rc::Rc<std::cell::RefCell<L>>,
3905 client_rtt: Option<Duration>,
3906 server_rtt: Option<Duration>,
3907 ) -> Option<(StreamId, Option<mio::Token>)>
3908 where
3909 L: ListenerHandler + L7ListenerHandler,
3910 {
3911 match position {
3912 Position::Client(..) => None,
3913 Position::Server => {
3914 // Already logged by a reset path; retire the stream after its RST is flushed.
3915 if stream.metrics.start.is_none() {
3916 let state = std::mem::replace(&mut stream.state, StreamState::Recycle);
3917 return match state {
3918 StreamState::Linked(token) => Some((stream_id, Some(token))),
3919 _ => Some((stream_id, None)),
3920 };
3921 }
3922
3923 // Don't recycle if the client hasn't sent END_STREAM yet —
3924 // more DATA frames may arrive for this stream.
3925 if !stream.front_received_end_of_stream {
3926 trace!(
3927 "{} Defer recycle stream {}: client still sending",
3928 log_module_context!(),
3929 global_stream_id
3930 );
3931 return None;
3932 }
3933 let stream_bytes = (
3934 stream.metrics.bin + stream.metrics.backend_bin,
3935 stream.metrics.bout + stream.metrics.backend_bout,
3936 );
3937 distribute_overhead(
3938 &mut stream.metrics,
3939 &mut bytes.overhead_bin,
3940 &mut bytes.overhead_bout,
3941 stream_bytes,
3942 byte_totals,
3943 streams.len(),
3944 streams.len() == 1,
3945 );
3946 debug.push(DebugEvent::StreamEvent(4, global_stream_id));
3947 trace!(
3948 "{} Recycle stream: {}",
3949 log_module_context!(),
3950 global_stream_id
3951 );
3952 let token = Self::complete_server_stream(stream, listener, client_rtt, server_rtt);
3953 Some((stream_id, token))
3954 }
3955 }
3956 }
3957
3958 /// Finalize a server-side stream after its response has been fully written.
3959 ///
3960 /// Generates an access log, resets metrics, and transitions the stream to `Recycle`.
3961 /// Returns the backend token if the stream was `Linked`, so the caller can call
3962 /// `endpoint.end_stream()` with the full `Context` (which can't be passed here
3963 /// because `stream` borrows from `context.streams`).
3964 ///
3965 /// Callers must distribute overhead *before* calling this, since the converter
3966 /// borrow may prevent `distribute_overhead()`.
3967 fn complete_server_stream<L>(
3968 stream: &mut crate::protocol::mux::Stream,
3969 listener: std::rc::Rc<std::cell::RefCell<L>>,
3970 client_rtt: Option<Duration>,
3971 server_rtt: Option<Duration>,
3972 ) -> Option<mio::Token>
3973 where
3974 L: ListenerHandler + L7ListenerHandler,
3975 {
3976 incr!(names::http::E2E_H2);
3977 stream.metrics.backend_stop();
3978 stream.generate_access_log(
3979 false,
3980 Some("H2::Complete"),
3981 listener,
3982 client_rtt,
3983 server_rtt,
3984 );
3985 stream.metrics.reset();
3986 let state = std::mem::replace(&mut stream.state, StreamState::Recycle);
3987 if let StreamState::Linked(token) = state {
3988 Some(token)
3989 } else {
3990 None
3991 }
3992 }
3993
3994 /// Compute the total bytes transferred across all active streams.
3995 ///
3996 /// Returns `(total_bytes_in, total_bytes_out)` where bytes_in = `bin + backend_bin`
3997 /// and bytes_out = `bout + backend_bout` for each stream.
3998 fn compute_stream_byte_totals<L: ListenerHandler + L7ListenerHandler>(
3999 &self,
4000 context: &Context<L>,
4001 ) -> (usize, usize) {
4002 let mut total_in = 0usize;
4003 let mut total_out = 0usize;
4004 for &gid in self.streams.values() {
4005 let m = &context.streams[gid].metrics;
4006 total_in += m.bin + m.backend_bin;
4007 total_out += m.bout + m.backend_bout;
4008 }
4009 (total_in, total_out)
4010 }
4011
4012 /// Distribute connection-level byte overhead proportionally to a single stream.
4013 ///
4014 /// `totals` should be pre-computed via [`compute_stream_byte_totals`] **before**
4015 /// taking a mutable borrow on the target stream, to avoid borrow conflicts.
4016 /// Delegates to the free function [`distribute_overhead`].
4017 fn distribute_overhead(&mut self, metrics: &mut SessionMetrics, totals: (usize, usize)) {
4018 let stream_bytes = (
4019 metrics.bin + metrics.backend_bin,
4020 metrics.bout + metrics.backend_bout,
4021 );
4022 distribute_overhead(
4023 metrics,
4024 &mut self.bytes.overhead_bin,
4025 &mut self.bytes.overhead_bout,
4026 stream_bytes,
4027 totals,
4028 self.streams.len(),
4029 self.streams.len() <= 1,
4030 );
4031 }
4032
4033 /// Attribute accumulated `zero_bytes_read` to the stream or to connection overhead.
4034 fn attribute_bytes_to_stream(&mut self, metrics: &mut SessionMetrics) {
4035 self.position
4036 .count_bytes_in(metrics, self.bytes.zero_bytes_read);
4037 self.bytes.zero_bytes_read = 0;
4038 }
4039
4040 fn attribute_bytes_to_overhead(&mut self) {
4041 self.bytes.overhead_bin += self.bytes.zero_bytes_read;
4042 self.bytes.zero_bytes_read = 0;
4043 }
4044
4045 /// Queue a WINDOW_UPDATE, coalescing with any existing entry for the same stream_id.
4046 /// RFC 9113 §6.9.1: window size increment MUST be 1..2^31-1 (0x7FFFFFFF).
4047 ///
4048 /// Always signals pending write so callers don't have to remember the
4049 /// edge-triggered epoll invariant (see memory feedback_epollet_signal_pending_write):
4050 /// under ET epoll a queued WINDOW_UPDATE without a live WRITABLE event bit
4051 /// is invisible to filter_interest() and will never get flushed.
4052 fn queue_window_update(&mut self, stream_id: u32, increment: u32) {
4053 let max_increment = i32::MAX as u32;
4054 if let Some(existing) = self.flow_control.pending_window_updates.get_mut(&stream_id) {
4055 let old = *existing;
4056 *existing = existing.saturating_add(increment).min(max_increment);
4057 // Coalescing invariant: the accumulated increment never decreases
4058 // and never exceeds i32::MAX (RFC 9113 §6.9 caps a WINDOW_UPDATE
4059 // increment at 2^31-1; emitting a larger value would be a protocol
4060 // error on the wire).
4061 debug_assert!(
4062 *existing >= old,
4063 "coalesced WINDOW_UPDATE increment must be monotonic non-decreasing"
4064 );
4065 debug_assert!(
4066 *existing <= max_increment,
4067 "coalesced WINDOW_UPDATE increment must stay within i32::MAX"
4068 );
4069 trace!(
4070 "{} WINDOW_UPDATE coalesced: stream={} old={} new={}",
4071 log_context!(self),
4072 stream_id,
4073 old,
4074 *existing
4075 );
4076 } else if self.flow_control.pending_window_updates.len() < self.max_pending_window_updates {
4077 self.flow_control
4078 .pending_window_updates
4079 .insert(stream_id, increment.min(max_increment));
4080 trace!(
4081 "{} WINDOW_UPDATE queued: stream={} increment={}",
4082 log_context!(self),
4083 stream_id,
4084 increment.min(max_increment)
4085 );
4086 } else {
4087 error!(
4088 "{} WINDOW_UPDATE dropped: queue full ({} entries), stream={} increment={}",
4089 log_context!(self),
4090 self.max_pending_window_updates,
4091 stream_id,
4092 increment
4093 );
4094 incr!(names::h2::WINDOW_UPDATE_DROPPED);
4095 }
4096 self.readiness.arm_writable();
4097 }
4098
4099 /// Re-enable READABLE if this connection is parked waiting for buffer space
4100 /// and the target stream's buffer now has enough room.
4101 ///
4102 /// This is the cross-readiness counterpart to the same-connection check in
4103 /// `writable()`. When the *other side* of a stream (frontend or backend)
4104 /// drains data via its own `writable()`, it frees buffer space that this
4105 /// connection was waiting for. Without this explicit wake-up the connection
4106 /// stays parked and the session deadlocks until a timeout fires.
4107 ///
4108 /// Returns `true` if READABLE was re-enabled.
4109 pub fn try_resume_reading<L>(&mut self, context: &Context<L>) -> bool
4110 where
4111 L: ListenerHandler + L7ListenerHandler,
4112 {
4113 if let Some((
4114 H2StreamId::Other {
4115 gid: global_stream_id,
4116 ..
4117 },
4118 amount,
4119 )) = self.expect_read
4120 {
4121 let stream = &context.streams[global_stream_id];
4122 let kawa = match self.position {
4123 Position::Client(..) => &stream.back,
4124 Position::Server => &stream.front,
4125 };
4126 if kawa.storage.available_space() >= amount {
4127 self.readiness.interest.insert(Ready::READABLE);
4128 return true;
4129 }
4130 }
4131 false
4132 }
4133
4134 /// Mark a stream's position-appropriate end-of-stream flag.
4135 ///
4136 /// Server reads from the front (client), so sets `front_received_end_of_stream`.
4137 /// Client reads from the back (backend), so sets `back_received_end_of_stream`.
4138 fn mark_end_of_stream(&self, stream: &mut crate::protocol::mux::Stream) {
4139 if self.position.is_server() {
4140 stream.front_received_end_of_stream = true;
4141 } else {
4142 stream.back_received_end_of_stream = true;
4143 }
4144 }
4145
4146 /// Cancel streams that have been idle longer than [`Self::stream_idle_timeout`].
4147 ///
4148 /// A stream is considered idle when no meaningful application data (non-empty
4149 /// DATA frames or HEADERS) has been received since the last activity timestamp
4150 /// in [`Self::stream_last_activity_at`].
4151 ///
4152 /// Mitigates slow-multiplex Slowloris (Pass 4 Medium #3): the connection-level
4153 /// idle timer resets on every frame, so a peer sending periodic control frames
4154 /// can pin `max_concurrent_streams` slots for the full nominal connection timeout.
4155 /// Per-stream idle deadlines guarantee each stream terminates if it stops making
4156 /// forward progress, regardless of connection-level liveness.
4157 ///
4158 /// Timed-out streams receive RST_STREAM(CANCEL) and are immediately removed
4159 /// from the streams map so they no longer count against MAX_CONCURRENT_STREAMS.
4160 /// Backend endpoints are notified and metrics are finalized.
4161 pub fn cancel_timed_out_streams<E, L>(&mut self, context: &mut Context<L>, endpoint: &mut E)
4162 where
4163 E: Endpoint,
4164 L: ListenerHandler + L7ListenerHandler,
4165 {
4166 // Per-connection scratch Vecs (`converter_buf`, `lowercase_buf`,
4167 // `cookie_buf`, `priorities_buf`) grow to a
4168 // high-water mark and never shrink. On a long-lived idle H2
4169 // connection that briefly carried a flurry of large headers, the
4170 // backing memory stays pinned indefinitely. Reclaim past
4171 // `SCRATCH_BUF_RETAIN` when the connection has live streams but
4172 // each scratch buffer holds 4× the cap. Quiet-time only — runs
4173 // at the top of every `cancel_timed_out_streams` invocation
4174 // (which is itself called from the readable hot loop, but only
4175 // on a session that has been idle long enough to risk timing
4176 // out a stream).
4177 const SCRATCH_BUF_RETAIN: usize = 16 * 1024;
4178 if self.converter_buf.capacity() > SCRATCH_BUF_RETAIN * 4 {
4179 self.converter_buf.shrink_to(SCRATCH_BUF_RETAIN);
4180 }
4181 if self.lowercase_buf.capacity() > SCRATCH_BUF_RETAIN * 4 {
4182 self.lowercase_buf.shrink_to(SCRATCH_BUF_RETAIN);
4183 }
4184 if self.cookie_buf.capacity() > SCRATCH_BUF_RETAIN * 4 {
4185 self.cookie_buf.shrink_to(SCRATCH_BUF_RETAIN);
4186 }
4187 if self.priorities_buf.capacity() > SCRATCH_BUF_RETAIN * 4 {
4188 self.priorities_buf.shrink_to(SCRATCH_BUF_RETAIN);
4189 }
4190
4191 if self.streams.is_empty()
4192 || (self.stream_last_activity_at.is_empty() && self.stream_fc_stalled_since.is_empty())
4193 {
4194 return;
4195 }
4196 let now = Instant::now();
4197 let deadline = self.stream_idle_timeout;
4198 // Two independent per-stream guards reap on the same deadline — see
4199 // `collect_timed_out_streams`. The flow-control-stall guard
4200 // (`stream_fc_stalled_since`) closes the HTTP/2 window-stall vector that
4201 // the bidirectional liveness guard (`stream_last_activity_at`) misses,
4202 // because an inbound DATA drip keeps the liveness timer warm while the
4203 // response stays window-blocked.
4204 let timed_out = collect_timed_out_streams(
4205 &self.stream_last_activity_at,
4206 &self.stream_fc_stalled_since,
4207 &self.streams,
4208 &self.rst_sent,
4209 now,
4210 deadline,
4211 );
4212 if timed_out.is_empty() {
4213 return;
4214 }
4215 for (sid, reason) in timed_out {
4216 info!(
4217 "{} H2 stream {} exceeded {:?} ({}), cancelling",
4218 log_context!(self),
4219 sid,
4220 deadline,
4221 reason
4222 );
4223 // M1: break reaps down by guard so a window-stall reap (a DoS
4224 // mitigation) is distinguishable from an ordinary idle reap on a
4225 // dashboard. M2: a window-stall reap whose stream dribbled some
4226 // outbound progress (`acc > 0`) below the floor is specifically a
4227 // stall-budget reap — the `WINDOW_UPDATE`-drip vector the budget
4228 // closes — counted as a subset. Read the accumulator BEFORE
4229 // `remove_dead_stream` evicts it below.
4230 match reason {
4231 "H2::WindowStall" => {
4232 count!(names::h2::STREAMS_REAPED_WINDOW_STALL, 1);
4233 if matches!(self.stream_fc_stalled_progress.get(&sid), Some(&acc) if acc > 0) {
4234 count!(names::h2::STREAMS_REAPED_STALL_BUDGET, 1);
4235 }
4236 }
4237 "H2::IdleTimeout" => count!(names::h2::STREAMS_REAPED_IDLE_TIMEOUT, 1),
4238 other => debug!("{} unexpected reap reason {}", log_context!(self), other),
4239 }
4240 // Route through the canonical chokepoint so dedupe (rst_sent),
4241 // queued-cap accounting (MAX_PENDING_RST_STREAMS via
4242 // total_rst_streams_queued), and edge-triggered-epoll arming
4243 // (Readiness::arm_writable) all stay consistent — see LIFECYCLE
4244 // §8.2. The previous direct push bypassed all three: a peer
4245 // that opens 200 streams and lets them all idle past
4246 // stream_idle_timeout could push past the queued cap silently
4247 // (no GOAWAY(ENHANCE_YOUR_CALM) escalation), a double-cancel
4248 // pass would grow pending_rst_streams instead of short-
4249 // circuiting on the existing rst_sent membership, and the
4250 // hand-rolled `interest.insert(WRITABLE) + signal_pending_write`
4251 // pair below skipped invariant 15. Counting these RSTs against
4252 // the cap is a deliberate behaviour change: 200 cumulative idle
4253 // cancellations from one peer IS abusive (pinning
4254 // MAX_CONCURRENT_STREAMS slots), and the GOAWAY(ENHANCE_YOUR_CALM)
4255 // escalation tells the peer to reconnect with a clean state.
4256 //
4257 // We deliberately ignore the `Option<MuxResult>` flood-violation
4258 // signal here — `cancel_timed_out_streams` returns `()` and is
4259 // called as best-effort housekeeping during the read path. A
4260 // flood violation that becomes visible mid-iteration will be
4261 // re-detected on the next `record_rst_emitted` call (the
4262 // counter is sticky), so dropping the early-return is safe.
4263 let _ = self.enqueue_rst(sid, H2Error::Cancel);
4264
4265 // Remove from streams map and recycle the context stream so the slot
4266 // no longer counts against MAX_CONCURRENT_STREAMS.
4267 // Compute totals per-stream before remove (matches RST_STREAM handler).
4268 let byte_totals = self.compute_stream_byte_totals(context);
4269 if let Some(global_stream_id) = self.streams.get(&sid).copied() {
4270 {
4271 let stream = &mut context.streams[global_stream_id];
4272 self.attribute_bytes_to_stream(&mut stream.metrics);
4273 }
4274 // Check if stream is linked to a backend — borrow must be scoped
4275 // so end_stream can take &mut context.
4276 let linked_token = context.streams[global_stream_id].linked_token();
4277 let (client_rtt, server_rtt) =
4278 Self::snapshot_rtts(&self.position, &self.socket, &*endpoint, linked_token);
4279 if let Some(token) = linked_token {
4280 endpoint.end_stream(token, global_stream_id, context);
4281 }
4282 let stream = &mut context.streams[global_stream_id];
4283 match &self.position {
4284 Position::Client(_, backend, BackendStatus::Connected) => {
4285 let mut backend_borrow = backend.borrow_mut();
4286 backend_borrow.active_requests =
4287 backend_borrow.active_requests.saturating_sub(1);
4288 }
4289 Position::Client(..) => {}
4290 Position::Server => {
4291 self.distribute_overhead(&mut stream.metrics, byte_totals);
4292 stream.metrics.backend_stop();
4293 stream.generate_access_log(
4294 true,
4295 Some(reason),
4296 context.listener.clone(),
4297 client_rtt,
4298 server_rtt,
4299 );
4300 stream.state = StreamState::Recycle;
4301 }
4302 }
4303 // Retire sid from streams/prioriser/stream_last_activity_at and
4304 // invalidate expect_write/expect_read if they reference this gid.
4305 self.remove_dead_stream(sid, global_stream_id);
4306 }
4307 }
4308 // Writable arming is already done by enqueue_rst -> arm_writable in
4309 // the loop above; the trailing pair was redundant after the chokepoint
4310 // routing landed.
4311 }
4312
4313 /// Queue a `RST_STREAM` frame for serialisation by
4314 /// [`Self::flush_pending_control_frames`] on the next writable tick.
4315 ///
4316 /// This is the canonical entry point for proxy-emitted stream resets:
4317 /// `DATA` on a closed stream, `MAX_CONCURRENT_STREAMS` refusal, and the
4318 /// per-stream error paths in [`Self::reset_stream`] all funnel through
4319 /// here. Serialisation is independent of the owning `Stream` still
4320 /// existing in `self.streams`, which is what lets us emit even after a
4321 /// caller has already called [`Self::remove_dead_stream`].
4322 ///
4323 /// Delegates the primitive work to [`enqueue_rst_into`] so the invariants
4324 /// are covered by unit tests that don't need a full `ConnectionH2`
4325 /// fixture. See that function's doc-comment for the three invariants
4326 /// (dedupe via `rst_sent`, MadeYouReset queued cap via
4327 /// `total_rst_streams_queued`, edge-triggered-epoll arm via
4328 /// [`Readiness::arm_writable`]).
4329 fn enqueue_rst(&mut self, wire_stream_id: StreamId, error: H2Error) -> Option<MuxResult> {
4330 let freshly_queued = enqueue_rst_into(
4331 &mut self.pending_rst_streams,
4332 &mut self.total_rst_streams_queued,
4333 &mut self.rst_sent,
4334 &mut self.readiness,
4335 wire_stream_id,
4336 error,
4337 );
4338 // Account ONLY when a new RST actually entered the queue.
4339 // Calling `enqueue_rst` for a stream that already has a queued
4340 // (or already-flushed) RST is the dedup short-circuit — counting
4341 // those would inflate `h2.frames.tx.rst_stream` /
4342 // `h2.rst_stream.sent.*` and trip the CVE-2025-8671 MadeYouReset
4343 // lifetime cap on frames that never reached the wire.
4344 //
4345 // Account at queue-time, not at drain-time. Doing it later in
4346 // `flush_pending_control_frames` would double-count any RST that
4347 // a re-entrant call (DATA on a closed stream we already RSTed)
4348 // tried to enqueue — and missing it at queue-time leaves
4349 // `cancel_timed_out_streams` / `refuse_stream_and_discard` /
4350 // DATA-on-closed-stream paths bypassing the lifetime cap
4351 // (security review LISA-001 on commit `da845c71`).
4352 if freshly_queued {
4353 self.account_emitted_rst(error)
4354 } else {
4355 None
4356 }
4357 }
4358
4359 /// Single accounting site for proxy-emitted RST_STREAM frames.
4360 /// Three things must happen for every emitted RST so flood-protection
4361 /// stays honest: the global tx counter, the per-error breakdown,
4362 /// and the MadeYouReset emitted-RST lifetime cap.
4363 ///
4364 /// Two distinct emission paths feed this helper:
4365 /// * Queued frames — [`Self::enqueue_rst`] (and therefore every
4366 /// callable that funnels through it: `reset_stream`,
4367 /// `refuse_stream_and_discard`, `cancel_timed_out_streams`,
4368 /// DATA-on-closed-stream) calls this once at queue-time. The
4369 /// drain in `flush_pending_control_frames` does NOT call it
4370 /// again — that would double-count.
4371 /// * Converter-emitted frames — the converter's `initialize`
4372 /// chokepoint (and the HPACK over-budget abort path) writes
4373 /// RST_STREAM frames straight into `kawa.out` from inside
4374 /// `kawa.prepare`. We collect those `H2Error` codes during the
4375 /// `write_streams` loop and call this helper for each one
4376 /// after `drop(converter)` (because the converter holds
4377 /// `&mut self.encoder`).
4378 ///
4379 /// Returning `Some(MuxResult)` means the caller MUST short-circuit
4380 /// with that result — the flood detector tripped its lifetime cap
4381 /// and converted to a connection-wide GOAWAY.
4382 fn account_emitted_rst(&mut self, error: H2Error) -> Option<MuxResult> {
4383 incr!(names::h2::FRAMES_TX_RST_STREAM);
4384 count!(metric_for_rst_stream_sent(error), 1);
4385 if !matches!(error, H2Error::NoError)
4386 && let Some(violation) = self.flood_detector.record_rst_emitted()
4387 {
4388 return Some(self.handle_flood_violation(violation));
4389 }
4390 None
4391 }
4392
4393 /// Refuse a newly-opened stream with RST_STREAM and discard its HEADERS payload.
4394 ///
4395 /// Used when MAX_CONCURRENT_STREAMS is exceeded or buffer pool is exhausted.
4396 /// Queues the RST_STREAM for the writable path (can't write to kawa.storage
4397 /// here because it is needed to discard the HEADERS payload).
4398 ///
4399 /// Also applies SETTINGS back-pressure per RFC 9113 §5.1.2: if refusals
4400 /// burst past [`BACKPRESSURE_REFUSAL_THRESHOLD`] within
4401 /// [`BACKPRESSURE_WINDOW_DURATION`], the advertised
4402 /// `SETTINGS_MAX_CONCURRENT_STREAMS` is halved via
4403 /// [`Self::apply_mcs_backpressure`].
4404 fn refuse_stream_and_discard(
4405 &mut self,
4406 stream_id: StreamId,
4407 error: H2Error,
4408 payload_len: u32,
4409 ) -> MuxResult {
4410 if let Some(result) = self.enqueue_rst(stream_id, error) {
4411 return result;
4412 }
4413 self.state = H2State::Discard;
4414 self.expect_read = Some((H2StreamId::Zero, payload_len as usize));
4415 self.record_refusal_for_backpressure();
4416 MuxResult::Continue
4417 }
4418
4419 /// RFC 9113 §5.1.2 SETTINGS back-pressure bookkeeping.
4420 ///
4421 /// Increments the refusal counter for the current back-pressure window
4422 /// and, when the burst threshold is crossed, halves the advertised
4423 /// `SETTINGS_MAX_CONCURRENT_STREAMS`. Further halving attempts in the
4424 /// same connection are suppressed by [`Self::mcs_backpressure_applied`]
4425 /// so sustained abuse does not collapse the cap to zero — callers can
4426 /// still promote the situation to `EnhanceYourCalm` via the flood
4427 /// detector.
4428 fn record_refusal_for_backpressure(&mut self) {
4429 if self.refuse_window_start.elapsed() >= BACKPRESSURE_WINDOW_DURATION {
4430 self.refuse_count_window = 0;
4431 self.refuse_window_start = Instant::now();
4432 }
4433 self.refuse_count_window = self.refuse_count_window.saturating_add(1);
4434 if !self.mcs_backpressure_applied
4435 && self.refuse_count_window >= BACKPRESSURE_REFUSAL_THRESHOLD
4436 {
4437 self.apply_mcs_backpressure();
4438 }
4439 }
4440
4441 /// Halve the advertised `SETTINGS_MAX_CONCURRENT_STREAMS` and mark the
4442 /// back-pressure state as applied. The new value takes effect locally
4443 /// immediately — subsequent stream-open checks in `handle_header_state`
4444 /// compare `self.streams.len()` against this reduced cap, so the peer
4445 /// starts receiving `REFUSED_STREAM` earlier. A full SETTINGS re-send on
4446 /// the wire is deferred until we have a mid-connection SETTINGS queue
4447 /// (the existing path in `handle_preface_state` only fires during the
4448 /// handshake); this is noted in the task log as a minimal first step.
4449 fn apply_mcs_backpressure(&mut self) {
4450 let previous = self.local_settings.settings_max_concurrent_streams;
4451 let reduced = (previous / 2).max(1);
4452 warn!(
4453 "{} H2 SETTINGS back-pressure: refusals={} in {}s — halving \
4454 SETTINGS_MAX_CONCURRENT_STREAMS {} -> {}",
4455 log_context!(self),
4456 self.refuse_count_window,
4457 BACKPRESSURE_WINDOW_DURATION.as_secs(),
4458 previous,
4459 reduced,
4460 );
4461 self.local_settings.settings_max_concurrent_streams = reduced;
4462 self.mcs_backpressure_applied = true;
4463 }
4464
4465 /// Log a flood violation with full session context and emit the GOAWAY.
4466 ///
4467 /// Centralises the "flood detected" reporting so every site that observes a
4468 /// [`H2FloodViolation`] gets the same session-scoped log line, matching the
4469 /// RUSTLS log-context convention. Also emits the per-kind statsd counter
4470 /// (`h2.flood.violation.<kind>`) so SOC dashboards can window the trip
4471 /// rate without parsing logs — every CVE-mitigation in the H2 family
4472 /// (Rapid Reset, MadeYouReset, CONTINUATION/PING/SETTINGS floods, header
4473 /// overflow, glitch) funnels through this site.
4474 pub fn handle_flood_violation(&mut self, violation: H2FloodViolation) -> MuxResult {
4475 count!(violation.metric_key, 1);
4476 warn!(
4477 "{} H2 flood detected: {} count {} exceeds threshold {}",
4478 log_context!(self),
4479 violation.reason,
4480 violation.count,
4481 violation.threshold,
4482 );
4483 self.goaway(violation.error)
4484 }
4485}
4486
4487/// Recover the [`H2Error`] code that the converter's `initialize`
4488/// chokepoint will encode into the synthesised RST_STREAM frame for a
4489/// kawa stuck in [`kawa::ParsingPhase::Error`]. Mirrors the parse +
4490/// fallback at `lib/src/protocol/mux/converter.rs::initialize` so the
4491/// flood-accounting helper sees the same code that lands on the wire.
4492fn rst_error_from_kawa<T: kawa::AsBuffer>(kawa: &kawa::Kawa<T>) -> H2Error {
4493 match kawa.parsing_phase {
4494 kawa::ParsingPhase::Error {
4495 kind: kawa::ParsingErrorKind::Processing { message },
4496 ..
4497 } => message.parse::<H2Error>().unwrap_or(H2Error::InternalError),
4498 _ => H2Error::InternalError,
4499 }
4500}
4501
4502/// Compile-time mapping from `(prefix, H2Error)` to a static metric key.
4503///
4504/// Materialises a `&'static str` literal via `concat!`, so the metric key
4505/// never crosses through a heap allocation and the statsd drain can store it
4506/// as `&'static str`. Adding a new `H2Error` variant fails the build here —
4507/// the metric breakdown stays in lock-step with RFC 9113 §7 codes.
4508///
4509/// Used for the per-error-code counters emitted around GOAWAY and RST_STREAM
4510/// in either direction (see `metric_for_goaway_sent` etc. below).
4511macro_rules! h2_error_metric_key {
4512 ($prefix:literal, $error:expr) => {
4513 match $error {
4514 H2Error::NoError => concat!($prefix, ".no_error"),
4515 H2Error::ProtocolError => concat!($prefix, ".protocol_error"),
4516 H2Error::InternalError => concat!($prefix, ".internal_error"),
4517 H2Error::FlowControlError => concat!($prefix, ".flow_control_error"),
4518 H2Error::SettingsTimeout => concat!($prefix, ".settings_timeout"),
4519 H2Error::StreamClosed => concat!($prefix, ".stream_closed"),
4520 H2Error::FrameSizeError => concat!($prefix, ".frame_size_error"),
4521 H2Error::RefusedStream => concat!($prefix, ".refused_stream"),
4522 H2Error::Cancel => concat!($prefix, ".cancel"),
4523 H2Error::CompressionError => concat!($prefix, ".compression_error"),
4524 H2Error::ConnectError => concat!($prefix, ".connect_error"),
4525 H2Error::EnhanceYourCalm => concat!($prefix, ".enhance_your_calm"),
4526 H2Error::InadequateSecurity => concat!($prefix, ".inadequate_security"),
4527 H2Error::HTTP11Required => concat!($prefix, ".http_1_1_required"),
4528 }
4529 };
4530}
4531
4532/// Static metric key for an outbound GOAWAY. Same call shape as the other three
4533/// helpers below — keeps the call sites uniform.
4534fn metric_for_goaway_sent(error: H2Error) -> &'static str {
4535 h2_error_metric_key!("h2.goaway.sent", error)
4536}
4537
4538/// Static metric key for an inbound GOAWAY by raw wire error code. Codes
4539/// outside RFC 9113 §7 fall into the dedicated `…unknown_error` bucket so the
4540/// breakdown stays bounded and operators can still spot non-standard peers.
4541fn metric_for_goaway_received(error_code: u32) -> &'static str {
4542 H2Error::try_from(error_code)
4543 .map(|e| h2_error_metric_key!("h2.goaway.received", e))
4544 .unwrap_or("h2.goaway.received.unknown_error")
4545}
4546
4547/// Static metric key for an outbound RST_STREAM. Mirrors
4548/// [`metric_for_goaway_sent`] under a separate namespace so RST and GOAWAY
4549/// rates can be alerted on independently.
4550fn metric_for_rst_stream_sent(error: H2Error) -> &'static str {
4551 h2_error_metric_key!("h2.rst_stream.sent", error)
4552}
4553
4554/// Static metric key for an inbound RST_STREAM by raw wire error code. Same
4555/// `…unknown_error` fallback as [`metric_for_goaway_received`].
4556fn metric_for_rst_stream_received(error_code: u32) -> &'static str {
4557 H2Error::try_from(error_code)
4558 .map(|e| h2_error_metric_key!("h2.rst_stream.received", e))
4559 .unwrap_or("h2.rst_stream.received.unknown_error")
4560}
4561
4562/// Static metric key for an inbound H2 frame by RFC 9113 §6 frame type.
4563/// Emitted at the `handle_frame` dispatch — single chokepoint that any
4564/// new H2 frame type must traverse, so adding a `Frame::*` variant fails
4565/// the build here. Counts are per-frame, not per-byte; pair with
4566/// `bytes_in` for traffic-mix dashboards.
4567fn h2_frame_rx_metric_key(frame: &Frame) -> &'static str {
4568 match frame {
4569 Frame::Data(_) => "h2.frames.rx.data",
4570 Frame::Headers(_) => "h2.frames.rx.headers",
4571 Frame::PushPromise(_) => "h2.frames.rx.push_promise",
4572 Frame::Priority(_) => "h2.frames.rx.priority",
4573 Frame::RstStream(_) => "h2.frames.rx.rst_stream",
4574 Frame::Settings(_) => "h2.frames.rx.settings",
4575 Frame::Ping(_) => "h2.frames.rx.ping",
4576 Frame::GoAway(_) => "h2.frames.rx.goaway",
4577 Frame::WindowUpdate(_) => "h2.frames.rx.window_update",
4578 Frame::Continuation(_) => "h2.frames.rx.continuation",
4579 Frame::PriorityUpdate(_) => "h2.frames.rx.priority_update",
4580 Frame::Unknown(_) => "h2.frames.rx.unknown",
4581 }
4582}
4583
4584impl<Front: SocketHandler> ConnectionH2<Front> {
4585 pub fn goaway(&mut self, error: H2Error) -> MuxResult {
4586 self.state = H2State::Error;
4587 self.drain.draining = true;
4588 self.expect_read = None;
4589 // Disarm the SETTINGS ACK timer: once we've committed to GOAWAY, the
4590 // timeout check at `readable()` / `flush_pending_control_frames()` must
4591 // not re-fire. Without this, `signal_pending_write()` below re-enters
4592 // `writable()` → `flush_pending_control_frames()` on the next tick,
4593 // the elapsed check is still true, and we emit another
4594 // `warn!` + `goaway()` pair, each bumping `h2.goaway.sent.*`.
4595 self.settings_sent_at = None;
4596 let kawa = &mut self.zero;
4597 kawa.storage.clear();
4598 // Severity tiering: only `InternalError` implies a sozu-side bug when
4599 // WE emit it. Every other non-`NoError` reason is "peer misbehaved,
4600 // sozu defended correctly" — operators don't need paging on abusive
4601 // or buggy peers. Caller sites already log the specific antecedent
4602 // (flood detected, parser failure, SETTINGS timeout, invalid window)
4603 // before reaching `goaway()`, so demoting this summary line avoids
4604 // duplicate noise without hiding the root cause.
4605 match error {
4606 H2Error::NoError => debug!("{} GOAWAY: {:?}", log_context!(self), error),
4607 H2Error::InternalError => error!("{} GOAWAY: {:?}", log_context!(self), error),
4608 _ => warn!("{} GOAWAY: {:?}", log_context!(self), error),
4609 }
4610 count!(metric_for_goaway_sent(error), 1);
4611
4612 // RFC 9113 §6.8: last_stream_id is the highest peer-initiated stream we processed
4613 match serializer::gen_goaway(kawa.storage.space(), self.highest_peer_stream_id, error) {
4614 Ok((_, size)) => {
4615 kawa.storage.fill(size);
4616 incr!(names::h2::FRAMES_TX_GOAWAY);
4617 self.state = H2State::GoAway;
4618 self.expect_write = Some(H2StreamId::Zero);
4619 self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
4620 self.readiness.signal_pending_write();
4621 MuxResult::Continue
4622 }
4623 Err(error) => {
4624 error!(
4625 "{} Could not serialize GoAwayFrame: {:?}",
4626 log_context!(self),
4627 error
4628 );
4629 self.force_disconnect()
4630 }
4631 }
4632 }
4633
4634 /// RFC 9113 §6.8: Initiate graceful shutdown using the double-GOAWAY pattern.
4635 ///
4636 /// First call sends GOAWAY with `last_stream_id = 0x7FFFFFFF` (MAX) to signal
4637 /// the intent to stop accepting new streams while allowing in-flight streams
4638 /// to complete. The connection enters draining mode.
4639 ///
4640 /// When `draining` is already true (second invocation), sends the final GOAWAY
4641 /// with the actual `highest_peer_stream_id` so the peer knows which streams
4642 /// were processed.
4643 pub fn graceful_goaway(&mut self) -> MuxResult {
4644 if self.drain.draining {
4645 // Second GOAWAY: send with the real last_stream_id
4646 return self.goaway(H2Error::NoError);
4647 }
4648
4649 // First GOAWAY: advertise MAX stream ID so the peer knows we are draining
4650 // but does not yet know the cutoff. This gives in-flight requests a chance
4651 // to arrive before we commit to a final last_stream_id.
4652 self.drain.draining = true;
4653 // Arm the forced-close timer from the moment the proxy decides to drain.
4654 // `Mux::shutting_down` samples it against `graceful_shutdown_deadline`
4655 // and returns `true` once the budget is exhausted so the session loop
4656 // tears the connection down instead of waiting forever.
4657 self.drain.started_at = Some(Instant::now());
4658 // Keep expect_read as-is: existing streams should continue reading
4659 // data during the drain window opened by the initial GOAWAY. Only
4660 // the final GOAWAY (via `goaway()`) removes READABLE.
4661 let kawa = &mut self.zero;
4662 kawa.storage.clear();
4663 debug!(
4664 "{} GOAWAY (graceful, initial): last_stream_id=0x7FFFFFFF",
4665 log_context!(self)
4666 );
4667 // The initial GOAWAY sends NO_ERROR on the wire — count it under
4668 // the same per-code key as the final GOAWAY. The downstream alert
4669 // that wants to distinguish drain from termination compares
4670 // against the `h2.goaway.sent.no_error` rate (drain) vs the other
4671 // variants (termination on error).
4672 count!(metric_for_goaway_sent(H2Error::NoError), 1);
4673
4674 match serializer::gen_goaway(kawa.storage.space(), STREAM_ID_MAX, H2Error::NoError) {
4675 Ok((_, size)) => {
4676 kawa.storage.fill(size);
4677 incr!(names::h2::FRAMES_TX_GOAWAY);
4678 // Stay in the current state so the connection can continue processing
4679 // existing streams. The final GOAWAY will transition to GoAway state.
4680 // Keep READABLE so in-flight request bodies can still be received
4681 // during the drain window. Only remove READABLE in the final GOAWAY
4682 // (via `goaway()`).
4683 self.expect_write = Some(H2StreamId::Zero);
4684 self.readiness.arm_writable();
4685 MuxResult::Continue
4686 }
4687 Err(error) => {
4688 error!(
4689 "{} Could not serialize graceful GoAwayFrame: {:?}",
4690 log_context!(self),
4691 error
4692 );
4693 self.force_disconnect()
4694 }
4695 }
4696 }
4697
4698 /// Returns `true` when the graceful-shutdown budget armed by
4699 /// [`Self::graceful_goaway`] has elapsed. A return of `true` signals
4700 /// the enclosing session loop that the proxy-initiated drain must
4701 /// transition to a forced close: remaining streams will not complete
4702 /// in time and keeping the connection open past the deadline defeats
4703 /// the soft-stop SLA.
4704 ///
4705 /// Returns `false` when:
4706 /// - drain has not started yet (`started_at` is `None`),
4707 /// - the knob is `0` / `None` (indefinite wait explicitly opted in),
4708 /// - or the elapsed time is still within the configured budget.
4709 pub fn graceful_shutdown_deadline_elapsed(&self) -> bool {
4710 match (self.drain.started_at, self.drain.graceful_shutdown_deadline) {
4711 (Some(started_at), Some(deadline)) => started_at.elapsed() >= deadline,
4712 _ => false,
4713 }
4714 }
4715
4716 /// Returns `true` if there is data queued waiting to be flushed:
4717 /// - H2 control frames in the zero buffer (GOAWAY, SETTINGS ACK, etc.)
4718 /// - A partially-written stream or control frame (`expect_write`)
4719 /// - Encrypted TLS records in rustls's output buffer not yet flushed to TCP
4720 ///
4721 /// The TLS check is critical: `shutting_down()` uses this to prevent
4722 /// premature session close while response DATA is still in rustls's
4723 /// buffer (accepted by `socket_write_vectored` but not yet on the wire).
4724 ///
4725 /// Does NOT check per-stream `back.out`/`back.blocks`; use
4726 /// [`Self::has_pending_write_full`] on paths that must honour
4727 /// LIFECYCLE invariant 16 (e.g. shutdown-drain).
4728 pub fn has_pending_write(&self) -> bool {
4729 if self.peer_gone_after_final_goaway() {
4730 return false;
4731 }
4732 self.expect_write.is_some()
4733 || !self.zero.storage.is_empty()
4734 || self.socket.socket_wants_write()
4735 }
4736
4737 /// True when the reaper has queued control frames (`RST_STREAM`) into
4738 /// `pending_rst_streams` that have not yet been serialized. Kept SEPARATE
4739 /// from [`Self::has_pending_write`] because that probe gates connection close
4740 /// (the `mod.rs` close-gating sites) and must NOT treat a queued RST as a
4741 /// reason to keep the connection open; this probe is consulted ONLY by the
4742 /// `MuxState::timeout` flush gate to push a silent-peer `RST_STREAM(CANCEL)`
4743 /// onto the wire before the connection closes.
4744 pub fn has_pending_control_write(&self) -> bool {
4745 !self.pending_rst_streams.is_empty()
4746 }
4747
4748 /// Connection-level [`Self::has_pending_write`] extended with a per-stream
4749 /// back-buffer probe (LIFECYCLE §9 invariant 16). Used by shutdown-drain
4750 /// paths that must not close while any open stream still has outbound
4751 /// kawa bytes queued — a voluntary scheduler yield can leave `back.out`
4752 /// or `back.blocks` non-empty without `expect_write` being set.
4753 pub fn has_pending_write_full<L>(&self, context: &Context<L>) -> bool
4754 where
4755 L: ListenerHandler + L7ListenerHandler,
4756 {
4757 self.has_pending_write() || any_stream_has_pending_back(&self.streams, &context.streams)
4758 }
4759
4760 /// Flush the zero buffer to the socket, counting bytes as connection overhead.
4761 ///
4762 /// Returns `true` if the socket stalled (WouldBlock / zero-length write),
4763 /// meaning the caller should stop writing and wait for the next writable event.
4764 /// Returns `false` when the buffer has been fully drained.
4765 fn flush_zero_to_socket(&mut self) -> bool {
4766 while !self.zero.storage.is_empty() {
4767 let (size, status) = self.socket.socket_write(self.zero.storage.data());
4768 #[cfg(debug_assertions)]
4769 trace!(
4770 "{} flush_zero_to_socket: written={}, status={:?}, wants_write={}",
4771 log_context!(self),
4772 size,
4773 status,
4774 self.socket.socket_wants_write()
4775 );
4776 self.zero.storage.consume(size);
4777 self.position.count_bytes_out_counter(size);
4778 self.bytes.overhead_bout += size;
4779 if update_readiness_after_write(size, status, &mut self.readiness) {
4780 return true;
4781 }
4782 }
4783 // Reset buffer positions after draining. consume() advances start but
4784 // never resets it, so without clear() the next fill would panic.
4785 self.zero.storage.clear();
4786 false
4787 }
4788
4789 /// Directly flush the zero buffer to the socket without going through
4790 /// the full writable() path. Used during shutdown when the event loop
4791 /// won't deliver new epoll events for this session (edge-triggered).
4792 pub fn flush_zero_buffer(&mut self) {
4793 if self.flush_zero_to_socket() {
4794 return;
4795 }
4796 self.expect_write = None;
4797 if self.socket.socket_wants_write() {
4798 let (_size, status) = self.socket.socket_write(&[]);
4799 let _ = update_readiness_after_write(0, status, &mut self.readiness);
4800 }
4801 }
4802
4803 pub fn create_stream<L>(
4804 &mut self,
4805 stream_id: StreamId,
4806 context: &mut Context<L>,
4807 ) -> Option<GlobalStreamId>
4808 where
4809 L: ListenerHandler + L7ListenerHandler,
4810 {
4811 // RFC 9113 §6.8: reject new streams on a draining connection
4812 if self.drain.draining {
4813 error!(
4814 "{} Rejecting new stream {} on draining connection",
4815 log_context!(self),
4816 stream_id
4817 );
4818 return None;
4819 }
4820 let highest_before = self.highest_peer_stream_id;
4821 let streams_before = self.streams.len();
4822 // Track the highest peer-initiated stream ID for GoAway frames
4823 // before any early return, so GoAway always reports the correct last stream.
4824 if stream_id > self.highest_peer_stream_id {
4825 self.highest_peer_stream_id = stream_id;
4826 }
4827 // highest_peer_stream_id is monotonic non-decreasing — it only ever
4828 // climbs to the largest id we have accepted (RFC 9113 §6.8 last-stream
4829 // reporting depends on this).
4830 debug_assert!(
4831 self.highest_peer_stream_id >= highest_before,
4832 "highest_peer_stream_id must never regress"
4833 );
4834 let global_stream_id = context.create_stream(
4835 Ulid::generate(),
4836 self.peer_settings.settings_initial_window_size,
4837 )?;
4838 self.last_stream_id = (stream_id + 2) & !1;
4839 self.streams.insert(stream_id, global_stream_id);
4840 self.stream_last_activity_at
4841 .insert(stream_id, Instant::now());
4842 // Post-conditions: the stream is now reachable in both indices, the
4843 // active count grew by exactly one (the id was not already present —
4844 // `handle_header_state` rejects re-used ids), and `last_stream_id` is
4845 // the even watermark just past this id so `new_stream_id` never collides.
4846 debug_assert_eq!(
4847 self.streams.get(&stream_id).copied(),
4848 Some(global_stream_id),
4849 "create_stream must register the wire->global mapping"
4850 );
4851 debug_assert!(
4852 self.stream_last_activity_at.contains_key(&stream_id),
4853 "create_stream must arm the per-stream idle timer"
4854 );
4855 debug_assert_eq!(
4856 self.streams.len(),
4857 streams_before + 1,
4858 "create_stream must add exactly one stream (id must not pre-exist)"
4859 );
4860 debug_assert!(
4861 self.last_stream_id > stream_id && self.last_stream_id & 1 == 0,
4862 "last_stream_id watermark must be the even value strictly above stream_id"
4863 );
4864 Some(global_stream_id)
4865 }
4866
4867 pub fn new_stream_id(&mut self) -> Option<StreamId> {
4868 let watermark_before = self.last_stream_id;
4869 let (issued, next) = next_stream_id(self.last_stream_id, self.position.is_client())?;
4870 self.last_stream_id = next;
4871 // Post-conditions: the locally-issued id has the parity of our role and
4872 // the watermark advanced strictly (so the next allocation cannot reuse
4873 // this id). `next_stream_id` already asserts parity vs `is_client`; here
4874 // we re-assert against `self.position` and the watermark monotonicity.
4875 debug_assert_eq!(
4876 issued & 1 == 1,
4877 self.position.is_client(),
4878 "locally-issued stream id parity must match our role"
4879 );
4880 debug_assert!(
4881 self.last_stream_id > watermark_before,
4882 "issuing a stream id must advance the watermark"
4883 );
4884 Some(issued)
4885 }
4886
4887 /// Test-only setter: jump `last_stream_id` close to [`STREAM_ID_MAX`] so
4888 /// that the next call to [`Self::new_stream_id`] exhausts the 31-bit
4889 /// space. FIX-22 ("Stream-ID exhaustion disconnects backend gracefully")
4890 /// exercises the `None`-return branch — reaching it through normal API
4891 /// usage would require issuing ~2³¹ requests, which is not tractable in
4892 /// an E2E harness.
4893 #[cfg(any(test, feature = "e2e-hooks"))]
4894 pub fn __test_set_last_stream_id(&mut self, id: StreamId) {
4895 self.last_stream_id = id;
4896 }
4897
4898 /// Cross-field invariant sweep for the H2 connection state machine,
4899 /// asserted as a run-to-completion post-condition at the end of every
4900 /// frame-handling pass (see the call in [`Self::handle_frame`]).
4901 ///
4902 /// These are relationships between *separate* fields that no single setter
4903 /// can guarantee on its own — exactly the class of bug TigerStyle's
4904 /// `check_invariants` targets. Each one is cheap (counter compares + a few
4905 /// `HashMap` membership probes); the whole function is `#[cfg(debug_assertions)]`
4906 /// and compiles out of release entirely.
4907 ///
4908 /// Encoded invariants:
4909 /// 1. **Stream-id watermark parity**: locally-issued ids never exceed
4910 /// `STREAM_ID_MAX`; `last_stream_id` stays the even watermark (it is
4911 /// rounded to `(id + 2) & !1` and initialised to 0).
4912 /// 2. **Per-stream caches are subsets of the live stream set**:
4913 /// `stream_last_activity_at` is keyed only by currently-tracked stream
4914 /// ids — a leak here would let a removed stream keep an idle timer and
4915 /// mis-fire `cancel_timed_out_streams`. (`rst_sent` is intentionally NOT
4916 /// a subset: a queued RST for an already-removed stream is legal.)
4917 /// 3. **RST queue accounting**: the never-decaying `total_rst_streams_queued`
4918 /// lifetime counter is always `>=` the currently-pending queue length
4919 /// (CVE-2025-8671 MadeYouReset cap relies on the lifetime counter never
4920 /// under-counting), and the pending queue stays within its hard cap +1
4921 /// (the escalation tripwire fires at the cap).
4922 /// 4. **Pending WINDOW_UPDATE bound**: the coalescing map never exceeds the
4923 /// per-connection cap derived from `max_concurrent_streams`.
4924 /// 5. **Drain/state coupling**: a terminal `GoAway`/`Error` state implies the
4925 /// connection is draining (`goaway()` sets both); the converse need not
4926 /// hold (graceful drain stays in a live state).
4927 #[cfg(debug_assertions)]
4928 fn check_invariants<L>(&self, context: &Context<L>)
4929 where
4930 L: ListenerHandler + L7ListenerHandler,
4931 {
4932 // (1) Watermark parity and bound.
4933 debug_assert!(
4934 self.last_stream_id & 1 == 0,
4935 "last_stream_id must stay an even watermark, got {}",
4936 self.last_stream_id
4937 );
4938
4939 // (2) Per-stream caches are subsets of the live stream set, and every
4940 // mapping points at a valid context slot.
4941 debug_assert!(
4942 self.stream_last_activity_at
4943 .keys()
4944 .all(|id| self.streams.contains_key(id)),
4945 "stream_last_activity_at must only track currently-open stream ids"
4946 );
4947 debug_assert!(
4948 self.streams
4949 .values()
4950 .all(|&gid| gid < context.streams.len()),
4951 "every stream mapping must point at a valid context slot"
4952 );
4953
4954 // (3) RST queue accounting.
4955 debug_assert!(
4956 self.total_rst_streams_queued >= self.pending_rst_streams.len(),
4957 "queued-RST lifetime counter ({}) must be >= currently-pending queue ({})",
4958 self.total_rst_streams_queued,
4959 self.pending_rst_streams.len()
4960 );
4961 debug_assert!(
4962 self.pending_rst_streams.len() <= MAX_PENDING_RST_STREAMS + 1,
4963 "pending RST queue must stay within its hard cap (escalates at the cap)"
4964 );
4965
4966 // (4) Pending WINDOW_UPDATE coalescing map bound.
4967 debug_assert!(
4968 self.flow_control.pending_window_updates.len() <= self.max_pending_window_updates,
4969 "pending WINDOW_UPDATE map must stay within its per-connection cap"
4970 );
4971
4972 // (5) Drain/state coupling: terminal states imply draining.
4973 debug_assert!(
4974 !matches!(self.state, H2State::GoAway | H2State::Error) || self.drain.draining,
4975 "GoAway/Error state must imply the connection is draining"
4976 );
4977 }
4978
4979 fn handle_frame<E, L>(
4980 &mut self,
4981 frame: Frame,
4982 wire_payload_len: u32,
4983 context: &mut Context<L>,
4984 endpoint: E,
4985 ) -> MuxResult
4986 where
4987 E: Endpoint,
4988 L: ListenerHandler + L7ListenerHandler,
4989 {
4990 trace!("{} {:#?}", log_context!(self), frame);
4991 // Per-frame-type RX counter. Single chokepoint covers every H2 frame
4992 // type — adding a new `Frame::*` variant fails the build inside the
4993 // helper, keeping the metric breakdown in lock-step with RFC 9113 §6.
4994 count!(h2_frame_rx_metric_key(&frame), 1);
4995 let result = match frame {
4996 Frame::Data(data) => self.handle_data_frame(data, wire_payload_len, context, endpoint),
4997 Frame::Headers(headers) => self.handle_headers_frame(headers, context, endpoint),
4998 Frame::PushPromise(_) => self.handle_push_promise_frame(),
4999 Frame::Priority(priority) => self.handle_priority_frame(priority, context, endpoint),
5000 Frame::RstStream(rst_stream) => {
5001 self.handle_rst_stream_frame(rst_stream, context, endpoint)
5002 }
5003 Frame::Settings(settings) => self.handle_settings_frame(settings, context),
5004 Frame::Ping(ping) => self.handle_ping_frame(ping),
5005 Frame::GoAway(goaway) => self.handle_goaway_frame(goaway, context, endpoint),
5006 Frame::WindowUpdate(wu) => self.handle_window_update_frame(wu, context, endpoint),
5007 Frame::PriorityUpdate(pu) => self.handle_priority_update_frame(pu),
5008 Frame::Continuation(_) => {
5009 // Unreachable: standalone CONTINUATION is rejected in
5010 // `handle_header_state` (RFC 9113 §6.10) and in-block
5011 // CONTINUATION is consumed by the inline header-parsing
5012 // path. Keep a defensive fallback that returns
5013 // PROTOCOL_ERROR rather than panicking in debug builds.
5014 self.attribute_bytes_to_overhead();
5015 warn!(
5016 "{} CONTINUATION frames are handled inline during header parsing",
5017 log_context!(self)
5018 );
5019 self.goaway(H2Error::ProtocolError)
5020 }
5021 // RFC 9113 §5.5: unknown frame types MUST be ignored and discarded.
5022 // The parser already consumed the payload; attribute the bytes
5023 // to connection-level overhead and continue.
5024 Frame::Unknown(raw) => {
5025 debug!(
5026 "{} Ignoring unknown H2 frame type {}",
5027 log_context!(self),
5028 raw
5029 );
5030 self.attribute_bytes_to_overhead();
5031 MuxResult::Continue
5032 }
5033 };
5034 // Run-to-completion post-condition: the connection-level cross-field
5035 // invariants must hold after every frame is dispatched, on success and
5036 // on the protocol-error paths alike.
5037 #[cfg(debug_assertions)]
5038 self.check_invariants(context);
5039 result
5040 }
5041
5042 /// RFC 9110 §8.6: Content-Length validation must be skipped for responses
5043 /// where the body is absent by definition:
5044 /// - Responses to HEAD requests (any status)
5045 /// - 1xx informational responses
5046 /// - 204 No Content
5047 /// - 304 Not Modified
5048 fn content_length_exempt(
5049 &self,
5050 context: &crate::protocol::kawa_h1::editor::HttpContext,
5051 ) -> bool {
5052 use crate::protocol::kawa_h1::parser::Method;
5053 // HEAD method responses (only relevant when reading backend responses)
5054 if self.position.is_client() && context.method == Some(Method::Head) {
5055 return true;
5056 }
5057 // 1xx, 204, 304 status codes
5058 if let Some(status) = context.status
5059 && ((100..200).contains(&status) || status == 204 || status == 304)
5060 {
5061 return true;
5062 }
5063 false
5064 }
5065
5066 fn handle_data_frame<E, L>(
5067 &mut self,
5068 data: parser::Data,
5069 wire_payload_len: u32,
5070 context: &mut Context<L>,
5071 mut endpoint: E,
5072 ) -> MuxResult
5073 where
5074 E: Endpoint,
5075 L: ListenerHandler + L7ListenerHandler,
5076 {
5077 // CVE-2019-9518: track empty DATA frames (no payload, no END_STREAM)
5078 if data.payload.is_empty() && !data.end_stream {
5079 let empty_before = self.flood_detector.empty_data_count;
5080 self.flood_detector.empty_data_count += 1;
5081 debug_assert_eq!(
5082 self.flood_detector.empty_data_count,
5083 empty_before + 1,
5084 "empty-DATA flood counter must advance by exactly one per empty frame"
5085 );
5086 check_flood_or_return!(self);
5087 }
5088 let Some(global_stream_id) = self.streams.get(&data.stream_id).copied() else {
5089 // The stream was terminated while data was expected,
5090 // probably due to automatic answer for invalid/unauthorized access.
5091 // RFC 9113 §6.9: we MUST still account for the DATA payload in
5092 // connection-level flow control using the full wire length
5093 // (including pad-length byte and padding), otherwise the window
5094 // shrinks permanently and eventually stalls the connection.
5095 self.flow_control.received_bytes_since_update += wire_payload_len;
5096 let conn_threshold = self.connection_config.initial_connection_window / 2;
5097 if self.flow_control.received_bytes_since_update >= conn_threshold {
5098 let increment = self.flow_control.received_bytes_since_update;
5099 self.queue_window_update(0, increment);
5100 self.flow_control.received_bytes_since_update = 0;
5101 self.readiness.arm_writable();
5102 }
5103 self.attribute_bytes_to_overhead();
5104 return MuxResult::Continue;
5105 };
5106 let mut slice = data.payload;
5107 let stream = &mut context.streams[global_stream_id];
5108 // Unpadded application payload size — what is forwarded to the backend
5109 // and counted against Content-Length.
5110 let content_len = slice.len();
5111 // Full wire-payload size (includes pad-length byte and padding).
5112 // RFC 9113 §5.2: padding counts against flow-control windows.
5113 let wire_len = wire_payload_len as usize;
5114 let cl_exempt = self.content_length_exempt(&stream.context);
5115
5116 // Extract declared content-length and update position-aware data counter
5117 let (data_received, declared_length) = {
5118 let parts = stream.split(&self.position);
5119 *parts.data_received += content_len;
5120 let total = *parts.data_received;
5121 let declared = match parts.rbuffer.body_size {
5122 kawa::BodySize::Length(n) => Some(n),
5123 _ => None,
5124 };
5125 (total, declared)
5126 };
5127
5128 // RFC 9113 §6.9 + §5.2: credit connection-level flow control BEFORE any
5129 // early-return path. Malformed DATA still consumed the peer's send
5130 // window; without crediting it back, repeated bad streams permanently
5131 // shrink the connection window and stall unrelated streams that share
5132 // the same H2 connection. Stream-level credit can stay below — once we
5133 // RST the violating stream, its per-stream window is moot per
5134 // RFC 9113 §6.9 (the receiver discards further frames for the stream).
5135 let conn_threshold = self.connection_config.initial_connection_window / 2;
5136 self.flow_control.received_bytes_since_update += wire_payload_len;
5137 if self.flow_control.received_bytes_since_update >= conn_threshold {
5138 let increment = self.flow_control.received_bytes_since_update;
5139 self.queue_window_update(0, increment);
5140 self.flow_control.received_bytes_since_update = 0;
5141 }
5142
5143 // RFC 9113 §8.1.1: if Content-Length is present, total DATA payload
5144 // must not exceed the declared length (check on every frame).
5145 // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses (body absent by definition).
5146 if !cl_exempt
5147 && let Some(expected) = declared_length
5148 && data_received > expected
5149 {
5150 error!(
5151 "{} Content-Length mismatch: received {} > declared {}",
5152 log_context!(self),
5153 data_received,
5154 expected
5155 );
5156 // Pair WRITABLE arming with the queued connection-level
5157 // WINDOW_UPDATE before returning; otherwise the credit sits
5158 // until the next inbound frame on this connection.
5159 if !self.flow_control.pending_window_updates.is_empty() {
5160 self.readiness.arm_writable();
5161 }
5162 let result = self.reset_stream(
5163 data.stream_id,
5164 global_stream_id,
5165 context,
5166 endpoint,
5167 H2Error::ProtocolError,
5168 );
5169 self.remove_dead_stream(data.stream_id, global_stream_id);
5170 return result;
5171 }
5172
5173 let stream = &mut context.streams[global_stream_id];
5174 self.attribute_bytes_to_stream(&mut stream.metrics);
5175 let stream_state = stream.state;
5176 let is_unlinked = matches!(stream_state, StreamState::Unlinked);
5177 let parts = stream.split(&self.position);
5178 let kawa = parts.rbuffer;
5179 self.position.count_bytes_in(parts.metrics, content_len);
5180
5181 // Stream-level flow control (only if stream is still open).
5182 // Connection-level credit was already applied above the CL check so
5183 // malformed DATA cannot starve the connection window for other streams.
5184 if !data.end_stream {
5185 self.queue_window_update(data.stream_id, wire_payload_len);
5186 }
5187
5188 // If we have pending updates, ensure we get a writable event.
5189 // Must use signal_pending_write() — not just interest.insert() — because
5190 // under edge-triggered epoll the WRITABLE event bit may have been consumed
5191 // by a previous write cycle. Without the event bit set, filter_interest()
5192 // returns 0 and the WINDOW_UPDATEs never get flushed, stalling the client.
5193 if !self.flow_control.pending_window_updates.is_empty() {
5194 self.readiness.arm_writable();
5195 }
5196
5197 // Refresh per-stream idle timer on non-empty DATA.
5198 // Empty DATA frames (CVE-2019-9518 vector) must NOT reset the timer,
5199 // otherwise an attacker can keep a stream alive indefinitely with
5200 // zero-length frames while pinning a MAX_CONCURRENT_STREAMS slot.
5201 if content_len > 0
5202 && let Some(t) = self.stream_last_activity_at.get_mut(&data.stream_id)
5203 {
5204 *t = Instant::now();
5205 }
5206
5207 if is_unlinked {
5208 // Backend is gone but client is still sending DATA.
5209 // Discard the data (flow control updates were already
5210 // queued above) to prevent the buffer from filling up.
5211 kawa.storage.clear();
5212 if data.end_stream {
5213 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
5214 self.mark_end_of_stream(stream);
5215 }
5216 } else {
5217 // Advance storage.head by the full wire payload length so the
5218 // next frame doesn't read stale pad-length+padding bytes.
5219 slice.start = slice.start.saturating_add(kawa.storage.head as u32);
5220 kawa.storage.head += wire_len;
5221
5222 // Emit chunk framing for chunked transfer encoding (H2→H1 path).
5223 // H2 converter ignores ChunkHeader and end_chunk Flags, so this is safe for H2→H2.
5224 if kawa.body_size == kawa::BodySize::Chunked && content_len > 0 {
5225 let hex_len = {
5226 let mut buf = Vec::with_capacity(16);
5227 let _ = write!(buf, "{content_len:x}");
5228 buf
5229 };
5230 kawa.push_block(kawa::Block::ChunkHeader(kawa::ChunkHeader {
5231 length: kawa::Store::from_vec(hex_len),
5232 }));
5233 }
5234
5235 kawa.push_block(kawa::Block::Chunk(kawa::Chunk {
5236 data: kawa::Store::Slice(slice),
5237 }));
5238
5239 if kawa.body_size == kawa::BodySize::Chunked && content_len > 0 {
5240 kawa.push_block(kawa::Block::Flags(kawa::Flags {
5241 end_body: false,
5242 end_chunk: true,
5243 end_header: false,
5244 end_stream: false,
5245 }));
5246 }
5247
5248 if data.end_stream {
5249 // RFC 9113 §8.1.1: on end_stream, total DATA must equal Content-Length.
5250 // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses.
5251 if !cl_exempt
5252 && let Some(expected) = declared_length
5253 && data_received != expected
5254 {
5255 error!(
5256 "{} Content-Length mismatch: received {} != declared {}",
5257 log_context!(self),
5258 data_received,
5259 expected
5260 );
5261 let result = self.reset_stream(
5262 data.stream_id,
5263 global_stream_id,
5264 context,
5265 endpoint,
5266 H2Error::ProtocolError,
5267 );
5268 self.remove_dead_stream(data.stream_id, global_stream_id);
5269 return result;
5270 }
5271 let is_chunked = kawa.body_size == kawa::BodySize::Chunked;
5272 kawa.push_block(kawa::Block::Flags(kawa::Flags {
5273 end_body: true,
5274 end_chunk: is_chunked,
5275 end_header: false,
5276 end_stream: true,
5277 }));
5278 kawa.parsing_phase = kawa::ParsingPhase::Terminated;
5279 self.mark_end_of_stream(stream);
5280 }
5281 if let StreamState::Linked(token) = stream_state {
5282 // Mirror of h1.rs:361-368 for the H2-backend → H2-frontend
5283 // path: edge-triggered epoll will NOT re-fire for bytes we
5284 // just pushed into stream.back; the synthetic event is the
5285 // only wake path. LIFECYCLE invariant 15.
5286 endpoint.readiness_mut(token).arm_writable();
5287 incr!(names::h2::SIGNAL_WRITABLE_REARMED_PEER_DATA);
5288 }
5289 }
5290 MuxResult::Continue
5291 }
5292
5293 fn handle_headers_frame<E, L>(
5294 &mut self,
5295 headers: Headers,
5296 context: &mut Context<L>,
5297 mut endpoint: E,
5298 ) -> MuxResult
5299 where
5300 E: Endpoint,
5301 L: ListenerHandler + L7ListenerHandler,
5302 {
5303 // HEADERS frames represent real application activity (new request
5304 // or response). Reset the timeout since the peer is actively
5305 // communicating, unlike control frames (PING, WINDOW_UPDATE).
5306 self.timeout_container.reset();
5307 if !headers.end_headers {
5308 // CVE-2024-27316: only initialize tracking on the very first HEADERS
5309 // fragment, not on re-entries from ContinuationFrame (which call
5310 // handle_frame(Frame::Headers) with the accumulated header block).
5311 if self.flood_detector.continuation_count == 0 {
5312 self.flood_detector.accumulated_header_size = headers.header_block_fragment.len;
5313 }
5314 debug!(
5315 "{} FRAGMENT: stream_id={}, len={}",
5316 log_context!(self),
5317 headers.stream_id,
5318 self.zero.storage.data().len()
5319 );
5320 self.state = H2State::ContinuationHeader(headers);
5321 return MuxResult::Continue;
5322 }
5323 // Header block is complete — reset CONTINUATION counters
5324 self.flood_detector.reset_continuation();
5325 // can this fail?
5326 let stream_id = headers.stream_id;
5327 let Some(global_stream_id) = self.streams.get(&stream_id).copied() else {
5328 error!(
5329 "{} Handling Headers frame with no attached stream {:#?}",
5330 log_context!(self),
5331 self
5332 );
5333 incr!(names::h2::HEADERS_NO_STREAM_ERROR);
5334 self.attribute_bytes_to_overhead();
5335 return self.force_disconnect();
5336 };
5337
5338 // Refresh per-stream idle timer on HEADERS (response headers or trailers
5339 // on an existing stream). Initial HEADERS that create the stream already
5340 // set the timestamp in create_stream().
5341 if let Some(t) = self.stream_last_activity_at.get_mut(&stream_id) {
5342 *t = Instant::now();
5343 }
5344
5345 if let Some(priority) = &headers.priority
5346 && self.prioriser.push_priority(stream_id, priority.clone())
5347 {
5348 self.reset_stream(
5349 stream_id,
5350 global_stream_id,
5351 context,
5352 endpoint,
5353 H2Error::ProtocolError,
5354 );
5355 self.remove_dead_stream(stream_id, global_stream_id);
5356 return MuxResult::Continue;
5357 }
5358
5359 let stream = &mut context.streams[global_stream_id];
5360 self.attribute_bytes_to_stream(&mut stream.metrics);
5361 let kawa = &mut self.zero;
5362 let buffer = headers.header_block_fragment.data(kawa.storage.buffer());
5363 let stream = &mut context.streams[global_stream_id];
5364 let parts = &mut stream.split(&self.position);
5365 let was_initial = parts.rbuffer.is_initial();
5366 let elide_x_real_ip = parts.context.elide_x_real_ip;
5367 let status = pkawa::handle_header(
5368 &mut self.decoder,
5369 &mut self.prioriser,
5370 stream_id,
5371 parts.rbuffer,
5372 buffer,
5373 headers.end_stream,
5374 parts.context,
5375 self.flood_detector.config.max_header_list_size,
5376 self.flood_detector.config.max_header_fields,
5377 elide_x_real_ip,
5378 );
5379 kawa.storage.clear();
5380 if let Err((error, global)) = status {
5381 match self.position {
5382 Position::Client(..) => incr!(names::http::BACKEND_PARSE_ERRORS),
5383 Position::Server => incr!(names::http::FRONTEND_PARSE_ERRORS),
5384 }
5385 if global {
5386 error!(
5387 "{} GOT GLOBAL ERROR WHILE PROCESSING HEADERS",
5388 log_context!(self)
5389 );
5390 return self.goaway(error);
5391 } else {
5392 let result =
5393 self.reset_stream(stream_id, global_stream_id, context, endpoint, error);
5394 self.remove_dead_stream(stream_id, global_stream_id);
5395 return result;
5396 }
5397 }
5398 if headers.end_stream {
5399 // RFC 9113 §8.1.1: when END_STREAM arrives via trailers,
5400 // validate that total DATA received matches Content-Length.
5401 // RFC 9110 §8.6: skip for HEAD/1xx/204/304 responses.
5402 if !was_initial && !self.content_length_exempt(&stream.context) {
5403 let parts = stream.split(&self.position);
5404 if let kawa::BodySize::Length(expected) = parts.rbuffer.body_size
5405 && *parts.data_received != expected
5406 {
5407 error!(
5408 "{} Content-Length mismatch on trailers: received {} != declared {}",
5409 log_context!(self),
5410 *parts.data_received,
5411 expected
5412 );
5413 let result = self.reset_stream(
5414 stream_id,
5415 global_stream_id,
5416 context,
5417 endpoint,
5418 H2Error::ProtocolError,
5419 );
5420 self.remove_dead_stream(stream_id, global_stream_id);
5421 return result;
5422 }
5423 }
5424 self.mark_end_of_stream(stream);
5425 }
5426 if let StreamState::Linked(token) = stream.state {
5427 // Mirror of handle_data_frame's rearm. LIFECYCLE invariant 15.
5428 endpoint.readiness_mut(token).arm_writable();
5429 incr!(names::h2::SIGNAL_WRITABLE_REARMED_PEER_HEADERS);
5430 }
5431 // was_initial prevents trailers from triggering connection
5432 if was_initial && self.position.is_server() {
5433 incr!(names::http::REQUESTS);
5434 gauge_add!(names::http::ACTIVE_REQUESTS, 1);
5435 stream.metrics.service_start();
5436 stream.request_counted = true;
5437 stream.state = StreamState::Link;
5438 context.pending_links.push_back(global_stream_id);
5439 }
5440 MuxResult::Continue
5441 }
5442
5443 fn handle_push_promise_frame(&mut self) -> MuxResult {
5444 self.attribute_bytes_to_overhead();
5445 match self.position {
5446 Position::Client(..) => {
5447 // RFC 9113 §8.4: Server push is deprecated. Sozu never sends
5448 // SETTINGS_ENABLE_PUSH=1, so receiving PUSH_PROMISE is a protocol error.
5449 error!(
5450 "{} Received PUSH_PROMISE but server push is not supported",
5451 log_context!(self)
5452 );
5453 self.goaway(H2Error::ProtocolError)
5454 }
5455 Position::Server => {
5456 // Clients must never send PUSH_PROMISE (RFC 9113 §8.4)
5457 error!("{} Received PUSH_PROMISE from client", log_context!(self));
5458 self.goaway(H2Error::ProtocolError)
5459 }
5460 }
5461 }
5462
5463 fn handle_priority_frame<E, L>(
5464 &mut self,
5465 priority: parser::Priority,
5466 context: &mut Context<L>,
5467 endpoint: E,
5468 ) -> MuxResult
5469 where
5470 E: Endpoint,
5471 L: ListenerHandler + L7ListenerHandler,
5472 {
5473 if let Some(global_stream_id) = self.streams.get(&priority.stream_id).copied() {
5474 let stream = &mut context.streams[global_stream_id];
5475 self.attribute_bytes_to_stream(&mut stream.metrics);
5476 } else {
5477 self.attribute_bytes_to_overhead();
5478 }
5479 // Pass 3 Medium #4: standalone PRIORITY frames can arrive for any
5480 // peer-chosen stream ID. Accept only currently-open streams and a
5481 // small idle look-ahead window; everything else is dropped before
5482 // it can feed memory into the priority map.
5483 if self.prioriser.push_priority_guarded(
5484 priority.stream_id,
5485 priority.inner,
5486 self.last_stream_id,
5487 &self.streams,
5488 ) {
5489 if let Some(global_stream_id) = self.streams.get(&priority.stream_id).copied() {
5490 let result = self.reset_stream(
5491 priority.stream_id,
5492 global_stream_id,
5493 context,
5494 endpoint,
5495 H2Error::ProtocolError,
5496 );
5497 self.remove_dead_stream(priority.stream_id, global_stream_id);
5498 return result;
5499 } else {
5500 error!(
5501 "{} INVALID PRIORITY RECEIVED ON INVALID STREAM",
5502 log_context!(self)
5503 );
5504 return self.goaway(H2Error::ProtocolError);
5505 }
5506 }
5507 MuxResult::Continue
5508 }
5509
5510 /// RFC 9218 §7.1: PRIORITY_UPDATE reprioritizes an open or idle-soon
5511 /// stream at the connection level. Decodes the priority field value
5512 /// (same grammar as the `priority` request header, `parse_rfc9218_priority`)
5513 /// and pushes it into the `Prioriser` through the same guarded path used
5514 /// for standalone PRIORITY frames — the guard bounds memory against a
5515 /// client spamming PRIORITY_UPDATE for far-future stream IDs.
5516 ///
5517 /// Prioritized stream ID `0` is a connection-level `PROTOCOL_ERROR`
5518 /// (RFC 9218 §7.1). For any other ID that is not currently open or
5519 /// within the idle look-ahead budget, the update is silently dropped
5520 /// (matches the PRIORITY-frame guard semantics — no state change).
5521 fn handle_priority_update_frame(&mut self, pu: parser::PriorityUpdate) -> MuxResult {
5522 self.attribute_bytes_to_overhead();
5523 if pu.prioritized_stream_id == 0 {
5524 error!(
5525 "{} PRIORITY_UPDATE with prioritized_stream_id=0 (RFC 9218 §7.1)",
5526 log_context!(self)
5527 );
5528 return self.goaway(H2Error::ProtocolError);
5529 }
5530 let (urgency, incremental) = pkawa::parse_rfc9218_priority(&pu.priority_field_value);
5531 let (prev_urgency, _) = self.prioriser.get(&pu.prioritized_stream_id);
5532 trace!(
5533 "{} PRIORITY_UPDATE stream={} urgency={}->{} incremental={} rearmed_writable=true",
5534 log_context!(self),
5535 pu.prioritized_stream_id,
5536 prev_urgency,
5537 urgency,
5538 incremental
5539 );
5540 let _ = self.prioriser.push_priority_guarded(
5541 pu.prioritized_stream_id,
5542 parser::PriorityPart::Rfc9218 {
5543 urgency,
5544 incremental,
5545 },
5546 self.last_stream_id,
5547 &self.streams,
5548 );
5549 // LIFECYCLE invariant 15: reprioritisation only changes ordering for
5550 // the NEXT write pass. Under ET epoll, if finalize_write already
5551 // stripped WRITABLE, the scheduler won't re-run without a synthetic
5552 // wake — pair the interest insert with signal_pending_write.
5553 self.readiness.arm_writable();
5554 incr!(names::h2::SIGNAL_WRITABLE_REARMED_PRIORITY_UPDATE);
5555 MuxResult::Continue
5556 }
5557
5558 fn handle_rst_stream_frame<E, L>(
5559 &mut self,
5560 rst_stream: parser::RstStream,
5561 context: &mut Context<L>,
5562 mut endpoint: E,
5563 ) -> MuxResult
5564 where
5565 E: Endpoint,
5566 L: ListenerHandler + L7ListenerHandler,
5567 {
5568 // Per-error-code counter for the inbound RST. Emitted before the
5569 // flood-detector trip check so even a connection that gets terminated
5570 // by `handle_flood_violation` shows up in the per-code breakdown
5571 // (the dedicated `h2.flood.violation.rst_stream_*` series tracks the
5572 // mitigation event itself).
5573 count!(metric_for_rst_stream_received(rst_stream.error_code), 1);
5574 // CVE-2023-44487 Rapid Reset + CVE-2019-9514: track RST_STREAM rate.
5575 let rst_count_before = self.flood_detector.rst_stream_count;
5576 self.flood_detector.rst_stream_count += 1;
5577 debug_assert_eq!(
5578 self.flood_detector.rst_stream_count,
5579 rst_count_before + 1,
5580 "per-window RST_STREAM counter must advance by exactly one per inbound RST"
5581 );
5582 check_flood_or_return!(self);
5583 // Additional CVE-2023-44487 mitigation: lifetime cap on RST_STREAM
5584 // frames received. The per-window counter above half-decays, so a
5585 // patient client can keep ~50 RST/s forever; a never-decaying
5586 // lifetime counter puts an absolute ceiling on that amplification.
5587 // Streams whose backend response has not yet started count toward a
5588 // much lower "abusive" ceiling — this is the signature Rapid Reset
5589 // pattern where the attacker pays one RST frame and we pay a
5590 // backend round-trip for each.
5591 //
5592 // "Response started" here means the Server has begun producing
5593 // response bytes (backend kawa buffer past its initial phase). For
5594 // the Client position the concept does not apply symmetrically
5595 // (RSTs received from the backend are rare and benign), so we
5596 // conservatively flag them as abusive too — lifetime cap still
5597 // dominates in practice.
5598 let response_started = match self.streams.get(&rst_stream.stream_id) {
5599 Some(global_stream_id) => {
5600 let stream = &context.streams[*global_stream_id];
5601 !stream.back.is_initial()
5602 }
5603 // Stream already gone (e.g. closed, not yet registered) —
5604 // treat as response-started to avoid over-counting benign
5605 // races as abusive.
5606 None => true,
5607 };
5608 if let Some(violation) = self.flood_detector.record_rst_lifetime(response_started) {
5609 return self.handle_flood_violation(violation);
5610 }
5611 // Rapid Reset signature (CVE-2023-44487): a RST that arrives before the
5612 // backend has begun answering. Emitted alongside the per-code counter
5613 // so the SOC can alert on the rate of pre-response RSTs without
5614 // having to differentiate by error code.
5615 if !response_started {
5616 count!(names::h2::RST_STREAM_RECEIVED_PRE_RESPONSE_START, 1);
5617 }
5618 debug!(
5619 "{} RstStream({} -> {})",
5620 log_context!(self),
5621 rst_stream.error_code,
5622 H2Error::try_from(rst_stream.error_code).map_or("UNKNOWN_ERROR", |e| e.as_str())
5623 );
5624 // Compute totals before removing the stream from the map,
5625 // so the removed stream's bytes are included in the total.
5626 let rst_byte_totals = self.compute_stream_byte_totals(context);
5627 if let Some(global_stream_id) = self.streams.get(&rst_stream.stream_id).copied() {
5628 let stream = &mut context.streams[global_stream_id];
5629 self.attribute_bytes_to_stream(&mut stream.metrics);
5630 let linked_token = stream.linked_token();
5631 let (client_rtt, server_rtt) =
5632 Self::snapshot_rtts(&self.position, &self.socket, &endpoint, linked_token);
5633 if let Some(token) = linked_token {
5634 endpoint.end_stream(token, global_stream_id, context);
5635 }
5636 let stream = &mut context.streams[global_stream_id];
5637 match &self.position {
5638 // Inbound RST_STREAM on the backend side terminates the in-flight
5639 // request without going through Connection::end_stream (the normal
5640 // place where Backend.active_requests is decremented), so do the
5641 // bookkeeping explicitly here to avoid leaking load counters.
5642 Position::Client(_, backend, BackendStatus::Connected) => {
5643 let mut backend_borrow = backend.borrow_mut();
5644 backend_borrow.active_requests =
5645 backend_borrow.active_requests.saturating_sub(1);
5646 }
5647 Position::Client(..) => {}
5648 Position::Server => {
5649 self.distribute_overhead(&mut stream.metrics, rst_byte_totals);
5650 // This is a special case, normally, all stream are terminated by the server
5651 // when the last byte of the response is written. Here, the reset is requested
5652 // on the server endpoint and immediately terminates, shortcutting the other path
5653 stream.metrics.backend_stop();
5654 stream.generate_access_log(
5655 true,
5656 Some("H2::ResetFrame"),
5657 context.listener.clone(),
5658 client_rtt,
5659 server_rtt,
5660 );
5661 stream.state = StreamState::Recycle;
5662 }
5663 }
5664 // Retire from streams/prioriser/stream_last_activity_at and
5665 // invalidate expect_write/expect_read if they reference this gid.
5666 self.remove_dead_stream(rst_stream.stream_id, global_stream_id);
5667 } else {
5668 self.attribute_bytes_to_overhead();
5669 }
5670 MuxResult::Continue
5671 }
5672
5673 fn handle_settings_frame<L>(
5674 &mut self,
5675 settings: parser::Settings,
5676 context: &mut Context<L>,
5677 ) -> MuxResult
5678 where
5679 L: ListenerHandler + L7ListenerHandler,
5680 {
5681 if settings.ack {
5682 // RFC 9113 §6.5: SETTINGS ACK must have empty payload
5683 if !settings.settings.is_empty() {
5684 error!("{} SETTINGS ACK with non-empty payload", log_context!(self));
5685 return self.goaway(H2Error::FrameSizeError);
5686 }
5687 // RFC 9113 §6.5: peer acknowledged our SETTINGS — clear timeout
5688 self.settings_sent_at = None;
5689 // RFC 7541 §4.2: sync the decoder's max allowed table size with
5690 // what we advertised. Currently a no-op (settings don't change at
5691 // runtime), but guards against future runtime SETTINGS updates.
5692 self.decoder.set_max_allowed_table_size(
5693 self.local_settings.settings_header_table_size as usize,
5694 );
5695 self.attribute_bytes_to_overhead();
5696 return MuxResult::Continue;
5697 }
5698 // CVE-2019-9515: track SETTINGS frame rate
5699 let settings_count_before = self.flood_detector.settings_count;
5700 let settings_lifetime_before = self.flood_detector.total_settings_received_lifetime;
5701 self.flood_detector.settings_count += 1;
5702 self.flood_detector.total_settings_received_lifetime = self
5703 .flood_detector
5704 .total_settings_received_lifetime
5705 .saturating_add(1);
5706 debug_assert_eq!(
5707 self.flood_detector.settings_count,
5708 settings_count_before + 1,
5709 "per-window SETTINGS counter must advance by one per non-ACK SETTINGS"
5710 );
5711 debug_assert!(
5712 self.flood_detector.total_settings_received_lifetime > settings_lifetime_before
5713 || settings_lifetime_before == u32::MAX,
5714 "lifetime SETTINGS counter must advance (or already be saturated)"
5715 );
5716 check_flood_or_return!(self);
5717 for setting in settings.settings {
5718 let v = setting.value;
5719 let mut is_error = false;
5720 #[rustfmt::skip]
5721 match setting.identifier {
5722 parser::SETTINGS_HEADER_TABLE_SIZE => {
5723 // Cap to the configured maximum — a malicious peer can
5724 // advertise up to 4 GB to inflate HPACK encoder memory.
5725 let cap = self.flood_detector.config.max_header_table_size;
5726 let capped = v.min(cap);
5727 self.peer_settings.settings_header_table_size = capped;
5728 self.encoder.set_max_table_size(capped as usize);
5729 // RFC 7541 §4.2 / §6.3: queue a dynamic-table-size-update
5730 // HPACK directive for the next header block we emit.
5731 // Without it, the peer's decoder keeps its previous (possibly
5732 // larger) table cap and our encoder-side change is silent
5733 // — conformance suites (h2spec `hpack/4.2`) will flag it.
5734 self.pending_table_size_update = Some(capped);
5735 },
5736 parser::SETTINGS_ENABLE_PUSH => { self.peer_settings.settings_enable_push = v == 1; is_error |= v > 1 },
5737 parser::SETTINGS_MAX_CONCURRENT_STREAMS => { self.peer_settings.settings_max_concurrent_streams = v },
5738 parser::SETTINGS_INITIAL_WINDOW_SIZE => { is_error |= self.update_initial_window_size(v, context) },
5739 parser::SETTINGS_MAX_FRAME_SIZE => { self.peer_settings.settings_max_frame_size = v; is_error |= !(MIN_MAX_FRAME_SIZE..MAX_MAX_FRAME_SIZE).contains(&v) },
5740 parser::SETTINGS_MAX_HEADER_LIST_SIZE => { self.peer_settings.settings_max_header_list_size = v },
5741 parser::SETTINGS_ENABLE_CONNECT_PROTOCOL => { self.peer_settings.settings_enable_connect_protocol = v == 1; is_error |= v > 1 },
5742 parser::SETTINGS_NO_RFC7540_PRIORITIES => { self.peer_settings.settings_no_rfc7540_priorities = v == 1; is_error |= v > 1 },
5743 other => { warn!("Unknown setting_id: {}, we MUST ignore this", other); self.flood_detector.glitch_count += 1 },
5744 };
5745 if is_error {
5746 error!("{} INVALID SETTING", log_context!(self));
5747 return self.goaway(H2Error::ProtocolError);
5748 }
5749 }
5750
5751 self.attribute_bytes_to_overhead();
5752
5753 // Enlarge the connection-level receive window for backend H2
5754 // connections (Position::Client). The server side does this in
5755 // the ServerSettings writable path, but the client needs to do
5756 // it here after receiving the server's initial SETTINGS.
5757 if self.position.is_client()
5758 && self.flow_control.window <= DEFAULT_INITIAL_WINDOW_SIZE as i32
5759 {
5760 let increment = self
5761 .connection_config
5762 .initial_connection_window
5763 .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
5764 if increment > 0 {
5765 self.queue_window_update(0, increment);
5766 }
5767 // Do NOT increment flow_control.window here: sending our own
5768 // WINDOW_UPDATE enlarges the peer's send allowance, not ours.
5769 // Our send window is only updated by WINDOW_UPDATEs we receive
5770 // from the peer (RFC 9113 §6.9).
5771 }
5772
5773 let kawa = &mut self.zero;
5774 let ack = &serializer::SETTINGS_ACKNOWLEDGEMENT;
5775 let buf = kawa.storage.space();
5776 if buf.len() < ack.len() {
5777 error!(
5778 "{} No space in zero buffer for SETTINGS ACK ({} available, {} needed)",
5779 log_context!(self),
5780 buf.len(),
5781 ack.len()
5782 );
5783 return self.force_disconnect();
5784 }
5785 buf[..ack.len()].copy_from_slice(ack);
5786 kawa.storage.fill(ack.len());
5787
5788 self.readiness.interest.insert(Ready::WRITABLE);
5789 self.readiness.interest.remove(Ready::READABLE);
5790 self.expect_write = Some(H2StreamId::Zero);
5791 self.readiness.signal_pending_write();
5792 MuxResult::Continue
5793 }
5794
5795 fn handle_ping_frame(&mut self, ping: parser::Ping) -> MuxResult {
5796 if ping.ack {
5797 self.attribute_bytes_to_overhead();
5798 return MuxResult::Continue;
5799 }
5800 // CVE-2019-9512: track non-ACK PING frame rate
5801 let ping_count_before = self.flood_detector.ping_count;
5802 let ping_lifetime_before = self.flood_detector.total_ping_received_lifetime;
5803 self.flood_detector.ping_count += 1;
5804 self.flood_detector.total_ping_received_lifetime = self
5805 .flood_detector
5806 .total_ping_received_lifetime
5807 .saturating_add(1);
5808 debug_assert_eq!(
5809 self.flood_detector.ping_count,
5810 ping_count_before + 1,
5811 "per-window PING counter must advance by one per non-ACK PING"
5812 );
5813 debug_assert!(
5814 self.flood_detector.total_ping_received_lifetime > ping_lifetime_before
5815 || ping_lifetime_before == u32::MAX,
5816 "lifetime PING counter must advance (or already be saturated)"
5817 );
5818 check_flood_or_return!(self);
5819 self.attribute_bytes_to_overhead();
5820 let kawa = &mut self.zero;
5821 let ping_response_size = serializer::PING_ACKNOWLEDGEMENT_HEADER.len() + 8;
5822 if kawa.storage.space().len() < ping_response_size {
5823 error!(
5824 "{} No space in zero buffer for PING response ({} available, {} needed)",
5825 log_context!(self),
5826 kawa.storage.space().len(),
5827 ping_response_size
5828 );
5829 return self.force_disconnect();
5830 }
5831 match serializer::gen_ping_acknowledgement(kawa.storage.space(), &ping.payload) {
5832 Ok((_, size)) => {
5833 kawa.storage.fill(size);
5834 incr!(names::h2::FRAMES_TX_PING_ACK);
5835 }
5836 Err(error) => {
5837 error!(
5838 "{} Could not serialize PingFrame: {:?}",
5839 log_context!(self),
5840 error
5841 );
5842 return self.force_disconnect();
5843 }
5844 };
5845 self.readiness.interest.insert(Ready::WRITABLE);
5846 self.readiness.interest.remove(Ready::READABLE);
5847 self.expect_write = Some(H2StreamId::Zero);
5848 self.readiness.signal_pending_write();
5849 MuxResult::Continue
5850 }
5851
5852 fn handle_goaway_frame<E, L>(
5853 &mut self,
5854 goaway: parser::GoAway,
5855 context: &mut Context<L>,
5856 mut endpoint: E,
5857 ) -> MuxResult
5858 where
5859 E: Endpoint,
5860 L: ListenerHandler + L7ListenerHandler,
5861 {
5862 self.attribute_bytes_to_overhead();
5863 let error_name =
5864 H2Error::try_from(goaway.error_code).map_or("UNKNOWN_ERROR", |e| e.as_str());
5865 if goaway.error_code == H2Error::NoError as u32 {
5866 debug!(
5867 "{} Received GOAWAY: last_stream_id={}, error={}, debug_data={:?}",
5868 log_context!(self),
5869 goaway.last_stream_id,
5870 error_name,
5871 goaway.additional_debug_data
5872 );
5873 } else {
5874 // Peer-originated failure: no variant of H2Error from a peer
5875 // implies a sozu bug. Impact handling is separate (retry above
5876 // `last_stream_id`, RST_STREAM for consumed streams) and logs
5877 // its own details below, so the summary drops to `warn!`.
5878 warn!(
5879 "{} Received GOAWAY: last_stream_id={}, error={}, debug_data={:?}",
5880 log_context!(self),
5881 goaway.last_stream_id,
5882 error_name,
5883 goaway.additional_debug_data
5884 );
5885 }
5886 count!(metric_for_goaway_received(goaway.error_code), 1);
5887 // RFC 9113 §6.8: begin graceful drain.
5888 self.drain.draining = true;
5889 self.drain.peer_last_stream_id = Some(goaway.last_stream_id);
5890
5891 // Streams with ID > last_stream_id were NOT processed by the peer.
5892 // Mark them for retry (StreamState::Link) so they can be retried
5893 // on a new connection.
5894 // IMPORTANT: do NOT call endpoint.end_stream() here — that would
5895 // remove the stream from the frontend's H2 stream map and send
5896 // RST_STREAM to the client, killing the request instead of retrying it.
5897 let mut retry_streams = Vec::new();
5898 for (&stream_id, &global_stream_id) in &self.streams {
5899 if stream_id > goaway.last_stream_id {
5900 retry_streams.push((stream_id, global_stream_id));
5901 }
5902 }
5903 for (stream_id, global_stream_id) in &retry_streams {
5904 // Remove from reverse index before transitioning away from Linked.
5905 if let StreamState::Linked(token) = context.streams[*global_stream_id].state {
5906 remove_backend_stream(&mut context.backend_streams, token, *global_stream_id);
5907 }
5908 let stream = &mut context.streams[*global_stream_id];
5909 if stream.front.consumed {
5910 // Request was already sent to this backend — we can't
5911 // replay it. Use the linked token's readiness (via endpoint)
5912 // so the RST_STREAM reaches the client.
5913 debug!(
5914 "{} GOAWAY: stream {} already consumed, cannot retry",
5915 log_context!(self),
5916 stream_id
5917 );
5918 if let StreamState::Linked(token) = stream.state {
5919 let front_readiness = endpoint.readiness_mut(token);
5920 forcefully_terminate_answer(stream, front_readiness, H2Error::RefusedStream);
5921 } else {
5922 warn!(
5923 "{} GOAWAY: stream {} consumed but not Linked, cannot notify frontend",
5924 log_context!(self),
5925 stream_id
5926 );
5927 }
5928 } else {
5929 stream.state = StreamState::Link;
5930 context.pending_links.push_back(*global_stream_id);
5931 }
5932 // Both retry (!consumed) and terminated (consumed) paths remove the
5933 // stream from self.streams without going through Connection::end_stream,
5934 // so decrement Backend.active_requests here to keep load metrics honest.
5935 if let Position::Client(_, backend, BackendStatus::Connected) = &self.position {
5936 let mut backend_borrow = backend.borrow_mut();
5937 backend_borrow.active_requests = backend_borrow.active_requests.saturating_sub(1);
5938 }
5939 // Retire from streams/prioriser/stream_last_activity_at and
5940 // invalidate expect_write/expect_read if they reference this gid.
5941 self.remove_dead_stream(*stream_id, *global_stream_id);
5942 }
5943
5944 // If no active streams remain, close immediately
5945 if self.streams.is_empty() {
5946 return self.goaway(H2Error::NoError);
5947 }
5948
5949 // Otherwise, let remaining streams (ID <= last_stream_id) complete.
5950 // The connection will be closed when all streams finish.
5951 MuxResult::Continue
5952 }
5953
5954 fn handle_window_update_frame<E, L>(
5955 &mut self,
5956 wu: WindowUpdate,
5957 context: &mut Context<L>,
5958 endpoint: E,
5959 ) -> MuxResult
5960 where
5961 E: Endpoint,
5962 L: ListenerHandler + L7ListenerHandler,
5963 {
5964 let stream_id = wu.stream_id;
5965 let increment = wu.increment;
5966
5967 // RFC 9113 §6.9: increment of 0 MUST be treated as an error.
5968 // Connection-level (stream 0) -> connection error (GOAWAY).
5969 // Stream-level -> stream error (RST_STREAM).
5970 if increment == 0 {
5971 if stream_id == 0 {
5972 error!(
5973 "{} WINDOW_UPDATE with zero increment on connection (stream 0)",
5974 log_context!(self)
5975 );
5976 return self.goaway(H2Error::ProtocolError);
5977 } else {
5978 error!(
5979 "{} WINDOW_UPDATE with zero increment on stream {}",
5980 log_context!(self),
5981 stream_id
5982 );
5983 if let Some(global_stream_id) = self.streams.get(&stream_id).copied() {
5984 let result = self.reset_stream(
5985 stream_id,
5986 global_stream_id,
5987 context,
5988 endpoint,
5989 H2Error::ProtocolError,
5990 );
5991 self.remove_dead_stream(stream_id, global_stream_id);
5992 return result;
5993 }
5994 // Stream not in map (already closed) — treat as glitch
5995 self.flood_detector.glitch_count += 1;
5996 check_flood_or_return!(self);
5997 self.attribute_bytes_to_overhead();
5998 return MuxResult::Continue;
5999 }
6000 }
6001
6002 // The parser masks the reserved bit (STREAM_ID_MASK), so increment <=
6003 // 2^31-1 and try_from always succeeds. Use try_from rather than `as` to
6004 // guard against a future parser change that drops the mask.
6005 let increment = i32::try_from(increment).unwrap_or(i32::MAX);
6006 // RFC 9113 §6.9: a non-zero WINDOW_UPDATE increment is in [1, 2^31-1].
6007 // Zero was short-circuited above; this asserts the masked value is a
6008 // legal positive increment before we add it to a window.
6009 debug_assert!(
6010 increment > 0,
6011 "WINDOW_UPDATE increment must be strictly positive at this point (zero handled above)"
6012 );
6013 if stream_id == 0 {
6014 // Count connection-level WINDOW_UPDATEs before touching the window
6015 // so a per-window flood stops us before we pay the arithmetic cost
6016 // on a million-frame burst. Zero-increment frames short-circuited
6017 // above, so every increment here is a legal-looking rate consumer.
6018 let wu0_before = self.flood_detector.window_update_stream0_count;
6019 self.flood_detector.window_update_stream0_count = self
6020 .flood_detector
6021 .window_update_stream0_count
6022 .saturating_add(1);
6023 debug_assert!(
6024 self.flood_detector.window_update_stream0_count > wu0_before
6025 || wu0_before == u32::MAX,
6026 "stream-0 WINDOW_UPDATE flood counter must advance before the flood check"
6027 );
6028 check_flood_or_return!(self);
6029 self.attribute_bytes_to_overhead();
6030 let window_before = self.flow_control.window;
6031 if let Some(window) = self.flow_control.window.checked_add(increment) {
6032 if self.flow_control.window <= 0 && window > 0 {
6033 self.readiness.arm_writable();
6034 }
6035 self.flow_control.window = window;
6036 // Flow-control replenish invariant (RFC 9113 §6.9): the
6037 // connection send window grows by exactly `increment` and stays
6038 // within i32 (the `checked_add` already rejected overflow, which
6039 // is a FLOW_CONTROL_ERROR on the wire). The window may legally
6040 // be negative (a SETTINGS change can shrink it below zero) but
6041 // a WINDOW_UPDATE only ever increases it.
6042 debug_assert_eq!(
6043 self.flow_control.window,
6044 window_before + increment,
6045 "connection window must increase by exactly the increment"
6046 );
6047 debug_assert!(
6048 self.flow_control.window > window_before,
6049 "a positive WINDOW_UPDATE must strictly grow the connection window"
6050 );
6051 debug!(
6052 "{} WINDOW_UPDATE received: stream=0 increment={} new_connection_window={}",
6053 log_context!(self),
6054 increment,
6055 self.flow_control.window
6056 );
6057 } else {
6058 error!("{} INVALID WINDOW INCREMENT", log_context!(self));
6059 return self.goaway(H2Error::FlowControlError);
6060 }
6061 } else if let Some(global_stream_id) = self.streams.get(&stream_id).copied() {
6062 let stream = &mut context.streams[global_stream_id];
6063 self.attribute_bytes_to_stream(&mut stream.metrics);
6064 let stream_window_before = stream.window;
6065 if let Some(window) = stream.window.checked_add(increment) {
6066 if stream.window <= 0 && window > 0 {
6067 self.readiness.arm_writable();
6068 }
6069 stream.window = window;
6070 // Same replenish invariant as the connection window, applied to
6071 // the per-stream send window (RFC 9113 §6.9.1). Overflow past
6072 // 2^31-1 is rejected by `checked_add` and handled as a
6073 // FLOW_CONTROL_ERROR RST_STREAM below.
6074 debug_assert_eq!(
6075 stream.window,
6076 stream_window_before + increment,
6077 "stream window must increase by exactly the increment"
6078 );
6079 debug_assert!(
6080 stream.window > stream_window_before,
6081 "a positive WINDOW_UPDATE must strictly grow the stream window"
6082 );
6083 debug!(
6084 "{} WINDOW_UPDATE received: stream={} increment={} new_stream_window={}",
6085 log_context!(self),
6086 stream_id,
6087 increment,
6088 stream.window
6089 );
6090 } else {
6091 let result = self.reset_stream(
6092 stream_id,
6093 global_stream_id,
6094 context,
6095 endpoint,
6096 H2Error::FlowControlError,
6097 );
6098 self.remove_dead_stream(stream_id, global_stream_id);
6099 return result;
6100 }
6101 } else {
6102 self.attribute_bytes_to_overhead();
6103 trace!(
6104 "{} Ignoring window update on closed stream {}: {}",
6105 log_context!(self),
6106 stream_id,
6107 increment
6108 );
6109 // Pass 3 Low #5: WINDOW_UPDATE on a closed stream is legal
6110 // (RFC 9113 §6.9.1) but has no useful effect, so a peer that
6111 // keeps sending them is wasting our cycles. Count it as a
6112 // glitch so a flood contributes to `check_flood()` and can
6113 // eventually trigger ENHANCE_YOUR_CALM.
6114 self.flood_detector.glitch_count += 1;
6115 check_flood_or_return!(self);
6116 }
6117 MuxResult::Continue
6118 }
6119
6120 fn update_initial_window_size<L>(&mut self, value: u32, context: &mut Context<L>) -> bool
6121 where
6122 L: ListenerHandler + L7ListenerHandler,
6123 {
6124 if value > FLOW_CONTROL_MAX_WINDOW {
6125 return true;
6126 }
6127 let delta = match i32::try_from(
6128 value as i64 - self.peer_settings.settings_initial_window_size as i64,
6129 ) {
6130 Ok(d) => d,
6131 Err(_) => {
6132 error!("{} initial window size delta overflow", log_context!(self));
6133 return true;
6134 }
6135 };
6136 let mut open_window = false;
6137 // Only update windows for streams owned by this connection
6138 for &global_stream_id in self.streams.values() {
6139 let stream = &mut context.streams[global_stream_id];
6140 // RFC 9113 §6.9.2: changes to SETTINGS_INITIAL_WINDOW_SIZE can cause
6141 // stream windows to exceed 2^31-1, which is a flow control error.
6142 match stream.window.checked_add(delta) {
6143 Some(new_window) => {
6144 open_window |= stream.window <= 0 && new_window > 0;
6145 stream.window = new_window;
6146 }
6147 None => return true,
6148 }
6149 }
6150 trace!(
6151 "{} UPDATE INIT WINDOW: {} {} {:?}",
6152 log_context!(self),
6153 delta,
6154 open_window,
6155 self.readiness
6156 );
6157 if open_window {
6158 self.readiness.arm_writable();
6159 }
6160 self.peer_settings.settings_initial_window_size = value;
6161 false
6162 }
6163
6164 pub fn force_disconnect(&mut self) -> MuxResult {
6165 self.state = H2State::Error;
6166 match &mut self.position {
6167 Position::Client(_, _, status) => {
6168 *status = BackendStatus::Disconnecting;
6169 self.readiness.event = Ready::HUP;
6170 debug!(
6171 "{} H2 force_disconnect client: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
6172 log_context!(self),
6173 self.state,
6174 self.streams.len(),
6175 self.expect_write,
6176 self.socket.socket_wants_write(),
6177 self.readiness
6178 );
6179 MuxResult::Continue
6180 }
6181 Position::Server => {
6182 if self.peer_gone_after_final_goaway() {
6183 return MuxResult::CloseSession;
6184 }
6185 // Don't disconnect immediately if rustls still has buffered TLS
6186 // records. Returning CloseSession here triggers shutdown(Write)
6187 // which sends FIN — but any TLS records still in rustls's buffer
6188 // (not yet flushed to the TCP send buffer) are lost, causing the
6189 // client to see "TLS decode error / unexpected eof".
6190 // Instead, keep WRITABLE interest and let the writable path flush.
6191 if self.socket.socket_wants_write() {
6192 debug!(
6193 "{} H2 force_disconnect delaying close: state={:?}, streams={}, expect_write={:?}, wants_write=true, readiness={:?}",
6194 log_context!(self),
6195 self.state,
6196 self.streams.len(),
6197 self.expect_write,
6198 self.readiness
6199 );
6200 self.readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
6201 self.ensure_tls_flushed();
6202 MuxResult::Continue
6203 } else {
6204 debug!(
6205 "{} H2 force_disconnect closing session: state={:?}, streams={}, expect_write={:?}, wants_write=false, readiness={:?}",
6206 log_context!(self),
6207 self.state,
6208 self.streams.len(),
6209 self.expect_write,
6210 self.readiness
6211 );
6212 MuxResult::CloseSession
6213 }
6214 }
6215 }
6216 }
6217
6218 pub fn close<E, L>(&mut self, context: &mut Context<L>, mut endpoint: E)
6219 where
6220 E: Endpoint,
6221 L: ListenerHandler + L7ListenerHandler,
6222 {
6223 match self.position {
6224 Position::Client(_, _, BackendStatus::KeepAlive) => {
6225 error!(
6226 "{} H2 connections do not use KeepAlive backend status",
6227 log_context!(self)
6228 );
6229 return;
6230 }
6231 Position::Client(..) => {}
6232 Position::Server => {
6233 let tls_pending_before = self.socket.socket_wants_write();
6234 if !self.streams.is_empty() || tls_pending_before || self.expect_write.is_some() {
6235 debug!(
6236 "{} H2 close with active state: state={:?}, streams={}, expect_write={:?}, wants_write={}, readiness={:?}",
6237 log_context!(self),
6238 self.state,
6239 self.streams.len(),
6240 self.expect_write,
6241 tls_pending_before,
6242 self.readiness
6243 );
6244 for (stream_id, global_stream_id) in &self.streams {
6245 let stream = &context.streams[*global_stream_id];
6246 debug!(
6247 "{} close stream id={} gid={}: state={:?}, front_eos={}, back_eos={}, front_phase={:?}, back_phase={:?}, front_completed={}, back_completed={}",
6248 log_context!(self),
6249 stream_id,
6250 global_stream_id,
6251 stream.state,
6252 stream.front_received_end_of_stream,
6253 stream.back_received_end_of_stream,
6254 stream.front.parsing_phase,
6255 stream.back.parsing_phase,
6256 stream.front.is_completed(),
6257 stream.back.is_completed()
6258 );
6259 }
6260 }
6261 if !self.close_notify_sent {
6262 trace!("{} H2 SENDING CLOSE NOTIFY", log_context!(self));
6263 }
6264 let (tls_pending_after, drain_rounds) =
6265 drain_tls_close_notify(&mut self.socket, &mut self.close_notify_sent);
6266 if tls_pending_after {
6267 // Severity tiering: key on stream-count + close-state, not
6268 // peer-vs-operator. Composes with the send-side `H2Error`
6269 // variant tier in `goaway()` — both rules demote benign
6270 // paths and keep loss-bearing paths loud.
6271 //
6272 // - `streams != 0` -> `error!`: live streams at
6273 // close time, response-byte loss is possible.
6274 // - `streams == 0` AND state in {GoAway, Error}
6275 // -> `warn!`: idle close after
6276 // a GOAWAY exchange (peer-initiated abort or our own
6277 // graceful drain). What's stranded is best-effort
6278 // GOAWAY/close_notify; no application data was queued.
6279 // - `streams == 0` from any other state
6280 // -> `error!`: unexpected
6281 // teardown path (no GOAWAY exchange) — keep loud so
6282 // unknown failure modes surface.
6283 if !self.streams.is_empty() {
6284 error!(
6285 "{} TLS buffer NOT fully drained on close: \
6286 pending_before={}, pending_after={}, drain_rounds={}, \
6287 state={:?}, streams={}, expect_write={:?}, \
6288 close_notify_sent={}, readiness={:?}",
6289 log_context!(self),
6290 tls_pending_before,
6291 tls_pending_after,
6292 drain_rounds,
6293 self.state,
6294 self.streams.len(),
6295 self.expect_write,
6296 self.close_notify_sent,
6297 self.readiness
6298 );
6299 } else if matches!(self.state, H2State::GoAway | H2State::Error) {
6300 warn!(
6301 "{} TLS buffer NOT fully drained on close: \
6302 pending_before={}, pending_after={}, drain_rounds={}, \
6303 state={:?}, streams={}, expect_write={:?}, \
6304 close_notify_sent={}, readiness={:?}",
6305 log_context!(self),
6306 tls_pending_before,
6307 tls_pending_after,
6308 drain_rounds,
6309 self.state,
6310 self.streams.len(),
6311 self.expect_write,
6312 self.close_notify_sent,
6313 self.readiness
6314 );
6315 } else {
6316 error!(
6317 "{} TLS buffer NOT fully drained on close: \
6318 pending_before={}, pending_after={}, drain_rounds={}, \
6319 state={:?}, streams={}, expect_write={:?}, \
6320 close_notify_sent={}, readiness={:?}",
6321 log_context!(self),
6322 tls_pending_before,
6323 tls_pending_after,
6324 drain_rounds,
6325 self.state,
6326 self.streams.len(),
6327 self.expect_write,
6328 self.close_notify_sent,
6329 self.readiness
6330 );
6331 }
6332 }
6333 return;
6334 }
6335 }
6336 // reconnection is handled by the server for each stream separately
6337 for global_stream_id in self.streams.values() {
6338 trace!("{} end stream: {}", log_context!(self), global_stream_id);
6339 if let StreamState::Linked(token) = context.streams[*global_stream_id].state {
6340 endpoint.end_stream(token, *global_stream_id, context);
6341 }
6342 }
6343 }
6344
6345 /// Reset a stream: tear down kawa state, emit `RST_STREAM` on the wire,
6346 /// and record MadeYouReset accounting.
6347 ///
6348 /// `wire_stream_id` is the on-wire `StreamId`; `stream_id` is the internal
6349 /// `GlobalStreamId` slot. Callers already carry both so we pass them
6350 /// explicitly rather than scanning `self.streams`. The wire id is threaded
6351 /// into [`Self::enqueue_rst`] which queues the frame for serialisation in
6352 /// [`Self::flush_pending_control_frames`] on the next writable tick —
6353 /// independent of whether the caller immediately evicts the slot via
6354 /// `remove_dead_stream` (which they usually do). This is what guarantees
6355 /// the RST reaches the peer for malformed HEADERS / flow-control /
6356 /// content-length violations flagged by h2spec 2.0.
6357 pub fn reset_stream<E, L>(
6358 &mut self,
6359 wire_stream_id: StreamId,
6360 stream_id: GlobalStreamId,
6361 context: &mut Context<L>,
6362 mut endpoint: E,
6363 error: H2Error,
6364 ) -> MuxResult
6365 where
6366 E: Endpoint,
6367 L: ListenerHandler + L7ListenerHandler,
6368 {
6369 // Compute totals before taking mutable borrows on the target stream.
6370 let reset_byte_totals = self.compute_stream_byte_totals(context);
6371 context.unlink_stream(stream_id);
6372 let stream = &mut context.streams[stream_id];
6373 trace!(
6374 "{} reset H2 stream {}: {:#?}",
6375 log_context!(self),
6376 stream_id,
6377 stream.context
6378 );
6379 let old_state = std::mem::replace(&mut stream.state, StreamState::Unlinked);
6380 forcefully_terminate_answer(stream, &mut self.readiness, error);
6381 let linked_token = if let StreamState::Linked(token) = old_state {
6382 Some(token)
6383 } else {
6384 None
6385 };
6386 let (client_rtt, server_rtt) =
6387 Self::snapshot_rtts(&self.position, &self.socket, &endpoint, linked_token);
6388 if let Some(token) = linked_token {
6389 endpoint.end_stream(token, stream_id, context);
6390 }
6391 // Emit access log for server-side resets on streams that had active requests
6392 if self.position.is_server()
6393 && matches!(old_state, StreamState::Link | StreamState::Linked(_))
6394 {
6395 let stream = &mut context.streams[stream_id];
6396 self.distribute_overhead(&mut stream.metrics, reset_byte_totals);
6397 stream.metrics.backend_stop();
6398 stream.generate_access_log(
6399 true,
6400 Some("H2::Reset"),
6401 context.listener.clone(),
6402 client_rtt,
6403 server_rtt,
6404 );
6405 stream.metrics.reset();
6406 }
6407 // Queue the RST for wire emission. Independent of the owning stream
6408 // remaining in `self.streams` — callers typically follow this with
6409 // `remove_dead_stream`, which would otherwise evict the slot before
6410 // `write_streams` could run `kawa.prepare` against the converter.
6411 //
6412 // `enqueue_rst` performs every accounting side-effect at queue
6413 // time (per-error counter, global tx counter, CVE-2025-8671
6414 // MadeYouReset lifetime cap). Graceful `NoError` cancels —
6415 // stream recycle, propagated client-side cancel — are exempt
6416 // from the lifetime cap inside the accounting helper itself.
6417 if let Some(result) = self.enqueue_rst(wire_stream_id, error) {
6418 return result;
6419 }
6420 MuxResult::Continue
6421 }
6422
6423 pub fn end_stream<L>(&mut self, stream_gid: GlobalStreamId, context: &mut Context<L>)
6424 where
6425 L: ListenerHandler + L7ListenerHandler,
6426 {
6427 context.unlink_stream(stream_gid);
6428 let stream_context = context.http_context(stream_gid);
6429 trace!(
6430 "{} end H2 stream {}: {:#?}",
6431 log_context!(self),
6432 stream_gid,
6433 stream_context
6434 );
6435 match self.position {
6436 Position::Client(..) => {
6437 // Resolve the wire StreamId for this gid up front so the
6438 // subsequent cleanup does not hold an iterator borrow on
6439 // `self.streams` while also mutating it.
6440 let wire_stream_id = self
6441 .streams
6442 .iter()
6443 .find_map(|(&sid, &gid)| (gid == stream_gid).then_some(sid));
6444 if let Some(id) = wire_stream_id {
6445 // Only send RST_STREAM if the stream hasn't fully completed.
6446 // If both request and response are terminated, the stream is
6447 // already in "closed" state (RFC 9113 §5.1) — sending RST_STREAM
6448 // on a closed stream would be a protocol error that could cause
6449 // the H2 peer to close the entire connection.
6450 let stream = &context.streams[stream_gid];
6451 let fully_completed =
6452 stream.back_received_end_of_stream && stream.front.is_terminated();
6453 if !fully_completed && !self.rst_sent.contains(&id) {
6454 let kawa = &mut self.zero;
6455 let mut frame = [0; 13];
6456 if let Ok((_, _size)) =
6457 serializer::gen_rst_stream(&mut frame, id, H2Error::Cancel)
6458 {
6459 let buf = kawa.storage.space();
6460 if buf.len() >= frame.len() {
6461 buf[..frame.len()].copy_from_slice(&frame);
6462 kawa.storage.fill(frame.len());
6463 incr!(names::h2::FRAMES_TX_RST_STREAM);
6464 count!(metric_for_rst_stream_sent(H2Error::Cancel), 1);
6465 self.readiness.arm_writable();
6466 self.rst_sent.insert(id);
6467 }
6468 }
6469 }
6470 // Retire the stream and invalidate expect_write/expect_read
6471 // if they still reference this gid — the slot may be popped
6472 // by `shrink_trailing_recycle` on the next create_stream.
6473 self.remove_dead_stream(id, stream_gid);
6474 if context.streams[stream_gid].state != StreamState::Recycle {
6475 context.streams[stream_gid].state = StreamState::Unlinked;
6476 }
6477 return;
6478 }
6479 error!(
6480 "{} end_stream called for unknown global_stream_id {}",
6481 log_context!(self),
6482 stream_gid
6483 );
6484 }
6485 Position::Server => {
6486 let answers_rc = context.listener.borrow().get_answers().clone();
6487 let stream = &mut context.streams[stream_gid];
6488 match end_stream_decision(stream) {
6489 EndStreamAction::ForwardTerminated => {
6490 #[cfg(debug_assertions)]
6491 context
6492 .debug
6493 .push(DebugEvent::Str(format!("Close terminated {stream_gid}")));
6494 debug!(
6495 "{} CLOSING H2 TERMINATED STREAM {} {:?}",
6496 log_context!(self),
6497 stream_gid,
6498 stream
6499 );
6500 stream.state = StreamState::Unlinked;
6501 self.readiness.arm_writable();
6502 context.debug.set_interesting(true);
6503 }
6504 EndStreamAction::CloseDelimited => {
6505 debug!(
6506 "{} CLOSE DELIMITED H2 STREAM {} {:?}",
6507 log_context!(self),
6508 stream_gid,
6509 stream
6510 );
6511 stream.back.push_block(kawa::Block::Flags(kawa::Flags {
6512 end_body: true,
6513 end_chunk: false,
6514 end_header: false,
6515 end_stream: true,
6516 }));
6517 stream.back.parsing_phase = kawa::ParsingPhase::Terminated;
6518 stream.state = StreamState::Unlinked;
6519 self.readiness.arm_writable();
6520 context.debug.set_interesting(true);
6521 }
6522 EndStreamAction::ForwardUnterminated => {
6523 #[cfg(debug_assertions)]
6524 context
6525 .debug
6526 .push(DebugEvent::Str(format!("Close unterminated {stream_gid}")));
6527 debug!(
6528 "{} CLOSING H2 UNTERMINATED STREAM {} {:?}",
6529 log_context!(self),
6530 stream_gid,
6531 stream
6532 );
6533 forcefully_terminate_answer(
6534 stream,
6535 &mut self.readiness,
6536 H2Error::InternalError,
6537 );
6538 context.debug.set_interesting(true);
6539 }
6540 EndStreamAction::SendDefault(status) => {
6541 #[cfg(debug_assertions)]
6542 context.debug.push(DebugEvent::Str(format!(
6543 "Can't retry, send {status} on {stream_gid}"
6544 )));
6545 let answers = answers_rc.borrow();
6546 set_default_answer(stream, &mut self.readiness, status, &answers);
6547 }
6548 EndStreamAction::Reconnect => {
6549 debug!("{} H2 RECONNECT", log_context!(self));
6550 #[cfg(debug_assertions)]
6551 context
6552 .debug
6553 .push(DebugEvent::Str(format!("Retry {stream_gid}")));
6554 stream.state = StreamState::Link;
6555 context.pending_links.push_back(stream_gid);
6556 }
6557 }
6558 }
6559 }
6560 }
6561
6562 pub fn start_stream<L>(&mut self, stream: GlobalStreamId, _context: &mut Context<L>) -> bool
6563 where
6564 L: ListenerHandler + L7ListenerHandler,
6565 {
6566 // RFC 9113 §6.8: reject new streams on a draining connection
6567 if self.drain.draining {
6568 error!(
6569 "{} Cannot open new stream on draining connection (stream {})",
6570 log_context!(self),
6571 stream
6572 );
6573 return false;
6574 }
6575 // RFC 9113 §5.1.2: respect peer's max concurrent streams limit
6576 if self.streams.len() >= self.peer_settings.settings_max_concurrent_streams as usize {
6577 error!(
6578 "{} Cannot open new stream: active={} >= peer max_concurrent_streams={}",
6579 log_context!(self),
6580 self.streams.len(),
6581 self.peer_settings.settings_max_concurrent_streams
6582 );
6583 return false;
6584 }
6585 trace!(
6586 "{} start new H2 stream {} {:?}",
6587 log_context!(self),
6588 stream,
6589 self.readiness
6590 );
6591 let Some(stream_id) = self.new_stream_id() else {
6592 // Pass 4 Medium #5: the client-initiated stream-ID space
6593 // (31 bits, odd only) is exhausted. The backend is now useless
6594 // for new requests — gracefully drain it. Without this
6595 // transition, the Connection lingers in `Connected` state and
6596 // every subsequent request returns 503 because `start_stream`
6597 // keeps returning false.
6598 //
6599 // The session envelope is hoisted to a local because the
6600 // `match &mut self.position` below holds a mutable borrow on
6601 // `self.position`, and `log_context!(self)` reads that field
6602 // for its `position={...}` slot — calling the macro inside the
6603 // match arms would conflict with the active borrow. The
6604 // bidirectional regression guard in `lib/tests/log_layout.rs`
6605 // (and the matching scanner in `lib/build.rs`) recognises this
6606 // shape by scanning backward as well as forward from each log
6607 // call.
6608 let context = log_context!(self);
6609 match &mut self.position {
6610 Position::Client(cluster_id, backend, status) => {
6611 let backend_addr = backend.borrow().address;
6612 let cluster = cluster_id.clone();
6613 info!(
6614 "{} H2 backend stream IDs exhausted (cluster={}, backend={:?}) — draining",
6615 context, cluster, backend_addr
6616 );
6617 *status = BackendStatus::Disconnecting;
6618 }
6619 Position::Server => {
6620 error!(
6621 "{} H2 server stream IDs exhausted — sending graceful GOAWAY",
6622 context
6623 );
6624 }
6625 }
6626 self.graceful_goaway();
6627 return false;
6628 };
6629 self.streams.insert(stream_id, stream);
6630 self.stream_last_activity_at
6631 .insert(stream_id, Instant::now());
6632 self.readiness.arm_writable();
6633 true
6634 }
6635}
6636
6637#[cfg(test)]
6638mod tests {
6639 use std::{cell::RefCell, rc::Rc};
6640
6641 use super::*;
6642 use crate::{pool::Pool, protocol::kawa_h1::editor::HttpContext};
6643
6644 // ── H2FloodDetector ──────────────────────────────────────────────────
6645
6646 #[test]
6647 fn test_flood_detector_no_flood_below_threshold() {
6648 let config = H2FloodConfig::default();
6649 let mut detector = H2FloodDetector::new(config);
6650
6651 // All counters at zero -> no flood
6652 assert!(detector.check_flood().is_none());
6653
6654 // Increment each counter to exactly the threshold (not exceeding)
6655 detector.rst_stream_count = config.max_rst_stream_per_window;
6656 detector.ping_count = config.max_ping_per_window;
6657 detector.settings_count = config.max_settings_per_window;
6658 detector.empty_data_count = config.max_empty_data_per_window;
6659 detector.continuation_count = config.max_continuation_frames;
6660 detector.glitch_count = config.max_glitch_count;
6661 // At threshold but not exceeding -> no flood
6662 assert!(detector.check_flood().is_none());
6663 }
6664
6665 #[test]
6666 fn test_flood_detector_detects_rapid_reset() {
6667 let config = H2FloodConfig::default();
6668 let mut detector = H2FloodDetector::new(config);
6669
6670 detector.rst_stream_count = config.max_rst_stream_per_window + 1;
6671 assert!(matches!(
6672 detector.check_flood(),
6673 Some(H2FloodViolation {
6674 error: H2Error::EnhanceYourCalm,
6675 ..
6676 })
6677 ));
6678 }
6679
6680 #[test]
6681 fn test_flood_detector_detects_ping_flood() {
6682 let config = H2FloodConfig::default();
6683 let mut detector = H2FloodDetector::new(config);
6684
6685 detector.ping_count = config.max_ping_per_window + 1;
6686 assert!(matches!(
6687 detector.check_flood(),
6688 Some(H2FloodViolation {
6689 error: H2Error::EnhanceYourCalm,
6690 ..
6691 })
6692 ));
6693 }
6694
6695 #[test]
6696 fn test_flood_detector_detects_settings_flood() {
6697 let config = H2FloodConfig::default();
6698 let mut detector = H2FloodDetector::new(config);
6699
6700 detector.settings_count = config.max_settings_per_window + 1;
6701 assert!(matches!(
6702 detector.check_flood(),
6703 Some(H2FloodViolation {
6704 error: H2Error::EnhanceYourCalm,
6705 ..
6706 })
6707 ));
6708 }
6709
6710 #[test]
6711 fn test_flood_detector_detects_empty_data_flood() {
6712 let config = H2FloodConfig::default();
6713 let mut detector = H2FloodDetector::new(config);
6714
6715 detector.empty_data_count = config.max_empty_data_per_window + 1;
6716 assert!(matches!(
6717 detector.check_flood(),
6718 Some(H2FloodViolation {
6719 error: H2Error::EnhanceYourCalm,
6720 ..
6721 })
6722 ));
6723 }
6724
6725 #[test]
6726 fn test_flood_detector_detects_continuation_flood() {
6727 let config = H2FloodConfig::default();
6728 let mut detector = H2FloodDetector::new(config);
6729
6730 detector.continuation_count = config.max_continuation_frames + 1;
6731 assert!(matches!(
6732 detector.check_flood(),
6733 Some(H2FloodViolation {
6734 error: H2Error::EnhanceYourCalm,
6735 ..
6736 })
6737 ));
6738 }
6739
6740 #[test]
6741 fn test_flood_detector_detects_header_size_flood() {
6742 let config = H2FloodConfig::default();
6743 let mut detector = H2FloodDetector::new(config);
6744
6745 detector.accumulated_header_size = MAX_HEADER_LIST_SIZE as u32 + 1;
6746 assert!(matches!(
6747 detector.check_flood(),
6748 Some(H2FloodViolation {
6749 error: H2Error::EnhanceYourCalm,
6750 ..
6751 })
6752 ));
6753 }
6754
6755 #[test]
6756 fn test_flood_detector_detects_glitch_flood() {
6757 let config = H2FloodConfig::default();
6758 let mut detector = H2FloodDetector::new(config);
6759
6760 detector.glitch_count = config.max_glitch_count + 1;
6761 assert!(matches!(
6762 detector.check_flood(),
6763 Some(H2FloodViolation {
6764 error: H2Error::EnhanceYourCalm,
6765 ..
6766 })
6767 ));
6768 }
6769
6770 #[test]
6771 fn test_flood_detector_custom_thresholds() {
6772 let config = H2FloodConfig {
6773 max_rst_stream_per_window: 5,
6774 max_ping_per_window: 10,
6775 max_settings_per_window: 3,
6776 max_empty_data_per_window: 8,
6777 max_continuation_frames: 2,
6778 max_glitch_count: 15,
6779 ..H2FloodConfig::default()
6780 };
6781 let mut detector = H2FloodDetector::new(config);
6782
6783 // Below custom threshold -> no flood
6784 detector.rst_stream_count = 5;
6785 assert!(detector.check_flood().is_none());
6786
6787 // Above custom threshold -> flood
6788 detector.rst_stream_count = 6;
6789 assert!(matches!(
6790 detector.check_flood(),
6791 Some(H2FloodViolation {
6792 error: H2Error::EnhanceYourCalm,
6793 ..
6794 })
6795 ));
6796 }
6797
6798 #[test]
6799 fn test_flood_detector_reset_continuation() {
6800 let config = H2FloodConfig::default();
6801 let mut detector = H2FloodDetector::new(config);
6802
6803 detector.continuation_count = 15;
6804 detector.accumulated_header_size = 30000;
6805
6806 detector.reset_continuation();
6807
6808 assert_eq!(detector.continuation_count, 0);
6809 assert_eq!(detector.accumulated_header_size, 0);
6810 }
6811
6812 #[test]
6813 fn test_flood_detector_half_decay_on_window_expiry() {
6814 let config = H2FloodConfig::default();
6815 let mut detector = H2FloodDetector::new(config);
6816
6817 detector.rst_stream_count = 80;
6818 detector.ping_count = 60;
6819 detector.settings_count = 40;
6820 detector.empty_data_count = 20;
6821 detector.window_update_stream0_count = 90;
6822 detector.glitch_count = 50;
6823
6824 // Force window expiry by setting window_start to the past
6825 detector.window_start = Instant::now() - FLOOD_WINDOW_DURATION;
6826
6827 // check_flood calls maybe_reset_window which halves counters
6828 let _ = detector.check_flood();
6829
6830 assert_eq!(detector.rst_stream_count, 40);
6831 assert_eq!(detector.ping_count, 30);
6832 assert_eq!(detector.settings_count, 20);
6833 assert_eq!(detector.empty_data_count, 10);
6834 assert_eq!(detector.window_update_stream0_count, 45);
6835 assert_eq!(detector.glitch_count, 25);
6836 }
6837
6838 #[test]
6839 fn test_flood_detector_window_update_stream0_trips_at_threshold() {
6840 let config = H2FloodConfig {
6841 max_window_update_stream0_per_window: 5,
6842 ..H2FloodConfig::default()
6843 };
6844 let mut detector = H2FloodDetector::new(config);
6845
6846 // At threshold — no flood yet (strict greater-than, matches existing counters).
6847 detector.window_update_stream0_count = 5;
6848 assert!(detector.check_flood().is_none());
6849
6850 // Above threshold — flood with the correct violation reason + metric key.
6851 detector.window_update_stream0_count = 6;
6852 let violation = detector
6853 .check_flood()
6854 .expect("WINDOW_UPDATE stream-0 flood must trip above threshold");
6855 assert_eq!(violation.error, H2Error::EnhanceYourCalm);
6856 assert_eq!(violation.reason, "WINDOW_UPDATE stream 0");
6857 assert_eq!(
6858 violation.metric_key,
6859 "h2.flood.violation.window_update_stream0_window"
6860 );
6861 assert_eq!(violation.count, 6);
6862 assert_eq!(violation.threshold, 5);
6863 }
6864
6865 #[test]
6866 fn test_flood_detector_window_update_stream0_honours_default() {
6867 // Default threshold must match the documented constant so operators
6868 // can reason about behaviour without reading code.
6869 let detector = H2FloodDetector::default();
6870 assert_eq!(
6871 detector.config.max_window_update_stream0_per_window,
6872 DEFAULT_MAX_WINDOW_UPDATE_STREAM0_PER_WINDOW
6873 );
6874 assert_eq!(detector.window_update_stream0_count, 0);
6875 }
6876
6877 #[test]
6878 fn test_flood_detector_decay_prevents_flood() {
6879 let config = H2FloodConfig {
6880 max_rst_stream_per_window: 10,
6881 ..H2FloodConfig::default()
6882 };
6883 let mut detector = H2FloodDetector::new(config);
6884
6885 // Set counter just above threshold
6886 detector.rst_stream_count = 12;
6887
6888 // Without decay -> flood
6889 assert!(matches!(
6890 detector.check_flood(),
6891 Some(H2FloodViolation {
6892 error: H2Error::EnhanceYourCalm,
6893 ..
6894 })
6895 ));
6896
6897 // Reset and simulate window expiry
6898 detector.rst_stream_count = 12;
6899 detector.window_start = Instant::now() - FLOOD_WINDOW_DURATION;
6900
6901 // After decay: 12/2 = 6, which is below threshold 10 -> no flood
6902 assert!(detector.check_flood().is_none());
6903 }
6904
6905 #[test]
6906 fn test_flood_detector_lifetime_rst_cap_triggers_enhance_your_calm() {
6907 // CVE-2023-44487 Rapid Reset: a patient attacker that stays under
6908 // the half-decaying per-window threshold must still be stopped by
6909 // the lifetime cap. Simulate a response-started RST (no abusive
6910 // counter bump) so only the lifetime ceiling is tested.
6911 let mut detector = H2FloodDetector::default();
6912 for _ in 0..DEFAULT_MAX_RST_STREAM_LIFETIME {
6913 assert!(detector.record_rst_lifetime(true).is_none());
6914 }
6915 assert_eq!(
6916 detector.total_rst_received_lifetime,
6917 DEFAULT_MAX_RST_STREAM_LIFETIME
6918 );
6919 assert_eq!(detector.total_abusive_rst_received_lifetime, 0);
6920 // Next RST crosses the ceiling.
6921 assert!(matches!(
6922 detector.record_rst_lifetime(true),
6923 Some(H2FloodViolation {
6924 error: H2Error::EnhanceYourCalm,
6925 ..
6926 })
6927 ));
6928 }
6929
6930 #[test]
6931 fn test_flood_detector_abusive_rst_cap_triggers_first() {
6932 // Pre-response-start RSTs have a much lower ceiling; they trip
6933 // well before the generic lifetime cap.
6934 let mut detector = H2FloodDetector::default();
6935 for _ in 0..DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME {
6936 assert!(detector.record_rst_lifetime(false).is_none());
6937 }
6938 assert_eq!(
6939 detector.total_abusive_rst_received_lifetime,
6940 DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME
6941 );
6942 assert!(matches!(
6943 detector.record_rst_lifetime(false),
6944 Some(H2FloodViolation {
6945 error: H2Error::EnhanceYourCalm,
6946 ..
6947 })
6948 ));
6949 }
6950
6951 #[test]
6952 fn test_flood_detector_emitted_rst_below_threshold_is_clean() {
6953 // Server may legitimately RST some streams (protocol errors,
6954 // client-side abuse caught by other mitigations). Staying at the
6955 // threshold must not trip the ceiling.
6956 let mut detector = H2FloodDetector::default();
6957 for _ in 0..DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME {
6958 assert!(detector.record_rst_emitted().is_none());
6959 }
6960 assert_eq!(
6961 detector.total_rst_streams_emitted_lifetime,
6962 DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME
6963 );
6964 }
6965
6966 #[test]
6967 fn test_flood_detector_emitted_rst_cap_triggers_made_you_reset() {
6968 // CVE-2025-8671 MadeYouReset: unbounded server-emitted RST_STREAM is
6969 // a DoS vector equivalent to Rapid Reset with the emission direction
6970 // flipped. Crossing the ceiling must surface a EnhanceYourCalm
6971 // violation so the caller can GOAWAY.
6972 let mut detector = H2FloodDetector::default();
6973 for _ in 0..DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME {
6974 assert!(detector.record_rst_emitted().is_none());
6975 }
6976 let violation = detector
6977 .record_rst_emitted()
6978 .expect("emitting past the cap should produce a violation");
6979 assert!(matches!(
6980 violation,
6981 H2FloodViolation {
6982 error: H2Error::EnhanceYourCalm,
6983 reason: "MadeYouReset: lifetime server-emitted RST_STREAM",
6984 ..
6985 }
6986 ));
6987 assert_eq!(violation.count, DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME + 1);
6988 assert_eq!(violation.threshold, DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME);
6989 }
6990
6991 #[test]
6992 fn test_flood_detector_emitted_rst_counter_does_not_decay() {
6993 // Unlike the windowed rst_stream_count, the emitted lifetime counter
6994 // is strictly monotonic — a patient attacker cannot reset it by
6995 // waiting out a window. maybe_reset_window must NOT touch it.
6996 let mut detector = H2FloodDetector::default();
6997 for _ in 0..10 {
6998 detector.record_rst_emitted();
6999 }
7000 detector.window_start = Instant::now() - FLOOD_WINDOW_DURATION;
7001 // Force a window reset through check_flood.
7002 let _ = detector.check_flood();
7003 assert_eq!(detector.total_rst_streams_emitted_lifetime, 10);
7004 }
7005
7006 /// Every violation kind must carry a metric_key under the agreed
7007 /// `h2.flood.violation.*` namespace, and the keys must be unique. The
7008 /// statsd counter at `handle_flood_violation` reads `violation.metric_key`
7009 /// directly — drift between the construction site and the metric name
7010 /// would silently lose alerting on a CVE mitigation.
7011 #[test]
7012 fn test_flood_violation_metric_keys_are_unique_and_namespaced() {
7013 // Helper: run `record_rst_lifetime` until it trips, returning the metric_key.
7014 fn key_from_rst_lifetime(response_started: bool) -> &'static str {
7015 let mut detector = H2FloodDetector::default();
7016 loop {
7017 if let Some(v) = detector.record_rst_lifetime(response_started) {
7018 return v.metric_key;
7019 }
7020 }
7021 }
7022
7023 // Helper: run `record_rst_emitted` until it trips, returning the metric_key.
7024 fn key_from_rst_emitted() -> &'static str {
7025 let mut detector = H2FloodDetector::default();
7026 loop {
7027 if let Some(v) = detector.record_rst_emitted() {
7028 return v.metric_key;
7029 }
7030 }
7031 }
7032
7033 // Helper: drive a single `check_flood` counter past its threshold.
7034 fn key_from_check_flood(setup: impl FnOnce(&mut H2FloodDetector)) -> &'static str {
7035 let mut detector = H2FloodDetector::default();
7036 setup(&mut detector);
7037 detector
7038 .check_flood()
7039 .expect("setup should always trip a flood")
7040 .metric_key
7041 }
7042
7043 let keys: [&'static str; 12] = [
7044 // Lifetime methods on the detector itself.
7045 key_from_rst_lifetime(true),
7046 key_from_rst_lifetime(false),
7047 key_from_rst_emitted(),
7048 // `check_flood` arms.
7049 key_from_check_flood(|d| d.rst_stream_count = u32::MAX),
7050 key_from_check_flood(|d| d.ping_count = u32::MAX),
7051 key_from_check_flood(|d| d.total_ping_received_lifetime = u32::MAX),
7052 key_from_check_flood(|d| d.settings_count = u32::MAX),
7053 key_from_check_flood(|d| d.total_settings_received_lifetime = u32::MAX),
7054 key_from_check_flood(|d| d.empty_data_count = u32::MAX),
7055 key_from_check_flood(|d| d.continuation_count = u32::MAX),
7056 key_from_check_flood(|d| d.accumulated_header_size = u32::MAX),
7057 key_from_check_flood(|d| d.glitch_count = u32::MAX),
7058 ];
7059
7060 for key in keys {
7061 assert!(
7062 key.starts_with("h2.flood.violation."),
7063 "metric key {key} is missing the h2.flood.violation. prefix",
7064 );
7065 }
7066 let mut deduped = keys.to_vec();
7067 deduped.sort_unstable();
7068 deduped.dedup();
7069 assert_eq!(
7070 deduped.len(),
7071 keys.len(),
7072 "metric keys must be unique across violation kinds; collisions: {keys:?}",
7073 );
7074 }
7075
7076 /// All four `metric_for_*` helpers must yield distinct, namespaced keys for
7077 /// every RFC 9113 §7 error code. The macro behind them uses `concat!`, so a
7078 /// new H2Error variant fails the build inside the macro — but a typo in
7079 /// the helper prefix would silently land. Walk every (direction × kind)
7080 /// pair and dedupe the set.
7081 /// `h2_frame_rx_metric_key` must yield a distinct `&'static str` per
7082 /// `Frame::*` variant. The single dispatch site in `handle_frame` reads
7083 /// from this helper, so a typo or duplicate would silently clobber the
7084 /// frame-mix dashboard. Asserting the literal set lets us compare against
7085 /// `doc/configure.md` and the RFC 9113 §6 frame catalogue without
7086 /// reconstructing every Frame variant in the test.
7087 #[test]
7088 fn test_h2_frame_rx_metric_keys_are_unique_and_namespaced() {
7089 // Update this list whenever a new Frame variant is added — the helper
7090 // match is also exhaustive, so the build will already break there
7091 // before anyone notices the test missing a key.
7092 let expected: [&'static str; 11] = [
7093 "h2.frames.rx.data",
7094 "h2.frames.rx.headers",
7095 "h2.frames.rx.push_promise",
7096 "h2.frames.rx.priority",
7097 "h2.frames.rx.rst_stream",
7098 "h2.frames.rx.settings",
7099 "h2.frames.rx.ping",
7100 "h2.frames.rx.goaway",
7101 "h2.frames.rx.window_update",
7102 "h2.frames.rx.continuation",
7103 "h2.frames.rx.unknown",
7104 ];
7105
7106 for key in expected {
7107 assert!(
7108 key.starts_with("h2.frames.rx."),
7109 "metric key {key} is missing the h2.frames.rx. prefix",
7110 );
7111 }
7112 let mut deduped = expected.to_vec();
7113 deduped.sort_unstable();
7114 deduped.dedup();
7115 assert_eq!(
7116 deduped.len(),
7117 expected.len(),
7118 "frame-rx metric keys must be unique; collisions in: {expected:?}",
7119 );
7120
7121 // Spot-check the helper for the one variant we can construct without
7122 // borrowing into a frame body — `Frame::Unknown(u8)` is just a tag.
7123 assert_eq!(
7124 h2_frame_rx_metric_key(&Frame::Unknown(42)),
7125 "h2.frames.rx.unknown",
7126 );
7127 }
7128
7129 #[test]
7130 fn test_per_error_code_metric_keys_are_unique_and_namespaced() {
7131 const ALL_ERRORS: [H2Error; 14] = [
7132 H2Error::NoError,
7133 H2Error::ProtocolError,
7134 H2Error::InternalError,
7135 H2Error::FlowControlError,
7136 H2Error::SettingsTimeout,
7137 H2Error::StreamClosed,
7138 H2Error::FrameSizeError,
7139 H2Error::RefusedStream,
7140 H2Error::Cancel,
7141 H2Error::CompressionError,
7142 H2Error::ConnectError,
7143 H2Error::EnhanceYourCalm,
7144 H2Error::InadequateSecurity,
7145 H2Error::HTTP11Required,
7146 ];
7147
7148 let mut keys: Vec<&'static str> = Vec::new();
7149 for error in ALL_ERRORS {
7150 let code = error as u32;
7151 keys.push(metric_for_goaway_sent(error));
7152 keys.push(metric_for_goaway_received(code));
7153 keys.push(metric_for_rst_stream_sent(error));
7154 keys.push(metric_for_rst_stream_received(code));
7155 }
7156 // …plus the four `unknown_error` fallbacks for codes outside RFC 9113 §7.
7157 let unknown_code = 0xff;
7158 assert!(H2Error::try_from(unknown_code).is_err());
7159 keys.push(metric_for_goaway_received(unknown_code));
7160 keys.push(metric_for_rst_stream_received(unknown_code));
7161 // …and the dedicated Rapid Reset signature counter.
7162 keys.push(names::h2::RST_STREAM_RECEIVED_PRE_RESPONSE_START);
7163
7164 for key in &keys {
7165 assert!(
7166 key.starts_with("h2.goaway.sent.")
7167 || key.starts_with("h2.goaway.received.")
7168 || key.starts_with("h2.rst_stream.sent.")
7169 || key.starts_with("h2.rst_stream.received."),
7170 "metric key {key} does not match a known per-error-code namespace",
7171 );
7172 }
7173 let mut deduped = keys.clone();
7174 deduped.sort_unstable();
7175 deduped.dedup();
7176 assert_eq!(
7177 deduped.len(),
7178 keys.len(),
7179 "per-error-code metric keys must be unique; collisions in: {keys:?}",
7180 );
7181 }
7182
7183 #[test]
7184 fn test_flood_detector_response_started_rst_not_abusive() {
7185 // When the backend response has begun, the RST is cheap for us
7186 // too — it only bumps the generic lifetime counter.
7187 let mut detector = H2FloodDetector::default();
7188 for _ in 0..(DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME + 100) {
7189 assert!(detector.record_rst_lifetime(true).is_none());
7190 }
7191 assert_eq!(detector.total_abusive_rst_received_lifetime, 0);
7192 assert_eq!(
7193 detector.total_rst_received_lifetime,
7194 DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME + 100
7195 );
7196 }
7197
7198 #[test]
7199 fn test_flood_detector_default_matches_new_default() {
7200 let from_default = H2FloodDetector::default();
7201 let from_new = H2FloodDetector::new(H2FloodConfig::default());
7202
7203 assert_eq!(from_default.rst_stream_count, from_new.rst_stream_count);
7204 assert_eq!(from_default.ping_count, from_new.ping_count);
7205 assert_eq!(from_default.settings_count, from_new.settings_count);
7206 assert_eq!(from_default.empty_data_count, from_new.empty_data_count);
7207 assert_eq!(from_default.continuation_count, from_new.continuation_count);
7208 assert_eq!(
7209 from_default.accumulated_header_size,
7210 from_new.accumulated_header_size
7211 );
7212 assert_eq!(from_default.glitch_count, from_new.glitch_count);
7213 assert_eq!(from_default.config, from_new.config);
7214 }
7215
7216 // ── Prioriser ────────────────────────────────────────────────────────
7217
7218 #[test]
7219 fn test_prioriser_defaults_for_unknown_stream() {
7220 let p = Prioriser::default();
7221 // Unknown stream -> RFC 9218 defaults: urgency 3, incremental false
7222 assert_eq!(p.get(&1), (3, false));
7223 assert_eq!(p.get(&999), (3, false));
7224 }
7225
7226 #[test]
7227 fn test_prioriser_push_rfc9218_and_get() {
7228 let mut p = Prioriser::default();
7229
7230 let invalid = p.push_priority(
7231 1,
7232 parser::PriorityPart::Rfc9218 {
7233 urgency: 0,
7234 incremental: true,
7235 },
7236 );
7237 assert!(!invalid);
7238 assert_eq!(p.get(&1), (0, true));
7239
7240 let invalid = p.push_priority(
7241 3,
7242 parser::PriorityPart::Rfc9218 {
7243 urgency: 7,
7244 incremental: false,
7245 },
7246 );
7247 assert!(!invalid);
7248 assert_eq!(p.get(&3), (7, false));
7249 }
7250
7251 #[test]
7252 fn test_prioriser_urgency_clamped_to_7() {
7253 let mut p = Prioriser::default();
7254
7255 p.push_priority(
7256 1,
7257 parser::PriorityPart::Rfc9218 {
7258 urgency: 255,
7259 incremental: false,
7260 },
7261 );
7262 assert_eq!(p.get(&1), (7, false));
7263 }
7264
7265 #[test]
7266 fn test_prioriser_update_priority() {
7267 let mut p = Prioriser::default();
7268
7269 p.push_priority(
7270 1,
7271 parser::PriorityPart::Rfc9218 {
7272 urgency: 3,
7273 incremental: false,
7274 },
7275 );
7276 assert_eq!(p.get(&1), (3, false));
7277
7278 // Update same stream
7279 p.push_priority(
7280 1,
7281 parser::PriorityPart::Rfc9218 {
7282 urgency: 1,
7283 incremental: true,
7284 },
7285 );
7286 assert_eq!(p.get(&1), (1, true));
7287 }
7288
7289 #[test]
7290 fn test_prioriser_remove() {
7291 let mut p = Prioriser::default();
7292
7293 p.push_priority(
7294 1,
7295 parser::PriorityPart::Rfc9218 {
7296 urgency: 0,
7297 incremental: true,
7298 },
7299 );
7300 assert_eq!(p.get(&1), (0, true));
7301
7302 p.remove(&1);
7303 // After removal, falls back to defaults
7304 assert_eq!(p.get(&1), (3, false));
7305 }
7306
7307 #[test]
7308 fn test_prioriser_rfc7540_self_dependency() {
7309 let mut p = Prioriser::default();
7310
7311 // Self-dependency should return true (invalid)
7312 let invalid = p.push_priority(
7313 5,
7314 parser::PriorityPart::Rfc7540 {
7315 stream_dependency: parser::StreamDependency {
7316 exclusive: false,
7317 stream_id: 5, // same as stream_id
7318 },
7319 weight: 16,
7320 },
7321 );
7322 assert!(invalid);
7323 }
7324
7325 #[test]
7326 fn test_prioriser_rfc7540_valid_dependency() {
7327 let mut p = Prioriser::default();
7328
7329 // Non-self dependency is valid (but ignored for scheduling)
7330 let invalid = p.push_priority(
7331 5,
7332 parser::PriorityPart::Rfc7540 {
7333 stream_dependency: parser::StreamDependency {
7334 exclusive: false,
7335 stream_id: 3, // different stream
7336 },
7337 weight: 16,
7338 },
7339 );
7340 assert!(!invalid);
7341 // Still returns defaults since RFC 7540 priority is ignored
7342 assert_eq!(p.get(&5), (3, false));
7343 }
7344
7345 #[test]
7346 fn test_prioriser_max_entries_cap() {
7347 let mut p = Prioriser::default();
7348
7349 // Fill up to MAX_PRIORITIES
7350 for i in 0..MAX_PRIORITIES as u32 {
7351 let stream_id = i * 2 + 1; // odd stream IDs
7352 p.push_priority(
7353 stream_id,
7354 parser::PriorityPart::Rfc9218 {
7355 urgency: (i % 8) as u8,
7356 incremental: false,
7357 },
7358 );
7359 }
7360
7361 // Next insert for a new stream should be silently rejected
7362 let next_id = (MAX_PRIORITIES as u32) * 2 + 1;
7363 let invalid = p.push_priority(
7364 next_id,
7365 parser::PriorityPart::Rfc9218 {
7366 urgency: 0,
7367 incremental: true,
7368 },
7369 );
7370 assert!(!invalid); // not a protocol error, just silently dropped
7371 assert_eq!(p.get(&next_id), (3, false)); // defaults, not stored
7372 }
7373
7374 #[test]
7375 fn test_prioriser_update_existing_at_cap() {
7376 let mut p = Prioriser::default();
7377
7378 // Fill to cap
7379 for i in 0..MAX_PRIORITIES as u32 {
7380 p.push_priority(
7381 i * 2 + 1,
7382 parser::PriorityPart::Rfc9218 {
7383 urgency: 3,
7384 incremental: false,
7385 },
7386 );
7387 }
7388
7389 // Updating an existing entry should still work even at cap
7390 p.push_priority(
7391 1,
7392 parser::PriorityPart::Rfc9218 {
7393 urgency: 0,
7394 incremental: true,
7395 },
7396 );
7397 assert_eq!(p.get(&1), (0, true));
7398 }
7399
7400 #[test]
7401 fn test_prioriser_guarded_accepts_open_stream() {
7402 let mut p = Prioriser::default();
7403 let mut open: HashMap<StreamId, GlobalStreamId> = HashMap::new();
7404 open.insert(3, 0);
7405 let invalid = p.push_priority_guarded(
7406 3,
7407 parser::PriorityPart::Rfc9218 {
7408 urgency: 1,
7409 incremental: false,
7410 },
7411 7,
7412 &open,
7413 );
7414 assert!(!invalid);
7415 assert_eq!(p.get(&3), (1, false));
7416 }
7417
7418 #[test]
7419 fn test_prioriser_guarded_accepts_idle_lookahead() {
7420 let mut p = Prioriser::default();
7421 let open: HashMap<StreamId, GlobalStreamId> = HashMap::new();
7422 // Just ahead of last_stream_id, within PRIORITY_IDLE_LOOKAHEAD.
7423 let invalid = p.push_priority_guarded(
7424 105,
7425 parser::PriorityPart::Rfc9218 {
7426 urgency: 2,
7427 incremental: true,
7428 },
7429 99,
7430 &open,
7431 );
7432 assert!(!invalid);
7433 assert_eq!(p.get(&105), (2, true));
7434 }
7435
7436 #[test]
7437 fn test_prioriser_guarded_drops_far_future_stream() {
7438 let mut p = Prioriser::default();
7439 let open: HashMap<StreamId, GlobalStreamId> = HashMap::new();
7440 // Beyond the 64-slot lookahead window.
7441 let invalid = p.push_priority_guarded(
7442 1_000_001,
7443 parser::PriorityPart::Rfc9218 {
7444 urgency: 0,
7445 incremental: false,
7446 },
7447 3,
7448 &open,
7449 );
7450 assert!(!invalid); // not a protocol error, just dropped
7451 // Default priority returned — no entry stored.
7452 assert_eq!(p.get(&1_000_001), (DEFAULT_URGENCY, false));
7453 }
7454
7455 #[test]
7456 fn test_prioriser_guarded_drops_closed_past_stream() {
7457 let mut p = Prioriser::default();
7458 let open: HashMap<StreamId, GlobalStreamId> = HashMap::new();
7459 // Past the counter and not open = already closed. Drop.
7460 let invalid = p.push_priority_guarded(
7461 3,
7462 parser::PriorityPart::Rfc9218 {
7463 urgency: 5,
7464 incremental: false,
7465 },
7466 99,
7467 &open,
7468 );
7469 assert!(!invalid);
7470 assert_eq!(p.get(&3), (DEFAULT_URGENCY, false));
7471 }
7472
7473 #[test]
7474 fn test_prioriser_guarded_cannot_flood_with_far_ids() {
7475 // Previously an attacker could pack MAX_PRIORITIES entries by picking
7476 // far-future stream IDs. The guard rejects them before the cap helps.
7477 let mut p = Prioriser::default();
7478 let open: HashMap<StreamId, GlobalStreamId> = HashMap::new();
7479 for delta in 10_000..(10_000 + MAX_PRIORITIES as u32) {
7480 p.push_priority_guarded(
7481 delta,
7482 parser::PriorityPart::Rfc9218 {
7483 urgency: 0,
7484 incremental: false,
7485 },
7486 0,
7487 &open,
7488 );
7489 }
7490 assert_eq!(p.priorities.len(), 0);
7491 }
7492
7493 // ── RFC 9218 §4 round-robin rotation ───────────────────────────────
7494
7495 /// Helper: mark `stream_id` as (urgency, incremental) in the map.
7496 fn set_prio(p: &mut Prioriser, stream_id: StreamId, urgency: u8, incremental: bool) {
7497 p.push_priority(
7498 stream_id,
7499 parser::PriorityPart::Rfc9218 {
7500 urgency,
7501 incremental,
7502 },
7503 );
7504 }
7505
7506 #[test]
7507 fn test_apply_incremental_rotation_all_non_incremental_is_noop() {
7508 // Non-incremental streams keep the existing (urgency, stream_id) sort.
7509 let mut p = Prioriser::default();
7510 set_prio(&mut p, 1, 3, false);
7511 set_prio(&mut p, 3, 3, false);
7512 set_prio(&mut p, 5, 3, false);
7513
7514 let mut buf = vec![1u32, 3, 5];
7515 let count = p.apply_incremental_rotation(&mut buf);
7516 assert_eq!(count, 0);
7517 assert_eq!(buf, vec![1, 3, 5]);
7518 }
7519
7520 #[test]
7521 fn test_apply_incremental_rotation_moves_incremental_to_tail() {
7522 // Within a same-urgency bucket non-incremental must come before
7523 // incremental, each subrange staying ascending.
7524 let mut p = Prioriser::default();
7525 set_prio(&mut p, 1, 3, true);
7526 set_prio(&mut p, 3, 3, false);
7527 set_prio(&mut p, 5, 3, true);
7528 set_prio(&mut p, 7, 3, false);
7529
7530 let mut buf = vec![1u32, 3, 5, 7];
7531 let count = p.apply_incremental_rotation(&mut buf);
7532 assert_eq!(count, 2);
7533 // Non-incremental first (3, 7), then incremental (1, 5) — ascending
7534 // within each subrange before the cursor rotation.
7535 assert_eq!(buf, vec![3, 7, 1, 5]);
7536 }
7537
7538 #[test]
7539 fn test_apply_incremental_rotation_respects_urgency_buckets() {
7540 // Different urgency buckets must not be mixed.
7541 let mut p = Prioriser::default();
7542 set_prio(&mut p, 1, 0, true); // urgent incremental
7543 set_prio(&mut p, 3, 3, false); // default non-incremental
7544 set_prio(&mut p, 5, 3, true); // default incremental
7545 set_prio(&mut p, 7, 5, false); // low-priority non-incremental
7546
7547 // Input is pre-sorted by (urgency, id) as the scheduler does.
7548 let mut buf = vec![1u32, 3, 5, 7];
7549 let count = p.apply_incremental_rotation(&mut buf);
7550 assert_eq!(count, 2);
7551 // Bucket 0: [1] (alone, stays). Bucket 3: [3] non-inc, [5] inc.
7552 // Bucket 5: [7] alone. Cross-bucket order is preserved.
7553 assert_eq!(buf, vec![1, 3, 5, 7]);
7554 }
7555
7556 #[test]
7557 fn test_apply_incremental_rotation_rotates_by_cursor() {
7558 // Three same-urgency incremental streams: cursor advancement shifts
7559 // the bucket so the next pass starts after the previously fired ID.
7560 let mut p = Prioriser::default();
7561 set_prio(&mut p, 1, 3, true);
7562 set_prio(&mut p, 3, 3, true);
7563 set_prio(&mut p, 5, 3, true);
7564
7565 let base = vec![1u32, 3, 5];
7566
7567 // Pass 1: cursor is 0 (initial), so order stays 1, 3, 5.
7568 let mut buf = base.clone();
7569 assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
7570 assert_eq!(buf, vec![1, 3, 5]);
7571 p.advance_incremental_cursor(Some(1));
7572
7573 // Pass 2: cursor is 1, rotate so 3 comes first.
7574 let mut buf = base.clone();
7575 assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
7576 assert_eq!(buf, vec![3, 5, 1]);
7577 p.advance_incremental_cursor(Some(3));
7578
7579 // Pass 3: cursor is 3, rotate so 5 comes first.
7580 let mut buf = base.clone();
7581 assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
7582 assert_eq!(buf, vec![5, 1, 3]);
7583 p.advance_incremental_cursor(Some(5));
7584
7585 // Pass 4: cursor is 5 (largest in bucket), wrap to 1.
7586 let mut buf = base;
7587 assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
7588 assert_eq!(buf, vec![1, 3, 5]);
7589 }
7590
7591 #[test]
7592 fn test_apply_incremental_rotation_cursor_unknown_id() {
7593 // Cursor points at an ID no longer active (stream completed). Rotation
7594 // should still start from the smallest ID greater than the cursor.
7595 let mut p = Prioriser::default();
7596 set_prio(&mut p, 3, 3, true);
7597 set_prio(&mut p, 5, 3, true);
7598 set_prio(&mut p, 7, 3, true);
7599 p.advance_incremental_cursor(Some(4)); // 4 is not in the bucket
7600
7601 let mut buf = vec![3u32, 5, 7];
7602 assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
7603 assert_eq!(buf, vec![5, 7, 3]);
7604 }
7605
7606 #[test]
7607 fn test_apply_incremental_rotation_single_stream_buckets() {
7608 // Single-stream buckets are a degenerate fast path: no reordering.
7609 let mut p = Prioriser::default();
7610 set_prio(&mut p, 1, 1, true);
7611 set_prio(&mut p, 3, 2, false);
7612 set_prio(&mut p, 5, 3, true);
7613
7614 let mut buf = vec![1u32, 3, 5];
7615 let count = p.apply_incremental_rotation(&mut buf);
7616 assert_eq!(count, 2);
7617 assert_eq!(buf, vec![1, 3, 5]);
7618 }
7619
7620 #[test]
7621 fn test_advance_incremental_cursor_none_is_noop() {
7622 // If no incremental stream fires (only non-incremental served), the
7623 // cursor must stay put so fairness is preserved for the next pass.
7624 let mut p = Prioriser::default();
7625 p.advance_incremental_cursor(Some(5));
7626 p.advance_incremental_cursor(None);
7627 assert_eq!(p.incremental_cursor, 5);
7628 }
7629
7630 #[test]
7631 fn test_apply_incremental_rotation_mixed_bucket_with_cursor() {
7632 // Same-urgency bucket with a mix: non-inc served first in ascending
7633 // order, then the incremental tail rotated by cursor.
7634 let mut p = Prioriser::default();
7635 set_prio(&mut p, 1, 3, true);
7636 set_prio(&mut p, 3, 3, false);
7637 set_prio(&mut p, 5, 3, true);
7638 set_prio(&mut p, 7, 3, false);
7639 set_prio(&mut p, 9, 3, true);
7640 p.advance_incremental_cursor(Some(5));
7641
7642 let mut buf = vec![1u32, 3, 5, 7, 9];
7643 let count = p.apply_incremental_rotation(&mut buf);
7644 assert_eq!(count, 3);
7645 // Non-inc (3, 7) first, then incremental rotated: cursor 5 means
7646 // next-after-5 = 9, then 1, then 5 (wrap).
7647 assert_eq!(buf, vec![3, 7, 9, 1, 5]);
7648 }
7649
7650 // ── H2FlowControl ───────────────────────────────────────────────────
7651
7652 #[test]
7653 fn test_flow_control_initial_state() {
7654 let fc = H2FlowControl {
7655 window: DEFAULT_INITIAL_WINDOW_SIZE as i32,
7656 received_bytes_since_update: 0,
7657 pending_window_updates: HashMap::new(),
7658 };
7659 assert_eq!(fc.window, 65535);
7660 assert_eq!(fc.received_bytes_since_update, 0);
7661 assert!(fc.pending_window_updates.is_empty());
7662 }
7663
7664 #[test]
7665 fn test_flow_control_window_update_coalescing() {
7666 let mut updates: HashMap<u32, u32> = HashMap::new();
7667
7668 // First update for stream 1
7669 updates.insert(1, 1000);
7670 assert_eq!(*updates.get(&1).unwrap(), 1000);
7671
7672 // Coalesce second update for same stream
7673 if let Some(existing) = updates.get_mut(&1) {
7674 *existing = existing.saturating_add(500).min(i32::MAX as u32);
7675 }
7676 assert_eq!(*updates.get(&1).unwrap(), 1500);
7677
7678 // Different stream gets its own entry
7679 updates.insert(3, 2000);
7680 assert_eq!(updates.len(), 2);
7681 assert_eq!(*updates.get(&3).unwrap(), 2000);
7682 }
7683
7684 #[test]
7685 fn test_flow_control_window_update_saturation() {
7686 let mut updates: HashMap<u32, u32> = HashMap::new();
7687
7688 // Insert near max and coalesce — should saturate to i32::MAX
7689 let max_increment = i32::MAX as u32;
7690 updates.insert(1, max_increment - 100);
7691 if let Some(existing) = updates.get_mut(&1) {
7692 *existing = existing.saturating_add(200).min(max_increment);
7693 }
7694 assert_eq!(*updates.get(&1).unwrap(), max_increment);
7695 }
7696
7697 #[test]
7698 fn test_flow_control_connection_window_can_go_negative() {
7699 // RFC 9113 §6.9.2: connection-level window can go negative
7700 let mut fc = H2FlowControl {
7701 window: 100,
7702 received_bytes_since_update: 0,
7703 pending_window_updates: HashMap::new(),
7704 };
7705
7706 // Simulate consuming more than available
7707 fc.window -= 200;
7708 assert_eq!(fc.window, -100);
7709 }
7710
7711 // ── H2FloodConfig ───────────────────────────────────────────────────
7712
7713 #[test]
7714 fn test_flood_config_default_values() {
7715 let config = H2FloodConfig::default();
7716 assert_eq!(config.max_rst_stream_per_window, 100);
7717 assert_eq!(config.max_ping_per_window, 100);
7718 assert_eq!(config.max_settings_per_window, 50);
7719 assert_eq!(config.max_empty_data_per_window, 100);
7720 assert_eq!(config.max_continuation_frames, 20);
7721 assert_eq!(config.max_glitch_count, 100);
7722 assert_eq!(config.max_rst_stream_lifetime, 10_000);
7723 assert_eq!(config.max_rst_stream_abusive_lifetime, 50);
7724 assert_eq!(config.max_header_list_size, MAX_HEADER_LIST_SIZE as u32);
7725 }
7726
7727 // ── distribute_overhead ─────────────────────────────────────────────
7728
7729 #[test]
7730 fn test_distribute_overhead_proportional() {
7731 let mut metrics = SessionMetrics::new(None);
7732 let mut overhead_bin = 1000;
7733 let mut overhead_bout = 500;
7734
7735 // Stream transferred 60% of total bytes (not last stream)
7736 distribute_overhead(
7737 &mut metrics,
7738 &mut overhead_bin,
7739 &mut overhead_bout,
7740 (600, 300), // stream_bytes
7741 (1000, 500), // total_bytes
7742 2, // active_streams
7743 false, // is_last_stream
7744 );
7745
7746 assert_eq!(metrics.bin, 600); // 60% of 1000
7747 assert_eq!(metrics.bout, 300); // 60% of 500
7748 assert_eq!(overhead_bin, 400); // 1000 - 600
7749 assert_eq!(overhead_bout, 200); // 500 - 300
7750 }
7751
7752 #[test]
7753 fn test_distribute_overhead_even_split_when_no_bytes() {
7754 let mut metrics = SessionMetrics::new(None);
7755 let mut overhead_bin = 100;
7756 let mut overhead_bout = 200;
7757
7758 // No bytes transferred -> even distribution (not last stream)
7759 distribute_overhead(
7760 &mut metrics,
7761 &mut overhead_bin,
7762 &mut overhead_bout,
7763 (0, 0), // stream_bytes
7764 (0, 0), // total_bytes
7765 4, // active_streams
7766 false, // is_last_stream
7767 );
7768
7769 assert_eq!(metrics.bin, 25); // 100 / 4
7770 assert_eq!(metrics.bout, 50); // 200 / 4
7771 assert_eq!(overhead_bin, 75);
7772 assert_eq!(overhead_bout, 150);
7773 }
7774
7775 #[test]
7776 fn test_distribute_overhead_clamps_to_remaining() {
7777 let mut metrics = SessionMetrics::new(None);
7778 let mut overhead_bin = 10;
7779 let mut overhead_bout = 10;
7780
7781 // Stream claims 100% of bytes but overhead is small (last stream)
7782 distribute_overhead(
7783 &mut metrics,
7784 &mut overhead_bin,
7785 &mut overhead_bout,
7786 (1000, 1000), // stream_bytes
7787 (1000, 1000), // total_bytes
7788 1, // active_streams
7789 true, // is_last_stream
7790 );
7791
7792 assert_eq!(metrics.bin, 10);
7793 assert_eq!(metrics.bout, 10);
7794 assert_eq!(overhead_bin, 0);
7795 assert_eq!(overhead_bout, 0);
7796 }
7797
7798 #[test]
7799 fn test_distribute_overhead_zero_active_streams() {
7800 let mut metrics = SessionMetrics::new(None);
7801 let mut overhead_bin = 100;
7802 let mut overhead_bout = 100;
7803
7804 // 0 active streams (edge case) — last stream gets all remainder
7805 distribute_overhead(
7806 &mut metrics,
7807 &mut overhead_bin,
7808 &mut overhead_bout,
7809 (0, 0),
7810 (0, 0),
7811 0,
7812 true,
7813 );
7814
7815 assert_eq!(metrics.bin, 100); // last stream gets all remaining
7816 assert_eq!(metrics.bout, 100);
7817 assert_eq!(overhead_bin, 0);
7818 assert_eq!(overhead_bout, 0);
7819 }
7820
7821 #[test]
7822 fn test_distribute_overhead_last_stream_gets_remainder() {
7823 let mut metrics1 = SessionMetrics::new(None);
7824 let mut metrics2 = SessionMetrics::new(None);
7825 let mut overhead_bin = 120;
7826 let mut overhead_bout = 120;
7827
7828 // First stream (not last): gets proportional share
7829 distribute_overhead(
7830 &mut metrics1,
7831 &mut overhead_bin,
7832 &mut overhead_bout,
7833 (100, 100), // stream_bytes
7834 (300, 300), // total_bytes
7835 3, // active_streams
7836 false, // is_last_stream
7837 );
7838
7839 let remaining_bin = overhead_bin;
7840 let remaining_bout = overhead_bout;
7841
7842 // Last stream: gets ALL remaining overhead (no rounding loss)
7843 distribute_overhead(
7844 &mut metrics2,
7845 &mut overhead_bin,
7846 &mut overhead_bout,
7847 (100, 100), // stream_bytes
7848 (300, 300), // total_bytes
7849 3, // active_streams
7850 true, // is_last_stream
7851 );
7852
7853 assert_eq!(metrics2.bin, remaining_bin);
7854 assert_eq!(metrics2.bout, remaining_bout);
7855 assert_eq!(overhead_bin, 0, "no remainder bytes should be lost");
7856 assert_eq!(overhead_bout, 0, "no remainder bytes should be lost");
7857 }
7858
7859 // ── H2FlowControl (additional edge cases) ─────────────────────────
7860
7861 #[test]
7862 fn test_flow_control_queue_window_update_cap() {
7863 // Verify DEFAULT_MAX_PENDING_WINDOW_UPDATES reflects 1 + 4*MAX_CONCURRENT_STREAMS
7864 assert_eq!(DEFAULT_MAX_PENDING_WINDOW_UPDATES, 1 + 100 * 4);
7865
7866 // Simulate queue reaching capacity
7867 let cap = DEFAULT_MAX_PENDING_WINDOW_UPDATES;
7868 let mut updates: HashMap<u32, u32> = HashMap::new();
7869 for i in 0..cap as u32 {
7870 updates.insert(i, 1000);
7871 }
7872 assert_eq!(updates.len(), cap);
7873
7874 // A new stream ID beyond capacity should be rejected
7875 let next_stream = cap as u32;
7876 let at_cap = updates.len() >= cap;
7877 assert!(at_cap);
7878 assert!(!updates.contains_key(&next_stream));
7879
7880 // Verify custom max_concurrent_streams produces proportional cap
7881 let custom_cap = 1 + 500_usize * 4;
7882 assert_eq!(custom_cap, 2001);
7883 }
7884
7885 #[test]
7886 fn test_h2_connection_config_defaults() {
7887 let config = H2ConnectionConfig::default();
7888 assert_eq!(config.initial_connection_window, ENLARGED_CONNECTION_WINDOW);
7889 assert_eq!(
7890 config.max_concurrent_streams,
7891 DEFAULT_MAX_CONCURRENT_STREAMS
7892 );
7893 assert_eq!(config.stream_shrink_ratio, 2);
7894 }
7895
7896 #[test]
7897 fn test_h2_connection_config_clamp_window_lower_bound() {
7898 // Below minimum: clamped to DEFAULT_INITIAL_WINDOW_SIZE (65535)
7899 let config = H2ConnectionConfig::new(100, 100, 2);
7900 assert_eq!(
7901 config.initial_connection_window,
7902 DEFAULT_INITIAL_WINDOW_SIZE
7903 );
7904 }
7905
7906 #[test]
7907 fn test_h2_connection_config_clamp_window_upper_bound() {
7908 // Above maximum: clamped to FLOW_CONTROL_MAX_WINDOW (2^31-1)
7909 let config = H2ConnectionConfig::new(u32::MAX, 100, 2);
7910 assert_eq!(config.initial_connection_window, FLOW_CONTROL_MAX_WINDOW);
7911 }
7912
7913 #[test]
7914 fn test_h2_connection_config_clamp_window_exact_minimum() {
7915 // Exactly minimum: no clamping, no zero-increment WINDOW_UPDATE risk
7916 let config = H2ConnectionConfig::new(DEFAULT_INITIAL_WINDOW_SIZE, 100, 2);
7917 assert_eq!(
7918 config.initial_connection_window,
7919 DEFAULT_INITIAL_WINDOW_SIZE
7920 );
7921 // Increment to send would be 0 — the code guards this with `if increment > 0`
7922 let increment = config
7923 .initial_connection_window
7924 .saturating_sub(DEFAULT_INITIAL_WINDOW_SIZE);
7925 assert_eq!(increment, 0);
7926 }
7927
7928 #[test]
7929 fn test_h2_connection_config_clamp_shrink_ratio() {
7930 // Below minimum: clamped to 2 (1 would defeat recycling)
7931 let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 100, 0);
7932 assert_eq!(config.stream_shrink_ratio, 2);
7933 let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 100, 1);
7934 assert_eq!(config.stream_shrink_ratio, 2);
7935 }
7936
7937 #[test]
7938 fn test_h2_connection_config_clamp_concurrent_streams() {
7939 let config = H2ConnectionConfig::new(ENLARGED_CONNECTION_WINDOW, 0, 2);
7940 assert_eq!(config.max_concurrent_streams, 1);
7941 }
7942
7943 #[test]
7944 fn test_h2_connection_config_from_optional_uses_defaults() {
7945 let config = H2ConnectionConfig::from_optional(None, None, None);
7946 let defaults = H2ConnectionConfig::default();
7947 assert_eq!(config, defaults);
7948 }
7949
7950 #[test]
7951 fn test_h2_connection_config_from_optional_overrides() {
7952 let config = H2ConnectionConfig::from_optional(Some(2_000_000), Some(500), Some(4));
7953 assert_eq!(config.initial_connection_window, 2_000_000);
7954 assert_eq!(config.max_concurrent_streams, 500);
7955 assert_eq!(config.stream_shrink_ratio, 4);
7956 }
7957
7958 #[test]
7959 fn test_flow_control_window_settings_change_negative() {
7960 // RFC 9113 §6.9.2: A change to SETTINGS_INITIAL_WINDOW_SIZE can cause
7961 // the flow-control window to become negative.
7962 let mut fc = H2FlowControl {
7963 window: 100,
7964 received_bytes_since_update: 0,
7965 pending_window_updates: HashMap::new(),
7966 };
7967
7968 // Simulate SETTINGS_INITIAL_WINDOW_SIZE reduction:
7969 // old_initial = 65535, new_initial = 10 => delta = 10 - 65535 = -65525
7970 let old_initial: i32 = DEFAULT_INITIAL_WINDOW_SIZE as i32;
7971 let new_initial: i32 = 10;
7972 let delta = new_initial - old_initial; // -65525
7973 fc.window += delta;
7974
7975 assert!(
7976 fc.window < 0,
7977 "Window must be able to go negative after settings change"
7978 );
7979 assert_eq!(fc.window, 100 + (10 - 65535));
7980 }
7981
7982 #[test]
7983 fn test_flow_control_coalesce_saturates_at_max_increment() {
7984 let max_increment = i32::MAX as u32;
7985 let mut updates: HashMap<u32, u32> = HashMap::new();
7986
7987 // Insert at max and try to coalesce more
7988 updates.insert(1, max_increment);
7989 if let Some(existing) = updates.get_mut(&1) {
7990 *existing = existing.saturating_add(1000).min(max_increment);
7991 }
7992 assert_eq!(*updates.get(&1).unwrap(), max_increment);
7993 }
7994
7995 // ── H2FloodConfig (additional) ───────────────────────────────────
7996
7997 #[test]
7998 fn test_flood_config_default_matches_constants() {
7999 let config = H2FloodConfig::default();
8000 assert_eq!(
8001 config.max_rst_stream_per_window,
8002 DEFAULT_MAX_RST_STREAM_PER_WINDOW
8003 );
8004 assert_eq!(config.max_ping_per_window, DEFAULT_MAX_PING_PER_WINDOW);
8005 assert_eq!(
8006 config.max_settings_per_window,
8007 DEFAULT_MAX_SETTINGS_PER_WINDOW
8008 );
8009 assert_eq!(
8010 config.max_empty_data_per_window,
8011 DEFAULT_MAX_EMPTY_DATA_PER_WINDOW
8012 );
8013 assert_eq!(
8014 config.max_continuation_frames,
8015 DEFAULT_MAX_CONTINUATION_FRAMES
8016 );
8017 assert_eq!(config.max_glitch_count, DEFAULT_MAX_GLITCH_COUNT);
8018 }
8019
8020 #[test]
8021 fn test_flood_config_equality() {
8022 let config_a = H2FloodConfig::default();
8023 let config_b = H2FloodConfig::default();
8024 assert_eq!(config_a, config_b);
8025
8026 let config_c = H2FloodConfig {
8027 max_rst_stream_per_window: 1,
8028 ..H2FloodConfig::default()
8029 };
8030 assert_ne!(config_a, config_c);
8031 }
8032
8033 // ── distribute_overhead (additional edge cases) ───────────────────
8034
8035 #[test]
8036 fn test_distribute_overhead_asymmetric_in_out() {
8037 let mut metrics = SessionMetrics::new(None);
8038 let mut overhead_bin = 1000;
8039 let mut overhead_bout = 1000;
8040
8041 // Stream transferred 100% inbound, 0% outbound (not last stream)
8042 distribute_overhead(
8043 &mut metrics,
8044 &mut overhead_bin,
8045 &mut overhead_bout,
8046 (500, 0), // stream_bytes
8047 (500, 100), // total_bytes
8048 2, // active_streams
8049 false, // is_last_stream
8050 );
8051
8052 assert_eq!(metrics.bin, 1000); // 100% of inbound overhead
8053 assert_eq!(metrics.bout, 0); // 0% of outbound overhead
8054 assert_eq!(overhead_bin, 0);
8055 assert_eq!(overhead_bout, 1000);
8056 }
8057
8058 #[test]
8059 fn test_distribute_overhead_many_streams_accumulate() {
8060 let mut metrics = SessionMetrics::new(None);
8061 let mut overhead_bin = 120;
8062 let mut overhead_bout = 120;
8063
8064 // Three equal streams, each calling distribute_overhead.
8065 // With is_last_stream on the third call, the last stream gets all
8066 // remaining overhead, so no rounding loss occurs.
8067 // call 1: 120 * 100/300 = 40 -> remaining 80
8068 // call 2: 80 * 100/300 = 26 -> remaining 54
8069 // call 3: last stream gets all remaining = 54
8070 // Total distributed: 40 + 26 + 54 = 120 (no loss)
8071 for i in 0..3 {
8072 distribute_overhead(
8073 &mut metrics,
8074 &mut overhead_bin,
8075 &mut overhead_bout,
8076 (100, 100), // stream_bytes
8077 (300, 300), // total_bytes
8078 3, // active_streams
8079 i == 2, // is_last_stream on final call
8080 );
8081 }
8082
8083 assert_eq!(metrics.bin, 120);
8084 assert_eq!(metrics.bout, 120);
8085 // No rounding residual — last stream absorbed the remainder
8086 assert_eq!(overhead_bin, 0);
8087 assert_eq!(overhead_bout, 0);
8088 }
8089
8090 // ── Hex chunk formatting ────────────────────────────────────────────
8091
8092 /// Verify that the Vec<u8> + write!() hex formatting used in
8093 /// handle_data_frame produces output identical to format!("{:x}").
8094 #[test]
8095 fn test_hex_chunk_length_formatting() {
8096 use std::io::Write as _;
8097
8098 let cases: &[(usize, &[u8])] = &[
8099 (1, b"1"),
8100 (15, b"f"),
8101 (16, b"10"),
8102 (255, b"ff"),
8103 (256, b"100"),
8104 (4096, b"1000"),
8105 (65535, b"ffff"),
8106 (65536, b"10000"),
8107 ];
8108
8109 for &(payload_len, expected) in cases {
8110 let mut buf = Vec::with_capacity(16);
8111 let _ = write!(buf, "{payload_len:x}");
8112 assert_eq!(
8113 buf, expected,
8114 "hex formatting mismatch for payload_len={payload_len}"
8115 );
8116 }
8117
8118 // usize::MAX tested separately to avoid temporary lifetime issue
8119 let max_expected = format!("{:x}", usize::MAX);
8120 let mut buf = Vec::with_capacity(16);
8121 let _ = write!(buf, "{:x}", usize::MAX);
8122 assert_eq!(buf, max_expected.as_bytes());
8123 }
8124
8125 // ── Stream-ID allocation / exhaustion ──────────────────────────────────
8126
8127 /// A fresh client connection starts with `last_stream_id == 0`. The first
8128 /// call MUST issue stream `1` (odd, RFC 9113 §5.1.1) and advance the
8129 /// watermark to `2`.
8130 #[test]
8131 fn test_next_stream_id_client_first_allocation() {
8132 let (issued, next) = next_stream_id(0, true).expect("fresh client must allocate");
8133 assert_eq!(issued, 1);
8134 assert_eq!(next, 2);
8135 }
8136
8137 /// Client allocation yields strictly increasing odd identifiers
8138 /// (1, 3, 5, ...) as required by RFC 9113 §5.1.1.
8139 #[test]
8140 fn test_next_stream_id_client_sequence_is_odd_and_monotonic() {
8141 let mut last = 0u32;
8142 let mut issued_ids = Vec::with_capacity(8);
8143 for _ in 0..8 {
8144 let (id, next) = next_stream_id(last, true).expect("unexhausted");
8145 assert_eq!(id & 1, 1, "client stream ids must be odd (RFC 9113 §5.1.1)");
8146 assert!(issued_ids.last().is_none_or(|prev: &u32| id > *prev));
8147 issued_ids.push(id);
8148 last = next;
8149 }
8150 assert_eq!(issued_ids, vec![1, 3, 5, 7, 9, 11, 13, 15]);
8151 }
8152
8153 /// Server-side allocation yields even identifiers. The helper
8154 /// convention is `watermark - 2` for server, `watermark - 1` for client,
8155 /// so both sides share the same monotonically-increasing even watermark.
8156 /// Sōzu never server-pushes, but the helper must be symmetric so push
8157 /// could be enabled without a regression.
8158 #[test]
8159 fn test_next_stream_id_server_is_even() {
8160 // `last = 2` means the most recent allocation advanced the watermark
8161 // to 2; server then issues `2 - 2 = 0`. This is an artefact of the
8162 // shared watermark and only matters in tests — server never uses it.
8163 let (issued, next) = next_stream_id(2, false).expect("server allocation");
8164 assert_eq!(issued & 1, 0, "server stream ids must be even");
8165 assert_eq!(next, 4);
8166 assert_eq!(issued, 2);
8167
8168 let (issued, next) = next_stream_id(next, false).expect("second slot");
8169 assert_eq!(issued, 4);
8170 assert_eq!(issued & 1, 0);
8171 assert_eq!(next, 6);
8172 }
8173
8174 /// The last client-issuable odd stream ID is `STREAM_ID_MAX = 0x7FFF_FFFF`.
8175 /// To issue it the watermark must advance to `STREAM_ID_MAX + 1 = 2³¹`;
8176 /// the caller therefore supplies `last = STREAM_ID_MAX - 1 = 0x7FFF_FFFE`.
8177 /// That call MUST succeed and return the max ID; the post-call watermark
8178 /// sits at `2³¹`, which is the sentinel that makes the next call fail.
8179 #[test]
8180 fn test_next_stream_id_client_final_slot_allocates() {
8181 let last = STREAM_ID_MAX - 1;
8182 let (issued, next) = next_stream_id(last, true).expect("final slot still allocates");
8183 assert_eq!(issued, STREAM_ID_MAX);
8184 assert_eq!(next, STREAM_ID_MAX + 1);
8185 // And the very next call MUST refuse rather than wrap.
8186 assert!(next_stream_id(next, true).is_none());
8187 }
8188
8189 /// Exhaustion case: once the client has issued stream ID `STREAM_ID_MAX`,
8190 /// the watermark sits at `STREAM_ID_MAX + 1`. The next request MUST return
8191 /// `None` — without this guard the helper would issue `STREAM_ID_MAX + 2`
8192 /// (wrapped down to an even id), which would (a) use the reserved
8193 /// high bit and (b) violate the odd-parity invariant for client streams.
8194 #[test]
8195 fn test_next_stream_id_client_exhausted_returns_none() {
8196 let last = STREAM_ID_MAX + 1;
8197 assert!(next_stream_id(last, true).is_none());
8198 }
8199
8200 /// Exhaustion via `checked_add` saturation: defence in depth in case a
8201 /// caller jumps `last_stream_id` close to `u32::MAX`. The helper must
8202 /// not panic nor overflow — it must return `None`.
8203 #[test]
8204 fn test_next_stream_id_saturates_near_u32_max() {
8205 assert!(next_stream_id(u32::MAX, true).is_none());
8206 assert!(next_stream_id(u32::MAX - 1, true).is_none());
8207 }
8208
8209 /// Server-side exhaustion: same guard, even-parity identifier space.
8210 #[test]
8211 fn test_next_stream_id_server_exhausted_returns_none() {
8212 let last = STREAM_ID_MAX + 1;
8213 assert!(next_stream_id(last, false).is_none());
8214 }
8215
8216 /// Regression guard: the helper must never issue a stream ID that
8217 /// exceeds `STREAM_ID_MAX` for either side, no matter where the
8218 /// watermark sits. This walks every value in a neighbourhood of the
8219 /// boundary to rule out off-by-one errors.
8220 #[test]
8221 fn test_next_stream_id_never_exceeds_stream_id_max() {
8222 for last in (STREAM_ID_MAX - 4)..=(STREAM_ID_MAX + 4) {
8223 for is_client in [true, false] {
8224 if let Some((issued, next)) = next_stream_id(last, is_client) {
8225 assert!(
8226 issued <= STREAM_ID_MAX,
8227 "issued id {issued} exceeds STREAM_ID_MAX (last={last}, is_client={is_client})"
8228 );
8229 // `next` is the post-allocation watermark and may sit at
8230 // STREAM_ID_MAX + 1 — the very next call must then return None.
8231 if next > STREAM_ID_MAX {
8232 assert!(
8233 next_stream_id(next, is_client).is_none(),
8234 "second call after final slot must report exhaustion"
8235 );
8236 }
8237 }
8238 }
8239 }
8240 }
8241
8242 /// The helper's `is_client` flag must cleanly split the ID space so that
8243 /// a client and a server peered on the same connection cannot collide.
8244 /// Given the same `last_stream_id`, the two parities must differ by 1.
8245 #[test]
8246 fn test_next_stream_id_client_server_parities_disjoint() {
8247 for last in [0u32, 2, 4, 10, 100, 1_000_000, STREAM_ID_MAX - 3] {
8248 let (client_id, _) = next_stream_id(last, true).unwrap();
8249 let (server_id, _) = next_stream_id(last, false).unwrap();
8250 assert_eq!(client_id & 1, 1);
8251 assert_eq!(server_id & 1, 0);
8252 assert_eq!(client_id.abs_diff(server_id), 1);
8253 }
8254 }
8255
8256 // ── LIFECYCLE §9 invariant 16: any_stream_id_matches ─────────────────
8257 //
8258 // Covers the iteration dispatch used by `any_stream_has_pending_back`.
8259 // Testing the probe directly against a synthetic closure keeps the
8260 // tests independent of the full `Stream` fixture (which requires a
8261 // `Pool` and a fully-built `HttpContext`).
8262
8263 #[test]
8264 fn test_any_stream_id_matches_empty_map_is_false() {
8265 let streams: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8266 assert!(!any_stream_id_matches(&streams, |_| true));
8267 }
8268
8269 #[test]
8270 fn test_any_stream_id_matches_all_probe_false_is_false() {
8271 let mut streams: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8272 streams.insert(1, 0);
8273 streams.insert(3, 1);
8274 streams.insert(5, 2);
8275 assert!(!any_stream_id_matches(&streams, |_| false));
8276 }
8277
8278 #[test]
8279 fn test_any_stream_id_matches_any_probe_true_is_true() {
8280 let mut streams: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8281 streams.insert(1, 0);
8282 streams.insert(3, 1);
8283 streams.insert(5, 2);
8284 // Probe is true only for GlobalStreamId == 1 (i.e. StreamId 3).
8285 assert!(any_stream_id_matches(&streams, |gid| gid == 1));
8286 }
8287
8288 #[test]
8289 fn test_any_stream_id_matches_single_entry() {
8290 let mut streams: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8291 streams.insert(42, 7);
8292 assert!(any_stream_id_matches(&streams, |gid| gid == 7));
8293 assert!(!any_stream_id_matches(&streams, |gid| gid == 8));
8294 }
8295
8296 #[test]
8297 fn test_any_stream_id_matches_short_circuits() {
8298 let mut streams: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8299 streams.insert(1, 0);
8300 streams.insert(3, 1);
8301 streams.insert(5, 2);
8302 streams.insert(7, 3);
8303 let mut calls = 0usize;
8304 let result = any_stream_id_matches(&streams, |_| {
8305 calls += 1;
8306 true
8307 });
8308 assert!(result);
8309 // `Iterator::any` short-circuits on the first `true` — so the probe
8310 // must fire at most once in this construction.
8311 assert_eq!(calls, 1);
8312 }
8313
8314 // ── cumulative-stall budget decision (fc_stall_budget_decision) ──
8315
8316 #[test]
8317 fn test_fc_stall_budget_open_window_always_clears() {
8318 // A genuinely open send window is a real un-stall, regardless of prior
8319 // accumulated progress or this pass's drain.
8320 assert_eq!(
8321 fc_stall_budget_decision(false, 0, None),
8322 FcStallAction::Clear
8323 );
8324 assert_eq!(
8325 fc_stall_budget_decision(false, 1, Some(5)),
8326 FcStallAction::Clear
8327 );
8328 assert_eq!(
8329 fc_stall_budget_decision(false, i32::MAX, Some(FC_STALL_CLEAR_FLOOR)),
8330 FcStallAction::Clear
8331 );
8332 }
8333
8334 #[test]
8335 fn test_fc_stall_budget_blocked_arms_and_accumulates() {
8336 // First blocked pass arms with this pass's drain.
8337 assert_eq!(
8338 fc_stall_budget_decision(true, 1, None),
8339 FcStallAction::Arm { progress: 1 }
8340 );
8341 // A blocked pass with no drain keeps the accumulator unchanged, so the
8342 // deadline keeps aging (a window-0 stall makes consumed == 0).
8343 assert_eq!(
8344 fc_stall_budget_decision(true, 0, Some(42)),
8345 FcStallAction::Arm { progress: 42 }
8346 );
8347 // Negative `consumed` is clamped to 0 (defensive; converter.window only
8348 // shrinks, so consumed is >= 0 in practice).
8349 assert_eq!(
8350 fc_stall_budget_decision(true, -10, Some(7)),
8351 FcStallAction::Arm { progress: 7 }
8352 );
8353 }
8354
8355 #[test]
8356 fn test_fc_stall_budget_floor_clears() {
8357 // Reaching the floor in a single pass (a full DATA frame of real
8358 // delivery) clears the deadline.
8359 assert_eq!(
8360 fc_stall_budget_decision(true, FC_STALL_CLEAR_FLOOR as i32, None),
8361 FcStallAction::Clear
8362 );
8363 // Exactly one byte below the floor still arms.
8364 assert_eq!(
8365 fc_stall_budget_decision(true, (FC_STALL_CLEAR_FLOOR - 1) as i32, None),
8366 FcStallAction::Arm {
8367 progress: FC_STALL_CLEAR_FLOOR - 1
8368 }
8369 );
8370 // Prior progress plus this pass crossing the floor clears.
8371 assert_eq!(
8372 fc_stall_budget_decision(true, 1, Some(FC_STALL_CLEAR_FLOOR - 1)),
8373 FcStallAction::Clear
8374 );
8375 }
8376
8377 #[test]
8378 fn test_fc_stall_budget_wu_drip_ages_until_floor() {
8379 // The WINDOW_UPDATE(+1) closure: a 1-byte-per-pass drip must keep the
8380 // deadline armed (aging) for the whole run up to the floor and only
8381 // clear on the pass that reaches it — so a drip granting < floor bytes
8382 // per idle period is reaped, never kept alive. This is the unit-level
8383 // proof that the budget closes the WINDOW_UPDATE-drip vector.
8384 let mut progress: Option<usize> = None;
8385 for pass in 1..FC_STALL_CLEAR_FLOOR {
8386 match fc_stall_budget_decision(true, 1, progress) {
8387 FcStallAction::Arm { progress: p } => {
8388 assert_eq!(p, pass, "drip accumulator off at pass {pass}");
8389 progress = Some(p);
8390 }
8391 FcStallAction::Clear => panic!("drip cleared the deadline early at pass {pass}"),
8392 }
8393 }
8394 // The pass that reaches the floor finally clears.
8395 assert_eq!(
8396 fc_stall_budget_decision(true, 1, progress),
8397 FcStallAction::Clear
8398 );
8399 }
8400
8401 // ── flow-control-stall reaper union (collect_timed_out_streams) ──
8402
8403 #[test]
8404 fn test_collect_timed_out_streams_reaps_fc_stall_despite_fresh_liveness() {
8405 // A window-stalled stream MUST be reaped on the flow-control-stall
8406 // deadline even if its bidirectional-liveness timer is fresh — an
8407 // inbound 1-byte DATA drip keeps `last_activity` warm but never touches
8408 // `fc_stalled`. Without the `fc_stalled` guard this stream is never
8409 // reaped (the pre-fix window-stall hold).
8410 let now = Instant::now();
8411 let deadline = std::time::Duration::from_secs(2);
8412 let mut live = HashMap::new();
8413 live.insert(7u32, 0usize);
8414 let rst_sent = HashSet::new();
8415 let mut last_activity = HashMap::new();
8416 last_activity.insert(7u32, now); // fresh: just received an inbound DATA drip
8417 let mut fc_stalled = HashMap::new();
8418 fc_stalled.insert(7u32, now - std::time::Duration::from_secs(5));
8419 let out =
8420 collect_timed_out_streams(&last_activity, &fc_stalled, &live, &rst_sent, now, deadline);
8421 assert_eq!(out, vec![(7u32, "H2::WindowStall")]);
8422 }
8423
8424 #[test]
8425 fn test_collect_timed_out_streams_idle_dedup_and_filters() {
8426 let now = Instant::now();
8427 let deadline = std::time::Duration::from_secs(2);
8428 let old = now - std::time::Duration::from_secs(5);
8429 let mut live = HashMap::new();
8430 for sid in [1u32, 3, 5, 9] {
8431 live.insert(sid, 0usize);
8432 }
8433 let mut rst_sent = HashSet::new();
8434 rst_sent.insert(9u32); // already resetting -> excluded
8435 let mut last_activity = HashMap::new();
8436 last_activity.insert(1u32, old); // idle past deadline
8437 last_activity.insert(3u32, now); // fresh -> survives
8438 last_activity.insert(5u32, old); // idle AND fc-stalled -> dedup to one entry
8439 last_activity.insert(9u32, old); // idle but rst_sent -> excluded
8440 last_activity.insert(11u32, old); // not a live stream -> excluded
8441 let mut fc_stalled = HashMap::new();
8442 fc_stalled.insert(5u32, old);
8443 let mut out =
8444 collect_timed_out_streams(&last_activity, &fc_stalled, &live, &rst_sent, now, deadline);
8445 out.sort();
8446 assert_eq!(
8447 out,
8448 vec![(1u32, "H2::IdleTimeout"), (5u32, "H2::IdleTimeout")]
8449 );
8450 }
8451
8452 #[test]
8453 fn test_collect_timed_out_streams_empty_when_all_fresh() {
8454 let now = Instant::now();
8455 let deadline = std::time::Duration::from_secs(2);
8456 let mut live = HashMap::new();
8457 live.insert(1u32, 0usize);
8458 let rst_sent = HashSet::new();
8459 let mut last_activity = HashMap::new();
8460 last_activity.insert(1u32, now);
8461 let mut fc_stalled = HashMap::new();
8462 fc_stalled.insert(1u32, now);
8463 assert!(
8464 collect_timed_out_streams(&last_activity, &fc_stalled, &live, &rst_sent, now, deadline)
8465 .is_empty()
8466 );
8467 }
8468
8469 // ── LIFECYCLE §9 invariant 16: any_stream_has_pending_back ───────────
8470
8471 /// Build a minimal `Stream` for invariant-16 probing. Uses the pool
8472 /// plumbing so `back.blocks` / `back.out` exist; every other field is
8473 /// default-valued because the predicate only reads the back buffer.
8474 fn make_stream_for_invariant_16(pool: &Rc<RefCell<Pool>>, session_ulid: Ulid) -> Stream {
8475 let http_ctx = HttpContext {
8476 keep_alive_backend: true,
8477 keep_alive_frontend: true,
8478 sticky_session_found: None,
8479 method: None,
8480 authority: None,
8481 path: None,
8482 status: None,
8483 reason: None,
8484 user_agent: None,
8485 x_request_id: None,
8486 xff_chain: None,
8487 #[cfg(feature = "opentelemetry")]
8488 otel: None,
8489 closing: false,
8490 session_id: session_ulid,
8491 id: Ulid::generate(),
8492 backend_id: None,
8493 cluster_id: None,
8494 protocol: Protocol::HTTPS,
8495 public_address: "127.0.0.1:0".parse().unwrap(),
8496 session_address: None,
8497 sticky_name: String::new(),
8498 sticky_session: None,
8499 backend_address: None,
8500 tls_server_name: None,
8501 tls_cert_names: None,
8502 strict_sni_binding: false,
8503 elide_x_real_ip: false,
8504 send_x_real_ip: false,
8505 tls_version: None,
8506 tls_cipher: None,
8507 tls_alpn: None,
8508 sozu_id_header: String::from("Sozu-Id"),
8509 redirect_location: None,
8510 www_authenticate: None,
8511 original_authority: None,
8512 headers_response: Vec::new(),
8513 retry_after_seconds: None,
8514 frontend_redirect_template: None,
8515 redirect_status: None,
8516 access_log_message: None,
8517 };
8518 Stream::new(Rc::downgrade(pool), http_ctx, 65_535)
8519 .expect("pool should have capacity for two buffers")
8520 }
8521
8522 fn make_pool_for_invariant_16() -> Rc<RefCell<Pool>> {
8523 // Two buffer slots per stream (front + back), ten stream slots is
8524 // plenty for the tests below.
8525 Rc::new(RefCell::new(Pool::with_capacity(4, 20, 16_384)))
8526 }
8527
8528 #[test]
8529 fn test_any_stream_has_pending_back_empty_map_is_false() {
8530 let pool = make_pool_for_invariant_16();
8531 let ulid = Ulid::generate();
8532 let streams_map: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8533 let context_streams = vec![make_stream_for_invariant_16(&pool, ulid)];
8534 assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
8535 }
8536
8537 #[test]
8538 fn test_any_stream_has_pending_back_all_drained_is_false() {
8539 let pool = make_pool_for_invariant_16();
8540 let ulid = Ulid::generate();
8541 let context_streams = vec![
8542 make_stream_for_invariant_16(&pool, ulid),
8543 make_stream_for_invariant_16(&pool, ulid),
8544 ];
8545 let mut streams_map: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8546 streams_map.insert(1, 0);
8547 streams_map.insert(3, 1);
8548 // Both freshly-built streams have empty back.out and back.blocks
8549 // (Kawa::new starts with empty deques).
8550 assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
8551 }
8552
8553 #[test]
8554 fn test_any_stream_has_pending_back_unknown_gid_is_false() {
8555 // LIFECYCLE invariant 16 defence-in-depth: an unknown
8556 // `GlobalStreamId` during a stream-removal race must not panic;
8557 // `.get()` must short-circuit to `false`.
8558 let pool = make_pool_for_invariant_16();
8559 let ulid = Ulid::generate();
8560 let context_streams = vec![make_stream_for_invariant_16(&pool, ulid)];
8561 let mut streams_map: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8562 // GlobalStreamId 42 is out of range for the 1-element slice above.
8563 streams_map.insert(7, 42);
8564 assert!(!any_stream_has_pending_back(&streams_map, &context_streams));
8565 }
8566
8567 #[test]
8568 fn test_any_stream_has_pending_back_with_pending_blocks_is_true() {
8569 let pool = make_pool_for_invariant_16();
8570 let ulid = Ulid::generate();
8571 let mut stream = make_stream_for_invariant_16(&pool, ulid);
8572 // Push one dummy block — any Block variant is fine; the predicate
8573 // only checks `blocks.is_empty()`.
8574 stream.back.blocks.push_back(kawa::Block::StatusLine);
8575 let mut streams_map: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8576 streams_map.insert(1, 0);
8577 assert!(any_stream_has_pending_back(&streams_map, &[stream]));
8578 }
8579
8580 #[test]
8581 fn test_any_stream_has_pending_back_with_pending_out_is_true() {
8582 let pool = make_pool_for_invariant_16();
8583 let ulid = Ulid::generate();
8584 let mut stream = make_stream_for_invariant_16(&pool, ulid);
8585 // Non-empty out buffer with no blocks.
8586 stream
8587 .back
8588 .out
8589 .push_back(kawa::OutBlock::Store(kawa::Store::Static(b"partial frame")));
8590 let mut streams_map: HashMap<StreamId, GlobalStreamId> = HashMap::new();
8591 streams_map.insert(1, 0);
8592 assert!(any_stream_has_pending_back(&streams_map, &[stream]));
8593 }
8594
8595 // ── ready_incremental_by_urgency mid-pass consistency ────────────────
8596 //
8597 // The full RED is in e2e and currently #[ignore]'d (timing-sensitive).
8598 // The scalar logic below pins the saturating_sub + bucket-scoped
8599 // decrement contract the scheduler at h2.rs:2412-2414 + h2.rs:2481
8600 // relies on: a same-urgency transition-to-ineligible MUST drop the
8601 // per-bucket count by exactly 1 and never underflow the u64.
8602
8603 fn make_bucket(counts: &[(u8, usize)]) -> HashMap<u8, usize> {
8604 counts.iter().copied().collect()
8605 }
8606
8607 #[test]
8608 fn ready_incremental_bucket_decrement_reduces_same_urgency_only() {
8609 let mut map = make_bucket(&[(1, 3), (3, 2)]);
8610 let urgency: u8 = 1;
8611 let is_incremental = true;
8612 // Simulate a stream in urgency=1 going ineligible mid-pass.
8613 if is_incremental && let Some(c) = map.get_mut(&urgency) {
8614 *c = c.saturating_sub(1);
8615 }
8616 assert_eq!(map.get(&1), Some(&2), "urgency-1 bucket must drop to 2");
8617 assert_eq!(map.get(&3), Some(&2), "urgency-3 bucket untouched");
8618 }
8619
8620 #[test]
8621 fn ready_incremental_bucket_decrement_saturates_at_zero() {
8622 let mut map = make_bucket(&[(0, 0)]);
8623 let urgency: u8 = 0;
8624 if let Some(c) = map.get_mut(&urgency) {
8625 *c = c.saturating_sub(1);
8626 }
8627 assert_eq!(map.get(&0), Some(&0), "saturating_sub must not underflow");
8628 }
8629
8630 #[test]
8631 fn ready_incremental_bucket_decrement_skipped_for_non_incremental() {
8632 let mut map = make_bucket(&[(1, 3)]);
8633 let is_incremental = false;
8634 if is_incremental && let Some(c) = map.get_mut(&1) {
8635 *c = c.saturating_sub(1);
8636 }
8637 assert_eq!(
8638 map.get(&1),
8639 Some(&3),
8640 "non-incremental transitions must not touch the bucket"
8641 );
8642 }
8643
8644 // ── enqueue_rst: queue / dedupe / counter / arm invariants ───────────
8645 //
8646 // `enqueue_rst_into` is the free-function primitive shared by all three
8647 // RST push sites (DATA-on-closed, refuse_stream_and_discard,
8648 // reset_stream). The method delegates; the invariants live here.
8649
8650 #[test]
8651 fn test_enqueue_rst_into_populates_queue_and_dedupe() {
8652 let mut pending: Vec<(StreamId, H2Error)> = Vec::new();
8653 let mut total: usize = 0;
8654 let mut sent: HashSet<StreamId> = HashSet::new();
8655 let mut readiness = Readiness::new();
8656
8657 let first = enqueue_rst_into(
8658 &mut pending,
8659 &mut total,
8660 &mut sent,
8661 &mut readiness,
8662 5,
8663 H2Error::ProtocolError,
8664 );
8665 assert!(first, "first call must report freshly_queued = true");
8666 // Second call for the same stream must be a no-op AND return
8667 // false so accounting in `Self::enqueue_rst` skips this case.
8668 let second = enqueue_rst_into(
8669 &mut pending,
8670 &mut total,
8671 &mut sent,
8672 &mut readiness,
8673 5,
8674 H2Error::InternalError,
8675 );
8676 assert!(
8677 !second,
8678 "second call for same stream must return freshly_queued = false"
8679 );
8680
8681 assert_eq!(pending.len(), 1, "dedupe must collapse to a single entry");
8682 assert_eq!(
8683 pending[0],
8684 (5, H2Error::ProtocolError),
8685 "the first error wins — second push is ignored"
8686 );
8687 assert_eq!(total, 1, "queued-cap counter must bump exactly once");
8688 assert!(sent.contains(&5), "rst_sent must record the id");
8689 }
8690
8691 #[test]
8692 fn test_enqueue_rst_into_bumps_total_for_distinct_ids() {
8693 let mut pending: Vec<(StreamId, H2Error)> = Vec::new();
8694 let mut total: usize = 0;
8695 let mut sent: HashSet<StreamId> = HashSet::new();
8696 let mut readiness = Readiness::new();
8697
8698 for sid in [1u32, 3, 5, 7] {
8699 enqueue_rst_into(
8700 &mut pending,
8701 &mut total,
8702 &mut sent,
8703 &mut readiness,
8704 sid,
8705 H2Error::ProtocolError,
8706 );
8707 }
8708
8709 assert_eq!(pending.len(), 4);
8710 assert_eq!(total, 4);
8711 assert_eq!(sent.len(), 4);
8712 }
8713
8714 #[test]
8715 fn test_enqueue_rst_into_arms_writable_in_invariant_15_form() {
8716 let mut pending: Vec<(StreamId, H2Error)> = Vec::new();
8717 let mut total: usize = 0;
8718 let mut sent: HashSet<StreamId> = HashSet::new();
8719 let mut readiness = Readiness::new();
8720
8721 // Precondition: no WRITABLE bits set.
8722 assert!(!readiness.interest.is_writable());
8723 assert!(!readiness.event.is_writable());
8724
8725 enqueue_rst_into(
8726 &mut pending,
8727 &mut total,
8728 &mut sent,
8729 &mut readiness,
8730 9,
8731 H2Error::FlowControlError,
8732 );
8733
8734 // Postcondition: invariant-15 — both `interest` and `event` WRITABLE
8735 // are raised so the next tick runs `writable()` under edge-triggered
8736 // epoll.
8737 assert!(
8738 readiness.interest.is_writable(),
8739 "arm_writable must raise the interest bit"
8740 );
8741 assert!(
8742 readiness.event.is_writable(),
8743 "arm_writable must raise the event bit (edge-triggered epoll)"
8744 );
8745 }
8746
8747 #[test]
8748 fn test_enqueue_rst_into_dedupe_does_not_rearm_writable() {
8749 // Dedupe is a pure short-circuit: if the stream id is already in
8750 // `rst_sent`, we do not touch the readiness. This matters because
8751 // a re-entrant reset_stream call during a cascading error path
8752 // would otherwise re-raise WRITABLE unnecessarily — harmless but
8753 // noisy in metrics.
8754 let mut pending: Vec<(StreamId, H2Error)> = Vec::new();
8755 let mut total: usize = 0;
8756 let mut sent: HashSet<StreamId> = HashSet::new();
8757 sent.insert(11);
8758 let mut readiness = Readiness::new();
8759
8760 enqueue_rst_into(
8761 &mut pending,
8762 &mut total,
8763 &mut sent,
8764 &mut readiness,
8765 11,
8766 H2Error::ProtocolError,
8767 );
8768
8769 assert!(
8770 pending.is_empty(),
8771 "already-sent ids must not queue a second frame"
8772 );
8773 assert_eq!(total, 0);
8774 assert!(!readiness.interest.is_writable());
8775 assert!(!readiness.event.is_writable());
8776 }
8777
8778 // ── forcefully_terminate_answer arms WRITABLE for ET epoll ───────────
8779 //
8780 // Gap A in the h2spec diagnosis: the pre-fix code set `interest` but
8781 // never raised `event`, so `filter_interest() = event & interest` was
8782 // zero and `writable()` was never scheduled. This test pins the fix.
8783
8784 #[test]
8785 fn test_forcefully_terminate_answer_arms_event_and_interest() {
8786 let pool = make_pool_for_invariant_16();
8787 let ulid = Ulid::generate();
8788 let mut stream = make_stream_for_invariant_16(&pool, ulid);
8789 let mut readiness = Readiness::new();
8790
8791 assert!(!readiness.interest.is_writable());
8792 assert!(!readiness.event.is_writable());
8793
8794 forcefully_terminate_answer(&mut stream, &mut readiness, H2Error::ProtocolError);
8795
8796 assert!(
8797 readiness.interest.is_writable(),
8798 "forcefully_terminate_answer must set the WRITABLE interest bit"
8799 );
8800 assert!(
8801 readiness.event.is_writable(),
8802 "forcefully_terminate_answer must set the WRITABLE event bit — \
8803 without this, filter_interest() = 0 under edge-triggered epoll \
8804 and writable() is never scheduled (h2spec Gap A)"
8805 );
8806 }
8807}