1use std::net;
13use std::path::PathBuf;
14use std::time::Duration;
15
16#[serde_with::serde_as]
21#[derive(Clone, serde::Serialize, serde::Deserialize)]
22pub struct ServerId(#[serde_as(as = "serde_with::hex::Hex")] pub(crate) Vec<u8>);
23
24impl ServerId {
25 #[allow(dead_code)]
26 pub(crate) fn len(&self) -> usize {
27 self.0.len()
28 }
29}
30
31impl std::fmt::Debug for ServerId {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_tuple("ServerId").field(&hex::encode(&self.0)).finish()
34 }
35}
36
37impl std::str::FromStr for ServerId {
38 type Err = hex::FromHexError;
39
40 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
41 hex::decode(s).map(Self)
42 }
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
51#[serde(rename_all = "kebab-case")]
52#[non_exhaustive]
53pub enum CongestionControl {
54 Loss,
57 Delay,
61}
62
63impl std::str::FromStr for CongestionControl {
65 type Err = String;
66
67 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
68 <Self as clap::ValueEnum>::from_str(s, true)
69 }
70}
71
72pub(crate) const DEFAULT_MAX_STREAMS: u64 = 1024;
74
75pub(crate) const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
77
78pub(crate) const DEFAULT_KEEP_ALIVE: Duration = Duration::from_secs(5);
80
81#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
83#[serde(deny_unknown_fields, default)]
84#[non_exhaustive]
85pub struct Client {
86 #[serde(skip_serializing_if = "Option::is_none")]
89 #[arg(
90 id = "client-quic-max-streams",
91 long = "client-quic-max-streams",
92 alias = "client-max-streams",
93 env = "MOQ_CLIENT_QUIC_MAX_STREAMS"
94 )]
95 pub max_streams: Option<u64>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
103 #[arg(
104 id = "client-quic-gso",
105 long = "client-quic-gso",
106 env = "MOQ_CLIENT_QUIC_GSO",
107 default_missing_value = "true",
108 num_args = 0..=1,
109 require_equals = true,
110 value_parser = clap::value_parser!(bool),
111 )]
112 pub gso: Option<bool>,
113
114 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
116 #[arg(
117 id = "client-quic-idle-timeout",
118 long = "client-quic-idle-timeout",
119 env = "MOQ_CLIENT_QUIC_IDLE_TIMEOUT",
120 value_parser = humantime::parse_duration,
121 )]
122 pub idle_timeout: Option<Duration>,
123
124 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
127 #[arg(
128 id = "client-quic-keep-alive",
129 long = "client-quic-keep-alive",
130 env = "MOQ_CLIENT_QUIC_KEEP_ALIVE",
131 value_parser = humantime::parse_duration,
132 )]
133 pub keep_alive: Option<Duration>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 #[arg(
138 id = "client-quic-mtu-discovery",
139 long = "client-quic-mtu-discovery",
140 env = "MOQ_CLIENT_QUIC_MTU_DISCOVERY",
141 default_missing_value = "true",
142 num_args = 0..=1,
143 require_equals = true,
144 value_parser = clap::value_parser!(bool),
145 )]
146 pub mtu_discovery: Option<bool>,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
151 #[arg(
152 id = "client-quic-congestion-control",
153 long = "client-quic-congestion-control",
154 env = "MOQ_CLIENT_QUIC_CONGESTION_CONTROL",
155 value_enum
156 )]
157 pub congestion_control: Option<CongestionControl>,
158
159 #[serde(default, skip_serializing_if = "Option::is_none")]
161 #[arg(id = "client-quic-qlog", long = "client-quic-qlog", env = "MOQ_CLIENT_QUIC_QLOG")]
162 pub qlog: Option<PathBuf>,
163}
164
165fn validate_qlog(qlog: Option<&PathBuf>) -> crate::Result<()> {
171 match qlog {
172 Some(_) if cfg!(not(feature = "qlog")) => Err(crate::Error::QlogUnsupported),
173 _ => Ok(()),
174 }
175}
176
177const MAX_IDLE_TIMEOUT: Duration = Duration::from_millis((1 << 62) - 1);
180
181fn validate_idle_timeout(idle_timeout: Option<Duration>) -> crate::Result<()> {
186 match idle_timeout {
187 Some(timeout) if timeout > MAX_IDLE_TIMEOUT => Err(crate::Error::IdleTimeoutRange),
188 _ => Ok(()),
189 }
190}
191
192impl Client {
193 pub(crate) fn validate(&self) -> crate::Result<()> {
195 validate_qlog(self.qlog.as_ref())?;
196 validate_idle_timeout(self.idle_timeout)
197 }
198
199 pub fn resolve(&self) -> Resolved {
201 Resolved::new(
202 self.max_streams,
203 self.gso,
204 self.idle_timeout,
205 self.keep_alive,
206 self.mtu_discovery,
207 self.congestion_control,
208 self.qlog.clone(),
209 )
210 }
211}
212
213impl Default for Resolved {
215 fn default() -> Self {
216 Client::default().resolve()
217 }
218}
219
220#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
225#[serde(deny_unknown_fields, default)]
226#[non_exhaustive]
227pub struct Server {
228 #[serde(skip_serializing_if = "Option::is_none")]
231 #[arg(
232 id = "server-quic-max-streams",
233 long = "server-quic-max-streams",
234 alias = "server-max-streams",
235 env = "MOQ_SERVER_QUIC_MAX_STREAMS"
236 )]
237 pub max_streams: Option<u64>,
238
239 #[serde(skip_serializing_if = "Option::is_none")]
241 #[arg(
242 id = "server-quic-gso",
243 long = "server-quic-gso",
244 env = "MOQ_SERVER_QUIC_GSO",
245 default_missing_value = "true",
246 num_args = 0..=1,
247 require_equals = true,
248 value_parser = clap::value_parser!(bool),
249 )]
250 pub gso: Option<bool>,
251
252 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
254 #[arg(
255 id = "server-quic-idle-timeout",
256 long = "server-quic-idle-timeout",
257 env = "MOQ_SERVER_QUIC_IDLE_TIMEOUT",
258 value_parser = humantime::parse_duration,
259 )]
260 pub idle_timeout: Option<Duration>,
261
262 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
264 #[arg(
265 id = "server-quic-keep-alive",
266 long = "server-quic-keep-alive",
267 env = "MOQ_SERVER_QUIC_KEEP_ALIVE",
268 value_parser = humantime::parse_duration,
269 )]
270 pub keep_alive: Option<Duration>,
271
272 #[serde(skip_serializing_if = "Option::is_none")]
274 #[arg(
275 id = "server-quic-mtu-discovery",
276 long = "server-quic-mtu-discovery",
277 env = "MOQ_SERVER_QUIC_MTU_DISCOVERY",
278 default_missing_value = "true",
279 num_args = 0..=1,
280 require_equals = true,
281 value_parser = clap::value_parser!(bool),
282 )]
283 pub mtu_discovery: Option<bool>,
284
285 #[serde(skip_serializing_if = "Option::is_none")]
288 #[arg(
289 id = "server-quic-congestion-control",
290 long = "server-quic-congestion-control",
291 env = "MOQ_SERVER_QUIC_CONGESTION_CONTROL",
292 value_enum
293 )]
294 pub congestion_control: Option<CongestionControl>,
295
296 #[arg(
304 id = "server-preferred-v4",
305 long = "server-preferred-v4",
306 env = "MOQ_SERVER_PREFERRED_V4"
307 )]
308 #[serde(default, skip_serializing_if = "Option::is_none")]
309 pub preferred_v4: Option<net::SocketAddrV4>,
310
311 #[arg(
313 id = "server-preferred-v6",
314 long = "server-preferred-v6",
315 env = "MOQ_SERVER_PREFERRED_V6"
316 )]
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub preferred_v6: Option<net::SocketAddrV6>,
319
320 #[arg(id = "server-quic-lb-id", long = "server-quic-lb-id", env = "MOQ_SERVER_QUIC_LB_ID")]
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub quic_lb_id: Option<ServerId>,
325
326 #[arg(
329 id = "server-quic-lb-nonce",
330 long = "server-quic-lb-nonce",
331 requires = "server-quic-lb-id",
332 env = "MOQ_SERVER_QUIC_LB_NONCE"
333 )]
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub quic_lb_nonce: Option<usize>,
336
337 #[serde(default, skip_serializing_if = "Option::is_none")]
345 #[arg(id = "server-quic-qlog", long = "server-quic-qlog", env = "MOQ_SERVER_QUIC_QLOG")]
346 pub qlog: Option<PathBuf>,
347}
348
349impl Server {
350 pub(crate) fn validate(&self) -> crate::Result<()> {
352 validate_qlog(self.qlog.as_ref())?;
353 validate_idle_timeout(self.idle_timeout)
354 }
355
356 #[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
358 pub(crate) fn resolve(&self) -> Resolved {
359 Resolved::new(
360 self.max_streams,
361 self.gso,
362 self.idle_timeout,
363 self.keep_alive,
364 self.mtu_discovery,
365 self.congestion_control,
366 self.qlog.clone(),
367 )
368 }
369}
370
371#[derive(Clone, Debug)]
381#[non_exhaustive]
382pub struct Resolved {
383 pub max_streams: u64,
385 pub gso: Option<bool>,
387 pub idle_timeout: Duration,
389 pub keep_alive: Option<Duration>,
391 pub mtu_discovery: bool,
393 pub congestion_control: Option<CongestionControl>,
396 pub qlog: Option<PathBuf>,
398}
399
400impl Resolved {
401 fn new(
402 max_streams: Option<u64>,
403 gso: Option<bool>,
404 idle_timeout: Option<Duration>,
405 keep_alive: Option<Duration>,
406 mtu_discovery: Option<bool>,
407 congestion_control: Option<CongestionControl>,
408 qlog: Option<PathBuf>,
409 ) -> Self {
410 let keep_alive = match keep_alive {
413 Some(d) if d.is_zero() => None,
414 Some(d) => Some(d),
415 None => Some(DEFAULT_KEEP_ALIVE),
416 };
417
418 Self {
419 max_streams: max_streams.unwrap_or(DEFAULT_MAX_STREAMS),
420 gso,
421 idle_timeout: idle_timeout.unwrap_or(DEFAULT_IDLE_TIMEOUT),
422 keep_alive,
423 mtu_discovery: mtu_discovery.unwrap_or(false),
424 congestion_control,
425 qlog,
426 }
427 }
428
429 #[cfg_attr(
436 not(any(feature = "quinn", feature = "noq", feature = "quiche", feature = "iroh")),
437 allow(dead_code)
438 )]
439 pub(crate) fn congestion(&self) -> CongestionControl {
440 self.congestion_control.unwrap_or(CongestionControl::Delay)
441 }
442
443 #[cfg_attr(not(any(feature = "quinn", feature = "noq", feature = "quiche")), allow(dead_code))]
448 pub(crate) fn qlog_dir(&self) -> Option<&std::path::Path> {
449 self.qlog.as_deref()
450 }
451
452 #[cfg_attr(not(feature = "iroh"), allow(dead_code))]
457 pub(crate) fn gso_disabled(&self) -> bool {
458 self.gso == Some(false)
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use clap::Parser;
466
467 #[derive(Parser)]
470 struct Both {
471 #[command(flatten)]
472 client: Client,
473 #[command(flatten)]
474 server: Server,
475 }
476
477 fn parse(args: &[&str]) -> Both {
478 let mut full = vec!["test"];
479 full.extend_from_slice(args);
480 Both::parse_from(full)
481 }
482
483 #[test]
484 fn defaults_apply_when_unset() {
485 let quic = Client::default().resolve();
486 assert_eq!(quic.max_streams, DEFAULT_MAX_STREAMS);
487 assert_eq!(quic.idle_timeout, DEFAULT_IDLE_TIMEOUT);
488 assert_eq!(quic.keep_alive, Some(DEFAULT_KEEP_ALIVE));
489 assert!(!quic.mtu_discovery);
490 assert_eq!(quic.gso, None);
491 assert!(!quic.gso_disabled());
492 }
493
494 #[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
496 #[test]
497 fn zero_keep_alive_disables_it() {
498 let disabled = Server {
499 keep_alive: Some(Duration::ZERO),
500 ..Default::default()
501 };
502 assert_eq!(disabled.resolve().keep_alive, None);
503
504 let explicit = Client {
505 keep_alive: Some(Duration::from_secs(2)),
506 ..Default::default()
507 };
508 assert_eq!(explicit.resolve().keep_alive, Some(Duration::from_secs(2)));
509 }
510
511 #[test]
512 fn gso_disabled_only_on_explicit_false() {
513 let off = Client {
514 gso: Some(false),
515 ..Default::default()
516 };
517 assert!(off.resolve().gso_disabled());
518 let on = Client {
519 gso: Some(true),
520 ..Default::default()
521 };
522 assert!(!on.resolve().gso_disabled());
523 }
524
525 #[test]
526 fn client_and_server_flags_are_distinct() {
527 let both = parse(&["--client-quic-max-streams", "5000", "--server-quic-max-streams", "9000"]);
528 assert_eq!(both.client.max_streams, Some(5000));
529 assert_eq!(both.server.max_streams, Some(9000));
530 }
531
532 #[test]
533 fn server_only_knobs_parse() {
534 let both = parse(&["--server-preferred-v4", "192.0.2.1:443", "--server-quic-lb-id", "ab"]);
535 assert_eq!(both.server.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
536 assert!(both.server.quic_lb_id.is_some());
537 assert_eq!(both.client.max_streams, None);
539 }
540
541 #[test]
542 fn deprecated_max_streams_aliases() {
543 let both = parse(&["--client-max-streams", "2048", "--server-max-streams", "4096"]);
544 assert_eq!(both.client.max_streams, Some(2048));
545 assert_eq!(both.server.max_streams, Some(4096));
546 }
547
548 #[test]
549 fn qlog_flags_are_distinct_per_role() {
550 let both = parse(&["--client-quic-qlog", "/tmp/client", "--server-quic-qlog", "/tmp/server"]);
551 assert_eq!(both.client.qlog.as_deref(), Some(std::path::Path::new("/tmp/client")));
552 assert_eq!(both.server.qlog.as_deref(), Some(std::path::Path::new("/tmp/server")));
553
554 assert_eq!(
555 both.client.resolve().qlog_dir(),
556 Some(std::path::Path::new("/tmp/client"))
557 );
558 assert_eq!(Client::default().resolve().qlog_dir(), None);
559 }
560
561 #[test]
564 fn qlog_requires_the_feature() {
565 let unset = Client::default().validate();
566 assert!(unset.is_ok(), "no directory configured is always fine");
567
568 let set = Client {
569 qlog: Some("/tmp/qlog".into()),
570 ..Default::default()
571 };
572
573 if cfg!(feature = "qlog") {
574 assert!(set.validate().is_ok());
575 } else {
576 assert!(matches!(set.validate(), Err(crate::Error::QlogUnsupported)));
577 }
578 }
579
580 #[test]
583 fn idle_timeout_beyond_the_varint_is_rejected() {
584 let over = Client {
585 idle_timeout: Some(MAX_IDLE_TIMEOUT + Duration::from_millis(1)),
586 ..Default::default()
587 };
588 assert!(matches!(over.validate(), Err(crate::Error::IdleTimeoutRange)));
589
590 let server = Server {
591 idle_timeout: Some(Duration::from_millis(u64::MAX)),
592 ..Default::default()
593 };
594 assert!(matches!(server.validate(), Err(crate::Error::IdleTimeoutRange)));
595
596 let at_limit = Client {
597 idle_timeout: Some(MAX_IDLE_TIMEOUT),
598 ..Default::default()
599 };
600 assert!(at_limit.validate().is_ok());
601 }
602
603 #[test]
604 fn toml_round_trips() {
605 let toml = r#"
606 max_streams = 7000
607 gso = false
608 preferred_v4 = "192.0.2.1:443"
609 congestion_control = "delay"
610 qlog = "/tmp/qlog"
611 "#;
612 let quic: Server = toml::from_str(toml).unwrap();
613 assert_eq!(quic.max_streams, Some(7000));
614 assert_eq!(quic.gso, Some(false));
615 assert_eq!(quic.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
616 assert_eq!(quic.congestion_control, Some(CongestionControl::Delay));
617 assert_eq!(quic.qlog.as_deref(), Some(std::path::Path::new("/tmp/qlog")));
618 }
619
620 #[test]
621 fn congestion_control_flags_parse() {
622 let both = parse(&[
623 "--client-quic-congestion-control",
624 "delay",
625 "--server-quic-congestion-control",
626 "loss",
627 ]);
628 assert_eq!(both.client.congestion_control, Some(CongestionControl::Delay));
629 assert_eq!(both.server.congestion_control, Some(CongestionControl::Loss));
630
631 assert_eq!(Client::default().resolve().congestion_control, None);
633 }
634
635 #[test]
638 fn congestion_defaults_to_delay() {
639 let mut quic = Client::default();
640 assert_eq!(quic.resolve().congestion(), CongestionControl::Delay);
641
642 quic.congestion_control = Some(CongestionControl::Loss);
644 assert_eq!(quic.resolve().congestion(), CongestionControl::Loss);
645 }
646}