moqtap_proxy/session.rs
1//! Per-connection proxy session — forwards streams between client and relay.
2
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
6use std::sync::{Arc, Mutex};
7use std::time::{Duration, Instant};
8
9use bytes::{Bytes, BytesMut};
10use tokio::sync::{mpsc, watch};
11use tokio::task::JoinSet;
12use tokio_util::sync::CancellationToken;
13
14use moqtap_client::transport::quic::QuicTransport;
15use moqtap_client::transport::{RecvStream, SendStream, Transport, TransportError};
16use moqtap_codec::dispatch::{AnyControlMessage, AnyDatagramHeader};
17use moqtap_codec::varint::VarInt;
18use moqtap_codec::version::DraftVersion;
19
20use crate::action::{Action, EgressConfig, Interest, StreamEnd};
21use crate::capability::{fetch_group_order_is_needed, ActionKind, Capabilities, Site};
22use crate::control::{
23 AbortOnDrop, ControlAttachment, ControlLeg, ControlPlane, SessionCommand, StreamCommand,
24 StreamRegistry, COMMAND_QUEUE_DEPTH,
25};
26use crate::egress::{self, CloseOrigin, DrainOutcome, EgressGauge, PendingQueue, SessionCloser};
27use crate::error::ProxyError;
28use crate::event::{
29 DataStreamHeaderKind, Effect, ImpairmentKind, ProxyEvent, SessionId, ShapeOutcome,
30};
31use crate::exec::{self, DeferredEffects, Plan, StreamSite};
32use crate::framer::{FetchGroupOrders, FramerConfig, FramerOut, ObjectFramer};
33use crate::hook::{FrameCtx, ObjectCtx, ProxyHook, StreamCtx};
34use crate::instrument::{Counters, Recorder};
35use crate::observer::ProxyObserver;
36use crate::parser::control::{ControlStreamParser, ParseResult, ParsedItem};
37use crate::shape::{
38 Acquire, Admission, Class, Scheduler, ShapeProfile, ShapeRecorder, ShapeStats, StreamKey,
39};
40use crate::transport::{self, TransportInstaller, TransportProfile};
41use crate::types::{DataStreamType, Leg, ProxySide};
42
43/// The transport type for upstream relay connections.
44#[derive(Debug, Clone)]
45pub enum UpstreamTransportType {
46 /// Raw QUIC — `upstream_addr` is `host:port`.
47 Quic,
48 /// WebTransport — `url` is the full WebTransport URL.
49 WebTransport {
50 /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
51 url: String,
52 },
53}
54
55/// Configuration for a proxy session's upstream connection.
56pub struct ProxySessionConfig {
57 /// The MoQT draft version to use for parsing.
58 pub draft: DraftVersion,
59 /// The transport type to use for the upstream connection.
60 pub upstream_transport: UpstreamTransportType,
61 /// Upstream relay address (e.g., `"192.168.1.10:4443"` for QUIC).
62 pub upstream_addr: String,
63 /// Whether to skip TLS verification for the upstream connection.
64 pub skip_upstream_cert_verify: bool,
65 /// Custom CA certificates for the upstream connection (DER-encoded).
66 pub upstream_ca_certs: Vec<Vec<u8>>,
67 /// Timeout in seconds for the upstream connection attempt. 0 means no timeout.
68 pub upstream_connect_timeout_secs: u64,
69 /// Optional QUIC transport parameters — flow-control windows, MTU,
70 /// keep-alive, congestion control — applied to the upstream relay
71 /// connection.
72 ///
73 /// `None` leaves quinn's defaults in place. Ignored for WebTransport
74 /// upstreams, which build their endpoint through `wtransport`.
75 ///
76 /// Setting this **and** `upstream_transport_profile` is refused when
77 /// the session connects, with [`ProxyError::TransportConfigAndProfile`]
78 /// naming [`Leg::Upstream`] — see that variant for why the two cannot
79 /// be merged. The refusal stands on a WebTransport upstream too, where
80 /// both fields would have been ignored: a contradiction reported on one
81 /// transport and swallowed on the other is worse than either answer.
82 pub upstream_transport_config: Option<Arc<quinn::TransportConfig>>,
83 /// The same parameters as `upstream_transport_config`, as a value that
84 /// can be written down, checked and stored.
85 ///
86 /// `Some(_)` builds the relay leg's `quinn::TransportConfig` from this
87 /// profile — through `upstream_installer`, or through
88 /// [`crate::transport::DefaultInstaller`] when there is none — and
89 /// installs it before the endpoint is built and before anything is
90 /// dialled. A profile the installer refuses is
91 /// [`ProxyError::TransportProfile`], and no connection is attempted.
92 ///
93 /// `None` is the behaviour callers had before this field existed. It is
94 /// the *only* alternative to `upstream_transport_config`, never a
95 /// companion to it.
96 pub upstream_transport_profile: Option<TransportProfile>,
97 /// How `upstream_transport_profile` becomes the config the relay leg
98 /// installs.
99 ///
100 /// `None` uses [`crate::transport::DefaultInstaller`], which applies
101 /// the profile over a fresh `quinn::TransportConfig::default()`. Supply
102 /// one to start from a base of your own instead — the trait exists
103 /// because a `quinn::TransportConfig` cannot be cloned, so the only way
104 /// to have a base *and* a profile is to build the base again for each
105 /// leg.
106 ///
107 /// **Inert without a profile.** [`TransportInstaller::build`] takes a
108 /// profile, so an installer set beside an empty
109 /// `upstream_transport_profile` is never called and the leg installs
110 /// nothing.
111 ///
112 /// **It composes with an `upstream_qlog` spec** — named in plain code
113 /// font because that field exists only under the `qlog` feature, so a
114 /// link from this always-compiled one would not resolve. A leg carrying
115 /// a profile, a spec and an installer builds its config here, once, and
116 /// the capture sink is attached to what came back;
117 /// [`TransportInstaller::build`] returns an owned
118 /// `quinn::TransportConfig` precisely so that the two can stack.
119 pub upstream_installer: Option<Arc<dyn TransportInstaller>>,
120 /// Where this leg's QUIC-level capture is written, if it is captured at
121 /// all.
122 ///
123 /// `Some(_)` builds the relay leg's `quinn::TransportConfig`, installs
124 /// the sink built from this spec on it, and dials with it — all before
125 /// the endpoint is built, because quinn accepts a sink in exactly one
126 /// place and that place is a method which mutates a
127 /// `quinn::TransportConfig`. It composes with
128 /// `upstream_transport_profile`, which is applied to the same config
129 /// first, and **not** with `upstream_transport_config`: a leg naming a
130 /// raw config and a spec is refused when the session connects, with
131 /// [`ProxyError::TransportConfigAndQlog`] naming [`Leg::Upstream`], for
132 /// the reason written out on that variant.
133 ///
134 /// A spec on its own, with neither of the other two fields set, is
135 /// enough: the leg builds a `quinn::TransportConfig::default()` for the
136 /// sink to go on and dials with it, rather than dialling with nothing
137 /// and leaving the capture attached to a config no connection uses.
138 ///
139 /// `None` is how a leg says it does not want a capture. A spec that
140 /// names no writer is not that — it is refused with
141 /// [`ProxyError::Qlog`], because a spec
142 /// is how a caller *asks* for a capture.
143 ///
144 /// # Taken by the first connection this session dials
145 ///
146 /// A [`QlogSpec`](crate::qlog::QlogSpec) owns its writer and is consumed
147 /// when it becomes a sink, so it has no `Clone` and there is exactly one
148 /// of it. [`ProxySession::new`] moves it out of this config and the
149 /// session's dial takes it, which is the only shape in which a single
150 /// writer belongs to a single connection.
151 ///
152 /// Two consequences worth stating rather than discovering. A
153 /// [`TransparentProxy`](crate::proxy::TransparentProxy) rebuilds this
154 /// config per accepted connection out of a shared template, so it cannot
155 /// carry a spec at all — and rather than dropping the field and coming
156 /// up, its `run` **refuses** a template that holds one, with
157 /// [`ProxyError::QlogOnProxyTemplate`] naming [`Leg::Upstream`]. Capture
158 /// a relay leg by driving [`ProxySession`] directly, one spec and one
159 /// writer per session. And a WebTransport upstream ignores this exactly as it
160 /// ignores `upstream_transport_config` — `wtransport` builds that
161 /// endpoint — which for a capture means a file that exists, parses,
162 /// names a qlog version and will never hold an event. There is no
163 /// refusal for it, because the step that builds the sink is the step
164 /// shared with the client leg, which has no upstream transport to
165 /// dispatch on. Capture the client leg instead — that endpoint is
166 /// always QUIC, even for a WebTransport client.
167 ///
168 /// [`ProxyError::TransportConfigAndQlog`]: crate::error::ProxyError::TransportConfigAndQlog
169 /// [`ProxyError::QlogOnProxyTemplate`]: crate::error::ProxyError::QlogOnProxyTemplate
170 #[cfg(feature = "qlog")]
171 pub upstream_qlog: Option<crate::qlog::QlogSpec>,
172 /// The socket every datagram of the **upstream** connection is sent
173 /// on and received from.
174 ///
175 /// `None` binds an ephemeral `0.0.0.0:0` socket, which is what this
176 /// session has always done. `Some(_)` builds the upstream endpoint
177 /// over the caller's socket instead, so a decorating implementation —
178 /// a tap, a counter, a network-impairment shim — sees and can alter
179 /// the whole relay leg. Ownership is shared, so the caller keeps its
180 /// handle on the socket while the session runs, and the relay sees the
181 /// supplied socket's address as this proxy's.
182 ///
183 /// This is the relay leg only. The client-facing leg is a separate
184 /// endpoint over a separate socket, supplied — or not — when the
185 /// listener is built.
186 ///
187 /// # A WebTransport upstream cannot honour this
188 ///
189 /// `upstream_transport_config` above is *ignored* for WebTransport
190 /// upstreams, because `wtransport` builds their endpoint. A socket is
191 /// not: it is refused. Connecting with
192 /// [`UpstreamTransportType::WebTransport`] and a socket set returns
193 /// [`ProxyError::UpstreamSocketUnsupported`] and connects to nothing.
194 ///
195 /// The two are treated differently because the consequences of
196 /// ignoring them are. A dropped transport config yields quinn's
197 /// defaults — a connection that works, with windows the caller did not
198 /// pick. A dropped socket yields a relay leg that bypasses the
199 /// caller's shim entirely, so every impairment armed on it is reported
200 /// by the shim and applied to nothing, and the run looks clean because
201 /// it *is* clean. That failure is invisible from the outside, so it is
202 /// made loud here instead.
203 ///
204 /// # One socket, one session
205 ///
206 /// Each session builds its own endpoint over the socket it is handed.
207 /// Two endpoints reading one socket take each other's datagrams —
208 /// whichever polls first gets a packet, and a packet for a connection
209 /// an endpoint does not own is discarded — so a socket shared across
210 /// sessions running concurrently breaks all of them. Give concurrent
211 /// sessions one socket each.
212 pub upstream_socket: Option<Arc<dyn quinn::AsyncUdpSocket>>,
213 /// Engine-side knobs for action execution — the per-stream deferred
214 /// write queue's byte budget and the ceiling on a hold.
215 ///
216 /// Ignored when the hook's [`crate::hook::ProxyHook::interest`] is
217 /// [`Interest::NONE`]: nothing is ever queued, so nothing reads them.
218 pub egress: EgressConfig,
219 /// How this session's **media** egress is shaped — named token
220 /// buckets, the class rules that aim at them, one bounded-queue policy
221 /// and the discipline that arbitrates between classes.
222 ///
223 /// `None` is today's behaviour exactly: no scheduler is constructed,
224 /// nothing extra is queued, and no deadline is armed.
225 ///
226 /// `Some(_)` is **configuration, not a hook capability**, and that is
227 /// the whole point of the field: it arms framing on its own, with no
228 /// hook and no observer. A profile that only took effect when someone
229 /// also attached a hook would let a user configure 500 kbps, get a byte
230 /// pump, and read a successful run — which is the failure mode this
231 /// knob exists to make impossible. Conversely, attaching an observer
232 /// never arms shaping: see `shaping_enabled` on `ForwardCtx`.
233 ///
234 /// Control streams are never shaped, on any path.
235 pub shape: Option<ShapeProfile>,
236}
237
238impl ProxySessionConfig {
239 /// Returns the ALPN protocol identifiers for the upstream connection.
240 ///
241 /// For QUIC upstreams, mirrors the negotiated client ALPN so we connect
242 /// to the relay with the same protocol the client is speaking. Falls
243 /// back to `self.draft.quic_alpn()` if the client ALPN is empty
244 /// (e.g., the listener didn't capture it).
245 pub fn upstream_alpn(&self, client_alpn: &[u8]) -> Vec<Vec<u8>> {
246 match &self.upstream_transport {
247 UpstreamTransportType::Quic => {
248 if client_alpn.is_empty() {
249 vec![self.draft.quic_alpn().to_vec()]
250 } else {
251 vec![client_alpn.to_vec()]
252 }
253 }
254 UpstreamTransportType::WebTransport { .. } => vec![b"h3".to_vec()],
255 }
256 }
257}
258
259impl Default for ProxySessionConfig {
260 fn default() -> Self {
261 Self {
262 draft: crate::capability::DEFAULT_DRAFT,
263 upstream_transport: UpstreamTransportType::Quic,
264 upstream_addr: String::new(),
265 skip_upstream_cert_verify: false,
266 upstream_ca_certs: Vec::new(),
267 upstream_connect_timeout_secs: 0,
268 upstream_transport_config: None,
269 upstream_transport_profile: None,
270 upstream_installer: None,
271 #[cfg(feature = "qlog")]
272 upstream_qlog: None,
273 upstream_socket: None,
274 egress: EgressConfig::default(),
275 shape: None,
276 }
277 }
278}
279
280/// A proxy session that forwards traffic between a client and an upstream
281/// relay. One session is created per accepted client connection.
282pub struct ProxySession {
283 session_id: SessionId,
284 config: ProxySessionConfig,
285 /// The ALPN the client negotiated with us (empty for WebTransport or
286 /// when unavailable). Drives both upstream ALPN selection and initial
287 /// draft detection for drafts 15+.
288 client_alpn: Vec<u8>,
289 observer: Arc<dyn ProxyObserver>,
290 hook: Arc<dyn ProxyHook>,
291 cancel: CancellationToken,
292 /// This session's slow-path counters, shared with every forwarding
293 /// task. One per session, not per process: a scenario asserting that a
294 /// session touched no slow path must not be spoiled by another session
295 /// running beside it.
296 counters: Arc<Recorder>,
297 /// This session's shaping counters, shared with every forwarding task
298 /// the same way `counters` is. A **sibling** of `Recorder`, not an
299 /// extension of it: `Counters` is compared whole against
300 /// `Counters::default()` by `tests/interest_none.rs` and by value
301 /// elsewhere, and it would lose `Copy` for a `Vec` that is empty on
302 /// every unshaped session.
303 ///
304 /// Always constructed, including when `config.shape` is `None`, for the
305 /// same reason `StreamRegistry` is: a structure that only existed when a
306 /// profile was configured would make the reports that name it
307 /// conditional on configuration nobody reading them can see. Its rows
308 /// are pre-sized from the profile's class list at this point and never
309 /// resized, so moving a running session to a different class list means
310 /// building a new session-scoped recorder rather than resizing this one.
311 shape_stats: Arc<ShapeRecorder>,
312 /// This session's attachment to its proxy's control plane, or `None`
313 /// when it has no proxy.
314 ///
315 /// `None` is not a degraded mode. A session constructed directly — which
316 /// is how this crate's own tests drive one, and how a caller that wants
317 /// one socket per session reaches the seam — belongs to no
318 /// [`TransparentProxy`](crate::proxy::TransparentProxy), so there is no
319 /// plane for it to register with and no
320 /// [`ProxyControl`](crate::control::ProxyControl) that could name it.
321 /// Making it an `Option` rather than always constructing one is what
322 /// keeps that honest: an unattached session cannot appear in a list of
323 /// live sessions belonging to a proxy that never accepted it.
324 control: Option<ControlAttachment>,
325 /// This session's relay-leg capture, until the dial takes it.
326 ///
327 /// Moved out of [`ProxySessionConfig::upstream_qlog`] when the session
328 /// is constructed, and out of here when it connects, because a spec owns
329 /// its writer and is consumed the moment it becomes a sink. It lives
330 /// beside the config rather than in it because the dial happens through
331 /// `&self` — a session is driven from behind an `Arc` — and there is no
332 /// way to take a value out of a shared reference.
333 ///
334 /// A `Mutex` and not a `OnceLock` or an atomic: the value is moved *out*
335 /// exactly once and the type has to allow that. The lock is taken once
336 /// per session, before the relay is dialled, and is never held across an
337 /// await.
338 ///
339 /// So a session run a second time dials without a capture. That is the
340 /// truthful answer rather than a limitation to work around — the writer
341 /// belongs to the connection that took it, and a second connection
342 /// writing into the same file would put both of their records behind one
343 /// preamble with nothing marking where either begins.
344 #[cfg(feature = "qlog")]
345 upstream_qlog: Mutex<Option<crate::qlog::QlogSpec>>,
346}
347
348impl ProxySession {
349 /// Create a new proxy session.
350 ///
351 /// `client_alpn` should be the ALPN the listener negotiated with the
352 /// client. Pass an empty slice if unavailable (e.g., WebTransport).
353 pub fn new(
354 session_id: SessionId,
355 #[cfg_attr(not(feature = "qlog"), allow(unused_mut))] mut config: ProxySessionConfig,
356 client_alpn: Vec<u8>,
357 observer: Arc<dyn ProxyObserver>,
358 hook: Arc<dyn ProxyHook>,
359 cancel: CancellationToken,
360 ) -> Self {
361 let shape_stats = Arc::new(ShapeRecorder::for_profile(config.shape.as_ref()));
362 // Taken out of the config here, and out of the session when it
363 // dials. The dial has only `&self` to work with, and a spec is a
364 // value that has to be moved to be used at all.
365 #[cfg(feature = "qlog")]
366 let upstream_qlog = Mutex::new(config.upstream_qlog.take());
367 Self {
368 session_id,
369 config,
370 client_alpn,
371 observer,
372 hook,
373 cancel,
374 counters: Arc::new(Recorder::new()),
375 shape_stats,
376 control: None,
377 #[cfg(feature = "qlog")]
378 upstream_qlog,
379 }
380 }
381
382 /// Attach this session to a proxy's control plane.
383 ///
384 /// Called by the accept loop between constructing the session and
385 /// spawning it, which is the only window in which the session is still
386 /// owned exclusively. It mints the command channel but registers
387 /// nothing: registration happens when the session begins to run, so that
388 /// the entry's lifetime is the session's and not this call's.
389 ///
390 /// It also **replaces** the shaping recorder, with one that forwards
391 /// everything it is charged into the proxy's own counters as well. A
392 /// second recorder installed beside the first would need a second set of
393 /// call sites on the data path, and a figure added to one and forgotten
394 /// at the other is a divergence nothing would report; forwarding from
395 /// inside means one call charges both or neither.
396 ///
397 /// Replacing rather than mutating is what that window buys. Nothing has
398 /// run, so the recorder being discarded is all zeros, and nothing has
399 /// cloned it — `ForwardCtx` takes its `Arc` when the session starts
400 /// forwarding, which is after this returns — so every task will hold the
401 /// recorder that reports to the proxy, not a mixture.
402 pub(crate) fn attach_control(&mut self, plane: Arc<ControlPlane>) {
403 self.shape_stats =
404 Arc::new(ShapeRecorder::attached(self.config.shape.as_ref(), plane.stats_recorder()));
405 self.control = Some(ControlAttachment::new(plane));
406 }
407
408 /// This session's slow-path counters.
409 ///
410 /// Replaces the deleted process-global `instrument::snapshot()`. Cheap:
411 /// a read of ~12 relaxed atomics plus a 128-slot histogram scan.
412 ///
413 /// A session whose hook declared [`Interest::NONE`] and whose observer
414 /// answers `false` to `wants_events` ends with
415 /// `counters() == Counters::default()` — that is what makes the
416 /// fast-path claim falsifiable rather than promised.
417 pub fn counters(&self) -> Counters {
418 self.counters.snapshot()
419 }
420
421 /// This session's shaping statistics.
422 ///
423 /// Readable **while the session runs**, which is the point: the
424 /// `ProxySession` is constructed behind an `Arc` before the accept task
425 /// is spawned (`tests/common/mod.rs`), so a scenario can sample its
426 /// classes without waiting for teardown and without a control plane.
427 ///
428 /// A session with no [`ShapeProfile`] ends — and begins, and stays — at
429 /// `shape_stats() == ShapeStats::default()`. That is a falsifiable
430 /// claim rather than a promise only because the shaping path does move
431 /// these counters when it is entered: see
432 /// [`ShapeStats::objects_seen`].
433 ///
434 /// Allocates one `Vec` and one `String` per configured class. Cheap,
435 /// but not free — this is a reader's call, not a data-path one.
436 pub fn shape_stats(&self) -> ShapeStats {
437 self.shape_stats.snapshot()
438 }
439
440 /// Run the proxy session with a raw QUIC client connection.
441 pub async fn run(&self, client_conn: quinn::Connection) -> Result<(), ProxyError> {
442 let client = Transport::Quic(QuicTransport::new(client_conn));
443 self.run_with_transport(client).await
444 }
445
446 /// Run the proxy session with a WebTransport client connection.
447 #[cfg(feature = "webtransport")]
448 pub async fn run_webtransport(
449 &self,
450 client_conn: wtransport::Connection,
451 ) -> Result<(), ProxyError> {
452 use moqtap_client::transport::webtransport::WebTransportTransport;
453 let client = Transport::WebTransport(WebTransportTransport::new(client_conn));
454 self.run_with_transport(client).await
455 }
456
457 /// The draft this session starts on. Drafts 15+ resolve unambiguously
458 /// from the client ALPN (`moqt-15` through `moqt-19`); otherwise we fall
459 /// back to `config.draft`, which the control stream refines once it
460 /// peeks at CLIENT_SETUP / SERVER_SETUP for the moq-00 cohort (drafts
461 /// 07–14).
462 ///
463 /// It is the *starting* draft and not the session's draft. That lives in
464 /// [`SessionDraft`], which every forwarding task reads and the control
465 /// stream writes.
466 fn initial_draft(&self) -> DraftVersion {
467 DraftVersion::from_alpn(&self.client_alpn).unwrap_or(self.config.draft)
468 }
469
470 /// Whether the starting draft is fixed (ALPN-derived) or is still open
471 /// to being named by a CLIENT_SETUP / SERVER_SETUP peek.
472 fn draft_is_fixed(&self) -> bool {
473 DraftVersion::from_alpn(&self.client_alpn).is_some()
474 }
475
476 /// Run the proxy session with an already-wrapped transport.
477 ///
478 /// Connects to the upstream relay, then forwards all streams and
479 /// datagrams bidirectionally between the client and relay. Parses
480 /// MoQT frames inline and emits events via the observer.
481 async fn run_with_transport(&self, client: Transport) -> Result<(), ProxyError> {
482 // Registered before the relay is dialled, and released by this
483 // function's scope rather than by a call at each of the several
484 // places the session can end. The guard covers the `?` below on a
485 // failed upstream connect, every return at the bottom, and this
486 // whole future being dropped by whoever spawned it — the last of
487 // which no enumerated teardown site would have covered. A session
488 // that stayed in the list after ending is the failure to avoid: the
489 // list would grow for the life of the proxy and every request naming
490 // a stale id would fail in a way that looks like a race.
491 //
492 // Everything the registration hands out is built here, above the
493 // dial, for the same reason the registration itself is: connecting
494 // to the relay is the longest single thing a session does, and a
495 // session that only became reachable afterwards would be
496 // unreachable for exactly as long as that took — including forever,
497 // on a relay that never answers. None of these four needs the relay.
498
499 // Two admission checks, both before the relay is dialled, before a
500 // registration exists and before a byte moves.
501 //
502 // The first is the draft this session will frame with. `DraftVersion`
503 // carries every variant under every feature set, so a build made with
504 // a reduced draft set can be configured for a draft it holds no codec
505 // for, and nothing about that configuration looks wrong. Such a
506 // session runs: every stream is bypassed as undecodable, no object
507 // reaches a hook, no class claims anything, and the run reports
508 // success — a byte pump that cannot be told apart from a quiet one.
509 //
510 // It is checked ahead of the shaping rules because a shaping rule is
511 // judged *against* a draft, and asking whether a rule suits a draft
512 // this build cannot frame answers with a matcher key when what is
513 // wrong is the build.
514 let draft = self.initial_draft();
515 if !crate::capability::draft_is_compiled(draft) {
516 return Err(ProxyError::DraftNotCompiled { draft });
517 }
518
519 // The second is the shaping profile: a rule keyed on a field this
520 // draft's units do not carry can never claim anything, so a session
521 // that ran with one would pace nothing, report shaping, and end
522 // green. The rule is dead configuration and the only useful moment to
523 // say so is the one before the run rather than during it.
524 //
525 // Checked here against the draft the session starts on, and checked
526 // a second time further down against the draft the peers name, if
527 // that turns out to be a different one. Both, rather than one or the
528 // other: this one is the only check that can refuse a session
529 // *before* it dials, and the later one is the only check that can
530 // see an answer the `moq-00` cohort does not carry in its ALPN. A
531 // rule this one refuses is dead on the draft the session was about
532 // to use, whatever the peers go on to say.
533 if let Some(profile) = self.config.shape.as_ref() {
534 Capabilities::for_draft(draft)
535 .admit_profile(profile)
536 .map_err(|source| ProxyError::ShapeRuleUnsupported { source })?;
537 }
538
539 let closer = SessionCloser::new(self.cancel.clone());
540 let streams = Arc::new(StreamRegistry::new());
541 let gauge = EgressGauge::new();
542 // One request channel per control-stream direction. Created before
543 // the control stream exists so that both halves have a home from
544 // the first instant: the sending halves go into the registry now,
545 // and the receiving halves are served by the two control pipes once
546 // `forward_control_stream` has streams to pipe.
547 let client_leg = ControlLeg::new();
548 let upstream_leg = ControlLeg::new();
549
550 let _registration = self.control.as_ref().map(|c| {
551 c.register(
552 self.session_id,
553 self.cancel.clone(),
554 closer.clone(),
555 Arc::clone(&streams),
556 [client_leg.inbox.clone(), upstream_leg.inbox.clone()],
557 self.config.egress,
558 )
559 });
560
561 // Connect to upstream relay
562 let relay = self.connect_upstream().await?;
563
564 let client = Arc::new(client);
565 let relay = Arc::new(relay);
566
567 let mut tasks: JoinSet<Result<(), ProxyError>> = JoinSet::new();
568
569 let initial_draft = self.initial_draft();
570 let draft_is_fixed = self.draft_is_fixed();
571 // One cell, shared by every task below. Built here because this is
572 // where the tasks are: the control stream learns the draft and the
573 // data, datagram and request tasks have to agree with it, and they
574 // are all spawned from this scope within a few lines of each other.
575 let session_draft = Arc::new(SessionDraft::new(initial_draft, draft_is_fixed));
576
577 // ── The gating expression ───────────────────────────────────
578 //
579 // `objects_enabled` is the *framing* gate — which pipe function
580 // `pipe_data` calls — and keeps its `observer_enabled ||` term
581 // because `ProxyEvent::Object` fires for an observer alone.
582 // `object_hook` is the *hook* gate. Collapsing the two would make
583 // an event observer attached to an `Interest::NONE` hook start
584 // calling — and honouring the `Action` returned by — a hook that
585 // declared no object interest.
586 //
587 // `shaping_enabled` is the third gate, and it deliberately has
588 // **no `observer_enabled ||` term** — the same asymmetry, for the
589 // same reason, as `object_hook`. A `ShapeProfile` is
590 // configuration; attaching an event observer must not start pacing
591 // production traffic. It is a term of `objects_enabled` because
592 // classification needs `ObjectMeta`, which only the framer
593 // produces: a configured profile has to arm framing on its own,
594 // with `Interest::NONE` and no observer, or the user gets a byte
595 // pump and a green run.
596 let interest = self.hook.interest();
597 let observer_enabled = self.observer.wants_events();
598 let shaping_enabled = self.config.shape.is_some();
599 let objects_enabled =
600 observer_enabled || interest.contains(Interest::OBJECTS) || shaping_enabled;
601 let object_hook = interest.contains(Interest::OBJECTS);
602 let control_mutation = interest.contains(Interest::CONTROL);
603 // A fourth reason to decode control frames, and the only one that is
604 // not about telling somebody. Drafts 18 and 19 write a fetch Object's
605 // Group ID as a difference whose sign the fetch's Group Order decides,
606 // and the order is on the FETCH — so on those two a session that
607 // frames data has to read its own control plane or it cannot read its
608 // own fetch streams. See `capability::fetch_group_order_is_needed`.
609 //
610 // The initial draft is exact here for the same reason it is in
611 // `bidi_streams_carry_requests`: drafts 18 and 19 have an ALPN each,
612 // and the one cohort that is a guess, `moq-00`, spans drafts 07 to 14
613 // and answers `false` for every member.
614 let fetch_orders_wanted = objects_enabled && fetch_group_order_is_needed(initial_draft);
615 let control_parse = observer_enabled || control_mutation;
616 let streams_enabled = interest.contains(Interest::STREAMS);
617 let datagram_hook = interest.contains(Interest::DATAGRAMS);
618
619 let base_ctx =
620 ForwardCtx {
621 session_id: self.session_id,
622 draft: Arc::clone(&session_draft),
623 draft_is_fixed,
624 observer: Arc::clone(&self.observer),
625 hook: Arc::clone(&self.hook),
626 cancel: self.cancel.clone(),
627 counters: Arc::clone(&self.counters),
628 shape_stats: Arc::clone(&self.shape_stats),
629 closer: closer.clone(),
630 egress: self.config.egress,
631 observer_enabled,
632 objects_enabled,
633 object_hook,
634 shaping_enabled,
635 // One shaper per session, shared by every forwarding task
636 // through the `Arc` — the class rules, the queue policy and
637 // the report-once state for `ShapeRuleUnmatchable` are all
638 // session-scoped, and a per-task copy would report the same
639 // unmatchable rule once per stream.
640 //
641 // Wrapped rather than held directly because a proxy can replace
642 // its profile while this session runs; see [`SessionShaper`] for
643 // what that costs and where the replacement is allowed to land.
644 shape: self.config.shape.clone().map(|p| {
645 Arc::new(SessionShaper::new(p, self.control.as_ref().map(|c| c.plane())))
646 }),
647 control_mutation,
648 control_parse,
649 fetch_orders_wanted,
650 // Always constructed, like `streams` and for the same reason:
651 // an empty table allocates nothing and touches no counter, so
652 // an `Option` here would buy nothing and would give the two
653 // control pipes a second thing to be conditional about.
654 fetch_orders: Arc::new(FetchGroupOrders::default()),
655 streams_enabled,
656 datagram_hook,
657 next_stream_id: Arc::new(AtomicU64::new(0)),
658 streams: Arc::clone(&streams),
659 gauge: Arc::clone(&gauge),
660 };
661
662 // The command task for this session's control-plane requests.
663 //
664 // Spawned here, and not into `tasks`, on purpose: the `JoinSet`
665 // below treats the *first* task to finish as the end of the session,
666 // so a task that returns when its channel closes would tear down a
667 // perfectly healthy session. It is deliberately spawned from inside
668 // this scope rather than beside the session's construction, because
669 // this is the first point at which the session's closer, its stream
670 // registry and both transport handles exist at once — everything a
671 // request could want to touch is reachable from the context cloned
672 // into it. `AbortOnDrop` ends it if this future is dropped without
673 // the cancellation token ever firing.
674 let _commands = self.control.as_ref().and_then(|c| c.take_inbox()).map(|inbox| {
675 let ctx = base_ctx.clone();
676 AbortOnDrop::new(tokio::spawn(serve_session_commands(inbox, ctx)))
677 });
678
679 // ── The shaping profile, judged again against the wire's draft ──
680 //
681 // The check above ran before the dial, on the draft the session
682 // started with. For the `moq-00` cohort that is a configured guess,
683 // because drafts 07 to 14 share one ALPN — and the peers name the
684 // real one in their SETUP a few milliseconds later. This is the same
685 // question asked of that answer.
686 //
687 // It runs *only* where the two can differ, so an ALPN-fixed session
688 // spawns nothing here and pays nothing. It is spawned into `tasks`
689 // rather than beside them because the `JoinSet` reads the first
690 // completion as the end of the session, which is exactly the
691 // treatment a dead profile deserves: the session ends naming the
692 // class and the key, instead of pacing nothing and reporting
693 // success. Having judged, it holds its slot until the session ends
694 // some other way.
695 if !draft_is_fixed {
696 if let Some(profile) = self.config.shape.clone() {
697 let ctx = base_ctx.clone();
698 tasks.spawn(async move {
699 let draft = ctx.resolved_draft().await;
700 // A session already going down is not judged. The wait
701 // above ends on cancellation as well as on an answer,
702 // and a refusal returned there would replace whatever
703 // actually ended the session with a verdict on a profile
704 // that is no longer going to shape anything.
705 if draft != initial_draft && !ctx.cancel.is_cancelled() {
706 Capabilities::for_draft(draft)
707 .admit_profile(&profile)
708 .map_err(|source| ProxyError::ShapeRuleUnsupported { source })?;
709 }
710 ctx.cancel.cancelled().await;
711 Ok(())
712 });
713 }
714 }
715
716 // ── Where the control plane is ──────────────────────────────
717 //
718 // Two questions, not one, and the draft answers them separately —
719 // see `control_plane_is_unidirectional` and
720 // `bidi_streams_carry_requests`, which quote the sections. On 07-15
721 // the control stream is the first client-initiated bidirectional
722 // stream and nothing else uses a bidirectional stream at all, so one
723 // task owns it. On 17-19 the control plane is a pair of
724 // unidirectional streams, one opened by each peer, and bidirectional
725 // streams carry requests — so the control legs travel with the
726 // unidirectional accept loops, which are the loops the control
727 // streams arrive on, and the bidirectional streams get accept loops
728 // of their own in both directions.
729 //
730 // Draft-16 answers one question each way and is the only draft that
731 // does: a bidirectional control stream, and request streams beside
732 // it. It takes the first branch's shape for the control stream and
733 // the second's for the requests.
734 //
735 // The mapping of a leg to a loop is the same half-turn
736 // `forward_control_stream` makes for its two pipes: a message the
737 // relay is meant to decode — `Leg::Upstream`, the `upstream_leg` —
738 // is written by the pipe forwarding *from* the client, so it goes
739 // to the client-to-relay loop.
740 let (client_uni_leg, relay_uni_leg) = if control_plane_is_unidirectional(initial_draft) {
741 for (source, dest, side) in [
742 (Arc::clone(&client), Arc::clone(&relay), ProxySide::ClientToProxy),
743 (Arc::clone(&relay), Arc::clone(&client), ProxySide::RelayToProxy),
744 ] {
745 let ctx = base_ctx.clone();
746 tasks.spawn(
747 async move { forward_request_streams(&source, &dest, side, &ctx).await },
748 );
749 }
750 (Some(upstream_leg), Some(client_leg))
751 } else {
752 // Draft-16 has request streams beside its bidirectional control
753 // stream, and either endpoint opens one. The relay's are taken
754 // here; the client's are taken inside `forward_control_stream`,
755 // after it has taken the control stream, because that is the same
756 // transport and only one accept may be outstanding on it.
757 if bidi_streams_carry_requests(initial_draft) {
758 let source = Arc::clone(&relay);
759 let dest = Arc::clone(&client);
760 let ctx = base_ctx.clone();
761 tasks.spawn(async move {
762 forward_request_streams(&source, &dest, ProxySide::RelayToProxy, &ctx).await
763 });
764 }
765 let client = Arc::clone(&client);
766 let relay = Arc::clone(&relay);
767 let ctx = base_ctx.clone();
768 tasks.spawn(async move {
769 forward_control_stream(&client, &relay, &ctx, client_leg, upstream_leg).await
770 });
771 (None, None)
772 };
773
774 // Client → Relay uni streams
775 {
776 let client = Arc::clone(&client);
777 let relay = Arc::clone(&relay);
778 let ctx = base_ctx.clone();
779 tasks.spawn(async move {
780 forward_uni_streams(&client, relay, ProxySide::ClientToProxy, &ctx, client_uni_leg)
781 .await
782 });
783 }
784
785 // Relay → Client uni streams
786 {
787 let client = Arc::clone(&client);
788 let relay = Arc::clone(&relay);
789 let ctx = base_ctx.clone();
790 tasks.spawn(async move {
791 forward_uni_streams(&relay, client, ProxySide::RelayToProxy, &ctx, relay_uni_leg)
792 .await
793 });
794 }
795
796 // Datagram forwarding: client → relay
797 {
798 let client = Arc::clone(&client);
799 let relay = Arc::clone(&relay);
800 let ctx = base_ctx.clone();
801 tasks.spawn(async move {
802 forward_datagrams(&client, &relay, ProxySide::ClientToProxy, &ctx).await
803 });
804 }
805
806 // Datagram forwarding: relay → client
807 {
808 let client = Arc::clone(&client);
809 let relay = Arc::clone(&relay);
810 let ctx = base_ctx.clone();
811 tasks.spawn(async move {
812 forward_datagrams(&relay, &client, ProxySide::RelayToProxy, &ctx).await
813 });
814 }
815
816 // Wait for first task to finish (signals session is done)
817 let first_result = tasks.join_next().await;
818
819 // Cancel remaining tasks
820 self.cancel.cancel();
821 tasks.shutdown().await;
822
823 // A hook that asked for a close is the reason, whatever the task
824 // that noticed the cancellation reported.
825 let reason = match closer.requested() {
826 Some((code, why, origin)) => {
827 // Named, not assumed. A close reaches the same latch from a
828 // hook's `Action::CloseSession` and from
829 // `ProxyControl::close_session`, and reporting both as the
830 // hook's told an observer that the scenario under test ended
831 // the session when the operator outside it had.
832 let who = match origin {
833 CloseOrigin::Hook => "hook",
834 CloseOrigin::ControlPlane => "control plane",
835 };
836 format!(
837 "{who} closed the session: code {code}, reason {:?}",
838 String::from_utf8_lossy(&why)
839 )
840 }
841 None => match &first_result {
842 Some(Ok(Ok(()))) => "completed".to_string(),
843 Some(Ok(Err(e))) => format!("{e}"),
844 Some(Err(e)) => format!("task panic: {e}"),
845 None => "no tasks".to_string(),
846 },
847 };
848 if self.observer.wants_events() {
849 self.observer
850 .on_event(&ProxyEvent::SessionEnded { session_id: self.session_id, reason });
851 }
852
853 // Close both sides. `close_args` is the pair a hook's
854 // `Action::CloseSession` recorded, or the proxy's own default when
855 // no hook asked for anything.
856 let (close_code, close_reason) = closer.close_args();
857 client.close(close_code, &close_reason);
858 relay.close(close_code, &close_reason);
859
860 match first_result {
861 Some(Ok(Ok(()))) | None => Ok(()),
862 Some(Ok(Err(e))) => Err(e),
863 Some(Err(e)) => Err(ProxyError::SessionClosed(format!("task panic: {e}"))),
864 }
865 }
866
867 /// Connect to the upstream relay (with optional timeout).
868 async fn connect_upstream(&self) -> Result<Transport, ProxyError> {
869 let timeout_secs = self.config.upstream_connect_timeout_secs;
870 if timeout_secs > 0 {
871 tokio::time::timeout(
872 std::time::Duration::from_secs(timeout_secs),
873 self.connect_upstream_inner(),
874 )
875 .await
876 .map_err(|_| {
877 ProxyError::UpstreamConnect(format!("connection timed out after {timeout_secs}s"))
878 })?
879 } else {
880 self.connect_upstream_inner().await
881 }
882 }
883
884 async fn connect_upstream_inner(&self) -> Result<Transport, ProxyError> {
885 // Resolved out here rather than inside the QUIC arm, and ahead of
886 // every other refusal below, because naming both a raw config and a
887 // profile is a contradiction in what the caller wrote — it is not a
888 // fact about the transport they picked, and it is answerable
889 // without touching the network. A WebTransport upstream reaches
890 // this line too, where both fields would then be ignored: a
891 // contradiction reported on one transport and swallowed on the
892 // other would be a rule that holds only where someone happened to
893 // test it.
894 //
895 // A capture is the one thing this line has a side effect for. The
896 // sink is built here, which writes the capture's preamble, so a
897 // WebTransport upstream carrying a spec leaves a file that exists
898 // and holds no event — `wtransport` builds that endpoint and never
899 // sees the config the sink went on. That is documented on the field
900 // rather than refused, and this is the reason it cannot be refused
901 // cheaply: the step that builds the sink is the step shared with
902 // the client leg, which has no transport to dispatch on, and moving
903 // it below the match to gain one would take the contradiction check
904 // down there with it — where a WebTransport upstream would stop
905 // hearing about the pair it is being refused for today.
906 let transport_config = transport::resolve(
907 Leg::Upstream,
908 self.config.upstream_transport_config.clone(),
909 self.config.upstream_transport_profile.as_ref(),
910 self.config.upstream_installer.as_ref(),
911 // Taken, not cloned: there is one writer and it belongs to this
912 // dial. A session dialled twice therefore captures the first
913 // connection and not the second, which is the only division of
914 // one writer between two connections that produces a readable
915 // file.
916 #[cfg(feature = "qlog")]
917 self.upstream_qlog.lock().expect("no session holds this across a panic").take(),
918 )?;
919
920 match &self.config.upstream_transport {
921 UpstreamTransportType::Quic => self.connect_upstream_quic(transport_config).await,
922 // Ahead of both `webtransport` arms on purpose: whether the
923 // feature is compiled in changes which *other* error a
924 // WebTransport upstream produces, and this refusal is about
925 // the socket rather than about the transport being reachable.
926 // A caller who supplied a socket must hear that it cannot be
927 // honoured, in either build.
928 UpstreamTransportType::WebTransport { .. } if self.config.upstream_socket.is_some() => {
929 Err(ProxyError::UpstreamSocketUnsupported)
930 }
931 #[cfg(feature = "webtransport")]
932 UpstreamTransportType::WebTransport { url } => {
933 let url = url.clone();
934 self.connect_upstream_webtransport(&url).await
935 }
936 #[cfg(not(feature = "webtransport"))]
937 UpstreamTransportType::WebTransport { .. } => {
938 Err(ProxyError::UpstreamConnect("webtransport feature not enabled".to_string()))
939 }
940 }
941 }
942
943 /// Connect to the upstream relay via QUIC.
944 ///
945 /// `transport_config` is what this leg resolved to before anything was
946 /// built — the caller's raw config, or one built from their profile, or
947 /// `None` for quinn's defaults. It arrives as an argument rather than
948 /// being read from `self.config` here so that there is exactly one
949 /// place the two fields are reconciled, and so that the reconciliation
950 /// happens before the transport is even dispatched on.
951 async fn connect_upstream_quic(
952 &self,
953 transport_config: Option<Arc<quinn::TransportConfig>>,
954 ) -> Result<Transport, ProxyError> {
955 let server_addr =
956 self.config.upstream_addr.parse().map_err(|e: std::net::AddrParseError| {
957 ProxyError::UpstreamConnect(e.to_string())
958 })?;
959
960 let mut tls_config = self.build_upstream_tls_config()?;
961 tls_config.alpn_protocols = self.config.upstream_alpn(&self.client_alpn);
962
963 let quic_config: quinn::crypto::rustls::QuicClientConfig =
964 tls_config.try_into().map_err(|e| ProxyError::TlsConfig(format!("{e}")))?;
965 let mut client_config = quinn::ClientConfig::new(Arc::new(quic_config));
966 if let Some(transport) = transport_config {
967 client_config.transport_config(transport);
968 }
969
970 // A supplied socket replaces the bind, and nothing else: the same
971 // client config, the same ALPN and the same `connect` follow. The
972 // endpoint takes no `ServerConfig` on either branch — this one
973 // only ever dials.
974 let mut endpoint = match &self.config.upstream_socket {
975 Some(socket) => {
976 let runtime = quinn::default_runtime().ok_or_else(|| {
977 ProxyError::UpstreamConnect("no async runtime found".to_string())
978 })?;
979 quinn::Endpoint::new_with_abstract_socket(
980 quinn::EndpointConfig::default(),
981 None,
982 Arc::clone(socket),
983 runtime,
984 )
985 .map_err(|e| ProxyError::UpstreamConnect(e.to_string()))?
986 }
987 None => quinn::Endpoint::client("0.0.0.0:0".parse().unwrap())
988 .map_err(|e| ProxyError::UpstreamConnect(e.to_string()))?,
989 };
990 endpoint.set_default_client_config(client_config);
991
992 let server_name =
993 self.config.upstream_addr.split(':').next().unwrap_or("localhost").to_string();
994
995 let conn = endpoint
996 .connect(server_addr, &server_name)
997 .map_err(|e| ProxyError::UpstreamConnect(e.to_string()))?
998 .await
999 .map_err(|e| ProxyError::UpstreamConnect(e.to_string()))?;
1000
1001 Ok(Transport::Quic(QuicTransport::new(conn)))
1002 }
1003
1004 /// Connect to the upstream relay via WebTransport.
1005 #[cfg(feature = "webtransport")]
1006 async fn connect_upstream_webtransport(&self, url: &str) -> Result<Transport, ProxyError> {
1007 use moqtap_client::transport::webtransport::WebTransportTransport;
1008
1009 let wt_config = if self.config.skip_upstream_cert_verify {
1010 wtransport::ClientConfig::builder()
1011 .with_bind_default()
1012 .with_no_cert_validation()
1013 .build()
1014 } else {
1015 wtransport::ClientConfig::builder().with_bind_default().with_native_certs().build()
1016 };
1017
1018 let endpoint = wtransport::Endpoint::client(wt_config)
1019 .map_err(|e| ProxyError::UpstreamConnect(e.to_string()))?;
1020
1021 let connection =
1022 endpoint.connect(url).await.map_err(|e| ProxyError::UpstreamConnect(e.to_string()))?;
1023
1024 Ok(Transport::WebTransport(WebTransportTransport::new(connection)))
1025 }
1026
1027 /// Build a rustls `ClientConfig` for the upstream connection.
1028 fn build_upstream_tls_config(&self) -> Result<rustls::ClientConfig, ProxyError> {
1029 if self.config.skip_upstream_cert_verify {
1030 Ok(rustls::ClientConfig::builder()
1031 .dangerous()
1032 .with_custom_certificate_verifier(Arc::new(SkipVerification))
1033 .with_no_client_auth())
1034 } else {
1035 let mut roots = rustls::RootCertStore::empty();
1036 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
1037 for der in &self.config.upstream_ca_certs {
1038 roots
1039 .add(rustls::pki_types::CertificateDer::from(der.clone()))
1040 .map_err(|e| ProxyError::TlsConfig(format!("bad CA cert: {e}")))?;
1041 }
1042 Ok(rustls::ClientConfig::builder().with_root_certificates(roots).with_no_client_auth())
1043 }
1044 }
1045}
1046
1047// ── Forwarding helpers ──────────────────────────────────────────
1048
1049/// One session's shaper, and the proxy profile it watches.
1050///
1051/// A session builds a [`Scheduler`] from the profile it was configured with
1052/// and shares it through the whole forwarding scope. That much has not
1053/// changed. What this adds is a place to notice that the proxy has been
1054/// given a *different* profile while the session runs, and a rule about when
1055/// the session is allowed to act on it.
1056///
1057/// # The swap happens between streams, never inside one
1058///
1059/// [`Self::current`] is read once per forwarded stream, and the
1060/// `Arc<Scheduler>` it hands back is what that stream classifies with, queues
1061/// under and paces against for the whole of its life. A stream that is
1062/// already forwarding keeps the scheduler it started with even after the
1063/// profile has moved on.
1064///
1065/// That is forced rather than chosen. A `Class` is an index into a
1066/// scheduler's class list, and a stream's egress queue holds the scheduler
1067/// its units were admitted under. Swapping mid-stream would classify a unit
1068/// against one profile's rules and release it against another profile's
1069/// buckets and demand rows — charging a class that is not the one that was
1070/// matched, or, where the new list is shorter, a class that does not exist.
1071/// Reading it per stream costs one `Mutex` acquisition where a `PendingQueue`
1072/// is already being built.
1073///
1074/// # A profile with a different class list is not taken up at all
1075///
1076/// The session's [`ShapeRecorder`] has one row per configured class,
1077/// pre-sized when the session is constructed and never resized, and a class
1078/// is charged to its row by position. A profile whose class list differs
1079/// from the one those rows were named after would therefore keep every
1080/// number correct and make every label on it wrong. So a live profile is
1081/// taken up only when its class names match, in order, the ones this session
1082/// started with; otherwise the session keeps its own until it ends. Changing
1083/// the class list of a running session is done by ending it.
1084struct SessionShaper {
1085 /// The proxy this session belongs to, or `None` for a session driven
1086 /// directly rather than through an accept loop — which has no proxy, so
1087 /// no profile can be installed on it and this never looks.
1088 plane: Option<Arc<ControlPlane>>,
1089 /// The class names this session's statistics rows were pre-sized from,
1090 /// and the test a live profile has to pass to be taken up.
1091 classes: Vec<String>,
1092 /// The scheduler in force, and the profile generation it was built at.
1093 current: Mutex<CachedShaper>,
1094}
1095
1096/// What [`SessionShaper`] keeps behind its lock.
1097struct CachedShaper {
1098 /// The proxy profile generation this scheduler was built from. A
1099 /// mismatch against the plane's is the whole of the "something changed"
1100 /// signal — comparing profiles would clone one per stream.
1101 generation: u64,
1102 /// The scheduler every stream opened since the last swap is using.
1103 scheduler: Arc<Scheduler>,
1104}
1105
1106impl SessionShaper {
1107 /// Build the shaper for a session configured with `profile`.
1108 ///
1109 /// `profile` is what the session's statistics rows were pre-sized from,
1110 /// so its class list is the one every later swap is measured against. A
1111 /// profile installed on the proxy between the session's configuration
1112 /// being copied and this call is taken up here, under the same rule a
1113 /// later one would be — that window is short, but a session that ignored
1114 /// it would run on a profile the proxy had already replaced with no way
1115 /// to notice.
1116 fn new(profile: ShapeProfile, plane: Option<Arc<ControlPlane>>) -> Self {
1117 let classes: Vec<String> = profile.classes().iter().map(|c| c.name.clone()).collect();
1118 let (generation, scheduler) = match &plane {
1119 Some(plane) => {
1120 let shape = plane.shape();
1121 let (generation, live) = shape.snapshot();
1122 let chosen = match live {
1123 Some(live) if same_classes(&live, &classes) => live,
1124 _ => profile,
1125 };
1126 (generation, Scheduler::with_switch(chosen, shape.switch()))
1127 }
1128 // No proxy, so no switch to share and no generation to watch.
1129 // Pacing is on and stays on, which is what a session driven
1130 // directly has always done.
1131 None => (0, Scheduler::new(profile)),
1132 };
1133 Self {
1134 plane,
1135 classes,
1136 current: Mutex::new(CachedShaper { generation, scheduler: Arc::new(scheduler) }),
1137 }
1138 }
1139
1140 /// The scheduler the next stream should run under.
1141 ///
1142 /// Takes up a profile installed since the last call when its class list
1143 /// matches; otherwise hands back what this session already had. Either
1144 /// way the generation is recorded, so a profile this session declined is
1145 /// not re-examined once per stream for the rest of the run — and a
1146 /// *later* profile that does match is still taken up, because the
1147 /// comparison is always against the class list the session started with.
1148 fn current(&self) -> Arc<Scheduler> {
1149 let mut cached = self.current.lock().expect("session shaper");
1150 if let Some(plane) = &self.plane {
1151 let shape = plane.shape();
1152 if shape.generation() != cached.generation {
1153 let (generation, live) = shape.snapshot();
1154 cached.generation = generation;
1155 if let Some(live) = live {
1156 if same_classes(&live, &self.classes) {
1157 cached.scheduler = Arc::new(Scheduler::with_switch(live, shape.switch()));
1158 }
1159 }
1160 }
1161 }
1162 Arc::clone(&cached.scheduler)
1163 }
1164}
1165
1166/// Whether `profile` names exactly `classes`, in the same order.
1167///
1168/// Names and order, because that pair is what makes a `Class::Rule(index)`
1169/// mean the same thing to the scheduler that produced it and to the
1170/// statistics row it is charged to. Same names in a different order would
1171/// charge each class to another one's row without a single count going
1172/// missing.
1173fn same_classes(profile: &ShapeProfile, classes: &[String]) -> bool {
1174 profile.classes().len() == classes.len()
1175 && profile.classes().iter().zip(classes).all(|(rule, name)| &rule.name == name)
1176}
1177
1178/// How long a task that needs the session's draft waits for the control
1179/// stream to name one before running on the draft the session started with.
1180///
1181/// The wait exists for one race, and the race is a small one. Drafts 07 to
1182/// 14 all negotiate the same ALPN, so those sessions start on a configured
1183/// guess and learn the real answer from CLIENT_SETUP — which every draft in
1184/// that cohort puts first on the wire, ahead of the subscription exchange
1185/// any data stream comes out of. So the bytes that settle the draft have
1186/// already arrived by the time a data stream exists, and what is left to
1187/// wait for is one task being polled rather than a round trip. The window is
1188/// sized well above that and is not a latency budget: it is the point at
1189/// which the session stops believing a SETUP is coming.
1190///
1191/// It has to end, because a peer that opens a data stream having sent no
1192/// SETUP at all is not a session any draft describes, and such a session
1193/// still has to run rather than stall. When the window expires the session
1194/// settles on the draft it started with — at the lowest [`DraftSource`]
1195/// rank, so a SETUP that turns up afterwards still refines the streams that
1196/// come after it.
1197///
1198/// A session with no control stream at all never reaches the window; see
1199/// [`SessionDraft::control_stream_open`].
1200const DRAFT_SETTLE_WINDOW: Duration = Duration::from_millis(100);
1201
1202/// Where a session's draft came from, ranked by how much it is worth.
1203///
1204/// A later answer replaces an earlier one only if it outranks it, which is
1205/// what makes the order here the whole policy and keeps it in one place.
1206#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1207enum DraftSource {
1208 /// Nobody named a draft, so the session kept the one it was configured
1209 /// for. Two things produce it: [`DRAFT_SETTLE_WINDOW`] expiring, and a
1210 /// control stream whose first message is readable enough to say it is
1211 /// not a SETUP — in both cases there is nothing to learn from and the
1212 /// tasks waiting on an answer are better off with the starting draft
1213 /// than with the wait.
1214 ///
1215 /// The lowest rank, because it is not an answer at all: it is the
1216 /// absence of one, and a SETUP that turns up afterwards — on this
1217 /// direction or the other — must still be able to replace it.
1218 Fallback,
1219 /// The highest draft in the `moq-00` cohort that CLIENT_SETUP offered.
1220 /// An offer rather than an agreement: a server is free to select a lower
1221 /// version out of the same list.
1222 Offered,
1223 /// The version SERVER_SETUP selected. This is the one the two peers are
1224 /// actually speaking, so it outranks the client's offer.
1225 Selected,
1226 /// The ALPN, which names exactly one draft from 15 on and is known
1227 /// before a byte is read. Nothing can improve on it, so it outranks
1228 /// everything and the session never waits.
1229 Alpn,
1230}
1231
1232/// The draft this session frames with, and the one place every task reads it
1233/// from.
1234///
1235/// # Why a shared cell rather than a field
1236///
1237/// Drafts 07 to 14 all negotiate the same ALPN, so a session in that cohort
1238/// starts on the draft its configuration named and learns the wire's answer
1239/// from the first SETUP on the control stream. Everything that has to agree
1240/// with that answer — the object framer on every data stream, the datagram
1241/// header decoder, the control-frame walker that places an injection, and
1242/// the capability table each hook site is shown — lives in a task that was
1243/// spawned before the control stream was even accepted. A draft copied into
1244/// each of those tasks is a copy of the guess, and no later correction can
1245/// reach it.
1246///
1247/// # Reading it
1248///
1249/// [`Self::now`] is the non-blocking read: the best answer so far, or the
1250/// starting draft while there is none. [`Self::resolved`] is the ordering
1251/// edge — it waits for an answer, and is what a task calls when running on
1252/// the wrong draft would produce a wrong result rather than a stale label.
1253///
1254/// # Writing it
1255///
1256/// [`Self::settle`] takes the first write of each rank and keeps the highest
1257/// (see [`DraftSource`]). Both control directions write: the client's
1258/// direction from CLIENT_SETUP and the relay's from SERVER_SETUP, so the
1259/// pair converges on the version the peers agreed rather than on whichever
1260/// direction was read first.
1261struct SessionDraft {
1262 /// The draft chosen before the relay was dialled — the ALPN's answer
1263 /// where there is one, and the configured draft otherwise. What
1264 /// [`Self::now`] answers while nothing has settled, and what the
1265 /// deadline settles on.
1266 initial: DraftVersion,
1267 /// The best answer so far, or `None` while the session is still running
1268 /// on `initial`. A `watch` rather than an atomic because the waiters are
1269 /// the point: this is what [`Self::resolved`] parks on.
1270 settled: watch::Sender<Option<(DraftVersion, DraftSource)>>,
1271 /// The instant [`Self::resolved`] stops waiting. Absolute, and shared by
1272 /// every waiter, so a session pays this window once rather than once per
1273 /// stream: the first waiter to reach it settles the cell, and every
1274 /// waiter after that returns immediately.
1275 deadline: tokio::time::Instant,
1276 /// Whether this session has a control stream at all yet.
1277 ///
1278 /// The only thing that can name a draft is a SETUP, and the only place a
1279 /// SETUP arrives is a control stream. Until one exists there is nothing
1280 /// to wait for, so [`Self::resolved`] does not wait — which is what
1281 /// keeps the window off the timing of a session that never opens one.
1282 ///
1283 /// It is a latch and not a promise. A peer that opened a data stream
1284 /// before its control stream gets the starting draft on that one stream,
1285 /// which is the same answer it would have got with no cell at all; every
1286 /// draft in the cohort puts the setup exchange first, so a session in
1287 /// which that happens is not one they describe.
1288 control_stream_open: AtomicBool,
1289}
1290
1291impl SessionDraft {
1292 /// The cell for a session starting on `initial`.
1293 ///
1294 /// `fixed` is whether that draft came from the ALPN. A fixed session is
1295 /// born settled, so it never waits and no SETUP peek can move it — which
1296 /// is the right reading of drafts 15 and later, where the SETUP message
1297 /// carries no version at all.
1298 fn new(initial: DraftVersion, fixed: bool) -> Self {
1299 let (settled, _) = watch::channel(fixed.then_some((initial, DraftSource::Alpn)));
1300 Self {
1301 initial,
1302 settled,
1303 deadline: tokio::time::Instant::now() + DRAFT_SETTLE_WINDOW,
1304 control_stream_open: AtomicBool::new(false),
1305 }
1306 }
1307
1308 /// Record that this session now has a control stream.
1309 ///
1310 /// Called where one starts being forwarded, in both topologies. What it
1311 /// buys is the *absence* of a wait everywhere else: see
1312 /// [`Self::control_stream_open`].
1313 fn note_control_stream(&self) {
1314 self.control_stream_open.store(true, Ordering::Release);
1315 }
1316
1317 /// The best answer so far, without waiting for a better one.
1318 fn now(&self) -> DraftVersion {
1319 self.settled.borrow().map_or(self.initial, |(draft, _)| draft)
1320 }
1321
1322 /// Record `draft` as this session's, if `source` outranks what is held.
1323 ///
1324 /// Answers whether it landed, so a caller that has work to do only when
1325 /// the session's draft actually moved can ask rather than compare.
1326 fn settle(&self, draft: DraftVersion, source: DraftSource) -> bool {
1327 self.settled.send_if_modified(|held| match held {
1328 Some((_, ranked)) if *ranked >= source => false,
1329 _ => {
1330 *held = Some((draft, source));
1331 true
1332 }
1333 })
1334 }
1335
1336 /// The draft, waited for.
1337 ///
1338 /// Returns at once when the session already has an answer, which is
1339 /// every session whose ALPN named a draft and every session whose
1340 /// control stream has already been read. It also returns at once when
1341 /// the session has no control stream yet, because nothing else can
1342 /// answer and waiting would put [`DRAFT_SETTLE_WINDOW`] on the front of
1343 /// every stream of a session that never opens one.
1344 ///
1345 /// Otherwise it waits for one of three things: a SETUP naming the draft,
1346 /// [`DRAFT_SETTLE_WINDOW`] expiring, or the session being cancelled —
1347 /// the last of which is why a teardown is not held up by a window that
1348 /// has barely started.
1349 async fn resolved(&self, cancel: &CancellationToken) -> DraftVersion {
1350 let mut changed = self.settled.subscribe();
1351 if let Some((draft, _)) = *changed.borrow_and_update() {
1352 return draft;
1353 }
1354 if !self.control_stream_open.load(Ordering::Acquire) {
1355 return self.initial;
1356 }
1357 tokio::select! {
1358 biased;
1359 () = cancel.cancelled() => {}
1360 _ = changed.changed() => {}
1361 () = tokio::time::sleep_until(self.deadline) => {
1362 self.settle(self.initial, DraftSource::Fallback);
1363 }
1364 }
1365 self.now()
1366 }
1367}
1368
1369/// Shared context for forwarding helpers, avoiding repeated parameter lists.
1370#[derive(Clone)]
1371struct ForwardCtx {
1372 session_id: SessionId,
1373 /// The draft this session frames with, shared by every task rather than
1374 /// copied into each — see [`SessionDraft`] for why that matters and for
1375 /// what settles it. Read through [`ForwardCtx::draft`], or through
1376 /// [`ForwardCtx::resolved_draft`] where the answer has to be right
1377 /// rather than current.
1378 draft: Arc<SessionDraft>,
1379 /// Whether `draft` is fixed (from ALPN) and should not be refined by
1380 /// peeking at SETUP messages.
1381 draft_is_fixed: bool,
1382 observer: Arc<dyn ProxyObserver>,
1383 hook: Arc<dyn ProxyHook>,
1384 cancel: CancellationToken,
1385 /// This session's slow-path counters.
1386 counters: Arc<Recorder>,
1387 /// This session's shaping counters. Cloned per task exactly as
1388 /// `counters` is, and carried unconditionally: an unshaped
1389 /// session's recorder has no class rows and no writer, so the cost of
1390 /// carrying it is one `Arc` clone per forwarding task and the cost of
1391 /// *not* carrying it would be an `Option` branch on the data path.
1392 shape_stats: Arc<ShapeRecorder>,
1393 /// Where an `Action::CloseSession` lands, and what `run_with_transport`
1394 /// reads its close code and reason back out of.
1395 closer: SessionCloser,
1396 /// Engine knobs for the per-stream deferred write queues.
1397 egress: EgressConfig,
1398 /// Cached `observer.wants_events()` — gates event construction and
1399 /// emission in the hot forwarding loop. When `false`, the proxy can
1400 /// skip parsing for observation purposes and run as a byte pump.
1401 observer_enabled: bool,
1402 /// Whether data streams are framed into objects — the *framing* gate,
1403 /// which decides whether `pipe_data` calls `pipe_data_framed` or
1404 /// `pipe_data_passthrough`. Keeps its `observer_enabled ||` term
1405 /// because `ProxyEvent::Object` is an observer-only guarantee. This is
1406 /// **not** the gate on calling `on_object`; see `object_hook`.
1407 objects_enabled: bool,
1408 /// Whether `ProxyHook::on_object` is consulted. `Interest::OBJECTS`
1409 /// alone, with no `observer_enabled ||` term: an event observer must
1410 /// not hand a hook that declared no object interest the power to drop,
1411 /// delay and rewrite traffic.
1412 object_hook: bool,
1413 /// Whether this session was configured with a
1414 /// [`ShapeProfile`].
1415 ///
1416 /// `config.shape.is_some()` alone, with **no `observer_enabled ||`
1417 /// term** — the same asymmetry as `object_hook` and for the same
1418 /// reason: shaping is configuration, so attaching an observer must not
1419 /// arm it. It *is* a term of `objects_enabled`, because a profile has
1420 /// to arm framing on its own.
1421 ///
1422 /// The read below is what makes that implication checkable rather than
1423 /// merely written down.
1424 ///
1425 /// Exactly `shape.is_some()`, and the two are kept as separate fields
1426 /// on purpose: this one is a `bool` a `debug_assert!` and a hot-path
1427 /// branch can read without touching an `Arc`, and `shape` is the
1428 /// engine. The equivalence is checked in `pipe_data`, where the
1429 /// framing decision is taken.
1430 shaping_enabled: bool,
1431 /// This session's shaper, or `None` when no
1432 /// [`ShapeProfile`] was configured.
1433 ///
1434 /// Unlike `shape_stats` and `streams`, which are always constructed,
1435 /// this is genuinely optional — there is nothing for an unshaped
1436 /// session to share, and an `Option` here is what makes "a session with
1437 /// `shape: None` adds nothing to the shaping path" a fact the type
1438 /// system carries rather than a claim a reviewer checks.
1439 ///
1440 /// `Some` or `None` is fixed for the session's life. A profile installed
1441 /// on the proxy afterwards can replace what is *inside* this, and cannot
1442 /// put something here: framing is armed at session start and a session
1443 /// that began as a byte pump produces no `ObjectMeta` to classify.
1444 shape: Option<Arc<SessionShaper>>,
1445 /// Whether `ProxyHook::on_control_message` is consulted, which also
1446 /// routes the control stream through the parse-then-forward pipe: the
1447 /// pass-through pipe writes before it parses, so a hook return there
1448 /// would be unexecutable by construction.
1449 control_mutation: bool,
1450 /// Whether a `ControlStreamParser` is built for somebody to *read*.
1451 /// `Interest::NONE` with no observer builds none, which is what makes
1452 /// `control_parsers_created == 0` unconditional on that path.
1453 ///
1454 /// Not the whole answer to "is there a parser": `fetch_orders_wanted` is
1455 /// the other, and it builds one for the session's own use. Ask
1456 /// [`ForwardCtx::control_frames_are_decoded`] rather than either alone.
1457 control_parse: bool,
1458 /// Whether this session has to decode control frames to read its own
1459 /// fetch streams — drafts 18 and 19, framing data.
1460 ///
1461 /// Unlike `control_parse` this arms no report and calls no hook. It is
1462 /// the one case where the proxy parses the control plane for itself, and
1463 /// it is why a hook declaring `Interest::OBJECTS` alone can still see a
1464 /// draft-19 fetch Object.
1465 fetch_orders_wanted: bool,
1466 /// What each FETCH this session carried asked for, waiting for the
1467 /// response stream that answers it.
1468 ///
1469 /// Written by both control pipes and read by the object framer; see
1470 /// [`FetchGroupOrders`].
1471 fetch_orders: Arc<FetchGroupOrders>,
1472 /// Whether `on_stream_open`, `on_stream_header` and `on_stream_end` are
1473 /// consulted. `Interest::STREAMS` contains `Interest::OBJECTS`
1474 /// structurally, so this implies `objects_enabled`.
1475 streams_enabled: bool,
1476 /// Whether `ProxyHook::on_datagram` is consulted.
1477 datagram_hook: bool,
1478 /// The session's [`StreamKey`] mint.
1479 /// One counter per session, shared by every forwarding task through the
1480 /// `Arc` — `ForwardCtx` is cloned per task and per stream, so a plain
1481 /// `AtomicU64` would give each clone its own sequence and two streams would
1482 /// collide on id 0. The `Arc` is what makes *unique for the session's
1483 /// lifetime* true rather than aspirational.
1484 next_stream_id: Arc<AtomicU64>,
1485 /// Every forwarded stream that is still live, and the gate each one
1486 /// releases when it ends.
1487 ///
1488 /// **Always constructed**, for every session, exactly like
1489 /// `next_stream_id` and unlike anything a `ShapeProfile` will later
1490 /// arm: `StreamAction::SerializeAfter` is gated by `Interest::STREAMS`
1491 /// and the capability table publishes it as an unconditional `Yes` at
1492 /// both stream sites, so a registry that only existed when a profile
1493 /// was configured would make that published cell a lie. An empty
1494 /// registry allocates nothing and touches no counter, so
1495 /// `interest_none.rs`'s whole-struct `Counters::default()` comparison
1496 /// and its `!release_timer_started()` companion stay falsifiable.
1497 streams: Arc<StreamRegistry>,
1498 /// How many bytes this session's egress queues are holding, summed
1499 /// across every stream.
1500 /// Always constructed, like `streams` and for a related reason: a gauge
1501 /// that only some queues reported into would answer *this session has
1502 /// nothing left to flush* while another stream still held a deferred frame,
1503 /// and the one caller that reads it — a requested close deciding whether it
1504 /// may stop waiting — would act on that answer.
1505 ///
1506 /// Costs one `Arc` clone per forwarding task and two relaxed atomic
1507 /// updates per *queued* unit. A session that queues nothing, which is
1508 /// every session with no timing action and no profile, never touches
1509 /// it: the counters only move inside `PendingQueue::push` and its
1510 /// releases.
1511 gauge: Arc<EgressGauge>,
1512}
1513
1514impl ForwardCtx {
1515 /// The draft this session frames with, as it stands now.
1516 fn draft(&self) -> DraftVersion {
1517 self.draft.now()
1518 }
1519
1520 /// Whether a control frame gets decoded on this session at all.
1521 ///
1522 /// Two unrelated reasons, deliberately summed in one place rather than
1523 /// spelled `a || b` at each of the pipes: `control_parse` is somebody
1524 /// asking to be told, and `fetch_orders_wanted` is the session needing
1525 /// the answer itself. A pipe that tested only the first left a
1526 /// draft-19 fetch stream unaddressable on an `Interest::OBJECTS`
1527 /// session, which is the shape of hook the object site exists for.
1528 fn control_frames_are_decoded(&self) -> bool {
1529 self.control_parse || self.fetch_orders_wanted
1530 }
1531
1532 /// What this session's draft can be asked for.
1533 ///
1534 /// Built here, at each site that needs one, rather than cached on this
1535 /// struct. [`Capabilities`] is a `Copy` newtype over a draft, so
1536 /// constructing it costs a move of one enum and answers for the draft
1537 /// the session is framing with *at that moment* — while a cached copy
1538 /// would have been built beside the guess and would go on answering for
1539 /// it after the peer named something else. One draft in one cell has one
1540 /// consumer to keep correct; a cached table beside it would be a second.
1541 fn caps(&self) -> Capabilities {
1542 Capabilities::for_draft(self.draft())
1543 }
1544
1545 /// The draft this session frames with, waited for.
1546 ///
1547 /// The ordering edge between the control stream, which learns the draft,
1548 /// and the tasks that have to agree with it. Called where the wrong
1549 /// draft produces a wrong result rather than a stale label: the object
1550 /// framer decides where an object ends, and a datagram header decoder
1551 /// decides what a datagram says. See [`SessionDraft::resolved`] for what
1552 /// bounds the wait.
1553 async fn resolved_draft(&self) -> DraftVersion {
1554 self.draft.resolved(&self.cancel).await
1555 }
1556
1557 /// Mint this stream's session-local identity.
1558 ///
1559 /// Called **once** per forwarded stream, at accept, and handed to every
1560 /// hook site that stream reaches. Monotonic, never reused, and
1561 /// deliberately not the transport stream id: on the WebTransport arm
1562 /// that is the constant `0` for every stream, so a transport-keyed
1563 /// identity collapses a whole side onto one entry.
1564 fn mint_key(&self, side: ProxySide) -> StreamKey {
1565 StreamKey { side, id: self.next_stream_id.fetch_add(1, Ordering::Relaxed) }
1566 }
1567
1568 /// Emit a proxy event only if the observer wants events.
1569 ///
1570 /// Takes a closure so the `ProxyEvent` is not constructed when
1571 /// observation is disabled — avoiding clones of message payloads in
1572 /// the hot path.
1573 fn emit(&self, event: impl FnOnce() -> ProxyEvent) {
1574 if self.observer_enabled {
1575 self.observer.on_event(&event());
1576 }
1577 }
1578
1579 /// A reporter for one stream direction, or for a datagram path
1580 /// (`stream_id: None`).
1581 fn reporter<'a>(&'a self, side: ProxySide, stream_id: Option<u64>) -> exec::Reporter<'a> {
1582 exec::Reporter::new(
1583 &*self.observer,
1584 self.observer_enabled,
1585 &self.counters,
1586 self.session_id,
1587 side,
1588 stream_id,
1589 )
1590 }
1591}
1592
1593/// Serve one session's control-plane requests until the session ends.
1594///
1595/// Runs beside the forwarding tasks rather than among them, because the
1596/// `JoinSet` in `run_with_transport` reads the first completion as the end
1597/// of the session and this loop finishes on its own terms — when the inbox
1598/// closes, or when the session is cancelled.
1599///
1600/// The cancellation branch is what makes the loop terminate for a session
1601/// that ends normally: the inbox's sender lives in the control plane's
1602/// registry entry, which is released by the registration guard *after* this
1603/// function's spawner has already returned, so waiting only on the channel
1604/// would keep the task alive past the session it belongs to.
1605///
1606/// The select is `biased` so that branch is polled first. That makes
1607/// cancellation the single exit for a session that is going down, whichever
1608/// way it was asked: a request that ends the session cancels and goes round
1609/// again, and the next poll leaves through the same door as a session that
1610/// was cancelled from outside. The alternative — returning from the request
1611/// arm — would give the same event two exits to keep correct.
1612async fn serve_session_commands(mut inbox: mpsc::Receiver<SessionCommand>, ctx: ForwardCtx) {
1613 loop {
1614 tokio::select! {
1615 biased;
1616 _ = ctx.cancel.cancelled() => return,
1617 command = inbox.recv() => match command {
1618 Some(SessionCommand::Close { drain }) => close_after_draining(drain, &ctx).await,
1619 // Every sender is gone, which can only happen once the
1620 // registry entry has been released. Nothing further can
1621 // arrive.
1622 None => return,
1623 },
1624 }
1625 }
1626}
1627
1628/// Give this session's egress queues `drain` to empty, then end it.
1629///
1630/// The close code and reason are already in the session's closer — a
1631/// requested close records them before it sends the request, so that a
1632/// session torn down by its peer half a millisecond later still closes with
1633/// what was asked for. This function's only job is the window, and what
1634/// happens at the end of it.
1635///
1636/// # Draining means the queues emptied, not that the timer expired
1637///
1638/// The wait ends the moment `EgressGauge` reads zero, which on a session
1639/// with nothing deferred is the first poll. Waiting out the full window
1640/// unconditionally would put a fixed cost on every close, and the cost is
1641/// the wrong one: it is paid by the sessions that had nothing to flush.
1642///
1643/// # And what is left is abandoned rather than flushed
1644///
1645/// The queues are put into discarding mode before the cancellation, so the
1646/// cancel arm of every pipe writes nothing and reports its whole remainder
1647/// as `Impairment { QueuedBytesAtTeardown }`. That is the opposite of what
1648/// an unrequested teardown does, and the difference is the deadline: an
1649/// ordinary teardown's best-effort flush is the last chance those bytes
1650/// have, while a close that was given a window and spent it has already
1651/// decided. Flushing past that point would hand the bytes to a connection
1652/// about to send `CONNECTION_CLOSE`, which discards its buffer — so they
1653/// would be neither confirmably delivered nor confirmably lost, and the one
1654/// arithmetic a caller can check would stop closing.
1655///
1656/// The cancellation is unconditional and comes last, so a session whose
1657/// drain completed and one whose drain expired end the same way and with
1658/// the same close arguments.
1659async fn close_after_draining(drain: Duration, ctx: &ForwardCtx) {
1660 tokio::select! {
1661 biased;
1662 // Already going down for some other reason. Its queues will be
1663 // handled by the ordinary teardown, which is the right treatment:
1664 // this close never got as far as setting a deadline.
1665 () = ctx.cancel.cancelled() => {}
1666 stranded = ctx.gauge.wait_idle(drain) => {
1667 if stranded > 0 {
1668 ctx.gauge.begin_discarding();
1669 }
1670 }
1671 }
1672 ctx.cancel.cancel();
1673}
1674
1675/// The deferred-write state of one stream direction, plus the two facts
1676/// every teardown helper needs about it.
1677///
1678/// Bundled because `PendingQueue` and `DeferredEffects` are only correct
1679/// when they move together, and because `propagate_reset` needs both the
1680/// stream's identity and its queue.
1681struct StreamState<'a> {
1682 stream_id: u64,
1683 /// This stream's session-local identity, minted once at accept and
1684 /// carried to every site it reaches. Distinct from `stream_id`, which
1685 /// is the transport id and is `0` on every WebTransport stream.
1686 key: StreamKey,
1687 /// `true` selects the control-stream rules at `Site::StreamEnd`, where
1688 /// a synthesized reset is a session-level protocol violation and is
1689 /// refused rather than executed.
1690 is_control_stream: bool,
1691 pending: &'a mut PendingQueue,
1692 deferred: &'a mut DeferredEffects,
1693}
1694
1695/// Whether a stream direction may keep running after a helper returned.
1696#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1697enum Flow {
1698 /// Keep forwarding.
1699 Continue,
1700 /// The stream is over — reset, terminated or torn down. Return `Ok`.
1701 StreamOver,
1702}
1703
1704// ── Abnormal teardown propagation ───────────────────────────────
1705
1706/// The egress side paired with an ingress side.
1707///
1708/// Forwarding helpers are handed the side bytes arrive on; a teardown
1709/// observed on the *destination* stream is reported against the side
1710/// those bytes leave on.
1711fn egress_side(side: ProxySide) -> ProxySide {
1712 match side {
1713 ProxySide::ClientToProxy => ProxySide::ProxyToRelay,
1714 ProxySide::RelayToProxy => ProxySide::ProxyToClient,
1715 // Already an egress side — forwarders never pass these in.
1716 other => other,
1717 }
1718}
1719
1720/// Whether a pipe error is an abnormal teardown the proxy already
1721/// mirrored and reported as [`ProxyEvent::StreamReset`].
1722///
1723/// Callers use this to avoid double-reporting one teardown — notably as a
1724/// `ParseError`, which means a *codec* failure.
1725fn is_mirrored_teardown(err: &ProxyError) -> bool {
1726 matches!(
1727 err,
1728 ProxyError::Transport(TransportError::StreamReset(_) | TransportError::Stopped(_))
1729 )
1730}
1731
1732/// Whether the draft defines a stream-reset error code vocabulary.
1733///
1734/// Drafts 07-10 do not, so a reset still carries the code but
1735/// [`Effect::StreamReset`] reports `code_defined: false` — the code is a
1736/// choice there rather than a claim. `exec` makes the same judgement for
1737/// the actions it executes; this copy exists because the two callers are
1738/// in different modules and neither owns the other's privacy.
1739const fn stream_reset_code_defined(draft: DraftVersion) -> bool {
1740 !matches!(
1741 draft,
1742 DraftVersion::Draft07
1743 | DraftVersion::Draft08
1744 | DraftVersion::Draft09
1745 | DraftVersion::Draft10
1746 )
1747}
1748
1749/// The application error code to reset a forwarded data stream with when
1750/// the source read failed for a reason that is not a peer `RESET_STREAM`.
1751///
1752/// `0x3` (SESSION_CLOSED on drafts 11-19) for a connection-level failure —
1753/// literally true when the relay dies mid-subgroup, and the code a real
1754/// publisher would send. `0x0` (INTERNAL_ERROR) for everything else, which
1755/// is verbatim what a proxy-internal failure is. Deliberately **not**
1756/// `0x1 CANCELLED`: its text asserts a control-plane event that never
1757/// happened and points the receiver at a PUBLISH_DONE that will never
1758/// arrive.
1759///
1760/// **The `0x3` arm is unreachable through the QUIC transport today, and
1761/// that is a defect one level down, not here.**
1762/// `moqtap-client/src/transport/quic.rs:87-92` maps `quinn::ReadError` with
1763/// one typed arm — `Reset(code)` — and collapses everything else, including
1764/// `ReadError::ConnectionLost(_)`, into `TransportError::Read(String)`.
1765/// Nothing in the workspace ever constructs `TransportError::ConnectionLost`
1766/// from a real read, so a relay that dies mid-subgroup arrives here as
1767/// `Read(..)` and is reset with `0x0` rather than `0x3`. The stream is still
1768/// **reset rather than FINed**, which is the substance of the guarantee —
1769/// a truncated group never looks complete — and only the code is less
1770/// specific than it should be. Closing it is one arm
1771/// in that `From` impl (`ReadError::ConnectionLost(_) =>
1772/// TransportError::ConnectionLost`), in a crate this one does not own.
1773fn synthesized_reset_code(err: &ProxyError) -> u64 {
1774 match err {
1775 ProxyError::Transport(TransportError::ConnectionLost | TransportError::Connection(_)) => {
1776 0x3
1777 }
1778 _ => 0x0,
1779 }
1780}
1781
1782/// What one poll of the source stream saw.
1783///
1784/// The two pipe loops that queue what they read — [`pipe_control_mutating`]
1785/// and [`pipe_data_framed`] — poll their source through
1786/// [`observe_source`] rather than calling `recv.read` directly, and this is
1787/// what it hands back.
1788enum Source {
1789 /// `recv.read`'s own result, verbatim: `Ok(Some(n))` bytes into the
1790 /// caller's buffer, `Ok(None)` a clean FIN, `Err` a failure.
1791 ///
1792 /// **A reset seen by the reset-only observer arrives here too**, as
1793 /// `Err(TransportError::StreamReset(code))` — byte-identical to what
1794 /// `recv.read` would have produced — so `propagate_reset` mirrors the
1795 /// same code down the same path and neither pipe loop has to know
1796 /// which observer was live.
1797 Read(Result<Option<usize>, TransportError>),
1798 /// The source can no longer be reset, and the reset-only observer must
1799 /// not be polled again: it resolves immediately every time (see
1800 /// [`RecvStream::received_reset`]), so re-polling it spins. The caller
1801 /// latches it off and the branch parks for the rest of the stream,
1802 /// which is exactly the disabled read branch this replaced.
1803 ResetUnobservable,
1804}
1805
1806/// Observe the source stream, **whatever the egress queue is doing**.
1807///
1808/// # The defect this exists to close
1809///
1810/// Both queueing pipe loops gate their read branch on
1811/// `PendingQueue::accepts_more()`, and that is the backpressure mechanism:
1812/// when it is false tokio does not evaluate the branch's expression, so
1813/// `recv.read` is not polled and nothing is consumed. Under
1814/// `Overflow::Block` a dry bucket holds the queue at `depth_objects`
1815/// indefinitely, so the gate stays shut for as long as `max_hold` — 30 s in
1816/// the shipped default posture.
1817///
1818/// A peer's `RESET_STREAM` surfaces **only** as `Err` from `recv.read`.
1819/// With the read branch shut it was therefore not observed at all:
1820/// `propagate_reset` was unreachable, and the mirrored reset that should
1821/// follow the peer's within microseconds arrived up to `max_hold` late.
1822/// The other
1823/// three branches cannot cover it — `StopWatcher` watches the
1824/// *destination's* `stopped()`, the release branch watches this proxy's own
1825/// clock, and `cancel` is session teardown.
1826///
1827/// # Why this does not delete `Overflow::Block`
1828///
1829/// `can_read` still gates **`recv.read`**, which is the only call that
1830/// consumes bytes. Nothing about the queue's depth, the admission decision,
1831/// or the once-per-stream backpressure latch moves. What changes is that
1832/// the shut state is no longer *silent*: instead of parking on nothing, the
1833/// loop parks on [`RecvStream::received_reset`], which reads no bytes and
1834/// therefore grants no `MAX_STREAM_DATA` credit. The peer stays blocked at
1835/// exactly the same offset it was blocked at before.
1836///
1837/// That is the discriminating property, and it is why the fix is not "poll
1838/// `recv.read` anyway and park the chunk": a look-ahead slot consumes a
1839/// chunk, and — worse — it only re-opens when the queue drains, so under a
1840/// dry bucket the *next* reset waits out `max_hold` all the same.
1841///
1842/// # Cancel safety
1843///
1844/// Every path awaits exactly one future and does nothing before it:
1845/// `RecvStream::read` and `RecvStream::received_reset` are both
1846/// cancel-safe, and `pending()` never completes. Dropping this future —
1847/// which `select!` does on every iteration another branch wins — loses
1848/// nothing.
1849async fn observe_source(
1850 recv: &mut PeekedRecv,
1851 buf: &mut [u8],
1852 can_read: bool,
1853 reset_observable: bool,
1854) -> Source {
1855 if can_read {
1856 return Source::Read(recv.read(buf).await);
1857 }
1858 if !reset_observable {
1859 // Nothing left to watch for on a queue-blocked stream. Park, which
1860 // is precisely the `if can_read` branch this replaced.
1861 return std::future::pending().await;
1862 }
1863 match recv.received_reset().await {
1864 // Synthesized into the error `recv.read` would have returned, so
1865 // the mirrored code is identical whichever observer saw it.
1866 Ok(Some(code)) => Source::Read(Err(TransportError::StreamReset(code))),
1867 Ok(None) => Source::ResetUnobservable,
1868 Err(e) => Source::Read(Err(e)),
1869 }
1870}
1871
1872/// Call `ProxyHook::on_stream_end` and execute what it returns.
1873///
1874/// Fires only when the hook declared [`Interest::STREAMS`]. The plan is
1875/// returned so the caller can honour a queued terminal
1876/// ([`Action::ResetStream`], the one non-`Pass` action admitted at a data
1877/// stream's end) or a session close, which is honoured at a control
1878/// stream's end too, because a close is session-scoped.
1879fn run_stream_end(
1880 end: StreamEnd,
1881 st: &mut StreamState<'_>,
1882 side: ProxySide,
1883 ctx: &ForwardCtx,
1884 report: &exec::Reporter<'_>,
1885) -> Plan {
1886 if !ctx.streams_enabled {
1887 return Plan::Nothing;
1888 }
1889 let draft = ctx.draft();
1890 let caps = ctx.caps();
1891 let scx = StreamCtx::new(
1892 ctx.session_id,
1893 side,
1894 st.stream_id,
1895 draft,
1896 st.is_control_stream,
1897 &caps,
1898 st.key,
1899 );
1900 let action = ctx.hook.on_stream_end(&scx, end);
1901 let unit = exec::Unit {
1902 target: exec::Target::StreamEnd { is_control_stream: st.is_control_stream },
1903 draft,
1904 arrived_at: Instant::now(),
1905 };
1906 let mut engine = exec::Engine {
1907 queue: Some(exec::Queue { pending: st.pending, deferred: st.deferred }),
1908 closer: &ctx.closer,
1909 };
1910 exec::execute(&unit, action, &mut engine, report).plan
1911}
1912
1913/// Mirror a source-side read failure onto the destination stream.
1914///
1915/// If the source peer sent `RESET_STREAM`, the destination stream must be
1916/// reset with the *same* application code. Letting the `SendStream` drop
1917/// instead sends a FIN — quinn's `SendStream::drop` calls `finish()` — so
1918/// the far end would see an abandoned, truncated stream as one that ended
1919/// cleanly, and the peer's code would never arrive.
1920///
1921/// **Every other read failure now resets the destination too**, with
1922/// a synthesized code from [`synthesized_reset_code`], reported as
1923/// `ActionApplied { effect: StreamReset { code, code_defined } }`. Before
1924/// this the destination was dropped, and quinn's `finish()`-on-drop made a
1925/// truncated group look complete to the peer.
1926///
1927/// **Except on a control stream.** Synthesizing a reset there is a
1928/// session-level protocol violation on every draft, so the destination
1929/// still ends with a FIN and the truncation is reported as
1930/// `Impairment { ControlStreamTruncated }` instead. `ProxySide` does not
1931/// carry the control/data distinction, so `StreamState` does.
1932///
1933/// Anything the hook deferred is drained **ignoring release times before**
1934/// the reset, which is what keeps "data, then reset" true.
1935async fn propagate_reset(
1936 err: &ProxyError,
1937 send: &mut SendStream,
1938 st: &mut StreamState<'_>,
1939 side: ProxySide,
1940 ctx: &ForwardCtx,
1941 report: &exec::Reporter<'_>,
1942) {
1943 let mirrored = match err {
1944 ProxyError::Transport(TransportError::StreamReset(code)) => Some(*code),
1945 _ => None,
1946 };
1947 let end = match mirrored {
1948 Some(code) => StreamEnd::Reset { code },
1949 None => StreamEnd::Cancelled,
1950 };
1951
1952 // The hook is told the stream ended before anything is torn down, so a
1953 // refusal it earns is reported against a stream that still exists. A
1954 // terminal it queues carries its own code and replaces ours; every
1955 // other plan leaves the peer's code — or the synthesized one — in
1956 // charge, which is what keeps the mirrored-reset guarantee true for
1957 // every hook that does not explicitly ask otherwise.
1958 let plan = run_stream_end(end, st, side, ctx, report);
1959 if matches!(plan, Plan::Terminal) {
1960 let _ = st.pending.drain_ignoring_release_times(send).await;
1961 report_unconfirmed(st, report);
1962 st.deferred.clear();
1963 return;
1964 }
1965
1966 if let Some(code) = mirrored {
1967 let _ = st.pending.drain_ignoring_release_times(send).await;
1968 report_unconfirmed(st, report);
1969 st.deferred.clear();
1970 let _ = send.reset(code);
1971 ctx.emit(|| ProxyEvent::StreamReset { session_id: ctx.session_id, side, code });
1972 return;
1973 }
1974
1975 if st.is_control_stream {
1976 report.impairment(ImpairmentKind::ControlStreamTruncated { error: err.to_string() });
1977 return;
1978 }
1979
1980 let code = synthesized_reset_code(err);
1981 let _ = st.pending.drain_ignoring_release_times(send).await;
1982 report_unconfirmed(st, report);
1983 st.deferred.clear();
1984 let _ = send.reset(code);
1985 report.applied(
1986 Site::StreamEnd,
1987 ActionKind::ResetStream,
1988 Effect::StreamReset { code, code_defined: stream_reset_code_defined(ctx.draft()) },
1989 );
1990}
1991
1992/// Report whatever a teardown drain could not vouch for, once.
1993///
1994/// The pairing `ImpairmentKind::QueuedBytesAtTeardown` was always meant to
1995/// have: a queue that was flushed best-effort into a transport that is
1996/// going away has delivered nothing it can prove, and reporting only what
1997/// stayed queued reports zero for exactly the case that loses data. See
1998/// `PendingQueue::unconfirmed_bytes`.
1999///
2000/// Zero, and therefore silent, on every stream that had nothing queued —
2001/// which is every stream in a session with no timing action.
2002fn report_unconfirmed(st: &StreamState<'_>, report: &exec::Reporter<'_>) {
2003 let stranded = st.pending.unconfirmed_bytes();
2004 if stranded > 0 {
2005 report.impairment(ImpairmentKind::QueuedBytesAtTeardown {
2006 stream_id: st.stream_id,
2007 bytes: stranded,
2008 });
2009 }
2010}
2011
2012/// Mirror a destination-side write failure onto the source stream.
2013///
2014/// If the destination peer sent `STOP_SENDING`, the source stream must be
2015/// stopped with the *same* application code. Letting the `RecvStream`
2016/// drop instead emits `STOP_SENDING` with a hard-coded 0 — quinn's
2017/// `RecvStream::drop` calls `stop(0)` — silently replacing the peer's
2018/// reason with "unspecified". Any other write failure is left to the
2019/// default teardown.
2020///
2021/// Two triggers reach here, and they are not interchangeable. The first
2022/// is a failed write: every inline `send.write_all` and the deferred
2023/// release branch route their error through this function. That trigger
2024/// alone leaves a source that has gone quiet unstopped indefinitely,
2025/// because nothing writes to notice. The second is [`StopWatcher`], a
2026/// `select!` branch over `SendStream::stopped()` that races the read, so
2027/// an idle stream learns about the peer's decision when the peer makes
2028/// it rather than when the proxy next produces.
2029///
2030/// Nothing queued can be delivered once the destination has stopped us, so
2031/// the queue is reported and cleared rather than drained.
2032fn propagate_stop(
2033 err: &ProxyError,
2034 recv: &mut PeekedRecv,
2035 st: &mut StreamState<'_>,
2036 side: ProxySide,
2037 ctx: &ForwardCtx,
2038 report: &exec::Reporter<'_>,
2039) {
2040 if let ProxyError::Transport(TransportError::Stopped(code)) = *err {
2041 let _ = recv.stop(code);
2042 let reported_side = egress_side(side);
2043 ctx.emit(|| ProxyEvent::StreamReset {
2044 session_id: ctx.session_id,
2045 side: reported_side,
2046 code,
2047 });
2048 let _ = run_stream_end(StreamEnd::Stopped { code }, st, side, ctx, report);
2049 // Measured, then abandoned, then reported — in that order. The
2050 // figure has to be read before the queue is cleared and the event
2051 // has to follow the clearing, because it says these bytes are gone;
2052 // between the two lines it is still true that they might yet be
2053 // written by something else on the way out.
2054 let stranded = st.pending.queued_bytes();
2055 st.pending.clear();
2056 st.deferred.clear();
2057 if stranded > 0 {
2058 report.impairment(ImpairmentKind::QueuedBytesAtTeardown {
2059 stream_id: st.stream_id,
2060 bytes: stranded,
2061 });
2062 }
2063 }
2064}
2065
2066/// A boxed `SendStream::stopped()` future.
2067///
2068/// Boxed because `stopped()` returns an opaque `impl Future` that cannot be
2069/// named, and [`StopWatcher`] has to *store* one across `select!`
2070/// iterations rather than rebuild it. One allocation per forwarded stream.
2071type StoppedFuture = Pin<Box<dyn Future<Output = Result<(), TransportError>> + Send>>;
2072
2073/// A destination-side `STOP_SENDING` watcher, hoisted once per forwarded
2074/// stream and used as a fourth `tokio::select!` branch.
2075///
2076/// # Why it is hoisted
2077///
2078/// `tokio::select!` drops and rebuilds every branch future each time round
2079/// the loop. Rebuilding `SendStream::stopped()` takes quinn's connection
2080/// state lock and inserts into a per-connection map
2081/// (`quinn-0.11.9/src/send_stream.rs:258-263`), which is per-*wake* work on
2082/// loops documented as doing none. So the future is built once, lives here
2083/// across iterations, and [`Self::watch`] *borrows* it rather than moving
2084/// it — a `select!` iteration that cancels this branch therefore loses
2085/// nothing and resumes the same future next time round.
2086///
2087/// # Why it is fused
2088///
2089/// Building it once means it can only resolve once: polling a completed
2090/// future panics with "`async fn` resumed after completion". [`Self::watch`]
2091/// clears the slot the instant the future returns, which both disables the
2092/// branch (through [`Self::is_watching`]) and makes a re-poll structurally
2093/// unreachable. The fuse is not belt and braces — without it the very next
2094/// `select!` iteration panics inside the forwarding task.
2095///
2096/// [`Self::armed`] is what keeps the fuse one-way: a retired watcher has an
2097/// empty slot, and without the flag the next [`Self::arm`] would rebuild it.
2098///
2099/// # Cost
2100///
2101/// One `Box::pin` per forwarded stream, allocated at the first `select!`
2102/// iteration and never again. Per stream, never per object.
2103struct StopWatcher {
2104 /// The hoisted `stopped()` future. `None` before [`Self::arm`], and
2105 /// again once it has resolved or [`Self::retire`] was called.
2106 watching: Option<StoppedFuture>,
2107 /// Set by the first [`Self::arm`], so a retired watcher stays retired.
2108 armed: bool,
2109}
2110
2111impl StopWatcher {
2112 /// An unarmed watcher. Allocates nothing.
2113 fn new() -> Self {
2114 Self { watching: None, armed: false }
2115 }
2116
2117 /// Build the watcher over `send`, once.
2118 /// Called from the top of each pipe's loop rather than before it, so
2119 /// `pipe_data_passthrough` — whose contract is *a stack buffer and a write*
2120 /// — allocates on its first `select!` iteration and not at function entry.
2121 /// Idempotent: a second call is a no-op, and a call after [`Self::retire`]
2122 /// does *not* re-arm.
2123 fn arm(&mut self, send: &SendStream) {
2124 if !self.armed {
2125 self.armed = true;
2126 self.watching = Some(Box::pin(send.stopped()));
2127 }
2128 }
2129
2130 /// Build a watcher over an arbitrary future.
2131 ///
2132 /// The fuse is a property of [`Self::watch`], not of quinn. A real
2133 /// `SendStream::stopped()` cannot be made to resolve on demand, and —
2134 /// this being the whole point — cannot be made to resolve twice, so
2135 /// the claim is proven against a future this crate controls.
2136 #[cfg(test)]
2137 fn watching_over(
2138 fut: impl Future<Output = Result<(), TransportError>> + Send + 'static,
2139 ) -> Self {
2140 Self { watching: Some(Box::pin(fut)), armed: true }
2141 }
2142
2143 /// Whether the `select!` branch should be enabled this iteration.
2144 fn is_watching(&self) -> bool {
2145 self.watching.is_some()
2146 }
2147
2148 /// Drop the watcher without polling it again.
2149 ///
2150 /// Called before every local `send.reset`: quinn keeps no
2151 /// stopped-notification for a stream it has locally reset, so a
2152 /// watcher held across a reset stays pending until the connection ends
2153 /// (see `SendStream::stopped`'s own docs). Every reset site returns
2154 /// from its pipe immediately afterwards, so this is about saying what
2155 /// the code means as much as about the residue.
2156 fn retire(&mut self) {
2157 self.watching = None;
2158 }
2159
2160 /// Resolve when the destination stops being useful — then never again.
2161 ///
2162 /// Stays pending forever once retired, so an enabled-but-retired
2163 /// branch cannot spin; the `if` guard is the fast path and this is the
2164 /// backstop.
2165 ///
2166 /// Cancellation-safe: the fuse below is reached only on completion, so
2167 /// a `select!` iteration that drops this future mid-poll leaves the
2168 /// hoisted future exactly where it was.
2169 async fn watch(&mut self) -> Result<(), TransportError> {
2170 let Some(fut) = self.watching.as_mut() else {
2171 return std::future::pending().await;
2172 };
2173 let outcome = fut.as_mut().await;
2174 // THE FUSE.
2175 self.watching = None;
2176 outcome
2177 }
2178}
2179
2180/// The one [`StopWatcher`] outcome that ends a stream.
2181///
2182/// Only an explicit peer `STOP_SENDING` is terminal. `Ok(())` cannot fire
2183/// on a live stream — quinn reports it only once the send state is gone —
2184/// and treating it as end-of-stream would race the FIN path's own
2185/// `send.finish()`. A lost connection is already the read side's business
2186/// and every pipe already has a teardown for it. Everything that is not a
2187/// `STOP_SENDING` therefore retires the watcher and the loop carries on
2188/// byte-for-byte as before.
2189///
2190/// This is what makes the watcher safe on a **control** stream, where an
2191/// idle stream is MoQT's normal steady state: idleness never resolves
2192/// `stopped()`, and no outcome except the peer's own decision can tear a
2193/// healthy session down.
2194fn stop_error(outcome: Result<(), TransportError>) -> Option<ProxyError> {
2195 match outcome {
2196 Err(e @ TransportError::Stopped(_)) => Some(ProxyError::Transport(e)),
2197 _ => None,
2198 }
2199}
2200
2201/// The label [`ProxyEvent::Shaped`] reports a class under.
2202///
2203/// An empty string for [`Class::Default`] and [`Class::Unshapeable`],
2204/// matching
2205/// [`ShapeStats::default_class`](crate::shape::ShapeStats::default_class)
2206/// and [`ShapeStats::unshapeable`](crate::shape::ShapeStats::unshapeable),
2207/// whose rows are unnamed for the same reason: a user-written class name is
2208/// unique by `ShapeError::DuplicateClassName`, so an empty label cannot
2209/// collide with one and "no rule claimed it" needs no invented name.
2210///
2211/// Allocates one `String`, and is called only from an event that is capped
2212/// at once per stream per outcome — never per unit.
2213fn class_label(shaper: &Scheduler, class: Class) -> String {
2214 match class {
2215 Class::Rule(index) => shaper.class_name(index),
2216 Class::Default | Class::Unshapeable => String::new(),
2217 }
2218}
2219
2220/// Flush anything the hook deferred, honouring its release times, as a
2221/// race against session cancellation.
2222///
2223/// The drain sits inside a `select!` arm body, which is not preemptible, so
2224/// writing it as a plain loop would let a `Hold` on a gate nobody releases
2225/// pin session teardown for up to `EgressConfig::max_hold`.
2226///
2227/// # `shaped`, and why it is a parameter rather than a `None`
2228///
2229/// This is the **FIN path**, and on the framed pipe the FIN path is the
2230/// ordinary MoQT subgroup shape: header, a handful of objects, FIN. Every
2231/// unit still queued when the source finishes is released by the drain
2232/// below, which means every clamp and every expiry those units earn is
2233/// decided there — so `shaped` is what turns those decisions into
2234/// `HoldClamped` and `Shaped { Expired }` instead of into nothing. It was
2235/// `None`-by-omission once, and the whole profile applied
2236/// itself to the normal case in silence; `shaping_reports_do_not_depend_on_a_fin`
2237/// is the gate.
2238///
2239/// `None` at the four callers that cannot produce a report: both control
2240/// pipes install no scheduler, `pipe_data_passthrough` installs no
2241/// scheduler, and `write_in_order` is reachable only behind
2242/// `pipe_data_framed`'s `shape.is_some()` guard taking the other branch.
2243async fn drain_pending(
2244 send: &mut SendStream,
2245 st: &mut StreamState<'_>,
2246 site: Site,
2247 shaped: Option<&ShapedStream>,
2248 ctx: &ForwardCtx,
2249 report: &exec::Reporter<'_>,
2250) -> Result<Flow, ProxyError> {
2251 if st.pending.is_empty() {
2252 return Ok(Flow::Continue);
2253 }
2254 let outcome = egress::drain_honouring_release_times(st.pending, send, &ctx.cancel, |outcome| {
2255 report_shaping(Some(outcome), shaped, ctx, report);
2256 })
2257 .await?;
2258 match outcome {
2259 DrainOutcome::Complete => {
2260 for owed in st.deferred.take_all() {
2261 report.applied_deferred(site, owed);
2262 }
2263 Ok(Flow::Continue)
2264 }
2265 DrainOutcome::Terminated { .. } => {
2266 st.pending.clear();
2267 st.deferred.clear();
2268 Ok(Flow::StreamOver)
2269 }
2270 DrainOutcome::CancelledMidDrain | DrainOutcome::WriteFailed | DrainOutcome::Discarded => {
2271 st.deferred.clear();
2272 // `unconfirmed_bytes`, not `queued_bytes`: the cancel fallback
2273 // may have handed everything to a transport `run_with_transport`
2274 // is closing, in which case nothing is left queued and nothing
2275 // reached the peer. See `PendingQueue::unconfirmed_bytes`.
2276 //
2277 // On `Discarded` the two are equal and both are exact: the
2278 // fallback wrote nothing, so nothing was handed anywhere and
2279 // the figure below is precisely what was abandoned.
2280 let stranded = st.pending.unconfirmed_bytes();
2281 if stranded > 0 {
2282 report.impairment(ImpairmentKind::QueuedBytesAtTeardown {
2283 stream_id: st.stream_id,
2284 bytes: stranded,
2285 });
2286 }
2287 Ok(Flow::StreamOver)
2288 }
2289 }
2290}
2291
2292/// Write bytes the hook was never shown, keeping wire order — on an
2293/// **unshaped** stream.
2294///
2295/// `session.rs` cannot enqueue on its own — `DeferredEffects`'s push is
2296/// `exec`'s, so the ledger and the deque can only move together — so when
2297/// something is already waiting the queue is drained at its release times
2298/// first. The three callers are a stream header, an oversized object's
2299/// passthrough chunk and a bypassed stream's bytes: none is addressable,
2300/// and none may be reordered against an object the hook did defer.
2301///
2302/// On an empty queue — every session with no timing action, which is every
2303/// `Interest::NONE` session — this is one `is_empty()` and the same
2304/// `send.write_all(&raw).await` the byte pump does.
2305///
2306/// A **shaped** stream calls [`exec::enqueue_unshown`] instead, and must:
2307/// this function would let these bytes escape the pacer, and the drain it
2308/// runs first honours release times, so on a paced queue it would block the
2309/// read arm for as long as the bucket took, inside a `select!` arm body that
2310/// polls no other branch.
2311async fn write_in_order(
2312 raw: &[u8],
2313 send: &mut SendStream,
2314 st: &mut StreamState<'_>,
2315 ctx: &ForwardCtx,
2316 report: &exec::Reporter<'_>,
2317) -> Result<Flow, ProxyError> {
2318 // `None`: `pipe_data_framed` routes every shaped stream to
2319 // `exec::enqueue_unshown` before it can reach here, so this queue has no
2320 // scheduler and no shaping decision to report.
2321 if drain_pending(send, st, Site::Object, None, ctx, report).await? == Flow::StreamOver {
2322 return Ok(Flow::StreamOver);
2323 }
2324 send.write_all(raw).await?;
2325 Ok(Flow::Continue)
2326}
2327
2328/// Forward the control stream on the drafts whose control stream is the
2329/// first client-initiated bidirectional stream — drafts 07 through 16.
2330///
2331/// Drafts 17 and later do not reach this function at all: they put the
2332/// control plane on a pair of unidirectional streams and use bidirectional
2333/// streams for requests, so their two control directions are picked out of
2334/// the unidirectional accept loop by [`classify_uni_stream`] and their
2335/// bidirectional streams are forwarded by [`forward_request_streams`]. See
2336/// [`control_plane_is_unidirectional`] for which drafts those are and what
2337/// the drafts say.
2338///
2339/// Draft-16 reaches it *and* has request streams. The first bidirectional
2340/// stream this function accepts is its control stream, and every one after it
2341/// is a request stream taken by
2342/// [`request_streams_beside_the_control_stream`], which runs as a branch of
2343/// the `select!` at the end rather than as a task of its own — see there for
2344/// why the ordering has to be settled by the code.
2345///
2346/// `client_leg` and `upstream_leg` are the two request channels the control
2347/// plane reaches this session's control stream through, and the mapping
2348/// between them and the two pipes is a half-turn worth stating: a message
2349/// the **client** is meant to decode is written by the pipe that forwards
2350/// *from* the relay, because that is the pipe holding the client-facing
2351/// write half. The registry gets the same senders under the two direction
2352/// keys, so a `reset_stream` naming either control direction reaches the
2353/// same task an injection would.
2354async fn forward_control_stream(
2355 client: &Transport,
2356 relay: &Transport,
2357 ctx: &ForwardCtx,
2358 client_leg: ControlLeg,
2359 upstream_leg: ControlLeg,
2360) -> Result<(), ProxyError> {
2361 debug_assert!(
2362 !control_plane_is_unidirectional(ctx.draft.initial),
2363 "a draft whose control plane is a pair of unidirectional streams must not have its \
2364 first bidirectional stream forwarded as the control stream",
2365 );
2366
2367 // Accept bi from client
2368 let (client_send, client_recv) = client.accept_bi().await?;
2369 // From here the session has somewhere a SETUP can arrive, so a task
2370 // that needs the draft has something to wait for. Recorded before the
2371 // relay leg is opened, because the client's CLIENT_SETUP is the message
2372 // that names the draft and it is already on its way.
2373 ctx.draft.note_control_stream();
2374 ctx.emit(|| ProxyEvent::BiStreamOpened {
2375 session_id: ctx.session_id,
2376 side: ProxySide::ClientToProxy,
2377 });
2378
2379 // Open bi to relay
2380 let (relay_send, relay_recv) = relay.open_bi().await?;
2381 ctx.emit(|| ProxyEvent::BiStreamOpened {
2382 session_id: ctx.session_id,
2383 side: ProxySide::ProxyToRelay,
2384 });
2385
2386 // Pipe client→relay and relay→client concurrently
2387 let ctx1 = ForwardCtx { ..ctx.clone() };
2388 let ctx2 = ForwardCtx { ..ctx.clone() };
2389
2390 // The control stream's two directions are two forwarded streams, so
2391 // they take two keys — the same rule every uni stream takes.
2392 let client_key = ctx1.mint_key(ProxySide::ClientToProxy);
2393 let relay_key = ctx2.mint_key(ProxySide::RelayToProxy);
2394
2395 // Registered like every other forwarded stream, so "live" means the
2396 // same thing for all of them. A hook can learn a control direction's
2397 // key at `Site::StreamEnd`, and a `SerializeAfter` naming a *live*
2398 // control direction must wait rather than be told it does not exist.
2399 // The client-to-proxy pipe writes toward the relay, so it serves the
2400 // upstream leg's requests; the relay-to-proxy pipe writes toward the
2401 // client and serves the client leg's.
2402 let ControlLeg { inbox: client_inbox, requests: client_requests } = client_leg;
2403 let ControlLeg { inbox: upstream_inbox, requests: upstream_requests } = upstream_leg;
2404 let client_guard = ctx.streams.register(client_key, upstream_inbox);
2405 let relay_guard = ctx.streams.register(relay_key, client_inbox);
2406
2407 let client_to_relay = tokio::spawn(async move {
2408 let _guard = client_guard;
2409 pipe_control(
2410 PeekedRecv::new(client_recv),
2411 relay_send,
2412 ProxySide::ClientToProxy,
2413 client_key,
2414 upstream_requests,
2415 &ctx1,
2416 )
2417 .await
2418 });
2419
2420 let relay_to_client = tokio::spawn(async move {
2421 let _guard = relay_guard;
2422 pipe_control(
2423 PeekedRecv::new(relay_recv),
2424 client_send,
2425 ProxySide::RelayToProxy,
2426 relay_key,
2427 client_requests,
2428 &ctx2,
2429 )
2430 .await
2431 });
2432
2433 tokio::select! {
2434 r = client_to_relay => r.map_err(|e| ProxyError::SessionClosed(e.to_string()))?,
2435 r = relay_to_client => r.map_err(|e| ProxyError::SessionClosed(e.to_string()))?,
2436 r = request_streams_beside_the_control_stream(client, relay, ctx) => r,
2437 _ = ctx.cancel.cancelled() => Ok(()),
2438 }
2439}
2440
2441/// The client-to-relay request-stream loop, for a draft whose control stream
2442/// is bidirectional and which has request streams as well — draft-16 alone.
2443/// See [`bidi_streams_carry_requests`].
2444///
2445/// # Why it is a branch of the control stream's `select!` and not a task
2446///
2447/// Because both take bidirectional streams off the same transport, and only one
2448/// accept may be outstanding if *the first one is the control stream* is to
2449/// mean anything. Running here, the loop starts after
2450/// [`forward_control_stream`] has already taken the control stream, so the
2451/// order is fixed by the code rather than by which task the runtime polled
2452/// first. A separate task racing the same `accept_bi` would forward the control
2453/// stream as a request stream on whichever runs of whichever build happened to
2454/// lose.
2455///
2456/// The relay-to-client direction has no such constraint — nothing else
2457/// accepts a relay-initiated bidirectional stream — so it is spawned as an
2458/// ordinary loop beside this function's caller.
2459///
2460/// On every other draft this never completes, which leaves the `select!`
2461/// above decided by the two control pipes exactly as it was before draft-16
2462/// had anywhere else to put a request.
2463async fn request_streams_beside_the_control_stream(
2464 client: &Transport,
2465 relay: &Transport,
2466 ctx: &ForwardCtx,
2467) -> Result<(), ProxyError> {
2468 if !bidi_streams_carry_requests(ctx.draft.initial) {
2469 std::future::pending::<()>().await;
2470 }
2471 forward_request_streams(client, relay, ProxySide::ClientToProxy, ctx).await
2472}
2473
2474/// Whether this draft carries control messages on a **pair of
2475/// unidirectional streams**, making a bidirectional stream a *request*
2476/// stream rather than the control stream.
2477///
2478/// True on drafts 17, 18 and 19; false on 07 through 16.
2479///
2480/// # What the drafts say
2481///
2482/// Draft-16 Section 3.3 (Session initialization): "The first stream opened
2483/// is a client-initiated bidirectional control stream where the endpoints
2484/// exchange Setup messages (Section 9.3), followed by other messages defined
2485/// in Section 9." One stream, opened by the client, carrying both directions.
2486///
2487/// Draft-17 Section 3.3, and identically draft-18 and draft-19 Section 3.3:
2488/// "MOQT uses a pair of unidirectional streams for creating the session and
2489/// exchanging control messages. Each peer opens one control stream beginning
2490/// with a SETUP message. Using a pair of unidirectional streams rather than
2491/// a single bidirectional stream allows either peer to send data as soon as
2492/// it is able." The same section then says what the bidirectional streams
2493/// are for: "In addition to the control streams, this specification uses
2494/// bidirectional streams to carry requests. A request stream begins with one
2495/// of these six message types: TRACK_STATUS, SUBSCRIBE, PUBLISH, FETCH,
2496/// PUBLISH_NAMESPACE, and SUBSCRIBE_NAMESPACE" — seven from draft-18, which
2497/// adds SUBSCRIBE_TRACKS.
2498///
2499/// So on 17-19 each direction of the control plane is a separate stream,
2500/// opened by the peer that writes on it: the client's control stream carries
2501/// client-to-relay control messages and the relay's carries the other
2502/// direction. Neither is closed for the session's lifetime.
2503///
2504/// # How a control stream is told apart from a data stream
2505///
2506/// By its first varint. Draft-17 Section 3.4 (Unidirectional Stream Types):
2507/// "All unidirectional MOQT streams start with a variable-length integer
2508/// indicating the type of the stream", and the table gives 0x05 for
2509/// FETCH_HEADER, 0x10-0x1D for SUBGROUP_HEADER and **0x2F00 for SETUP**.
2510/// Draft-18 and draft-19 keep the same table and add PADDING (0x132B3E28).
2511/// That 0x2F00 is also the SETUP *message* type (draft-17 Section 9.4), so
2512/// the control stream's type varint is the first field of its first message
2513/// and nothing has to be stripped before forwarding: see
2514/// [`CONTROL_STREAM_TYPE`].
2515const fn control_plane_is_unidirectional(draft: DraftVersion) -> bool {
2516 matches!(draft, DraftVersion::Draft17 | DraftVersion::Draft18 | DraftVersion::Draft19)
2517}
2518
2519/// Whether this draft puts **requests** on bidirectional streams of their
2520/// own, so that a bidirectional stream beyond the control stream is a stream
2521/// this proxy has to forward.
2522///
2523/// True on drafts 16 through 19; false on 07 through 15.
2524///
2525/// # Why this is not [`control_plane_is_unidirectional`]
2526///
2527/// Because draft-16 answers the two questions differently, and it is the only
2528/// draft that does. Its control plane is one client-initiated bidirectional
2529/// stream, exactly as on 07 through 15. Draft-16 Section 3.3: "The first
2530/// stream opened is a client-initiated bidirectional control stream where the
2531/// endpoints exchange Setup messages (Section 9.3), followed by other
2532/// messages defined in Section 9."
2533/// The same section then adds a second use: "This specification only specifies
2534/// two uses of bidirectional streams, the control stream, which begins with
2535/// CLIENT_SETUP, and SUBSCRIBE_NAMESPACE. Bidirectional streams MUST NOT begin
2536/// with any other message type unless negotiated."
2537///
2538/// Draft-16 Section 6.1 says who opens one: "The subscriber sends
2539/// SUBSCRIBE_NAMESPACE on a new bidirectional stream and the publisher MUST
2540/// send a single
2541/// REQUEST_OK or REQUEST_ERROR as the first message on the bidirectional
2542/// stream in response". Either endpoint of a session can be that subscriber,
2543/// so the streams arrive in both directions and each direction needs an accept
2544/// loop of its own.
2545///
2546/// Drafts 07 through 15 have no second use to forward: none of them puts any
2547/// message on a bidirectional stream other than the control stream. Drafts 17
2548/// through 19 moved the control plane off bidirectional streams entirely, so
2549/// there every bidirectional stream is a request stream and the first one is
2550/// no different from the rest.
2551///
2552/// # Why the initial draft is enough to decide it
2553///
2554/// Because this question is asked before any SETUP has been read, and the
2555/// answer cannot change once it is. Draft-16 has an ALPN of its own, so a
2556/// session that begins as draft-16 is draft-16; the one cohort where the
2557/// initial draft is a guess refined by the SETUP peek is `moq-00`, which
2558/// spans drafts 07 to 14 and answers `false` for every member. There is no
2559/// refinement that could turn this answer over.
2560///
2561/// # What the proxy does with one
2562///
2563/// Forwards it, and nothing more. Draft-16 withdraws a namespace subscription
2564/// by ending its stream. Draft-16 Section 6.1: "A SUBSCRIBE_NAMESPACE can be
2565/// cancelled by closing the stream with either a FIN or RESET_STREAM" — both
2566/// are already mirrored onto the far side by the pipes, because they are what
2567/// a forwarded stream ending looks like. Which of the two arrived is the
2568/// endpoints' business; this proxy holds neither end's request state and must
2569/// not start reading a cancellation into one.
2570const fn bidi_streams_carry_requests(draft: DraftVersion) -> bool {
2571 matches!(
2572 draft,
2573 DraftVersion::Draft16
2574 | DraftVersion::Draft17
2575 | DraftVersion::Draft18
2576 | DraftVersion::Draft19
2577 )
2578}
2579
2580/// The unidirectional stream type that marks a control stream on the drafts
2581/// [`control_plane_is_unidirectional`] names, and the SETUP message type on
2582/// the same drafts. They are one number, 0x2F00.
2583///
2584/// A control stream therefore starts with the first field of a SETUP message
2585/// and carries no separate stream header, which is why a control stream can
2586/// be forwarded byte for byte onto a fresh unidirectional stream: the type
2587/// varint the classifier read is the type varint the peer needs to read.
2588///
2589/// Encoded with the varint the draft uses — from draft-17 that is MoQT's
2590/// leading-ones form, in which 0x2F00 is the two bytes `AF 00` — so the
2591/// classifier decodes through [`DraftVersion`] rather than assuming a width.
2592const CONTROL_STREAM_TYPE: u64 = 0x2F00;
2593
2594/// The most bytes a unidirectional stream's type varint can occupy: nine,
2595/// which is MoQT's widest form from draft-17 (RFC 9000's is eight).
2596const MAX_UNI_TYPE_LEN: usize = 9;
2597
2598/// What a unidirectional stream's leading varint says the stream is.
2599#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2600enum UniStreamKind {
2601 /// One direction of the control plane: [`CONTROL_STREAM_TYPE`].
2602 Control,
2603 /// Anything else — a subgroup or fetch header, padding, or a type this
2604 /// crate does not know. All of them are forwarded as data.
2605 Data,
2606}
2607
2608/// Read a unidirectional stream's type varint and say what the stream is.
2609///
2610/// The bytes it reads are handed back inside the returned [`PeekedRecv`], so
2611/// the pipe that takes the stream sees them exactly as if they had never
2612/// been taken off it. Nothing is stripped: on these drafts the type varint
2613/// *is* the SETUP message's type field.
2614///
2615/// # It reads, so it can block — which is why it runs per stream
2616///
2617/// A stream that is opened and then stays silent produces no varint, and
2618/// this waits for one. That is why the call site is inside the per-stream
2619/// task rather than in the accept loop: a peer that opens a stream and
2620/// writes nothing must not stop the session accepting the *next* one.
2621///
2622/// # A stream that ends or fails before its type arrives is data
2623///
2624/// Not because it is one, but because there is nothing left to decide with
2625/// and the pipe is the honest place to surface the end: it sees the same EOF
2626/// or the same reset one read later and reports it the way it reports every
2627/// other one. Answering `Control` on no evidence would hand the session's
2628/// injection channel to a stream that carried nothing.
2629async fn classify_uni_stream(
2630 mut recv: RecvStream,
2631 draft: DraftVersion,
2632) -> (PeekedRecv, UniStreamKind) {
2633 let mut head: Vec<u8> = Vec::new();
2634 let mut buf = [0u8; MAX_UNI_TYPE_LEN];
2635 let kind = loop {
2636 // One byte is enough to learn the varint's width, and the width is
2637 // enough to know when to stop reading.
2638 let want = head.first().map_or(1, |&first| draft.varint_len(first)).min(MAX_UNI_TYPE_LEN);
2639 if head.len() >= want {
2640 let mut cursor = &head[..want];
2641 break match draft.decode_varint(&mut cursor) {
2642 Ok(v) if v.into_inner() == CONTROL_STREAM_TYPE => UniStreamKind::Control,
2643 _ => UniStreamKind::Data,
2644 };
2645 }
2646 match recv.read(&mut buf[..want - head.len()]).await {
2647 Ok(Some(n)) if n > 0 => head.extend_from_slice(&buf[..n]),
2648 _ => break UniStreamKind::Data,
2649 }
2650 };
2651 (PeekedRecv::with_prefix(recv, Bytes::from(head)), kind)
2652}
2653
2654/// A receive stream with bytes already taken off it.
2655///
2656/// Nothing says what a unidirectional stream is for until its first varint
2657/// has been read, and reading it consumes it. The classifier hands those
2658/// bytes back here, and the pipe that takes the stream reads them first and
2659/// the transport afterwards, so a stream that was classified is
2660/// indistinguishable from one that was not.
2661///
2662/// On drafts whose streams are never classified the prefix is empty and this
2663/// is a [`RecvStream`] with one extra branch on the read path.
2664struct PeekedRecv {
2665 inner: RecvStream,
2666 /// Bytes taken off `inner` before it was handed over, not yet handed to
2667 /// a reader.
2668 prefix: Bytes,
2669}
2670
2671impl PeekedRecv {
2672 /// A stream nothing has been read from.
2673 fn new(inner: RecvStream) -> Self {
2674 Self { inner, prefix: Bytes::new() }
2675 }
2676
2677 /// A stream `prefix` was read from, to be replayed before the rest.
2678 fn with_prefix(inner: RecvStream, prefix: Bytes) -> Self {
2679 Self { inner, prefix }
2680 }
2681
2682 /// See [`RecvStream::stream_id`].
2683 fn stream_id(&self) -> u64 {
2684 self.inner.stream_id()
2685 }
2686
2687 /// See [`RecvStream::read`], with the replayed prefix ahead of it.
2688 ///
2689 /// Cancel-safe for the same reason `RecvStream::read` is, and the prefix
2690 /// branch adds nothing to worry about: it awaits nothing, so it either
2691 /// runs to completion on its first poll or is never entered at all.
2692 async fn read(&mut self, buf: &mut [u8]) -> Result<Option<usize>, TransportError> {
2693 if !self.prefix.is_empty() {
2694 let n = self.prefix.len().min(buf.len());
2695 buf[..n].copy_from_slice(&self.prefix[..n]);
2696 let _ = self.prefix.split_to(n);
2697 return Ok(Some(n));
2698 }
2699 self.inner.read(buf).await
2700 }
2701
2702 /// See [`RecvStream::received_reset`].
2703 ///
2704 /// Not affected by the prefix: a peer's `RESET_STREAM` is about the
2705 /// stream, and bytes already taken off it were taken before it was sent.
2706 async fn received_reset(&mut self) -> Result<Option<u64>, TransportError> {
2707 self.inner.received_reset().await
2708 }
2709
2710 /// See [`RecvStream::stop`]. The unread prefix goes with everything else
2711 /// that was in flight.
2712 fn stop(&mut self, code: u64) -> Result<(), TransportError> {
2713 self.prefix = Bytes::new();
2714 self.inner.stop(code)
2715 }
2716}
2717
2718/// The ingress side of the other direction of the same stream.
2719///
2720/// A bidirectional stream is forwarded by two pipes, and the second one
2721/// carries bytes the other way. `ClientToProxy` and `RelayToProxy` are the
2722/// two ingress sides; this is the turn between them.
2723fn paired_ingress_side(side: ProxySide) -> ProxySide {
2724 match side {
2725 ProxySide::ClientToProxy => ProxySide::RelayToProxy,
2726 ProxySide::RelayToProxy => ProxySide::ClientToProxy,
2727 // Egress sides; forwarders never pass these in.
2728 other => other,
2729 }
2730}
2731
2732/// Forward bidirectional **request** streams, on the drafts where that is
2733/// what a bidirectional stream is — see [`control_plane_is_unidirectional`].
2734///
2735/// One accept loop per direction, because on these drafts either endpoint
2736/// opens request streams: a subscriber opens one to SUBSCRIBE and a
2737/// publisher opens one to PUBLISH, so a proxy that only accepted the
2738/// client's would drop every request the relay ever made. Each accepted
2739/// stream is paired with one opened on the far side and forwarded by two
2740/// pipes, one per direction.
2741///
2742/// # Why the control pipe and not the data pipe
2743///
2744/// Because a request stream carries the same framing the control stream does.
2745/// Draft-17 Section 9 (draft-18 and draft-19 Section 10): "Every message on a
2746/// control or request stream is formatted as follows", and the figure beneath
2747/// it gives Message Type, Message Length and Message Payload. So the messages
2748/// on a request stream are decodable, and a hook that asked for
2749/// [`Interest::CONTROL`] is shown them at [`Site::Control`] exactly as it is
2750/// shown the control stream's. Handing them to the object framer instead would
2751/// produce a bypass and a stream of nothing.
2752///
2753/// # What an injection cannot reach
2754///
2755/// This registers each direction under a fresh per-stream channel, which is
2756/// the one the registry hands a `reset_stream` to. The two channels an
2757/// injection is routed to belong to the session's control legs and go to the
2758/// two unidirectional control streams, so a request stream's pipe can never
2759/// be handed an `Inject` — which is the whole point of separating them.
2760///
2761/// # One conservatism, stated
2762///
2763/// Both pipes run with the control stream's end-of-stream rules, under which
2764/// a hook's `ResetStream` is refused as a session-level protocol violation.
2765/// On a request stream that is stricter than the draft: draft-17 Section
2766/// 3.3.1 says a request MAY be cancelled by either endpoint and that
2767/// implementations SHOULD do it by resetting the stream. Refusing is the
2768/// conservative direction — nothing is destroyed that the draft would have
2769/// kept — and it is what this crate's published capability table says
2770/// happens, so it is left alone here rather than changed silently.
2771async fn forward_request_streams(
2772 source: &Transport,
2773 dest: &Transport,
2774 side: ProxySide,
2775 ctx: &ForwardCtx,
2776) -> Result<(), ProxyError> {
2777 debug_assert!(
2778 bidi_streams_carry_requests(ctx.draft.initial),
2779 "a draft that puts no message on a bidirectional stream beyond the control stream has \
2780 no request stream to forward",
2781 );
2782 loop {
2783 // No cancellation branch, which is deliberate and is the shape
2784 // `forward_control_stream` has always had: this loop is ended by the
2785 // session aborting it, and by `accept_bi` failing when the
2786 // connection goes, not by returning on its own.
2787 //
2788 // Measured rather than assumed. An earlier version raced this accept
2789 // against `ctx.cancel`, and returning first on cancellation moved
2790 // `run_with_transport` past `tasks.shutdown()` and into
2791 // `client.close()` / `relay.close()` before the *per-stream* tasks
2792 // had run their own teardown drains — the drain in
2793 // `pipe_data_framed`'s cancel branch that writes what a hook was
2794 // holding and fires a queued terminal. On a current-thread runtime
2795 // that reordering is deterministic, and
2796 // `actions_timing::cancelling_while_an_object_is_held_tears_down_promptly`
2797 // failed on it every run: no `RESET_STREAM` at the relay within a
2798 // second, the connection closing out from under the drain instead.
2799 // A task the session has to abort is a task whose abort yields, and
2800 // the drains get their turn.
2801 let (source_send, source_recv) = source.accept_bi().await?;
2802 ctx.emit(|| ProxyEvent::BiStreamOpened { session_id: ctx.session_id, side });
2803
2804 let (dest_send, dest_recv) = dest.open_bi().await?;
2805 ctx.emit(|| ProxyEvent::BiStreamOpened {
2806 session_id: ctx.session_id,
2807 side: egress_side(side),
2808 });
2809
2810 // Two directions, two keys, two registrations — the rule every
2811 // forwarded stream takes, and the one `forward_control_stream`
2812 // takes for the control stream's two directions.
2813 let back_side = paired_ingress_side(side);
2814 let forward_key = ctx.mint_key(side);
2815 let back_key = ctx.mint_key(back_side);
2816 let (forward_inbox, forward_requests) = mpsc::channel(COMMAND_QUEUE_DEPTH);
2817 let (back_inbox, back_requests) = mpsc::channel(COMMAND_QUEUE_DEPTH);
2818 let forward_guard = ctx.streams.register(forward_key, forward_inbox);
2819 let back_guard = ctx.streams.register(back_key, back_inbox);
2820
2821 let forward_ctx = ctx.clone();
2822 tokio::spawn(async move {
2823 let _guard = forward_guard;
2824 let result = pipe_control(
2825 PeekedRecv::new(source_recv),
2826 dest_send,
2827 side,
2828 forward_key,
2829 forward_requests,
2830 &forward_ctx,
2831 )
2832 .await;
2833 report_request_stream_end(result, side, &forward_ctx);
2834 });
2835
2836 let back_ctx = ctx.clone();
2837 tokio::spawn(async move {
2838 let _guard = back_guard;
2839 let result = pipe_control(
2840 PeekedRecv::new(dest_recv),
2841 source_send,
2842 back_side,
2843 back_key,
2844 back_requests,
2845 &back_ctx,
2846 )
2847 .await;
2848 report_request_stream_end(result, back_side, &back_ctx);
2849 });
2850 }
2851}
2852
2853/// Report a request-stream pipe that ended badly, on the same terms
2854/// [`forward_uni_streams`] reports one.
2855///
2856/// An abnormal teardown is an ordinary protocol event: it was already
2857/// mirrored onto the far side and already reported as
2858/// [`ProxyEvent::StreamReset`], and repeating it as a `ParseError` would
2859/// claim the codec failed on bytes that were forwarded.
2860fn report_request_stream_end(result: Result<(), ProxyError>, side: ProxySide, ctx: &ForwardCtx) {
2861 if let Err(e) = result {
2862 if !is_mirrored_teardown(&e) {
2863 ctx.emit(|| ProxyEvent::ParseError {
2864 session_id: ctx.session_id,
2865 side,
2866 error: format!("request stream pipe: {e}"),
2867 });
2868 }
2869 }
2870}
2871
2872/// Hand one control leg's requests to the stream that turned out to be that
2873/// direction's control stream.
2874///
2875/// The leg's channel exists from the moment the session registers, which is
2876/// before any stream has arrived, so an injection can be accepted for a
2877/// session whose control stream has not been established yet — that is the
2878/// promise [`crate::control::ProxyControl::inject_control`] makes. On the
2879/// drafts where the control stream is picked out of the unidirectional
2880/// accept loop, the task that will serve it is not known until its first
2881/// varint has been read, so the leg is pumped into that stream's own inbox
2882/// once it is: the same inbox the registry hands a `reset_stream` to, so one
2883/// task serves both verbs and they stay in the order they were asked for.
2884///
2885/// The returned guard ends the pump when the stream's task ends. A request
2886/// still in the leg's channel at that point stays there and is discarded
2887/// with the session, which is the outcome `inject_control` documents for
2888/// every message it accepts and cannot place.
2889fn pump_control_leg(leg: ControlLeg, inbox: mpsc::Sender<StreamCommand>) -> AbortOnDrop {
2890 let ControlLeg { inbox: _leg_inbox, mut requests } = leg;
2891 AbortOnDrop::new(tokio::spawn(async move {
2892 while let Some(command) = requests.recv().await {
2893 if inbox.send(command).await.is_err() {
2894 return;
2895 }
2896 }
2897 }))
2898}
2899
2900/// The largest control-message header this crate can meet: an eight-byte
2901/// type varint followed by an eight-byte length varint.
2902const MAX_CONTROL_HEADER: usize = 16;
2903
2904/// A declared control-message payload length above which
2905/// [`ControlFrameWalker`] stops believing what it is reading.
2906///
2907/// Not a protocol limit and not enforced on anything — the bytes are
2908/// forwarded either way. It is a sanity bound on the walker's *own*
2909/// arithmetic: drafts 11 and later cap a control payload at 65535 by
2910/// framing it in sixteen bits, and drafts 07-10 frame it as a varint that
2911/// can say 2^62 but never does. A length that large is not a large message,
2912/// it is a length field read at the wrong offset — most likely because the
2913/// session's draft guess is wrong for the moq-00 cohort, where the framing
2914/// style changed at draft 11.
2915///
2916/// Without the bound the walker would count down through that number for
2917/// the rest of the session, hold every injection, and claim at teardown
2918/// that a message was half-written. With it, the walker says it does not
2919/// know where the boundaries are, which is the truth and which suppresses
2920/// both.
2921const MAX_CONTROL_PAYLOAD: usize = 1024 * 1024;
2922
2923/// Where the message boundaries are on a control stream being forwarded
2924/// verbatim.
2925///
2926/// A control stream is one framed byte sequence — type, length, payload,
2927/// repeated — and a byte injected into the middle of a payload is read by
2928/// the peer as part of that payload, leaving its decoder wrong about every
2929/// message after it. So an injection has to be placed *between* messages,
2930/// and on the pass-through pipe nothing else knows where that is: that pipe
2931/// forwards whatever `recv.read` returned, and read boundaries are not
2932/// message boundaries.
2933///
2934/// This walks the framing without decoding anything. It reads a type
2935/// varint's length from its first byte, reads the payload length, and then
2936/// counts payload bytes down to zero — one varint decode per message and no
2937/// per-byte work beyond the header. It allocates nothing and never holds a
2938/// message; the bytes go straight out as they always did.
2939///
2940/// # Why not the control parser
2941///
2942/// [`ControlStreamParser`] already knows this framing and is already built
2943/// on the pipes that observe or mutate. It also buffers each message whole
2944/// and decodes it into an `AnyControlMessage`, which is the cost the
2945/// pass-through pipe exists not to pay — and on an `Interest::NONE` session
2946/// with no observer it is not built at all, so a stream that has never been
2947/// parsed has no idea where it stands.
2948///
2949/// # It is only as right as the draft it was given
2950///
2951/// The framing changed at draft 11: earlier drafts write the payload length
2952/// as a QUIC varint, later ones as a fixed 16-bit big-endian field. This
2953/// walker is built from the session's current draft, which for the moq-00
2954/// cohort (drafts 07-14) is a configured guess until a SETUP is peeked. A
2955/// wrong guess makes the lengths wrong and the boundaries wrong with them.
2956/// It is the same exposure the object framer already documents for the same
2957/// cohort, and it fails the same way: [`Self::at_boundary`] latches to
2958/// `false` as soon as a header cannot be made sense of, so an injection on
2959/// a stream whose framing has been lost is held rather than written into
2960/// the middle of something.
2961struct ControlFrameWalker {
2962 draft: DraftVersion,
2963 /// Payload bytes still owed on the message being forwarded.
2964 remaining: usize,
2965 /// Header bytes of the next message collected so far.
2966 header: [u8; MAX_CONTROL_HEADER],
2967 /// How many of `header` are populated.
2968 header_len: usize,
2969 /// Set once the framing stops making sense, and never cleared. A
2970 /// walker that has lost the stream reports no boundaries at all, which
2971 /// holds every later injection instead of placing it by guesswork.
2972 lost: bool,
2973}
2974
2975/// What one more header byte told [`ControlFrameWalker`].
2976enum HeaderStep {
2977 /// The header is not complete yet.
2978 NeedMore,
2979 /// The header is complete and the message's payload is this long.
2980 Payload(usize),
2981 /// The header cannot be read on this draft.
2982 Lost,
2983}
2984
2985impl ControlFrameWalker {
2986 /// A walker positioned at the start of a control stream, which is a
2987 /// message boundary.
2988 fn new(draft: DraftVersion) -> Self {
2989 Self { draft, remaining: 0, header: [0; MAX_CONTROL_HEADER], header_len: 0, lost: false }
2990 }
2991
2992 /// Whether everything written so far ends on a message boundary, so
2993 /// another message may be written now.
2994 fn at_boundary(&self) -> bool {
2995 !self.lost && self.remaining == 0 && self.header_len == 0
2996 }
2997
2998 /// Whether a message has been started and not finished.
2999 ///
3000 /// Distinct from `!at_boundary()`: a walker that has lost the framing
3001 /// is at no boundary but also cannot claim a message is half-written,
3002 /// and reporting a truncation it cannot see would be a fabrication.
3003 fn is_mid_message(&self) -> bool {
3004 !self.lost && (self.remaining > 0 || self.header_len > 0)
3005 }
3006
3007 /// Account for `data` being forwarded, and answer the offset within it
3008 /// of the first message boundary it reaches.
3009 ///
3010 /// `None` when no message completes inside `data` — either because it
3011 /// is a middle slice of a long message, or because the framing has been
3012 /// lost. The *first* boundary rather than the last, so an injection
3013 /// held over from an earlier chunk goes out as early as this chunk
3014 /// allows.
3015 fn advance(&mut self, data: &[u8]) -> Option<usize> {
3016 if self.lost {
3017 return None;
3018 }
3019 let mut first = None;
3020 let mut i = 0;
3021 while i < data.len() {
3022 if self.remaining > 0 {
3023 let take = self.remaining.min(data.len() - i);
3024 self.remaining -= take;
3025 i += take;
3026 if self.remaining == 0 && first.is_none() {
3027 first = Some(i);
3028 }
3029 continue;
3030 }
3031 if self.header_len == MAX_CONTROL_HEADER {
3032 self.lost = true;
3033 return first;
3034 }
3035 self.header[self.header_len] = data[i];
3036 self.header_len += 1;
3037 i += 1;
3038 match self.header_step() {
3039 HeaderStep::NeedMore => {}
3040 HeaderStep::Lost => {
3041 self.lost = true;
3042 return first;
3043 }
3044 HeaderStep::Payload(len) => {
3045 self.header_len = 0;
3046 self.remaining = len;
3047 // A zero-length payload is a whole message in its
3048 // header, so the boundary is here rather than after
3049 // some later byte.
3050 if len == 0 && first.is_none() {
3051 first = Some(i);
3052 }
3053 }
3054 }
3055 }
3056 first
3057 }
3058
3059 /// Read the header collected so far, if it is complete.
3060 fn header_step(&self) -> HeaderStep {
3061 let type_len = self.draft.varint_len(self.header[0]);
3062 if type_len > MAX_CONTROL_HEADER {
3063 return HeaderStep::Lost;
3064 }
3065 if self.header_len < type_len {
3066 return HeaderStep::NeedMore;
3067 }
3068 if self.draft.uses_fixed_length_framing() {
3069 if self.header_len < type_len + 2 {
3070 return HeaderStep::NeedMore;
3071 }
3072 let hi = self.header[type_len] as usize;
3073 let lo = self.header[type_len + 1] as usize;
3074 return HeaderStep::Payload((hi << 8) | lo);
3075 }
3076 if self.header_len <= type_len {
3077 return HeaderStep::NeedMore;
3078 }
3079 let len_len = self.draft.varint_len(self.header[type_len]);
3080 if type_len + len_len > MAX_CONTROL_HEADER {
3081 return HeaderStep::Lost;
3082 }
3083 if self.header_len < type_len + len_len {
3084 return HeaderStep::NeedMore;
3085 }
3086 let mut cursor = &self.header[type_len..type_len + len_len];
3087 match self.draft.decode_varint(&mut cursor) {
3088 Ok(v) if v.into_inner() as usize <= MAX_CONTROL_PAYLOAD => {
3089 HeaderStep::Payload(v.into_inner() as usize)
3090 }
3091 // A length no control message has, so the field was read at the
3092 // wrong offset — see `MAX_CONTROL_PAYLOAD`.
3093 Ok(_) => HeaderStep::Lost,
3094 Err(_) => HeaderStep::Lost,
3095 }
3096 }
3097}
3098
3099/// Write one forwarded chunk with any held injections spliced in at
3100/// `split`.
3101///
3102/// `split` is the offset within `data` at which the destination stream is
3103/// between messages; `None` means it is not, so the chunk goes out whole
3104/// and the injections keep waiting. Injections are written in the order
3105/// they were requested, and each is written verbatim: the control plane's
3106/// contract is that they are already framed.
3107async fn write_with_injections(
3108 send: &mut SendStream,
3109 data: &[u8],
3110 split: Option<usize>,
3111 injections: &mut std::collections::VecDeque<Bytes>,
3112) -> Result<(), TransportError> {
3113 let Some(split) = split else {
3114 return send.write_all(data).await;
3115 };
3116 let (head, tail) = data.split_at(split);
3117 if !head.is_empty() {
3118 send.write_all(head).await?;
3119 }
3120 while let Some(bytes) = injections.pop_front() {
3121 send.write_all(&bytes).await?;
3122 }
3123 if !tail.is_empty() {
3124 send.write_all(tail).await?;
3125 }
3126 Ok(())
3127}
3128
3129/// Pipe one direction of a stream carrying MoQT control-message framing.
3130///
3131/// Three kinds of stream reach here, and they are the same shape on the
3132/// wire: the two directions of a bidirectional control stream on drafts
3133/// 07-16, one unidirectional control stream on drafts 17-19, and either
3134/// direction of a request stream on drafts 17-19 — draft-17 Section 9 says
3135/// "Every message on a control or request stream is formatted as follows",
3136/// one framing for both.
3137///
3138/// What separates them is not this function but what reaches its `requests`
3139/// channel: a control direction's channel is one of the session's two
3140/// control legs, so it carries injections; a request stream's is the
3141/// per-stream channel the registry hands a `reset_stream` to, and nothing
3142/// routes an injection there.
3143///
3144/// Bytes are forwarded to the peer immediately upon receipt — the parser
3145/// runs on a cloned copy purely to emit observer events. A stuck or
3146/// erroring parser can never block forwarding. This matches the
3147/// pass-through semantics of the data-stream and datagram paths.
3148///
3149/// If `ctx.draft_is_fixed` is false (moq-00 cohort, drafts 07–14), the
3150/// parser start is deferred until enough bytes arrive to peek the first
3151/// SETUP message and pick a concrete draft. Bytes observed during that
3152/// detection window are still forwarded immediately.
3153async fn pipe_control(
3154 recv: PeekedRecv,
3155 send: SendStream,
3156 side: ProxySide,
3157 key: StreamKey,
3158 requests: mpsc::Receiver<StreamCommand>,
3159 ctx: &ForwardCtx,
3160) -> Result<(), ProxyError> {
3161 if ctx.control_mutation {
3162 pipe_control_mutating(recv, send, side, key, requests, ctx).await
3163 } else {
3164 pipe_control_passthrough(recv, send, side, key, requests, ctx).await
3165 }
3166}
3167
3168/// Build a non-capturing control parser and count it.
3169fn new_control_parser(draft: DraftVersion, ctx: &ForwardCtx) -> ControlStreamParser {
3170 ctx.counters.note_control_parser_created();
3171 ControlStreamParser::new(draft)
3172}
3173
3174/// Build a capturing control parser and count it.
3175fn new_capturing_control_parser(draft: DraftVersion, ctx: &ForwardCtx) -> ControlStreamParser {
3176 ctx.counters.note_control_parser_created();
3177 ControlStreamParser::new_capturing(draft)
3178}
3179
3180/// Forward-first control stream pipe.
3181///
3182/// Bytes are forwarded to the peer the instant they arrive; the parser
3183/// runs on a cloned copy purely to drive observer events. No hook can
3184/// rewrite frames on this path because the bytes are already in flight.
3185///
3186/// # What it tracks even with nothing observing
3187///
3188/// Two things, and each only because nothing else on this path could.
3189///
3190/// A [`ControlFrameWalker`], which counts message lengths so a control-plane
3191/// injection can be placed between two messages rather than inside one. It
3192/// decodes no message, buffers no message and allocates nothing — one
3193/// varint read per message and a running byte count — so the "pure byte
3194/// pump" claim survives it in every sense a counter can see. It is not a
3195/// [`ControlStreamParser`] and does not touch `control_parsers_created`.
3196///
3197/// And the SETUP peek that settles the session's draft, on the `moq-00`
3198/// cohort where the ALPN does not. It is deliberately **not** behind
3199/// `observer_enabled`: the draft is what the object framer frames with, what
3200/// a datagram header decodes as, what the walker above measures with, and
3201/// what the capability table each hook site is shown answers for. A session
3202/// carrying a shaping profile with no observer and no interests needs every
3203/// one of those and would, behind that gate, have detected nothing at all —
3204/// so the profile would have been judged against the guess, armed against
3205/// the guess, and reported success. The peek costs one varint read per chunk
3206/// until it answers, and it answers on the chunk carrying the first SETUP.
3207async fn pipe_control_passthrough(
3208 mut recv: PeekedRecv,
3209 mut send: SendStream,
3210 side: ProxySide,
3211 key: StreamKey,
3212 mut requests: mpsc::Receiver<StreamCommand>,
3213 ctx: &ForwardCtx,
3214) -> Result<(), ProxyError> {
3215 let stream_id = recv.stream_id();
3216 let mut buf = [0u8; 8192];
3217
3218 // Where the destination stream's message boundaries are — the only
3219 // thing on this pipe that knows, because this pipe forwards read
3220 // chunks and read chunks end wherever the transport said. Injections
3221 // are held until it says the stream is between messages; see
3222 // `ControlFrameWalker` for what it costs and what it cannot promise.
3223 let mut walker = ControlFrameWalker::new(ctx.draft());
3224 let mut injections: std::collections::VecDeque<Bytes> = std::collections::VecDeque::new();
3225 // Whether the request channel still has senders. It has one for as
3226 // long as this stream is registered, which is this task's whole life,
3227 // so the latch is a guard against a `None` that would otherwise make
3228 // the branch complete immediately and spin the loop.
3229 let mut serving_requests = true;
3230
3231 // Built only when somebody is going to read the frames. An
3232 // `Interest::NONE` session with no observer allocates no parser at all,
3233 // which is what makes `control_parsers_created == 0` unconditional
3234 // rather than a claim about the read loop.
3235 let mut parser: Option<ControlStreamParser> =
3236 if ctx.control_frames_are_decoded() && ctx.draft_is_fixed {
3237 Some(new_control_parser(ctx.draft(), ctx))
3238 } else {
3239 None
3240 };
3241 // Refused frames already reported on this direction. Alongside the
3242 // parser rather than inside it, and reset by neither: a parser rebuilt
3243 // once the draft settles inherits this direction's acknowledgement, so
3244 // the once-per-direction impairment stays once per direction.
3245 let mut refused_seen: u64 = 0;
3246
3247 // Never non-empty on this path: `Site::Control` is not reached here, and
3248 // `Site::StreamEnd`'s only queueing action, `ResetStream`, is refused on
3249 // a control stream. `PendingQueue::new` allocates nothing.
3250 let mut pending =
3251 PendingQueue::new(ctx.egress, Arc::clone(&ctx.counters)).with_gauge(Arc::clone(&ctx.gauge));
3252 let mut deferred = DeferredEffects::new();
3253 let report = ctx.reporter(side, Some(stream_id));
3254
3255 // Every byte forwarded on this stream so far, held only while the draft
3256 // is still unsettled and released the instant it settles. Two things
3257 // read it, and both need it from byte zero: the peek that names the
3258 // draft, and the walker rebuilt around that name, which has to be walked
3259 // forward over what was already forwarded or it would think the stream
3260 // starts where the SETUP ended.
3261 let mut detect_buf = BytesMut::new();
3262 // Whether the draft is still open to being named by this direction's
3263 // SETUP. `false` from the first instant on an ALPN-fixed session, which
3264 // is where nothing below runs at all.
3265 let mut detecting = !ctx.draft_is_fixed;
3266
3267 let mut stop = StopWatcher::new();
3268
3269 loop {
3270 stop.arm(&send);
3271 let watching = stop.is_watching();
3272
3273 tokio::select! {
3274 result = recv.read(&mut buf) => {
3275 let chunk = match result {
3276 Ok(chunk) => chunk,
3277 Err(e) => {
3278 let e = ProxyError::from(e);
3279 stop.retire();
3280 let mut st = StreamState {
3281 stream_id,
3282 key,
3283 is_control_stream: true,
3284 pending: &mut pending,
3285 deferred: &mut deferred,
3286 };
3287 propagate_reset(&e, &mut send, &mut st, side, ctx, &report).await;
3288 return Err(e);
3289 }
3290 };
3291 match chunk {
3292 Some(n) => {
3293 let data = &buf[..n];
3294
3295 // ── The SETUP peek, ahead of everything ─────────
3296 //
3297 // First because the two things below it are built
3298 // from the draft: the walker decides where a message
3299 // ends, which is the framing that changed at draft
3300 // 11, and the parser decodes with the draft's codec.
3301 // Settling after the write would place this chunk's
3302 // injection by the guess it was about to stop
3303 // believing.
3304 //
3305 // `Some` exactly on the chunk that ends the peek,
3306 // carrying every byte forwarded on this stream so
3307 // far — because the parser built below has seen
3308 // none of them and the walker has to be re-walked
3309 // over the ones this chunk does not contain.
3310 let settled: Option<Bytes> = if !detecting {
3311 None
3312 } else {
3313 detect_buf.extend_from_slice(data);
3314 match peek_draft(&detect_buf, side) {
3315 DraftPeek::Named(named) => {
3316 detecting = false;
3317 ctx.draft.settle(named, setup_rank(side));
3318 Some(detect_buf.split().freeze())
3319 }
3320 // Nothing on this stream can name a draft,
3321 // so waiting for more of it only delays
3322 // every task parked on the answer. The
3323 // session keeps the draft it started with,
3324 // and says so at the rank that lets the
3325 // other direction still improve on it.
3326 DraftPeek::NotSetup => {
3327 detecting = false;
3328 ctx.draft.settle(ctx.draft.initial, DraftSource::Fallback);
3329 Some(detect_buf.split().freeze())
3330 }
3331 DraftPeek::NeedMore if detect_buf.len() >= DETECT_BUF_MAX => {
3332 detecting = false;
3333 ctx.emit(|| ProxyEvent::ParseError {
3334 session_id: ctx.session_id,
3335 side,
3336 error: format!(
3337 "control draft detection gave up after {} bytes; \
3338 falling back to {}",
3339 detect_buf.len(),
3340 ctx.draft(),
3341 ),
3342 });
3343 ctx.draft.settle(ctx.draft.initial, DraftSource::Fallback);
3344 Some(detect_buf.split().freeze())
3345 }
3346 DraftPeek::NeedMore => None,
3347 }
3348 };
3349
3350 // The walker, re-armed around the settled draft.
3351 //
3352 // It was built from the session's starting draft and
3353 // has been counting message lengths in that draft's
3354 // framing ever since — which, on the cohort that
3355 // reaches this line, may have been the wrong framing
3356 // from the first byte. A walker that read a length
3357 // field at the wrong offset latches and stays
3358 // latched, and a latched walker places no injection
3359 // ever again on this direction. So it is rebuilt
3360 // from byte zero rather than corrected: replaying
3361 // the bytes already forwarded leaves it exactly
3362 // where the old one stood, and right this time.
3363 //
3364 // Only the bytes *before* this chunk are replayed.
3365 // This chunk is the one the split below is computed
3366 // over, and advancing it twice would consume it.
3367 if let Some(forwarded) = settled.as_ref() {
3368 walker = ControlFrameWalker::new(ctx.draft());
3369 let prior = forwarded.len() - data.len();
3370 let _ = walker.advance(&forwarded[..prior]);
3371 }
3372
3373 // Where an injection may go, decided before the
3374 // write and from this chunk alone: offset 0 when
3375 // the previous chunk left the stream between
3376 // messages, otherwise the first boundary this
3377 // chunk reaches, and `None` when it reaches none.
3378 let split = if injections.is_empty() {
3379 let _ = walker.advance(data);
3380 None
3381 } else if walker.at_boundary() {
3382 let _ = walker.advance(data);
3383 Some(0)
3384 } else {
3385 walker.advance(data)
3386 };
3387
3388 // ── Forward immediately — no gating on parse ────
3389 if let Err(e) =
3390 write_with_injections(&mut send, data, split, &mut injections).await
3391 {
3392 let e = ProxyError::from(e);
3393 let mut st = StreamState {
3394 stream_id,
3395 key,
3396 is_control_stream: true,
3397 pending: &mut pending,
3398 deferred: &mut deferred,
3399 };
3400 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
3401 return Err(e);
3402 }
3403
3404 // ── Observer-only parse (side path) ─────────────
3405 // Skip parsing when nobody is observing: the proxy
3406 // becomes a pure byte pump on the control stream.
3407 // The parser is built on the chunk that settled the
3408 // draft, and is fed everything buffered up to that
3409 // point, so it starts at the stream's first byte
3410 // however many chunks the peek took.
3411 if let Some(forwarded) = settled {
3412 if ctx.control_frames_are_decoded() && parser.is_none() {
3413 parser = Some(new_control_parser(ctx.draft(), ctx));
3414 }
3415 if let Some(p) = parser.as_mut() {
3416 emit_parsed_frames(
3417 p,
3418 &forwarded,
3419 &mut refused_seen,
3420 side,
3421 ctx,
3422 &report,
3423 );
3424 }
3425 } else if let Some(p) = parser.as_mut() {
3426 emit_parsed_frames(p, data, &mut refused_seen, side, ctx, &report);
3427 }
3428 }
3429 None => {
3430 let mut st = StreamState {
3431 stream_id,
3432 key,
3433 is_control_stream: true,
3434 pending: &mut pending,
3435 deferred: &mut deferred,
3436 };
3437 if let Plan::CloseSession { .. } =
3438 run_stream_end(StreamEnd::Fin, &mut st, side, ctx, &report)
3439 {
3440 return Ok(());
3441 }
3442 ctx.emit(|| ProxyEvent::StreamClosed {
3443 session_id: ctx.session_id,
3444 side,
3445 });
3446 let _ = send.finish();
3447 return Ok(());
3448 }
3449 }
3450 }
3451 command = requests.recv(),
3452 if serving_requests && injections.len() < COMMAND_QUEUE_DEPTH =>
3453 {
3454 match command {
3455 Some(StreamCommand::Reset { code }) => {
3456 stop.retire();
3457 let _ = send.reset(code);
3458 let _ = recv.stop(code);
3459 // No event, for the reason `pipe_data_passthrough`
3460 // gives at its copy of this arm: the peer's
3461 // `RESET_STREAM` is the consequence, and neither
3462 // existing reset event means *the control plane asked
3463 // for this*.
3464 return Ok(());
3465 }
3466 Some(StreamCommand::Inject { bytes }) => {
3467 // Written now only when the stream is between
3468 // messages *and* nothing is already waiting;
3469 // otherwise it queues behind what is, so injections
3470 // reach the peer in the order they were requested.
3471 if injections.is_empty() && walker.at_boundary() {
3472 if let Err(e) = send.write_all(&bytes).await {
3473 let e = ProxyError::from(e);
3474 let mut st = StreamState {
3475 stream_id,
3476 key,
3477 is_control_stream: true,
3478 pending: &mut pending,
3479 deferred: &mut deferred,
3480 };
3481 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
3482 return Err(e);
3483 }
3484 } else {
3485 injections.push_back(bytes);
3486 }
3487 }
3488 None => serving_requests = false,
3489 }
3490 }
3491 outcome = stop.watch(), if watching => {
3492 // An idle control stream is MoQT's steady state, so this
3493 // branch has the largest blast radius in the session: a
3494 // false positive tears down a healthy connection. It is
3495 // safe because `stop_error` makes the peer's own
3496 // `STOP_SENDING` the only terminal outcome — see its docs.
3497 if let Some(e) = stop_error(outcome) {
3498 let mut st = StreamState {
3499 stream_id,
3500 key,
3501 is_control_stream: true,
3502 pending: &mut pending,
3503 deferred: &mut deferred,
3504 };
3505 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
3506 return Err(e);
3507 }
3508 }
3509 _ = ctx.cancel.cancelled() => {
3510 // A requested close whose drain window ran out, cutting a
3511 // control message in half. Reported and left alone: writing
3512 // the rest of the message would mean the proxy inventing
3513 // control-stream bytes neither peer wrote, and the peer's
3514 // decoder is going to see a truncated message either way.
3515 //
3516 // Conditioned on the session discarding — that is, on a
3517 // close that was given a deadline and spent it — because
3518 // every other teardown reaches this branch too, and on
3519 // those the peer is the one that went away.
3520 if ctx.gauge.is_discarding() && walker.is_mid_message() {
3521 report.impairment(ImpairmentKind::ControlStreamTruncated {
3522 error: "the drain window for a requested close expired with a control \
3523 message part-written"
3524 .to_string(),
3525 });
3526 }
3527 // Session teardown: drop the streams, which sends a FIN.
3528 // `cancel` also fires on a *clean* session end — the
3529 // first forwarding task to finish cancels the rest — so
3530 // resetting here would turn every orderly disconnect
3531 // into a RESET_STREAM no peer asked for, and MoQT treats
3532 // a reset control stream as a session-level error.
3533 return Ok(());
3534 }
3535 }
3536 }
3537}
3538
3539/// Parse-then-forward control stream pipe.
3540///
3541/// Bytes are withheld until a complete control message has been parsed, at
3542/// which point the hook's `on_control_message` is consulted and the
3543/// [`Action`] it returns is executed — forwarded verbatim, replaced,
3544/// dropped, or deferred behind the stream's queue. This adds a per-frame
3545/// latency cost; a hook that only observes should leave `interest()`
3546/// without [`Interest::CONTROL`] and take the pass-through path instead.
3547async fn pipe_control_mutating(
3548 mut recv: PeekedRecv,
3549 mut send: SendStream,
3550 side: ProxySide,
3551 key: StreamKey,
3552 mut requests: mpsc::Receiver<StreamCommand>,
3553 ctx: &ForwardCtx,
3554) -> Result<(), ProxyError> {
3555 let stream_id = recv.stream_id();
3556 let mut buf = [0u8; 8192];
3557
3558 // No `ControlFrameWalker` here, and none is needed: this pipe withholds
3559 // bytes until a whole message has been parsed and writes one message
3560 // per write, so control returning to the `select!` below is by itself
3561 // the statement that the destination stream is between messages. That
3562 // is what makes an injection sound on this path with no extra
3563 // bookkeeping.
3564 let mut serving_requests = true;
3565
3566 // Capturing parser — we need the original raw bytes so the hook can
3567 // choose to pass them through unchanged.
3568 let mut parser: Option<ControlStreamParser> = if ctx.draft_is_fixed {
3569 Some(new_capturing_control_parser(ctx.draft(), ctx))
3570 } else {
3571 None
3572 };
3573 // As on the pass-through pipe: this direction's acknowledgement,
3574 // outliving the parser that may be rebuilt under it.
3575 let mut refused_seen: u64 = 0;
3576
3577 let mut pending =
3578 PendingQueue::new(ctx.egress, Arc::clone(&ctx.counters)).with_gauge(Arc::clone(&ctx.gauge));
3579 let mut deferred = DeferredEffects::new();
3580 let report = ctx.reporter(side, Some(stream_id));
3581
3582 let mut detect_buf = BytesMut::new();
3583
3584 let mut stop = StopWatcher::new();
3585 // Whether the reset-only observer still has an answer for this stream.
3586 // See [`Source::ResetUnobservable`]: once it says no, it says no
3587 // immediately and forever, so it is latched off rather than re-polled.
3588 let mut reset_observable = true;
3589
3590 loop {
3591 stop.arm(&send);
3592 let watching = stop.is_watching();
3593 let can_read = pending.accepts_more();
3594 let head_release = pending.head_release();
3595
3596 tokio::select! {
3597 source = observe_source(&mut recv, &mut buf, can_read, reset_observable) => {
3598 let result = match source {
3599 Source::Read(result) => result,
3600 Source::ResetUnobservable => {
3601 reset_observable = false;
3602 continue;
3603 }
3604 };
3605 let chunk = match result {
3606 Ok(chunk) => chunk,
3607 Err(e) => {
3608 let e = ProxyError::from(e);
3609 stop.retire();
3610 let mut st = StreamState {
3611 stream_id,
3612 key,
3613 is_control_stream: true,
3614 pending: &mut pending,
3615 deferred: &mut deferred,
3616 };
3617 propagate_reset(&e, &mut send, &mut st, side, ctx, &report).await;
3618 return Err(e);
3619 }
3620 };
3621 match chunk {
3622 Some(n) => {
3623 let data = &buf[..n];
3624
3625 let parsed = match parser.as_mut() {
3626 Some(p) => {
3627 forward_mutated_frames(
3628 p,
3629 data,
3630 &mut refused_seen,
3631 &mut send,
3632 stream_id,
3633 key,
3634 &mut pending,
3635 &mut deferred,
3636 side,
3637 ctx,
3638 &report,
3639 )
3640 .await
3641 }
3642 None => {
3643 detect_buf.extend_from_slice(data);
3644 // The same peek the pass-through pipe makes,
3645 // and it publishes to the same cell: this
3646 // pipe is the control stream of a session
3647 // whose hook declared `Interest::CONTROL`,
3648 // and its data streams need the draft just
3649 // as much as any other session's.
3650 let new_parser = match peek_draft(&detect_buf, side) {
3651 DraftPeek::Named(named) => {
3652 ctx.draft.settle(named, setup_rank(side));
3653 Some(new_capturing_control_parser(ctx.draft(), ctx))
3654 }
3655 // Nothing here will ever name a draft.
3656 // Stop holding bytes for an answer that
3657 // is not coming — on this pipe that is
3658 // the whole stream, not just the peek.
3659 DraftPeek::NotSetup => {
3660 ctx.draft.settle(ctx.draft.initial, DraftSource::Fallback);
3661 Some(new_capturing_control_parser(ctx.draft(), ctx))
3662 }
3663 DraftPeek::NeedMore
3664 if detect_buf.len() >= DETECT_BUF_MAX =>
3665 {
3666 ctx.emit(|| ProxyEvent::ParseError {
3667 session_id: ctx.session_id,
3668 side,
3669 error: format!(
3670 "control draft detection gave up after {} bytes; \
3671 falling back to {}",
3672 detect_buf.len(),
3673 ctx.draft(),
3674 ),
3675 });
3676 ctx.draft.settle(ctx.draft.initial, DraftSource::Fallback);
3677 Some(new_capturing_control_parser(ctx.draft(), ctx))
3678 }
3679 // Still detecting; nothing to forward yet.
3680 DraftPeek::NeedMore => None,
3681 };
3682
3683 match new_parser {
3684 Some(mut p) => {
3685 let buffered = detect_buf.split().freeze();
3686 let out = forward_mutated_frames(
3687 &mut p,
3688 &buffered,
3689 &mut refused_seen,
3690 &mut send,
3691 stream_id,
3692 key,
3693 &mut pending,
3694 &mut deferred,
3695 side,
3696 ctx,
3697 &report,
3698 )
3699 .await;
3700 parser = Some(p);
3701 out
3702 }
3703 None => Ok(Flow::Continue),
3704 }
3705 }
3706 };
3707
3708 match parsed {
3709 Ok(Flow::Continue) => {}
3710 Ok(Flow::StreamOver) => return Ok(()),
3711 Err(e) => {
3712 let mut st = StreamState {
3713 stream_id,
3714 key,
3715 is_control_stream: true,
3716 pending: &mut pending,
3717 deferred: &mut deferred,
3718 };
3719 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
3720 return Err(e);
3721 }
3722 }
3723 }
3724 None => {
3725 let mut st = StreamState {
3726 stream_id,
3727 key,
3728 is_control_stream: true,
3729 pending: &mut pending,
3730 deferred: &mut deferred,
3731 };
3732 if drain_pending(&mut send, &mut st, Site::Control, None, ctx, &report).await?
3733 == Flow::StreamOver
3734 {
3735 return Ok(());
3736 }
3737 if let Plan::CloseSession { .. } =
3738 run_stream_end(StreamEnd::Fin, &mut st, side, ctx, &report)
3739 {
3740 return Ok(());
3741 }
3742 ctx.emit(|| ProxyEvent::StreamClosed {
3743 session_id: ctx.session_id,
3744 side,
3745 });
3746 let _ = send.finish();
3747 return Ok(());
3748 }
3749 }
3750 }
3751 () = egress::wait_release(head_release.clone(), &ctx.cancel),
3752 if head_release.is_some() =>
3753 {
3754 // The write that pays back a `Delay` or a `Hold` can fail
3755 // with the destination peer's `STOP_SENDING` exactly like
3756 // the seven inline write sites — and on a stream whose
3757 // hook defers, it is the *only* write there is. A bare `?`
3758 // here returns without mirroring, `recv` is dropped, and
3759 // quinn's `RecvStream::drop` stops the source with a
3760 // hard-coded 0: the peer's reason silently replaced by
3761 // "unspecified" on the one path built to carry it.
3762 //
3763 // The stream-level `StopWatcher` branch does not cover
3764 // this. Once `select!` has picked this branch, its arm body
3765 // runs to completion with no branch polling at all, so a
3766 // `STOP_SENDING` that lands while `release_due_units` is
3767 // inside `write_all` surfaces here and nowhere else.
3768 let released = release_due_units(
3769 &mut pending,
3770 &mut deferred,
3771 &mut send,
3772 Site::Control,
3773 // The control pipes are never shaped. This queue was
3774 // built without a scheduler, so it can produce no shaping
3775 // decision; passing `None` here means it could not report
3776 // one either.
3777 None,
3778 ctx,
3779 &report,
3780 )
3781 .await;
3782 match released {
3783 Ok(Flow::StreamOver) => return Ok(()),
3784 Ok(Flow::Continue) => {}
3785 Err(e) => {
3786 let mut st = StreamState {
3787 stream_id,
3788 key,
3789 is_control_stream: true,
3790 pending: &mut pending,
3791 deferred: &mut deferred,
3792 };
3793 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
3794 return Err(e);
3795 }
3796 }
3797 }
3798 command = requests.recv(), if serving_requests => {
3799 match command {
3800 Some(StreamCommand::Reset { code }) => {
3801 // Everything queued goes with the stream, which is
3802 // what a reset means: the destination is abandoned,
3803 // so units still waiting for a release time have
3804 // nowhere to be written.
3805 pending.clear();
3806 deferred.clear();
3807 stop.retire();
3808 let _ = send.reset(code);
3809 let _ = recv.stop(code);
3810 // No event, for the reason `pipe_data_passthrough`
3811 // gives at its copy of this arm: the peer's
3812 // `RESET_STREAM` is the consequence, and neither
3813 // existing reset event means *the control plane asked
3814 // for this*.
3815 return Ok(());
3816 }
3817 Some(StreamCommand::Inject { bytes }) => {
3818 // Behind whatever a hook has deferred, when it has
3819 // deferred anything. Writing inline past a
3820 // non-empty queue would put the injected message
3821 // ahead of messages the hook explicitly asked to
3822 // hold back, reordering the control stream against
3823 // the one decision that exists to order it.
3824 if pending.is_empty() {
3825 if let Err(e) = send.write_all(&bytes).await {
3826 let e = ProxyError::from(e);
3827 let mut st = StreamState {
3828 stream_id,
3829 key,
3830 is_control_stream: true,
3831 pending: &mut pending,
3832 deferred: &mut deferred,
3833 };
3834 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
3835 return Err(e);
3836 }
3837 } else {
3838 exec::enqueue_unshown(&mut pending, &mut deferred, bytes, &report);
3839 }
3840 }
3841 None => serving_requests = false,
3842 }
3843 }
3844 outcome = stop.watch(), if watching => {
3845 // See `pipe_control_passthrough`'s copy of this branch for
3846 // why an idle control stream is not endangered by it.
3847 if let Some(e) = stop_error(outcome) {
3848 let mut st = StreamState {
3849 stream_id,
3850 key,
3851 is_control_stream: true,
3852 pending: &mut pending,
3853 deferred: &mut deferred,
3854 };
3855 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
3856 return Err(e);
3857 }
3858 }
3859 _ = ctx.cancel.cancelled() => {
3860 let _ = pending.drain_ignoring_release_times(&mut send).await;
3861 // See `PendingQueue::unconfirmed_bytes`: a flush into a
3862 // transport that is being closed leaves nothing queued and
3863 // delivers nothing, so `queued_bytes` reports zero for a
3864 // stream whose bytes are gone.
3865 let stranded = pending.unconfirmed_bytes();
3866 if stranded > 0 {
3867 report.impairment(ImpairmentKind::QueuedBytesAtTeardown {
3868 stream_id,
3869 bytes: stranded,
3870 });
3871 }
3872 // Session teardown: drop the streams, which sends a FIN.
3873 // `cancel` also fires on a *clean* session end — the
3874 // first forwarding task to finish cancels the rest — so
3875 // resetting here would turn every orderly disconnect
3876 // into a RESET_STREAM no peer asked for, and MoQT treats
3877 // a reset control stream as a session-level error.
3878 return Ok(());
3879 }
3880 }
3881 }
3882}
3883
3884/// Which stream a shaped release is happening on, for the events it owes.
3885///
3886/// `None` at the two control call sites, and that `None` is what makes *control
3887/// streams are never shaped* structural rather than remembered: a control pipe
3888/// installs no scheduler on its queue *and* has nothing to report a shaping
3889/// decision against, so neither the decision nor its event can appear there.
3890///
3891/// Carries the stream's **own** scheduler rather than reaching for the
3892/// session's current one. The two differ from the moment a profile is
3893/// installed on the proxy while this stream is forwarding: this stream was
3894/// classified, queued and paced by the scheduler recorded here, so a report
3895/// about one of its units has to be labelled and deduplicated against that
3896/// scheduler. Asking the session for its current shaper instead would name
3897/// the report after whatever class sits at that index in the *new* profile —
3898/// a correct number under a wrong label, which is the one failure the whole
3899/// shaping surface is written to avoid.
3900#[derive(Clone)]
3901struct ShapedStream {
3902 side: ProxySide,
3903 key: StreamKey,
3904 stream_id: u64,
3905 /// The scheduler this stream runs under, for the life of the stream.
3906 shaper: Arc<Scheduler>,
3907}
3908
3909/// Write every unit whose release time has arrived, in order.
3910///
3911/// The body of the `select!` release branch, shared by the control and data
3912/// pipes. Each released unit pays back exactly one ledger entry — the
3913/// second half of a `Delay` / `Hold`, whose first half reported
3914/// `Effect::Queued` when the decision was taken.
3915///
3916/// On a shaped data stream this is also where the pacer runs: every
3917/// `pop_next_due` below is a `Scheduler::acquire`, so a class whose bucket is
3918/// dry simply stops yielding units and the loop ends with the queue intact.
3919/// Nothing about the shape of this function changes — that is the point of
3920/// putting the seam in `pop_next_due` rather than beside it.
3921async fn release_due_units(
3922 pending: &mut PendingQueue,
3923 deferred: &mut DeferredEffects,
3924 send: &mut SendStream,
3925 site: Site,
3926 shaped: Option<&ShapedStream>,
3927 ctx: &ForwardCtx,
3928 report: &exec::Reporter<'_>,
3929) -> Result<Flow, ProxyError> {
3930 // A release timer coarser than the engine asked for is reported once
3931 // per session, on its first deferred release, rather than silently
3932 // absorbed into the lateness distribution.
3933 if let Some(backend) = crate::release_timer::backend() {
3934 if !backend.is_high_resolution() && ctx.counters.claim_coarse_timer_report() {
3935 report.impairment(ImpairmentKind::CoarseReleaseTimer { backend, detail: None });
3936 }
3937 }
3938
3939 let now = Instant::now();
3940 while let Some(unit) = pending.pop_next_due(now) {
3941 pending.record_release(&unit, now);
3942 let outcome = pending.take_shape_report();
3943 if matches!(outcome, Some(egress::ShapeReport::Expired)) {
3944 // An expiry replaces the whole queue with the reset it decided
3945 // on, so the ledger's entries went with the units that owed
3946 // them. Clearing it here keeps `DeferredEffects::len() ==
3947 // PendingQueue::len()` — the invariant `exec::push_unit` exists
3948 // to hold — and stops the synthesized terminal paying back a
3949 // `Delay` that never reached the wire.
3950 deferred.clear();
3951 }
3952 report_shaping(outcome, shaped, ctx, report);
3953 if let Some(owed) = deferred.pop() {
3954 report.applied_deferred(site, owed);
3955 }
3956 if let egress::Written::Terminated { .. } = egress::write_unit(unit, send).await? {
3957 pending.clear();
3958 deferred.clear();
3959 return Ok(Flow::StreamOver);
3960 }
3961 }
3962 // A refusal reports too: the clamp is decided when the head is *not*
3963 // yielded, so reading the report only after a successful pop would lose
3964 // the one case that matters.
3965 report_shaping(pending.take_shape_report(), shaped, ctx, report);
3966 Ok(Flow::Continue)
3967}
3968
3969/// Emit whatever a shaped release decided, if anything.
3970///
3971/// Nothing at all on an unshaped stream and on both control pipes: the queue
3972/// only ever produces a report when a scheduler was installed on it.
3973fn report_shaping(
3974 outcome: Option<egress::ShapeReport>,
3975 shaped: Option<&ShapedStream>,
3976 ctx: &ForwardCtx,
3977 report: &exec::Reporter<'_>,
3978) {
3979 let (Some(outcome), Some(stream)) = (outcome, shaped) else { return };
3980 match outcome {
3981 // `HoldClamped`'s cardinality is already *once per clamped unit*, and a
3982 // shaping clamp is exactly that: a unit released at `max_hold` because
3983 // the bucket would not have released it at all.
3984 egress::ShapeReport::Clamped { requested, applied } => {
3985 report.impairment(ImpairmentKind::HoldClamped { requested, applied });
3986 }
3987 // Once per session per class, and the scheduler owns the latch
3988 // because the burst is the profile's rather than this stream's: the
3989 // queue re-decides it on every refusal, and every stream carrying the
3990 // class re-decides it too. `None` means somebody has already said it.
3991 egress::ShapeReport::BurstBelowUnit { class, burst_bytes, unit_bytes } => {
3992 if let Some(name) = stream.shaper.claim_burst_report(class) {
3993 report.impairment(ImpairmentKind::ShapeBurstBelowUnit {
3994 class: name,
3995 burst_bytes,
3996 unit_bytes,
3997 });
3998 }
3999 }
4000 egress::ShapeReport::Expired => ctx.emit(|| ProxyEvent::Shaped {
4001 session_id: ctx.session_id,
4002 side: stream.side,
4003 key: stream.key,
4004 stream_id: stream.stream_id,
4005 // An expiry abandons the whole destination stream, so like a
4006 // policy reset it is about the stream and not about the unit
4007 // that happened to outlive its clamp.
4008 class: String::new(),
4009 outcome: ShapeOutcome::Expired,
4010 }),
4011 }
4012}
4013
4014/// Feed bytes into the capturing control parser, then execute the hook's
4015/// decision on each completed frame.
4016///
4017/// This pipe owns the forwarding path: nothing reaches the far side except
4018/// what this function writes. A frame the decoder refuses is therefore
4019/// written verbatim rather than skipped — no hook can be consulted about a
4020/// message that did not decode, but dropping it would remove a control
4021/// message from a session neither peer knows is missing one.
4022#[allow(clippy::too_many_arguments)]
4023async fn forward_mutated_frames(
4024 parser: &mut ControlStreamParser,
4025 data: &[u8],
4026 refused_seen: &mut u64,
4027 send: &mut SendStream,
4028 stream_id: u64,
4029 key: StreamKey,
4030 pending: &mut PendingQueue,
4031 deferred: &mut DeferredEffects,
4032 side: ProxySide,
4033 ctx: &ForwardCtx,
4034 report: &exec::Reporter<'_>,
4035) -> Result<Flow, ProxyError> {
4036 if let ParseResult::Framed(items) = parser.feed(data) {
4037 // Ahead of the hook, for the same reason the observation-only pipe
4038 // reports ahead of its events: the frame that was lost preceded the
4039 // frames the hook is about to be handed.
4040 report_refused_frames(&items, refused_seen, ctx, report);
4041
4042 for item in items {
4043 // A frame this proxy could not read still has a peer that may
4044 // be able to. On this pipe the parser *is* the forwarding path,
4045 // so bytes it kept to itself never reach the far side at all:
4046 // the message would be deleted from the session, and every
4047 // Request ID and state transition it carried with it. No hook
4048 // is consulted, because there is no decoded message to offer
4049 // one, and no action can be taken on bytes nobody can read.
4050 let mut frame = match item {
4051 ParsedItem::Frame(frame) => frame,
4052 ParsedItem::Refused(refused) => {
4053 let raw = refused.raw_bytes.expect("capturing parser must populate raw_bytes");
4054 send.write_all(&raw).await?;
4055 continue;
4056 }
4057 };
4058 let raw = frame.raw_bytes.take().expect("capturing parser must populate raw_bytes");
4059
4060 let arrived_at = Instant::now();
4061 let draft = ctx.draft();
4062 let caps = ctx.caps();
4063 let cx = FrameCtx::new(ctx.session_id, side, draft, Some(stream_id), arrived_at, &caps);
4064 let action = ctx.hook.on_control_message(&cx, &frame.message, &raw);
4065 let unit = exec::Unit { target: exec::Target::Control { raw }, draft, arrived_at };
4066 let mut engine = exec::Engine {
4067 queue: Some(exec::Queue { pending, deferred }),
4068 closer: &ctx.closer,
4069 };
4070 let out = exec::execute(&unit, action, &mut engine, report);
4071
4072 match out.plan {
4073 Plan::WriteNow(bytes) => {
4074 // Read off what is going out rather than off what came
4075 // in, and only here rather than beside the decode above:
4076 // a hook on this pipe may rewrite a FETCH, and the
4077 // publisher answers the request it receives. A frame the
4078 // hook dropped reaches `Plan::Nothing` and files nothing,
4079 // because no response stream will ever come for it.
4080 note_fetch_order(&bytes, ctx);
4081 send.write_all(&bytes).await?;
4082 }
4083 Plan::Nothing => {}
4084 // `Truncate` and `ResetStream` are refused on every control
4085 // stream on every draft, so no terminal can be queued here;
4086 // handled rather than `unreachable!()`d because a panicking
4087 // forwarding task is worse than a redundant arm.
4088 Plan::Terminal => {
4089 let mut st =
4090 StreamState { stream_id, key, is_control_stream: true, pending, deferred };
4091 let _ = drain_pending(send, &mut st, Site::Control, None, ctx, report).await?;
4092 return Ok(Flow::StreamOver);
4093 }
4094 // Stream-shaped plans; only `execute_stream` produces
4095 // them and it is never called from the control path.
4096 Plan::RejectStream { .. }
4097 | Plan::OpenStreamAfter { .. }
4098 | Plan::SerializeStreamAfter { .. } => {}
4099 Plan::CloseSession { .. } => return Ok(Flow::StreamOver),
4100 }
4101
4102 if ctx.observer_enabled {
4103 ctx.observer.on_event(&control_event(ctx.session_id, side, frame.message));
4104 }
4105 }
4106 }
4107 Ok(Flow::Continue)
4108}
4109
4110/// Feed bytes to the control parser and emit observer events for any
4111/// completed frames.
4112///
4113/// The hook is deliberately not invoked here — on the pass-through path
4114/// the bytes have already been forwarded, so an [`Action`] returned there
4115/// would be unexecutable. Hooks that need to see control messages without
4116/// rewriting them should be implemented as a [`ProxyObserver`]; hooks that
4117/// need to rewrite them declare [`Interest::CONTROL`], which routes traffic
4118/// through `pipe_control_mutating` instead.
4119fn emit_parsed_frames(
4120 parser: &mut ControlStreamParser,
4121 data: &[u8],
4122 refused_seen: &mut u64,
4123 side: ProxySide,
4124 ctx: &ForwardCtx,
4125 report: &exec::Reporter<'_>,
4126) {
4127 match parser.feed(data) {
4128 ParseResult::Framed(items) => {
4129 // What the session needs for itself, before anything about
4130 // telling somebody: a parser exists on this path for two
4131 // unrelated reasons and only one of them is an observer. The
4132 // bytes went out verbatim on this pipe, so the message decoded
4133 // here is exactly the one the peer will act on.
4134 note_fetch_orders(&items, ctx);
4135
4136 // Before the events, because a chunk carrying a refused frame
4137 // ahead of a good one lost the first and delivered the second,
4138 // and an observer reading in order should learn of the loss
4139 // where it happened rather than after everything that survived
4140 // it. Outside the observer gate because the counter it moves is
4141 // the proxy's own record of what it could not read; the report
4142 // beside it is gated within.
4143 report_refused_frames(&items, refused_seen, ctx, report);
4144
4145 if !ctx.observer_enabled {
4146 return;
4147 }
4148 for item in items {
4149 // A refused frame has no message to report as one. Its
4150 // bytes were forwarded before this function was called, so
4151 // the impairment above is the whole of what is owed here.
4152 if let ParsedItem::Frame(frame) = item {
4153 ctx.observer.on_event(&control_event(ctx.session_id, side, frame.message));
4154 }
4155 }
4156 }
4157 ParseResult::NeedMore => {}
4158 }
4159}
4160
4161/// File the Group Order every FETCH in this batch asked for.
4162///
4163/// For the pass-through pipe, whose frames reach the peer unchanged, so the
4164/// message decoded from them is the one the publisher will answer.
4165fn note_fetch_orders(items: &[ParsedItem], ctx: &ForwardCtx) {
4166 if !ctx.fetch_orders_wanted {
4167 return;
4168 }
4169 for item in items {
4170 if let ParsedItem::Frame(frame) = item {
4171 if let Some((request_id, order)) = frame.message.fetch_group_order() {
4172 ctx.fetch_orders.record(request_id, order);
4173 }
4174 }
4175 }
4176}
4177
4178/// File the Group Order a FETCH asked for, from the bytes leaving the proxy.
4179///
4180/// For the mutating pipe, where the frame the hook returned is the one the
4181/// peer receives and therefore the one that settles the response's order. It
4182/// is decoded a second time here for that reason alone: the message decoded
4183/// on the way in is what *arrived*, and on this pipe those are allowed to
4184/// differ. Bytes the hook returned that no longer decode file nothing, and
4185/// the stream they were about is bypassed rather than read against an order
4186/// the publisher never agreed to.
4187fn note_fetch_order(outgoing: &[u8], ctx: &ForwardCtx) {
4188 if !ctx.fetch_orders_wanted {
4189 return;
4190 }
4191 let Ok(message) = AnyControlMessage::decode(ctx.draft(), &mut &outgoing[..]) else {
4192 return;
4193 };
4194 if let Some((request_id, order)) = message.fetch_group_order() {
4195 ctx.fetch_orders.record(request_id, order);
4196 }
4197}
4198
4199/// Report the control frames the decoder refused in one feed.
4200///
4201/// Called from both control pipes: one parser refusing a frame is one
4202/// parser, and a helper wired into a single site would have left the other
4203/// pipe as silent as neither was.
4204///
4205/// The counter takes every refusal; the impairment goes out once per
4206/// direction and carries the count it went out with. `seen` is that
4207/// direction's running acknowledgement, and it is a caller's local because
4208/// the parser deliberately holds no reporting state - how often to say a
4209/// thing is a property of the event stream, not of the framing.
4210fn report_refused_frames(
4211 items: &[ParsedItem],
4212 seen: &mut u64,
4213 ctx: &ForwardCtx,
4214 report: &exec::Reporter<'_>,
4215) {
4216 let mut refused = items.iter().filter_map(|item| match item {
4217 ParsedItem::Refused(r) => Some(r),
4218 ParsedItem::Frame(_) => None,
4219 });
4220 let Some(head) = refused.next() else { return };
4221 let count = 1 + refused.count() as u64;
4222
4223 ctx.counters.note_control_frames_not_decodable(count);
4224
4225 // Read before `seen` moves: this is the first report on this direction
4226 // exactly when nothing had been acknowledged before it.
4227 let first = *seen == 0;
4228 *seen += count;
4229 if first {
4230 report.impairment(ImpairmentKind::ControlFrameNotDecodable {
4231 type_id: head.type_id,
4232 total: *seen,
4233 });
4234 }
4235}
4236
4237/// The observer event one parsed control frame produces.
4238//
4239// `AnyControlMessage::is_setup` is `unreachable!()` in a build with no
4240// draft feature enabled — the enum has no variants there, so it is
4241// uninhabited and every expression after the call is genuinely dead. The
4242// allow is scoped to exactly that build so a real unreachable branch in a
4243// normal build is still an error.
4244#[cfg_attr(
4245 not(any(
4246 feature = "draft07",
4247 feature = "draft08",
4248 feature = "draft09",
4249 feature = "draft10",
4250 feature = "draft11",
4251 feature = "draft12",
4252 feature = "draft13",
4253 feature = "draft14",
4254 feature = "draft15",
4255 feature = "draft16",
4256 feature = "draft17",
4257 feature = "draft18",
4258 feature = "draft19"
4259 )),
4260 allow(unreachable_code)
4261)]
4262fn control_event(
4263 session_id: SessionId,
4264 side: ProxySide,
4265 message: moqtap_codec::dispatch::AnyControlMessage,
4266) -> ProxyEvent {
4267 if message.is_setup() {
4268 ProxyEvent::SetupMessage { session_id, side, message }
4269 } else {
4270 ProxyEvent::ControlMessage { session_id, side, message }
4271 }
4272}
4273
4274/// Forward unidirectional streams from source to destination.
4275///
4276/// # Why `dest` is an `Arc` and `source` is not
4277///
4278/// [`StreamAction::OpenAfter`](crate::action::StreamAction::OpenAfter)
4279/// defers `dest.open_uni()` past the accept
4280/// loop, into the spawned per-stream task, so the destination transport has
4281/// to be *shared* rather than borrowed for the loop's lifetime. Both call
4282/// sites already hold an `Arc<Transport>` and `Transport` is not `Clone`,
4283/// so this is the only shape available. `source` stays a borrow: nothing is
4284/// ever done with it outside the loop.
4285///
4286/// # The two open topologies, and why the default one did not move
4287///
4288/// [`StreamAction::Open`](crate::action::StreamAction::Open)
4289/// — and therefore every session that never returns
4290/// `OpenAfter` — keeps `dest.open_uni()` **in the accept loop**, between the
4291/// `Site::StreamOpen` decision and the spawn, exactly where it has always
4292/// been. That is what keeps the two reject sites observably different: a
4293/// reject at the open site creates no peer stream at all, while a reject at
4294/// the header site resets a peer stream that already exists having carried
4295/// nothing. Opening lazily for every stream would collapse that difference
4296/// into one behaviour and silently retire a published capability
4297/// distinction.
4298///
4299/// The `OpenAfter` arm spawns first and opens inside the task, after the
4300/// delay — and it opens *before* the first byte is read, so by the time the
4301/// header site is reached the peer stream exists there too and a reject
4302/// there still resets it.
4303async fn forward_uni_streams(
4304 source: &Transport,
4305 dest: Arc<Transport>,
4306 side: ProxySide,
4307 ctx: &ForwardCtx,
4308 control: Option<ControlLeg>,
4309) -> Result<(), ProxyError> {
4310 // `Some` only on the drafts whose control plane is a pair of
4311 // unidirectional streams, where one of the streams this loop accepts is
4312 // this direction's control stream. Shared rather than owned because
4313 // which one it is cannot be known until a stream's first varint has been
4314 // read, and that read happens inside the per-stream task: whichever task
4315 // reads `CONTROL_STREAM_TYPE` first takes the leg, and a second one — a
4316 // peer opening two control streams, which the drafts forbid — finds it
4317 // gone and is forwarded as a control stream the control plane cannot
4318 // reach, rather than stealing the channel from the first.
4319 let control = Arc::new(Mutex::new(control));
4320 debug_assert!(
4321 control.lock().expect("nothing holds this yet").is_none()
4322 || control_plane_is_unidirectional(ctx.draft.initial),
4323 "a control leg belongs on the unidirectional accept loop only where the control plane \
4324 is a pair of unidirectional streams",
4325 );
4326 loop {
4327 tokio::select! {
4328 result = source.accept_uni() => {
4329 let mut recv = result?;
4330 let stream_id = recv.stream_id();
4331 // Minted before the open decision, so the key a hook is
4332 // shown at `Site::StreamOpen` is the key it will see again
4333 // at the header and at the end.
4334 let key = ctx.mint_key(side);
4335 ctx.emit(|| ProxyEvent::UniStreamOpened {
4336 session_id: ctx.session_id,
4337 side,
4338 });
4339
4340 // What the `Site::StreamOpen` decision changed about how
4341 // this stream starts. Both stay `None` for `Open`, for a
4342 // hook that declared no stream interest, and for every
4343 // refused action — so the default topology below is the
4344 // one every existing test still takes.
4345 let mut open_after: Option<Duration> = None;
4346 let mut serialize_after: Option<StreamKey> = None;
4347
4348 // The reject decision is taken between `accept_uni` and
4349 // `open_uni`, so a rejected stream never exists on the far
4350 // side at all.
4351 if ctx.streams_enabled {
4352 let report = ctx.reporter(side, Some(stream_id));
4353 let draft = ctx.draft();
4354 let caps = ctx.caps();
4355 let scx = StreamCtx::new(
4356 ctx.session_id,
4357 side,
4358 stream_id,
4359 draft,
4360 false,
4361 &caps,
4362 key,
4363 );
4364 let action = ctx.hook.on_stream_open(&scx);
4365 let out = exec::execute_stream(
4366 StreamSite::Open,
4367 draft,
4368 action,
4369 &report,
4370 );
4371 // An exhaustive `match`, not an `if let`:
4372 // `OpenStreamAfter` and `SerializeStreamAfter` are
4373 // decided here and honoured further down, and a
4374 // wildcard would let a plan this site forgets to carry
4375 // become a silent no-op instead of a compile error.
4376 match out.plan {
4377 Plan::RejectStream { code } => {
4378 let _ = recv.stop(code);
4379 continue;
4380 }
4381 Plan::OpenStreamAfter { after } => open_after = Some(after),
4382 Plan::SerializeStreamAfter { target } => {
4383 serialize_after = Some(target);
4384 }
4385 Plan::Nothing => {}
4386 Plan::WriteNow(_) | Plan::Terminal | Plan::CloseSession { .. } => {}
4387 }
4388 }
4389
4390 // Registered *before* the open, and released by dropping
4391 // the guard. Before, because `open_uni().await` is a
4392 // suspension point and a stream this one might be
4393 // serialized behind must be waitable from the moment its
4394 // key exists. A stream rejected above never gets here, so
4395 // a key naming one answers "nothing to wait for", which is
4396 // the truth: it was never forwarded.
4397 // The stream's request channel is minted with its
4398 // registration and dies with it: the sending half lives in
4399 // the registry entry, the receiving half in the task
4400 // below, so a key that has been retired cannot be reached
4401 // and a task that is running always can be.
4402 let (inbox, requests) = mpsc::channel(COMMAND_QUEUE_DEPTH);
4403 // A second sender, kept only where a stream on this loop
4404 // might turn out to be a control stream, so that the
4405 // session's control leg can be pumped into the same inbox
4406 // the registry already reaches this stream through. `None`
4407 // everywhere else, which is every draft through 16.
4408 let control_inbox =
4409 control_plane_is_unidirectional(ctx.draft.initial).then(|| inbox.clone());
4410 let guard = ctx.streams.register(key, inbox);
4411
4412 // The default topology, unmoved: open between the decision
4413 // and the spawn. `OpenAfter` is the only arm that defers,
4414 // and it opens inside the task instead.
4415 let opened = match open_after {
4416 None => Some(dest.open_uni().await?),
4417 Some(_) => None,
4418 };
4419
4420 let ctx = ctx.clone();
4421 let dest = Arc::clone(&dest);
4422 let control = Arc::clone(&control);
4423
4424 tokio::spawn(async move {
4425 // Moved in, and dropped on every exit from this task —
4426 // returns, `?`, panics, and the task future being dropped
4427 // wholesale at session teardown. That is what makes *the
4428 // gate is released on every termination path* a structural
4429 // claim rather than a list.
4430 let _guard = guard;
4431
4432 let send = match opened {
4433 Some(send) => send,
4434 None => {
4435 let after = open_after.unwrap_or_default();
4436 tokio::select! {
4437 () = tokio::time::sleep(after) => {}
4438 () = ctx.cancel.cancelled() => return,
4439 }
4440 match dest.open_uni().await {
4441 Ok(send) => send,
4442 Err(e) => {
4443 // The destination connection went away
4444 // during the delay. The four other
4445 // top-level tasks fail on it too and
4446 // end the session; this is the
4447 // diagnostic, reported through the same
4448 // channel and with the same
4449 // already-mirrored guard as a pipe
4450 // failure.
4451 let e = ProxyError::from(e);
4452 if !is_mirrored_teardown(&e) {
4453 ctx.emit(|| ProxyEvent::ParseError {
4454 session_id: ctx.session_id,
4455 side,
4456 error: format!("deferred uni stream open: {e}"),
4457 });
4458 }
4459 return;
4460 }
4461 }
4462 }
4463 };
4464
4465 // What this stream is, on the drafts where a
4466 // unidirectional stream can be either half of the
4467 // control plane or a data stream. Everywhere else the
4468 // question does not arise and nothing is read here.
4469 //
4470 // The *starting* draft, here and at the other four
4471 // topology reads, and not the session's current one:
4472 // where the control plane lives was decided once, in
4473 // `run_with_transport`, and the tasks that implement
4474 // that decision were spawned from it. A SETUP peek that
4475 // moved the answer afterwards would leave one loop
4476 // forwarding request streams and another expecting a
4477 // control stream on a topology nobody built.
4478 let (recv, kind) = if control_plane_is_unidirectional(ctx.draft.initial) {
4479 classify_uni_stream(recv, ctx.draft.initial).await
4480 } else {
4481 (PeekedRecv::new(recv), UniStreamKind::Data)
4482 };
4483
4484 let result = match kind {
4485 UniStreamKind::Control => {
4486 // The other topology's copy of the same latch:
4487 // this session has a control stream, so a task
4488 // waiting on the draft has something to wait
4489 // for. See `SessionDraft::control_stream_open`.
4490 ctx.draft.note_control_stream();
4491 // Held for the pipe's whole life and dropped
4492 // with it, so the leg stops being pumped the
4493 // moment there is nothing to pump it into. The
4494 // leg is taken only when there is an inbox to
4495 // pump it into, so a build that somehow reached
4496 // this arm without one leaves the leg where it
4497 // is rather than dropping the session's only
4498 // route for an injection.
4499 //
4500 // A `SerializeAfter` returned for this stream at
4501 // `Site::StreamOpen` is not honoured here, and
4502 // was not on the drafts where the control stream
4503 // is a bidirectional stream either: holding a
4504 // control stream's first write behind another
4505 // stream would hold SETUP, and the session with
4506 // it.
4507 let _pump = control_inbox.and_then(|inbox| {
4508 control
4509 .lock()
4510 .expect("no task holds the control leg across a panic")
4511 .take()
4512 .map(|leg| pump_control_leg(leg, inbox))
4513 });
4514 pipe_control(recv, send, side, key, requests, &ctx).await
4515 }
4516 UniStreamKind::Data => {
4517 pipe_data(recv, send, side, key, serialize_after, requests, &ctx)
4518 .await
4519 }
4520 };
4521
4522 if let Err(e) = result {
4523 // An abnormal teardown is an ordinary protocol
4524 // event, already reported as `StreamReset` and
4525 // already mirrored onto the far side. Reporting
4526 // it again as `ParseError` would claim the codec
4527 // failed and that the bytes were still forwarded,
4528 // both of which are false.
4529 if !is_mirrored_teardown(&e) {
4530 ctx.emit(|| ProxyEvent::ParseError {
4531 session_id: ctx.session_id,
4532 side,
4533 error: format!("uni stream pipe: {e}"),
4534 });
4535 }
4536 }
4537 });
4538 }
4539 _ = ctx.cancel.cancelled() => {
4540 return Ok(());
4541 }
4542 }
4543 }
4544}
4545
4546/// Determine the data stream type from the first varint on the stream.
4547///
4548/// MoQT data streams start with a stream type varint:
4549/// - 0x04 = Subgroup
4550/// - 0x05 = Fetch
4551///
4552/// The varint itself is not consumed here: the framer is fed the stream
4553/// from its first byte and the header decoder owns the type field.
4554fn detect_stream_type(first_byte: u8) -> DataStreamType {
4555 // The stream type varint is a single byte for values < 64.
4556 // Subgroup = 0x04, Fetch = 0x05.
4557 match first_byte {
4558 0x05 => DataStreamType::Fetch,
4559 // Default to Subgroup for 0x04 and anything else
4560 _ => DataStreamType::Subgroup,
4561 }
4562}
4563
4564/// Pipe a unidirectional data stream.
4565///
4566/// The choice made here is the whole cost model of the data path:
4567/// `pipe_data_passthrough` never allocates and never decodes, while
4568/// `pipe_data_framed` buffers each object whole so it can be reported.
4569async fn pipe_data(
4570 recv: PeekedRecv,
4571 send: SendStream,
4572 side: ProxySide,
4573 key: StreamKey,
4574 serialize_after: Option<StreamKey>,
4575 requests: mpsc::Receiver<StreamCommand>,
4576 ctx: &ForwardCtx,
4577) -> Result<(), ProxyError> {
4578 if let Some(target) = serialize_after {
4579 let report = ctx.reporter(side, Some(recv.stream_id()));
4580 await_serialize_target(target, key, ctx, &report).await;
4581 }
4582 // The whole claim, checked where the framing decision is actually
4583 // taken rather than only where it is computed: a configured
4584 // `ShapeProfile` implies framing. Classification needs `ObjectMeta`,
4585 // and only `pipe_data_framed` produces it — so a shaped session that
4586 // reached the pass-through pipe would be a byte pump reporting
4587 // success, which is the one outcome the assertion exists to prevent.
4588 debug_assert!(
4589 !ctx.shaping_enabled || ctx.objects_enabled,
4590 "a session with a ShapeProfile must be framed: shaping cannot classify a byte pump"
4591 );
4592 // And the two shaping fields agree. They are separate so the hot path
4593 // can test a `bool` without touching an `Arc`, which is exactly the
4594 // kind of duplication that drifts: a session that armed framing for a
4595 // profile it then failed to build a shaper for would classify nothing
4596 // and report success.
4597 debug_assert_eq!(
4598 ctx.shaping_enabled,
4599 ctx.shape.is_some(),
4600 "shaping_enabled is the cached `shape.is_some()`, not a second decision"
4601 );
4602 if ctx.objects_enabled {
4603 pipe_data_framed(recv, send, side, key, requests, ctx).await
4604 } else {
4605 pipe_data_passthrough(recv, send, side, key, requests, ctx).await
4606 }
4607}
4608
4609/// What a data stream's task does with a control-plane request.
4610///
4611/// Shared by both data pipes because the answer is the same on each: a
4612/// reset ends the stream, and an injection cannot happen here.
4613///
4614/// The caller does the resetting, because it holds `&mut send` and
4615/// `&mut recv`; this only says what to do.
4616enum StreamRequest {
4617 /// Reset the destination and stop the source with this code.
4618 Reset(u64),
4619 /// Nothing to do — keep forwarding.
4620 Ignore,
4621 /// The channel has no senders left; stop polling it.
4622 Closed,
4623}
4624
4625/// Interpret one request delivered to a data stream's task.
4626fn data_stream_request(command: Option<StreamCommand>) -> StreamRequest {
4627 match command {
4628 Some(StreamCommand::Reset { code }) => StreamRequest::Reset(code),
4629 // Injection is a control-stream operation and is routed by leg to
4630 // one of the two control directions, so nothing sends this here.
4631 // Handled rather than `unreachable!()`d, because a panicking
4632 // forwarding task is worse than a branch that does nothing — the
4633 // same ruling the plan matches in this file already take.
4634 Some(StreamCommand::Inject { .. }) => StreamRequest::Ignore,
4635 None => StreamRequest::Closed,
4636 }
4637}
4638
4639/// Hold this stream until `target` ends —
4640/// [`StreamAction::SerializeAfter`](crate::action::StreamAction::SerializeAfter).
4641///
4642/// The peer stream is already open (that is what the action says: *open now,
4643/// write nothing until*), so what is being held is the first write on it. This
4644/// function holds the whole pipe rather than gating one queued unit: the effect
4645/// on the wire is identical — nothing is written — and the read is held with
4646/// it, which is `Overflow::Block`'s own answer to *the destination is not
4647/// ready*, not a new mechanism.
4648///
4649/// # Three ways this cannot hang the session
4650///
4651/// 1. **Session cancellation** is one of the three racers, so teardown is
4652/// never waiting on a hook's bookkeeping.
4653/// 2. **[`EgressConfig::max_hold`]** is the ceiling, so a target whose gate
4654/// is somehow never released costs a bounded delay rather than a stream
4655/// that lives forever. It is the same ceiling a `Hold` gets, for the same
4656/// reason — a scenario may not make a stream unkillable.
4657/// 3. **A target that cannot end later than now resolves immediately** and
4658/// says so once. Three cases are one report: a key that was never
4659/// forwarded, a stream that has already ended, and *this* stream. The
4660/// third is the interesting one — a self-serialize is unsatisfiable by
4661/// construction, and left unguarded it would be a `max_hold` stall
4662/// attributed to the pacer rather than to the hook that asked for it.
4663async fn await_serialize_target(
4664 target: StreamKey,
4665 key: StreamKey,
4666 ctx: &ForwardCtx,
4667 report: &exec::Reporter<'_>,
4668) {
4669 let gate = if target == key { None } else { ctx.streams.gate_for(target) };
4670 match gate {
4671 None => report.impairment(ImpairmentKind::SerializeTargetUnknown { key, target }),
4672 Some(gate) => {
4673 tokio::select! {
4674 () = gate.wait() => {}
4675 () = tokio::time::sleep(ctx.egress.max_hold) => {}
4676 () = ctx.cancel.cancelled() => {}
4677 }
4678 }
4679 }
4680}
4681
4682/// Forward a unidirectional data stream without interpreting it.
4683///
4684/// A stack buffer, a write and one boxed stop-watcher per stream — no
4685/// parser and still no per-object work. This is the path every session
4686/// takes when nothing is observing and no hook declared object or stream
4687/// interest.
4688///
4689/// The watcher is the single heap allocation this function makes, and it
4690/// is made lazily on the first `select!` iteration (see [`StopWatcher`]),
4691/// once per forwarded stream. `Counters` has no allocation field, so
4692/// `interest_none.rs`'s whole-struct `Counters::default()` comparison
4693/// cannot see this cost — this sentence is the only gate it has, which is
4694/// why it is stated rather than quietly dropped.
4695async fn pipe_data_passthrough(
4696 mut recv: PeekedRecv,
4697 mut send: SendStream,
4698 side: ProxySide,
4699 key: StreamKey,
4700 mut requests: mpsc::Receiver<StreamCommand>,
4701 ctx: &ForwardCtx,
4702) -> Result<(), ProxyError> {
4703 let stream_id = recv.stream_id();
4704 let mut buf = [0u8; 8192];
4705 let mut serving_requests = true;
4706
4707 // `Interest::STREAMS` contains `Interest::OBJECTS`, so a session that
4708 // reaches this function has `streams_enabled == false` and never
4709 // queues anything. The queue is here because the teardown helpers take
4710 // one; `PendingQueue::new` allocates nothing.
4711 let mut pending =
4712 PendingQueue::new(ctx.egress, Arc::clone(&ctx.counters)).with_gauge(Arc::clone(&ctx.gauge));
4713 let mut deferred = DeferredEffects::new();
4714 let report = ctx.reporter(side, Some(stream_id));
4715
4716 let mut stop = StopWatcher::new();
4717
4718 loop {
4719 stop.arm(&send);
4720 let watching = stop.is_watching();
4721
4722 tokio::select! {
4723 result = recv.read(&mut buf) => {
4724 let chunk = match result {
4725 Ok(chunk) => chunk,
4726 Err(e) => {
4727 let e = ProxyError::from(e);
4728 stop.retire();
4729 let mut st = StreamState {
4730 stream_id,
4731 key,
4732 is_control_stream: false,
4733 pending: &mut pending,
4734 deferred: &mut deferred,
4735 };
4736 propagate_reset(&e, &mut send, &mut st, side, ctx, &report).await;
4737 return Err(e);
4738 }
4739 };
4740 match chunk {
4741 Some(n) => {
4742 if let Err(e) = send.write_all(&buf[..n]).await {
4743 let e = ProxyError::from(e);
4744 let mut st = StreamState {
4745 stream_id,
4746 key,
4747 is_control_stream: false,
4748 pending: &mut pending,
4749 deferred: &mut deferred,
4750 };
4751 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
4752 return Err(e);
4753 }
4754 }
4755 None => {
4756 let mut st = StreamState {
4757 stream_id,
4758 key,
4759 is_control_stream: false,
4760 pending: &mut pending,
4761 deferred: &mut deferred,
4762 };
4763 match run_stream_end(StreamEnd::Fin, &mut st, side, ctx, &report) {
4764 Plan::Terminal => {
4765 // `None`: the pass-through pipe installs no
4766 // scheduler on its queue, so no shaping
4767 // decision can be taken here.
4768 let _ = drain_pending(
4769 &mut send, &mut st, Site::Object, None, ctx, &report,
4770 )
4771 .await?;
4772 return Ok(());
4773 }
4774 Plan::CloseSession { .. } => return Ok(()),
4775 _ => {}
4776 }
4777 ctx.emit(|| ProxyEvent::StreamClosed {
4778 session_id: ctx.session_id,
4779 side,
4780 });
4781 let _ = send.finish();
4782 return Ok(());
4783 }
4784 }
4785 }
4786 command = requests.recv(), if serving_requests => {
4787 match data_stream_request(command) {
4788 StreamRequest::Reset(code) => {
4789 stop.retire();
4790 let _ = send.reset(code);
4791 let _ = recv.stop(code);
4792 // No event. `ProxyEvent::StreamReset` means a
4793 // teardown this proxy *observed* on a peer, and
4794 // `ActionApplied` means a hook asked for one; a
4795 // control-plane reset is neither, and borrowing
4796 // either would make an existing event ambiguous
4797 // for every reader that already relies on it. What
4798 // it produces is a `RESET_STREAM` carrying `code`
4799 // at the destination peer, which is the
4800 // consequence worth observing.
4801 return Ok(());
4802 }
4803 StreamRequest::Ignore => {}
4804 StreamRequest::Closed => serving_requests = false,
4805 }
4806 }
4807 outcome = stop.watch(), if watching => {
4808 // The idle case: nothing is being written on this stream,
4809 // so no `write_all` can surface the peer's `STOP_SENDING`
4810 // and without this branch the source is never stopped.
4811 if let Some(e) = stop_error(outcome) {
4812 let mut st = StreamState {
4813 stream_id,
4814 key,
4815 is_control_stream: false,
4816 pending: &mut pending,
4817 deferred: &mut deferred,
4818 };
4819 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
4820 return Err(e);
4821 }
4822 }
4823 _ = ctx.cancel.cancelled() => {
4824 // Session teardown: drop the streams, which sends a FIN.
4825 // `cancel` also fires on a *clean* session end — the
4826 // first forwarding task to finish cancels the rest — so
4827 // resetting here would turn every orderly disconnect
4828 // into a RESET_STREAM no peer asked for, and MoQT treats
4829 // a reset control stream as a session-level error.
4830 return Ok(());
4831 }
4832 }
4833 }
4834}
4835
4836/// Forward a unidirectional data stream through the object framer.
4837///
4838/// Every byte written to the destination comes out of
4839/// [`ObjectFramer::poll`], so a forwarded stream on which no action was
4840/// taken is byte-identical to the received one — the framer only decides
4841/// where the boundaries are. The cost is latency: an object is not
4842/// forwarded until it is buffered whole, or until the framer gives up on
4843/// it and streams it through.
4844///
4845/// # The draft this frames with
4846///
4847/// Taken once, at the top, from the session's shared cell and **waited
4848/// for** — see [`SessionDraft::resolved`]. Once, because the draft decides
4849/// where an object ends: a stream framed half under one draft and half under
4850/// another would report object boundaries that were never on the wire.
4851/// Waited for, because on drafts 07 to 14 the ALPN names no draft and the
4852/// answer arrives on the control stream, in a task this one was spawned
4853/// alongside — so reading the cell without waiting is a race the session
4854/// loses whenever the two tasks are polled in the other order, and losing it
4855/// means framing every object on this stream against the configured guess.
4856///
4857/// A wrong draft is not a fidelity failure — the framer latches a bypass and
4858/// forwards the rest of the stream byte for byte — but it is a silent
4859/// failure of everything built on the framing: no object reaches a hook, no
4860/// shaping class claims one, and the session reports success.
4861async fn pipe_data_framed(
4862 mut recv: PeekedRecv,
4863 mut send: SendStream,
4864 side: ProxySide,
4865 key: StreamKey,
4866 mut requests: mpsc::Receiver<StreamCommand>,
4867 ctx: &ForwardCtx,
4868) -> Result<(), ProxyError> {
4869 // The ordering edge. Ahead of the first read, so no byte of this stream
4870 // is interpreted before the draft it is interpreted under is known, and
4871 // held in a local for the stream's whole life: every hook site, every
4872 // report and the framer itself answer for the same draft, whatever the
4873 // control stream learns later.
4874 let draft = ctx.resolved_draft().await;
4875 let caps = Capabilities::for_draft(draft);
4876 let stream_id = recv.stream_id();
4877 let mut buf = [0u8; 8192];
4878 let mut serving_requests = true;
4879 let mut framer: Option<ObjectFramer> = None;
4880 // The drafts 17-19 subgroup-ID mode from this stream's header, which
4881 // separates a reserved header mode from the *subgroup ID is the first
4882 // object's ID* mode when an elide is judged.
4883 let mut subgroup_id_mode: Option<u8> = None;
4884 let mut not_addressable_reported = false;
4885 // A separate latch from `not_addressable_reported`, because the two
4886 // reports have different audiences and different conditions: that one
4887 // fires on every session with a framer, this one only on a session with a
4888 // profile, where the same object additionally escapes a configured rate.
4889 let mut unpaced_reported = false;
4890
4891 // The session's shaper, or `None`. Everything below that reads it is
4892 // behind this one binding, so an unshaped stream's admission cost is a
4893 // single `Option` test per object and nothing else.
4894 //
4895 // Read **once**, here, and held for the whole stream. That is what makes
4896 // a profile installed on the proxy while this stream runs land on the
4897 // next stream rather than in the middle of this one: the classification
4898 // below, the queue built from it and every release decision it makes all
4899 // come from this one `Arc`, so a unit cannot be classified against one
4900 // profile's rules and charged against another's buckets.
4901 let shaper = ctx.shape.as_ref().map(|s| s.current());
4902 let shape = shaper.as_deref();
4903 // The one construction site in the crate that installs a scheduler. Both
4904 // control pipes and `pipe_data_passthrough` call `PendingQueue::new` and
4905 // stop there, so *the control pipes are never shaped* is a property of
4906 // which queue got a shaper and not of a rule anyone has to remember.
4907 let mut pending = PendingQueue::new(ctx.egress, Arc::clone(&ctx.counters))
4908 .with_gauge(Arc::clone(&ctx.gauge))
4909 .with_shape_depth(shape.and_then(Scheduler::blocking_depth))
4910 .with_shaper(shaper.clone(), Arc::clone(&ctx.shape_stats), side);
4911 let mut deferred = DeferredEffects::new();
4912 let report = ctx.reporter(side, Some(stream_id));
4913 // What a shaping decision on this stream is reported against, and `None`
4914 // when there is nothing to decide. Carries the same `Arc` the queue got,
4915 // so a report is labelled by the scheduler that produced it.
4916 let shaped_stream = shaper.clone().map(|shaper| ShapedStream { side, key, stream_id, shaper });
4917
4918 let mut stop = StopWatcher::new();
4919 // Admission state, all per stream.
4920 //
4921 // `shaped_units` is the counter `Matcher::every_nth` is defined
4922 // against: hook-visible units on *this stream*, never
4923 // `ObjectMeta::index_in_stream` (which counts oversized objects the
4924 // hook never sees) and never anything wider (which the tokio scheduler
4925 // orders, destroying reproducibility).
4926 let mut shaped_units: u64 = 0;
4927 // The class of the most recently classified unit. What a *stream*-level
4928 // report — a block episode — is charged to, because a stream has no
4929 // single class of its own.
4930 let mut last_class = Class::Default;
4931 // Whether any unit on this stream has been classified yet, and whether
4932 // two of them disagreed. Head-gating makes configured shaping and
4933 // head-of-line blocking indistinguishable from outside, so a stream that
4934 // carries two classes has to say so — once.
4935 let mut first_class: Option<Class> = None;
4936 let mut mixed_reported = false;
4937 // Edge triggers. `blocked` re-arms when the queue drains, so
4938 // `blocked_episodes` counts episodes rather than `select!` iterations;
4939 // `drop_reported` never re-arms, because `ProxyEvent::Shaped` is capped
4940 // at once per stream per outcome.
4941 let mut blocked = false;
4942 let mut drop_reported = false;
4943 // Whether the reset-only observer still has an answer for this stream;
4944 // see [`Source::ResetUnobservable`]. This is the stream whose read
4945 // branch a shaping profile can hold shut for `max_hold`, so it is the
4946 // stream the observer exists for.
4947 let mut reset_observable = true;
4948
4949 loop {
4950 stop.arm(&send);
4951 let watching = stop.is_watching();
4952 let can_read = pending.accepts_more();
4953 let head_release = pending.head_release();
4954
4955 // `Overflow::Block`, measured where it actually happens: `can_read`
4956 // false means `observe_source` does not call `recv.read()`, so
4957 // nothing is consumed off the wire and no flow-control credit is
4958 // granted. (It parks on the peer's reset instead, which reads no
4959 // bytes — see `observe_source`. The episode is the same episode.)
4960 // Counted only when a blocking depth was installed — engine
4961 // backpressure is not shaping, and charging it here would make
4962 // `blocked_episodes` non-zero under `DropTail`, where nothing
4963 // blocks.
4964 if shape.and_then(Scheduler::blocking_depth).is_some() {
4965 if !can_read {
4966 if !blocked {
4967 blocked = true;
4968 ctx.shape_stats.note_blocked(last_class);
4969 }
4970 } else {
4971 blocked = false;
4972 }
4973 }
4974
4975 tokio::select! {
4976 source = observe_source(&mut recv, &mut buf, can_read, reset_observable) => {
4977 let result = match source {
4978 Source::Read(result) => result,
4979 Source::ResetUnobservable => {
4980 reset_observable = false;
4981 continue;
4982 }
4983 };
4984 let chunk = match result {
4985 Ok(chunk) => chunk,
4986 Err(e) => {
4987 let e = ProxyError::from(e);
4988 stop.retire();
4989 let mut st = StreamState {
4990 stream_id,
4991 key,
4992 is_control_stream: false,
4993 pending: &mut pending,
4994 deferred: &mut deferred,
4995 };
4996 propagate_reset(&e, &mut send, &mut st, side, ctx, &report).await;
4997 return Err(e);
4998 }
4999 };
5000 match chunk {
5001 Some(n) => {
5002 let data = &buf[..n];
5003 if data.is_empty() {
5004 continue;
5005 }
5006 // The framer sees the stream from its first byte;
5007 // the stream-type field belongs to the header
5008 // decoder, not to this loop.
5009 let framer = framer.get_or_insert_with(|| {
5010 let framer = ObjectFramer::with_recorder(
5011 detect_stream_type(data[0]),
5012 draft,
5013 FramerConfig::default(),
5014 Arc::clone(&ctx.counters),
5015 );
5016 // Handed over only where a fetch stream cannot be
5017 // read without it, so that a framer holding one on
5018 // a draft that needs none could not quietly become
5019 // the way the answer is expected to arrive.
5020 if fetch_group_order_is_needed(draft) {
5021 framer.with_fetch_group_orders(Arc::clone(&ctx.fetch_orders))
5022 } else {
5023 framer
5024 }
5025 });
5026 framer.feed(data);
5027
5028 loop {
5029 let arrived_at = Instant::now();
5030 // Every arm but `Object` yields bytes no rule can
5031 // see — a stream header, an oversized object's
5032 // passthrough chunk, a bypassed stream's tail —
5033 // so the default tag is `Unshapeable` and only
5034 // the object arm overwrites it. They still take
5035 // an ordering slot; they just charge no bucket.
5036 pending.tag_unit(Class::Unshapeable);
5037 let raw = match framer.poll() {
5038 FramerOut::NeedMore => break,
5039 FramerOut::Header { header, raw } => {
5040 ctx.emit(|| ProxyEvent::DataStreamHeader {
5041 session_id: ctx.session_id,
5042 side,
5043 header: header.clone(),
5044 });
5045 if let DataStreamHeaderKind::Subgroup(h) = &header {
5046 subgroup_id_mode = h.subgroup_id_mode();
5047 }
5048 if ctx.streams_enabled {
5049 let scx = StreamCtx::new(
5050 ctx.session_id,
5051 side,
5052 stream_id,
5053 draft,
5054 false,
5055 &caps,
5056 key,
5057 );
5058 let action =
5059 ctx.hook.on_stream_header(&scx, &header);
5060 let out = exec::execute_stream(
5061 StreamSite::Header,
5062 draft,
5063 action,
5064 &report,
5065 );
5066 // The peer stream already exists, so
5067 // it is reset having carried zero
5068 // payload bytes, and the source is
5069 // stopped. No header byte is
5070 // forwarded.
5071 match out.plan {
5072 Plan::RejectStream { code } => {
5073 stop.retire();
5074 let _ = send.reset(code);
5075 let _ = recv.stop(code);
5076 return Ok(());
5077 }
5078 // Nothing has been written on
5079 // this stream yet — the header's
5080 // own bytes go out below, after
5081 // the match — so holding here
5082 // is the same "write nothing
5083 // until" the open site gives.
5084 // Awaiting inside a `select!`
5085 // arm body suspends the other
5086 // branches, which is why the
5087 // wait races cancellation; the
5088 // release branch already has
5089 // exactly this property.
5090 Plan::SerializeStreamAfter { target } => {
5091 await_serialize_target(
5092 target, key, ctx, &report,
5093 )
5094 .await;
5095 }
5096 // `OpenAfter` cannot reach here:
5097 // the header site refuses it
5098 // with `WrongSite`, so `admit`
5099 // returned `Err` and the plan is
5100 // `Nothing`.
5101 Plan::OpenStreamAfter { .. } => {}
5102 Plan::Nothing => {}
5103 Plan::WriteNow(_)
5104 | Plan::Terminal
5105 | Plan::CloseSession { .. } => {}
5106 }
5107 }
5108 raw
5109 }
5110 FramerOut::Object { meta, raw } => {
5111 // ── ADMISSION ──────────────────────
5112 //
5113 // The shaping path's entry point.
5114 // Gated on `ctx.shape`, which is
5115 // `Some` exactly when a profile was
5116 // configured — with no
5117 // `observer_enabled ||` term, exactly
5118 // as the arming gate — so a session
5119 // with no profile adds nothing here
5120 // and its `ShapeStats` stays
5121 // `default()` for the same reason its
5122 // `Counters` do.
5123 //
5124 // `note_object_seen` is taken before
5125 // anything decides: it counts what the
5126 // shaper *saw* on the wire, which must
5127 // not depend on whether a hook was
5128 // also consulted, on what that hook
5129 // returned, or on what a policy did to
5130 // the unit. The class rows below are
5131 // charged from the same `raw.len()`,
5132 // so the conservation identity the
5133 // release side completes is an
5134 // identity over one measurement and
5135 // not two.
5136 if let Some(shaper) = shape {
5137 ctx.shape_stats.note_object_seen(side, raw.len() as u64);
5138 let unit_index = shaped_units;
5139 shaped_units += 1;
5140 last_class = shaper.classify(
5141 side,
5142 &meta,
5143 unit_index,
5144 |class, field| {
5145 report.impairment(
5146 ImpairmentKind::ShapeRuleUnmatchable {
5147 class: shaper.class_name(class),
5148 field,
5149 draft,
5150 },
5151 );
5152 },
5153 );
5154 // The class rides with the unit from
5155 // here: `PendingQueue::push` reads
5156 // this tag, so `exec`'s own pushes —
5157 // a `Delay`, a `Hold`, an elided
5158 // ordering slot — are charged to the
5159 // same class without `exec` ever
5160 // naming one.
5161 pending.tag_unit(last_class);
5162 // Two classes on one stream means the
5163 // head decides the whole stream's
5164 // throughput. Said once, with a
5165 // counter behind it, or a scenario
5166 // author reads head-of-line blocking
5167 // as their configured shaping.
5168 match first_class {
5169 None => first_class = Some(last_class),
5170 Some(first)
5171 if first != last_class && !mixed_reported =>
5172 {
5173 mixed_reported = true;
5174 ctx.shape_stats.note_mixed_class_stream(side);
5175 report.impairment(
5176 ImpairmentKind::ClassChangedMidStream {
5177 key,
5178 stream_id,
5179 },
5180 );
5181 }
5182 Some(_) => {}
5183 }
5184 // Admission runs **before** the
5185 // hook, and that is the coherent
5186 // choice rather than an accident:
5187 // under `Overflow::Block` a unit
5188 // the queue has no room for is
5189 // never read off the wire at all,
5190 // so the hook never sees it. A
5191 // `DropTail` that showed the hook
5192 // an object the engine had already
5193 // decided to discard would let it
5194 // return `Replace` and report an
5195 // `ActionApplied { Replaced }` for
5196 // a wire change that never
5197 // happened.
5198 match shaper.admit(
5199 raw.len(),
5200 pending.queued_bytes(),
5201 pending.len(),
5202 ) {
5203 Admission::Admit => {}
5204 Admission::DropTail => {
5205 let unit = exec::Unit {
5206 target: exec::Target::Object {
5207 meta: &meta,
5208 subgroup_id_mode,
5209 raw: raw.clone(),
5210 },
5211 draft,
5212 arrived_at,
5213 };
5214 // A guard that refuses
5215 // leaves the unit admitted
5216 // and the queue one over
5217 // depth: a shaper may not
5218 // corrupt a stream's
5219 // absolute object IDs to
5220 // honour a depth limit.
5221 if exec::shape_elide(&unit, &report) {
5222 framer.note_elided(&meta);
5223 ctx.shape_stats
5224 .note_dropped(last_class, raw.len() as u64);
5225 if !drop_reported {
5226 drop_reported = true;
5227 ctx.emit(|| ProxyEvent::Shaped {
5228 session_id: ctx.session_id,
5229 side,
5230 key,
5231 stream_id,
5232 class: class_label(shaper, last_class),
5233 outcome: ShapeOutcome::Dropped,
5234 });
5235 }
5236 continue;
5237 }
5238 }
5239 Admission::ResetStream { code } => {
5240 ctx.shape_stats.note_stream_reset_by_shaping(side);
5241 // Everything queued is
5242 // discarded by design, not
5243 // lost: the destination is
5244 // gone. The same shape the
5245 // `ElideFixupLost` teardown
5246 // takes — including the
5247 // order, which is reset
5248 // first and report second.
5249 // The event names the code
5250 // the stream was reset with,
5251 // and an event that names a
5252 // reset the transport has
5253 // not been asked for yet is
5254 // a claim rather than a
5255 // record.
5256 pending.clear();
5257 deferred.clear();
5258 stop.retire();
5259 let _ = send.reset(code);
5260 ctx.emit(|| ProxyEvent::Shaped {
5261 session_id: ctx.session_id,
5262 side,
5263 key,
5264 stream_id,
5265 // A stream reset is
5266 // about the stream, not
5267 // about the unit that
5268 // tripped it, so it
5269 // carries no class.
5270 class: String::new(),
5271 outcome: ShapeOutcome::StreamReset { code },
5272 });
5273 return Ok(());
5274 }
5275 }
5276 }
5277 ctx.emit(|| ProxyEvent::Object {
5278 session_id: ctx.session_id,
5279 side,
5280 meta,
5281 });
5282 if !ctx.object_hook {
5283 raw
5284 } else {
5285 let ocx = ObjectCtx::new(
5286 ctx.session_id,
5287 side,
5288 stream_id,
5289 &meta,
5290 arrived_at,
5291 &caps,
5292 );
5293 let action = ctx.hook.on_object(&ocx, &raw);
5294 let unit = exec::Unit {
5295 target: exec::Target::Object {
5296 meta: &meta,
5297 subgroup_id_mode,
5298 raw: raw.clone(),
5299 },
5300 draft,
5301 arrived_at,
5302 };
5303 let mut engine = exec::Engine {
5304 queue: Some(exec::Queue {
5305 pending: &mut pending,
5306 deferred: &mut deferred,
5307 }),
5308 closer: &ctx.closer,
5309 };
5310 let out =
5311 exec::execute(&unit, action, &mut engine, &report);
5312 if out.note_elided {
5313 framer.note_elided(&meta);
5314 }
5315 match out.plan {
5316 Plan::WriteNow(bytes) => {
5317 if let Err(e) =
5318 send.write_all(&bytes).await
5319 {
5320 let e = ProxyError::from(e);
5321 let mut st = StreamState {
5322 stream_id,
5323 key,
5324 is_control_stream: false,
5325 pending: &mut pending,
5326 deferred: &mut deferred,
5327 };
5328 propagate_stop(
5329 &e, &mut recv, &mut st, side, ctx,
5330 &report,
5331 );
5332 return Err(e);
5333 }
5334 }
5335 Plan::Nothing => {}
5336 Plan::Terminal => {
5337 let mut st = StreamState {
5338 stream_id,
5339 key,
5340 is_control_stream: false,
5341 pending: &mut pending,
5342 deferred: &mut deferred,
5343 };
5344 let drained = drain_pending(
5345 &mut send,
5346 &mut st,
5347 Site::Object,
5348 shaped_stream.as_ref(),
5349 ctx,
5350 &report,
5351 )
5352 .await;
5353 // The source has not FINed, so
5354 // a `STOP_SENDING` surfacing on
5355 // the terminal's own write must
5356 // still be mirrored upstream.
5357 if let Err(e) = drained {
5358 let mut st = StreamState {
5359 stream_id,
5360 key,
5361 is_control_stream: false,
5362 pending: &mut pending,
5363 deferred: &mut deferred,
5364 };
5365 propagate_stop(
5366 &e, &mut recv, &mut st, side, ctx,
5367 &report,
5368 );
5369 return Err(e);
5370 }
5371 // The destination is reset;
5372 // dropping `recv` stops the
5373 // source, which is what the
5374 // pass-through path has
5375 // always done.
5376 return Ok(());
5377 }
5378 // Only `execute_stream` can
5379 // produce these three, and it is
5380 // called from the two stream
5381 // decision sites, never here.
5382 // Handled rather than
5383 // `unreachable!()`d: a panicking
5384 // forwarding task is worse than a
5385 // redundant arm.
5386 Plan::RejectStream { .. }
5387 | Plan::OpenStreamAfter { .. }
5388 | Plan::SerializeStreamAfter { .. } => {}
5389 Plan::CloseSession { .. } => return Ok(()),
5390 }
5391 continue;
5392 }
5393 }
5394 FramerOut::Passthrough(raw) => {
5395 // A `Passthrough` on a stream the framer
5396 // is still parsing is an object too big
5397 // to buffer: its `ObjectMeta` was decoded
5398 // and discarded, so nothing outside the
5399 // framer can address it. Said once per
5400 // stream; the counter keeps the total.
5401 if !not_addressable_reported && !framer.is_bypassed() {
5402 not_addressable_reported = true;
5403 report.impairment(
5404 ImpairmentKind::ObjectNotAddressable {
5405 stream_id,
5406 total: 1,
5407 },
5408 );
5409 }
5410 // ...and on a shaped session it is not
5411 // merely unaddressable, it is unpaced.
5412 // These bytes carry no `ObjectMeta`, so
5413 // no rule claims them and the release
5414 // seam grants them without asking a
5415 // bucket — one object crosses a class's
5416 // rate whole. `ShapeStats::unshapeable`
5417 // already holds the figure; what it
5418 // cannot say is whose ceiling it went
5419 // over, so the report names the class
5420 // this stream's classified units are
5421 // charged to. Once per stream, like the
5422 // report above and for the same reason.
5423 if let Some(shaper) = shape {
5424 if !unpaced_reported {
5425 unpaced_reported = true;
5426 report.impairment(
5427 ImpairmentKind::ShapeUnpacedObject {
5428 class: class_label(shaper, last_class),
5429 stream_id,
5430 bytes: raw.len() as u64,
5431 },
5432 );
5433 }
5434 }
5435 raw
5436 }
5437 FramerOut::Bypassed { reason, fixup_owed } => {
5438 report.impairment(ImpairmentKind::FramerBypass {
5439 stream_id,
5440 draft,
5441 reason,
5442 });
5443 // `FramerBypass` is the whole report, and
5444 // that is a change. A fetch stream on
5445 // drafts 18 and 19 used to bypass on
5446 // every session, so a `Fetch`-aimed class
5447 // there could never fire and was told so
5448 // once per session as
5449 // `ShapeRuleUnmatchable`. Such a stream
5450 // is framed now whenever the session
5451 // carried its FETCH, so the same report
5452 // would claim a working class is dead on
5453 // the strength of one stream that named a
5454 // request nobody made.
5455 if fixup_owed {
5456 // An elide fix-up was still owed when
5457 // parsing stopped, so every later
5458 // object on this stream would carry a
5459 // stale delta. The destination is
5460 // reset rather than fed bytes that
5461 // decode to the wrong Object IDs.
5462 //
5463 // The reset goes first and the report
5464 // second. The event names the code the
5465 // destination was reset with, so
5466 // emitting it above `send.reset` would
5467 // be describing a wire change that had
5468 // not been made yet — and this arm has
5469 // no second event to correct it with.
5470 pending.clear();
5471 deferred.clear();
5472 stop.retire();
5473 let _ = send.reset(0);
5474 report.impairment(ImpairmentKind::ElideFixupLost {
5475 stream_id,
5476 reason,
5477 code: 0,
5478 });
5479 return Ok(());
5480 }
5481 // Carries no bytes: nothing to forward.
5482 continue;
5483 }
5484 FramerOut::Error(error) => {
5485 ctx.emit(|| ProxyEvent::ParseError {
5486 session_id: ctx.session_id,
5487 side,
5488 error: error.clone(),
5489 });
5490 continue;
5491 }
5492 };
5493
5494 // On a shaped stream every byte is queued, never
5495 // written inline. `write_in_order` would do two
5496 // wrong things here: let these bytes escape the
5497 // pacer, and — because it drains honouring
5498 // release times first — block this arm body for
5499 // as long as the bucket took, with no other
5500 // branch polled.
5501 if shape.is_some() {
5502 exec::enqueue_unshown(
5503 &mut pending,
5504 &mut deferred,
5505 raw,
5506 &report,
5507 );
5508 continue;
5509 }
5510 let mut st = StreamState {
5511 stream_id,
5512 key,
5513 is_control_stream: false,
5514 pending: &mut pending,
5515 deferred: &mut deferred,
5516 };
5517 match write_in_order(&raw, &mut send, &mut st, ctx, &report).await {
5518 Ok(Flow::Continue) => {}
5519 Ok(Flow::StreamOver) => return Ok(()),
5520 Err(e) => {
5521 let mut st = StreamState {
5522 stream_id,
5523 key,
5524 is_control_stream: false,
5525 pending: &mut pending,
5526 deferred: &mut deferred,
5527 };
5528 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
5529 return Err(e);
5530 }
5531 }
5532 }
5533 }
5534 None => {
5535 let mut st = StreamState {
5536 stream_id,
5537 key,
5538 is_control_stream: false,
5539 pending: &mut pending,
5540 deferred: &mut deferred,
5541 };
5542 // Anything the hook deferred goes out at its release
5543 // time, as a race against cancellation.
5544 if drain_pending(&mut send, &mut st, Site::Object, shaped_stream.as_ref(), ctx, &report).await?
5545 == Flow::StreamOver
5546 {
5547 return Ok(());
5548 }
5549 // Anything still buffered belongs to a truncated
5550 // final object. Forward it, or the peer's clean
5551 // FIN silently loses bytes.
5552 if let Some(framer) = framer.as_mut() {
5553 if let Some(tail) = framer.finish() {
5554 if let Err(e) = send.write_all(&tail).await {
5555 let e = ProxyError::from(e);
5556 let mut st = StreamState {
5557 stream_id,
5558 key,
5559 is_control_stream: false,
5560 pending: &mut pending,
5561 deferred: &mut deferred,
5562 };
5563 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
5564 return Err(e);
5565 }
5566 }
5567 }
5568 let mut st = StreamState {
5569 stream_id,
5570 key,
5571 is_control_stream: false,
5572 pending: &mut pending,
5573 deferred: &mut deferred,
5574 };
5575 match run_stream_end(StreamEnd::Fin, &mut st, side, ctx, &report) {
5576 // `ResetStream` at the data stream's end: the
5577 // clean FIN becomes a reset carrying the code.
5578 Plan::Terminal => {
5579 let _ = drain_pending(
5580 &mut send,
5581 &mut st,
5582 Site::Object,
5583 shaped_stream.as_ref(),
5584 ctx,
5585 &report,
5586 )
5587 .await?;
5588 return Ok(());
5589 }
5590 Plan::CloseSession { .. } => return Ok(()),
5591 _ => {}
5592 }
5593 ctx.emit(|| ProxyEvent::StreamClosed {
5594 session_id: ctx.session_id,
5595 side,
5596 });
5597 let _ = send.finish();
5598 return Ok(());
5599 }
5600 }
5601 }
5602 () = egress::wait_release(head_release.clone(), &ctx.cancel),
5603 if head_release.is_some() =>
5604 {
5605 // The deferred-release half of stop propagation, and
5606 // structurally the same gap:
5607 // this write can fail with the destination peer's
5608 // `STOP_SENDING` exactly like the seven inline write sites,
5609 // and on a stream whose hook defers it is the *only* write
5610 // there is. A bare `?` returns without mirroring, `recv` is
5611 // dropped, and quinn's `RecvStream::drop` stops the source
5612 // with a hard-coded 0.
5613 //
5614 // The stream-level `StopWatcher` branch does not cover
5615 // this. Once `select!` has picked this branch its arm body
5616 // runs to completion with no branch polling at all, so a
5617 // `STOP_SENDING` that lands while `release_due_units` is
5618 // inside `write_all` surfaces here and nowhere else.
5619 let released = release_due_units(
5620 &mut pending,
5621 &mut deferred,
5622 &mut send,
5623 Site::Object,
5624 shaped_stream.as_ref(),
5625 ctx,
5626 &report,
5627 )
5628 .await;
5629 match released {
5630 Ok(Flow::StreamOver) => return Ok(()),
5631 Ok(Flow::Continue) => {}
5632 Err(e) => {
5633 let mut st = StreamState {
5634 stream_id,
5635 key,
5636 is_control_stream: false,
5637 pending: &mut pending,
5638 deferred: &mut deferred,
5639 };
5640 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
5641 return Err(e);
5642 }
5643 }
5644 }
5645 command = requests.recv(), if serving_requests => {
5646 match data_stream_request(command) {
5647 StreamRequest::Reset(code) => {
5648 // The same shape the shaping reset and the
5649 // `ElideFixupLost` teardown take: everything queued
5650 // is discarded by design rather than lost, because
5651 // the destination is being abandoned.
5652 pending.clear();
5653 deferred.clear();
5654 stop.retire();
5655 let _ = send.reset(code);
5656 let _ = recv.stop(code);
5657 return Ok(());
5658 }
5659 StreamRequest::Ignore => {}
5660 StreamRequest::Closed => serving_requests = false,
5661 }
5662 }
5663 outcome = stop.watch(), if watching => {
5664 // The idle case on the framed path: a hook that holds or
5665 // delays leaves long stretches with no write at all, and
5666 // without this branch the source is not stopped until the
5667 // next one.
5668 if let Some(e) = stop_error(outcome) {
5669 let mut st = StreamState {
5670 stream_id,
5671 key,
5672 is_control_stream: false,
5673 pending: &mut pending,
5674 deferred: &mut deferred,
5675 };
5676 propagate_stop(&e, &mut recv, &mut st, side, ctx, &report);
5677 return Err(e);
5678 }
5679 }
5680 _ = ctx.cancel.cancelled() => {
5681 // Session teardown. Delivered late beats lost silently:
5682 // everything queued goes out ignoring release times, then
5683 // whatever the framer holds, so a mid-object cancel does
5684 // not drop bytes the peer already sent. Then fall through
5685 // to the same FIN-on-drop the pass-through path takes.
5686 let _ = pending.drain_ignoring_release_times(&mut send).await;
5687 // `unconfirmed_bytes`, not `queued_bytes`. The drain above
5688 // hands its units to quinn, which buffers them and returns
5689 // `Ok`; `run_with_transport` then closes the connection and
5690 // they never reach the peer. Reporting the residue reports
5691 // zero and the object is silently gone — see
5692 // `PendingQueue::unconfirmed_bytes`.
5693 let stranded = pending.unconfirmed_bytes();
5694 if stranded > 0 {
5695 report.impairment(ImpairmentKind::QueuedBytesAtTeardown {
5696 stream_id,
5697 bytes: stranded,
5698 });
5699 }
5700 if let Some(framer) = framer.as_mut() {
5701 if let Some(tail) = framer.finish() {
5702 let _ = send.write_all(&tail).await;
5703 }
5704 }
5705 return Ok(());
5706 }
5707 }
5708 }
5709}
5710
5711/// The [`ActionKind`] a returned [`Action`] will be reported as.
5712///
5713/// Needed only on the datagram path, where the transport can reject an
5714/// action the engine admitted and `ActionFailed` has to name it.
5715///
5716/// Exhaustive on purpose, with no catch-all: `Action` is
5717/// `#[non_exhaustive]` only for other crates, so a variant added here
5718/// stops this file compiling rather than being silently reported as
5719/// `Pass`.
5720fn action_kind(action: &Action) -> ActionKind {
5721 match action {
5722 Action::Pass => ActionKind::Pass,
5723 Action::Replace(_) => ActionKind::Replace,
5724 Action::ReplacePayload(_) => ActionKind::ReplacePayload,
5725 Action::Drop(_) => ActionKind::DropElide,
5726 Action::Delay { .. } => ActionKind::Delay,
5727 Action::Hold { .. } => ActionKind::Hold,
5728 Action::Truncate { .. } => ActionKind::Truncate,
5729 Action::ResetStream { .. } => ActionKind::ResetStream,
5730 Action::CloseSession { .. } => ActionKind::CloseSession,
5731 }
5732}
5733
5734/// Whether a `send_datagram` failure ends the session.
5735///
5736/// Only connection-level failures do. A datagram the transport refused —
5737/// a payload above the path MTU is the obvious one — is reported and
5738/// forgotten: the session survives, on every interest, hooked or not.
5739fn is_connection_level(err: &TransportError) -> bool {
5740 matches!(err, TransportError::ConnectionLost | TransportError::Connection(_))
5741}
5742
5743/// Whether a decoded datagram header carries an Object Status.
5744///
5745/// A status datagram has no payload slot at all, so `ReplacePayload` has
5746/// nothing to splice after and is refused there. There is no uniform
5747/// codec accessor for this yet — `AnyDatagramHeader`'s per-draft types
5748/// disagree on both the field's name and its shape — so the match is here,
5749/// one arm per enabled draft feature, in the same shape
5750/// `dispatch.rs`'s own accessors generate. Drafts 07-13 each answer for
5751/// themselves, because where a status can be stated moves twice across
5752/// them: draft-07 hangs it off a declared payload length of zero, draft-08
5753/// accepts that and adds a dedicated status message, and draft-09 drops
5754/// the zero-length form and keeps only the message.
5755#[allow(unused_variables)]
5756fn datagram_is_status(header: &AnyDatagramHeader) -> bool {
5757 match header {
5758 #[cfg(feature = "draft07")]
5759 AnyDatagramHeader::Draft07(h) => h.is_status(),
5760 #[cfg(feature = "draft08")]
5761 AnyDatagramHeader::Draft08(h) => h.is_status(),
5762 #[cfg(feature = "draft09")]
5763 AnyDatagramHeader::Draft09(h) => h.is_status(),
5764 #[cfg(feature = "draft10")]
5765 AnyDatagramHeader::Draft10(h) => h.is_status(),
5766 #[cfg(feature = "draft11")]
5767 AnyDatagramHeader::Draft11(h) => h.is_status(),
5768 #[cfg(feature = "draft12")]
5769 AnyDatagramHeader::Draft12(h) => h.is_status(),
5770 #[cfg(feature = "draft13")]
5771 AnyDatagramHeader::Draft13(h) => h.is_status(),
5772 #[cfg(feature = "draft14")]
5773 AnyDatagramHeader::Draft14(h) => h.status.is_some(),
5774 #[cfg(feature = "draft15")]
5775 AnyDatagramHeader::Draft15(h) => h.object_status.is_some(),
5776 #[cfg(feature = "draft16")]
5777 AnyDatagramHeader::Draft16(h) => h.object_status.is_some(),
5778 #[cfg(feature = "draft17")]
5779 AnyDatagramHeader::Draft17(h) => h.object_status.is_some(),
5780 #[cfg(feature = "draft18")]
5781 AnyDatagramHeader::Draft18(h) => h.object_status.is_some(),
5782 #[cfg(feature = "draft19")]
5783 AnyDatagramHeader::Draft19(h) => h.object_status.is_some(),
5784 #[allow(unreachable_patterns)]
5785 _ => false,
5786 }
5787}
5788
5789/// Forward datagrams from source to destination.
5790///
5791/// Datagrams have no queue: they are per-connection and unordered by
5792/// definition, so a FIFO would impose ordering the protocol does not have.
5793/// `Delay` and `Hold` are refused at this site.
5794///
5795/// # Datagrams are policed, not paced
5796///
5797/// A datagram is admitted or discarded on arrival, against its class's
5798/// bucket, and never queued. That is not a reduced form of what the stream
5799/// path does — it is the only sound form for this carrier. A queue would
5800/// impose a delivery order the protocol does not have, and there is nothing
5801/// a delay could protect: a datagram carries one Object whole, has no
5802/// successor whose framing is written against it and no stream whose object
5803/// IDs would need renumbering behind a hole. So the two things that make a
5804/// stream unit's discard expensive are both absent, and the arriving unit is
5805/// the right one to drop.
5806///
5807/// The decision is taken **before** the hook, exactly as stream admission
5808/// is, and for the same reason: showing a hook a unit the engine has already
5809/// decided to discard would let it return `Replace` and report an
5810/// `ActionApplied` for a wire change that never happened.
5811///
5812/// [`Class::Default`] and [`Class::Unshapeable`] name no bucket, so an
5813/// unclaimed datagram and one whose header did not decode are both admitted
5814/// unconditionally — which is what makes a configured class's figures mean
5815/// something rather than absorbing everything the session sent.
5816///
5817/// Cost to an unshaped session: one `Option::as_ref` per datagram, and no
5818/// header decode it was not already doing — `tests/interest_none.rs`
5819/// compares a whole `Counters` and a byte pump, and this must not move
5820/// either.
5821async fn forward_datagrams(
5822 source: &Transport,
5823 dest: &Transport,
5824 side: ProxySide,
5825 ctx: &ForwardCtx,
5826) -> Result<(), ProxyError> {
5827 let report = ctx.reporter(side, None);
5828
5829 // The same ordering edge the framed data pipe takes, and here for the
5830 // same reason: a datagram header decodes under one draft's codec, and on
5831 // the `moq-00` cohort the draft is named on the control stream by a task
5832 // this one was spawned alongside. Taken before the report below as well
5833 // as before the loop, because that report names the draft it judged the
5834 // profile against and a report naming the guess would send an author
5835 // looking at the wrong column.
5836 let draft = ctx.resolved_draft().await;
5837 let caps = Capabilities::for_draft(draft);
5838
5839 // Shaper-visible datagrams on this direction, which is the only scope a
5840 // datagram has: it belongs to no stream, so `Matcher::every_nth` counts
5841 // per forwarding task and the session's two directions count apart.
5842 let mut shaped_units: u64 = 0;
5843 // Edge-triggering for `note_tokens_exhausted`, which counts episodes
5844 // rather than units — the per-direction analogue of the per-stream latch
5845 // the queue keeps. Without it a class configured below the arrival rate
5846 // reports one episode per datagram, which is a throughput figure wearing
5847 // an episode's name.
5848 let mut tokens_dry = false;
5849 // `ProxyEvent::ShapedDatagram` is once per direction per outcome, for the
5850 // reason the event says: a per-datagram event would drown an observer at
5851 // line rate, and the running totals are in `ShapeStats`.
5852 let mut policed_reported = false;
5853
5854 loop {
5855 tokio::select! {
5856 result = source.recv_datagram() => {
5857 let data = result?;
5858 let arrived_at = Instant::now();
5859
5860 // Decode only when someone will read it: an observer, or a
5861 // hook that asked for datagrams.
5862 let mut header: Option<AnyDatagramHeader> = None;
5863 let mut header_len: Option<usize> = None;
5864 let mut is_status = false;
5865 // `shaping_enabled` joins the two readers here because a
5866 // class keyed on a track alias, a Location or a priority
5867 // needs the header to have been read. A profile with no such
5868 // class still pays for it, which is the same bargain the
5869 // framed path takes: `objects_enabled` frames every stream
5870 // for a profile that might key on nothing.
5871 if ctx.observer_enabled || ctx.datagram_hook || ctx.shaping_enabled {
5872 let mut cursor = &data[..];
5873 if let Ok(decoded) = AnyDatagramHeader::decode(draft, &mut cursor) {
5874 ctx.counters.note_datagram_header_decoded();
5875 header_len = Some(data.len() - cursor.len());
5876 is_status = datagram_is_status(&decoded);
5877 if ctx.observer_enabled {
5878 ctx.observer.on_event(&ProxyEvent::Datagram {
5879 session_id: ctx.session_id,
5880 side,
5881 header: decoded.clone(),
5882 payload_len: cursor.len(),
5883 });
5884 }
5885 header = Some(decoded);
5886 }
5887 }
5888
5889
5890 // ── POLICING ────────────────────────────────────────
5891 //
5892 // Gated on `ctx.shape`, which is `Some` exactly when a
5893 // profile was configured, with no `observer_enabled ||`
5894 // term — attaching an observer must not arm shaping.
5895 let unit_len = data.len() as u64;
5896 let mut policed_class = None;
5897 if let Some(shaper) = ctx.shape.as_ref().map(|s| s.current()) {
5898 let class = match header.as_ref() {
5899 Some(decoded) => {
5900 // `note_object_seen` before anything decides,
5901 // exactly as the framed path takes it: it counts
5902 // what the shaper saw, which must not depend on
5903 // what a rule or a bucket then did with it.
5904 ctx.shape_stats.note_object_seen(side, unit_len);
5905 let meta = decoded.meta();
5906 let unit_index = shaped_units;
5907 shaped_units += 1;
5908 shaper.classify_datagram(
5909 side,
5910 draft,
5911 &meta,
5912 unit_index,
5913 |class, field| {
5914 report.impairment(ImpairmentKind::ShapeRuleUnmatchable {
5915 class: shaper.class_name(class),
5916 field,
5917 draft,
5918 });
5919 },
5920 )
5921 }
5922 // A datagram whose header did not decode has no
5923 // identity for a rule to name, so no rule can claim
5924 // it and no bucket charges it — the same answer, and
5925 // the same row, an object too large for the framer to
5926 // buffer gets.
5927 None => {
5928 ctx.shape_stats.note_unshapeable_seen(side, unit_len);
5929 Class::Unshapeable
5930 }
5931 };
5932
5933 match shaper.acquire(class, unit_len, arrived_at) {
5934 Acquire::Now => {
5935 tokens_dry = false;
5936 policed_class = Some(class);
5937 }
5938 refusal => {
5939 // Four refusals, two causes, and the crate keeps
5940 // them apart everywhere else: a bucket that had
5941 // nothing is not a class held back by a rival.
5942 if matches!(refusal, Acquire::Starved(_)) {
5943 ctx.shape_stats.note_starved(class);
5944 } else if !tokens_dry {
5945 tokens_dry = true;
5946 ctx.shape_stats.note_tokens_exhausted(class);
5947 }
5948 ctx.shape_stats.note_dropped(class, unit_len);
5949 if !policed_reported {
5950 policed_reported = true;
5951 let label = class_label(&shaper, class);
5952 ctx.emit(|| ProxyEvent::ShapedDatagram {
5953 session_id: ctx.session_id,
5954 side,
5955 class: label,
5956 outcome: ShapeOutcome::Policed,
5957 });
5958 }
5959 continue;
5960 }
5961 }
5962 }
5963 if !ctx.datagram_hook {
5964 // The un-hooked branch, which is what an
5965 // `Interest::NONE` session takes. A rejected datagram
5966 // is reported and forgotten rather than ending the
5967 // session: `ActionFailed` cannot be used, because
5968 // nobody took an action on it.
5969 if let Some(class) = policed_class {
5970 ctx.shape_stats.note_delivered(side, class, unit_len);
5971 }
5972 if let Err(e) = dest.send_datagram(data) {
5973 if is_connection_level(&e) {
5974 return Err(ProxyError::from(e));
5975 }
5976 report.impairment(ImpairmentKind::DatagramNotSent {
5977 error: e.to_string(),
5978 });
5979 }
5980 continue;
5981 }
5982
5983 // The hook fires even when the header did not decode: an
5984 // undecodable datagram is exactly the case a scenario wants
5985 // to see, and `header: None` is what tells it apart.
5986 let cx = FrameCtx::new(
5987 ctx.session_id,
5988 side,
5989 draft,
5990 None,
5991 arrived_at,
5992 &caps,
5993 );
5994 let action = ctx.hook.on_datagram(&cx, header.as_ref(), &data);
5995 let kind = action_kind(&action);
5996 let unit = exec::Unit {
5997 target: exec::Target::Datagram {
5998 raw: data.clone(),
5999 header_len,
6000 is_status,
6001 },
6002 draft,
6003 arrived_at,
6004 };
6005 let mut engine = exec::Engine { queue: None, closer: &ctx.closer };
6006 let out = exec::execute(&unit, action, &mut engine, &report);
6007 let admitted = out.is_applied();
6008
6009 match out.plan {
6010 Plan::WriteNow(bytes) => {
6011 // Charged where the shaper hands the unit onward,
6012 // which is where the queue charges a stream unit —
6013 // before the write, so a transport that refuses the
6014 // datagram is one impairment rather than also a hole
6015 // in the conservation identity. A datagram the *hook*
6016 // dropped is never charged, exactly as an object the
6017 // hook dropped never reaches the queue.
6018 if let Some(class) = policed_class {
6019 ctx.shape_stats.note_delivered(side, class, bytes.len() as u64);
6020 }
6021 if let Err(e) = dest.send_datagram(bytes) {
6022 if is_connection_level(&e) {
6023 return Err(ProxyError::from(e));
6024 }
6025 if admitted {
6026 // The action was admitted and the transport
6027 // rejected it. Neither applied nor refused
6028 // would be true.
6029 report.failed(Site::Datagram, kind, e.to_string());
6030 } else {
6031 report.impairment(ImpairmentKind::DatagramNotSent {
6032 error: e.to_string(),
6033 });
6034 }
6035 }
6036 }
6037 Plan::Nothing => {}
6038 Plan::CloseSession { .. } => return Ok(()),
6039 // Stream-shaped plans; `execute` at the datagram site
6040 // cannot produce one, and a panic here would be worse
6041 // than a redundant arm.
6042 Plan::Terminal
6043 | Plan::RejectStream { .. }
6044 | Plan::OpenStreamAfter { .. }
6045 | Plan::SerializeStreamAfter { .. } => {}
6046 }
6047 }
6048 _ = ctx.cancel.cancelled() => {
6049 return Ok(());
6050 }
6051 }
6052 }
6053}
6054
6055/// Determine the encoded length of a QUIC varint from its first byte.
6056fn varint_len(first_byte: u8) -> usize {
6057 1 << (first_byte >> 6)
6058}
6059
6060/// The most bytes a control stream is buffered for while its first message
6061/// is peeked at.
6062///
6063/// Not a protocol limit. It bounds how long a session that opened a control
6064/// stream and wrote something unreadable on it keeps the tasks waiting for
6065/// its draft: past this, the session keeps the draft it started with and
6066/// says so.
6067const DETECT_BUF_MAX: usize = 64 * 1024;
6068
6069/// What [`peek_draft`] made of a control stream's opening bytes.
6070///
6071/// Three answers rather than an `Option`, because "not yet" and "not ever"
6072/// have opposite consequences: one says keep buffering and keep the tasks
6073/// waiting, the other says stop both. Collapsing them is what made a stream
6074/// that opens with anything but a SETUP buffer 64 KiB before giving up, and
6075/// a stream that never sends that much never gave up at all.
6076enum DraftPeek {
6077 /// The first message names this draft.
6078 Named(DraftVersion),
6079 /// Too few bytes so far. Buffer more and ask again.
6080 NeedMore,
6081 /// The first message is not a SETUP this peek can read, and no number
6082 /// of further bytes will change that: the type varint is already whole
6083 /// and it is not one of the four this function knows.
6084 NotSetup,
6085}
6086
6087/// Which [`DraftSource`] a SETUP peeked at on `side` carries.
6088///
6089/// A CLIENT_SETUP lists what the client will accept; a SERVER_SETUP names
6090/// the one the server picked out of that list. The second is the session's
6091/// actual version, so it outranks the first — see [`DraftSource`].
6092fn setup_rank(side: ProxySide) -> DraftSource {
6093 match side {
6094 ProxySide::ClientToProxy | ProxySide::ProxyToRelay => DraftSource::Offered,
6095 ProxySide::RelayToProxy | ProxySide::ProxyToClient => DraftSource::Selected,
6096 }
6097}
6098
6099/// Try to name the concrete draft by peeking at the first SETUP message on a
6100/// control stream.
6101///
6102/// - On the `ClientToProxy` direction, looks at CLIENT_SETUP's
6103/// `supported_versions` list and returns the highest draft in the 07–14
6104/// range we support.
6105/// - On the `RelayToProxy` direction, looks at SERVER_SETUP's
6106/// `selected_version` and returns the matching draft.
6107/// - For draft-15+ the SETUP carries no version, but those cases don't
6108/// reach this function because the caller only invokes it when the
6109/// draft isn't already fixed by ALPN.
6110fn peek_draft(buf: &[u8], side: ProxySide) -> DraftPeek {
6111 if buf.is_empty() {
6112 return DraftPeek::NeedMore;
6113 }
6114
6115 // Decode the message type varint. The first byte's top two bits give
6116 // the varint length. For drafts 07–10 the type is 0x40/0x41, encoded
6117 // as a 2-byte varint. For drafts 11–16 it's 0x20/0x21, a 1-byte varint.
6118 //
6119 // This peek only ever resolves a draft in the moq-00 cohort (07–14), so
6120 // RFC 9000 is the right encoding throughout. Draft-15+ are settled by
6121 // ALPN before any bytes arrive, and from draft-17 both the type id
6122 // (0x2F00) and the varint encoding itself changed; such a SETUP falls out
6123 // of the match below as an unrecognized type.
6124 let type_len = varint_len(buf[0]);
6125 if buf.len() < type_len {
6126 return DraftPeek::NeedMore;
6127 }
6128 let mut cur = &buf[..type_len];
6129 let Ok(type_id) = VarInt::decode(&mut cur).map(VarInt::into_inner) else {
6130 return DraftPeek::NotSetup;
6131 };
6132
6133 // Distinguish framing by the type id:
6134 // 0x40 = CLIENT_SETUP (drafts 07–10, varint length)
6135 // 0x41 = SERVER_SETUP (drafts 07–10, varint length)
6136 // 0x20 = CLIENT_SETUP (drafts 11+, u16-BE length)
6137 // 0x21 = SERVER_SETUP (drafts 11+, u16-BE length)
6138 //
6139 // Anything else is `NotSetup` rather than `NeedMore`, and that is the
6140 // whole reason for the distinction: the type varint is decided by bytes
6141 // that have already arrived, so a stream opening with something else
6142 // will never open with a SETUP however long it is buffered.
6143 let (is_client_setup, is_server_setup, uses_u16_length) = match type_id {
6144 0x40 => (true, false, false),
6145 0x41 => (false, true, false),
6146 0x20 => (true, false, true),
6147 0x21 => (false, true, true),
6148 _ => return DraftPeek::NotSetup,
6149 };
6150
6151 // The message we peek at is the one we'd expect to see first on this
6152 // direction. Anything else is bytes this direction cannot read a version
6153 // out of — the other direction's SETUP, most likely — and no amount of
6154 // further buffering makes it readable here.
6155 match side {
6156 ProxySide::ClientToProxy | ProxySide::ProxyToRelay if !is_client_setup => {
6157 return DraftPeek::NotSetup
6158 }
6159 ProxySide::RelayToProxy | ProxySide::ProxyToClient if !is_server_setup => {
6160 return DraftPeek::NotSetup
6161 }
6162 _ => {}
6163 }
6164
6165 let (payload_start, payload_len) = if uses_u16_length {
6166 if buf.len() < type_len + 2 {
6167 return DraftPeek::NeedMore;
6168 }
6169 let len = ((buf[type_len] as usize) << 8) | (buf[type_len + 1] as usize);
6170 (type_len + 2, len)
6171 } else {
6172 if buf.len() <= type_len {
6173 return DraftPeek::NeedMore;
6174 }
6175 let vl = varint_len(buf[type_len]);
6176 if buf.len() < type_len + vl {
6177 return DraftPeek::NeedMore;
6178 }
6179 let mut cur = &buf[type_len..type_len + vl];
6180 let Ok(v) = VarInt::decode(&mut cur) else {
6181 return DraftPeek::NotSetup;
6182 };
6183 (type_len + vl, v.into_inner() as usize)
6184 };
6185
6186 if buf.len() < payload_start + payload_len {
6187 return DraftPeek::NeedMore;
6188 }
6189 let payload = &buf[payload_start..payload_start + payload_len];
6190
6191 // From here the message is whole, so every remaining failure is a
6192 // property of its contents: a version list this build has no draft for
6193 // is `NotSetup`, not `NeedMore`.
6194 if is_client_setup {
6195 // CLIENT_SETUP (draft 07–14): number_of_supported_versions (varint)
6196 // then that many version varints. Pick the highest draft we
6197 // support in the moq-00 cohort (07–14).
6198 let mut cur = payload;
6199 let Ok(count) = VarInt::decode(&mut cur).map(VarInt::into_inner) else {
6200 return DraftPeek::NotSetup;
6201 };
6202 let mut best: Option<DraftVersion> = None;
6203 for _ in 0..count {
6204 let Ok(v) = VarInt::decode(&mut cur).map(VarInt::into_inner) else {
6205 return DraftPeek::NotSetup;
6206 };
6207 if let Some(d) = version_varint_to_draft(v) {
6208 if (7..=14).contains(&d.number()) {
6209 best = Some(match best {
6210 Some(b) if b.number() >= d.number() => b,
6211 _ => d,
6212 });
6213 }
6214 }
6215 }
6216 best.map_or(DraftPeek::NotSetup, DraftPeek::Named)
6217 } else {
6218 // SERVER_SETUP (draft 07–14): selected_version (varint) then
6219 // parameters. We only need the first varint.
6220 let mut cur = payload;
6221 let Ok(v) = VarInt::decode(&mut cur).map(VarInt::into_inner) else {
6222 return DraftPeek::NotSetup;
6223 };
6224 match version_varint_to_draft(v) {
6225 Some(d) if (7..=14).contains(&d.number()) => DraftPeek::Named(d),
6226 _ => DraftPeek::NotSetup,
6227 }
6228 }
6229}
6230
6231/// Convert an on-wire MoQT version varint (`0xff000000 + draft`) to a
6232/// `DraftVersion`, or `None` if the value is malformed or unsupported.
6233fn version_varint_to_draft(v: u64) -> Option<DraftVersion> {
6234 const BASE: u64 = 0xff000000;
6235 if !(BASE..=BASE + 255).contains(&v) {
6236 return None;
6237 }
6238 DraftVersion::from_number((v - BASE) as u8)
6239}
6240
6241/// TLS certificate verifier that skips all verification (testing only).
6242#[derive(Debug)]
6243struct SkipVerification;
6244
6245impl rustls::client::danger::ServerCertVerifier for SkipVerification {
6246 fn verify_server_cert(
6247 &self,
6248 _end_entity: &rustls::pki_types::CertificateDer<'_>,
6249 _intermediates: &[rustls::pki_types::CertificateDer<'_>],
6250 _server_name: &rustls::pki_types::ServerName<'_>,
6251 _ocsp_response: &[u8],
6252 _now: rustls::pki_types::UnixTime,
6253 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
6254 Ok(rustls::client::danger::ServerCertVerified::assertion())
6255 }
6256
6257 fn verify_tls12_signature(
6258 &self,
6259 _message: &[u8],
6260 _cert: &rustls::pki_types::CertificateDer<'_>,
6261 _dcs: &rustls::DigitallySignedStruct,
6262 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
6263 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
6264 }
6265
6266 fn verify_tls13_signature(
6267 &self,
6268 _message: &[u8],
6269 _cert: &rustls::pki_types::CertificateDer<'_>,
6270 _dcs: &rustls::DigitallySignedStruct,
6271 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
6272 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
6273 }
6274
6275 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
6276 vec![
6277 rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
6278 rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
6279 rustls::SignatureScheme::ED25519,
6280 rustls::SignatureScheme::RSA_PSS_SHA256,
6281 rustls::SignatureScheme::RSA_PSS_SHA384,
6282 rustls::SignatureScheme::RSA_PSS_SHA512,
6283 ]
6284 }
6285}
6286
6287#[cfg(test)]
6288mod tests {
6289 use super::*;
6290
6291 // These fixtures build SETUP bytes with a local varint encoder rather
6292 // than through `moqtap_codec::draftNN::message`. Two reasons, and they
6293 // are the same two the acceptance suite gives: a test that encodes with
6294 // the decoder it is testing cannot see a shared misunderstanding of the
6295 // wire format, and naming a per-draft codec module here would break every
6296 // reduced-draft build of this crate.
6297
6298 /// Encode a QUIC variable-length integer.
6299 fn varint(v: u64, out: &mut Vec<u8>) {
6300 match v {
6301 0..=63 => out.push(v as u8),
6302 64..=16_383 => out.extend_from_slice(&((v as u16) | 0x4000).to_be_bytes()),
6303 16_384..=1_073_741_823 => {
6304 out.extend_from_slice(&((v as u32) | 0x8000_0000).to_be_bytes());
6305 }
6306 _ => out.extend_from_slice(&(v | 0xC000_0000_0000_0000).to_be_bytes()),
6307 }
6308 }
6309
6310 /// `[type varint][payload length varint][payload]` — drafts 07–10.
6311 fn frame_varint_length(type_id: u64, payload: &[u8]) -> Vec<u8> {
6312 let mut out = Vec::new();
6313 varint(type_id, &mut out);
6314 varint(payload.len() as u64, &mut out);
6315 out.extend_from_slice(payload);
6316 out
6317 }
6318
6319 /// `[type varint][payload length u16-BE][payload]` — drafts 11+.
6320 fn frame_u16_length(type_id: u64, payload: &[u8]) -> Vec<u8> {
6321 let mut out = Vec::new();
6322 varint(type_id, &mut out);
6323 out.extend_from_slice(&(payload.len() as u16).to_be_bytes());
6324 out.extend_from_slice(payload);
6325 out
6326 }
6327
6328 // ── Control-stream message boundaries ───────────────────────────
6329 //
6330 // `ControlFrameWalker` is the only thing on the pass-through control
6331 // pipe that knows where one message ends and the next begins, and an
6332 // injection placed anywhere else desynchronizes the peer's decoder for
6333 // the rest of the session. These tests are byte-level on purpose: the
6334 // walker's whole job is arithmetic over the framing, and driving a live
6335 // session to check it would test the transport's chunking instead.
6336
6337 /// Two messages, fed as one read, and the walker names the seam.
6338 ///
6339 /// The fixed-length framing (drafts 11 and later): type varint, then a
6340 /// sixteen-bit big-endian length.
6341 ///
6342 /// *Ablation, recorded:* have `advance` return the **last** boundary in
6343 /// the chunk rather than the first — change `if first.is_none()` to an
6344 /// unconditional assignment. The `Some(first.len())` assertion below
6345 /// goes red with the real message
6346 ///
6347 /// ```text
6348 /// assertion `left == right` failed: the seam is where the first message
6349 /// ends, so an injection goes between the two rather than after both
6350 /// left: Some(13)
6351 /// right: Some(7)
6352 /// ```
6353 ///
6354 /// which is the injection arriving one message later than it could
6355 /// have — correct on the wire, and later than the caller asked for.
6356 #[test]
6357 fn the_walker_names_the_seam_between_two_messages() {
6358 let first = frame_u16_length(0x40, &[1, 2, 3]);
6359 let second = frame_u16_length(0x41, &[9, 9]);
6360 let mut stream = first.clone();
6361 stream.extend_from_slice(&second);
6362
6363 let mut walker = ControlFrameWalker::new(DraftVersion::Draft14);
6364 assert!(walker.at_boundary(), "the start of a control stream is a boundary");
6365 assert_eq!(
6366 walker.advance(&stream),
6367 Some(first.len()),
6368 "the seam is where the first message ends, so an injection goes between the two \
6369 rather than after both"
6370 );
6371 assert!(walker.at_boundary(), "both messages are whole, so the stream ends on a boundary");
6372 assert!(!walker.is_mid_message());
6373 }
6374
6375 /// A message split across two reads has its boundary found on the read
6376 /// that completes it, and none on the read that does not.
6377 ///
6378 /// This is the case the walker exists for. The pass-through pipe writes
6379 /// whatever `recv.read` returned, so without this the byte after any
6380 /// chunk would be taken for a message boundary — and half of them are
6381 /// in the middle of a payload.
6382 #[test]
6383 fn a_message_split_across_reads_offers_no_boundary_until_it_completes() {
6384 let message = frame_u16_length(0x40, &[7; 40]);
6385 let cut = 12;
6386
6387 let mut walker = ControlFrameWalker::new(DraftVersion::Draft14);
6388 assert_eq!(walker.advance(&message[..cut]), None, "a partial message reaches no seam");
6389 assert!(!walker.at_boundary(), "an injection here would land inside the payload");
6390 assert!(walker.is_mid_message(), "and a teardown here truncates a message");
6391
6392 assert_eq!(walker.advance(&message[cut..]), Some(message.len() - cut));
6393 assert!(walker.at_boundary());
6394 assert!(!walker.is_mid_message());
6395 }
6396
6397 /// The earlier framing — a varint payload length, drafts 07 to 10 — is
6398 /// walked too, and the walker is built from the session's draft rather
6399 /// than assuming one.
6400 #[test]
6401 fn the_walker_reads_the_varint_length_framing() {
6402 let first = frame_varint_length(0x40, &[1, 2, 3, 4]);
6403 let second = frame_varint_length(0x41, &[]);
6404 let mut stream = first.clone();
6405 stream.extend_from_slice(&second);
6406
6407 let mut walker = ControlFrameWalker::new(DraftVersion::Draft09);
6408 assert_eq!(walker.advance(&stream), Some(first.len()));
6409 assert!(walker.at_boundary(), "an empty payload is a whole message in its header");
6410
6411 // The same bytes under the later framing are read as one enormous
6412 // message, which is the mis-framing `MAX_CONTROL_PAYLOAD` catches.
6413 let mut wrong = ControlFrameWalker::new(DraftVersion::Draft14);
6414 assert_eq!(wrong.advance(&stream), None);
6415 }
6416
6417 /// A length no control message has means the length field was read at
6418 /// the wrong offset, and the walker says so by offering nothing.
6419 ///
6420 /// Silence rather than a guess is the point: a walker that kept
6421 /// counting would hold every injection for the rest of the session and
6422 /// would claim at teardown that a message was half-written, neither of
6423 /// which it can actually see.
6424 #[test]
6425 fn an_impossible_length_stops_the_walker_claiming_anything() {
6426 let mut stream = Vec::new();
6427 varint(0x40, &mut stream);
6428 varint(MAX_CONTROL_PAYLOAD as u64 + 1, &mut stream);
6429 stream.extend_from_slice(&[0u8; 8]);
6430
6431 let mut walker = ControlFrameWalker::new(DraftVersion::Draft09);
6432 assert_eq!(walker.advance(&stream), None);
6433 assert!(!walker.at_boundary(), "nothing may be injected onto a stream it cannot follow");
6434 assert!(
6435 !walker.is_mid_message(),
6436 "and nothing may be reported as truncated either — it has no idea whether it was"
6437 );
6438
6439 // Latched: a later chunk that would have parsed cleanly on its own
6440 // changes nothing, because the stream position is already lost.
6441 assert_eq!(walker.advance(&frame_varint_length(0x41, &[1])), None);
6442 assert!(!walker.at_boundary());
6443 }
6444
6445 /// CLIENT_SETUP's payload: version count, versions, then no parameters.
6446 fn client_setup_payload(drafts: &[u8]) -> Vec<u8> {
6447 let mut payload = Vec::new();
6448 varint(drafts.len() as u64, &mut payload);
6449 for &n in drafts {
6450 varint(0xff00_0000 + u64::from(n), &mut payload);
6451 }
6452 varint(0, &mut payload);
6453 payload
6454 }
6455
6456 /// SERVER_SETUP's payload: the selected version, then no parameters.
6457 fn server_setup_payload(draft: u8) -> Vec<u8> {
6458 let mut payload = Vec::new();
6459 varint(0xff00_0000 + u64::from(draft), &mut payload);
6460 varint(0, &mut payload);
6461 payload
6462 }
6463
6464 /// Build a draft-07 CLIENT_SETUP on the wire (type 0x40, varint length).
6465 fn encode_client_setup_d07(drafts: &[u8]) -> Vec<u8> {
6466 frame_varint_length(0x40, &client_setup_payload(drafts))
6467 }
6468
6469 /// Build a draft-14 CLIENT_SETUP on the wire (type 0x20, u16-BE length).
6470 fn encode_client_setup_d14(drafts: &[u8]) -> Vec<u8> {
6471 frame_u16_length(0x20, &client_setup_payload(drafts))
6472 }
6473
6474 /// Build a draft-07 SERVER_SETUP on the wire (type 0x41, varint length).
6475 fn encode_server_setup_d07(draft: u8) -> Vec<u8> {
6476 frame_varint_length(0x41, &server_setup_payload(draft))
6477 }
6478
6479 /// Build a draft-14 SERVER_SETUP on the wire (type 0x21, u16-BE length).
6480 fn encode_server_setup_d14(draft: u8) -> Vec<u8> {
6481 frame_u16_length(0x21, &server_setup_payload(draft))
6482 }
6483
6484 /// The draft [`peek_draft`] named, or `None` for either non-answer.
6485 ///
6486 /// The rows below that care *which* non-answer it was say so with
6487 /// `matches!` instead; this is for the rows that only care that a draft
6488 /// was named.
6489 fn named(buf: &[u8], side: ProxySide) -> Option<DraftVersion> {
6490 match peek_draft(buf, side) {
6491 DraftPeek::Named(d) => Some(d),
6492 DraftPeek::NeedMore | DraftPeek::NotSetup => None,
6493 }
6494 }
6495
6496 #[test]
6497 fn the_local_encoder_agrees_with_the_framing_detect_reads() {
6498 // 0x40 is a two-byte varint, 0x20 a one-byte one — the whole
6499 // reason `peek_draft` branches on the type id.
6500 let d07 = encode_client_setup_d07(&[7]);
6501 assert_eq!(&d07[..2], &[0x40, 0x40], "0x40 encodes as a 2-byte varint");
6502 assert_eq!(varint_len(d07[0]), 2);
6503
6504 let d14 = encode_client_setup_d14(&[14]);
6505 assert_eq!(d14[0], 0x20, "0x20 encodes as a 1-byte varint");
6506 assert_eq!(varint_len(d14[0]), 1);
6507 // Payload length is u16-BE and covers exactly the payload.
6508 let declared = ((d14[1] as usize) << 8) | (d14[2] as usize);
6509 assert_eq!(declared, d14.len() - 3);
6510 }
6511
6512 #[test]
6513 fn detect_picks_highest_draft_from_07_10_varint_framing() {
6514 // Drafts 07 and 09 offered; expect 09.
6515 let bytes = encode_client_setup_d07(&[7, 9]);
6516 assert_eq!(named(&bytes, ProxySide::ClientToProxy), Some(DraftVersion::Draft09));
6517 }
6518
6519 #[test]
6520 fn detect_picks_highest_draft_from_11_14_u16_framing() {
6521 // Drafts 11, 13, 14 offered; expect 14.
6522 let bytes = encode_client_setup_d14(&[11, 13, 14]);
6523 assert_eq!(named(&bytes, ProxySide::ClientToProxy), Some(DraftVersion::Draft14));
6524 }
6525
6526 #[test]
6527 fn detect_from_server_setup_varint_framing() {
6528 let bytes = encode_server_setup_d07(10);
6529 assert_eq!(named(&bytes, ProxySide::RelayToProxy), Some(DraftVersion::Draft10));
6530 }
6531
6532 #[test]
6533 fn detect_from_server_setup_u16_framing() {
6534 let bytes = encode_server_setup_d14(14);
6535 assert_eq!(named(&bytes, ProxySide::RelayToProxy), Some(DraftVersion::Draft14));
6536 }
6537
6538 /// **A short buffer is asked again; a wrong one is not.**
6539 ///
6540 /// The two non-answers are separate variants because they have opposite
6541 /// consequences for everything waiting on the draft. `NeedMore` says the
6542 /// bytes to decide on have not arrived, so the pipe keeps buffering and
6543 /// the waiters keep waiting. `NotSetup` says they have arrived and they
6544 /// decided against: the type varint is whole and it is not a SETUP, so
6545 /// no further byte can change the answer and the session must stop
6546 /// waiting for one. Collapsed into a single `None`, the second case
6547 /// buffered 64 KiB before giving up — and a control stream that never
6548 /// carries that much never gave up at all.
6549 #[test]
6550 fn a_short_buffer_needs_more_and_a_wrong_first_message_never_will() {
6551 let bytes = encode_client_setup_d14(&[14]);
6552 // One byte in: the type varint is read, but the u16 length field
6553 // that follows it is not there yet.
6554 assert!(matches!(peek_draft(&bytes[..1], ProxySide::ClientToProxy), DraftPeek::NeedMore));
6555 // Whole, and the answer is a draft.
6556 assert_eq!(named(&bytes, ProxySide::ClientToProxy), Some(DraftVersion::Draft14));
6557
6558 // 0x10 is GOAWAY. The type varint is one byte and it has arrived,
6559 // so this stream will never open with a SETUP.
6560 assert!(matches!(
6561 peek_draft(&[0x10u8, 0x00, 0x00], ProxySide::ClientToProxy),
6562 DraftPeek::NotSetup
6563 ));
6564 // Even one byte of it is enough to say so.
6565 assert!(matches!(peek_draft(&[0x10u8], ProxySide::ClientToProxy), DraftPeek::NotSetup));
6566 }
6567
6568 #[test]
6569 fn detect_ignores_15_plus_versions_in_moq_00_setup() {
6570 // A malformed CLIENT_SETUP advertising only draft-15 over moq-00
6571 // (which shouldn't happen in practice). We refuse to pick 15 here
6572 // because 15+ uses ALPN, not CLIENT_SETUP — and the message is
6573 // whole, so the refusal is final rather than a request for more.
6574 let bytes = encode_client_setup_d14(&[15]);
6575 assert!(matches!(peek_draft(&bytes, ProxySide::ClientToProxy), DraftPeek::NotSetup));
6576 }
6577
6578 #[test]
6579 fn detect_setup_wrong_direction_is_final() {
6580 // CLIENT_SETUP peeked as SERVER_SETUP. The type id says which one it
6581 // is, so this is decided and not pending.
6582 let bytes = encode_client_setup_d14(&[14]);
6583 assert!(matches!(peek_draft(&bytes, ProxySide::RelayToProxy), DraftPeek::NotSetup));
6584 }
6585
6586 /// **The ranking is the policy, and the cell enforces it.**
6587 ///
6588 /// A CLIENT_SETUP lists what the client will take; a SERVER_SETUP names
6589 /// what the two agreed. So the relay's direction must be able to correct
6590 /// the client's, and the client's must not be able to undo it — which is
6591 /// the only ordering under which the two control directions racing each
6592 /// other converges on the version actually in use.
6593 #[test]
6594 fn a_selected_version_outranks_an_offered_one_whichever_lands_first() {
6595 for (first, second) in [
6596 (
6597 (DraftVersion::Draft14, DraftSource::Offered),
6598 (DraftVersion::Draft11, DraftSource::Selected),
6599 ),
6600 (
6601 (DraftVersion::Draft11, DraftSource::Selected),
6602 (DraftVersion::Draft14, DraftSource::Offered),
6603 ),
6604 ] {
6605 let cell = SessionDraft::new(DraftVersion::Draft07, false);
6606 assert!(cell.settle(first.0, first.1), "the first answer lands on an empty cell");
6607 cell.settle(second.0, second.1);
6608 assert_eq!(
6609 cell.now(),
6610 DraftVersion::Draft11,
6611 "SERVER_SETUP's selected version wins whichever direction was read first",
6612 );
6613 }
6614 }
6615
6616 /// **Giving up is a floor, not an answer.**
6617 ///
6618 /// A session that stopped waiting keeps the draft it started with, and a
6619 /// SETUP that arrives afterwards still refines every stream opened after
6620 /// it. The opposite — a fallback that settled the question — would make
6621 /// a slow client permanently misframed, which is the failure this whole
6622 /// cell exists to end.
6623 #[test]
6624 fn a_late_setup_still_outranks_a_fallback() {
6625 let cell = SessionDraft::new(DraftVersion::Draft14, false);
6626 assert_eq!(
6627 cell.now(),
6628 DraftVersion::Draft14,
6629 "the starting draft, before anything settles"
6630 );
6631 assert!(cell.settle(DraftVersion::Draft14, DraftSource::Fallback));
6632 assert!(cell.settle(DraftVersion::Draft11, DraftSource::Offered));
6633 assert_eq!(cell.now(), DraftVersion::Draft11);
6634 }
6635
6636 /// **A walker built on the wrong draft holds every injection, and the
6637 /// rebuild lets them go.**
6638 ///
6639 /// The framing changed at draft 11: earlier drafts write a control
6640 /// message's payload length as a varint, later ones as a fixed 16-bit
6641 /// field. So a walker built from a session's *configured* draft and fed
6642 /// the other cohort's bytes reads the length field at the wrong offset —
6643 /// here it reads 3073 where 12 was written — and then counts down
6644 /// through a message that ends nowhere. `at_boundary()` answers `false`
6645 /// from that point on, forever, and an injection is only ever written
6646 /// when it answers `true`. The consequence is silent: the control plane
6647 /// accepts the injection, the session reports success, and nothing is
6648 /// ever placed on that direction again.
6649 ///
6650 /// The rebuild is what ends it. Replaying the same bytes under the draft
6651 /// the client named leaves the walker where the old one stood and right
6652 /// about it, so the next injection goes out.
6653 #[test]
6654 fn a_walker_rebuilt_on_the_named_draft_finds_the_boundary_the_guess_lost() {
6655 let setup = encode_client_setup_d07(&[7]);
6656
6657 let mut guessed = ControlFrameWalker::new(DraftVersion::Draft14);
6658 let _ = guessed.advance(&setup);
6659 assert!(
6660 !guessed.at_boundary(),
6661 "a draft-14 walker reads draft-07's varint length field as sixteen bits of \
6662 something else, so it never reaches the end of the first message and every \
6663 injection waits behind it",
6664 );
6665
6666 let mut rebuilt = ControlFrameWalker::new(DraftVersion::Draft07);
6667 let _ = rebuilt.advance(&setup);
6668 assert!(
6669 rebuilt.at_boundary(),
6670 "rebuilt on the draft the client named and replayed over the same bytes, the \
6671 walker is between messages and an injection may be written",
6672 );
6673 }
6674
6675 /// **An ALPN-fixed session is born settled and cannot be peeked out of
6676 /// it.**
6677 ///
6678 /// Drafts 15 and later carry no version in their SETUP at all, so a
6679 /// peek that thought it had found one there found something else.
6680 #[test]
6681 fn an_alpn_fixed_session_ignores_every_setup() {
6682 let cell = SessionDraft::new(DraftVersion::Draft17, true);
6683 assert!(!cell.settle(DraftVersion::Draft11, DraftSource::Selected));
6684 assert_eq!(cell.now(), DraftVersion::Draft17);
6685 }
6686
6687 #[test]
6688 fn a_non_reset_read_failure_picks_a_code_the_draft_defines() {
6689 // The synthesized-code vocabulary: `0x3` for a connection-level
6690 // failure, `0x0` for
6691 // anything else, and never `0x1 CANCELLED`.
6692 let lost = ProxyError::Transport(TransportError::ConnectionLost);
6693 assert_eq!(synthesized_reset_code(&lost), 0x3);
6694 let conn = ProxyError::Transport(TransportError::Connection("gone".into()));
6695 assert_eq!(synthesized_reset_code(&conn), 0x3);
6696 let read = ProxyError::Transport(TransportError::Read("boom".into()));
6697 assert_eq!(synthesized_reset_code(&read), 0x0);
6698 assert!(!stream_reset_code_defined(DraftVersion::Draft07));
6699 assert!(stream_reset_code_defined(DraftVersion::Draft11));
6700 }
6701
6702 // ── the stop-watcher's fuse ────────────────────────────────────────
6703
6704 /// The watcher resolves once and is never polled again.
6705 ///
6706 /// The fuse is mandatory, not defensive. The watcher is hoisted
6707 /// across `select!` iterations precisely so quinn's `stopped()` is not
6708 /// rebuilt per wake, and the price of hoisting is that the *same*
6709 /// future is offered to `select!` every time round the loop. A
6710 /// completed future polled again panics with "`async fn` resumed after
6711 /// completion", inside a spawned forwarding task, where a dropped
6712 /// `JoinHandle` swallows the message and the symptom is a stream that
6713 /// silently stops forwarding.
6714 ///
6715 /// The positive half comes first and is what makes the negative half
6716 /// mean anything: "it did not panic" is green by default over a
6717 /// watcher that never resolved, so the test asserts that it *did*
6718 /// resolve — with the value it was given, and by observing
6719 /// `is_watching()` flip — before asserting that a second poll is inert.
6720 ///
6721 /// *Ablation (run, and it fails):* delete `self.watching = None;` —
6722 /// the line marked `THE FUSE` in [`StopWatcher::watch`]. The
6723 /// `is_watching()` assertion below goes red immediately, and the
6724 /// second `watch()` panics with "`async fn` resumed after completion"
6725 /// rather than staying pending.
6726 #[tokio::test]
6727 async fn the_watcher_is_not_repolled_after_it_resolves() {
6728 let mut watcher = StopWatcher::watching_over(async { Err(TransportError::Stopped(0x2a)) });
6729 assert!(watcher.is_watching(), "a freshly armed watcher must enable its branch");
6730
6731 // Positive proof that it resolved, and to what.
6732 let outcome = watcher.watch().await;
6733 assert!(
6734 matches!(outcome, Err(TransportError::Stopped(0x2a))),
6735 "the watcher must hand back the peer's code verbatim, got {outcome:?}"
6736 );
6737 assert!(
6738 !watcher.is_watching(),
6739 "a resolved watcher must retire itself, or the next select! iteration re-polls a \
6740 completed future and the forwarding task panics"
6741 );
6742
6743 // What the next `select!` iteration does: the branch is disabled by
6744 // `is_watching()`, and even if it were not, `watch()` is inert.
6745 let repoll =
6746 tokio::time::timeout(std::time::Duration::from_millis(200), watcher.watch()).await;
6747 assert!(repoll.is_err(), "a retired watcher must stay pending forever, not resolve again");
6748 }
6749
6750 /// `stop_error` is the safety argument for the control-path watcher,
6751 /// asserted rather than described.
6752 ///
6753 /// An idle control stream is MoQT's normal steady state, so the only
6754 /// outcome allowed to tear a session down is the peer's own
6755 /// `STOP_SENDING`. `Ok(())` cannot fire on a live stream and a lost
6756 /// connection is the read side's business; both must be inert here.
6757 ///
6758 /// *Ablation:* make `stop_error` return `Some` for any `Err`. The
6759 /// `Connection` row goes red — and end to end, every session whose
6760 /// destination connection ends would mirror a stop it never received.
6761 #[test]
6762 fn only_a_peer_stop_ends_a_stream() {
6763 assert!(matches!(
6764 stop_error(Err(TransportError::Stopped(7))),
6765 Some(ProxyError::Transport(TransportError::Stopped(7)))
6766 ));
6767 assert!(stop_error(Ok(())).is_none(), "a finished-and-acked stream is not a teardown");
6768 assert!(
6769 stop_error(Err(TransportError::Connection("gone".into()))).is_none(),
6770 "a lost connection is the read side's teardown, not a mirrored STOP_SENDING"
6771 );
6772 }
6773
6774 // ── The relay leg's transport configuration ────────────────────
6775
6776 /// A session pointed at an address that cannot be parsed.
6777 ///
6778 /// Every test below asserts about what happens *before* a socket
6779 /// exists, so an unparseable address is the cheapest way to prove the
6780 /// resolution ran first: a run that reaches the address at all reports
6781 /// `UpstreamConnect`, and one that was refused earlier reports its own
6782 /// refusal. Neither ever touches the network, so none of these can
6783 /// hang or flake.
6784 fn unroutable_session(config: ProxySessionConfig) -> ProxySession {
6785 ProxySession::new(
6786 SessionId(1),
6787 config,
6788 Vec::new(),
6789 Arc::new(crate::observer::NoOpProxyObserver),
6790 Arc::new(crate::hook::NoOpHook),
6791 CancellationToken::new(),
6792 )
6793 }
6794
6795 fn unroutable_config() -> ProxySessionConfig {
6796 ProxySessionConfig { upstream_addr: "not an address".to_string(), ..Default::default() }
6797 }
6798
6799 /// Counts the builds and returns a config built the default way.
6800 struct CountingInstaller(Arc<std::sync::atomic::AtomicUsize>);
6801
6802 impl TransportInstaller for CountingInstaller {
6803 fn build(
6804 &self,
6805 profile: &TransportProfile,
6806 ) -> Result<quinn::TransportConfig, crate::transport::TransportProfileError> {
6807 self.0.fetch_add(1, Ordering::Relaxed);
6808 profile.into_config()
6809 }
6810 }
6811
6812 #[tokio::test]
6813 async fn an_upstream_leg_naming_both_a_config_and_a_profile_is_refused_before_it_dials() {
6814 let mut config = unroutable_config();
6815 config.upstream_transport_config = Some(Arc::new(quinn::TransportConfig::default()));
6816 config.upstream_transport_profile = Some(TransportProfile::default());
6817
6818 let err = unroutable_session(config)
6819 .connect_upstream()
6820 .await
6821 .err()
6822 .expect("a contradiction is not a connection");
6823 assert!(
6824 matches!(err, ProxyError::TransportConfigAndProfile { leg: Leg::Upstream }),
6825 "the relay leg's contradiction has to be reported as the relay leg's: {err}"
6826 );
6827 }
6828
6829 /// The same contradiction, on a WebTransport upstream that would have
6830 /// ignored both fields.
6831 ///
6832 /// Ignoring them is exactly why this matters: a rule enforced only on
6833 /// the transport someone happened to test is a rule a caller finds out
6834 /// about by changing an unrelated setting.
6835 #[tokio::test]
6836 async fn the_refusal_does_not_depend_on_the_upstream_transport() {
6837 let mut config = unroutable_config();
6838 config.upstream_transport =
6839 UpstreamTransportType::WebTransport { url: "https://127.0.0.1:1/".to_string() };
6840 config.upstream_transport_config = Some(Arc::new(quinn::TransportConfig::default()));
6841 config.upstream_transport_profile = Some(TransportProfile::default());
6842
6843 let err = unroutable_session(config)
6844 .connect_upstream()
6845 .await
6846 .err()
6847 .expect("a contradiction is not a connection");
6848 assert!(
6849 matches!(err, ProxyError::TransportConfigAndProfile { leg: Leg::Upstream }),
6850 "{err}"
6851 );
6852 }
6853
6854 #[tokio::test]
6855 async fn an_upstream_profile_that_cannot_be_honoured_stops_the_session_connecting() {
6856 let mut config = unroutable_config();
6857 config.upstream_transport_profile =
6858 Some(TransportProfile { initial_mtu: Some(900), ..Default::default() });
6859
6860 let err = unroutable_session(config)
6861 .connect_upstream()
6862 .await
6863 .err()
6864 .expect("an unhonourable profile is not a connection");
6865 assert!(
6866 matches!(
6867 err,
6868 ProxyError::TransportProfile {
6869 leg: Leg::Upstream,
6870 source: crate::transport::TransportProfileError::MtuBelowFloor { .. },
6871 }
6872 ),
6873 "{err}"
6874 );
6875 }
6876
6877 #[tokio::test]
6878 async fn an_upstream_profile_is_built_through_the_installer_before_anything_is_dialled() {
6879 let builds = Arc::new(std::sync::atomic::AtomicUsize::new(0));
6880 let mut config = unroutable_config();
6881 config.upstream_transport_profile =
6882 Some(TransportProfile { initial_mtu: Some(1350), ..Default::default() });
6883 config.upstream_installer = Some(Arc::new(CountingInstaller(Arc::clone(&builds))));
6884
6885 let err = unroutable_session(config)
6886 .connect_upstream()
6887 .await
6888 .err()
6889 .expect("the address is deliberately unparseable");
6890 assert!(
6891 matches!(err, ProxyError::UpstreamConnect(_)),
6892 "the profile was accepted, so the session must have got as far as the address: {err}"
6893 );
6894 assert_eq!(
6895 builds.load(Ordering::Relaxed),
6896 1,
6897 "the leg builds its config through the installer, once, before the endpoint exists"
6898 );
6899 }
6900}