Skip to main content

moqtap_proxy/
transport.rs

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