Skip to main content

moqtap_proxy/
lib.rs

1#![deny(missing_docs)]
2
3//! MoQT intercepting proxy — transparent stream forwarding with inline
4//! frame parsing, observation and impairment.
5//!
6//! This crate provides a transparent proxy that sits between a MoQT client
7//! and relay, forwarding all bytes bidirectionally while parsing MoQT frames
8//! inline to emit structured events. It does not participate in MoQT state
9//! management — it observes, and it does what a hook tells it to do, but it
10//! never acts as an endpoint.
11//!
12//! # Observing
13//!
14//! [`proxy::TransparentProxy`] accepts client connections and hands each one
15//! to a [`session::ProxySession`]. Every frame the session parses is reported
16//! to an [`observer::ProxyObserver`] as a [`event::ProxyEvent`]. An observer
17//! that answers `false` to [`observer::ProxyObserver::wants_events`] asks for
18//! no parsing of its own; what the session parses then depends entirely on
19//! the hook.
20//!
21//! # Acting — the hook API
22//!
23//! [`hook::ProxyHook`] is the acting side. It has one method per *site* —
24//! control message, stream open, stream header, object, datagram, stream end
25//! — plus [`hook::ProxyHook::interest`]. Every method is synchronous and
26//! defaulted, so a hook implements only the sites it cares about, and the
27//! trait stays usable as `Arc<dyn ProxyHook>`. The two stream-decision sites
28//! return an [`action::StreamAction`]; every other site returns an
29//! [`action::Action`] — pass, replace the frame or just its payload, delay,
30//! hold behind an [`action::Gate`], elide, truncate, reset the stream, close
31//! the session.
32//!
33//! What the session arms is decided **once**, at session start, from
34//! [`hook::ProxyHook::interest`]. An [`action::Interest::NONE`] hook adds
35//! nothing to the path the observer already asked for — with a no-op
36//! observer that is the byte pump, bit for bit: no framing, no control
37//! parse, no datagram decode, and no site is ever called. Wider interest is
38//! paid for structurally — [`action::Interest::OBJECTS`] puts data streams
39//! through [`framer::ObjectFramer`], which buffers an object whole, up to
40//! [`framer::FramerConfig`]'s cap, before it forwards it.
41//!
42//! # What cannot be asked for
43//!
44//! Not every action is representable at every site, on every draft, on every
45//! kind of stream — on the drafts where a subgroup's ID is defined by its
46//! first object, eliding that object would redefine the subgroup, and
47//! resetting a control stream is a protocol violation whatever the reason.
48//! [`capability::Capabilities`] answers that question ahead of
49//! time, per draft, and hands back a [`capability::Support`]; the engine asks
50//! it again when the action arrives. An action it refuses is reported as
51//! [`event::ProxyEvent::ActionRefused`] carrying the [`capability::Refusal`]
52//! that explains it, and the frame is forwarded unchanged. Nothing a hook can
53//! return tears a session down by accident.
54//!
55//! Degradation the proxy imposed on *itself* — a stream it could not frame, a
56//! queue that filled, a hold it clamped, a release timer coarser than it asked
57//! for — is reported as [`event::ProxyEvent::Impairment`] rather than absorbed
58//! silently.
59//!
60//! Those reports arrive **after** the thing they describe, never before: the
61//! reset has been handed to the transport, the datagram has come back
62//! refused, the queue has already been abandoned. So an impairment is a fact
63//! about the past, and an action a guard refuses produces none at all — which
64//! matters because nothing retracts one. `ProxyEvent::ActionApplied` is the
65//! deliberate exception and says so on itself: the engine emits it on
66//! admission and the transport can still decline, which is what
67//! [`event::ProxyEvent::ActionFailed`] is for.
68//!
69//! Each impairment also names *which connection* it is about, and that is not
70//! the same as the direction it was noticed on. Most of them report a failure
71//! to write — a queue that could not be flushed, a datagram the far side
72//! refused, a destination stream that had to be reset — so they belong to the
73//! opposite leg from the one the bytes arrived on. A reader who mapped
74//! [`event::ProxySide`] onto a connection would attribute those to the healthy
75//! leg, plausibly and silently, so the event carries the leg outright and
76//! leaves it absent for the four reports that are about a profile, a draft or
77//! the host rather than about a connection at all.
78//!
79//! # Three layers to degrade at
80//!
81//! Everything below is one of three, stacked, and which one a question
82//! belongs to is usually the whole of the answer. Underneath the transport
83//! is a **socket** the caller supplies — [`listener::Listener::bind_with_socket`]
84//! for the client leg, [`session::ProxySessionConfig::upstream_socket`] for
85//! the relay leg — which sees UDP datagrams before QUIC does and can lose,
86//! delay, reorder or meter them. In the middle are that leg's **transport
87//! parameters**, a [`transport::TransportProfile`] or a raw
88//! `quinn::TransportConfig`, which decide what QUIC *does* about all that:
89//! the windows, the congestion controller, the loss-detection thresholds,
90//! the MTU floor. Above QUIC is the **object scheduler**, a
91//! [`shape::ShapeProfile`], which paces and starves MoQT objects that QUIC
92//! has already delivered intact — never reorders them, because a
93//! destination stream keeps one FIFO whose head gates everything behind it.
94//!
95//! They compose in one direction: **the lower layer acts and the upper one
96//! reacts.** A datagram the socket drops becomes a retransmission whose timing
97//! the transport parameters govern, and the bytes arrive late at the framer,
98//! which is where the scheduler's queue fills. Nothing runs the other way — an
99//! object this crate held back or removed was never handed to QUIC at all, so
100//! there is nothing for QUIC to repair and the peer's stack cannot tell it
101//! apart from a relay that published less. That is also why a scenario picks a
102//! layer rather than turning all three on: *does the player survive a bad
103//! network?* is the bottom two, *does it survive a bad relay?* is the top one,
104//! and a run with both armed cannot attribute what it saw to either.
105//!
106//! The one place the stack is read upwards is worth knowing, because it
107//! looks like a bug from inside the top layer: [`shape::Overflow::Block`]
108//! stalls this proxy's read loop and reaches the publisher only if the
109//! middle layer lets it, since a receive window wide enough absorbs the
110//! backpressure before the sender ever notices. Reporting is layered the
111//! same way and does not aggregate — [`shape::ShapeStats`] and
112//! [`event::ProxyEvent::Impairment`] describe the object layer only, and a
113//! datagram lost beneath QUIC is in neither.
114//!
115//! # The conditions these three layers reach
116//!
117//! An inventory rather than a tutorial: the industry-recognised degradation
118//! classes, and the type that produces each. It is here so that "can this
119//! reproduce X?" is answered by a list with names in it rather than by reading
120//! three modules. Every row was checked against the code.
121//!
122//! **Socket layer** — a `quinn_netem::DirectionProfile`, one per direction, so
123//! every row below is independently settable uplink and downlink:
124//!
125//! | Condition | Produced by |
126//! |---|---|
127//! | Random loss | `LossModel::Bernoulli` |
128//! | Bursty loss | `LossModel::GilbertElliott` |
129//! | Deterministic loss pattern | `LossModel::Pattern` |
130//! | Fixed added latency | `DelayModel` |
131//! | Jitter, correlated | `DelayModel::{Uniform, Normal, Pareto, ParetoNormal}` |
132//! | Latency spikes | `DirectionProfile::timeline` over `delay` |
133//! | Reordering | `ReorderModel` |
134//! | Duplication | `DupModel` |
135//! | Corruption | `CorruptModel` |
136//! | Sustained bandwidth limit | `RateModel::{bps, burst_bytes}` |
137//! | Bandwidth step, ramp, oscillation | `DirectionProfile::timeline` over `rate` |
138//! | Bufferbloat | `RateModel::queue_bytes` |
139//! | Blackout or handover window | `DirectionProfile::blackouts` |
140//! | Asymmetric uplink/downlink | `ImpairProfile::{uplink, downlink}` |
141//! | High-BDP path | `Preset::Satellite`, with the windows below |
142//! | MTU black hole | `DirectionProfile::mtu_blackhole` |
143//!
144//! **Transport layer** — a [`transport::TransportProfile`], per leg:
145//!
146//! | Condition | Produced by |
147//! |---|---|
148//! | Flow-control stall | `receive_window`, `stream_receive_window`, `send_window` |
149//! | Stream-concurrency starvation | `max_concurrent_uni_streams`, `max_concurrent_bidi_streams` |
150//! | Idle timeout and keep-alive | `max_idle_timeout`, `keep_alive_interval` |
151//! | Congestion-controller sensitivity | `Congestion::{Cubic, Bbr, NewReno}`, `initial_rtt` |
152//! | Loss-detection sensitivity | `packet_threshold`, `time_threshold`, `persistent_congestion_threshold`, `ack_frequency` |
153//! | Abrupt connection close | [`control::ProxyControl::close_session`] |
154//!
155//! **Object layer** — an [`action::Action`] returned from a hook, or a
156//! [`shape::ShapeProfile`] with no hook at all:
157//!
158//! | Condition | Produced by |
159//! |---|---|
160//! | Object drop, per track/group/subgroup/id | [`action::Action::Drop`] with [`action::DropMode`] |
161//! | Object delay, group hold | [`action::Action::Delay`], [`action::Action::Hold`] |
162//! | Partial object, truncated stream | [`action::Action::Truncate`] |
163//! | Stream abort mid-subgroup | [`action::Action::ResetStream`] |
164//! | Object payload corruption | [`action::Action::Replace`], [`action::Action::ReplacePayload`] |
165//! | Cross-stream reordering | `OpenAfter` at the stream-open site |
166//! | Head-of-line blocking | `SerializeAfter` |
167//! | Priority inversion, track starvation | [`shape::Discipline::StrictPriority`], [`shape::Discipline::WeightedRoundRobin`] |
168//! | Per-track bandwidth budgets | [`shape::ClassRule`] keyed on `track_alias` or `priority` |
169//! | Control-message drop, delay, rewrite | [`hook::ProxyHook::on_control_message`] |
170//! | Protocol fault injection | [`control::ProxyControl::inject_control`], raw bytes |
171//! | Datagram-mode degradation | [`hook::ProxyHook::on_datagram`] |
172//!
173//! Two cross-cutting properties, each with a limit worth knowing:
174//!
175//! * **Reproducibility.** Every impairment decision comes from
176//!   `Pcg32::seeded(profile.seed, stream_id)`, so a seed and a packet index
177//!   reproduce a decision sequence exactly. Wall-clock packet *timing* is not
178//!   reproducible and is not claimed to be — see the pacer's own docs for why a
179//!   stream of deadlines reproduces a rate rather than a spacing.
180//! * **Evidence.** [`event::ProxyEvent::Impairment`] with
181//!   [`event::ImpairmentKind`], [`event::ShapeOutcome`] and [`event::Effect`]
182//!   report the object layer; `quinn_netem::StatsSnapshot` reports the socket
183//!   layer. They do not aggregate, deliberately. The counts divide on the
184//!   same principle: what a **hook** decided is on [`instrument::Counters`]
185//!   and what a **profile** did is on [`shape::ProxyStats`], because a hook
186//!   runs whether or not a profile was configured, and a figure on the wrong
187//!   side of that line can only ever be a partial count.
188//!
189//! # Shaping — degradation without a hook
190//!
191//! A [`shape::ShapeProfile`] on [`session::ProxySessionConfig::shape`] is the
192//! other way to impair a session, and it is **configuration rather than hook
193//! code**: named token buckets, [`shape::ClassRule`]s that aim a
194//! [`shape::Matcher`] at one of them, a bounded-queue policy, and a
195//! [`shape::Discipline`] arbitrating between classes that share a bucket.
196//! *Starve the video track while audio flows* is then a value a caller builds,
197//! with no `ProxyHook` involved at all — a profile arms object framing on its
198//! own, because classification needs the [`framer::ObjectMeta`] only the
199//! framing path produces. A mistyped bucket name is rejected by
200//! [`shape::ShapeProfile::try_new`] rather than becoming a silently inert rule,
201//! and what a run actually did is readable *while it runs* from
202//! [`session::ProxySession::shape_stats`], or for a whole proxy — every session
203//! it has accepted, ended ones included — from
204//! [`control::ProxyControl::stats`]. The proxy-wide form is cumulative where
205//! [`control::ProxyControl::sessions`] is instantaneous, and it splits its byte
206//! figures across both connections and both directions, so *the shaper read
207//! this from the client and wrote that to the relay* is two numbers rather than
208//! one.
209//!
210//! Ordering is not a policy option. Each unit is classified and charged
211//! individually, but a destination stream keeps one FIFO whose head gates
212//! everything behind it, because object IDs are delta-encoded on drafts
213//! 14-19 and the framer's re-encoding primitive handles removal, not
214//! reordering. A stream carrying two classes says so, once, as
215//! `ImpairmentKind::ClassChangedMidStream`.
216//!
217//! What shaping deliberately does **not** cover, because a limit discovered
218//! from a green run is worse than one read here:
219//!
220//! * **Control streams are never shaped**, on any path — no scheduler is
221//!   installed on them, so no control frame can reach a bucket even by
222//!   accident. Pacing SUBSCRIBE behind a video bucket would make MoQT's
223//!   idle steady state look like a dead session.
224//! * **Datagrams are policed, not paced.** A datagram-aimed class discards
225//!   what its bucket has no tokens for, on arrival, and never delays
226//!   anything: there is no queue on that path and there should not be one,
227//!   because a FIFO would impose a delivery order the protocol does not
228//!   have. So a datagram-mode audio track can be held to a rate and cannot
229//!   be smoothed, and a rule asking for [`action::Action::Delay`] or
230//!   [`action::Action::Hold`] at the datagram site is refused as
231//!   `Refusal::WrongSite` rather than approximated. Reordering and delay
232//!   under a whole connection are `quinn-netem`'s, one layer down.
233//! * **A `Fetch`-aimed class is live on drafts 07-17 and dead on 18-19**,
234//!   whose fetch objects carry a Group ID *difference* the fetch's Group
235//!   Order gives a direction to; that order is settled on the control plane
236//!   and never reaches the data stream, so the framer abandons the stream at
237//!   its header and no rule ever sees a unit. Reported the same way and for
238//!   the same reason, per draft rather than unconditionally.
239//! * **On drafts 15, 16 and 17 an elide on a fetch unit is paid for rather
240//!   than refused.** Those drafts let a fetch object leave a field off and
241//!   take the previous object's, so deleting one object's bytes would move
242//!   every Location behind it while the stream still decoded. The framer
243//!   re-encodes the survivor's framing against the frame now in front of it,
244//!   which settles the debt in one frame exactly as the subgroup fix-up
245//!   does.
246//! * **There is no `Expiry::Drop`.** A unit dropped at release time cannot
247//!   arm the framer's positional elide fix-up — its successors are already
248//!   framed — so on drafts 14-19 it would shift every later object's
249//!   absolute ID and report corruption as loss. Under the default
250//!   [`shape::Expiry::Deliver`] a starved class is **late, never lossy**;
251//!   [`shape::Expiry::ResetStream`] abandons the stream instead. Discarding
252//!   an *arriving* unit is [`shape::Overflow::DropTail`]'s job, which runs
253//!   early enough to pay the fix-up.
254//! * **[`shape::Overflow::Block`] stops this proxy's read loop; it does not
255//!   stall the peer.** At quinn's defaults a stream's receive window is
256//!   1.25 MB and the connection-level `receive_window` is `VarInt::MAX` —
257//!   effectively unlimited — so a blocked stream still absorbs its queue
258//!   plus ~1.25 MB before the sender's write blocks, and blocked streams
259//!   never slow the connection as a whole. Assert `Block` on this crate's
260//!   own counters and event order, never on the peer's send rate. Making
261//!   backpressure reach the publisher is a transport setting rather than a
262//!   missing feature: put a small `stream_receive_window` on the leg that
263//!   *receives* from it, through
264//!   [`transport::TransportProfile`], or on the raw config directly with
265//!   [`listener::ListenerConfig::transport_config`] or
266//!   [`session::ProxySessionConfig::upstream_transport_config`]. Measured
267//!   against one session with a two-object queue held shut: at quinn's
268//!   default (a 1 250 000-byte per-stream window) the source pushed about
269//!   1.25 MB before its first write pended; behind a 64 KiB window it
270//!   stalled at 82 370 bytes, with the shaper's own numbers identical in
271//!   both runs.
272//!
273//!   Note that 82 370 is *past* a 64 KiB window, by 16 834 bytes, and that
274//!   is not an anomaly — it is what having a reader means. The source's
275//!   allowance is the window plus everything the proxy has already drained
276//!   off the wire, so with a proxy reading, the stall lands beyond the
277//!   window; against a receiver that never reads at all it lands a little
278//!   short of it, by however much of the final write did not fit. Both are
279//!   correct, for different readers. Either way the number moves with the
280//!   writer's chunk size and the reader's queue depth, so it is a property
281//!   of the fixture and not a constant to assert against.
282//! * **[`shape::BucketConfig::ceil_bps`] is accepted and never borrowed
283//!   against.** A profile setting it above `rate_bps` measures a flat
284//!   `rate_bps`. There is no runtime report for it, deliberately:
285//!   `ShapeError` has no variant a bucket misconfiguration could land in,
286//!   and `ImpairmentKind::ShapeRuleUnmatchable` is keyed on a *wire* field
287//!   and a draft, so reusing it would make a per-draft report fire on a
288//!   draft-independent fact.
289//!
290//! # Below QUIC — the socket seams
291//!
292//! Everything above shapes MoQT objects, which sit *above* QUIC: damage
293//! there is indistinguishable from a relay that dropped or delayed media,
294//! and QUIC will never repair it because as far as QUIC is concerned
295//! nothing was lost. The other place to degrade traffic is *below* QUIC,
296//! on the datagrams themselves, where damage is precisely what the peer's
297//! QUIC stack is built to absorb — loss triggers retransmission, jitter
298//! inflates the RTT estimate, a rate limit drives the congestion
299//! controller. The two answer different questions and compose freely.
300//!
301//! [`listener::Listener::bind_with_socket`] builds the client-facing
302//! endpoint over a socket the caller already owns, and
303//! [`session::ProxySessionConfig::upstream_socket`] does the same for the
304//! relay leg. Both take any `Arc<dyn quinn::AsyncUdpSocket>`, so this
305//! crate stays agnostic: a tap, a counter and an impairment shim are the
306//! same seam, and none of them is a dependency here.
307//!
308//! Three things a caller has to know:
309//!
310//! * **The client-facing seam reaches WebTransport clients too**, and by
311//!   construction rather than by luck. The proxy never builds a
312//!   WebTransport endpoint on that side — it builds the QUIC endpoint,
313//!   reads the negotiated ALPN off the handshake, and hands an `h3`
314//!   client's still-connecting QUIC connection to the WebTransport library
315//!   to finish. That library adopts a connection already living on this
316//!   endpoint rather than binding one, so there is no second datagram path.
317//! * **A WebTransport *upstream* cannot honour a socket, and says so.**
318//!   Setting one there returns
319//!   [`error::ProxyError::UpstreamSocketUnsupported`] and connects to
320//!   nothing. `upstream_transport_config` is merely ignored in the same
321//!   situation, and the asymmetry is the point: a dropped transport config
322//!   yields a working connection with unchosen windows, while a dropped
323//!   socket yields a relay leg that bypasses the caller's decorator
324//!   entirely — every impairment armed on it reported and applied to
325//!   nothing, and a run that looks clean because it *is* clean.
326//! * **One socket per session on the upstream seam.** Two endpoints reading
327//!   one socket steal each other's datagrams, and a packet for a connection
328//!   an endpoint does not own is discarded, so sessions that run
329//!   concurrently need a socket each.
330//!
331//! Those two seams are reached by building a [`listener::Listener`] or a
332//! [`session::ProxySession`] yourself. A
333//! [`proxy::TransparentProxy`] binds its own listener inside `run()` and
334//! copies its session template per connection, so it reaches both through
335//! one call instead: `proxy::TransparentProxy::set_impaired_socket` takes
336//! a leg's socket **and** the `quinn_netem::ImpairHandle` it was wrapped
337//! with, together, and is what makes
338//! `control::ProxyControl::set_impair` able to arm anything. Together
339//! because nothing can check that a handle belongs to the socket beside it —
340//! `AsyncUdpSocket` is a trait object with no downcast and the decorator
341//! exposes no accessor — so taking them as two independent settings would
342//! make a mismatched pair expressible, and a mismatched pair impairs traffic
343//! nobody sends.
344//!
345//! Those two method names, and every `quinn_netem::` name on this page, are
346//! in plain code font rather than linked because all of them exist only under
347//! the `impair` feature, while this page is built without it. A link to one
348//! would resolve to nothing, and an unresolved intra-doc link is an error
349//! under the documentation build rather than a warning.
350//!
351//! Nothing that happens under a supplied socket appears in this crate's
352//! reporting. [`shape::ShapeStats`] and
353//! [`event::ProxyEvent::Impairment`] describe the object layer; a datagram
354//! dropped beneath QUIC is in neither, and the socket is where to count it.
355//!
356//! # Transport parameters, per leg
357//!
358//! A [`transport::Leg`] is a *connection* — this proxy holds two, one to
359//! the client and one to the relay — and each carries its own QUIC
360//! transport parameters. Do not read it as
361//! [`event::ProxySide`], which is a *direction of travel* over a leg and
362//! therefore has four variants where `Leg` has two; both types exist in
363//! this crate and the compiler will not catch the confusion.
364//!
365//! Each leg takes those parameters one of two ways, never both at once. A
366//! raw `quinn::TransportConfig` is installed as it was given. A
367//! [`transport::TransportProfile`] is a value that can be written down,
368//! checked and stored, and the leg builds a config from it through a
369//! [`transport::TransportInstaller`] — [`transport::DefaultInstaller`]
370//! unless the caller supplies one. Either way the config is installed
371//! before the endpoint is built and before anything is dialled, so a
372//! refusal costs no socket and cannot be mistaken for a network fault.
373//!
374//! Setting both on one leg is [`error::ProxyError::TransportConfigAndProfile`]
375//! rather than a merge, and that is a fact about `quinn::TransportConfig`
376//! rather than a policy: it has no `Clone` and no getters, so no merge can
377//! be written that does not discard the caller's config while keeping the
378//! profile — the failure the tests would not see. A caller who wants both
379//! applies the profile to their own config with
380//! [`transport::TransportProfile::apply_to`], or supplies an installer that
381//! builds the base itself.
382//!
383//! # Capturing the QUIC layer
384//!
385//! Under the off-by-default `qlog` feature, `qlog::QlogSpec` says where a
386//! connection's QUIC-level capture should be written. It is written in plain
387//! code font here for the same reason the `quinn_netem` names above are: this
388//! page is built without that feature, and a link to an item that does not
389//! exist in the build is an error rather than a warning.
390//!
391//! **Each leg carries a spec of its own**, beside the transport settings it
392//! already carries: `ListenerConfig::qlog` for the client leg and
393//! `ProxySessionConfig::upstream_qlog` for the relay leg. The leg builds its
394//! `quinn::TransportConfig` once — applying its
395//! [`transport::TransportProfile`] first, through its
396//! [`transport::TransportInstaller`] if it has one — and installs the sink
397//! on what came back, before the endpoint exists, because quinn accepts a
398//! sink in exactly one place and that place is a method on a config. A spec
399//! on its own is enough: the leg builds a default config for the sink to go
400//! on rather than attaching the capture to a config no connection uses. A
401//! leg naming a **raw** `quinn::TransportConfig` and a spec together is
402//! refused with `ProxyError::TransportConfigAndQlog` naming the leg, for the
403//! same reason a raw config and a profile are: the config is the caller's
404//! and this crate may not mutate it behind their back.
405//!
406//! `QlogSpec::attach_to` is that installation step on its own, for a caller
407//! building a config by hand, and `QlogSpec::into_stream` is the seam under
408//! it for one being installed somewhere this crate does not reach. Both
409//! write the file's preamble, so the artifact exists from that moment.
410//!
411//! **A spec is single-use and therefore refused on a proxy template.** It owns
412//! its writer and is consumed when it becomes a sink, so it has no `Clone` and
413//! there is exactly one of it, while [`proxy::TransparentProxy`] copies both
414//! leg configs — once for the listener, once per accepted connection. A
415//! `ProxyConfig` carrying one on either leg is refused by
416//! `TransparentProxy::run` with `ProxyError::QlogOnProxyTemplate`, before it
417//! binds, rather than dropped: a proxy that came up anyway would report success
418//! and leave the caller's file uncreated. Capture by building the
419//! [`listener::ListenerConfig`] and calling [`listener::Listener::bind`]
420//! yourself, or by driving a [`session::ProxySession`] directly — which is also
421//! the only shape in which *one capture per connection* is expressible, since
422//! one sink shared by an endpoint's connections writes all of them into one
423//! file, behind one preamble, with no record saying where one ends.
424//!
425//! The capture is the layer *underneath* everything else this crate reports.
426//! quinn records packets sent and received, congestion and loss, and nothing
427//! above QUIC — no object, no stream id, no shaping class — so a datagram
428//! dropped beneath QUIC appears there and in none of the counters, while an
429//! object dropped by a shaping rule appears in the counters and not there.
430//! Two facts about the artifact are worth knowing before anyone builds a
431//! check on one: the file and its preamble are written when the sink is
432//! built, so a capture that reached no connection is still a valid, non-empty
433//! file; and a connection that carried nothing still sends packets, so a
434//! non-zero byte total is not by itself evidence of traffic. The module
435//! documentation carries the measurements.
436//!
437//! # Asking a proxy that is already running
438//!
439//! Everything above is settled before the thing it configures exists — the
440//! listener consumes its config as it binds, each session copies its own
441//! before it dials, and a hook's interest is sampled once at session start.
442//! That is what makes a run reproducible from the values that started it,
443//! and it is also why there was no way to ask a live proxy anything.
444//!
445//! [`proxy::TransparentProxy::control`] hands back a
446//! [`control::ProxyControl`], and it can be called before
447//! [`proxy::TransparentProxy::run`] is awaited — which is the usual case,
448//! because `run()` does not return until the proxy is finished. The handle
449//! is therefore defined for a proxy that has not bound yet:
450//! [`control::ProxyControl::local_addr`] answers
451//! [`error::ProxyError::NotBound`] until the endpoint exists, which is how a
452//! proxy configured with port 0 tells its caller which port it chose.
453//!
454//! [`control::ProxyControl::sessions`] lists the sessions that are live at
455//! that instant, and it is a **census rather than a log**: an id appears
456//! when its session starts running and is gone once that session ends, for
457//! any reason, including the ones no teardown site could be written for. A
458//! caller that wants the history reads
459//! [`event::ProxyEvent::SessionStarted`] and
460//! [`event::ProxyEvent::SessionEnded`] from its observer instead, and should
461//! expect the two views to disagree at the edges — the events are emitted
462//! around accepting a connection, the census around running the session, and
463//! a session whose upstream connect fails is in the first and only briefly
464//! in the second.
465//!
466//! Requests that *act* on a live session — as opposed to reporting on one —
467//! are refused with a [`control::ControlError`] rather than by returning
468//! success and doing nothing. The type is neither `Eq` nor
469//! `#[non_exhaustive]`, so a caller outside this crate can match every
470//! variant with no wildcard arm and find out at compile time when a new
471//! refusal appears.
472//!
473//! Three of those requests act on one named session, and each names the
474//! consequence it produces rather than the state it changes:
475//!
476//! * [`control::ProxyControl::close_session`] gives the session's egress
477//!   queues a bounded window — [`action::EgressConfig::drain_timeout`], 100
478//!   ms by default — to flush, then closes both legs with the code and
479//!   reason it was given, whether or not the window was enough. Anything
480//!   still queued when it expires is abandoned and named, per stream, as
481//!   [`event::ImpairmentKind::QueuedBytesAtTeardown`], so what the peer
482//!   received plus what the impairments name is what was queued when the
483//!   close was asked for.
484//! * [`control::ProxyControl::reset_stream`] resets one live forwarded
485//!   stream, which the destination peer sees as `RESET_STREAM` carrying the
486//!   requested code.
487//! * [`control::ProxyControl::inject_control`] writes a framed control
488//!   message onto the session's existing control stream, between two
489//!   forwarded messages, so the peer decodes it in sequence.
490//!
491//! Only the first of those three produces observer events, and it produces
492//! them through the session rather than from the call: a
493//! [`event::ProxyEvent::SessionEnded`] whose reason names the **control
494//! plane** rather than a hook — both reach the same latch, and reporting an
495//! operator's close as a hook's was a sentence about the run that was simply
496//! untrue — plus whatever the expired drain window abandoned. The other two
497//! emit nothing, and each says why on its own page: neither has anything to
498//! report that the caller does not already hold in the return value, and
499//! neither may borrow an existing event without making it ambiguous for the
500//! readers that already rely on it. What they produce is observable at the
501//! peer, which is where a consequence belongs.
502//!
503//! # Reconfiguring a proxy that is already running
504//!
505//! The other requests change what the proxy *is*. They reach three
506//! different distances, and the distances are not a matter of how they were
507//! implemented — they are what the thing being changed will admit. Read
508//! them as reach first and as settings second, because a caller who reads
509//! them the other way round will measure the wrong connection and believe
510//! the answer.
511//!
512//! **`control::ProxyControl::set_impair` reaches traffic already in
513//! flight.** It arms a datagram impairment on one leg's socket, below QUIC,
514//! so the next datagram that leg passes carries it. Datagrams already
515//! handed to the operating system are gone.
516//! `control::ProxyControl::clear_impair` removes it and is idempotent.
517//! Both need the leg's socket **and** the `quinn_netem::ImpairHandle` it was
518//! wrapped with to have been handed over together, before `run()`, through
519//! `proxy::TransparentProxy::set_impaired_socket`; a leg the proxy holds
520//! no handle for is refused rather than accepted and ignored, because a
521//! profile armed on a socket nobody's traffic crosses is reported as applied
522//! and does nothing, and the run that follows looks clean because it *is*
523//! clean.
524//!
525//! **[`control::ProxyControl::set_shaper_enabled`] reaches the next release
526//! decision each queue makes**, on every session already running. It stops
527//! the token buckets and the discipline without discarding the profile, so
528//! switching it back on resumes the same configuration and the same
529//! counters. A stream held by a bucket that will refill resumes within one
530//! pacing interval; a stream held by a bucket configured at **zero** does
531//! not resume until its `max_hold` clamp, because the wait it is on is a
532//! timer a per-stream queue armed and nothing session-wide reaches one.
533//!
534//! **[`control::ProxyControl::set_shape`] reaches the next stream** a
535//! running session forwards, and every session accepted afterwards. Not the
536//! next object: a stream's egress queue holds the scheduler its units were
537//! admitted under, and a class is an index into that scheduler's class list,
538//! so a unit classified against one profile and released against another
539//! charges a class that was not the one matched. Two further limits, both
540//! structural: a session that started **unshaped** stays unshaped, because
541//! framing is armed at session start and there are no objects to classify;
542//! and a running session takes the new profile only if its **class list is
543//! unchanged** — same names, same order — because
544//! [`shape::ShapeStats`] rows are pre-sized per session and charged by
545//! position, so a different list would keep every number right and every
546//! label on it wrong. Sessions accepted afterwards have no such limit. To
547//! move a running session onto a different class list, end it with
548//! `close_session`.
549//!
550//! **[`control::ProxyControl::set_transport`] reaches no connection that
551//! already exists — ever.** A QUIC connection takes its
552//! `quinn::TransportConfig` once, at setup, and keeps it for life; quinn
553//! offers four setters on a live connection (two stream-count limits, two
554//! windows) and no way to replace the configuration behind it. So a profile
555//! set on [`transport::Leg::Upstream`] reaches the next connection this
556//! proxy **opens**, which is the next session it accepts, and a profile set
557//! on [`transport::Leg::Client`] reaches the next connection it
558//! **accepts** — which the proxy does not initiate and which may never
559//! arrive. On a proxy that never dials or accepts again, the call is a
560//! permanent silent no-op that returns `Ok(())`, and nothing in the return
561//! value distinguishes that from a setting that reached everything
562//! afterwards. The one instrument that makes it bite on a session already
563//! running is `close_session`: a leg that reconnects is a leg that installs
564//! it. A WebTransport upstream cannot take one at all — that endpoint is
565//! built inside the WebTransport library, which takes no transport config
566//! and hands back no endpoint — and says so with
567//! [`control::ControlError::Unsupported`] rather than storing it.
568//!
569//! # Writing the configuration down
570//!
571//! Everything above is configured in Rust, which is what a test wants and not
572//! what an operator wants. The off-by-default `serde` feature derives
573//! `Serialize` and `Deserialize` on the configuration types — the shaping
574//! profile with its buckets, classes and matchers, and both legs' transport
575//! parameters — so a caller can read them from a file instead of building
576//! them in code. It is written in plain code font here for the reason the
577//! `quinn_netem` names above are: this page is built without that feature, so
578//! a link would resolve to nothing.
579//!
580//! The feature buys the types and nothing more. This crate reads no file,
581//! interprets no run, and has no opinion about what a run *is*: a caller
582//! deserializes what it wants, hands the profiles to a session, and drives
583//! whatever changes part-way through against [`control::ProxyControl`]
584//! itself. That is the same line [`hook::ProxyHook`] draws — the mechanism
585//! and the extension point are here, and what to do with them belongs to
586//! whoever wrote the consumer.
587//!
588//! One property is worth knowing before reading a profile from a file.
589//! [`shape::ShapeProfile`]'s fields are private so that
590//! [`shape::ShapeProfile::try_new`] is the only way to build one, and its
591//! `Deserialize` is routed through a public-field mirror whose `TryFrom` calls
592//! that constructor. A profile read from a file therefore runs the same seven
593//! validations as one built in Rust, and there is no deserialization path that
594//! skips them — including the one that catches a class naming a bucket that
595//! does not exist, which would otherwise parse, arm, report shaping and shape
596//! nothing.
597//!
598//! # Instrumentation
599//!
600//! [`instrument::Recorder`] counts the slow paths: framers created, objects
601//! elided, actions refused, egress items queued, release lateness. This is
602//! what makes `Interest::NONE` a falsifiable claim rather than a promise —
603//! a session that declared no interest ends with an all-zero
604//! [`instrument::Counters`]. [`shape::ShapeStats`] is its sibling rather
605//! than an extension of it: one row per configured class plus a default
606//! row, an unshapeable row and the session totals, where a class that saw
607//! nothing reports a **zero row** and never an absent one. Its two
608//! duration-valued fields are reported and never asserted — machine load
609//! moves them, and their load-independent companion counts are what a gate
610//! reads.
611//!
612//! Every session total is reported three times: flat, and again under
613//! [`shape::ShapeStats::uplink`] and [`shape::ShapeStats::downlink`]. The
614//! flat figure is **defined as the sum of the two**, so the views cannot
615//! disagree and no existing reader breaks. It matters on a session that
616//! shapes both legs, where one number cannot tell an uplink stall from a
617//! downlink one and a report will attribute downlink starvation to the
618//! uplink class.
619//!
620//! [`shape::ProxyStats`], read through [`control::ProxyControl::stats`], is
621//! the same figures kept a second time for a whole proxy — cumulative across
622//! every session it has accepted, ended ones included, and charged by the
623//! same writers, so no figure can reach a session's rows and miss it. It
624//! carries one axis a session's own totals do not: a **leg** as well as a
625//! direction, because a proxy holds two connections and a byte crosses both.
626//! Read [`shape::LegStats`] before reading a cell — a figure is charged
627//! where it was measured, so `per_leg[Client].uplink` is what was read from
628//! the client and `per_leg[Upstream].uplink` is what was written to the
629//! relay, and the difference between them is what the shaper kept back.
630//!
631//! **Every field here has a producer**, and it took a deletion rather than
632//! five new writers to make that true. A zero that looks like a measurement
633//! is how a clean run gets believed, and five fields here were one. Two of
634//! them counted what a **hook** did — a deferral and a truncation — while
635//! everything on this page is gated on a configured [`shape::ShapeProfile`],
636//! so in this type they could only ever have been partial counts; they are
637//! [`instrument::Counters::units_delayed`] and
638//! [`instrument::Counters::objects_truncated`] now, beside the elide count
639//! that had already settled where a hook's decision belongs. The other three
640//! were `Duration` totals a reader cannot calibrate, and the timing
641//! dimension is reported as a distribution by
642//! [`instrument::Counters::release_errors`] instead.
643//!
644//! Separately, and a different kind of zero: the three event figures on a
645//! [`shape::DirectionStats`] are charged where traffic arrived, so they read
646//! zero in a *departure* cell. That is stated on the fields as well.
647//!
648//! # Migrating
649//!
650//! *From 0.4*: [`session::ProxySessionConfig`] gained the `shape`,
651//! `upstream_socket`, `upstream_transport_profile` and `upstream_installer`
652//! fields, and [`listener::ListenerConfig`] gained `transport_profile` and
653//! `installer`. Neither is `#[non_exhaustive]`, so an exhaustive struct
654//! literal must name them all — `None` on each is 0.4 behaviour exactly, and
655//! [`session::ProxySessionConfig::default()`](session::ProxySessionConfig)
656//! is unaffected. [`listener::Listener::bind`] is unchanged and still binds
657//! its own socket. `StreamCtx::new` gained a seventh argument, which only
658//! affects code that builds one by hand to unit-test its own hook.
659//!
660//! *From 0.3*: the 0.3 hook trait survives as [`hook::LegacyProxyHook`],
661//! deprecated. Wrap an existing implementation in [`hook::LegacyHook`] to run
662//! it against this engine unchanged; its control-message and datagram
663//! rewrites become [`action::Action::Replace`].
664
665// Declared alphabetically, which says nothing about the graph. What does:
666// `instrument`, `qlog` and `types` name nothing else in this crate, so each of
667// them sits below every consumer it has and can be read, replaced or compiled
668// on its own. `tests/leaf_modules.rs` is what keeps that true — a single
669// `use crate::…` added to one of the three would end it silently otherwise.
670pub mod action;
671pub mod capability;
672pub mod control;
673pub mod error;
674pub mod event;
675pub mod framer;
676pub mod hook;
677pub mod instrument;
678pub mod listener;
679pub mod observer;
680pub mod parser;
681pub mod proxy;
682pub mod session;
683pub mod shape;
684pub mod transport;
685pub mod types;
686
687// Engine internals. Not `pub mod`: the deferred-write queue, the action
688// executor and the release wheel are how the sites above are implemented,
689// not part of what this crate promises. Keeping them private also keeps
690// them out of `#![deny(missing_docs)]` and lets any of the three be
691// replaced without a breaking change.
692//
693mod egress;
694mod exec;
695mod release_timer;
696
697#[cfg(feature = "cert-gen")]
698pub mod cert;
699
700// Behind `qlog`, which is off by default because turning it on adds a
701// derive-macro toolchain to everybody's build. What it holds is a description
702// of where a QUIC-layer capture should be written; the capture itself is
703// written by quinn.
704#[cfg(feature = "qlog")]
705pub mod qlog;