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