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 pub(crate) fn resolve(&self) -> Resolved {
360 Resolved::new(
361 self.max_streams,
362 self.gso,
363 self.idle_timeout,
364 self.keep_alive,
365 self.mtu_discovery,
366 self.congestion_control,
367 self.qlog.clone(),
368 )
369 }
370}
371
372#[derive(Clone, Debug)]
382#[non_exhaustive]
383pub struct Resolved {
384 pub max_streams: u64,
386 pub gso: Option<bool>,
388 pub idle_timeout: Duration,
390 pub keep_alive: Option<Duration>,
392 pub mtu_discovery: bool,
394 pub congestion_control: Option<CongestionControl>,
397 pub qlog: Option<PathBuf>,
399}
400
401impl Resolved {
402 fn new(
403 max_streams: Option<u64>,
404 gso: Option<bool>,
405 idle_timeout: Option<Duration>,
406 keep_alive: Option<Duration>,
407 mtu_discovery: Option<bool>,
408 congestion_control: Option<CongestionControl>,
409 qlog: Option<PathBuf>,
410 ) -> Self {
411 let keep_alive = match keep_alive {
414 Some(d) if d.is_zero() => None,
415 Some(d) => Some(d),
416 None => Some(DEFAULT_KEEP_ALIVE),
417 };
418
419 Self {
420 max_streams: max_streams.unwrap_or(DEFAULT_MAX_STREAMS),
421 gso,
422 idle_timeout: idle_timeout.unwrap_or(DEFAULT_IDLE_TIMEOUT),
423 keep_alive,
424 mtu_discovery: mtu_discovery.unwrap_or(false),
425 congestion_control,
426 qlog,
427 }
428 }
429
430 #[cfg_attr(not(any(feature = "quinn", feature = "noq", feature = "quiche")), allow(dead_code))]
435 pub(crate) fn qlog_dir(&self) -> Option<&std::path::Path> {
436 self.qlog.as_deref()
437 }
438
439 #[cfg_attr(not(feature = "iroh"), allow(dead_code))]
444 pub(crate) fn gso_disabled(&self) -> bool {
445 self.gso == Some(false)
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use clap::Parser;
453
454 #[derive(Parser)]
457 struct Both {
458 #[command(flatten)]
459 client: Client,
460 #[command(flatten)]
461 server: Server,
462 }
463
464 fn parse(args: &[&str]) -> Both {
465 let mut full = vec!["test"];
466 full.extend_from_slice(args);
467 Both::parse_from(full)
468 }
469
470 #[test]
471 fn defaults_apply_when_unset() {
472 let quic = Client::default().resolve();
473 assert_eq!(quic.max_streams, DEFAULT_MAX_STREAMS);
474 assert_eq!(quic.idle_timeout, DEFAULT_IDLE_TIMEOUT);
475 assert_eq!(quic.keep_alive, Some(DEFAULT_KEEP_ALIVE));
476 assert!(!quic.mtu_discovery);
477 assert_eq!(quic.gso, None);
478 assert!(!quic.gso_disabled());
479 }
480
481 #[test]
482 fn zero_keep_alive_disables_it() {
483 let disabled = Server {
484 keep_alive: Some(Duration::ZERO),
485 ..Default::default()
486 };
487 assert_eq!(disabled.resolve().keep_alive, None);
488
489 let explicit = Client {
490 keep_alive: Some(Duration::from_secs(2)),
491 ..Default::default()
492 };
493 assert_eq!(explicit.resolve().keep_alive, Some(Duration::from_secs(2)));
494 }
495
496 #[test]
497 fn gso_disabled_only_on_explicit_false() {
498 let off = Client {
499 gso: Some(false),
500 ..Default::default()
501 };
502 assert!(off.resolve().gso_disabled());
503 let on = Client {
504 gso: Some(true),
505 ..Default::default()
506 };
507 assert!(!on.resolve().gso_disabled());
508 }
509
510 #[test]
511 fn client_and_server_flags_are_distinct() {
512 let both = parse(&["--client-quic-max-streams", "5000", "--server-quic-max-streams", "9000"]);
513 assert_eq!(both.client.max_streams, Some(5000));
514 assert_eq!(both.server.max_streams, Some(9000));
515 }
516
517 #[test]
518 fn server_only_knobs_parse() {
519 let both = parse(&["--server-preferred-v4", "192.0.2.1:443", "--server-quic-lb-id", "ab"]);
520 assert_eq!(both.server.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
521 assert!(both.server.quic_lb_id.is_some());
522 assert_eq!(both.client.max_streams, None);
524 }
525
526 #[test]
527 fn deprecated_max_streams_aliases() {
528 let both = parse(&["--client-max-streams", "2048", "--server-max-streams", "4096"]);
529 assert_eq!(both.client.max_streams, Some(2048));
530 assert_eq!(both.server.max_streams, Some(4096));
531 }
532
533 #[test]
534 fn qlog_flags_are_distinct_per_role() {
535 let both = parse(&["--client-quic-qlog", "/tmp/client", "--server-quic-qlog", "/tmp/server"]);
536 assert_eq!(both.client.qlog.as_deref(), Some(std::path::Path::new("/tmp/client")));
537 assert_eq!(both.server.qlog.as_deref(), Some(std::path::Path::new("/tmp/server")));
538
539 assert_eq!(
540 both.client.resolve().qlog_dir(),
541 Some(std::path::Path::new("/tmp/client"))
542 );
543 assert_eq!(Client::default().resolve().qlog_dir(), None);
544 }
545
546 #[test]
549 fn qlog_requires_the_feature() {
550 let unset = Client::default().validate();
551 assert!(unset.is_ok(), "no directory configured is always fine");
552
553 let set = Client {
554 qlog: Some("/tmp/qlog".into()),
555 ..Default::default()
556 };
557
558 if cfg!(feature = "qlog") {
559 assert!(set.validate().is_ok());
560 } else {
561 assert!(matches!(set.validate(), Err(crate::Error::QlogUnsupported)));
562 }
563 }
564
565 #[test]
568 fn idle_timeout_beyond_the_varint_is_rejected() {
569 let over = Client {
570 idle_timeout: Some(MAX_IDLE_TIMEOUT + Duration::from_millis(1)),
571 ..Default::default()
572 };
573 assert!(matches!(over.validate(), Err(crate::Error::IdleTimeoutRange)));
574
575 let server = Server {
576 idle_timeout: Some(Duration::from_millis(u64::MAX)),
577 ..Default::default()
578 };
579 assert!(matches!(server.validate(), Err(crate::Error::IdleTimeoutRange)));
580
581 let at_limit = Client {
582 idle_timeout: Some(MAX_IDLE_TIMEOUT),
583 ..Default::default()
584 };
585 assert!(at_limit.validate().is_ok());
586 }
587
588 #[test]
589 fn toml_round_trips() {
590 let toml = r#"
591 max_streams = 7000
592 gso = false
593 preferred_v4 = "192.0.2.1:443"
594 congestion_control = "delay"
595 qlog = "/tmp/qlog"
596 "#;
597 let quic: Server = toml::from_str(toml).unwrap();
598 assert_eq!(quic.max_streams, Some(7000));
599 assert_eq!(quic.gso, Some(false));
600 assert_eq!(quic.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
601 assert_eq!(quic.congestion_control, Some(CongestionControl::Delay));
602 assert_eq!(quic.qlog.as_deref(), Some(std::path::Path::new("/tmp/qlog")));
603 }
604
605 #[test]
606 fn congestion_control_flags_parse() {
607 let both = parse(&[
608 "--client-quic-congestion-control",
609 "delay",
610 "--server-quic-congestion-control",
611 "loss",
612 ]);
613 assert_eq!(both.client.congestion_control, Some(CongestionControl::Delay));
614 assert_eq!(both.server.congestion_control, Some(CongestionControl::Loss));
615
616 assert_eq!(Client::default().resolve().congestion_control, None);
618 }
619}