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