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}