use std::sync::Arc;
use std::time::Duration;
use quinn::congestion;
use quinn::{AckFrequencyConfig, IdleTimeout, MtuDiscoveryConfig, VarInt};
use crate::error::ProxyError;
const QUIC_INITIAL_MTU: u16 = 1200;
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
#[non_exhaustive]
pub struct TransportProfile {
pub congestion: Option<Congestion>,
pub initial_rtt: Option<Duration>,
pub receive_window: Option<u64>,
pub stream_receive_window: Option<u64>,
pub send_window: Option<u64>,
pub max_concurrent_uni_streams: Option<u64>,
pub max_concurrent_bidi_streams: Option<u64>,
pub max_idle_timeout: Option<Duration>,
pub keep_alive_interval: Option<Duration>,
pub packet_threshold: Option<u32>,
pub time_threshold: Option<f32>,
pub persistent_congestion_threshold: Option<u32>,
pub ack_frequency: Option<AckFrequency>,
pub initial_mtu: Option<u16>,
pub min_mtu: Option<u16>,
pub mtu_discovery: Option<MtuDiscovery>,
pub send_fairness: Option<bool>,
pub datagram_receive_buffer: Option<DatagramBuffer>,
}
impl TransportProfile {
pub fn validate(&self) -> Result<(), TransportProfileError> {
if let Some(bytes) = self.receive_window {
varint("receive_window", bytes)?;
}
if let Some(bytes) = self.stream_receive_window {
varint("stream_receive_window", bytes)?;
}
if let Some(count) = self.max_concurrent_uni_streams {
varint("max_concurrent_uni_streams", count)?;
}
if let Some(count) = self.max_concurrent_bidi_streams {
varint("max_concurrent_bidi_streams", count)?;
}
if let Some(idle) = self.max_idle_timeout {
idle_timeout(idle)?;
}
if let (Some(keep_alive), Some(idle)) = (self.keep_alive_interval, self.max_idle_timeout) {
if keep_alive >= idle {
return Err(TransportProfileError::KeepAliveNotBelowIdle { keep_alive, idle });
}
}
if let Some(threshold) = self.time_threshold {
if !threshold.is_finite() || threshold <= 1.0 {
return Err(TransportProfileError::TimeThreshold(threshold));
}
}
if let Some(ack) = &self.ack_frequency {
ack.validate()?;
}
if let Some(mtu) = self.initial_mtu {
mtu_floor("initial_mtu", mtu)?;
}
if let Some(mtu) = self.min_mtu {
mtu_floor("min_mtu", mtu)?;
}
if let (Some(min), Some(initial)) = (self.min_mtu, self.initial_mtu) {
if min > initial {
return Err(TransportProfileError::MtuInverted { min, initial });
}
}
Ok(())
}
pub fn apply_to(&self, tc: &mut quinn::TransportConfig) -> Result<(), TransportProfileError> {
self.validate()?;
if let Some(controller) = self.congestion {
tc.congestion_controller_factory(controller.factory());
}
if let Some(rtt) = self.initial_rtt {
tc.initial_rtt(rtt);
}
if let Some(bytes) = self.receive_window {
tc.receive_window(varint("receive_window", bytes)?);
}
if let Some(bytes) = self.stream_receive_window {
tc.stream_receive_window(varint("stream_receive_window", bytes)?);
}
if let Some(bytes) = self.send_window {
tc.send_window(bytes);
}
if let Some(count) = self.max_concurrent_uni_streams {
tc.max_concurrent_uni_streams(varint("max_concurrent_uni_streams", count)?);
}
if let Some(count) = self.max_concurrent_bidi_streams {
tc.max_concurrent_bidi_streams(varint("max_concurrent_bidi_streams", count)?);
}
if let Some(idle) = self.max_idle_timeout {
tc.max_idle_timeout(Some(IdleTimeout::from(idle_timeout(idle)?)));
}
if let Some(interval) = self.keep_alive_interval {
tc.keep_alive_interval(Some(interval));
}
if let Some(threshold) = self.packet_threshold {
tc.packet_threshold(threshold);
}
if let Some(threshold) = self.time_threshold {
tc.time_threshold(threshold);
}
if let Some(threshold) = self.persistent_congestion_threshold {
tc.persistent_congestion_threshold(threshold);
}
if let Some(ack) = &self.ack_frequency {
tc.ack_frequency_config(Some(ack.to_config()?));
}
if let Some(mtu) = self.initial_mtu {
tc.initial_mtu(mtu);
}
if let Some(mtu) = self.min_mtu {
tc.min_mtu(mtu);
}
if let Some(discovery) = self.mtu_discovery {
tc.mtu_discovery_config(discovery.to_config());
}
if let Some(fair) = self.send_fairness {
tc.send_fairness(fair);
}
if let Some(buffer) = self.datagram_receive_buffer {
tc.datagram_receive_buffer_size(buffer.to_size());
}
Ok(())
}
pub fn into_config(&self) -> Result<quinn::TransportConfig, TransportProfileError> {
let mut tc = quinn::TransportConfig::default();
self.apply_to(&mut tc)?;
Ok(tc)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Congestion {
Cubic,
Bbr,
NewReno,
}
impl Congestion {
fn factory(self) -> Arc<dyn congestion::ControllerFactory + Send + Sync + 'static> {
match self {
Self::Cubic => Arc::new(congestion::CubicConfig::default()),
Self::Bbr => Arc::new(congestion::BbrConfig::default()),
Self::NewReno => Arc::new(congestion::NewRenoConfig::default()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
#[non_exhaustive]
pub struct AckFrequency {
pub ack_eliciting_threshold: u64,
pub max_ack_delay: Option<Duration>,
pub reordering_threshold: u64,
}
impl Default for AckFrequency {
fn default() -> Self {
Self { ack_eliciting_threshold: 1, max_ack_delay: None, reordering_threshold: 2 }
}
}
impl AckFrequency {
fn validate(&self) -> Result<(), TransportProfileError> {
varint("ack_frequency.ack_eliciting_threshold", self.ack_eliciting_threshold)?;
varint("ack_frequency.reordering_threshold", self.reordering_threshold)?;
Ok(())
}
fn to_config(&self) -> Result<AckFrequencyConfig, TransportProfileError> {
let mut config = AckFrequencyConfig::default();
config.ack_eliciting_threshold(varint(
"ack_frequency.ack_eliciting_threshold",
self.ack_eliciting_threshold,
)?);
config.max_ack_delay(self.max_ack_delay);
config.reordering_threshold(varint(
"ack_frequency.reordering_threshold",
self.reordering_threshold,
)?);
Ok(config)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum MtuDiscovery {
Off,
UpTo(u16),
}
impl MtuDiscovery {
fn to_config(self) -> Option<MtuDiscoveryConfig> {
match self {
Self::Off => None,
Self::UpTo(bytes) => {
let mut config = MtuDiscoveryConfig::default();
config.upper_bound(bytes);
Some(config)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum DatagramBuffer {
Disabled,
Bytes(usize),
}
impl DatagramBuffer {
fn to_size(self) -> Option<usize> {
match self {
Self::Disabled => None,
Self::Bytes(bytes) => Some(bytes),
}
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum TransportProfileError {
#[error("{field} = {value} exceeds the QUIC varint range")]
VarIntRange {
field: &'static str,
value: u64,
},
#[error("keep_alive_interval {keep_alive:?} must be below max_idle_timeout {idle:?}")]
KeepAliveNotBelowIdle {
keep_alive: Duration,
idle: Duration,
},
#[error("min_mtu {min} is above initial_mtu {initial}")]
MtuInverted {
min: u16,
initial: u16,
},
#[error("{field} = {value} is below QUIC's {floor}-byte floor; quinn would silently raise it")]
MtuBelowFloor {
field: &'static str,
value: u16,
floor: u16,
},
#[error("time_threshold {0} must be finite and greater than 1.0")]
TimeThreshold(f32),
}
fn varint(field: &'static str, value: u64) -> Result<VarInt, TransportProfileError> {
VarInt::from_u64(value).map_err(|_| TransportProfileError::VarIntRange { field, value })
}
fn idle_timeout(idle: Duration) -> Result<VarInt, TransportProfileError> {
let millis = u64::try_from(idle.as_millis()).unwrap_or(u64::MAX);
varint("max_idle_timeout", millis)
}
fn mtu_floor(field: &'static str, value: u16) -> Result<(), TransportProfileError> {
if value < QUIC_INITIAL_MTU {
return Err(TransportProfileError::MtuBelowFloor { field, value, floor: QUIC_INITIAL_MTU });
}
Ok(())
}
pub use crate::types::Leg;
pub trait TransportInstaller: Send + Sync + 'static {
fn build(
&self,
profile: &TransportProfile,
) -> Result<quinn::TransportConfig, TransportProfileError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DefaultInstaller;
impl TransportInstaller for DefaultInstaller {
fn build(
&self,
profile: &TransportProfile,
) -> Result<quinn::TransportConfig, TransportProfileError> {
profile.into_config()
}
}
pub(crate) fn resolve(
leg: Leg,
raw: Option<Arc<quinn::TransportConfig>>,
profile: Option<&TransportProfile>,
installer: Option<&Arc<dyn TransportInstaller>>,
#[cfg(feature = "qlog")] qlog: Option<crate::qlog::QlogSpec>,
) -> Result<Option<Arc<quinn::TransportConfig>>, ProxyError> {
match (raw, profile) {
(Some(_), Some(_)) => Err(ProxyError::TransportConfigAndProfile { leg }),
(Some(config), None) => {
#[cfg(feature = "qlog")]
if qlog.is_some() {
return Err(ProxyError::TransportConfigAndQlog { leg });
}
Ok(Some(config))
}
(None, Some(profile)) => {
#[cfg_attr(not(feature = "qlog"), allow(unused_mut))]
let mut config = match installer {
Some(installer) => installer.build(profile),
None => DefaultInstaller.build(profile),
}
.map_err(|source| ProxyError::TransportProfile { leg, source })?;
#[cfg(feature = "qlog")]
if let Some(spec) = qlog {
spec.attach_to(&mut config).map_err(|source| ProxyError::Qlog { leg, source })?;
}
Ok(Some(Arc::new(config)))
}
(None, None) => {
#[cfg(feature = "qlog")]
if let Some(spec) = qlog {
let mut config = quinn::TransportConfig::default();
spec.attach_to(&mut config).map_err(|source| ProxyError::Qlog { leg, source })?;
return Ok(Some(Arc::new(config)));
}
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn healthy() -> TransportProfile {
TransportProfile {
congestion: Some(Congestion::Bbr),
initial_rtt: Some(Duration::from_millis(40)),
receive_window: Some(8 * 1024 * 1024),
stream_receive_window: Some(1024 * 1024),
send_window: Some(8 * 1024 * 1024),
max_concurrent_uni_streams: Some(256),
max_concurrent_bidi_streams: Some(16),
max_idle_timeout: Some(Duration::from_secs(30)),
keep_alive_interval: Some(Duration::from_secs(5)),
packet_threshold: Some(3),
time_threshold: Some(1.125),
persistent_congestion_threshold: Some(3),
ack_frequency: Some(AckFrequency::default()),
initial_mtu: Some(1350),
min_mtu: Some(1200),
mtu_discovery: Some(MtuDiscovery::UpTo(1452)),
send_fairness: Some(true),
datagram_receive_buffer: Some(DatagramBuffer::Bytes(64 * 1024)),
}
}
const VARINT_MAX: u64 = (1 << 62) - 1;
#[test]
fn a_profile_that_sets_nothing_validates() {
assert_eq!(
TransportProfile::default().validate(),
Ok(()),
"an all-`None` profile has no opinion to be wrong about"
);
}
#[test]
fn a_fully_populated_healthy_profile_is_accepted() {
assert_eq!(
healthy().validate(),
Ok(()),
"the control profile must be valid or every refusal below is unattributable"
);
}
#[test]
fn a_value_above_the_varint_ceiling_is_refused_and_names_its_field() {
type Edit = fn(&mut TransportProfile);
let rows: [(&str, Edit, &str); 5] = [
("connection window", |p| p.receive_window = Some(VARINT_MAX + 1), "receive_window"),
(
"stream window",
|p| p.stream_receive_window = Some(VARINT_MAX + 1),
"stream_receive_window",
),
(
"uni stream count",
|p| p.max_concurrent_uni_streams = Some(VARINT_MAX + 1),
"max_concurrent_uni_streams",
),
(
"bidi stream count",
|p| p.max_concurrent_bidi_streams = Some(VARINT_MAX + 1),
"max_concurrent_bidi_streams",
),
(
"ack-eliciting threshold",
|p| {
p.ack_frequency = Some(AckFrequency {
ack_eliciting_threshold: VARINT_MAX + 1,
..Default::default()
});
},
"ack_frequency.ack_eliciting_threshold",
),
];
for (label, edit, field) in rows {
let mut profile = healthy();
edit(&mut profile);
assert_eq!(
profile.validate(),
Err(TransportProfileError::VarIntRange { field, value: VARINT_MAX + 1 }),
"{label}"
);
}
let mut profile = healthy();
profile.receive_window = Some(VARINT_MAX);
profile.stream_receive_window = Some(VARINT_MAX);
profile.max_concurrent_uni_streams = Some(VARINT_MAX);
profile.max_concurrent_bidi_streams = Some(VARINT_MAX);
assert_eq!(profile.validate(), Ok(()), "exactly the ceiling is accepted");
}
#[test]
fn the_reordering_threshold_is_checked_under_its_own_dotted_name() {
let mut profile = healthy();
profile.ack_frequency =
Some(AckFrequency { reordering_threshold: VARINT_MAX + 1, ..Default::default() });
assert_eq!(
profile.validate(),
Err(TransportProfileError::VarIntRange {
field: "ack_frequency.reordering_threshold",
value: VARINT_MAX + 1,
}),
"`reordering_threshold` alone would not say which part of the file to look at"
);
}
#[test]
fn a_keep_alive_at_the_idle_timeout_is_refused() {
let mut profile = healthy();
profile.max_idle_timeout = Some(Duration::from_secs(10));
profile.keep_alive_interval = Some(Duration::from_secs(10));
assert_eq!(
profile.validate(),
Err(TransportProfileError::KeepAliveNotBelowIdle {
keep_alive: Duration::from_secs(10),
idle: Duration::from_secs(10),
}),
"a keep-alive due exactly when the connection is already closed saves nothing"
);
profile.keep_alive_interval = Some(Duration::from_millis(9_999));
assert_eq!(profile.validate(), Ok(()), "one millisecond below is enough");
}
#[test]
fn a_keep_alive_without_an_idle_timeout_is_not_compared_to_anything() {
let mut profile = healthy();
profile.max_idle_timeout = None;
profile.keep_alive_interval = Some(Duration::from_secs(3600));
assert_eq!(
profile.validate(),
Ok(()),
"with no idle timeout in the profile there is no timeout this could fail to prevent"
);
}
#[test]
fn a_min_mtu_above_the_initial_mtu_is_refused() {
let mut profile = healthy();
profile.initial_mtu = Some(1300);
profile.min_mtu = Some(1400);
assert_eq!(
profile.validate(),
Err(TransportProfileError::MtuInverted { min: 1400, initial: 1300 }),
"a discovery floor above the starting size is an empty range"
);
profile.min_mtu = Some(1300);
assert_eq!(profile.validate(), Ok(()), "equal is a range of one, which is usable");
}
#[test]
fn an_mtu_below_the_quic_floor_is_refused_rather_than_silently_raised() {
let mut profile = healthy();
profile.initial_mtu = Some(900);
assert_eq!(
profile.validate(),
Err(TransportProfileError::MtuBelowFloor {
field: "initial_mtu",
value: 900,
floor: 1200
}),
"quinn would raise 900 to 1200 without a word, so a run at 900 never happens"
);
let mut profile = healthy();
profile.min_mtu = Some(1199);
assert_eq!(
profile.validate(),
Err(TransportProfileError::MtuBelowFloor {
field: "min_mtu",
value: 1199,
floor: 1200
}),
"one byte below the floor is still below the floor"
);
let mut profile = healthy();
profile.initial_mtu = Some(1200);
profile.min_mtu = Some(1200);
assert_eq!(profile.validate(), Ok(()), "exactly the floor is accepted");
}
#[test]
fn the_mtu_floor_is_reported_before_the_inversion() {
let mut profile = healthy();
profile.initial_mtu = Some(900);
profile.min_mtu = Some(1000);
assert_eq!(
profile.validate(),
Err(TransportProfileError::MtuBelowFloor {
field: "initial_mtu",
value: 900,
floor: 1200
}),
"the single-field fault comes first; the inversion may not survive fixing it"
);
}
#[test]
fn a_time_threshold_that_is_not_a_usable_multiplier_is_refused() {
let mut profile = healthy();
profile.time_threshold = Some(1.0);
assert_eq!(
profile.validate(),
Err(TransportProfileError::TimeThreshold(1.0)),
"a multiplier of exactly one declares loss the instant an ack becomes possible"
);
profile.time_threshold = Some(f32::NAN);
assert!(
matches!(profile.validate(), Err(TransportProfileError::TimeThreshold(t)) if t.is_nan()),
"a non-finite multiplier is not a multiplier"
);
profile.time_threshold = Some(1.000_001);
assert_eq!(profile.validate(), Ok(()), "anything above one is a usable multiplier");
}
#[test]
fn every_field_applies_to_a_config_without_panicking() {
let profile = healthy();
let mut tc = quinn::TransportConfig::default();
assert_eq!(
profile.apply_to(&mut tc),
Ok(()),
"every field in the control profile has a setter that accepts it"
);
}
#[test]
fn the_other_variants_of_the_wrapper_enums_also_apply() {
let mut profile = healthy();
profile.mtu_discovery = Some(MtuDiscovery::Off);
profile.datagram_receive_buffer = Some(DatagramBuffer::Disabled);
profile.congestion = Some(Congestion::NewReno);
let mut tc = quinn::TransportConfig::default();
assert_eq!(profile.apply_to(&mut tc), Ok(()), "discovery off and datagrams disabled apply");
profile.congestion = Some(Congestion::Cubic);
let mut tc = quinn::TransportConfig::default();
assert_eq!(profile.apply_to(&mut tc), Ok(()), "cubic applies");
}
#[test]
fn into_config_and_apply_to_agree_on_acceptance_and_on_the_error() {
let profile = TransportProfile::default();
let mut tc = quinn::TransportConfig::default();
assert_eq!(
profile.apply_to(&mut tc).is_ok(),
profile.into_config().is_ok(),
"a default profile is accepted by both or by neither"
);
let mut profile = healthy();
profile.initial_mtu = Some(800);
let mut tc = quinn::TransportConfig::default();
assert_eq!(
profile.apply_to(&mut tc),
profile.into_config().map(|_| ()),
"a refused profile is refused identically by both"
);
assert_eq!(
profile.into_config().map(|_| ()),
Err(TransportProfileError::MtuBelowFloor {
field: "initial_mtu",
value: 800,
floor: 1200
}),
"and the error is the one `validate` gives"
);
}
#[derive(Default)]
struct RecordingInstaller {
seen: std::sync::Mutex<Vec<TransportProfile>>,
}
impl TransportInstaller for RecordingInstaller {
fn build(
&self,
profile: &TransportProfile,
) -> Result<quinn::TransportConfig, TransportProfileError> {
self.seen.lock().expect("no test holds this across a panic").push(profile.clone());
profile.into_config()
}
}
fn resolve_uncaptured(
leg: Leg,
raw: Option<Arc<quinn::TransportConfig>>,
profile: Option<&TransportProfile>,
installer: Option<&Arc<dyn TransportInstaller>>,
) -> Result<Option<Arc<quinn::TransportConfig>>, ProxyError> {
resolve(
leg,
raw,
profile,
installer,
#[cfg(feature = "qlog")]
None,
)
}
#[test]
fn a_leg_naming_neither_installs_nothing() {
assert!(
resolve_uncaptured(Leg::Client, None, None, None)
.expect("nothing named is nothing to refuse")
.is_none(),
"a leg with no opinion has to stay exactly as it was before profiles existed"
);
}
#[test]
fn a_raw_config_is_installed_as_it_was_given() {
let raw = Arc::new(quinn::TransportConfig::default());
let resolved = resolve_uncaptured(Leg::Client, Some(Arc::clone(&raw)), None, None)
.expect("a raw config alone is not a contradiction")
.expect("and it is what the leg installs");
assert!(
Arc::ptr_eq(&raw, &resolved),
"the caller's own config must reach the leg, not a copy of it — there is no copy"
);
}
#[test]
fn a_profile_alone_is_built_by_the_default_installer() {
let profile = TransportProfile { initial_mtu: Some(1350), ..Default::default() };
assert!(
resolve_uncaptured(Leg::Upstream, None, Some(&profile), None)
.expect("a valid profile builds")
.is_some(),
"a leg carrying only a profile installs the config built from it"
);
}
#[test]
fn a_supplied_installer_is_what_builds_the_profile() {
let installer = Arc::new(RecordingInstaller::default());
let dynamic: Arc<dyn TransportInstaller> = installer.clone();
let profile = TransportProfile { congestion: Some(Congestion::Bbr), ..Default::default() };
assert!(resolve_uncaptured(Leg::Client, None, Some(&profile), Some(&dynamic))
.expect("the installer accepted the profile")
.is_some());
let seen = installer.seen.lock().expect("uncontended");
assert_eq!(seen.len(), 1, "the installer is consulted exactly once per leg");
assert_eq!(
seen[0], profile,
"the leg must hand the caller's own profile to the caller's own installer"
);
}
#[test]
fn a_leg_naming_both_a_config_and_a_profile_is_refused_with_its_own_leg() {
for leg in [Leg::Client, Leg::Upstream] {
let raw = Arc::new(quinn::TransportConfig::default());
let err = resolve_uncaptured(leg, Some(raw), Some(&TransportProfile::default()), None)
.expect_err("naming both is a contradiction, not a merge");
assert!(
matches!(err, ProxyError::TransportConfigAndProfile { leg: reported } if reported == leg),
"{leg:?} must be refused as {leg:?}, got {err}"
);
assert!(
err.to_string().contains("apply_to"),
"the message has to name the supported way to have both, or the first reader \
takes this for a regression: {err}"
);
}
}
#[test]
fn a_profile_the_installer_refuses_refuses_the_leg_and_names_it() {
let profile = TransportProfile { initial_mtu: Some(900), ..Default::default() };
let err = resolve_uncaptured(Leg::Upstream, None, Some(&profile), None)
.expect_err("an unhonourable profile must not become a connection");
assert!(
matches!(
err,
ProxyError::TransportProfile {
leg: Leg::Upstream,
source: TransportProfileError::MtuBelowFloor { field: "initial_mtu", .. },
}
),
"the refusal carries both the leg and the reason: {err}"
);
}
#[cfg(feature = "qlog")]
#[derive(Clone)]
struct Captured(Arc<std::sync::Mutex<Vec<u8>>>);
#[cfg(feature = "qlog")]
impl std::io::Write for Captured {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().expect("no test holds this across a panic").extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[cfg(feature = "qlog")]
fn spec_over_a_sink() -> (crate::qlog::QlogSpec, Arc<std::sync::Mutex<Vec<u8>>>) {
let sink = Arc::new(std::sync::Mutex::new(Vec::new()));
let spec = crate::qlog::QlogSpec {
writer: Some(Box::new(Captured(Arc::clone(&sink)))),
title: Some("a leg".to_string()),
description: None,
};
(spec, sink)
}
#[cfg(feature = "qlog")]
fn written(sink: &Arc<std::sync::Mutex<Vec<u8>>>) -> usize {
sink.lock().expect("uncontended").len()
}
#[cfg(feature = "qlog")]
#[test]
fn a_spec_alone_installs_a_config_for_the_sink_to_go_on() {
let (spec, sink) = spec_over_a_sink();
let resolved = resolve(Leg::Client, None, None, None, Some(spec))
.expect("a spec with a writer is not a contradiction")
.expect("a leg asking only for a capture still installs the config carrying it");
assert!(
written(&sink) > 0,
"the preamble is written when the sink is built, so an empty writer means the spec \
never became one"
);
drop(resolved);
}
#[cfg(feature = "qlog")]
#[test]
fn a_profile_and_a_spec_are_applied_to_one_config_built_by_the_installer() {
let installer = Arc::new(RecordingInstaller::default());
let dynamic: Arc<dyn TransportInstaller> = installer.clone();
let profile = TransportProfile { initial_mtu: Some(1350), ..Default::default() };
let (spec, sink) = spec_over_a_sink();
assert!(
resolve(Leg::Upstream, None, Some(&profile), Some(&dynamic), Some(spec))
.expect("a profile and a spec are not a contradiction")
.is_some(),
"a leg carrying both installs the one config they were both written into"
);
assert!(written(&sink) > 0, "and the sink is on that config");
let seen = installer.seen.lock().expect("uncontended");
assert_eq!(
seen.as_slice(),
&[profile],
"a spec must not bypass the caller's installer: a leg that built its own config here \
would run on a base nobody supplied and report success"
);
}
#[cfg(feature = "qlog")]
#[test]
fn a_leg_naming_both_a_config_and_a_spec_is_refused_with_its_own_leg() {
for leg in [Leg::Client, Leg::Upstream] {
let (spec, sink) = spec_over_a_sink();
let raw = Arc::new(quinn::TransportConfig::default());
let err = resolve(leg, Some(raw), None, None, Some(spec))
.expect_err("a config the sink cannot be installed on is not a leg with a capture");
assert!(
matches!(err, ProxyError::TransportConfigAndQlog { leg: reported } if reported == leg),
"{leg:?} must be refused as {leg:?}, and as the config-and-spec pair rather than \
the config-and-profile one — the two have different fixes: {err}"
);
assert_eq!(
written(&sink),
0,
"and nothing may be written on the way to refusing: a preamble here is a file the \
caller will read as the start of a capture that never happened"
);
}
}
#[cfg(feature = "qlog")]
#[test]
fn a_leg_naming_all_three_hears_about_the_config_and_the_profile() {
let (spec, sink) = spec_over_a_sink();
let raw = Arc::new(quinn::TransportConfig::default());
let err =
resolve(Leg::Client, Some(raw), Some(&TransportProfile::default()), None, Some(spec))
.expect_err("three fields that cannot be combined are still a refusal");
assert!(
matches!(err, ProxyError::TransportConfigAndProfile { leg: Leg::Client }),
"the answer has to be fixed rather than whichever check ran first: {err}"
);
assert_eq!(written(&sink), 0, "and no capture is begun for a leg that is refused");
}
#[cfg(feature = "qlog")]
#[test]
fn a_spec_with_no_writer_refuses_the_leg_that_carries_it() {
let blind = crate::qlog::QlogSpec {
writer: None,
title: Some("a leg".to_string()),
description: None,
};
let err = resolve(Leg::Upstream, None, None, None, Some(blind))
.expect_err("a spec with nowhere to write is a mistake, not a request for no capture");
assert!(
matches!(
err,
ProxyError::Qlog { leg: Leg::Upstream, source: crate::qlog::QlogError::NoWriter }
),
"the refusal carries both the leg and the reason: {err}"
);
}
#[test]
fn the_ack_frequency_default_is_quinns_own_and_not_a_derived_one() {
let ack = AckFrequency::default();
assert_eq!(
(ack.ack_eliciting_threshold, ack.reordering_threshold),
(1, 2),
"a derived default would ask the peer to ack every packet and never ack reordering"
);
assert_eq!(ack.max_ack_delay, None, "`None` leaves the peer's advertised delay in place");
}
}