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