use std::net;
use std::path::PathBuf;
use std::time::Duration;
#[serde_with::serde_as]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct ServerId(#[serde_as(as = "serde_with::hex::Hex")] pub(crate) Vec<u8>);
impl ServerId {
#[allow(dead_code)]
pub(crate) fn len(&self) -> usize {
self.0.len()
}
}
impl std::fmt::Debug for ServerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ServerId").field(&hex::encode(&self.0)).finish()
}
}
impl std::str::FromStr for ServerId {
type Err = hex::FromHexError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
hex::decode(s).map(Self)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum CongestionControl {
Loss,
Delay,
}
pub(crate) const DEFAULT_MAX_STREAMS: u64 = 1024;
pub(crate) const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) const DEFAULT_KEEP_ALIVE: Duration = Duration::from_secs(5);
#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Client {
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(
id = "client-quic-max-streams",
long = "client-quic-max-streams",
alias = "client-max-streams",
env = "MOQ_CLIENT_QUIC_MAX_STREAMS"
)]
pub max_streams: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(
id = "client-quic-gso",
long = "client-quic-gso",
env = "MOQ_CLIENT_QUIC_GSO",
default_missing_value = "true",
num_args = 0..=1,
require_equals = true,
value_parser = clap::value_parser!(bool),
)]
pub gso: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
#[arg(
id = "client-quic-idle-timeout",
long = "client-quic-idle-timeout",
env = "MOQ_CLIENT_QUIC_IDLE_TIMEOUT",
value_parser = humantime::parse_duration,
)]
pub idle_timeout: Option<Duration>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
#[arg(
id = "client-quic-keep-alive",
long = "client-quic-keep-alive",
env = "MOQ_CLIENT_QUIC_KEEP_ALIVE",
value_parser = humantime::parse_duration,
)]
pub keep_alive: Option<Duration>,
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(
id = "client-quic-mtu-discovery",
long = "client-quic-mtu-discovery",
env = "MOQ_CLIENT_QUIC_MTU_DISCOVERY",
default_missing_value = "true",
num_args = 0..=1,
require_equals = true,
value_parser = clap::value_parser!(bool),
)]
pub mtu_discovery: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(
id = "client-quic-congestion-control",
long = "client-quic-congestion-control",
env = "MOQ_CLIENT_QUIC_CONGESTION_CONTROL",
value_enum
)]
pub congestion_control: Option<CongestionControl>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[arg(id = "client-quic-qlog", long = "client-quic-qlog", env = "MOQ_CLIENT_QUIC_QLOG")]
pub qlog: Option<PathBuf>,
}
fn validate_qlog(qlog: Option<&PathBuf>) -> crate::Result<()> {
match qlog {
Some(_) if cfg!(not(feature = "qlog")) => Err(crate::Error::QlogUnsupported),
_ => Ok(()),
}
}
impl Client {
pub(crate) fn validate(&self) -> crate::Result<()> {
validate_qlog(self.qlog.as_ref())
}
pub(crate) fn resolve(&self) -> Resolved {
Resolved::new(
self.max_streams,
self.gso,
self.idle_timeout,
self.keep_alive,
self.mtu_discovery,
self.congestion_control,
self.qlog.clone(),
)
}
}
#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Server {
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(
id = "server-quic-max-streams",
long = "server-quic-max-streams",
alias = "server-max-streams",
env = "MOQ_SERVER_QUIC_MAX_STREAMS"
)]
pub max_streams: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(
id = "server-quic-gso",
long = "server-quic-gso",
env = "MOQ_SERVER_QUIC_GSO",
default_missing_value = "true",
num_args = 0..=1,
require_equals = true,
value_parser = clap::value_parser!(bool),
)]
pub gso: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
#[arg(
id = "server-quic-idle-timeout",
long = "server-quic-idle-timeout",
env = "MOQ_SERVER_QUIC_IDLE_TIMEOUT",
value_parser = humantime::parse_duration,
)]
pub idle_timeout: Option<Duration>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
#[arg(
id = "server-quic-keep-alive",
long = "server-quic-keep-alive",
env = "MOQ_SERVER_QUIC_KEEP_ALIVE",
value_parser = humantime::parse_duration,
)]
pub keep_alive: Option<Duration>,
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(
id = "server-quic-mtu-discovery",
long = "server-quic-mtu-discovery",
env = "MOQ_SERVER_QUIC_MTU_DISCOVERY",
default_missing_value = "true",
num_args = 0..=1,
require_equals = true,
value_parser = clap::value_parser!(bool),
)]
pub mtu_discovery: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(
id = "server-quic-congestion-control",
long = "server-quic-congestion-control",
env = "MOQ_SERVER_QUIC_CONGESTION_CONTROL",
value_enum
)]
pub congestion_control: Option<CongestionControl>,
#[arg(
id = "server-preferred-v4",
long = "server-preferred-v4",
env = "MOQ_SERVER_PREFERRED_V4"
)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preferred_v4: Option<net::SocketAddrV4>,
#[arg(
id = "server-preferred-v6",
long = "server-preferred-v6",
env = "MOQ_SERVER_PREFERRED_V6"
)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preferred_v6: Option<net::SocketAddrV6>,
#[arg(id = "server-quic-lb-id", long = "server-quic-lb-id", env = "MOQ_SERVER_QUIC_LB_ID")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quic_lb_id: Option<ServerId>,
#[arg(
id = "server-quic-lb-nonce",
long = "server-quic-lb-nonce",
requires = "server-quic-lb-id",
env = "MOQ_SERVER_QUIC_LB_NONCE"
)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quic_lb_nonce: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[arg(id = "server-quic-qlog", long = "server-quic-qlog", env = "MOQ_SERVER_QUIC_QLOG")]
pub qlog: Option<PathBuf>,
}
impl Server {
pub(crate) fn validate(&self) -> crate::Result<()> {
validate_qlog(self.qlog.as_ref())
}
pub(crate) fn resolve(&self) -> Resolved {
Resolved::new(
self.max_streams,
self.gso,
self.idle_timeout,
self.keep_alive,
self.mtu_discovery,
self.congestion_control,
self.qlog.clone(),
)
}
}
#[derive(Clone, Debug)]
pub(crate) struct Resolved {
pub max_streams: u64,
pub gso: Option<bool>,
pub idle_timeout: Duration,
pub keep_alive: Option<Duration>,
pub mtu_discovery: bool,
pub congestion_control: Option<CongestionControl>,
pub qlog: Option<PathBuf>,
}
impl Resolved {
fn new(
max_streams: Option<u64>,
gso: Option<bool>,
idle_timeout: Option<Duration>,
keep_alive: Option<Duration>,
mtu_discovery: Option<bool>,
congestion_control: Option<CongestionControl>,
qlog: Option<PathBuf>,
) -> Self {
let keep_alive = match keep_alive {
Some(d) if d.is_zero() => None,
Some(d) => Some(d),
None => Some(DEFAULT_KEEP_ALIVE),
};
Self {
max_streams: max_streams.unwrap_or(DEFAULT_MAX_STREAMS),
gso,
idle_timeout: idle_timeout.unwrap_or(DEFAULT_IDLE_TIMEOUT),
keep_alive,
mtu_discovery: mtu_discovery.unwrap_or(false),
congestion_control,
qlog,
}
}
#[cfg_attr(not(any(feature = "quinn", feature = "noq", feature = "quiche")), allow(dead_code))]
pub(crate) fn qlog_dir(&self) -> Option<&std::path::Path> {
self.qlog.as_deref()
}
#[cfg_attr(not(any(feature = "quiche", feature = "iroh")), allow(dead_code))]
pub(crate) fn gso_disabled(&self) -> bool {
self.gso == Some(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(Parser)]
struct Both {
#[command(flatten)]
client: Client,
#[command(flatten)]
server: Server,
}
fn parse(args: &[&str]) -> Both {
let mut full = vec!["test"];
full.extend_from_slice(args);
Both::parse_from(full)
}
#[test]
fn defaults_apply_when_unset() {
let quic = Client::default().resolve();
assert_eq!(quic.max_streams, DEFAULT_MAX_STREAMS);
assert_eq!(quic.idle_timeout, DEFAULT_IDLE_TIMEOUT);
assert_eq!(quic.keep_alive, Some(DEFAULT_KEEP_ALIVE));
assert!(!quic.mtu_discovery);
assert_eq!(quic.gso, None);
assert!(!quic.gso_disabled());
}
#[test]
fn zero_keep_alive_disables_it() {
let disabled = Server {
keep_alive: Some(Duration::ZERO),
..Default::default()
};
assert_eq!(disabled.resolve().keep_alive, None);
let explicit = Client {
keep_alive: Some(Duration::from_secs(2)),
..Default::default()
};
assert_eq!(explicit.resolve().keep_alive, Some(Duration::from_secs(2)));
}
#[test]
fn gso_disabled_only_on_explicit_false() {
let off = Client {
gso: Some(false),
..Default::default()
};
assert!(off.resolve().gso_disabled());
let on = Client {
gso: Some(true),
..Default::default()
};
assert!(!on.resolve().gso_disabled());
}
#[test]
fn client_and_server_flags_are_distinct() {
let both = parse(&["--client-quic-max-streams", "5000", "--server-quic-max-streams", "9000"]);
assert_eq!(both.client.max_streams, Some(5000));
assert_eq!(both.server.max_streams, Some(9000));
}
#[test]
fn server_only_knobs_parse() {
let both = parse(&["--server-preferred-v4", "192.0.2.1:443", "--server-quic-lb-id", "ab"]);
assert_eq!(both.server.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
assert!(both.server.quic_lb_id.is_some());
assert_eq!(both.client.max_streams, None);
}
#[test]
fn deprecated_max_streams_aliases() {
let both = parse(&["--client-max-streams", "2048", "--server-max-streams", "4096"]);
assert_eq!(both.client.max_streams, Some(2048));
assert_eq!(both.server.max_streams, Some(4096));
}
#[test]
fn qlog_flags_are_distinct_per_role() {
let both = parse(&["--client-quic-qlog", "/tmp/client", "--server-quic-qlog", "/tmp/server"]);
assert_eq!(both.client.qlog.as_deref(), Some(std::path::Path::new("/tmp/client")));
assert_eq!(both.server.qlog.as_deref(), Some(std::path::Path::new("/tmp/server")));
assert_eq!(
both.client.resolve().qlog_dir(),
Some(std::path::Path::new("/tmp/client"))
);
assert_eq!(Client::default().resolve().qlog_dir(), None);
}
#[test]
fn qlog_requires_the_feature() {
let unset = Client::default().validate();
assert!(unset.is_ok(), "no directory configured is always fine");
let set = Client {
qlog: Some("/tmp/qlog".into()),
..Default::default()
};
if cfg!(feature = "qlog") {
assert!(set.validate().is_ok());
} else {
assert!(matches!(set.validate(), Err(crate::Error::QlogUnsupported)));
}
}
#[test]
fn toml_round_trips() {
let toml = r#"
max_streams = 7000
gso = false
preferred_v4 = "192.0.2.1:443"
congestion_control = "delay"
qlog = "/tmp/qlog"
"#;
let quic: Server = toml::from_str(toml).unwrap();
assert_eq!(quic.max_streams, Some(7000));
assert_eq!(quic.gso, Some(false));
assert_eq!(quic.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
assert_eq!(quic.congestion_control, Some(CongestionControl::Delay));
assert_eq!(quic.qlog.as_deref(), Some(std::path::Path::new("/tmp/qlog")));
}
#[test]
fn congestion_control_flags_parse() {
let both = parse(&[
"--client-quic-congestion-control",
"delay",
"--server-quic-congestion-control",
"loss",
]);
assert_eq!(both.client.congestion_control, Some(CongestionControl::Delay));
assert_eq!(both.server.congestion_control, Some(CongestionControl::Loss));
assert_eq!(Client::default().resolve().congestion_control, None);
}
}