Skip to main content

moqtap_proxy/
session.rs

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