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}