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