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")]
152 #[arg(
153 id = "client-quic-congestion-control",
154 long = "client-quic-congestion-control",
155 env = "MOQ_CLIENT_QUIC_CONGESTION_CONTROL",
156 value_enum
157 )]
158 pub congestion_control: Option<CongestionControl>,
159
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 #[arg(id = "client-quic-qlog", long = "client-quic-qlog", env = "MOQ_CLIENT_QUIC_QLOG")]
163 pub qlog: Option<PathBuf>,
164}
165
166fn validate_qlog(qlog: Option<&PathBuf>) -> crate::Result<()> {
172 match qlog {
173 Some(_) if cfg!(not(feature = "qlog")) => Err(crate::Error::QlogUnsupported),
174 _ => Ok(()),
175 }
176}
177
178const MAX_IDLE_TIMEOUT: Duration = Duration::from_millis((1 << 62) - 1);
181
182fn validate_idle_timeout(idle_timeout: Option<Duration>) -> crate::Result<()> {
187 match idle_timeout {
188 Some(timeout) if timeout > MAX_IDLE_TIMEOUT => Err(crate::Error::IdleTimeoutRange),
189 _ => Ok(()),
190 }
191}
192
193impl Client {
194 pub(crate) fn validate(&self) -> crate::Result<()> {
196 validate_qlog(self.qlog.as_ref())?;
197 validate_idle_timeout(self.idle_timeout)
198 }
199
200 pub fn resolve(&self) -> Resolved {
202 Resolved::new(
203 self.max_streams,
204 self.gso,
205 self.idle_timeout,
206 self.keep_alive,
207 self.mtu_discovery,
208 self.congestion_control,
209 self.qlog.clone(),
210 )
211 }
212}
213
214impl Default for Resolved {
216 fn default() -> Self {
217 Client::default().resolve()
218 }
219}
220
221#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
226#[serde(deny_unknown_fields, default)]
227#[non_exhaustive]
228pub struct Server {
229 #[serde(skip_serializing_if = "Option::is_none")]
232 #[arg(
233 id = "server-quic-max-streams",
234 long = "server-quic-max-streams",
235 alias = "server-max-streams",
236 env = "MOQ_SERVER_QUIC_MAX_STREAMS"
237 )]
238 pub max_streams: Option<u64>,
239
240 #[serde(skip_serializing_if = "Option::is_none")]
242 #[arg(
243 id = "server-quic-gso",
244 long = "server-quic-gso",
245 env = "MOQ_SERVER_QUIC_GSO",
246 default_missing_value = "true",
247 num_args = 0..=1,
248 require_equals = true,
249 value_parser = clap::value_parser!(bool),
250 )]
251 pub gso: Option<bool>,
252
253 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
255 #[arg(
256 id = "server-quic-idle-timeout",
257 long = "server-quic-idle-timeout",
258 env = "MOQ_SERVER_QUIC_IDLE_TIMEOUT",
259 value_parser = humantime::parse_duration,
260 )]
261 pub idle_timeout: Option<Duration>,
262
263 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
265 #[arg(
266 id = "server-quic-keep-alive",
267 long = "server-quic-keep-alive",
268 env = "MOQ_SERVER_QUIC_KEEP_ALIVE",
269 value_parser = humantime::parse_duration,
270 )]
271 pub keep_alive: Option<Duration>,
272
273 #[serde(skip_serializing_if = "Option::is_none")]
275 #[arg(
276 id = "server-quic-mtu-discovery",
277 long = "server-quic-mtu-discovery",
278 env = "MOQ_SERVER_QUIC_MTU_DISCOVERY",
279 default_missing_value = "true",
280 num_args = 0..=1,
281 require_equals = true,
282 value_parser = clap::value_parser!(bool),
283 )]
284 pub mtu_discovery: Option<bool>,
285
286 #[serde(skip_serializing_if = "Option::is_none")]
290 #[arg(
291 id = "server-quic-congestion-control",
292 long = "server-quic-congestion-control",
293 env = "MOQ_SERVER_QUIC_CONGESTION_CONTROL",
294 value_enum
295 )]
296 pub congestion_control: Option<CongestionControl>,
297
298 #[arg(
306 id = "server-preferred-v4",
307 long = "server-preferred-v4",
308 env = "MOQ_SERVER_PREFERRED_V4"
309 )]
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub preferred_v4: Option<net::SocketAddrV4>,
312
313 #[arg(
315 id = "server-preferred-v6",
316 long = "server-preferred-v6",
317 env = "MOQ_SERVER_PREFERRED_V6"
318 )]
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub preferred_v6: Option<net::SocketAddrV6>,
321
322 #[arg(id = "server-quic-lb-id", long = "server-quic-lb-id", env = "MOQ_SERVER_QUIC_LB_ID")]
325 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub quic_lb_id: Option<ServerId>,
327
328 #[arg(
331 id = "server-quic-lb-nonce",
332 long = "server-quic-lb-nonce",
333 requires = "server-quic-lb-id",
334 env = "MOQ_SERVER_QUIC_LB_NONCE"
335 )]
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub quic_lb_nonce: Option<usize>,
338
339 #[serde(default, skip_serializing_if = "Option::is_none")]
347 #[arg(id = "server-quic-qlog", long = "server-quic-qlog", env = "MOQ_SERVER_QUIC_QLOG")]
348 pub qlog: Option<PathBuf>,
349}
350
351impl Server {
352 pub(crate) fn validate(&self) -> crate::Result<()> {
354 validate_qlog(self.qlog.as_ref())?;
355 validate_idle_timeout(self.idle_timeout)
356 }
357
358 #[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
360 pub(crate) fn resolve(&self) -> Resolved {
361 Resolved::new(
362 self.max_streams,
363 self.gso,
364 self.idle_timeout,
365 self.keep_alive,
366 self.mtu_discovery,
367 self.congestion_control,
368 self.qlog.clone(),
369 )
370 }
371}
372
373#[derive(Clone, Debug)]
383#[non_exhaustive]
384pub struct Resolved {
385 pub max_streams: u64,
387 pub gso: Option<bool>,
389 pub idle_timeout: Duration,
391 pub keep_alive: Option<Duration>,
393 pub mtu_discovery: bool,
395 pub congestion_control: Option<CongestionControl>,
398 pub qlog: Option<PathBuf>,
400}
401
402impl Resolved {
403 fn new(
404 max_streams: Option<u64>,
405 gso: Option<bool>,
406 idle_timeout: Option<Duration>,
407 keep_alive: Option<Duration>,
408 mtu_discovery: Option<bool>,
409 congestion_control: Option<CongestionControl>,
410 qlog: Option<PathBuf>,
411 ) -> Self {
412 let keep_alive = match keep_alive {
415 Some(d) if d.is_zero() => None,
416 Some(d) => Some(d),
417 None => Some(DEFAULT_KEEP_ALIVE),
418 };
419
420 Self {
421 max_streams: max_streams.unwrap_or(DEFAULT_MAX_STREAMS),
422 gso,
423 idle_timeout: idle_timeout.unwrap_or(DEFAULT_IDLE_TIMEOUT),
424 keep_alive,
425 mtu_discovery: mtu_discovery.unwrap_or(false),
426 congestion_control,
427 qlog,
428 }
429 }
430
431 #[cfg_attr(not(any(feature = "quinn", feature = "noq", feature = "quiche")), allow(dead_code))]
436 pub(crate) fn qlog_dir(&self) -> Option<&std::path::Path> {
437 self.qlog.as_deref()
438 }
439
440 #[cfg_attr(not(feature = "iroh"), allow(dead_code))]
445 pub(crate) fn gso_disabled(&self) -> bool {
446 self.gso == Some(false)
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 use clap::Parser;
454
455 #[derive(Parser)]
458 struct Both {
459 #[command(flatten)]
460 client: Client,
461 #[command(flatten)]
462 server: Server,
463 }
464
465 fn parse(args: &[&str]) -> Both {
466 let mut full = vec!["test"];
467 full.extend_from_slice(args);
468 Both::parse_from(full)
469 }
470
471 #[test]
472 fn defaults_apply_when_unset() {
473 let quic = Client::default().resolve();
474 assert_eq!(quic.max_streams, DEFAULT_MAX_STREAMS);
475 assert_eq!(quic.idle_timeout, DEFAULT_IDLE_TIMEOUT);
476 assert_eq!(quic.keep_alive, Some(DEFAULT_KEEP_ALIVE));
477 assert!(!quic.mtu_discovery);
478 assert_eq!(quic.gso, None);
479 assert!(!quic.gso_disabled());
480 }
481
482 #[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
484 #[test]
485 fn zero_keep_alive_disables_it() {
486 let disabled = Server {
487 keep_alive: Some(Duration::ZERO),
488 ..Default::default()
489 };
490 assert_eq!(disabled.resolve().keep_alive, None);
491
492 let explicit = Client {
493 keep_alive: Some(Duration::from_secs(2)),
494 ..Default::default()
495 };
496 assert_eq!(explicit.resolve().keep_alive, Some(Duration::from_secs(2)));
497 }
498
499 #[test]
500 fn gso_disabled_only_on_explicit_false() {
501 let off = Client {
502 gso: Some(false),
503 ..Default::default()
504 };
505 assert!(off.resolve().gso_disabled());
506 let on = Client {
507 gso: Some(true),
508 ..Default::default()
509 };
510 assert!(!on.resolve().gso_disabled());
511 }
512
513 #[test]
514 fn client_and_server_flags_are_distinct() {
515 let both = parse(&["--client-quic-max-streams", "5000", "--server-quic-max-streams", "9000"]);
516 assert_eq!(both.client.max_streams, Some(5000));
517 assert_eq!(both.server.max_streams, Some(9000));
518 }
519
520 #[test]
521 fn server_only_knobs_parse() {
522 let both = parse(&["--server-preferred-v4", "192.0.2.1:443", "--server-quic-lb-id", "ab"]);
523 assert_eq!(both.server.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
524 assert!(both.server.quic_lb_id.is_some());
525 assert_eq!(both.client.max_streams, None);
527 }
528
529 #[test]
530 fn deprecated_max_streams_aliases() {
531 let both = parse(&["--client-max-streams", "2048", "--server-max-streams", "4096"]);
532 assert_eq!(both.client.max_streams, Some(2048));
533 assert_eq!(both.server.max_streams, Some(4096));
534 }
535
536 #[test]
537 fn qlog_flags_are_distinct_per_role() {
538 let both = parse(&["--client-quic-qlog", "/tmp/client", "--server-quic-qlog", "/tmp/server"]);
539 assert_eq!(both.client.qlog.as_deref(), Some(std::path::Path::new("/tmp/client")));
540 assert_eq!(both.server.qlog.as_deref(), Some(std::path::Path::new("/tmp/server")));
541
542 assert_eq!(
543 both.client.resolve().qlog_dir(),
544 Some(std::path::Path::new("/tmp/client"))
545 );
546 assert_eq!(Client::default().resolve().qlog_dir(), None);
547 }
548
549 #[test]
552 fn qlog_requires_the_feature() {
553 let unset = Client::default().validate();
554 assert!(unset.is_ok(), "no directory configured is always fine");
555
556 let set = Client {
557 qlog: Some("/tmp/qlog".into()),
558 ..Default::default()
559 };
560
561 if cfg!(feature = "qlog") {
562 assert!(set.validate().is_ok());
563 } else {
564 assert!(matches!(set.validate(), Err(crate::Error::QlogUnsupported)));
565 }
566 }
567
568 #[test]
571 fn idle_timeout_beyond_the_varint_is_rejected() {
572 let over = Client {
573 idle_timeout: Some(MAX_IDLE_TIMEOUT + Duration::from_millis(1)),
574 ..Default::default()
575 };
576 assert!(matches!(over.validate(), Err(crate::Error::IdleTimeoutRange)));
577
578 let server = Server {
579 idle_timeout: Some(Duration::from_millis(u64::MAX)),
580 ..Default::default()
581 };
582 assert!(matches!(server.validate(), Err(crate::Error::IdleTimeoutRange)));
583
584 let at_limit = Client {
585 idle_timeout: Some(MAX_IDLE_TIMEOUT),
586 ..Default::default()
587 };
588 assert!(at_limit.validate().is_ok());
589 }
590
591 #[test]
592 fn toml_round_trips() {
593 let toml = r#"
594 max_streams = 7000
595 gso = false
596 preferred_v4 = "192.0.2.1:443"
597 congestion_control = "delay"
598 qlog = "/tmp/qlog"
599 "#;
600 let quic: Server = toml::from_str(toml).unwrap();
601 assert_eq!(quic.max_streams, Some(7000));
602 assert_eq!(quic.gso, Some(false));
603 assert_eq!(quic.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
604 assert_eq!(quic.congestion_control, Some(CongestionControl::Delay));
605 assert_eq!(quic.qlog.as_deref(), Some(std::path::Path::new("/tmp/qlog")));
606 }
607
608 #[test]
609 fn congestion_control_flags_parse() {
610 let both = parse(&[
611 "--client-quic-congestion-control",
612 "delay",
613 "--server-quic-congestion-control",
614 "loss",
615 ]);
616 assert_eq!(both.client.congestion_control, Some(CongestionControl::Delay));
617 assert_eq!(both.server.congestion_control, Some(CongestionControl::Loss));
618
619 assert_eq!(Client::default().resolve().congestion_control, None);
621 }
622}