Skip to main content

moqtap_proxy/
transport.rs

1//! QUIC transport parameters as a value a scenario author can write down.
2//!
3//! A [`TransportProfile`] is a typed, serializable, per-leg description of
4//! the QUIC knobs a run wants: congestion controller, windows, loss-detection
5//! thresholds, MTU, keep-alive. Eighteen optional fields and nothing else —
6//! a field left `None` is a field this profile has no opinion about, and
7//! [`TransportProfile::apply_to`] leaves such a field exactly as it found it.
8//!
9//! # Beside `quinn::TransportConfig`, not on top of it
10//!
11//! Both legs already accept a raw `quinn::TransportConfig`, and they still do.
12//! A profile is applied *over* whatever the caller built, rather than replacing
13//! it, and the reason that is the only workable shape is a missing trait:
14//! `quinn::TransportConfig` has three impls — the inherent one, `Default` and
15//! `Debug` — no `Clone`, and no public getter for any field. A wrapper that
16//! owned the configuration could therefore neither copy the caller's config nor
17//! read it back, so it could only ever hand back a fresh default with the
18//! caller's settings discarded. Sitting beside the type and mutating it in
19//! place is the one arrangement in which *everything this profile does not name
20//! is untouched* is a fact rather than a claim. See
21//! [`TransportProfile::apply_to`] for the full statement, including the trap it
22//! leaves for a maintainer.
23//!
24//! # What is deliberately not a field
25//!
26//! There is no `enable_segmentation_offload`. Segmentation offload is turned
27//! off while a socket-level impairment is armed, because GSO hands the kernel
28//! one buffer to cut into many datagrams: the socket decorator then sees one
29//! send where the wire carries several, and loss, delay and rate accounting
30//! all count the wrong unit. A profile able to switch offload back on would
31//! let a configuration file undo that from a distance — in a file that says
32//! nothing about impairment — and the only symptom would be impairment
33//! figures that quietly disagree with what crossed the wire. The knob is
34//! absent, so there is nothing to undo it with.
35//!
36//! **That is the whole of the list, and it has to stay whole to be worth
37//! consulting.** A quinn knob that is neither a field above nor named here
38//! has not been ruled on at all, and a reader who comes here to find out
39//! why it is missing takes the silence for a decision — the one thing it
40//! cannot be. Carrying the knob and writing a paragraph here are the two
41//! ways to leave this section true; there is no third.
42//!
43//! # Installing one on a leg
44//!
45//! A profile is a value until a connection installs it. [`Leg`] names which
46//! of the proxy's two connections is being talked about, and
47//! [`TransportInstaller`] is the step that turns the profile into the
48//! `quinn::TransportConfig` that leg hands to quinn — [`DefaultInstaller`]
49//! when the caller supplies none. Both legs refuse to carry a raw
50//! `quinn::TransportConfig` *and* a profile at once, for the reason spelled
51//! out on [`crate::error::ProxyError::TransportConfigAndProfile`]: the
52//! merge that would appear to combine them cannot exist.
53//!
54//! Under the `qlog` feature a leg carries a third thing, a `qlog::QlogSpec`
55//! saying where its QUIC-level capture goes — plain code font because none
56//! of it exists in a build without the feature. A spec composes with a
57//! profile, which is applied to the same config the sink is attached to, and
58//! it composes with an installer too: [`TransportInstaller::build`] hands
59//! back an **owned** `quinn::TransportConfig`, so the sink is attached to
60//! the caller's own base afterwards and the three settings stack rather than
61//! one of them winning silently. A spec is still refused beside a raw
62//! config, for a reason of the same shape as the one above: a sink is
63//! installed by mutating a `quinn::TransportConfig`, and a raw config
64//! arrives behind an `Arc` that cannot be mutated. `resolve` below is where
65//! all of it is decided, once per leg and before any endpoint exists.
66//!
67//! # Validating
68//!
69//! [`TransportProfile::validate`] answers before any connection exists, and
70//! every rule it enforces is a case where quinn would otherwise accept a
71//! value and not honour it. That is the whole reason the type has a
72//! validator rather than just a set of setters: a transport parameter that
73//! is configured, reported as applied, and silently replaced by something
74//! else is indistinguishable from one that worked, and a run built on it is
75//! believed.
76
77use std::sync::Arc;
78use std::time::Duration;
79
80use quinn::congestion;
81use quinn::{AckFrequencyConfig, IdleTimeout, MtuDiscoveryConfig, VarInt};
82
83use crate::error::ProxyError;
84
85/// QUIC's guaranteed-deliverable UDP payload size, in bytes, and the floor
86/// that `quinn::TransportConfig::initial_mtu` and `min_mtu` silently raise
87/// any smaller value to.
88///
89/// Defined here rather than imported because quinn keeps its `INITIAL_MTU`
90/// private. The number is fixed by QUIC itself — the handshake establishes
91/// that the path carries an unfragmented 1200-byte datagram body, so nothing
92/// below it is a meaningful path MTU and quinn refuses to hold one.
93const QUIC_INITIAL_MTU: u16 = 1200;
94
95/// A per-leg description of QUIC transport parameters.
96///
97/// Every field is optional and every `None` means *leave this alone*. There
98/// is no field whose `None` is a value: a profile that sets three knobs is a
99/// profile about three knobs, and the other thirteen belong to whoever built
100/// the `quinn::TransportConfig` it is applied to.
101///
102/// `#[non_exhaustive]` **with** a [`Default`], as the shaping configs are:
103/// the attribute lets a later release add a seventeenth knob without a
104/// break, and outside this crate it makes both struct-expression and
105/// `..Default::default()` syntax illegal, so the `Default` is what leaves a
106/// construction path open at all. The documented way to build one is
107/// therefore `TransportProfile::default()` followed by field assignment,
108/// which is what the example does — it is the only path the attribute
109/// leaves, so it is the one worth proving.
110///
111/// ```
112/// use std::time::Duration;
113///
114/// use moqtap_proxy::transport::{Congestion, MtuDiscovery, TransportProfile};
115///
116/// let mut profile = TransportProfile::default();
117/// profile.congestion = Some(Congestion::Bbr);
118/// profile.initial_rtt = Some(Duration::from_millis(40));
119/// profile.receive_window = Some(8 * 1024 * 1024);
120/// profile.initial_mtu = Some(1350);
121/// profile.mtu_discovery = Some(MtuDiscovery::Off);
122///
123/// profile.validate()?;
124/// let config = profile.into_config()?;
125/// # let _ = config;
126/// # Ok::<(), moqtap_proxy::transport::TransportProfileError>(())
127/// ```
128#[derive(Debug, Clone, Default, PartialEq)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
131#[non_exhaustive]
132pub struct TransportProfile {
133    /// Which congestion controller to install.
134    ///
135    /// Each variant installs that controller's own default configuration.
136    /// The controllers behave very differently on a lossy path — BBR keeps
137    /// sending through loss that collapses a Cubic sender — so a run
138    /// comparing two of them wants this named explicitly rather than
139    /// inherited from whatever quinn's default happens to be that release.
140    pub congestion: Option<Congestion>,
141    /// The RTT to assume before a measurement exists.
142    ///
143    /// It decides the first retransmission timeout, so on a long path a
144    /// default that is far too low spends the opening exchange
145    /// retransmitting packets that were merely in flight.
146    pub initial_rtt: Option<Duration>,
147    /// Connection-wide flow-control window, in bytes.
148    ///
149    /// The cap on unacknowledged data across all streams. Too small for the
150    /// bandwidth-delay product and the sender stalls on flow control at a
151    /// throughput that has nothing to do with the congestion controller
152    /// under test.
153    pub receive_window: Option<u64>,
154    /// Per-stream flow-control window, in bytes.
155    ///
156    /// Held below [`TransportProfile::receive_window`] so that one slow
157    /// reader cannot monopolise the connection's receive buffers.
158    pub stream_receive_window: Option<u64>,
159    /// Cap on unacknowledged outgoing data, in bytes.
160    ///
161    /// The send-side counterpart of [`TransportProfile::receive_window`],
162    /// and the one window quinn takes as a plain `u64` rather than a QUIC
163    /// varint, so no range check applies to it.
164    pub send_window: Option<u64>,
165    /// How many unidirectional streams the peer may have open at once.
166    ///
167    /// MoQT carries media on unidirectional streams, so this is the ceiling
168    /// on concurrent subgroups; a value below what a subscription needs
169    /// shows up as senders blocked waiting for a stream credit rather than
170    /// as anything resembling congestion.
171    pub max_concurrent_uni_streams: Option<u64>,
172    /// How many bidirectional streams the peer may have open at once.
173    ///
174    /// **What this starves depends on the draft, and a profile is
175    /// installed on a leg before any version has been negotiated**, so it
176    /// cannot depend on which. Three answers over the range, each taken
177    /// from that draft's own Section 3.3:
178    ///
179    /// * Drafts 07 through 15 specify a single use of bidirectional
180    ///   streams, the control stream (draft-15 Section 3.3). A cap of zero
181    ///   there does not impair a session, it prevents one.
182    /// * Draft-16 specifies two, the control stream and
183    ///   SUBSCRIBE_NAMESPACE (draft-16 Section 3.3). It is the one draft
184    ///   on which a cap can starve something and leave the session
185    ///   running.
186    /// * Drafts 17 through 19 moved the control plane onto a pair of
187    ///   unidirectional streams and give bidirectional streams to
188    ///   requests alone — six message types on draft-17, seven on drafts
189    ///   18 and 19 (draft-17 Section 3.3). A cap there is the request-side
190    ///   counterpart of
191    ///   [`TransportProfile::max_concurrent_uni_streams`] on the media
192    ///   side, and it is the case this knob is carried for.
193    ///
194    /// So a value written without knowing which draft the run will
195    /// negotiate is a foot-gun rather than a setting, and that is a reason
196    /// to say so here rather than a reason to leave the field out.
197    pub max_concurrent_bidi_streams: Option<u64>,
198    /// How long a connection may sit idle before it is closed.
199    ///
200    /// The effective timeout is the smaller of this and the peer's own, so
201    /// setting it here only ever shortens the wait.
202    pub max_idle_timeout: Option<Duration>,
203    /// How often to send a packet purely to keep the connection alive.
204    ///
205    /// Must be strictly below [`TransportProfile::max_idle_timeout`] when
206    /// both are set — see
207    /// [`TransportProfileError::KeepAliveNotBelowIdle`].
208    pub keep_alive_interval: Option<Duration>,
209    /// How many packets may be acknowledged after a packet before it is
210    /// declared lost.
211    ///
212    /// The reordering tolerance of loss detection. Lowering it makes a
213    /// reordering path look like a lossy one, which is occasionally the
214    /// point and is otherwise a way to misread a run.
215    pub packet_threshold: Option<u32>,
216    /// Loss-detection time threshold, as a multiple of the round-trip
217    /// estimate.
218    ///
219    /// Must be finite and greater than `1.0`: it is a multiplier on the
220    /// RTT, so a value at or below one declares packets lost before an
221    /// acknowledgement could have arrived.
222    pub time_threshold: Option<f32>,
223    /// How many consecutive probe timeouts amount to persistent
224    /// congestion.
225    ///
226    /// quinn multiplies the probe timeout by this to get the window it
227    /// looks for entirely-lost packets in, and a path judged persistently
228    /// congested has its congestion window collapsed to the minimum.
229    /// Lowering it makes a sender give up on a bad path sooner.
230    ///
231    /// **Nothing in this crate demonstrates its effect.** Persistent
232    /// congestion is entered on a *duration* of losses, and no test here
233    /// asserts a duration, so a gate for this field could assert only that
234    /// a setter accepted the value. It ships under that stated limit,
235    /// which is a different thing from a knob accepted and ignored — the
236    /// same footing as [`TransportProfile::ack_frequency`].
237    pub persistent_congestion_threshold: Option<u32>,
238    /// Acknowledgement frequency to request of the peer.
239    ///
240    /// `None` leaves quinn's default, which is not to negotiate the
241    /// extension at all. `Some` asks for it, with the knobs in
242    /// [`AckFrequency`].
243    pub ack_frequency: Option<AckFrequency>,
244    /// The packet size to start with, in bytes.
245    ///
246    /// Must be at least 1200 — see
247    /// [`TransportProfileError::MtuBelowFloor`].
248    pub initial_mtu: Option<u16>,
249    /// The packet size never to go below, in bytes, after black-hole
250    /// detection has lowered the discovered MTU.
251    ///
252    /// Must be at least 1200, and no larger than
253    /// [`TransportProfile::initial_mtu`].
254    pub min_mtu: Option<u16>,
255    /// Whether to search for a larger path MTU, and how far.
256    ///
257    /// quinn's default is to search, so `None` here means *keep searching*
258    /// and [`MtuDiscovery::Off`] is the only way to stop it. The two are
259    /// deliberately distinguishable.
260    pub mtu_discovery: Option<MtuDiscovery>,
261    /// Whether to share send capacity fairly between streams rather than
262    /// draining them in priority order.
263    ///
264    /// It changes which subgroup arrives first when several are ready at
265    /// once, which is visible in delivery order and not in any counter.
266    pub send_fairness: Option<bool>,
267    /// How much room to give incoming QUIC datagrams, in bytes.
268    ///
269    /// [`DatagramBuffer::Disabled`] refuses datagrams outright, which is a
270    /// different thing from leaving the field unset — see
271    /// [`DatagramBuffer`] for why that distinction has a type rather than a
272    /// nested `Option`.
273    pub datagram_receive_buffer: Option<DatagramBuffer>,
274}
275
276impl TransportProfile {
277    /// Everything wrong with this profile, before any connection exists.
278    ///
279    /// Returns the **first** failure, checked in field-declaration order, as
280    /// the other validators in this workspace do: a profile with two
281    /// mistakes reports the earlier field, and fixing it reveals the second.
282    /// The order is fixed so the answer is repeatable rather than dependent
283    /// on which check happened to be written first.
284    ///
285    /// Two deliberate departures from strict declaration order, both because
286    /// the more useful thing to be told comes first:
287    ///
288    /// * [`TransportProfileError::KeepAliveNotBelowIdle`] is checked at
289    ///   `keep_alive_interval`, the later of the two fields it compares, so
290    ///   that the earlier field has already had its own range check.
291    /// * Both MTU floors are checked before
292    ///   [`TransportProfileError::MtuInverted`]. A value below the floor is
293    ///   a single-field fault with a single-field fix, and once both values
294    ///   are legal the inversion may not exist any more.
295    ///
296    /// # What is not checked, and why
297    ///
298    /// The idle timeout has no error variant of its own. `IdleTimeout`
299    /// converts from a `Duration` through `as_millis` against the same
300    /// varint ceiling as the windows, which puts the limit around 146
301    /// million years — no configuration file reaches it, and an error a
302    /// reader can never meet is worse than no error at all. The conversion
303    /// is nevertheless fallible in Rust, because `Duration::from_secs(u64::MAX)`
304    /// exists, so it is folded into
305    /// [`TransportProfileError::VarIntRange`] under the field name
306    /// `max_idle_timeout`. That keeps the path free of a panic without
307    /// adding a rule to the list an author has to read.
308    pub fn validate(&self) -> Result<(), TransportProfileError> {
309        // `congestion` and `initial_rtt` have no invalid values: every
310        // controller is installable and every `Duration` is an assumable
311        // round trip.
312        if let Some(bytes) = self.receive_window {
313            varint("receive_window", bytes)?;
314        }
315        if let Some(bytes) = self.stream_receive_window {
316            varint("stream_receive_window", bytes)?;
317        }
318        // `send_window` takes a plain `u64`, so it has no varint ceiling.
319        if let Some(count) = self.max_concurrent_uni_streams {
320            varint("max_concurrent_uni_streams", count)?;
321        }
322        if let Some(count) = self.max_concurrent_bidi_streams {
323            varint("max_concurrent_bidi_streams", count)?;
324        }
325        if let Some(idle) = self.max_idle_timeout {
326            idle_timeout(idle)?;
327        }
328        if let (Some(keep_alive), Some(idle)) = (self.keep_alive_interval, self.max_idle_timeout) {
329            // A keep-alive at or above the idle timeout cannot prevent the
330            // timeout it exists to prevent: the connection is already gone
331            // when the packet that would have saved it is due.
332            if keep_alive >= idle {
333                return Err(TransportProfileError::KeepAliveNotBelowIdle { keep_alive, idle });
334            }
335        }
336        // `packet_threshold` and `persistent_congestion_threshold` are plain
337        // counts with no ceiling to exceed, and quinn honours every `u32`
338        // either is given. Zero included: it makes an extremely eager loss
339        // detector rather than an ignored setting, which is the distinction
340        // that decides whether a rule belongs here.
341        if let Some(threshold) = self.time_threshold {
342            // It is a multiplier on the round-trip estimate, so anything at
343            // or below 1.0 declares a packet lost before an acknowledgement
344            // for it could have arrived, and a non-finite value is not a
345            // multiplier at all.
346            if !threshold.is_finite() || threshold <= 1.0 {
347                return Err(TransportProfileError::TimeThreshold(threshold));
348            }
349        }
350        if let Some(ack) = &self.ack_frequency {
351            ack.validate()?;
352        }
353        if let Some(mtu) = self.initial_mtu {
354            mtu_floor("initial_mtu", mtu)?;
355        }
356        if let Some(mtu) = self.min_mtu {
357            mtu_floor("min_mtu", mtu)?;
358        }
359        if let (Some(min), Some(initial)) = (self.min_mtu, self.initial_mtu) {
360            // `min_mtu` is the floor discovery may fall back to and
361            // `initial_mtu` is where it starts; a floor above the start is
362            // a range with nothing in it.
363            if min > initial {
364                return Err(TransportProfileError::MtuInverted { min, initial });
365            }
366        }
367        // `mtu_discovery`, `send_fairness` and `datagram_receive_buffer`
368        // have no invalid values: every variant and every bool is a
369        // configuration quinn honours as written.
370        Ok(())
371    }
372
373    /// Write this profile's fields into `tc`, leaving every field it does not
374    /// set untouched.
375    ///
376    /// # Why this takes `&mut` and returns nothing
377    /// `quinn::TransportConfig` has exactly three impls — the inherent setters,
378    /// `Default` and `Debug`. There is no `Clone`, and every field is private
379    /// with no getter. So there is no way to write `fn apply(&self, base:
380    /// &TransportConfig) -> TransportConfig`: the function cannot copy `base`
381    /// and cannot read a single value out of it, so the only thing it could
382    /// return is a fresh default with the caller's configuration silently
383    /// thrown away. Mutating in place is the one shape in which *leaves the
384    /// rest untouched* is true rather than merely claimed.
385    ///
386    /// The trap this leaves for whoever maintains it: `base.clone()`
387    /// **compiles**. `&TransportConfig` is `Clone` even though
388    /// `TransportConfig` is not, so the call clones the reference and the
389    /// mistake only surfaces at the return, as "`TransportConfig` does not
390    /// implement `Clone`, so `&TransportConfig` was cloned instead". Anyone
391    /// who reaches for the by-value signature will meet that message and
392    /// should read it as the reason this signature is what it is.
393    ///
394    /// # All or nothing
395    ///
396    /// The first statement is `self.validate()?`, and that is the whole of
397    /// how this method is kept from installing something `validate` would
398    /// have rejected — there is no second list of rules to drift out of step
399    /// with the first, and no field-by-field reading to do to check it. It
400    /// also means `tc` is either fully written or not written at all: a
401    /// profile that fails returns before the first setter runs, so a caller
402    /// who ignores the error is not left with a half-applied config.
403    ///
404    /// The conversions below re-run the fallible steps with `?` rather than
405    /// unwrapping them. They cannot fail after `validate` has passed, but
406    /// expressing that as a panic would make a future divergence between the
407    /// two lists into a crash instead of an error.
408    pub fn apply_to(&self, tc: &mut quinn::TransportConfig) -> Result<(), TransportProfileError> {
409        self.validate()?;
410
411        if let Some(controller) = self.congestion {
412            tc.congestion_controller_factory(controller.factory());
413        }
414        if let Some(rtt) = self.initial_rtt {
415            tc.initial_rtt(rtt);
416        }
417        if let Some(bytes) = self.receive_window {
418            tc.receive_window(varint("receive_window", bytes)?);
419        }
420        if let Some(bytes) = self.stream_receive_window {
421            tc.stream_receive_window(varint("stream_receive_window", bytes)?);
422        }
423        if let Some(bytes) = self.send_window {
424            tc.send_window(bytes);
425        }
426        if let Some(count) = self.max_concurrent_uni_streams {
427            tc.max_concurrent_uni_streams(varint("max_concurrent_uni_streams", count)?);
428        }
429        if let Some(count) = self.max_concurrent_bidi_streams {
430            tc.max_concurrent_bidi_streams(varint("max_concurrent_bidi_streams", count)?);
431        }
432        if let Some(idle) = self.max_idle_timeout {
433            tc.max_idle_timeout(Some(IdleTimeout::from(idle_timeout(idle)?)));
434        }
435        if let Some(interval) = self.keep_alive_interval {
436            tc.keep_alive_interval(Some(interval));
437        }
438        if let Some(threshold) = self.packet_threshold {
439            tc.packet_threshold(threshold);
440        }
441        if let Some(threshold) = self.time_threshold {
442            tc.time_threshold(threshold);
443        }
444        if let Some(threshold) = self.persistent_congestion_threshold {
445            tc.persistent_congestion_threshold(threshold);
446        }
447        if let Some(ack) = &self.ack_frequency {
448            tc.ack_frequency_config(Some(ack.to_config()?));
449        }
450        if let Some(mtu) = self.initial_mtu {
451            tc.initial_mtu(mtu);
452        }
453        if let Some(mtu) = self.min_mtu {
454            tc.min_mtu(mtu);
455        }
456        if let Some(discovery) = self.mtu_discovery {
457            tc.mtu_discovery_config(discovery.to_config());
458        }
459        if let Some(fair) = self.send_fairness {
460            tc.send_fairness(fair);
461        }
462        if let Some(buffer) = self.datagram_receive_buffer {
463            tc.datagram_receive_buffer_size(buffer.to_size());
464        }
465
466        Ok(())
467    }
468
469    /// A fresh config carrying only this profile.
470    ///
471    /// Equivalent to [`TransportProfile::apply_to`] over a
472    /// `quinn::TransportConfig::default()`, and defined that way rather than
473    /// duplicated, so the two can only ever accept and refuse the same
474    /// profiles. Use it for a leg with no configuration of its own; use
475    /// `apply_to` for a leg that already has one.
476    pub fn into_config(&self) -> Result<quinn::TransportConfig, TransportProfileError> {
477        let mut tc = quinn::TransportConfig::default();
478        self.apply_to(&mut tc)?;
479        Ok(tc)
480    }
481}
482
483/// The congestion controller to install.
484///
485/// Deliberately **not** `#[non_exhaustive]`. `tests/transport_exhaustive.rs`
486/// compiles as its own crate and matches this enum with no `_` arm, which is
487/// legal only while the attribute is absent; the attribute would force the
488/// wildcard in, and after that a fourth controller compiles green out there
489/// with nobody told it is unhandled. What the test can check is that every
490/// variant named here is matchable, constructible and installable by a
491/// downstream consumer, so adding one is a visible break rather than a
492/// silent widening. What no test can check is the other direction: whether
493/// quinn has grown a controller this list has never heard of. That stays a
494/// reading of quinn's `congestion` module whenever the dependency moves.
495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
496#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
497#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
498pub enum Congestion {
499    /// CUBIC, quinn's default: loss-based, and the controller most of the
500    /// internet is running.
501    Cubic,
502    /// BBR: rate-based, and the interesting one against an impaired path,
503    /// because it keeps sending through loss that collapses a loss-based
504    /// sender.
505    Bbr,
506    /// NewReno: the textbook loss-based controller, useful as a slow,
507    /// predictable baseline.
508    NewReno,
509}
510
511impl Congestion {
512    /// The factory quinn wants, boxed as the trait object its setter takes.
513    ///
514    /// Each variant carries that controller's own default configuration.
515    /// Exposing the individual controller knobs would be a second
516    /// configuration surface with its own validation, and none of it is
517    /// serializable.
518    fn factory(self) -> Arc<dyn congestion::ControllerFactory + Send + Sync + 'static> {
519        match self {
520            Self::Cubic => Arc::new(congestion::CubicConfig::default()),
521            Self::Bbr => Arc::new(congestion::BbrConfig::default()),
522            Self::NewReno => Arc::new(congestion::NewRenoConfig::default()),
523        }
524    }
525}
526
527/// Acknowledgement frequency to request of the peer.
528///
529/// A serializable mirror of quinn's `AckFrequencyConfig`, which has private
530/// fields, no getters and no serde support, so it cannot itself appear in a
531/// profile. The three fields are the three knobs that type exposes, and the
532/// [`Default`] is **hand-written to equal quinn's own defaults** rather than
533/// derived: a derived one would give an ack-eliciting threshold of zero,
534/// which asks the peer to acknowledge every single packet, and a reordering
535/// threshold of zero, which asks it never to acknowledge reordering
536/// promptly. Neither is a sensible starting point, and both would arrive
537/// silently in any file that named one field and left the others out.
538///
539/// With this `Default`, `Some(AckFrequency::default())` means exactly what
540/// `Some(AckFrequencyConfig::default())` means in quinn: negotiate the
541/// extension, with its recommended values.
542///
543/// `#[non_exhaustive]`, so outside this crate one is built as
544/// `AckFrequency::default()` followed by field assignment — the attribute
545/// makes both the struct expression and `..Default::default()` illegal
546/// there. Kept rather than dropped because it and the `Default` above are
547/// one mechanism: this type mirrors an upstream config that gains a knob
548/// from time to time, and the attribute guarantees that every construction
549/// still reachable starts from the hand-written `Default`. A fourth field
550/// added in a later release therefore arrives carrying quinn's recommended
551/// value in code that was written before it existed, instead of the zero a
552/// struct expression would have left there — and zero, for both thresholds
553/// here, is a request the peer will honour and nobody meant to make.
554#[derive(Debug, Clone, PartialEq, Eq)]
555#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
556#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
557#[non_exhaustive]
558pub struct AckFrequency {
559    /// How many ack-eliciting packets the peer may receive before it must
560    /// send an acknowledgement.
561    ///
562    /// Zero asks it to acknowledge every one. Defaults to 1, which is
563    /// quinn's own default and acknowledges every other packet.
564    pub ack_eliciting_threshold: u64,
565    /// The longest the peer may wait before acknowledging, when the
566    /// threshold above has not been reached.
567    ///
568    /// `None` leaves the peer's own advertised `max_ack_delay` in place,
569    /// which is quinn's default and is why `None` is not ambiguous here.
570    pub max_ack_delay: Option<Duration>,
571    /// How far out of order a packet may arrive before the peer must
572    /// acknowledge immediately.
573    ///
574    /// Zero asks it never to. Defaults to 2, which is quinn's own default
575    /// and one below the default packet threshold, as quinn recommends.
576    pub reordering_threshold: u64,
577}
578
579impl Default for AckFrequency {
580    fn default() -> Self {
581        Self { ack_eliciting_threshold: 1, max_ack_delay: None, reordering_threshold: 2 }
582    }
583}
584
585impl AckFrequency {
586    /// The two varint-valued thresholds, checked against the QUIC varint
587    /// ceiling before they can fail at connect time.
588    ///
589    /// Field names are reported dotted — `ack_frequency.reordering_threshold`
590    /// — because `reordering_threshold` on its own would not tell a reader
591    /// which part of the file to look at.
592    fn validate(&self) -> Result<(), TransportProfileError> {
593        varint("ack_frequency.ack_eliciting_threshold", self.ack_eliciting_threshold)?;
594        varint("ack_frequency.reordering_threshold", self.reordering_threshold)?;
595        Ok(())
596    }
597
598    /// Build quinn's config from this mirror.
599    fn to_config(&self) -> Result<AckFrequencyConfig, TransportProfileError> {
600        let mut config = AckFrequencyConfig::default();
601        config.ack_eliciting_threshold(varint(
602            "ack_frequency.ack_eliciting_threshold",
603            self.ack_eliciting_threshold,
604        )?);
605        config.max_ack_delay(self.max_ack_delay);
606        config.reordering_threshold(varint(
607            "ack_frequency.reordering_threshold",
608            self.reordering_threshold,
609        )?);
610        Ok(config)
611    }
612}
613
614/// Whether to search for a larger path MTU, and how far.
615///
616/// quinn's default is to search, so an unset
617/// [`TransportProfile::mtu_discovery`] means discovery stays **on**.
618/// [`MtuDiscovery::Off`] is the only way to say otherwise, and it has to be
619/// expressible separately from unset: turning discovery off is a real choice
620/// for a run that wants the packet size it configured to be the packet size
621/// it gets, rather than the start of a binary search.
622#[derive(Debug, Clone, Copy, PartialEq, Eq)]
623#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
624#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
625pub enum MtuDiscovery {
626    /// Do not search. The packet size stays where `initial_mtu` put it.
627    Off,
628    /// Search, but no higher than this many bytes.
629    ///
630    /// Everything else about the search — interval, minimum change,
631    /// black-hole cooldown — stays at quinn's defaults.
632    UpTo(u16),
633}
634
635impl MtuDiscovery {
636    /// The value quinn's `mtu_discovery_config` setter takes, where `None`
637    /// genuinely disables discovery rather than meaning "unchanged".
638    fn to_config(self) -> Option<MtuDiscoveryConfig> {
639        match self {
640            Self::Off => None,
641            Self::UpTo(bytes) => {
642                let mut config = MtuDiscoveryConfig::default();
643                config.upper_bound(bytes);
644                Some(config)
645            }
646        }
647    }
648}
649
650/// How much room to give incoming QUIC datagrams.
651///
652/// This exists instead of `Option<Option<usize>>`, and the nested option is not
653/// a matter of taste. Written down, the outer `None` and the inner `None` are
654/// the same three characters: `{`datagram_receive_buffer`: null}` in a
655/// hand-written file deserializes to the **outer** `None`, so an author who
656/// wrote it to disable datagram reception silently gets "change nothing", and
657/// their datagrams keep arriving. `deny_unknown_fields` cannot catch it — the
658/// key is well known and the value is well typed. Naming the two answers makes
659/// them impossible to confuse.
660#[derive(Debug, Clone, Copy, PartialEq, Eq)]
661#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
662#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
663pub enum DatagramBuffer {
664    /// Refuse incoming datagrams entirely.
665    Disabled,
666    /// Accept incoming datagrams, buffering up to this many bytes.
667    Bytes(usize),
668}
669
670impl DatagramBuffer {
671    /// The value quinn's `datagram_receive_buffer_size` setter takes, where
672    /// `None` disables datagram reception.
673    fn to_size(self) -> Option<usize> {
674        match self {
675            Self::Disabled => None,
676            Self::Bytes(bytes) => Some(bytes),
677        }
678    }
679}
680
681/// Why a [`TransportProfile`] cannot be honoured.
682///
683/// No `Eq`: [`TransportProfileError::TimeThreshold`] carries an `f32`, and
684/// the one value that most wants reporting — `NaN` — is not equal to
685/// itself. `PartialEq` is what an `f32` admits, and it is enough for a test
686/// to compare a returned error against an expected one.
687///
688/// Deliberately **not** `#[non_exhaustive]`: `tests/transport_exhaustive.rs`
689/// builds one profile per variant, calls [`TransportProfile::validate`], and
690/// matches what comes back with no `_` arm — a match a crate outside this
691/// one can only write while the attribute is absent. A sixth variant fails
692/// that build until a profile someone could actually write is shown to reach
693/// it, which is the check worth having: not that the variant exists, but
694/// that it is a refusal an author can trip over and therefore fix. The
695/// attribute would replace all of that with a wildcard arm that silently
696/// accepts anything.
697#[derive(Debug, Clone, PartialEq, thiserror::Error)]
698pub enum TransportProfileError {
699    /// A value above `2^62 - 1`, which is the largest number QUIC's variable
700    /// length integer encoding can carry.
701    ///
702    /// Caught here rather than at connect time, where quinn returns it
703    /// without naming a field and the leg carries on with a default the
704    /// author never asked for.
705    #[error("{field} = {value} exceeds the QUIC varint range")]
706    VarIntRange {
707        /// The profile field holding the oversized value, spelled as the
708        /// field is, and dotted for a field inside [`AckFrequency`].
709        field: &'static str,
710        /// The value that does not fit.
711        value: u64,
712    },
713    /// A keep-alive interval at or above the idle timeout.
714    ///
715    /// Such a keep-alive cannot prevent the timeout it exists to prevent:
716    /// the connection has already been closed by the time the packet that
717    /// would have saved it is due.
718    #[error("keep_alive_interval {keep_alive:?} must be below max_idle_timeout {idle:?}")]
719    KeepAliveNotBelowIdle {
720        /// The configured keep-alive interval.
721        keep_alive: Duration,
722        /// The idle timeout it fails to stay below.
723        idle: Duration,
724    },
725    /// A minimum MTU above the initial MTU.
726    ///
727    /// The minimum is the floor MTU discovery may fall back to and the
728    /// initial is where it starts, so a floor above the start describes a
729    /// range with nothing in it.
730    #[error("min_mtu {min} is above initial_mtu {initial}")]
731    MtuInverted {
732        /// The configured minimum MTU.
733        min: u16,
734        /// The initial MTU it exceeds.
735        initial: u16,
736    },
737    /// An MTU below the 1200 bytes QUIC guarantees.
738    ///
739    /// quinn's `initial_mtu` and `min_mtu` setters both raise a smaller
740    /// value to 1200 without a word, so a profile modelling a constrained
741    /// path at 900 bytes validates, applies, reports applied, and runs at
742    /// 1200. Refused here, naming the field, because a configured value that
743    /// is quietly replaced is the failure this crate exists to make
744    /// impossible.
745    #[error("{field} = {value} is below QUIC's {floor}-byte floor; quinn would silently raise it")]
746    MtuBelowFloor {
747        /// Which of `initial_mtu` or `min_mtu` holds the value.
748        field: &'static str,
749        /// The value that would have been raised.
750        value: u16,
751        /// The floor it is below, which is always 1200.
752        floor: u16,
753    },
754    /// A loss-detection time threshold that is not a usable multiplier.
755    ///
756    /// It multiplies the round-trip estimate, so at or below `1.0` it
757    /// declares a packet lost before an acknowledgement for it could have
758    /// arrived, and a non-finite value is not a multiplier at all.
759    #[error("time_threshold {0} must be finite and greater than 1.0")]
760    TimeThreshold(f32),
761}
762
763/// Convert to a QUIC varint, naming the field if it does not fit.
764///
765/// The single place the `u64`-to-`VarInt` conversion happens, so
766/// [`TransportProfile::validate`] and [`TransportProfile::apply_to`] cannot
767/// disagree about which values are acceptable.
768fn varint(field: &'static str, value: u64) -> Result<VarInt, TransportProfileError> {
769    VarInt::from_u64(value).map_err(|_| TransportProfileError::VarIntRange { field, value })
770}
771
772/// Convert an idle timeout to the varint of milliseconds quinn stores.
773///
774/// The saturation matters only for the error message: a `Duration` whose
775/// millisecond count does not fit a `u64` is already unimaginably past the
776/// varint ceiling, and reporting `u64::MAX` says so as well as the true
777/// figure would while keeping the error's `value` field a `u64`.
778fn idle_timeout(idle: Duration) -> Result<VarInt, TransportProfileError> {
779    let millis = u64::try_from(idle.as_millis()).unwrap_or(u64::MAX);
780    varint("max_idle_timeout", millis)
781}
782
783/// Refuse an MTU quinn would silently raise.
784///
785/// Shared by both MTU fields so the floor is written once; the field name is
786/// passed in because the error has to say which one.
787fn mtu_floor(field: &'static str, value: u16) -> Result<(), TransportProfileError> {
788    if value < QUIC_INITIAL_MTU {
789        return Err(TransportProfileError::MtuBelowFloor { field, value, floor: QUIC_INITIAL_MTU });
790    }
791    Ok(())
792}
793
794pub use crate::types::Leg;
795
796// ── Installing a profile on a leg ───────────────────────────────────────
797
798/// Builds the `quinn::TransportConfig` a leg installs.
799///
800/// A leg with a [`TransportProfile`] and no installer of its own uses
801/// [`DefaultInstaller`], so supplying one replaces exactly one step and
802/// nothing else: the leg still installs whatever comes back, still installs
803/// it before its endpoint exists, and still refuses a leg that names a raw
804/// `quinn::TransportConfig` as well as a profile.
805///
806/// # What this is for: a base configuration *and* a profile
807///
808/// A leg takes a raw config or a profile, never both — see
809/// [`ProxyError::TransportConfigAndProfile`], which is where the reason is
810/// written out. The short form is that
811/// `quinn::TransportConfig` can be neither cloned nor read back, so no code
812/// here can accept a caller's config and return a modified copy of it.
813///
814/// An installer is how a caller has both anyway, and it works because it
815/// **builds** the base rather than being handed one: `build` constructs its
816/// own `quinn::TransportConfig`, applies the profile over it with
817/// [`TransportProfile::apply_to`], and returns the result. Nothing is
818/// copied, so nothing is silently dropped, and the caller's own settings
819/// survive because the caller is the one making them.
820///
821/// `Send + Sync + 'static` because one installer serves every connection a
822/// leg carries, for as long as the proxy runs, from whichever task accepts
823/// them.
824pub trait TransportInstaller: Send + Sync + 'static {
825    /// Turn `profile` into the config this leg will install.
826    ///
827    /// An error refuses the connection instead of falling back to a
828    /// default. A leg that connected anyway would be running with
829    /// parameters nobody chose while reporting success, which is the one
830    /// outcome every rule in this module exists to prevent.
831    ///
832    /// # Owned, not `Arc`
833    ///
834    /// The return type is a plain `quinn::TransportConfig` and the reason is
835    /// what the caller may still need to do to it. A QUIC-level capture sink
836    /// is installed by **mutating** a `quinn::TransportConfig`, and an `Arc`
837    /// that may already be shared cannot be mutated — `Arc::get_mut` hands
838    /// back nothing the moment a second handle exists. An installer that
839    /// returned one would therefore be unusable on any leg that also asked
840    /// for a capture, and the only way to keep such a leg working would be
841    /// to skip the installer: a caller who supplied one would find it never
842    /// called, with nothing saying so. Handing back the value means the leg
843    /// can attach whatever else it owes to it and every setting survives.
844    ///
845    /// The leg wraps the result in an `Arc` itself, once, after it has
846    /// finished with it. An implementation that has an `Arc` already should
847    /// build a fresh config rather than trying to unwrap one — that is the
848    /// same rebuild-per-leg this trait exists for.
849    fn build(
850        &self,
851        profile: &TransportProfile,
852    ) -> Result<quinn::TransportConfig, TransportProfileError>;
853}
854
855/// The installer a leg uses when it was given none.
856///
857/// [`TransportProfile::into_config`] and deliberately nothing more. Every
858/// field the profile does not name is therefore
859/// `quinn::TransportConfig::default()`'s — quinn's own choice rather than
860/// one this crate invented and would have to keep in step with a
861/// dependency upgrade.
862#[derive(Debug, Clone, Copy, Default)]
863pub struct DefaultInstaller;
864
865impl TransportInstaller for DefaultInstaller {
866    fn build(
867        &self,
868        profile: &TransportProfile,
869    ) -> Result<quinn::TransportConfig, TransportProfileError> {
870        profile.into_config()
871    }
872}
873
874/// What a leg installs, from the fields a caller may have set and the
875/// installer it may have supplied.
876///
877/// `None` back means the leg installs nothing and quinn's defaults apply,
878/// which is the case that has to stay bit-identical to the behaviour from
879/// before profiles existed.
880///
881/// Called once per leg, **before** the endpoint is built, so every refusal
882/// below costs a caller no socket, no handshake and no connection to tear
883/// down — and none of them can be mistaken for a network fault, which is
884/// what the same error arriving mid-connection would look like.
885///
886/// An installer with no profile beside it is not consulted: `build` takes a
887/// profile and there is none to give it. That is the one inert combination
888/// here, and it is called out on both `installer` fields rather than left
889/// for a caller to discover from a run where nothing happened.
890///
891/// # A spec changes what three of those cases install
892///
893/// Under the `qlog` feature a leg may also carry a `qlog::QlogSpec` — named
894/// in plain code font here, as the variants below are, because none of it
895/// exists in a build without the feature and a link from this
896/// always-compiled item would not resolve. A sink can only be installed by
897/// mutating a `quinn::TransportConfig` this function still holds by value,
898/// and that single fact decides all four combinations:
899///
900/// * **Raw config alone** — installed as it was given, exactly as before.
901/// * **Raw config and a spec** — `ProxyError::TransportConfigAndQlog`,
902///   because the config arrives behind an `Arc` that can be neither cloned
903///   nor mutated. The variant carries the whole reason.
904/// * **Profile and a spec** — the leg's [`TransportInstaller`] builds the
905///   config, exactly as it does for a profile with no spec beside it, and
906///   the sink is attached to what it returned. The three compose because
907///   `build` hands back an owned `quinn::TransportConfig` rather than an
908///   `Arc`: the base is the caller's, the profile is applied over it by the
909///   installer, and the sink goes on last. A leg with no installer of its
910///   own gets [`DefaultInstaller`]'s base, which is
911///   `quinn::TransportConfig::default()`.
912/// * **Spec alone** — still a fresh `quinn::TransportConfig` with the sink
913///   on it, and the leg installs it. This is the case worth being careful
914///   about: a leg that named only a spec used to install nothing, and
915///   installing nothing here would leave the commonest way of asking for a
916///   capture producing no capture and no error.
917///
918/// A spec with no writer never reaches an endpoint: `attach_to` validates
919/// before it builds, so `QlogError::NoWriter` is answered here, and by the
920/// time a connection exists the sink is one quinn actually returned rather
921/// than the silent `None` it answers a writer-less configuration with.
922pub(crate) fn resolve(
923    leg: Leg,
924    raw: Option<Arc<quinn::TransportConfig>>,
925    profile: Option<&TransportProfile>,
926    installer: Option<&Arc<dyn TransportInstaller>>,
927    #[cfg(feature = "qlog")] qlog: Option<crate::qlog::QlogSpec>,
928) -> Result<Option<Arc<quinn::TransportConfig>>, ProxyError> {
929    match (raw, profile) {
930        // Checked first, and before the spec is looked at, so a leg that
931        // named all three hears about this pair. It is the older rule and
932        // the one whose fix — apply the profile to your own config — also
933        // resolves the other, so reporting it first sends the caller
934        // somewhere useful either way.
935        (Some(_), Some(_)) => Err(ProxyError::TransportConfigAndProfile { leg }),
936        (Some(config), None) => {
937            #[cfg(feature = "qlog")]
938            if qlog.is_some() {
939                return Err(ProxyError::TransportConfigAndQlog { leg });
940            }
941            Ok(Some(config))
942        }
943        (None, Some(profile)) => {
944            // One build, whether or not a capture was asked for. The
945            // installer is the leg's single source of a base config, so a
946            // spec cannot quietly move the leg onto a different one.
947            #[cfg_attr(not(feature = "qlog"), allow(unused_mut))]
948            let mut config = match installer {
949                Some(installer) => installer.build(profile),
950                None => DefaultInstaller.build(profile),
951            }
952            .map_err(|source| ProxyError::TransportProfile { leg, source })?;
953            #[cfg(feature = "qlog")]
954            if let Some(spec) = qlog {
955                spec.attach_to(&mut config).map_err(|source| ProxyError::Qlog { leg, source })?;
956            }
957            Ok(Some(Arc::new(config)))
958        }
959        (None, None) => {
960            #[cfg(feature = "qlog")]
961            if let Some(spec) = qlog {
962                // The one arm that installs a config out of nothing. A leg
963                // asking only for a capture is the commonest way to ask for
964                // one at all, and answering it with `None` would leave the
965                // sink attached to a config nobody installed — a file
966                // holding a preamble and never an event.
967                let mut config = quinn::TransportConfig::default();
968                spec.attach_to(&mut config).map_err(|source| ProxyError::Qlog { leg, source })?;
969                return Ok(Some(Arc::new(config)));
970            }
971            Ok(None)
972        }
973    }
974}
975
976#[cfg(test)]
977mod tests {
978    use super::*;
979
980    /// A profile with every field set and every value sane.
981    ///
982    /// Used as the starting point for the refusal tests, so each of them
983    /// changes exactly one field. A refusal test built from
984    /// `TransportProfile::default()` would leave the other seventeen fields at
985    /// `None` and would still pass against a `validate` that ignored them.
986    fn healthy() -> TransportProfile {
987        // Every field named, with no `..Default::default()`: adding a
988        // nineteenth knob should break this fixture, because a fixture that
989        // silently leaves the new field at `None` stops being the
990        // fully-populated control it is used as.
991        TransportProfile {
992            congestion: Some(Congestion::Bbr),
993            initial_rtt: Some(Duration::from_millis(40)),
994            receive_window: Some(8 * 1024 * 1024),
995            stream_receive_window: Some(1024 * 1024),
996            send_window: Some(8 * 1024 * 1024),
997            max_concurrent_uni_streams: Some(256),
998            max_concurrent_bidi_streams: Some(16),
999            max_idle_timeout: Some(Duration::from_secs(30)),
1000            keep_alive_interval: Some(Duration::from_secs(5)),
1001            packet_threshold: Some(3),
1002            time_threshold: Some(1.125),
1003            persistent_congestion_threshold: Some(3),
1004            ack_frequency: Some(AckFrequency::default()),
1005            initial_mtu: Some(1350),
1006            min_mtu: Some(1200),
1007            mtu_discovery: Some(MtuDiscovery::UpTo(1452)),
1008            send_fairness: Some(true),
1009            datagram_receive_buffer: Some(DatagramBuffer::Bytes(64 * 1024)),
1010        }
1011    }
1012
1013    /// The largest value QUIC's varint encoding carries, and the first one
1014    /// above it.
1015    const VARINT_MAX: u64 = (1 << 62) - 1;
1016
1017    #[test]
1018    fn a_profile_that_sets_nothing_validates() {
1019        assert_eq!(
1020            TransportProfile::default().validate(),
1021            Ok(()),
1022            "an all-`None` profile has no opinion to be wrong about"
1023        );
1024    }
1025
1026    #[test]
1027    fn a_fully_populated_healthy_profile_is_accepted() {
1028        assert_eq!(
1029            healthy().validate(),
1030            Ok(()),
1031            "the control profile must be valid or every refusal below is unattributable"
1032        );
1033    }
1034
1035    #[test]
1036    fn a_value_above_the_varint_ceiling_is_refused_and_names_its_field() {
1037        // One row per field that goes through a varint setter. Five fields,
1038        // five rows, and the expected errors differ in the field name, so a
1039        // `validate` that reported one fixed name cannot pass.
1040        type Edit = fn(&mut TransportProfile);
1041
1042        let rows: [(&str, Edit, &str); 5] = [
1043            ("connection window", |p| p.receive_window = Some(VARINT_MAX + 1), "receive_window"),
1044            (
1045                "stream window",
1046                |p| p.stream_receive_window = Some(VARINT_MAX + 1),
1047                "stream_receive_window",
1048            ),
1049            (
1050                "uni stream count",
1051                |p| p.max_concurrent_uni_streams = Some(VARINT_MAX + 1),
1052                "max_concurrent_uni_streams",
1053            ),
1054            (
1055                "bidi stream count",
1056                |p| p.max_concurrent_bidi_streams = Some(VARINT_MAX + 1),
1057                "max_concurrent_bidi_streams",
1058            ),
1059            (
1060                "ack-eliciting threshold",
1061                |p| {
1062                    p.ack_frequency = Some(AckFrequency {
1063                        ack_eliciting_threshold: VARINT_MAX + 1,
1064                        ..Default::default()
1065                    });
1066                },
1067                "ack_frequency.ack_eliciting_threshold",
1068            ),
1069        ];
1070
1071        for (label, edit, field) in rows {
1072            let mut profile = healthy();
1073            edit(&mut profile);
1074            assert_eq!(
1075                profile.validate(),
1076                Err(TransportProfileError::VarIntRange { field, value: VARINT_MAX + 1 }),
1077                "{label}"
1078            );
1079        }
1080
1081        // The positive control for the whole table: the ceiling itself fits,
1082        // so none of the rows above is passing because the fixture was
1083        // already invalid.
1084        let mut profile = healthy();
1085        profile.receive_window = Some(VARINT_MAX);
1086        profile.stream_receive_window = Some(VARINT_MAX);
1087        profile.max_concurrent_uni_streams = Some(VARINT_MAX);
1088        profile.max_concurrent_bidi_streams = Some(VARINT_MAX);
1089        assert_eq!(profile.validate(), Ok(()), "exactly the ceiling is accepted");
1090    }
1091
1092    #[test]
1093    fn the_reordering_threshold_is_checked_under_its_own_dotted_name() {
1094        let mut profile = healthy();
1095        profile.ack_frequency =
1096            Some(AckFrequency { reordering_threshold: VARINT_MAX + 1, ..Default::default() });
1097        assert_eq!(
1098            profile.validate(),
1099            Err(TransportProfileError::VarIntRange {
1100                field: "ack_frequency.reordering_threshold",
1101                value: VARINT_MAX + 1,
1102            }),
1103            "`reordering_threshold` alone would not say which part of the file to look at"
1104        );
1105    }
1106
1107    #[test]
1108    fn a_keep_alive_at_the_idle_timeout_is_refused() {
1109        let mut profile = healthy();
1110        profile.max_idle_timeout = Some(Duration::from_secs(10));
1111        profile.keep_alive_interval = Some(Duration::from_secs(10));
1112        assert_eq!(
1113            profile.validate(),
1114            Err(TransportProfileError::KeepAliveNotBelowIdle {
1115                keep_alive: Duration::from_secs(10),
1116                idle: Duration::from_secs(10),
1117            }),
1118            "a keep-alive due exactly when the connection is already closed saves nothing"
1119        );
1120
1121        profile.keep_alive_interval = Some(Duration::from_millis(9_999));
1122        assert_eq!(profile.validate(), Ok(()), "one millisecond below is enough");
1123    }
1124
1125    #[test]
1126    fn a_keep_alive_without_an_idle_timeout_is_not_compared_to_anything() {
1127        let mut profile = healthy();
1128        profile.max_idle_timeout = None;
1129        profile.keep_alive_interval = Some(Duration::from_secs(3600));
1130        assert_eq!(
1131            profile.validate(),
1132            Ok(()),
1133            "with no idle timeout in the profile there is no timeout this could fail to prevent"
1134        );
1135    }
1136
1137    #[test]
1138    fn a_min_mtu_above_the_initial_mtu_is_refused() {
1139        let mut profile = healthy();
1140        profile.initial_mtu = Some(1300);
1141        profile.min_mtu = Some(1400);
1142        assert_eq!(
1143            profile.validate(),
1144            Err(TransportProfileError::MtuInverted { min: 1400, initial: 1300 }),
1145            "a discovery floor above the starting size is an empty range"
1146        );
1147
1148        profile.min_mtu = Some(1300);
1149        assert_eq!(profile.validate(), Ok(()), "equal is a range of one, which is usable");
1150    }
1151
1152    #[test]
1153    fn an_mtu_below_the_quic_floor_is_refused_rather_than_silently_raised() {
1154        let mut profile = healthy();
1155        profile.initial_mtu = Some(900);
1156        assert_eq!(
1157            profile.validate(),
1158            Err(TransportProfileError::MtuBelowFloor {
1159                field: "initial_mtu",
1160                value: 900,
1161                floor: 1200
1162            }),
1163            "quinn would raise 900 to 1200 without a word, so a run at 900 never happens"
1164        );
1165
1166        let mut profile = healthy();
1167        profile.min_mtu = Some(1199);
1168        assert_eq!(
1169            profile.validate(),
1170            Err(TransportProfileError::MtuBelowFloor {
1171                field: "min_mtu",
1172                value: 1199,
1173                floor: 1200
1174            }),
1175            "one byte below the floor is still below the floor"
1176        );
1177
1178        let mut profile = healthy();
1179        profile.initial_mtu = Some(1200);
1180        profile.min_mtu = Some(1200);
1181        assert_eq!(profile.validate(), Ok(()), "exactly the floor is accepted");
1182    }
1183
1184    #[test]
1185    fn the_mtu_floor_is_reported_before_the_inversion() {
1186        let mut profile = healthy();
1187        profile.initial_mtu = Some(900);
1188        profile.min_mtu = Some(1000);
1189        assert_eq!(
1190            profile.validate(),
1191            Err(TransportProfileError::MtuBelowFloor {
1192                field: "initial_mtu",
1193                value: 900,
1194                floor: 1200
1195            }),
1196            "the single-field fault comes first; the inversion may not survive fixing it"
1197        );
1198    }
1199
1200    #[test]
1201    fn a_time_threshold_that_is_not_a_usable_multiplier_is_refused() {
1202        let mut profile = healthy();
1203        profile.time_threshold = Some(1.0);
1204        assert_eq!(
1205            profile.validate(),
1206            Err(TransportProfileError::TimeThreshold(1.0)),
1207            "a multiplier of exactly one declares loss the instant an ack becomes possible"
1208        );
1209
1210        profile.time_threshold = Some(f32::NAN);
1211        // Compared with `matches!` rather than `assert_eq!`: `NaN != NaN`,
1212        // so the error carrying it is not equal to itself either. This is
1213        // the reason the error type has no `Eq`.
1214        assert!(
1215            matches!(profile.validate(), Err(TransportProfileError::TimeThreshold(t)) if t.is_nan()),
1216            "a non-finite multiplier is not a multiplier"
1217        );
1218
1219        profile.time_threshold = Some(1.000_001);
1220        assert_eq!(profile.validate(), Ok(()), "anything above one is a usable multiplier");
1221    }
1222
1223    #[test]
1224    fn every_field_applies_to_a_config_without_panicking() {
1225        let profile = healthy();
1226        let mut tc = quinn::TransportConfig::default();
1227        assert_eq!(
1228            profile.apply_to(&mut tc),
1229            Ok(()),
1230            "every field in the control profile has a setter that accepts it"
1231        );
1232    }
1233
1234    #[test]
1235    fn the_other_variants_of_the_wrapper_enums_also_apply() {
1236        // `healthy` picks one variant of each two-variant enum; this covers
1237        // the other, so no arm of `to_config` or `to_size` is unexercised.
1238        let mut profile = healthy();
1239        profile.mtu_discovery = Some(MtuDiscovery::Off);
1240        profile.datagram_receive_buffer = Some(DatagramBuffer::Disabled);
1241        profile.congestion = Some(Congestion::NewReno);
1242        let mut tc = quinn::TransportConfig::default();
1243        assert_eq!(profile.apply_to(&mut tc), Ok(()), "discovery off and datagrams disabled apply");
1244
1245        profile.congestion = Some(Congestion::Cubic);
1246        let mut tc = quinn::TransportConfig::default();
1247        assert_eq!(profile.apply_to(&mut tc), Ok(()), "cubic applies");
1248    }
1249
1250    #[test]
1251    fn into_config_and_apply_to_agree_on_acceptance_and_on_the_error() {
1252        // Nothing here reads a field back out of the `TransportConfig`. Its
1253        // hand-written `Debug` would make that possible, and it would assert
1254        // only that the setter stored what it was given — not that quinn
1255        // honoured it, which is the part that matters and the part no test
1256        // in this module can see. The assertions are on the profile and on
1257        // the error.
1258        let profile = TransportProfile::default();
1259        let mut tc = quinn::TransportConfig::default();
1260        assert_eq!(
1261            profile.apply_to(&mut tc).is_ok(),
1262            profile.into_config().is_ok(),
1263            "a default profile is accepted by both or by neither"
1264        );
1265
1266        let mut profile = healthy();
1267        profile.initial_mtu = Some(800);
1268        let mut tc = quinn::TransportConfig::default();
1269        assert_eq!(
1270            profile.apply_to(&mut tc),
1271            profile.into_config().map(|_| ()),
1272            "a refused profile is refused identically by both"
1273        );
1274        assert_eq!(
1275            profile.into_config().map(|_| ()),
1276            Err(TransportProfileError::MtuBelowFloor {
1277                field: "initial_mtu",
1278                value: 800,
1279                floor: 1200
1280            }),
1281            "and the error is the one `validate` gives"
1282        );
1283    }
1284
1285    /// An installer that records every profile it was asked to build.
1286    /// The only observable a test has: `quinn::TransportConfig` cannot be read
1287    /// back, so *the leg installed what came out of here* is proved by the leg
1288    /// reaching this at all, with the profile the caller set, and then coming
1289    /// up.
1290    #[derive(Default)]
1291    struct RecordingInstaller {
1292        seen: std::sync::Mutex<Vec<TransportProfile>>,
1293    }
1294
1295    impl TransportInstaller for RecordingInstaller {
1296        fn build(
1297            &self,
1298            profile: &TransportProfile,
1299        ) -> Result<quinn::TransportConfig, TransportProfileError> {
1300            self.seen.lock().expect("no test holds this across a panic").push(profile.clone());
1301            profile.into_config()
1302        }
1303    }
1304
1305    /// [`resolve`] for a leg that asked for no capture.
1306    ///
1307    /// The spec argument exists only under the `qlog` feature, so every row
1308    /// that has nothing to do with capturing goes through this and reads the
1309    /// same in both builds. The alternative — a `#[cfg]` on the fifth
1310    /// argument of each call — puts a conditional in eight places to say
1311    /// "and no capture" eight times.
1312    fn resolve_uncaptured(
1313        leg: Leg,
1314        raw: Option<Arc<quinn::TransportConfig>>,
1315        profile: Option<&TransportProfile>,
1316        installer: Option<&Arc<dyn TransportInstaller>>,
1317    ) -> Result<Option<Arc<quinn::TransportConfig>>, ProxyError> {
1318        resolve(
1319            leg,
1320            raw,
1321            profile,
1322            installer,
1323            #[cfg(feature = "qlog")]
1324            None,
1325        )
1326    }
1327
1328    #[test]
1329    fn a_leg_naming_neither_installs_nothing() {
1330        assert!(
1331            resolve_uncaptured(Leg::Client, None, None, None)
1332                .expect("nothing named is nothing to refuse")
1333                .is_none(),
1334            "a leg with no opinion has to stay exactly as it was before profiles existed"
1335        );
1336    }
1337
1338    #[test]
1339    fn a_raw_config_is_installed_as_it_was_given() {
1340        let raw = Arc::new(quinn::TransportConfig::default());
1341        let resolved = resolve_uncaptured(Leg::Client, Some(Arc::clone(&raw)), None, None)
1342            .expect("a raw config alone is not a contradiction")
1343            .expect("and it is what the leg installs");
1344        assert!(
1345            Arc::ptr_eq(&raw, &resolved),
1346            "the caller's own config must reach the leg, not a copy of it — there is no copy"
1347        );
1348    }
1349
1350    #[test]
1351    fn a_profile_alone_is_built_by_the_default_installer() {
1352        // Written as a struct expression with `..Default::default()`,
1353        // which is legal here and illegal downstream: the `default()` then
1354        // field-assignment form the type documents is what
1355        // `field_reassign_with_default` fires on inside this crate.
1356        let profile = TransportProfile { initial_mtu: Some(1350), ..Default::default() };
1357        assert!(
1358            resolve_uncaptured(Leg::Upstream, None, Some(&profile), None)
1359                .expect("a valid profile builds")
1360                .is_some(),
1361            "a leg carrying only a profile installs the config built from it"
1362        );
1363    }
1364
1365    #[test]
1366    fn a_supplied_installer_is_what_builds_the_profile() {
1367        let installer = Arc::new(RecordingInstaller::default());
1368        let dynamic: Arc<dyn TransportInstaller> = installer.clone();
1369        let profile = TransportProfile { congestion: Some(Congestion::Bbr), ..Default::default() };
1370
1371        assert!(resolve_uncaptured(Leg::Client, None, Some(&profile), Some(&dynamic))
1372            .expect("the installer accepted the profile")
1373            .is_some());
1374        let seen = installer.seen.lock().expect("uncontended");
1375        assert_eq!(seen.len(), 1, "the installer is consulted exactly once per leg");
1376        assert_eq!(
1377            seen[0], profile,
1378            "the leg must hand the caller's own profile to the caller's own installer"
1379        );
1380    }
1381
1382    #[test]
1383    fn a_leg_naming_both_a_config_and_a_profile_is_refused_with_its_own_leg() {
1384        // One row per leg. The `leg` in the error is the whole point of the
1385        // field — a proxy holds two of these and *which one did I get wrong* is
1386        // the only question the caller has.
1387        for leg in [Leg::Client, Leg::Upstream] {
1388            let raw = Arc::new(quinn::TransportConfig::default());
1389            let err = resolve_uncaptured(leg, Some(raw), Some(&TransportProfile::default()), None)
1390                .expect_err("naming both is a contradiction, not a merge");
1391            assert!(
1392                matches!(err, ProxyError::TransportConfigAndProfile { leg: reported } if reported == leg),
1393                "{leg:?} must be refused as {leg:?}, got {err}"
1394            );
1395            assert!(
1396                err.to_string().contains("apply_to"),
1397                "the message has to name the supported way to have both, or the first reader \
1398                 takes this for a regression: {err}"
1399            );
1400        }
1401    }
1402
1403    #[test]
1404    fn a_profile_the_installer_refuses_refuses_the_leg_and_names_it() {
1405        // quinn would raise 900 to 1200 without a word, which is the whole
1406        // reason the profile refuses it first.
1407        let profile = TransportProfile { initial_mtu: Some(900), ..Default::default() };
1408
1409        let err = resolve_uncaptured(Leg::Upstream, None, Some(&profile), None)
1410            .expect_err("an unhonourable profile must not become a connection");
1411        assert!(
1412            matches!(
1413                err,
1414                ProxyError::TransportProfile {
1415                    leg: Leg::Upstream,
1416                    source: TransportProfileError::MtuBelowFloor { field: "initial_mtu", .. },
1417                }
1418            ),
1419            "the refusal carries both the leg and the reason: {err}"
1420        );
1421    }
1422
1423    // ── and what a capture changes about all four ──────────────────────
1424
1425    /// A writer that keeps everything, readable while the sink is alive.
1426    ///
1427    /// Unbuffered on purpose: every assertion below is about whether a sink
1428    /// was built at all, and a buffered writer would hold the preamble until
1429    /// something dropped it.
1430    #[cfg(feature = "qlog")]
1431    #[derive(Clone)]
1432    struct Captured(Arc<std::sync::Mutex<Vec<u8>>>);
1433
1434    #[cfg(feature = "qlog")]
1435    impl std::io::Write for Captured {
1436        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1437            self.0.lock().expect("no test holds this across a panic").extend_from_slice(buf);
1438            Ok(buf.len())
1439        }
1440
1441        fn flush(&mut self) -> std::io::Result<()> {
1442            Ok(())
1443        }
1444    }
1445
1446    /// A spec writing into `sink`, and the sink itself.
1447    #[cfg(feature = "qlog")]
1448    fn spec_over_a_sink() -> (crate::qlog::QlogSpec, Arc<std::sync::Mutex<Vec<u8>>>) {
1449        let sink = Arc::new(std::sync::Mutex::new(Vec::new()));
1450        let spec = crate::qlog::QlogSpec {
1451            writer: Some(Box::new(Captured(Arc::clone(&sink)))),
1452            title: Some("a leg".to_string()),
1453            description: None,
1454        };
1455        (spec, sink)
1456    }
1457
1458    /// How many bytes the capture holds. Non-zero means a sink was built,
1459    /// because the preamble is written as it is built and nothing else in
1460    /// these tests connects.
1461    #[cfg(feature = "qlog")]
1462    fn written(sink: &Arc<std::sync::Mutex<Vec<u8>>>) -> usize {
1463        sink.lock().expect("uncontended").len()
1464    }
1465
1466    /// A leg carrying only a spec still installs a config.
1467    ///
1468    /// The case most likely to be silently wrong, and the reason is that
1469    /// `None` back from `resolve` used to be the right answer for a leg that
1470    /// named neither of the other two fields. A leg that installed nothing
1471    /// here would leave the sink attached to a `quinn::TransportConfig` that
1472    /// went nowhere — and that produces a file which exists, parses, names a
1473    /// qlog version and holds no event, which is the one failure a caller
1474    /// watching their disk cannot see.
1475    #[cfg(feature = "qlog")]
1476    #[test]
1477    fn a_spec_alone_installs_a_config_for_the_sink_to_go_on() {
1478        let (spec, sink) = spec_over_a_sink();
1479        let resolved = resolve(Leg::Client, None, None, None, Some(spec))
1480            .expect("a spec with a writer is not a contradiction")
1481            .expect("a leg asking only for a capture still installs the config carrying it");
1482        assert!(
1483            written(&sink) > 0,
1484            "the preamble is written when the sink is built, so an empty writer means the spec \
1485             never became one"
1486        );
1487        drop(resolved);
1488    }
1489
1490    /// A profile and a spec reach one config, and the installer is what
1491    /// built it.
1492    ///
1493    /// The installer is the leg's only source of a base config, so a spec
1494    /// beside a profile must not move the leg onto a different one. The
1495    /// recording installer is what makes that checkable rather than
1496    /// asserted: it counts every profile it is asked to build, and here it
1497    /// must be asked for exactly the profile the caller set. The sink is
1498    /// attached to what it returned, which is possible at all because
1499    /// `build` hands back an owned config rather than an `Arc`.
1500    #[cfg(feature = "qlog")]
1501    #[test]
1502    fn a_profile_and_a_spec_are_applied_to_one_config_built_by_the_installer() {
1503        let installer = Arc::new(RecordingInstaller::default());
1504        let dynamic: Arc<dyn TransportInstaller> = installer.clone();
1505        let profile = TransportProfile { initial_mtu: Some(1350), ..Default::default() };
1506
1507        let (spec, sink) = spec_over_a_sink();
1508        assert!(
1509            resolve(Leg::Upstream, None, Some(&profile), Some(&dynamic), Some(spec))
1510                .expect("a profile and a spec are not a contradiction")
1511                .is_some(),
1512            "a leg carrying both installs the one config they were both written into"
1513        );
1514        assert!(written(&sink) > 0, "and the sink is on that config");
1515        let seen = installer.seen.lock().expect("uncontended");
1516        assert_eq!(
1517            seen.as_slice(),
1518            &[profile],
1519            "a spec must not bypass the caller's installer: a leg that built its own config here \
1520             would run on a base nobody supplied and report success"
1521        );
1522    }
1523
1524    /// A leg naming a raw config and a spec is refused, as its own thing,
1525    /// with its own leg — and no capture is begun on the way out.
1526    #[cfg(feature = "qlog")]
1527    #[test]
1528    fn a_leg_naming_both_a_config_and_a_spec_is_refused_with_its_own_leg() {
1529        // One row per leg, as for the config-and-profile pair: a proxy holds
1530        // two of these and *which one did I get wrong* is the only question the
1531        // caller has.
1532        for leg in [Leg::Client, Leg::Upstream] {
1533            let (spec, sink) = spec_over_a_sink();
1534            let raw = Arc::new(quinn::TransportConfig::default());
1535            let err = resolve(leg, Some(raw), None, None, Some(spec))
1536                .expect_err("a config the sink cannot be installed on is not a leg with a capture");
1537            assert!(
1538                matches!(err, ProxyError::TransportConfigAndQlog { leg: reported } if reported == leg),
1539                "{leg:?} must be refused as {leg:?}, and as the config-and-spec pair rather than \
1540                 the config-and-profile one — the two have different fixes: {err}"
1541            );
1542            assert_eq!(
1543                written(&sink),
1544                0,
1545                "and nothing may be written on the way to refusing: a preamble here is a file the \
1546                 caller will read as the start of a capture that never happened"
1547            );
1548        }
1549    }
1550
1551    /// The older pair is reported first when a leg names all three.
1552    ///
1553    /// Not a preference between the two refusals so much as a fixed answer:
1554    /// a leg with two faults reports one of them, and which one has to be
1555    /// the same every time or the fix a caller is told to make depends on
1556    /// the order of the checks. The config-and-profile pair is the one
1557    /// whose fix — apply the profile to your own config — also resolves the
1558    /// other, so it is the useful half to be sent to.
1559    #[cfg(feature = "qlog")]
1560    #[test]
1561    fn a_leg_naming_all_three_hears_about_the_config_and_the_profile() {
1562        let (spec, sink) = spec_over_a_sink();
1563        let raw = Arc::new(quinn::TransportConfig::default());
1564        let err =
1565            resolve(Leg::Client, Some(raw), Some(&TransportProfile::default()), None, Some(spec))
1566                .expect_err("three fields that cannot be combined are still a refusal");
1567        assert!(
1568            matches!(err, ProxyError::TransportConfigAndProfile { leg: Leg::Client }),
1569            "the answer has to be fixed rather than whichever check ran first: {err}"
1570        );
1571        assert_eq!(written(&sink), 0, "and no capture is begun for a leg that is refused");
1572    }
1573
1574    /// A spec that names no writer refuses the leg, and says so as itself.
1575    ///
1576    /// quinn's own answer to a missing writer is no sink and no error, so a
1577    /// leg that let it through would connect, run, report success, and leave
1578    /// the caller's file untouched. The refusal has to carry the leg — a
1579    /// proxy has two — and the `NoWriter` reason, which is what tells a
1580    /// caller their spec is unfinished rather than their disk unwritable.
1581    #[cfg(feature = "qlog")]
1582    #[test]
1583    fn a_spec_with_no_writer_refuses_the_leg_that_carries_it() {
1584        let blind = crate::qlog::QlogSpec {
1585            writer: None,
1586            title: Some("a leg".to_string()),
1587            description: None,
1588        };
1589        let err = resolve(Leg::Upstream, None, None, None, Some(blind))
1590            .expect_err("a spec with nowhere to write is a mistake, not a request for no capture");
1591        assert!(
1592            matches!(
1593                err,
1594                ProxyError::Qlog { leg: Leg::Upstream, source: crate::qlog::QlogError::NoWriter }
1595            ),
1596            "the refusal carries both the leg and the reason: {err}"
1597        );
1598    }
1599
1600    #[test]
1601    fn the_ack_frequency_default_is_quinns_own_and_not_a_derived_one() {
1602        let ack = AckFrequency::default();
1603        assert_eq!(
1604            (ack.ack_eliciting_threshold, ack.reordering_threshold),
1605            (1, 2),
1606            "a derived default would ask the peer to ack every packet and never ack reordering"
1607        );
1608        assert_eq!(ack.max_ack_delay, None, "`None` leaves the peer's advertised delay in place");
1609    }
1610}