Skip to main content

moqtap_proxy/
control.rs

1//! The control plane — a live handle onto a running proxy.
2//!
3//! Everything else in this crate is configured before it starts: a
4//! [`ListenerConfig`](crate::listener::ListenerConfig) is consumed when the
5//! endpoint is bound, a [`ProxySessionConfig`](crate::session::ProxySessionConfig)
6//! is copied once per accepted connection, and a
7//! [`ProxyHook`](crate::hook::ProxyHook) declares its interest once at
8//! session start. That is deliberate — it is what makes a run reproducible
9//! from the values that started it — but it leaves no way to ask a proxy
10//! that is *already running* anything at all.
11//!
12//! [`ProxyControl`] is that way. It is obtained from
13//! [`TransparentProxy::control`](crate::proxy::TransparentProxy::control)
14//! **before** the accept loop is awaited, because the caller that owns the
15//! proxy is usually the caller that is about to give up its thread of
16//! control to `run()`; a handle that could only be taken afterwards could
17//! not be taken at all. Everything it reports is therefore defined for a
18//! proxy that has not bound yet: [`ProxyControl::local_addr`] answers
19//! [`ProxyError::NotBound`] until the listener exists, and
20//! [`ProxyControl::sessions`] answers an empty list.
21//!
22//! # Two pieces of shared state, both released by a guard
23//!
24//! The handle reads two things the proxy used to keep as stack locals of
25//! `run()`: the bound listener, and the set of sessions that are live right
26//! now. Both are published into this module when they come into existence
27//! and removed when they go, and in both cases the removal is a `Drop`
28//! rather than a call at each exit site.
29//!
30//! That choice is the whole correctness argument for
31//! [`ProxyControl::sessions`]. A session ends for at least five distinct
32//! reasons — a forwarding task returned, a forwarding task errored, the peer
33//! went away, a hook asked for a close, the proxy was cancelled — and a
34//! sixth that no enumeration covers, the session's whole future being
35//! dropped by whoever spawned it. A list of removal sites is only ever as
36//! complete as the reader who wrote it, and the failure it produces is
37//! silent: `sessions()` keeps naming an id nothing can act on, and every
38//! later call that takes that id fails in a way that looks like a race.
39//! `Drop` is complete by construction, so the registration is handed out as
40//! a `SessionGuard` and lives in the frame of the function that runs the
41//! session.
42//!
43//! A third piece is held here and released by nothing: the proxy-wide
44//! shaping counters behind [`ProxyControl::stats`]. They are the one report
45//! in this module that is *cumulative* rather than instantaneous, and that
46//! is exactly why they cannot be assembled from the two above. A total
47//! summed over the session list would fall every time a client disconnected,
48//! because the list is released by a `Drop` and a session that has ended
49//! leaves nothing behind — and a statistic that goes down under normal
50//! operation cannot be alerted on. So each session is handed the counters
51//! when it attaches, charges them as it forwards, and leaves them behind
52//! when it goes.
53//!
54//! # Reaching a session that is already running
55//!
56//! Registering an id is enough to *list* a session but not to *act* on one.
57//! Acting needs the session's cancellation token, its close request slot,
58//! its stream registry, its two control-stream inboxes and its egress
59//! knobs — and every one of those is built by the session before it dials
60//! the upstream relay, which is what lets the whole entry go into the
61//! registry at the top of the run function. A session spends its longest
62//! single operation connecting, sometimes for as long as its connect
63//! timeout allows and sometimes forever; one that only became reachable
64//! afterwards would be unreachable for exactly that long.
65//!
66//! What is deliberately **not** in an entry is either connection. The two
67//! `Transport`s are owned by the forwarding scope, and a table scanned by
68//! every list call has no business holding them alive past the session that
69//! owns them.
70//!
71//! The channels are created with the session rather than attached later
72//! because a task attached after the fact could never cover the sessions
73//! that were already running when it was attached — which is precisely the
74//! set a control plane exists to reach.
75//!
76//! # Three levels of reach, and why they are not one mechanism
77//!
78//! [`ProxyControl::close_session`] acts on the whole session, so it goes to
79//! the session's own command task: it is the only request that has to
80//! *wait* for something — the bounded egress drain — and a waiting request
81//! needs somewhere to wait that is not the caller's thread.
82//!
83//! [`ProxyControl::reset_stream`] and [`ProxyControl::inject_control`] act on
84//! one stream, and neither waits. They resolve in the caller: the registry
85//! entry carries the session's `StreamRegistry`, so a key that names nothing
86//! live is refused synchronously, and a key that names a live stream is reached
87//! by dropping a `StreamCommand` into that stream's own inbox. Routing them
88//! through the session task as well would have added a hop and a second place
89//! for a request to be lost, and would have made *there is no such stream* an
90//! answer that arrives asynchronously — which a synchronous method signature
91//! cannot deliver.
92//!
93//! Nothing here accepts a request and discards it. Every method either
94//! delivers to a task that will act on it or answers a [`ControlError`]
95//! saying why it could not.
96//!
97//! # Reconfiguring, and the one verb that cannot reach a live connection
98//!
99//! Three of the requests here change what the proxy *is* rather than what a
100//! particular session is doing, and they reach three different distances.
101//! The distances are not a matter of how they were written; they are what
102//! the thing being changed will admit.
103//!
104//! `ProxyControl::set_impair` reaches traffic already in flight. It acts
105//! below QUIC, on the datagrams a leg's socket is about to pass, and the
106//! next one out carries the new profile.
107//!
108//! [`ProxyControl::set_shape`] and [`ProxyControl::set_shaper_enabled`]
109//! reach a running session. The switch is read on the next release decision
110//! a queue makes, which for a stream held by a bucket that will refill is
111//! within one pacing interval and for a stream held by a bucket configured
112//! at zero is not until the `max_hold` clamp — see the method for why
113//! nothing here can do better. A new profile is taken up by a running
114//! session at its next stream, because a stream's egress queue holds the
115//! scheduler its units were admitted under and a class is an index into that
116//! scheduler's class list.
117//!
118//! [`ProxyControl::set_transport`] reaches **no connection that already
119//! exists, ever**. A QUIC connection takes its transport configuration once,
120//! at setup, and keeps it for life — quinn offers four setters on a live
121//! connection and no way to replace the configuration behind it. So a
122//! profile set on the relay leg reaches the next connection this proxy
123//! *opens*, which is the next session it accepts, and a profile set on the
124//! client leg reaches the next connection it *accepts*, which the proxy does
125//! not initiate and which may never arrive. On a proxy that never dials or
126//! accepts again — every client already connected, nothing new coming — the
127//! call is a permanent silent no-op that returns `Ok(())`, and no return
128//! value here distinguishes that from a setting that reached everything
129//! afterwards. It is written out on the method, in this module, and in the
130//! crate documentation, in those words, because a caller who reads it as
131//! "set the window" and measures the session in front of them will measure
132//! the old window and believe the new one.
133//!
134//! # Two kinds of name here are deliberately not links
135//!
136//! `SessionGuard`, `StreamRegistry` and `StreamCommand` are crate-internal,
137//! and `set_impair` and `clear_impair` exist only under the `impair` feature.
138//! This page is public and is built without that feature, so a link to
139//! either kind resolves to nothing — and an unresolved intra-doc link is an
140//! error under the documentation build, not a warning. Both are written in
141//! plain code font instead, so the prose can still name the thing it is
142//! describing without breaking that build. Turning one back into a link is
143//! what reddens it.
144
145use std::collections::HashMap;
146use std::net::SocketAddr;
147use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
148use std::sync::{Arc, Mutex};
149use std::time::Duration;
150
151use bytes::Bytes;
152use tokio::sync::mpsc;
153use tokio::task::JoinHandle;
154use tokio_util::sync::CancellationToken;
155
156use crate::action::{EgressConfig, Gate};
157use crate::egress::SessionCloser;
158use crate::error::ProxyError;
159use crate::event::SessionId;
160use crate::listener::Listener;
161use crate::shape::{ProxyRecorder, ProxyStats, ShapeError, ShapeProfile, StreamKey};
162use crate::transport::{
163    DefaultInstaller, TransportInstaller, TransportProfile, TransportProfileError,
164};
165use crate::types::Leg;
166
167/// How many commands a session's inbox holds before a sender has to wait.
168///
169/// Small on purpose. A control caller issues one request and waits for its
170/// answer, so depth beyond a handful only ever buys the ability to queue
171/// work behind a session that has stopped serving its inbox — which is a
172/// session that is on its way down, and a queued command aimed at it should
173/// be refused rather than parked.
174pub(crate) const COMMAND_QUEUE_DEPTH: usize = 16;
175
176/// Why a control-plane request could not be carried out.
177///
178/// Distinct from [`ProxyError`], which describes a proxy that could not be
179/// built or a connection that could not be made. Every variant here is a
180/// request the proxy understood and declined, and each says which of the
181/// three reasons applied: the thing addressed is not there, the thing asked
182/// for cannot be done on that leg over that transport, or the value supplied
183/// was refused by the same check that would have refused it in a
184/// configuration file.
185///
186/// Neither `Eq` nor `#[non_exhaustive]`, and both omissions are load-bearing.
187/// `Eq` is unavailable because [`TransportProfileError`] reaches an `f32`.
188/// `#[non_exhaustive]` is absent so that a caller outside this crate can
189/// match every variant with no wildcard arm — a match that stops compiling
190/// when a variant is added, which is the one place a new refusal reliably
191/// gets noticed.
192///
193/// One variant is `#[cfg]`-gated: `ControlError::Impairment` exists only
194/// under the `impair` feature, which is also the only build in which the
195/// method that returns it exists. A downstream exhaustive match therefore
196/// carries the same `#[cfg(feature = "impair")]` on that arm. Reaching for a
197/// `_` arm to avoid the attribute would satisfy the feature-on build as well
198/// and swallow every later variant with it, which is the whole of what the
199/// missing `#[non_exhaustive]` buys. It is written in plain code font rather
200/// than linked, because a link from this always-compiled page would not
201/// resolve in a build without the feature — the same convention the crate
202/// page uses for every other feature-gated name.
203#[derive(Debug, Clone, PartialEq, thiserror::Error)]
204pub enum ControlError {
205    /// No session with this id has ever run on this proxy.
206    ///
207    /// Session ids are minted per [`TransparentProxy`](crate::proxy::TransparentProxy)
208    /// and are not comparable across proxies, so an id from one proxy handed
209    /// to another lands here rather than acting on an unrelated session.
210    /// A session that ran and has since ended answers
211    /// [`ControlError::SessionEnded`] instead; the two are told apart by the
212    /// highest id this proxy has ever registered, which is enough because
213    /// ids are minted monotonically.
214    ///
215    /// The one id that is here and *did* exist: a connection accepted but
216    /// not yet running. `SessionStarted` is emitted at accept and
217    /// registration happens when the session begins to run, so an id in that
218    /// window is in no registry — see [`ProxyControl::sessions`], which says
219    /// the same thing about the census. Which of the two refusals such an id
220    /// gets is not fixed: the split is made on the highest id ever
221    /// registered, and two connections accepted together may reach their run
222    /// functions in either order, so an id awaiting registration answers
223    /// this unless a *later* one registered first, in which case it answers
224    /// [`ControlError::SessionEnded`]. Neither answer is retryable and
225    /// neither acts on anything, so the difference is one of wording rather
226    /// than of what a caller may then do.
227    #[error("no session {0:?}")]
228    NoSuchSession(SessionId),
229    /// This proxy ran a session with this id and is no longer running it,
230    /// or is running it but has stopped taking requests for it.
231    ///
232    /// The ordinary race, and not a fault: a session may end between the
233    /// call that listed it and the call that acts on it, and a control
234    /// plane that panicked there could not be driven from a timeline. Every
235    /// verb that names a [`SessionId`] can answer this.
236    ///
237    /// Separate from [`ControlError::NoSuchSession`] because the two mean
238    /// different things to a caller holding a list: a lookup miss on an id
239    /// this proxy never issued says the list came from somewhere else,
240    /// while this says the list was right and the session ended underneath
241    /// the call. Neither is retryable.
242    ///
243    /// It also carries one case that is neither: a request that could not
244    /// be *taken*, because the target already has
245    /// [`COMMAND_QUEUE_DEPTH`](self) requests outstanding and has served
246    /// none of them. That is a session or a stream that is not going to
247    /// serve them, and the frozen refusal set has no "try again later"
248    /// answer to give instead. Reaching it takes sixteen unanswered
249    /// requests aimed at one target.
250    #[error("session {0:?} has already ended")]
251    SessionEnded(SessionId),
252    /// The session is live but has no stream under this key.
253    ///
254    /// A key that names a stream which has already ended is reported the
255    /// same way as one that never existed. The two are not distinguished
256    /// anywhere in this crate: a stream's registration is removed as it
257    /// ends, so there is no record left to tell them apart, and inventing
258    /// one would mean holding every finished stream's identity for the life
259    /// of the session.
260    #[error("no stream {stream:?} on session {id:?}")]
261    NoSuchStream {
262        /// The session the stream was looked for on.
263        id: SessionId,
264        /// The key that matched nothing live.
265        stream: StreamKey,
266    },
267    /// The request is meaningful in general but cannot be carried out on
268    /// this leg, because of what that leg's transport is able to expose.
269    ///
270    /// Reported rather than ignored. A leg that quietly declined would leave
271    /// the caller holding a successful return and a proxy that behaves as
272    /// though nothing was asked — the failure this crate is least willing to
273    /// ship, because it is invisible from the outside and survives every
274    /// green run.
275    #[error("{what} is unsupported on the {leg:?} leg over {transport}")]
276    Unsupported {
277        /// What was asked for, in the words a caller would use for it.
278        what: &'static str,
279        /// The leg it was asked of.
280        leg: Leg,
281        /// That leg's transport, named as a caller would recognise it —
282        /// `"WebTransport"`, `"QUIC"`.
283        transport: &'static str,
284    },
285    /// A [`TransportProfile`] was
286    /// refused.
287    ///
288    /// The same check that runs when a profile is supplied in a
289    /// configuration, reporting the same error, so a profile that a leg
290    /// would not have started with is not one it can be moved to.
291    #[error(transparent)]
292    Profile(#[from] TransportProfileError),
293    /// A [`ShapeProfile`] was refused.
294    ///
295    /// As with [`ControlError::Profile`], this is the construction-time
296    /// check rather than a second, looser one.
297    #[error(transparent)]
298    Shape(#[from] ShapeError),
299    /// A datagram impairment profile was refused by `quinn-netem`, and this
300    /// is the reason it gave.
301    ///
302    /// Present only under the `impair` feature, which is also the only build
303    /// in which [`ProxyControl::set_impair`] exists — the variant and the
304    /// method that produces it are gated together, so there is no build
305    /// carrying a refusal nothing can return.
306    ///
307    /// # Why it is not [`ControlError::Unsupported`]
308    ///
309    /// It was, and the loss was the whole reason. `quinn_netem::ProfileError`
310    /// distinguishes thirteen ways a profile is armed and does nothing — a
311    /// reorder model on a direction with no delay, a token bucket whose burst
312    /// is below one datagram, a peer filter that matches no peer — and every
313    /// one of them arrived here as the same sentence naming the leg. The
314    /// caller was then told to re-run
315    /// `quinn_netem::ImpairProfile::validate` to find out what had actually
316    /// happened, which is a workaround for a type that could not express the
317    /// answer, written into the documentation as though it were a workflow.
318    /// `Unsupported` also says something that is not true here. That variant
319    /// means the request cannot be carried out **on this leg over this
320    /// transport**; a refused impairment profile is refused on every leg and
321    /// every transport, because it is the profile that is wrong. A caller
322    /// matching on the two now gets one arm for *this proxy cannot impair that
323    /// leg*, which is fixed by supplying a socket, and one for *this profile is
324    /// not armable*, which is fixed by editing the profile.
325    ///
326    /// A refused profile changes nothing: whatever was armed on that leg
327    /// before the call is still armed.
328    #[cfg(feature = "impair")]
329    #[error("the impairment profile for the {leg:?} leg was refused: {source}")]
330    Impairment {
331        /// The leg the profile was aimed at.
332        leg: Leg,
333        /// Why `quinn-netem` would not arm it.
334        source: quinn_netem::ProfileError,
335    },
336}
337
338/// A handle onto a running proxy.
339///
340/// Cheap to clone — one `Arc` bump — and every clone reads and writes the
341/// same state, so a handle may be moved into a task, kept beside the proxy,
342/// or both. It holds no lock across an `await` and takes no part in the
343/// forwarding path.
344///
345/// Obtained from
346/// [`TransparentProxy::control`](crate::proxy::TransparentProxy::control),
347/// which can be called at any point after the proxy is constructed —
348/// including before
349/// [`TransparentProxy::run`](crate::proxy::TransparentProxy::run) is
350/// awaited, which is the usual case, because `run()` does not return until
351/// the proxy is finished.
352///
353/// # What it reports
354///
355/// * [`ProxyControl::local_addr`] — the address the listener bound, or
356///   [`ProxyError::NotBound`] before there is one.
357/// * [`ProxyControl::sessions`] — the ids of the sessions that are live at this
358///   instant.
359/// * [`ProxyControl::stats`] — what this proxy's shaping has done across every
360///   session it has accepted, ended ones included, cleared by
361///   [`ProxyControl::reset_stats`]. The one report here that is cumulative
362///   rather than instantaneous, which is why it survives the disconnect that
363///   removes a session from the list above.
364///
365/// The first two are snapshots of a proxy that keeps moving. A session named by
366/// `sessions()` may end before the next line of the caller's code runs;
367/// that is not a defect in the snapshot but the reason every request that
368/// takes a [`SessionId`] can answer [`ControlError::SessionEnded`].
369///
370/// # What it changes, and how far each change reaches
371///
372/// * `ProxyControl::set_impair` / `ProxyControl::clear_impair` — the next
373///   datagram that leg's socket passes, in either direction. The only verb here
374///   that reaches traffic already flowing over a connection that already
375///   exists.
376/// * [`ProxyControl::set_shaper_enabled`] — the next release decision each
377///   queue makes, on every session already running.
378/// * [`ProxyControl::set_shape`] — the next stream a running session forwards,
379///   and every session accepted afterwards.
380/// * [`ProxyControl::set_transport`] — the next connection the leg makes or
381///   accepts, and **never** one that already exists.
382/// * [`ProxyControl::close_session`], [`ProxyControl::reset_stream`],
383///   [`ProxyControl::inject_control`] — one named session, or one stream of it,
384///   immediately.
385#[derive(Debug, Clone)]
386pub struct ProxyControl {
387    plane: Arc<ControlPlane>,
388}
389
390impl ProxyControl {
391    /// Wrap a proxy's control state in a handle.
392    pub(crate) fn new(plane: Arc<ControlPlane>) -> Self {
393        Self { plane }
394    }
395
396    /// The address this proxy's listener is bound to.
397    ///
398    /// [`ProxyError::NotBound`] until
399    /// [`TransparentProxy::run`](crate::proxy::TransparentProxy::run) has
400    /// bound the endpoint, and again once it has returned and the endpoint
401    /// is gone. Between those two points this is the address a client
402    /// connects to, which is the reason the method exists: a proxy
403    /// configured with port 0 chooses its port inside `run()`, and before
404    /// this there was no way to learn which port that was without binding
405    /// the socket in the caller and handing it in.
406    ///
407    /// A bound listener can still fail to report its address — the query
408    /// goes to the operating system — and that failure is reported as
409    /// [`ProxyError::Listener`], distinct from not being bound at all.
410    pub fn local_addr(&self) -> Result<SocketAddr, ProxyError> {
411        match self.plane.bound() {
412            Some(listener) => listener.local_addr(),
413            None => Err(ProxyError::NotBound),
414        }
415    }
416
417    /// The ids of the sessions that are live right now.
418    ///
419    /// An id appears here when its session starts running and disappears
420    /// when that session ends, for any reason at all — including the ones
421    /// no teardown site could be written for. It is **not** a log: a
422    /// session that has ended leaves nothing behind, so a caller that wants
423    /// a history of everything the proxy handled reads
424    /// [`ProxyEvent::SessionStarted`](crate::event::ProxyEvent::SessionStarted)
425    /// and
426    /// [`ProxyEvent::SessionEnded`](crate::event::ProxyEvent::SessionEnded)
427    /// from its observer instead.
428    ///
429    /// Sorted ascending, which makes the value comparable between two calls
430    /// without the caller sorting it first. Ids are minted monotonically per
431    /// proxy, so the order is also arrival order.
432    ///
433    /// # This list and the event stream do not have to agree
434    ///
435    /// `SessionStarted` is emitted when a connection is accepted, before the
436    /// session has dialled the upstream relay; registration here happens
437    /// when the session begins to run. A session whose upstream connect
438    /// fails is therefore reported as started, is briefly listed here, and
439    /// then disappears — and a session that has been accepted but has not
440    /// been polled yet is in the event stream and not in this list. Sessions
441    /// driven by constructing a
442    /// [`ProxySession`](crate::session::ProxySession) directly, rather than
443    /// through a proxy's accept loop, appear in neither: they belong to no
444    /// proxy and so to no control plane.
445    pub fn sessions(&self) -> Vec<SessionId> {
446        self.plane.live()
447    }
448
449    /// What this proxy's shaping has done, across every session it has
450    /// accepted — including the ones that have already ended.
451    ///
452    /// The complement of [`Self::sessions`], and the difference between them
453    /// is the whole reason this exists. That list is what is live *now* and
454    /// shrinks when a client disconnects; these figures are cumulative and
455    /// monotone, so a caller polling them across a disconnect sees them hold
456    /// rather than fall. A total summed over the live list could not have
457    /// that property: session registrations are released by a `Drop`, and a
458    /// statistic that goes down under normal operation cannot be alerted on.
459    ///
460    /// It follows that a session which ended for any of the reasons no
461    /// teardown site could be written for — its whole future dropped, its
462    /// task cancelled mid-write — has still contributed everything it moved,
463    /// because each unit was charged at the instant it was handled.
464    ///
465    /// Defined before the proxy has bound: a proxy that has accepted nothing
466    /// answers `ProxyStats::default()` rather than an error, the same shape
467    /// [`Self::sessions`] takes.
468    ///
469    /// # An all-zero answer means "no profile", not "no traffic"
470    ///
471    /// Every counter behind this is written by the shaping path, which a
472    /// session with no [`ShapeProfile`] never enters. A proxy forwarding
473    /// gigabytes with no profile configured reports
474    /// `ProxyStats::default()`, and nothing in the value distinguishes that
475    /// from a proxy nothing has connected to. Ask [`Self::sessions`], or an
476    /// observer's event stream, which of the two it is.
477    ///
478    /// # Sessions driven directly are not counted
479    ///
480    /// A [`ProxySession`](crate::session::ProxySession) constructed by a
481    /// caller instead of accepted by this proxy belongs to no control plane,
482    /// so it reports only its own
483    /// [`ShapeStats`](crate::shape::ShapeStats) — the same rule
484    /// [`Self::sessions`] follows, and for the same reason: a proxy must not
485    /// claim traffic it never accepted.
486    ///
487    /// # Cost
488    ///
489    /// A read of about forty relaxed atomics plus nine per class row, and
490    /// one `Vec` and one `String` allocated per class row. Cheap, and still
491    /// a reader's call rather than something to poll in a tight loop.
492    pub fn stats(&self) -> ProxyStats {
493        self.plane.stats.snapshot()
494    }
495
496    /// Zero every figure [`Self::stats`] reports, and start again from here.
497    ///
498    /// For a caller measuring a phase rather than a run: reset, drive the
499    /// traffic, read. Without it the only way to get a phase figure is to
500    /// subtract two snapshots, which is correct but leaves every assertion
501    /// written against a difference rather than a value.
502    ///
503    /// # What it does not do
504    ///
505    /// It does not touch any session's own
506    /// [`ShapeStats`](crate::shape::ShapeStats). Those are a session's for
507    /// its whole life, and a proxy-level verb that silently rewrote them
508    /// would make a scenario reading both see two different pasts.
509    ///
510    /// It does not clear the class rows themselves, only their counters. The
511    /// rows are sized once, from the first shaped session this proxy
512    /// accepts, because a class index means what the scheduler that produced
513    /// it says it means; dropping them here would let the next session
514    /// install a different class list and report figures under it.
515    ///
516    /// # It does not stop traffic
517    ///
518    /// The counters are cleared one relaxed store at a time while the
519    /// forwarding tasks keep charging them, so a reset that races a live
520    /// stream can land between two increments of the same unit and leave a
521    /// row part-cleared. There is no atomic form of this — no primitive
522    /// clears forty counters at once — and pausing the proxy to get one
523    /// would be a far larger promise than the figures are worth. Reset when
524    /// the traffic you are about to measure has not started.
525    pub fn reset_stats(&self) {
526        self.plane.stats.reset();
527    }
528
529    /// Set the QUIC transport parameters one leg uses — **for connections it
530    /// has not made yet**.
531    ///
532    /// # This never reaches a connection that already exists
533    ///
534    /// Read that sentence as the whole of what this method does, because
535    /// every other sentence here is a consequence of it. A QUIC connection
536    /// takes its transport configuration once, at setup, and holds it for
537    /// life; quinn exposes four setters on a connection that is already up —
538    /// the two stream-count limits and the two windows — and no way at all
539    /// to hand it a different configuration. So a profile set here reaches:
540    /// * on [`Leg::Upstream`], the next connection **this proxy opens**, which
541    ///   is the next session it accepts, because a session dials the relay
542    ///   once and then forwards over that connection until it ends;
543    ///
544    /// * on [`Leg::Client`], the next connection **this proxy accepts**, which
545    ///   the proxy does not initiate and which may never arrive.
546    ///
547    /// It follows that on a proxy which never dials or accepts again — one
548    /// whose clients have all connected, one nothing is connecting to, one
549    /// about to be cancelled — this call is a **permanent silent no-op that
550    /// returns `Ok(())`**. Nothing about the return value distinguishes that
551    /// from a setting that reached every connection made afterwards, because
552    /// there is nothing to distinguish: the parameters were installed, and
553    /// what did or did not happen next is traffic rather than configuration.
554    /// A caller that needs the change to bite on a session already running
555    /// has one instrument, and it is
556    /// [`ProxyControl::close_session`] — a leg that reconnects is a leg
557    /// that installs this.
558    ///
559    /// # Every call is absolute: `None` is a default, not "leave alone"
560    ///
561    /// The profile is built into a **fresh** `quinn::TransportConfig` — a
562    /// `TransportConfig::default()` with the profile applied over it — and
563    /// that config is installed whole. So a field left `None` is not carried
564    /// over from the profile installed before it: it takes quinn's default.
565    ///
566    /// Two consecutive calls therefore do not compose. A call setting
567    /// `stream_receive_window` followed by a call setting only
568    /// `max_idle_timeout` leaves the leg with the idle timeout **and quinn's
569    /// default window**, not with both settings. `TransportProfile::default()`
570    /// puts a leg back on quinn's defaults entirely, which is the only way to
571    /// clear a setting and is why this is worth stating: a merge would look
572    /// like a convenience and would make `None` mean two different things —
573    /// "unset" when a profile is built and "unchanged" when it is installed —
574    /// leaving no profile that could ever clear a field, and no way for a
575    /// caller restoring normal conditions to say so.
576    ///
577    /// # What it replaces
578    ///
579    /// Whatever that leg's transport parameters were, whether they came from
580    /// a [`TransportProfile`] or from a raw `quinn::TransportConfig` in the
581    /// configuration the proxy was built with. The two cannot be combined —
582    /// see
583    /// [`ProxyError::TransportConfigAndProfile`]
584    /// — so a live profile that was merged with a configured raw config
585    /// would be a merge that cannot be written; a live profile that sat
586    /// beside one would turn every subsequent connect into that refusal.
587    /// Replacing is the only answer that leaves the leg usable, and it means
588    /// a caller who set a raw config at build time loses it from the first
589    /// call to this.
590    ///
591    /// The profile is built through the leg's own
592    /// [`TransportInstaller`] when the
593    /// proxy was given one, so a leg keeps building its configuration the
594    /// way it always did.
595    ///
596    /// # Errors
597    ///
598    /// [`ControlError::Profile`] for a profile the leg would not have
599    /// started with — the same check, reporting the same error, run here
600    /// rather than at the next connect, so a refusal is attached to the call
601    /// that caused it instead of to a connection failure minutes later.
602    ///
603    /// [`ControlError::Unsupported`] on [`Leg::Upstream`] when the upstream
604    /// is WebTransport. That endpoint is built inside the WebTransport
605    /// library, which takes no `quinn::TransportConfig` and hands back no
606    /// endpoint to install one on, so the profile could only be stored and
607    /// ignored.
608    pub fn set_transport(&self, leg: Leg, p: TransportProfile) -> Result<(), ControlError> {
609        match leg {
610            Leg::Client => {
611                let config = self.plane.build_transport(leg, &p)?;
612                self.plane.set_client_transport(config);
613                Ok(())
614            }
615            Leg::Upstream if self.plane.legs.upstream_webtransport => Err(
616                // Named exactly as the field on `ProxySessionConfig` names
617                // it, so the refusal and the setting it refuses read as the
618                // same thing.
619                ControlError::Unsupported {
620                    what: "transport profile",
621                    leg,
622                    transport: "webtransport",
623                },
624            ),
625            Leg::Upstream => {
626                // Built and thrown away: the point is the refusal, which has
627                // to happen now rather than at the next dial. The profile
628                // itself is what is stored, so each session still runs the
629                // installer once for its own connection exactly as a
630                // configured profile does.
631                self.plane.build_transport(leg, &p)?;
632                *self.plane.upstream_transport.lock().expect("control plane") = Some(p);
633                Ok(())
634            }
635        }
636    }
637
638    /// Replace the shaping profile this proxy's sessions pace with.
639    ///
640    /// # What it reaches, and when
641    ///
642    /// Every session this proxy accepts afterwards shapes with `p`, whatever
643    /// the profile it was configured with.
644    ///
645    /// A session that is **already running** picks `p` up at the next stream
646    /// it forwards, not at the next object. That granularity is forced: a
647    /// stream's egress queue holds the scheduler its units were admitted
648    /// under, and a class is an index into that scheduler's class list, so a
649    /// unit classified under one profile and released under another charges
650    /// one profile's class against the other profile's buckets. Streams
651    /// already forwarding therefore run to their end under the profile they
652    /// started with, and on MoQT that is rarely a long wait — media arrives
653    /// on a fresh unidirectional stream per subgroup.
654    ///
655    /// A session that started **unshaped** stays unshaped for its whole
656    /// life, and nothing here changes that. Shaping classifies
657    /// [`ObjectMeta`](crate::framer::ObjectMeta), which only the object
658    /// framer produces, and whether a session frames at all is settled at
659    /// session start from the profile it was configured with. A session that
660    /// began as a byte pump has no objects to classify and no queue to pace.
661    ///
662    /// # A session whose class list would change keeps its own profile
663    ///
664    /// A running session takes `p` only if `p`'s class names are the same,
665    /// in the same order, as the ones the session started with. Otherwise it
666    /// keeps the profile it has until it ends, and only sessions accepted
667    /// afterwards get the new one.
668    ///
669    /// The reason is the statistics.
670    /// [`ShapeStats`](crate::shape::ShapeStats) has one row per configured
671    /// class, pre-sized when the session is constructed and never resized,
672    /// and a class is charged to its row **by position**. A profile with a
673    /// different class list would therefore charge its classes to rows still
674    /// named after the old profile's — every number in
675    /// [`ProxySession::shape_stats`](crate::session::ProxySession::shape_stats)
676    /// correct, and every label on it wrong — or, for a class beyond the end
677    /// of the original list, silently to the default row. Refusing the swap
678    /// per session is the only answer that keeps a reader's numbers
679    /// attributable. To move a running session onto a different class list,
680    /// end it with [`ProxyControl::close_session`]; its replacement starts
681    /// on the new profile.
682    ///
683    /// # Buckets start full
684    ///
685    /// A session that takes `p` builds a scheduler for it, and a fresh
686    /// scheduler's buckets are full as of that instant — the same choice a
687    /// session's first scheduler makes, so that a session does not open with
688    /// a burst-sized delay in front of its first object. Setting the same
689    /// profile repeatedly therefore hands every class a fresh burst each
690    /// time, and a caller doing that in a tight loop measures no rate limit
691    /// at all. The report-once diagnostics
692    /// (`ImpairmentKind::ShapeRuleUnmatchable`,
693    /// `ImpairmentKind::ShapeBurstBelowUnit`) start again with the new
694    /// scheduler too, so a rule that is unmatchable under both profiles is
695    /// reported once per profile rather than once per session.
696    ///
697    /// # Errors
698    ///
699    /// [`ControlError::Shape`] for a profile [`ShapeProfile::try_new`]
700    /// rejects. Since that constructor is the only way to build a
701    /// `ShapeProfile`, a profile arriving here has already passed it and the
702    /// refusal is unreachable today; the check is re-run rather than assumed
703    /// so that the rules live in exactly one place and a later construction
704    /// path cannot arrive here unchecked.
705    pub fn set_shape(&self, p: ShapeProfile) -> Result<(), ControlError> {
706        // Through the constructor, not around it: re-validating field by
707        // field here would be a second copy of the rules, and the two would
708        // drift in the direction that accepts something a session cannot
709        // run.
710        let checked = ShapeProfile::try_new(
711            p.buckets().to_vec(),
712            p.classes().to_vec(),
713            p.queue().clone(),
714            p.discipline(),
715        )?;
716        self.plane.shape.set(checked);
717        Ok(())
718    }
719
720    /// Turn pacing off, or back on, for every session this proxy is running
721    /// and every session it accepts afterwards.
722    ///
723    /// Off is not "no profile". The profile stays exactly where it was, the
724    /// classes keep claiming units, the per-stream queue depth keeps
725    /// applying and the statistics keep moving — what stops is the token
726    /// bucket and the discipline, so every unit is released as soon as the
727    /// queue reaches it. `set_shaper_enabled(true)` resumes with the same
728    /// configuration and the same counters, which is what makes this usable
729    /// as a switch in a timeline rather than as a way of throwing a profile
730    /// away.
731    ///
732    /// # When a stream that is already held resumes
733    ///
734    /// The switch is read on the next release decision a queue makes, and a
735    /// queue makes one when its head's wait expires. Switching pacing off
736    /// therefore does not reach into a wait that is already running; it
737    /// changes the answer the stream gets when that wait ends. How long that
738    /// is depends on why the stream was held:
739    /// * held by a bucket that will refill — one unit's worth of the
740    ///   configured rate, so a stream paced at a real rate resumes within one
741    ///   pacing interval;
742    ///
743    /// * held behind another class — as soon as that class drains, which is now
744    ///   immediate;
745    /// * held by a bucket configured at **zero**, or one whose burst cannot
746    ///   cover a unit — there is no refill instant, so the stream's wait is the
747    ///   queue's `max_hold` clamp and the switch is not read until it fires. On
748    ///   a stopped class, switching pacing off does **not** promptly release
749    ///   what is already queued. Nothing in this crate can: the wait is a timer
750    ///   a per-stream queue armed, and there is no session-wide wake that
751    ///   reaches one. Use [`ProxyControl::set_shape`] to move the session onto
752    ///   a profile with a rate, or end the session, if that is what is wanted.
753    ///
754    /// # It changes nothing on a proxy with no profile
755    ///
756    /// Returns nothing, and cannot fail, so it is silent about a proxy where
757    /// no session has a [`ShapeProfile`] to pace with — there is nothing to
758    /// switch and no consequence to report. That is the one case where
759    /// calling this has no observable effect whatever.
760    pub fn set_shaper_enabled(&self, on: bool) {
761        self.plane.shape.set_enabled(on);
762    }
763
764    /// Install a datagram impairment on one leg's socket.
765    ///
766    /// Effective from the next datagram that leg passes through its socket,
767    /// in both directions. It is the only verb here that reaches traffic
768    /// already in flight, because it acts below QUIC on the datagrams
769    /// themselves rather than on anything a connection settled at setup.
770    /// Datagrams already handed to the operating system are gone — the shim
771    /// sees a datagram once, on its way through, and a profile armed after that
772    /// cannot recall it. So *effective immediately* means the next datagram,
773    /// not the last one.
774    ///
775    /// The other leg is untouched. The two are separate sockets carrying
776    /// separate connections, and a profile armed on one says nothing about
777    /// the other.
778    ///
779    /// Arming replaces whatever was armed before, resets both directions'
780    /// sequence numbers and moves the tick origin to this instant, so a
781    /// recorded decision log reads from the moment of the call. It does not
782    /// clear the counters.
783    ///
784    /// # Errors
785    ///
786    /// [`ControlError::Unsupported`] when this proxy holds no impairment
787    /// handle for `leg` — which is every leg unless the socket and its
788    /// handle were handed over together before `run()`, and the refusal says
789    /// so and names the call that fixes it. This is the case that must not
790    /// be silent: a profile stored against a leg whose datagrams never cross
791    /// an impaired socket is armed, reported as applied, and applied to
792    /// nothing, and the run that follows looks clean because it *is* clean.
793    ///
794    /// [`ControlError::Impairment`] for a profile `quinn-netem` refuses,
795    /// carrying that crate's own reason unchanged — which of the thirteen
796    /// ways a profile arms and does nothing this one is. A caller therefore
797    /// learns what is wrong from the answer to the call that was wrong,
798    /// rather than by running `quinn_netem::ImpairProfile::validate` again
799    /// to find out. A refused profile changes nothing — whatever was armed
800    /// before is still armed.
801    ///
802    /// # No observer event, and that is a decision rather than an omission
803    /// Nothing is emitted here — not on arming, not on the first datagram the
804    /// profile touches. Three separate reasons, and the first alone settles it:
805    /// * **there is no session to name.** Every
806    ///   [`ProxyEvent`](crate::event::ProxyEvent) variant carries a
807    ///   [`SessionId`], and observers dispatch on it. A leg's socket carries
808    ///   every session on that leg, including the ones this proxy has not
809    ///   accepted yet — and those are precisely the sessions the profile will
810    ///   impair for their whole lives. Fanning one event out over the sessions
811    ///   that happen to be live would be a report that is silent about the
812    ///   population it most affects.
813    /// * **the caller already knows.** This method is synchronous and its
814    ///   return value is the answer. An event saying *the thing you just asked
815    ///   for was accepted* tells an observer nothing the call site did not
816    ///   have.
817    /// * **the consequence is not observable from here.** The shim acts below
818    ///   QUIC, on datagrams that have already been encrypted and coalesced, so
819    ///   nothing in this crate can say which session or which stream lost what.
820    ///   `quinn-netem`'s own counters and decision log are the report for that,
821    ///   and they are read through the handle rather than through an event
822    ///   stream.
823    ///
824    /// What an observer *does* see is the fallout: a leg impaired hard
825    /// enough produces ordinary session and stream events — resets, parse
826    /// errors, [`ProxyEvent::SessionEnded`](crate::event::ProxyEvent::SessionEnded)
827    /// — with no marker distinguishing them from the same failures arriving
828    /// from a real network. That is the honest position: the proxy cannot
829    /// tell either.
830    #[cfg(feature = "impair")]
831    pub fn set_impair(&self, leg: Leg, p: quinn_netem::ImpairProfile) -> Result<(), ControlError> {
832        let handle = self.plane.impair_handle(leg).ok_or(ControlError::Unsupported {
833            what: "a datagram impairment",
834            leg,
835            transport: ControlPlane::NO_SOCKET,
836        })?;
837        handle.arm(p).map_err(|source| ControlError::Impairment { leg, source })
838    }
839
840    /// Remove the datagram impairment from one leg's socket.
841    ///
842    /// Effective from the next datagram that leg passes, exactly as
843    /// [`ProxyControl::set_impair`] is, and idempotent: clearing a leg that
844    /// is not impaired is not an error and not a state change. The other leg
845    /// keeps whatever it has.
846    ///
847    /// The counters and the decision log are **not** cleared. A scenario
848    /// that armed an impairment, cleared it and then read what it had done
849    /// would otherwise find nothing, with no counter anywhere saying a phase
850    /// had been discarded.
851    ///
852    /// A leg this proxy holds no handle for has nothing to clear and nothing
853    /// happens. Unlike `set_impair` there is no refusal to give — the
854    /// signature returns nothing — and none is owed: the request was to have
855    /// no impairment on that leg, and that is exactly the state it is in.
856    /// A caller finding out whether the proxy can impair a leg at all asks
857    /// `set_impair`.
858    ///
859    /// # No observer event
860    ///
861    /// For the reasons `set_impair` gives, all of which apply unchanged: no
862    /// session to name, a synchronous answer the caller already has, and a
863    /// consequence — datagrams no longer being interfered with — that is
864    /// invisible from above QUIC by construction. Disarming is even less
865    /// reportable than arming, because what it produces is the *absence* of
866    /// a loss, and there is no event for a packet that was not dropped.
867    #[cfg(feature = "impair")]
868    pub fn clear_impair(&self, leg: Leg) {
869        if let Some(handle) = self.plane.impair_handle(leg) {
870            handle.disarm();
871        }
872    }
873
874    /// End one session, giving its egress queues a bounded window to flush
875    /// first, and close both of its legs with `code` and `reason`.
876    ///
877    /// Returns as soon as the request has been taken. The drain and the
878    /// close happen in the session, which is the only place that can see
879    /// them through — a method that waited would have to hold the caller
880    /// for as long as the drain took, and the drain is bounded precisely so
881    /// that nobody has to.
882    ///
883    /// # The window, and what happens at the end of it
884    ///
885    /// The window is
886    /// [`EgressConfig::drain_timeout`](crate::action::EgressConfig::drain_timeout)
887    /// from the session's configuration, 100 ms by default. Within it, the
888    /// session keeps running exactly as it was: units come off the
889    /// per-stream queues at their release times, a shaped class keeps
890    /// paying its bucket, and a hook that deferred something still gets it
891    /// written. The window ends early — and this is the common case — the
892    /// moment the session has nothing queued anywhere.
893    ///
894    /// When it ends because the timeout expired, whatever is still queued
895    /// is **abandoned and reported**, once per stream, as
896    /// [`ImpairmentKind::QueuedBytesAtTeardown`](crate::event::ImpairmentKind::QueuedBytesAtTeardown).
897    /// It is not flushed first. A flush at that point would hand the bytes
898    /// to a connection that is about to send `CONNECTION_CLOSE`, which
899    /// discards whatever it had buffered — so the bytes would be neither
900    /// confirmably delivered nor confirmably lost, and no count of them
901    /// would add up. Abandoning them keeps the arithmetic exact: what the
902    /// peer received plus what the impairments name is what was queued when
903    /// this was called.
904    ///
905    /// Either way the session then closes with `code` and `reason`. That
906    /// part is unconditional for the call that was accepted — a drain that
907    /// ran out of time changes what reached the peer, never what the close
908    /// says. A call that is *refused* starts nothing at all; see **Errors**,
909    /// which covers the one refusal a caller can produce deliberately.
910    ///
911    /// # Control streams are not repaired
912    ///
913    /// Nothing is synthesized on a control stream to tidy up the close. If
914    /// a control message was half-written when the window closed, the peer
915    /// gets a truncated message and the session reports
916    /// [`ImpairmentKind::ControlStreamTruncated`](crate::event::ImpairmentKind::ControlStreamTruncated).
917    /// Completing the message would mean the proxy inventing control-stream
918    /// bytes that neither peer wrote.
919    ///
920    /// # Errors
921    ///
922    /// [`ControlError::SessionEnded`] when the session has already ended or
923    /// is already ending — the ordinary race, since a session may end
924    /// between listing it and closing it — and
925    /// [`ControlError::NoSuchSession`] for an id this proxy never ran.
926    ///
927    /// "Already ending" covers one case that is not a race at all, and a
928    /// caller can produce it deliberately: **a second call while the first
929    /// call's drain window is still open**. The first close fixed the code
930    /// and the reason and nothing revises them, so a second call's pair
931    /// would reach neither peer. It is refused here rather than accepted,
932    /// because the only alternative is to take a request and drop it — the
933    /// session's command task is inside the first drain and cancels when it
934    /// comes out, so it never returns for a second. A caller that wants a
935    /// different code has to ask before the first close, not after it.
936    ///
937    /// One thing can survive a refusal, and only in the direction that
938    /// helps: a call that got as far as fixing the pair and then found the
939    /// session unreachable still closes it with what was asked for. So a
940    /// caller that sees this error was either late for everything — the two
941    /// refusals above — or late only for the drain.
942    ///
943    /// # What an observer sees
944    /// This is the one verb here that produces events, and it produces them
945    /// through the session rather than from this call — after the drain, which
946    /// is what makes them a record of what happened rather than of what was
947    /// asked for:
948    ///
949    /// * one
950    ///   [`ProxyEvent::SessionEnded`](crate::event::ProxyEvent::SessionEnded)
951    ///   whose `reason` begins **`*control plane closed the session*`** and
952    ///   quotes `code` and `reason`. That wording is the point: a hook's
953    ///   `Action::CloseSession` reaches the same latch and reports `*hook
954    ///   closed the session*`, and for a while both said the latter, so an
955    ///   operator ending a session was recorded as the scenario under test
956    ///   ending it.
957    /// * one
958    ///   [`ImpairmentKind::QueuedBytesAtTeardown`](crate::event::ImpairmentKind::QueuedBytesAtTeardown)
959    ///   per stream the window ran out on, and none at all for the ordinary
960    ///   case where everything drained.
961    /// * at most one
962    ///   [`ImpairmentKind::ControlStreamTruncated`](crate::event::ImpairmentKind::ControlStreamTruncated)
963    ///   per control direction that was mid-message when the window closed.
964    ///
965    /// Nothing is emitted at the moment the request is *accepted*. The
966    /// return value is that answer, and an event that duplicated it would be
967    /// the only event in this enum an observer could receive for something
968    /// that had not happened yet.
969    pub fn close_session(
970        &self,
971        id: SessionId,
972        code: u32,
973        reason: &[u8],
974    ) -> Result<(), ControlError> {
975        let handle = self.plane.reach(id)?;
976        // Recorded here rather than in the session task so that the pair is
977        // fixed the instant the request is accepted. A session that is torn
978        // down by its peer half a millisecond later still closes with the
979        // code that was asked for, which is what makes this method's
980        // promise independent of how long the session survives it.
981        //
982        // A *losing* record is a session that some other close already
983        // owns — a second call inside the first one's drain window, or a
984        // hook's `Action::CloseSession` that landed between the lookup above
985        // and this line. The first writer keeps the pair, so this call's
986        // code and reason are going nowhere, and the request is refused
987        // instead of handed over. Handing it over would be this crate's
988        // cardinal failure in miniature: a close accepted, reported as
989        // applied, and reaching neither peer.
990        if !handle.closer.record(code, Bytes::copy_from_slice(reason)) {
991            return Err(ControlError::SessionEnded(id));
992        }
993        handle.request(id, SessionCommand::Close { drain: handle.egress.drain_timeout })
994    }
995
996    /// Reset one live forwarded stream, immediately.
997    ///
998    /// `stream` names a stream this session is forwarding; the key is the
999    /// one carried on the events and hook contexts for that stream. `code`
1000    /// becomes the `RESET_STREAM` application error code on the
1001    /// destination, and the source is stopped with the same code, so both
1002    /// peers learn the stream was abandoned rather than finished.
1003    ///
1004    /// # Bytes already handed to the transport are gone
1005    ///
1006    /// A reset abandons the destination stream. Whatever the proxy had
1007    /// written but the transport had not yet acknowledged goes with it —
1008    /// QUIC does not retransmit data on a stream that has been reset — and
1009    /// so does everything this stream still had queued. That is what a
1010    /// reset *is*, and it is why the method exists, but it means the peer's
1011    /// view of this stream ends at an arbitrary byte and no count of what
1012    /// it received is predictable from what was sent.
1013    ///
1014    /// # Errors
1015    ///
1016    /// [`ControlError::NoSuchStream`] when no stream with that key is live
1017    /// — which is the same answer for a key that never existed and for one
1018    /// whose stream has already ended, because a stream's registration is
1019    /// removed as it ends and nothing is left to tell them apart. Plus the
1020    /// two session-level refusals.
1021    ///
1022    /// # One case where `Ok(())` is delivery rather than a reset
1023    ///
1024    /// The request is handed to the task that owns the stream's write half
1025    /// and is acted on the next time that task comes round its own loop,
1026    /// which is immediately in every case but one. A *control* direction on
1027    /// the forward-first pipe stops reading its request channel once it is
1028    /// holding [`COMMAND_QUEUE_DEPTH`](self) injections it has not been able
1029    /// to place — the backpressure that makes a further
1030    /// [`ProxyControl::inject_control`] answer
1031    /// [`ControlError::SessionEnded`] rather than queue without limit — and a
1032    /// reset handed over in that state waits until an injection can be
1033    /// written, which that method's own documentation says can be never.
1034    /// Below that depth, and on every data stream, the reset is served at
1035    /// once.
1036    ///
1037    /// # No observer event, and neither existing one may be borrowed
1038    ///
1039    /// The stream's task performs the reset and emits nothing.
1040    /// [`ProxyEvent::StreamReset`](crate::event::ProxyEvent::StreamReset)
1041    /// means a teardown this proxy *observed* a peer perform, and
1042    /// [`ProxyEvent::ActionApplied`](crate::event::ProxyEvent::ActionApplied)
1043    /// means a hook asked for one at a named site. Reusing either would make
1044    /// it ambiguous for every reader that already relies on it — an observer
1045    /// counting peer resets would start counting the operator's, with
1046    /// nothing in the payload to separate them — and this crate does not
1047    /// widen the meaning of a shipped event to save adding one.
1048    ///
1049    /// A variant of its own was considered and declined: it would carry
1050    /// nothing the caller does not already hold. The session, the stream and
1051    /// the code came from this call, and the answer to "did it happen" is
1052    /// the return value. What is worth observing is the *consequence*, and
1053    /// that is observed where consequences are — at the peer, as a
1054    /// `RESET_STREAM` carrying `code`, which is what this crate's own gate
1055    /// for the method checks.
1056    ///
1057    /// Anything the stream still had queued goes with it and is **not**
1058    /// reported as
1059    /// [`ImpairmentKind::QueuedBytesAtTeardown`](crate::event::ImpairmentKind::QueuedBytesAtTeardown).
1060    /// That report is about a *teardown* losing bytes it was trying to
1061    /// deliver; here the caller asked for the destination to be abandoned,
1062    /// and the bytes going with it is what the word means rather than a
1063    /// reduction in what the proxy could do.
1064    pub fn reset_stream(
1065        &self,
1066        id: SessionId,
1067        stream: StreamKey,
1068        code: u64,
1069    ) -> Result<(), ControlError> {
1070        let handle = self.plane.reach(id)?;
1071        let inbox =
1072            handle.streams.inbox_for(stream).ok_or(ControlError::NoSuchStream { id, stream })?;
1073        match inbox.try_send(StreamCommand::Reset { code }) {
1074            Ok(()) => Ok(()),
1075            // The task ended between the lookup and the send, so its
1076            // receiver is gone. Indistinguishable, to a caller, from a key
1077            // that was already retired — and reported the same way.
1078            Err(mpsc::error::TrySendError::Closed(_)) => {
1079                Err(ControlError::NoSuchStream { id, stream })
1080            }
1081            Err(mpsc::error::TrySendError::Full(_)) => Err(ControlError::SessionEnded(id)),
1082        }
1083    }
1084
1085    /// Put a control message on one leg of a live session, in sequence.
1086    ///
1087    /// `leg` names whose decoder the message is for:
1088    /// [`Leg::Client`] writes toward the client, [`Leg::Upstream`] toward
1089    /// the relay. The bytes are written onto the **session's existing
1090    /// control stream**, interleaved with the messages already flowing on
1091    /// it, at a point the receiving decoder accepts.
1092    ///
1093    /// # What "in sequence" costs, and why it is not a fresh stream
1094    ///
1095    /// A new stream would be far simpler and would look like working code:
1096    /// the write succeeds, the bytes arrive. The peer reads them as
1097    /// whatever a new stream of that kind is — a data stream header on a
1098    /// fresh unidirectional stream, a request on a fresh bidirectional one
1099    /// — and the message is never seen as a control message at all. On the
1100    /// drafts that carry the control plane on unidirectional streams it
1101    /// would be worse than useless: a second stream announcing itself as a
1102    /// control stream is a second SETUP, which a peer is entitled to close
1103    /// the session over. So the write goes to the task that owns the
1104    /// control stream's write half, and lands between two forwarded
1105    /// messages rather than inside one — a control stream is a single
1106    /// framed byte sequence, and bytes spliced into the middle of a
1107    /// message's payload desynchronize the peer's decoder for the rest of
1108    /// the session.
1109    ///
1110    /// Because of that, a message injected while one is mid-flight is held
1111    /// until the message in flight has been written. That wait is bounded
1112    /// by the peer finishing the write it started, not by anything this
1113    /// proxy does.
1114    ///
1115    /// # A held message can wait forever, and this returns `Ok(())` anyway
1116    /// The call returns as soon as the request has been taken, before any write
1117    /// is attempted — it has to, or a caller would be held for as long as the
1118    /// peer took. So `Ok(())` means *accepted for writing*, and on the
1119    /// forward-first control pipe — the one a session takes unless a hook
1120    /// declared `Interest::CONTROL` — there are two ways for the write never to
1121    /// happen:
1122    /// * **the direction goes quiet mid-message.** That pipe forwards read
1123    ///   chunks and finds the boundaries in the bytes it is forwarding, so a
1124    ///   direction whose peer stopped writing part-way through a message never
1125    ///   reaches one. The injection waits for a peer that may never write
1126    ///   again.
1127    /// * **the proxy has lost the framing.** The boundary walk is driven by the
1128    ///   session's draft, which for the moq-00 cohort (drafts 07-14) is a
1129    ///   configured guess until a SETUP is peeked. A wrong guess makes the
1130    ///   lengths wrong; rather than place a message by guesswork the walk
1131    ///   latches to *the boundaries are unknown*, and from then on nothing is
1132    ///   injected on that direction at all. That is deliberate and it is the
1133    ///   better of the two outcomes — a misplaced injection desynchronizes the
1134    ///   peer's decoder for the rest of the session, while one that never
1135    ///   arrives leaves every other message intact.
1136    ///
1137    /// A message still held when the session ends is discarded, and nothing
1138    /// reports it: no event, no impairment, no error. A caller that needs to
1139    /// know an injection landed observes it at the peer, which is what this
1140    /// crate's own gate for the method does.
1141    ///
1142    /// That silence is the weakest point on this page, and it is written
1143    /// down rather than smoothed over. An accepted request that never
1144    /// reaches the wire is exactly the failure this crate refuses
1145    /// everywhere else, and the only thing standing in for a report is the
1146    /// paragraph above. An impairment for it was considered and not added:
1147    /// the held messages live in a local deque of the control pipe, the
1148    /// pipe returns from seven places, and a report wired into some of them
1149    /// would be a promise that is kept for some teardowns and not others —
1150    /// which is worse than no promise, because a scenario would then read
1151    /// the absence of the event as delivery. The report that would be worth
1152    /// having has to be raised by something whose completeness is
1153    /// structural, the way `SessionGuard` is for the session census, and
1154    /// that is not a wiring change.
1155    ///
1156    /// A hook declaring `Interest::CONTROL` puts the stream on the pipe that
1157    /// decodes each message before forwarding it, where every return to the
1158    /// select is a boundary by construction and neither case above exists.
1159    ///
1160    /// # `bytes` must already be framed, and is not checked
1161    ///
1162    /// Pass a complete, framed control message for the session's draft —
1163    /// what `AnyControlMessage::encode` produces, which is the type varint,
1164    /// the length field, and the payload. The proxy writes it verbatim: it
1165    /// does not frame it, does not decode it, and does not know what it
1166    /// says. A payload passed without its framing is the second way to get
1167    /// this wrong that still looks like working code — the write succeeds
1168    /// and the peer's decoder reads the first bytes of the payload as a
1169    /// message type and length, and is lost from there on.
1170    /// Nothing is validated because there is nothing to answer with: the
1171    /// refusals this method can give are about *reaching* a session, and
1172    /// inventing a *those bytes were not a message* refusal would mean decoding
1173    /// every injection on the session's forwarding path.
1174    ///
1175    /// # Which stream that is, per draft
1176    ///
1177    /// Two topologies, and the session picks between them from its draft.
1178    /// Through draft 16 the control plane is one client-initiated
1179    /// bidirectional stream and `leg` names which of its two directions to
1180    /// write on. From draft 17 it is a **pair of unidirectional streams**,
1181    /// one opened by each peer, and bidirectional streams carry requests
1182    /// instead — draft-17 Section 3.3. There `leg` names which of the two
1183    /// unidirectional control streams to write on, and a request stream is
1184    /// never a candidate: the session routes an injection to the control
1185    /// leg's channel, which only a control direction's pipe ever serves.
1186    ///
1187    /// The `leg` mapping is the same on both: the leg is the peer whose
1188    /// decoder reads what is written. [`Leg::Upstream`] puts the message in
1189    /// front of the relay, [`Leg::Client`] in front of the client.
1190    ///
1191    /// # Errors
1192    ///
1193    /// The two session-level refusals. A session whose control stream has
1194    /// not been established yet is **not** an error: the message waits and
1195    /// is written as soon as there is a stream to write it on. On drafts 17
1196    /// and later that wait covers a little more ground — the control stream
1197    /// is identified from the first varint of a unidirectional stream, so a
1198    /// session whose peer has not opened one yet has no control stream in
1199    /// that direction and the message waits for it exactly as it waits for
1200    /// a message boundary.
1201    ///
1202    /// # What an observer sees
1203    ///
1204    /// Nothing for the injection itself — no event on acceptance, none on
1205    /// the write, none on the discard described above. The bytes are written
1206    /// verbatim and are never decoded here, so there is no message to put on
1207    /// a
1208    /// [`ProxyEvent::ControlMessage`](crate::event::ProxyEvent::ControlMessage)
1209    /// and no honest way to synthesize one; and the peer's own reaction, if
1210    /// it has one, arrives as ordinary forwarded traffic.
1211    pub fn inject_control(
1212        &self,
1213        id: SessionId,
1214        leg: Leg,
1215        bytes: Vec<u8>,
1216    ) -> Result<(), ControlError> {
1217        let handle = self.plane.reach(id)?;
1218        handle
1219            .control_inbox(leg)
1220            .try_send(StreamCommand::Inject { bytes: Bytes::from(bytes) })
1221            .map_err(|_| ControlError::SessionEnded(id))
1222    }
1223}
1224
1225/// The state a [`ProxyControl`] reads, owned by the proxy and shared with
1226/// every handle it hands out.
1227///
1228/// One per [`TransparentProxy`](crate::proxy::TransparentProxy), never
1229/// process-global: session ids are minted from a counter on the proxy and
1230/// start again at 1 for each one, so two proxies in a process would collide
1231/// on every id in a shared table.
1232pub(crate) struct ControlPlane {
1233    /// The client-facing leg: the listener once it exists, and the transport
1234    /// parameters a live request has installed on it.
1235    ///
1236    /// One lock over both because the two have to move together. A request
1237    /// arriving while `run()` is between binding the endpoint and publishing
1238    /// it must not be dropped: it stores the parameters and finds no
1239    /// listener, and the publish then installs them. Split across two locks
1240    /// there is a window in which a request stores its parameters after the
1241    /// publish has read them and before the listener is visible, and the
1242    /// setting is accepted, reported as applied, and reaches nothing.
1243    client: Mutex<ClientLeg>,
1244    /// The transport parameters a live request has set for the relay leg, or
1245    /// `None` while the proxy's own template is what every session dials
1246    /// with.
1247    upstream_transport: Mutex<Option<TransportProfile>>,
1248    /// The two legs' fixed facts, from the configuration the proxy was built
1249    /// with.
1250    legs: LegSetup,
1251    /// This proxy's shaping profile and its pacing switch.
1252    shape: ShapeControl,
1253    /// What every session this proxy accepts reports its shaping figures
1254    /// into, on top of its own.
1255    ///
1256    /// Held here rather than on the [`TransparentProxy`] because this is
1257    /// what a session is already handed: `attach_control` is the one call
1258    /// that reaches every accepted session and no directly-driven one, which
1259    /// is exactly the set whose traffic belongs in a proxy-wide total.
1260    ///
1261    /// Constructed with the plane and never replaced, so it outlives every
1262    /// session and a total taken from it never falls when one ends —
1263    /// unlike [`Self::sessions`], whose entries are released by a `Drop`.
1264    stats: Arc<ProxyRecorder>,
1265    /// The two legs' impairment handles, indexed by [`Self::leg_index`].
1266    ///
1267    /// `None` for a leg whose socket this proxy did not wrap, which is every
1268    /// leg unless the caller handed both halves over — see
1269    /// [`TransparentProxy::set_impaired_socket`](crate::proxy::TransparentProxy::set_impaired_socket).
1270    #[cfg(feature = "impair")]
1271    impair: Mutex<[Option<quinn_netem::ImpairHandle>; 2]>,
1272    /// Every session that is running right now.
1273    sessions: Mutex<HashMap<SessionId, SessionHandle>>,
1274    /// The highest id ever registered here.
1275    ///
1276    /// The only trace a finished session leaves, and it exists to separate
1277    /// [`ControlError::SessionEnded`] from [`ControlError::NoSuchSession`].
1278    /// Ids are minted monotonically from a counter on the proxy, so an id
1279    /// at or below this one has run; anything above it has not. A set of
1280    /// retired ids would answer the same question and would grow for the
1281    /// life of the proxy.
1282    ///
1283    /// `0` before any session registers, and no session is ever `SessionId(0)`.
1284    high_water: AtomicU64,
1285}
1286
1287// Hand-written rather than derived because `Listener` has no `Debug`, and
1288// giving it one would widen a public type's surface for a diagnostic nobody
1289// reads. Nothing here prints the sessions either: the map is behind a lock
1290// that a formatting call has no business taking, and `sessions()` is the
1291// supported way to ask.
1292impl std::fmt::Debug for ControlPlane {
1293    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1294        f.debug_struct("ControlPlane").finish_non_exhaustive()
1295    }
1296}
1297
1298impl ControlPlane {
1299    /// An empty control plane: nothing bound, no sessions.
1300    pub(crate) fn new(legs: LegSetup) -> Arc<Self> {
1301        Arc::new(Self {
1302            client: Mutex::new(ClientLeg { listener: None, transport: None }),
1303            upstream_transport: Mutex::new(None),
1304            legs,
1305            shape: ShapeControl::new(),
1306            stats: Arc::new(ProxyRecorder::new()),
1307            #[cfg(feature = "impair")]
1308            impair: Mutex::new([None, None]),
1309            sessions: Mutex::new(HashMap::new()),
1310            high_water: AtomicU64::new(0),
1311        })
1312    }
1313
1314    /// Publish `listener` as this proxy's bound endpoint, and hand back the
1315    /// guard that un-publishes it.
1316    ///
1317    /// A guard rather than a matching `clear` call because the accept loop
1318    /// leaves by three routes — cancellation, a fatal listener error
1319    /// propagated with `?`, and the whole future being dropped by whoever
1320    /// spawned it — and only one of them is a place a call could be
1321    /// written.
1322    ///
1323    /// Any transport parameters a request set before there was a listener
1324    /// are installed here, under the same lock the request took, which is
1325    /// what makes a request that arrives during the bind land on the
1326    /// endpoint rather than on nothing.
1327    pub(crate) fn publish_listener(self: &Arc<Self>, listener: Arc<Listener>) -> BoundGuard {
1328        let mut client = self.client.lock().expect("control plane");
1329        if let Some(transport) = &client.transport {
1330            listener.set_transport(Arc::clone(transport));
1331        }
1332        client.listener = Some(listener);
1333        drop(client);
1334        BoundGuard { plane: Arc::clone(self) }
1335    }
1336
1337    /// The bound listener, or `None` before there is one.
1338    fn bound(&self) -> Option<Arc<Listener>> {
1339        self.client.lock().expect("control plane").listener.clone()
1340    }
1341
1342    /// Forget the bound listener.
1343    fn unbind(&self) {
1344        self.client.lock().expect("control plane").listener = None;
1345    }
1346
1347    /// The transport parameters the client leg should bind with, or `None`
1348    /// when no request has set any and the proxy's template stands.
1349    pub(crate) fn client_transport(&self) -> Option<Arc<quinn::TransportConfig>> {
1350        self.client.lock().expect("control plane").transport.clone()
1351    }
1352
1353    /// Install `transport` on the client leg, now if there is an endpoint
1354    /// and at bind time if there is not.
1355    fn set_client_transport(&self, transport: Arc<quinn::TransportConfig>) {
1356        let mut client = self.client.lock().expect("control plane");
1357        client.transport = Some(Arc::clone(&transport));
1358        if let Some(listener) = &client.listener {
1359            listener.set_transport(transport);
1360        }
1361    }
1362
1363    /// The profile every session this proxy accepts from now on dials the
1364    /// relay with, or `None` when the proxy's template stands.
1365    pub(crate) fn upstream_transport(&self) -> Option<TransportProfile> {
1366        self.upstream_transport.lock().expect("control plane").clone()
1367    }
1368
1369    /// This proxy's shaping profile and pacing switch, for a session to
1370    /// build its scheduler from and to watch.
1371    pub(crate) fn shape(&self) -> &ShapeControl {
1372        &self.shape
1373    }
1374
1375    /// The proxy-wide shaping counters, for a session to report into.
1376    ///
1377    /// Handed out as an `Arc` clone rather than a borrow because the session
1378    /// keeps it: its recorder forwards into this for the session's whole
1379    /// life, which outlives any borrow of the plane a call could hold.
1380    pub(crate) fn stats_recorder(&self) -> Arc<ProxyRecorder> {
1381        Arc::clone(&self.stats)
1382    }
1383
1384    /// Turn `profile` into the config `leg` would install, refusing exactly
1385    /// what a configured profile on that leg would be refused for.
1386    ///
1387    /// Built through the leg's own [`TransportInstaller`] when it has one,
1388    /// so a caller who supplied an installer to start with gets the same
1389    /// step here — a live request that quietly used the default installer
1390    /// instead would produce a connection built differently from every one
1391    /// the proxy made before it, with nothing saying so.
1392    fn build_transport(
1393        &self,
1394        leg: Leg,
1395        profile: &TransportProfile,
1396    ) -> Result<Arc<quinn::TransportConfig>, ControlError> {
1397        let installer = match leg {
1398            Leg::Client => &self.legs.client_installer,
1399            Leg::Upstream => &self.legs.upstream_installer,
1400        };
1401        match installer {
1402            Some(installer) => installer.build(profile),
1403            None => DefaultInstaller.build(profile),
1404        }
1405        .map(Arc::new)
1406        .map_err(ControlError::Profile)
1407    }
1408
1409    /// Which slot of the per-leg arrays `leg` owns.
1410    #[cfg(feature = "impair")]
1411    fn leg_index(leg: Leg) -> usize {
1412        match leg {
1413            Leg::Client => 0,
1414            Leg::Upstream => 1,
1415        }
1416    }
1417
1418    /// The impairment handle for `leg`, or `None` when this proxy was never
1419    /// given one.
1420    #[cfg(feature = "impair")]
1421    fn impair_handle(&self, leg: Leg) -> Option<quinn_netem::ImpairHandle> {
1422        self.impair.lock().expect("control plane")[Self::leg_index(leg)].clone()
1423    }
1424
1425    /// Record the handle that arms `leg`'s socket.
1426    #[cfg(feature = "impair")]
1427    pub(crate) fn set_impair_handle(&self, leg: Leg, handle: quinn_netem::ImpairHandle) {
1428        self.impair.lock().expect("control plane")[Self::leg_index(leg)] = Some(handle);
1429    }
1430
1431    /// What a leg with no impairment handle is told, and how to fix it.
1432    ///
1433    /// Carried in `Unsupported::transport` because that is the only field
1434    /// left to say it in and a caller who cannot act on the refusal is worse
1435    /// off than one who was never given the verb.
1436    #[cfg(feature = "impair")]
1437    const NO_SOCKET: &'static str = "a socket this proxy did not wrap — hand that leg's socket \
1438                                     and the ImpairHandle it was wrapped with to \
1439                                     TransparentProxy::set_impaired_socket before run()";
1440}
1441
1442/// The client-facing leg's two mutable facts, under one lock.
1443struct ClientLeg {
1444    /// The listener, from the moment `run()` binds it until `run()` returns.
1445    listener: Option<Arc<Listener>>,
1446    /// The transport parameters a live request installed, kept so that a
1447    /// request that arrives before the bind is not lost.
1448    transport: Option<Arc<quinn::TransportConfig>>,
1449}
1450
1451/// What the control plane knows about the two legs from the configuration
1452/// the proxy was built with, and cannot learn any other way.
1453///
1454/// Copied out of the proxy's templates when the plane is built rather than
1455/// read back from them on demand, because a handle is reachable from tasks
1456/// that do not hold the proxy.
1457#[derive(Default)]
1458pub(crate) struct LegSetup {
1459    /// How a client-leg [`TransportProfile`] becomes the config the listener
1460    /// installs.
1461    pub(crate) client_installer: Option<Arc<dyn TransportInstaller>>,
1462    /// The same for the relay leg.
1463    pub(crate) upstream_installer: Option<Arc<dyn TransportInstaller>>,
1464    /// Whether the relay leg is a WebTransport upstream, which cannot take
1465    /// transport parameters at all.
1466    pub(crate) upstream_webtransport: bool,
1467}
1468
1469/// This proxy's shaping profile, and whether it paces.
1470///
1471/// One per proxy, watched by every session it accepts. The generation is
1472/// what lets a session notice a change without comparing profiles: a session
1473/// caches the generation its scheduler was built at, and a mismatch is the
1474/// signal to look.
1475pub(crate) struct ShapeControl {
1476    /// The profile a live request installed, or `None` while the proxy's own
1477    /// template is what every session shapes with.
1478    profile: Mutex<Option<ShapeProfile>>,
1479    /// Bumped, under the lock above, every time the profile is replaced.
1480    generation: AtomicU64,
1481    /// Whether the schedulers built from this pace at all. Behind its own
1482    /// `Arc` because every scheduler holds a clone of it.
1483    enabled: Arc<AtomicBool>,
1484}
1485
1486impl ShapeControl {
1487    /// A control with no profile of its own and pacing switched on, which is
1488    /// the state that leaves a proxy behaving exactly as its configuration
1489    /// says.
1490    fn new() -> Self {
1491        Self {
1492            profile: Mutex::new(None),
1493            generation: AtomicU64::new(0),
1494            enabled: Arc::new(AtomicBool::new(true)),
1495        }
1496    }
1497
1498    /// Replace the profile and move the generation on.
1499    fn set(&self, profile: ShapeProfile) {
1500        let mut held = self.profile.lock().expect("shape control");
1501        *held = Some(profile);
1502        self.generation.fetch_add(1, Ordering::AcqRel);
1503    }
1504
1505    /// The current generation, without cloning a profile.
1506    ///
1507    /// What a session checks per stream; the clone below is paid only when
1508    /// this has moved.
1509    pub(crate) fn generation(&self) -> u64 {
1510        self.generation.load(Ordering::Acquire)
1511    }
1512
1513    /// The generation and the profile it names, read together.
1514    ///
1515    /// Read under the lock the setter writes under, so the pair cannot
1516    /// straddle two calls to [`Self::set`] — a session that cached a
1517    /// generation from one profile and a profile from another would stop
1518    /// noticing the newer of the two.
1519    pub(crate) fn snapshot(&self) -> (u64, Option<ShapeProfile>) {
1520        let held = self.profile.lock().expect("shape control");
1521        (self.generation.load(Ordering::Acquire), held.clone())
1522    }
1523
1524    /// The switch every scheduler built from this reads.
1525    pub(crate) fn switch(&self) -> Arc<AtomicBool> {
1526        Arc::clone(&self.enabled)
1527    }
1528
1529    /// Turn pacing on or off for every session at once.
1530    fn set_enabled(&self, on: bool) {
1531        self.enabled.store(on, Ordering::Release);
1532    }
1533}
1534
1535impl ControlPlane {
1536    /// Register `id` as live, and hand back the guard that ends it.
1537    ///
1538    /// See this module's own documentation for why the removal is a `Drop`
1539    /// and not a call on each of the session's termination paths.
1540    pub(crate) fn register(self: &Arc<Self>, id: SessionId, handle: SessionHandle) -> SessionGuard {
1541        self.sessions.lock().expect("control plane").insert(id, handle);
1542        self.high_water.fetch_max(id.0, Ordering::AcqRel);
1543        SessionGuard { plane: Arc::clone(self), id }
1544    }
1545
1546    /// Retire `id`. Idempotent.
1547    fn end(&self, id: SessionId) {
1548        self.sessions.lock().expect("control plane").remove(&id);
1549    }
1550
1551    /// Every live id, ascending.
1552    fn live(&self) -> Vec<SessionId> {
1553        let mut ids: Vec<SessionId> =
1554            self.sessions.lock().expect("control plane").keys().copied().collect();
1555        ids.sort_by_key(|id| id.0);
1556        ids
1557    }
1558
1559    /// The handle for one live session, or `None` when it is not registered.
1560    ///
1561    /// The only correct way to read the map: a caller that held the lock
1562    /// itself could still be holding it while it talked to the session.
1563    fn session(&self, id: SessionId) -> Option<SessionHandle> {
1564        self.sessions.lock().expect("control plane").get(&id).cloned()
1565    }
1566
1567    /// The handle to act on `id` through, or the refusal that says why
1568    /// there is none.
1569    /// Three answers, and the third is the one worth stating: a session that is
1570    /// registered but whose cancellation token has already fired is refused as
1571    /// ended rather than acted on. It is going down and its tasks are
1572    /// unwinding, so a request accepted there would be taken and then never
1573    /// served — which is exactly the *accepts a request and discards it*
1574    /// outcome this module refuses to have.
1575    fn reach(&self, id: SessionId) -> Result<SessionHandle, ControlError> {
1576        match self.session(id) {
1577            Some(handle) if !handle.cancel.is_cancelled() => Ok(handle),
1578            Some(_) => Err(ControlError::SessionEnded(id)),
1579            // `id.0 != 0` because the proxy's counter starts at 1, so
1580            // `SessionId(0)` names nothing however many sessions have run;
1581            // without it a proxy that has never registered anything would
1582            // report that id as ended.
1583            None if id.0 != 0 && id.0 <= self.high_water.load(Ordering::Acquire) => {
1584                Err(ControlError::SessionEnded(id))
1585            }
1586            None => Err(ControlError::NoSuchSession(id)),
1587        }
1588    }
1589}
1590
1591/// The published bound listener's registration. Un-publishes on drop.
1592pub(crate) struct BoundGuard {
1593    plane: Arc<ControlPlane>,
1594}
1595
1596impl Drop for BoundGuard {
1597    fn drop(&mut self) {
1598        self.plane.unbind();
1599    }
1600}
1601
1602/// One live session's registration. Ends it on drop.
1603///
1604/// Held in the frame of the function that runs the session, so the
1605/// registration lasts exactly as long as the session does — including when
1606/// that function returns early, and including when its future is dropped
1607/// wholesale rather than run to completion.
1608pub(crate) struct SessionGuard {
1609    plane: Arc<ControlPlane>,
1610    id: SessionId,
1611}
1612
1613impl Drop for SessionGuard {
1614    fn drop(&mut self) {
1615        self.plane.end(self.id);
1616    }
1617}
1618
1619/// What the control plane keeps for one live session.
1620///
1621/// Everything here is either a handle onto shared state the session set up
1622/// before it began forwarding, or a way to reach a task it spawned. Nothing
1623/// here is a `Transport`: the two connections are owned by the forwarding
1624/// scope, and putting them in a table scanned by every list call would keep
1625/// them alive past the session that owns them.
1626///
1627/// The entry can therefore be built at the *top* of the session's run
1628/// function, before the upstream relay has even been dialled — which is
1629/// what makes a session reachable during the one operation that can take
1630/// seconds.
1631///
1632/// `Clone` because a request takes a copy out from under the registry lock
1633/// before it does anything with it; holding that lock while talking to a
1634/// session would let one unresponsive session block every other request.
1635#[derive(Debug, Clone)]
1636pub(crate) struct SessionHandle {
1637    /// The session's own token — a child of the proxy's, so cancelling it
1638    /// ends this session and leaves the proxy accepting. Read, not
1639    /// cancelled, by this module: a session that is already going down is
1640    /// refused rather than asked to do more.
1641    cancel: CancellationToken,
1642    /// Where the close code and reason are fixed, and where the session's
1643    /// own teardown reads them back out.
1644    closer: SessionCloser,
1645    /// Every stream this session is forwarding, and each one's inbox.
1646    streams: Arc<StreamRegistry>,
1647    /// The two control-stream directions' inboxes, indexed by the leg whose
1648    /// peer decodes what is written there. See [`Self::control_inbox`].
1649    control: [mpsc::Sender<StreamCommand>; 2],
1650    /// The session's egress knobs, for the drain window a close is given.
1651    egress: EgressConfig,
1652    /// Where a request for the session as a whole is delivered.
1653    commands: mpsc::Sender<SessionCommand>,
1654}
1655
1656impl SessionHandle {
1657    /// The control-stream inbox whose writes `leg`'s peer decodes.
1658    ///
1659    /// The mapping is a half-turn and is worth stating once: the pipe that
1660    /// forwards *from* the client writes *to* the relay, so a message meant
1661    /// for the relay's decoder — [`Leg::Upstream`] — goes to the
1662    /// client-to-proxy direction's inbox, and a message for the client goes
1663    /// to the relay-to-proxy direction's.
1664    fn control_inbox(&self, leg: Leg) -> &mpsc::Sender<StreamCommand> {
1665        match leg {
1666            Leg::Client => &self.control[0],
1667            Leg::Upstream => &self.control[1],
1668        }
1669    }
1670
1671    /// Hand `command` to this session's command task.
1672    fn request(&self, id: SessionId, command: SessionCommand) -> Result<(), ControlError> {
1673        // Both failures are the same answer: the request was not taken.
1674        // `Closed` is a session whose task has stopped, `Full` is one that
1675        // has sixteen outstanding and has served none — see
1676        // `ControlError::SessionEnded`, which says why there is no third.
1677        self.commands.try_send(command).map_err(|_| ControlError::SessionEnded(id))
1678    }
1679}
1680
1681/// A request delivered to one session's command task.
1682///
1683/// Crate-internal and not a mirror of the public surface: the two
1684/// stream-level verbs resolve in the caller and never appear here, and the
1685/// one that does reach a session arrives already validated, so the session
1686/// task never has to decide whether a request was well-formed.
1687pub(crate) enum SessionCommand {
1688    /// Give this session's egress queues `drain` to flush, then end it.
1689    ///
1690    /// Carries no close code, because the code and reason were recorded
1691    /// into the session's closer before this was sent and are read back
1692    /// from there at teardown. Carrying them here as well would be a second
1693    /// source for one fact, and the two could disagree if the session were
1694    /// torn down by its peer between the two steps — which is precisely the
1695    /// case the recording is done first to get right.
1696    Close {
1697        /// How long the queues get before whatever is left is abandoned.
1698        drain: Duration,
1699    },
1700}
1701
1702/// A request delivered to the task that owns one forwarded stream.
1703///
1704/// A channel rather than a shared handle because both operations here need
1705/// `&mut SendStream`, and the stream is moved by value into its forwarding
1706/// task on every path this proxy takes. See
1707/// [`StreamEntry`] for the rest of that
1708/// argument.
1709#[derive(Debug)]
1710pub(crate) enum StreamCommand {
1711    /// Abandon this stream: reset the destination with `code` and stop the
1712    /// source with the same code.
1713    Reset {
1714        /// The application error code both peers are given.
1715        code: u64,
1716    },
1717    /// Write these bytes onto this stream at the next point the receiving
1718    /// decoder accepts one.
1719    ///
1720    /// Only the two control directions ever receive this. A data stream's
1721    /// task has no notion of a message boundary — it forwards chunks — so
1722    /// it could not honour the "in sequence" half of the request, and
1723    /// nothing routes one to it.
1724    Inject {
1725        /// A complete, framed control message, written verbatim.
1726        bytes: Bytes,
1727    },
1728}
1729
1730/// The two ends of one control-stream direction's request channel.
1731///
1732/// Created in the session's run function, before the control stream exists,
1733/// so that the sending half can go into the control-plane registry at the
1734/// same moment as the rest of the session's entry. An injection that
1735/// arrives before the control stream has been established waits in this
1736/// channel rather than being refused — the session is live, and there will
1737/// be a stream.
1738pub(crate) struct ControlLeg {
1739    /// Where a request for this direction is delivered.
1740    pub(crate) inbox: mpsc::Sender<StreamCommand>,
1741    /// What the direction's pipe serves.
1742    pub(crate) requests: mpsc::Receiver<StreamCommand>,
1743}
1744
1745impl ControlLeg {
1746    /// A fresh channel for one control-stream direction.
1747    pub(crate) fn new() -> Self {
1748        let (inbox, requests) = mpsc::channel(COMMAND_QUEUE_DEPTH);
1749        Self { inbox, requests }
1750    }
1751}
1752
1753/// A session's attachment to its proxy's control plane.
1754///
1755/// Built when the proxy constructs the session, which is before the session
1756/// runs and long before it has anything worth reaching. It holds the
1757/// channel's two halves apart until then: the sending half goes into the
1758/// registry when the session registers, and the receiving half is taken out
1759/// once, by the session itself, when it has the context to serve it.
1760///
1761/// A session built directly, rather than by a proxy's accept loop, has no
1762/// attachment at all — it belongs to no proxy, so there is no plane for it
1763/// to register with.
1764pub(crate) struct ControlAttachment {
1765    plane: Arc<ControlPlane>,
1766    commands: mpsc::Sender<SessionCommand>,
1767    inbox: Mutex<Option<mpsc::Receiver<SessionCommand>>>,
1768}
1769
1770impl ControlAttachment {
1771    /// Attach a session to `plane`, minting its command channel.
1772    pub(crate) fn new(plane: Arc<ControlPlane>) -> Self {
1773        let (commands, inbox) = mpsc::channel(COMMAND_QUEUE_DEPTH);
1774        Self { plane, commands, inbox: Mutex::new(Some(inbox)) }
1775    }
1776
1777    /// Register this session as live under `id`, reachable through
1778    /// everything a request might have to touch.
1779    ///
1780    /// The returned guard is the registration; drop it and the id is gone.
1781    ///
1782    /// Every argument is state the session builds before it dials the
1783    /// upstream relay, which is why this can be called at the top of the
1784    /// run function: a session spends its longest single operation
1785    /// connecting, and one that only became reachable afterwards would be
1786    /// unreachable for exactly as long as that took.
1787    pub(crate) fn register(
1788        &self,
1789        id: SessionId,
1790        cancel: CancellationToken,
1791        closer: SessionCloser,
1792        streams: Arc<StreamRegistry>,
1793        control: [mpsc::Sender<StreamCommand>; 2],
1794        egress: EgressConfig,
1795    ) -> SessionGuard {
1796        self.plane.register(
1797            id,
1798            SessionHandle {
1799                cancel,
1800                closer,
1801                streams,
1802                control,
1803                egress,
1804                commands: self.commands.clone(),
1805            },
1806        )
1807    }
1808
1809    /// The plane this session is attached to.
1810    ///
1811    /// Handed out so that the session can build a shaper that watches the
1812    /// proxy's profile. Nothing else the session does needs the plane: the
1813    /// registration goes through [`Self::register`] and the command channel
1814    /// through [`Self::take_inbox`], both of which hand back exactly what
1815    /// they cover.
1816    pub(crate) fn plane(&self) -> Arc<ControlPlane> {
1817        Arc::clone(&self.plane)
1818    }
1819
1820    /// Take the receiving half of the command channel.
1821    ///
1822    /// Answers `Some` exactly once per session. A second caller gets `None`
1823    /// rather than a second receiver, because two tasks draining one inbox
1824    /// would each see half the requests and neither would know it.
1825    pub(crate) fn take_inbox(&self) -> Option<mpsc::Receiver<SessionCommand>> {
1826        self.inbox.lock().expect("control attachment").take()
1827    }
1828}
1829
1830/// A spawned task that is aborted when this value is dropped.
1831///
1832/// Used for a session's command task, which has no natural end of its own:
1833/// it waits on a channel and on a cancellation token, and if the session's
1834/// future is dropped without either firing — which is what happens when
1835/// whoever spawned the session aborts it — the task would otherwise outlive
1836/// the session that owns it, holding the session's context alive with it.
1837pub(crate) struct AbortOnDrop {
1838    task: JoinHandle<()>,
1839}
1840
1841impl AbortOnDrop {
1842    /// Take ownership of `task`.
1843    pub(crate) fn new(task: JoinHandle<()>) -> Self {
1844        Self { task }
1845    }
1846}
1847
1848impl Drop for AbortOnDrop {
1849    fn drop(&mut self) {
1850        self.task.abort();
1851    }
1852}
1853
1854// ── The live-stream registry ────────────────────────────────────────────
1855//
1856// A stream's entry is what a request naming it is routed through, so this
1857// lives beside `StreamCommand` rather than beside the shaping profile it was
1858// first written next to. The move is what keeps `shape` from naming anything
1859// in this module.
1860
1861/// Every forwarded stream that is still live, and the [`Gate`] each one
1862/// releases when it ends.
1863///
1864/// This is what
1865/// [`StreamAction::SerializeAfter`](crate::action::StreamAction::SerializeAfter)
1866/// waits on. It is **always constructed**, independently of whether the
1867/// session has a [`ShapeProfile`]: `SerializeAfter` is gated by
1868/// `Interest::STREAMS`, and the capability table publishes it as an
1869/// unconditional `Yes` at both stream sites, so a registry that only existed
1870/// when a profile was configured would make that published cell a lie. Empty,
1871/// it is one `HashMap` header behind an `Arc` and allocates nothing until a
1872/// stream registers.
1873///
1874/// Engine-internal despite living in a `pub` module: a scenario author names
1875/// a [`StreamKey`], never a registry.
1876///
1877/// # Why a lookup miss is not an error
1878///
1879/// [`Self::gate_for`] answers `None` for a key that never existed **and** for
1880/// one whose stream has already ended, because an entry is removed as it is
1881/// released. Both mean the same thing to a waiter — there is nothing left to
1882/// wait for — and the two are deliberately not distinguished: a serialize
1883/// that resolves immediately is correct in both cases, and the caller reports
1884/// `SerializeTargetUnknown` once so the run says which streams did it.
1885#[derive(Debug)]
1886pub(crate) struct StreamRegistry {
1887    live: Mutex<HashMap<StreamKey, StreamEntry>>,
1888}
1889
1890/// What the registry keeps for one live stream.
1891///
1892/// Two things, and they are here for two different callers. The [`Gate`] is
1893/// what a `SerializeAfter` waits on and has always been the whole of an
1894/// entry. The inbox is how a request that arrives from *outside* the
1895/// session reaches the task that owns the stream's write half.
1896///
1897/// The inbox is a channel rather than a shared handle because
1898/// `SendStream::write_all` and `SendStream::reset` both take `&mut self`
1899/// and the stream is moved by value into its forwarding task on every
1900/// path. Sharing it would mean a lock, and that lock would be held across
1901/// `write_all().await` — on the byte-pump fast path, for as long as the
1902/// destination's flow control took. Handing the task a message instead
1903/// costs one `select!` branch and keeps the write where it already is.
1904#[derive(Debug, Clone)]
1905pub(crate) struct StreamEntry {
1906    /// Released when the stream ends.
1907    gate: Gate,
1908    /// Where a request aimed at this stream is delivered.
1909    inbox: tokio::sync::mpsc::Sender<StreamCommand>,
1910}
1911
1912impl StreamRegistry {
1913    /// An empty registry.
1914    pub(crate) fn new() -> Self {
1915        Self { live: Mutex::new(HashMap::new()) }
1916    }
1917
1918    /// Register `key` as live, and hand back the guard that ends it.
1919    ///
1920    /// The guard is the whole release mechanism, and it is a guard rather
1921    /// than a call at each teardown site on purpose. The gate has to be
1922    /// released on **every** termination path without exception — FIN,
1923    /// mirrored reset, synthesized reset, `STOP_SENDING`, cancellation — and
1924    /// a missed release is not a loud failure but a `max_hold` stall on some
1925    /// *other* stream. An enumerated list is only as complete as the reader;
1926    /// `Drop` is complete by construction, and it additionally covers the
1927    /// paths no enumeration would have listed: a `?` return, a panicking
1928    /// forwarding task, and the task future being dropped wholesale by
1929    /// `JoinSet::shutdown` at session teardown.
1930    /// `inbox` is where a request naming this key is delivered; it is the
1931    /// sending half of a channel whose receiving half the forwarding task
1932    /// holds, so the entry going away and the task stopping are one event.
1933    pub(crate) fn register(
1934        self: &Arc<Self>,
1935        key: StreamKey,
1936        inbox: tokio::sync::mpsc::Sender<StreamCommand>,
1937    ) -> StreamGuard {
1938        self.live
1939            .lock()
1940            .expect("stream registry")
1941            .insert(key, StreamEntry { gate: Gate::new(), inbox });
1942        StreamGuard { registry: Arc::clone(self), key }
1943    }
1944
1945    /// The gate to wait on for `target`, or `None` when there is nothing to
1946    /// wait for — see the type's own doc for why those are one answer.
1947    pub(crate) fn gate_for(&self, target: StreamKey) -> Option<Gate> {
1948        self.live.lock().expect("stream registry").get(&target).map(|e| e.gate.clone())
1949    }
1950
1951    /// Where to deliver a request aimed at `target`, or `None` when no such
1952    /// stream is live.
1953    ///
1954    /// The same "never existed" / "already ended" conflation
1955    /// [`Self::gate_for`] makes, and for the same reason: the entry is
1956    /// removed as the stream ends, so nothing is left to tell the two
1957    /// apart. A caller reports both as "no such stream", which is what a
1958    /// request aimed at either can act on.
1959    pub(crate) fn inbox_for(
1960        &self,
1961        target: StreamKey,
1962    ) -> Option<tokio::sync::mpsc::Sender<StreamCommand>> {
1963        self.live.lock().expect("stream registry").get(&target).map(|e| e.inbox.clone())
1964    }
1965
1966    /// Retire `key`: remove it and release its gate.
1967    ///
1968    /// Idempotent, and the removal is what makes a later `gate_for` on the
1969    /// same key answer `None` rather than handing out an already-released
1970    /// gate that would look live.
1971    fn end(&self, key: StreamKey) {
1972        let entry = self.live.lock().expect("stream registry").remove(&key);
1973        if let Some(entry) = entry {
1974            entry.gate.release();
1975        }
1976    }
1977
1978    /// How many streams are live. Tests only.
1979    #[cfg(test)]
1980    fn len(&self) -> usize {
1981        self.live.lock().expect("stream registry").len()
1982    }
1983}
1984
1985/// One live stream's registration. Ends it on drop.
1986///
1987/// Held by the forwarding task for exactly as long as the stream is
1988/// forwarding; see [`StreamRegistry::register`] for why the release is a
1989/// `Drop` rather than a call on each teardown path.
1990pub(crate) struct StreamGuard {
1991    registry: Arc<StreamRegistry>,
1992    key: StreamKey,
1993}
1994
1995impl Drop for StreamGuard {
1996    fn drop(&mut self) {
1997        self.registry.end(self.key);
1998    }
1999}
2000
2001#[cfg(test)]
2002mod tests {
2003    use super::*;
2004    use crate::types::ProxySide;
2005
2006    fn handle() -> SessionHandle {
2007        let (commands, _inbox) = mpsc::channel(COMMAND_QUEUE_DEPTH);
2008        let cancel = CancellationToken::new();
2009        SessionHandle {
2010            closer: SessionCloser::new(cancel.clone()),
2011            cancel,
2012            streams: Arc::new(StreamRegistry::new()),
2013            control: [ControlLeg::new().inbox, ControlLeg::new().inbox],
2014            egress: EgressConfig::default(),
2015            commands,
2016        }
2017    }
2018
2019    /// A registration lasts exactly as long as its guard.
2020    ///
2021    /// The whole reliability claim of `sessions()` rests on this: no
2022    /// termination path in `session.rs` calls a removal, so if the guard's
2023    /// `Drop` did not remove the entry, nothing would, and the list would
2024    /// grow monotonically for the life of the proxy.
2025    #[test]
2026    fn a_dropped_guard_takes_its_id_out_of_the_list() {
2027        let plane = ControlPlane::new(LegSetup::default());
2028        assert!(plane.live().is_empty());
2029
2030        let first = plane.register(SessionId(1), handle());
2031        let second = plane.register(SessionId(2), handle());
2032        assert_eq!(plane.live(), vec![SessionId(1), SessionId(2)]);
2033
2034        drop(first);
2035        assert_eq!(
2036            plane.live(),
2037            vec![SessionId(2)],
2038            "the id that ended goes, and the one still running stays"
2039        );
2040
2041        drop(second);
2042        assert!(plane.live().is_empty());
2043    }
2044
2045    /// The list comes back ascending however the ids went in.
2046    #[test]
2047    fn the_list_is_sorted_by_id() {
2048        let plane = ControlPlane::new(LegSetup::default());
2049        let _c = plane.register(SessionId(3), handle());
2050        let _a = plane.register(SessionId(1), handle());
2051        let _b = plane.register(SessionId(2), handle());
2052
2053        assert_eq!(plane.live(), vec![SessionId(1), SessionId(2), SessionId(3)]);
2054    }
2055
2056    /// The inbox is handed out once, so two tasks cannot split one session's
2057    /// requests between them.
2058    #[test]
2059    fn the_command_inbox_can_only_be_taken_once() {
2060        let attachment = ControlAttachment::new(ControlPlane::new(LegSetup::default()));
2061        assert!(attachment.take_inbox().is_some());
2062        assert!(attachment.take_inbox().is_none());
2063    }
2064
2065    /// The three answers a request naming a session can get before it
2066    /// reaches anything, and the fact that separates the first two.
2067    /// A caller holding a list needs "your list was stale" and *that id was
2068    /// never mine* to be different answers, and once a session ends nothing is
2069    /// left of it but its id — so the split rests entirely on the high-water
2070    /// mark. Without it every ended session would be reported as one that never
2071    /// existed, and a control plane driven from a timeline could not tell a
2072    /// session that finished early from a typo.
2073    ///
2074    /// *Ablation, recorded:* delete the `high_water` arm from
2075    /// `ControlPlane::reach`, leaving a lookup miss to answer
2076    /// `NoSuchSession`. This test goes red with
2077    ///
2078    /// ```text
2079    /// assertion `left == right` failed: a session this proxy ran and has
2080    /// finished is a race, not a mistake
2081    ///   left: NoSuchSession(SessionId(2))
2082    ///  right: SessionEnded(SessionId(2))
2083    /// ```
2084    #[test]
2085    fn an_ended_session_and_an_id_that_never_ran_are_different_answers() {
2086        let plane = ControlPlane::new(LegSetup::default());
2087
2088        assert_eq!(
2089            plane.reach(SessionId(2)).err(),
2090            Some(ControlError::NoSuchSession(SessionId(2))),
2091            "nothing has ever run here"
2092        );
2093
2094        let registration = plane.register(SessionId(2), handle());
2095        assert!(plane.reach(SessionId(2)).is_ok(), "a running session is reachable");
2096
2097        drop(registration);
2098        assert_eq!(
2099            plane.reach(SessionId(2)).err(),
2100            Some(ControlError::SessionEnded(SessionId(2))),
2101            "a session this proxy ran and has finished is a race, not a mistake"
2102        );
2103        assert_eq!(
2104            plane.reach(SessionId(3)).err(),
2105            Some(ControlError::NoSuchSession(SessionId(3))),
2106            "an id above everything this proxy has minted was never its own"
2107        );
2108    }
2109
2110    /// A session whose token has fired is refused rather than acted on.
2111    ///
2112    /// Its registration is still in the map — the guard lives in the
2113    /// session's own frame and that frame is unwinding — so a lookup finds
2114    /// it. Its tasks are on their way out, though, so a request accepted
2115    /// there would be taken and never served, which is the one outcome this
2116    /// module refuses to have.
2117    #[test]
2118    fn a_session_that_is_already_going_down_takes_no_more_requests() {
2119        let plane = ControlPlane::new(LegSetup::default());
2120        let entry = handle();
2121        let cancel = entry.cancel.clone();
2122        let _registration = plane.register(SessionId(1), entry);
2123
2124        assert!(plane.reach(SessionId(1)).is_ok());
2125        cancel.cancel();
2126        assert_eq!(
2127            plane.reach(SessionId(1)).err(),
2128            Some(ControlError::SessionEnded(SessionId(1))),
2129            "still listed, no longer serving"
2130        );
2131        assert_eq!(plane.live(), vec![SessionId(1)], "and it is still listed, which is why");
2132    }
2133
2134    /// A stream key that names nothing live is refused, and the refusal
2135    /// names both the session and the key so a caller with several sessions
2136    /// can tell which lookup missed.
2137    #[test]
2138    fn resetting_a_stream_that_is_not_live_is_refused_by_key() {
2139        let plane = ControlPlane::new(LegSetup::default());
2140        let _registration = plane.register(SessionId(1), handle());
2141        let control = ProxyControl::new(Arc::clone(&plane));
2142
2143        let stream = StreamKey { side: crate::types::ProxySide::ClientToProxy, id: 4 };
2144        assert_eq!(
2145            control.reset_stream(SessionId(1), stream, 9),
2146            Err(ControlError::NoSuchStream { id: SessionId(1), stream }),
2147            "the session is live and the stream is not"
2148        );
2149    }
2150
2151    fn key(id: u64) -> StreamKey {
2152        StreamKey { side: ProxySide::ClientToProxy, id }
2153    }
2154
2155    /// The sending half of a channel whose receiver is dropped at once.
2156    ///
2157    /// These tests are about the gate and the entry's lifetime, not about
2158    /// delivery: a sender with no receiver still registers, still clones
2159    /// and still goes away with its entry, which is all the registry
2160    /// promises. A test that needed a request delivered would have to run a
2161    /// stream, and that lives in `session.rs`.
2162    fn inbox() -> tokio::sync::mpsc::Sender<StreamCommand> {
2163        tokio::sync::mpsc::channel(1).0
2164    }
2165
2166    /// A registered stream is waitable; ending it releases the gate a waiter
2167    /// already took, and the entry goes away so the *next* asker is told
2168    /// there is nothing to wait for.
2169    ///
2170    /// *Ablation:* drop the `gate.release()` from `StreamRegistry::end` —
2171    /// the `is_released` assertion goes red, which is the `max_hold` stall
2172    /// this whole mechanism exists to avoid.
2173    #[test]
2174    fn ending_a_stream_releases_the_gate_a_waiter_already_took() {
2175        let registry = Arc::new(StreamRegistry::new());
2176        let guard = registry.register(key(1), inbox());
2177
2178        let held = registry.gate_for(key(1)).expect("a live stream is waitable");
2179        assert!(!held.is_released(), "a live stream's gate is not released");
2180        assert_eq!(registry.len(), 1);
2181
2182        drop(guard);
2183
2184        assert!(held.is_released(), "ending the stream releases the gate a waiter already holds");
2185        assert!(registry.gate_for(key(1)).is_none(), "an ended stream is no longer waitable");
2186        assert_eq!(registry.len(), 0, "the entry is removed, not left released");
2187    }
2188
2189    /// A key that was never registered and a key whose stream has already
2190    /// ended are the same answer, which is what lets the caller report
2191    /// `SerializeTargetUnknown` once for both.
2192    #[test]
2193    fn an_unknown_and_an_ended_key_are_the_same_answer() {
2194        let registry = Arc::new(StreamRegistry::new());
2195        assert!(registry.gate_for(key(7)).is_none(), "never registered");
2196
2197        drop(registry.register(key(7), inbox()));
2198        assert!(registry.gate_for(key(7)).is_none(), "registered, then ended");
2199    }
2200
2201    /// Two sides may mint the same numeric id, and the registry must keep
2202    /// them apart — the runtime half of
2203    /// `a_stream_key_is_scoped_by_side_as_well_as_id`.
2204    #[test]
2205    fn the_registry_scopes_entries_by_side() {
2206        let registry = Arc::new(StreamRegistry::new());
2207        let client = StreamKey { side: ProxySide::ClientToProxy, id: 3 };
2208        let relay = StreamKey { side: ProxySide::RelayToProxy, id: 3 };
2209
2210        let _client_guard = registry.register(client, inbox());
2211        let relay_guard = registry.register(relay, inbox());
2212        assert_eq!(registry.len(), 2);
2213
2214        drop(relay_guard);
2215        assert!(registry.gate_for(client).is_some(), "the client-side stream is still live");
2216        assert!(registry.gate_for(relay).is_none());
2217    }
2218}