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