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
63pub(crate) const DEFAULT_MAX_STREAMS: u64 = 1024;
65
66pub(crate) const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
68
69pub(crate) const DEFAULT_KEEP_ALIVE: Duration = Duration::from_secs(5);
71
72#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
74#[serde(deny_unknown_fields, default)]
75#[non_exhaustive]
76pub struct Client {
77 #[serde(skip_serializing_if = "Option::is_none")]
80 #[arg(
81 id = "client-quic-max-streams",
82 long = "client-quic-max-streams",
83 alias = "client-max-streams",
84 env = "MOQ_CLIENT_QUIC_MAX_STREAMS"
85 )]
86 pub max_streams: Option<u64>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
94 #[arg(
95 id = "client-quic-gso",
96 long = "client-quic-gso",
97 env = "MOQ_CLIENT_QUIC_GSO",
98 default_missing_value = "true",
99 num_args = 0..=1,
100 require_equals = true,
101 value_parser = clap::value_parser!(bool),
102 )]
103 pub gso: Option<bool>,
104
105 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
107 #[arg(
108 id = "client-quic-idle-timeout",
109 long = "client-quic-idle-timeout",
110 env = "MOQ_CLIENT_QUIC_IDLE_TIMEOUT",
111 value_parser = humantime::parse_duration,
112 )]
113 pub idle_timeout: Option<Duration>,
114
115 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
118 #[arg(
119 id = "client-quic-keep-alive",
120 long = "client-quic-keep-alive",
121 env = "MOQ_CLIENT_QUIC_KEEP_ALIVE",
122 value_parser = humantime::parse_duration,
123 )]
124 pub keep_alive: Option<Duration>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
128 #[arg(
129 id = "client-quic-mtu-discovery",
130 long = "client-quic-mtu-discovery",
131 env = "MOQ_CLIENT_QUIC_MTU_DISCOVERY",
132 default_missing_value = "true",
133 num_args = 0..=1,
134 require_equals = true,
135 value_parser = clap::value_parser!(bool),
136 )]
137 pub mtu_discovery: Option<bool>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
142 #[arg(
143 id = "client-quic-congestion-control",
144 long = "client-quic-congestion-control",
145 env = "MOQ_CLIENT_QUIC_CONGESTION_CONTROL",
146 value_enum
147 )]
148 pub congestion_control: Option<CongestionControl>,
149
150 #[serde(default, skip_serializing_if = "Option::is_none")]
152 #[arg(id = "client-quic-qlog", long = "client-quic-qlog", env = "MOQ_CLIENT_QUIC_QLOG")]
153 pub qlog: Option<PathBuf>,
154}
155
156fn validate_qlog(qlog: Option<&PathBuf>) -> crate::Result<()> {
162 match qlog {
163 Some(_) if cfg!(not(feature = "qlog")) => Err(crate::Error::QlogUnsupported),
164 _ => Ok(()),
165 }
166}
167
168impl Client {
169 pub(crate) fn validate(&self) -> crate::Result<()> {
171 validate_qlog(self.qlog.as_ref())
172 }
173
174 pub(crate) fn resolve(&self) -> Resolved {
176 Resolved::new(
177 self.max_streams,
178 self.gso,
179 self.idle_timeout,
180 self.keep_alive,
181 self.mtu_discovery,
182 self.congestion_control,
183 self.qlog.clone(),
184 )
185 }
186}
187
188#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
193#[serde(deny_unknown_fields, default)]
194#[non_exhaustive]
195pub struct Server {
196 #[serde(skip_serializing_if = "Option::is_none")]
199 #[arg(
200 id = "server-quic-max-streams",
201 long = "server-quic-max-streams",
202 alias = "server-max-streams",
203 env = "MOQ_SERVER_QUIC_MAX_STREAMS"
204 )]
205 pub max_streams: Option<u64>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 #[arg(
210 id = "server-quic-gso",
211 long = "server-quic-gso",
212 env = "MOQ_SERVER_QUIC_GSO",
213 default_missing_value = "true",
214 num_args = 0..=1,
215 require_equals = true,
216 value_parser = clap::value_parser!(bool),
217 )]
218 pub gso: Option<bool>,
219
220 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
222 #[arg(
223 id = "server-quic-idle-timeout",
224 long = "server-quic-idle-timeout",
225 env = "MOQ_SERVER_QUIC_IDLE_TIMEOUT",
226 value_parser = humantime::parse_duration,
227 )]
228 pub idle_timeout: Option<Duration>,
229
230 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
233 #[arg(
234 id = "server-quic-keep-alive",
235 long = "server-quic-keep-alive",
236 env = "MOQ_SERVER_QUIC_KEEP_ALIVE",
237 value_parser = humantime::parse_duration,
238 )]
239 pub keep_alive: Option<Duration>,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
243 #[arg(
244 id = "server-quic-mtu-discovery",
245 long = "server-quic-mtu-discovery",
246 env = "MOQ_SERVER_QUIC_MTU_DISCOVERY",
247 default_missing_value = "true",
248 num_args = 0..=1,
249 require_equals = true,
250 value_parser = clap::value_parser!(bool),
251 )]
252 pub mtu_discovery: Option<bool>,
253
254 #[serde(skip_serializing_if = "Option::is_none")]
257 #[arg(
258 id = "server-quic-congestion-control",
259 long = "server-quic-congestion-control",
260 env = "MOQ_SERVER_QUIC_CONGESTION_CONTROL",
261 value_enum
262 )]
263 pub congestion_control: Option<CongestionControl>,
264
265 #[arg(
273 id = "server-preferred-v4",
274 long = "server-preferred-v4",
275 env = "MOQ_SERVER_PREFERRED_V4"
276 )]
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub preferred_v4: Option<net::SocketAddrV4>,
279
280 #[arg(
282 id = "server-preferred-v6",
283 long = "server-preferred-v6",
284 env = "MOQ_SERVER_PREFERRED_V6"
285 )]
286 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub preferred_v6: Option<net::SocketAddrV6>,
288
289 #[arg(id = "server-quic-lb-id", long = "server-quic-lb-id", env = "MOQ_SERVER_QUIC_LB_ID")]
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub quic_lb_id: Option<ServerId>,
294
295 #[arg(
298 id = "server-quic-lb-nonce",
299 long = "server-quic-lb-nonce",
300 requires = "server-quic-lb-id",
301 env = "MOQ_SERVER_QUIC_LB_NONCE"
302 )]
303 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub quic_lb_nonce: Option<usize>,
305
306 #[serde(default, skip_serializing_if = "Option::is_none")]
314 #[arg(id = "server-quic-qlog", long = "server-quic-qlog", env = "MOQ_SERVER_QUIC_QLOG")]
315 pub qlog: Option<PathBuf>,
316}
317
318impl Server {
319 pub(crate) fn validate(&self) -> crate::Result<()> {
321 validate_qlog(self.qlog.as_ref())
322 }
323
324 pub(crate) fn resolve(&self) -> Resolved {
326 Resolved::new(
327 self.max_streams,
328 self.gso,
329 self.idle_timeout,
330 self.keep_alive,
331 self.mtu_discovery,
332 self.congestion_control,
333 self.qlog.clone(),
334 )
335 }
336}
337
338#[derive(Clone, Debug)]
344pub(crate) struct Resolved {
345 pub max_streams: u64,
347 pub gso: Option<bool>,
349 pub idle_timeout: Duration,
351 pub keep_alive: Option<Duration>,
353 pub mtu_discovery: bool,
355 pub congestion_control: Option<CongestionControl>,
357 pub qlog: Option<PathBuf>,
359}
360
361impl Resolved {
362 fn new(
363 max_streams: Option<u64>,
364 gso: Option<bool>,
365 idle_timeout: Option<Duration>,
366 keep_alive: Option<Duration>,
367 mtu_discovery: Option<bool>,
368 congestion_control: Option<CongestionControl>,
369 qlog: Option<PathBuf>,
370 ) -> Self {
371 let keep_alive = match keep_alive {
374 Some(d) if d.is_zero() => None,
375 Some(d) => Some(d),
376 None => Some(DEFAULT_KEEP_ALIVE),
377 };
378
379 Self {
380 max_streams: max_streams.unwrap_or(DEFAULT_MAX_STREAMS),
381 gso,
382 idle_timeout: idle_timeout.unwrap_or(DEFAULT_IDLE_TIMEOUT),
383 keep_alive,
384 mtu_discovery: mtu_discovery.unwrap_or(false),
385 congestion_control,
386 qlog,
387 }
388 }
389
390 #[cfg_attr(not(any(feature = "quinn", feature = "noq", feature = "quiche")), allow(dead_code))]
395 pub(crate) fn qlog_dir(&self) -> Option<&std::path::Path> {
396 self.qlog.as_deref()
397 }
398
399 #[cfg_attr(not(any(feature = "quiche", feature = "iroh")), allow(dead_code))]
405 pub(crate) fn gso_disabled(&self) -> bool {
406 self.gso == Some(false)
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use clap::Parser;
414
415 #[derive(Parser)]
418 struct Both {
419 #[command(flatten)]
420 client: Client,
421 #[command(flatten)]
422 server: Server,
423 }
424
425 fn parse(args: &[&str]) -> Both {
426 let mut full = vec!["test"];
427 full.extend_from_slice(args);
428 Both::parse_from(full)
429 }
430
431 #[test]
432 fn defaults_apply_when_unset() {
433 let quic = Client::default().resolve();
434 assert_eq!(quic.max_streams, DEFAULT_MAX_STREAMS);
435 assert_eq!(quic.idle_timeout, DEFAULT_IDLE_TIMEOUT);
436 assert_eq!(quic.keep_alive, Some(DEFAULT_KEEP_ALIVE));
437 assert!(!quic.mtu_discovery);
438 assert_eq!(quic.gso, None);
439 assert!(!quic.gso_disabled());
440 }
441
442 #[test]
443 fn zero_keep_alive_disables_it() {
444 let disabled = Server {
445 keep_alive: Some(Duration::ZERO),
446 ..Default::default()
447 };
448 assert_eq!(disabled.resolve().keep_alive, None);
449
450 let explicit = Client {
451 keep_alive: Some(Duration::from_secs(2)),
452 ..Default::default()
453 };
454 assert_eq!(explicit.resolve().keep_alive, Some(Duration::from_secs(2)));
455 }
456
457 #[test]
458 fn gso_disabled_only_on_explicit_false() {
459 let off = Client {
460 gso: Some(false),
461 ..Default::default()
462 };
463 assert!(off.resolve().gso_disabled());
464 let on = Client {
465 gso: Some(true),
466 ..Default::default()
467 };
468 assert!(!on.resolve().gso_disabled());
469 }
470
471 #[test]
472 fn client_and_server_flags_are_distinct() {
473 let both = parse(&["--client-quic-max-streams", "5000", "--server-quic-max-streams", "9000"]);
474 assert_eq!(both.client.max_streams, Some(5000));
475 assert_eq!(both.server.max_streams, Some(9000));
476 }
477
478 #[test]
479 fn server_only_knobs_parse() {
480 let both = parse(&["--server-preferred-v4", "192.0.2.1:443", "--server-quic-lb-id", "ab"]);
481 assert_eq!(both.server.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
482 assert!(both.server.quic_lb_id.is_some());
483 assert_eq!(both.client.max_streams, None);
485 }
486
487 #[test]
488 fn deprecated_max_streams_aliases() {
489 let both = parse(&["--client-max-streams", "2048", "--server-max-streams", "4096"]);
490 assert_eq!(both.client.max_streams, Some(2048));
491 assert_eq!(both.server.max_streams, Some(4096));
492 }
493
494 #[test]
495 fn qlog_flags_are_distinct_per_role() {
496 let both = parse(&["--client-quic-qlog", "/tmp/client", "--server-quic-qlog", "/tmp/server"]);
497 assert_eq!(both.client.qlog.as_deref(), Some(std::path::Path::new("/tmp/client")));
498 assert_eq!(both.server.qlog.as_deref(), Some(std::path::Path::new("/tmp/server")));
499
500 assert_eq!(
501 both.client.resolve().qlog_dir(),
502 Some(std::path::Path::new("/tmp/client"))
503 );
504 assert_eq!(Client::default().resolve().qlog_dir(), None);
505 }
506
507 #[test]
510 fn qlog_requires_the_feature() {
511 let unset = Client::default().validate();
512 assert!(unset.is_ok(), "no directory configured is always fine");
513
514 let set = Client {
515 qlog: Some("/tmp/qlog".into()),
516 ..Default::default()
517 };
518
519 if cfg!(feature = "qlog") {
520 assert!(set.validate().is_ok());
521 } else {
522 assert!(matches!(set.validate(), Err(crate::Error::QlogUnsupported)));
523 }
524 }
525
526 #[test]
527 fn toml_round_trips() {
528 let toml = r#"
529 max_streams = 7000
530 gso = false
531 preferred_v4 = "192.0.2.1:443"
532 congestion_control = "delay"
533 qlog = "/tmp/qlog"
534 "#;
535 let quic: Server = toml::from_str(toml).unwrap();
536 assert_eq!(quic.max_streams, Some(7000));
537 assert_eq!(quic.gso, Some(false));
538 assert_eq!(quic.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
539 assert_eq!(quic.congestion_control, Some(CongestionControl::Delay));
540 assert_eq!(quic.qlog.as_deref(), Some(std::path::Path::new("/tmp/qlog")));
541 }
542
543 #[test]
544 fn congestion_control_flags_parse() {
545 let both = parse(&[
546 "--client-quic-congestion-control",
547 "delay",
548 "--server-quic-congestion-control",
549 "loss",
550 ]);
551 assert_eq!(both.client.congestion_control, Some(CongestionControl::Delay));
552 assert_eq!(both.server.congestion_control, Some(CongestionControl::Loss));
553
554 assert_eq!(Client::default().resolve().congestion_control, None);
556 }
557}