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