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")]
143 #[arg(
144 id = "client-quic-congestion-control",
145 long = "client-quic-congestion-control",
146 env = "MOQ_CLIENT_QUIC_CONGESTION_CONTROL",
147 value_enum
148 )]
149 pub congestion_control: Option<CongestionControl>,
150
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 #[arg(id = "client-quic-qlog", long = "client-quic-qlog", env = "MOQ_CLIENT_QUIC_QLOG")]
154 pub qlog: Option<PathBuf>,
155}
156
157fn validate_qlog(qlog: Option<&PathBuf>) -> crate::Result<()> {
163 match qlog {
164 Some(_) if cfg!(not(feature = "qlog")) => Err(crate::Error::QlogUnsupported),
165 _ => Ok(()),
166 }
167}
168
169impl Client {
170 pub(crate) fn validate(&self) -> crate::Result<()> {
172 validate_qlog(self.qlog.as_ref())
173 }
174
175 pub(crate) fn resolve(&self) -> Resolved {
177 Resolved::new(
178 self.max_streams,
179 self.gso,
180 self.idle_timeout,
181 self.keep_alive,
182 self.mtu_discovery,
183 self.congestion_control,
184 self.qlog.clone(),
185 )
186 }
187}
188
189#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
194#[serde(deny_unknown_fields, default)]
195#[non_exhaustive]
196pub struct Server {
197 #[serde(skip_serializing_if = "Option::is_none")]
200 #[arg(
201 id = "server-quic-max-streams",
202 long = "server-quic-max-streams",
203 alias = "server-max-streams",
204 env = "MOQ_SERVER_QUIC_MAX_STREAMS"
205 )]
206 pub max_streams: Option<u64>,
207
208 #[serde(skip_serializing_if = "Option::is_none")]
210 #[arg(
211 id = "server-quic-gso",
212 long = "server-quic-gso",
213 env = "MOQ_SERVER_QUIC_GSO",
214 default_missing_value = "true",
215 num_args = 0..=1,
216 require_equals = true,
217 value_parser = clap::value_parser!(bool),
218 )]
219 pub gso: Option<bool>,
220
221 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
223 #[arg(
224 id = "server-quic-idle-timeout",
225 long = "server-quic-idle-timeout",
226 env = "MOQ_SERVER_QUIC_IDLE_TIMEOUT",
227 value_parser = humantime::parse_duration,
228 )]
229 pub idle_timeout: Option<Duration>,
230
231 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
234 #[arg(
235 id = "server-quic-keep-alive",
236 long = "server-quic-keep-alive",
237 env = "MOQ_SERVER_QUIC_KEEP_ALIVE",
238 value_parser = humantime::parse_duration,
239 )]
240 pub keep_alive: Option<Duration>,
241
242 #[serde(skip_serializing_if = "Option::is_none")]
244 #[arg(
245 id = "server-quic-mtu-discovery",
246 long = "server-quic-mtu-discovery",
247 env = "MOQ_SERVER_QUIC_MTU_DISCOVERY",
248 default_missing_value = "true",
249 num_args = 0..=1,
250 require_equals = true,
251 value_parser = clap::value_parser!(bool),
252 )]
253 pub mtu_discovery: Option<bool>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
259 #[arg(
260 id = "server-quic-congestion-control",
261 long = "server-quic-congestion-control",
262 env = "MOQ_SERVER_QUIC_CONGESTION_CONTROL",
263 value_enum
264 )]
265 pub congestion_control: Option<CongestionControl>,
266
267 #[arg(
275 id = "server-preferred-v4",
276 long = "server-preferred-v4",
277 env = "MOQ_SERVER_PREFERRED_V4"
278 )]
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub preferred_v4: Option<net::SocketAddrV4>,
281
282 #[arg(
284 id = "server-preferred-v6",
285 long = "server-preferred-v6",
286 env = "MOQ_SERVER_PREFERRED_V6"
287 )]
288 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub preferred_v6: Option<net::SocketAddrV6>,
290
291 #[arg(id = "server-quic-lb-id", long = "server-quic-lb-id", env = "MOQ_SERVER_QUIC_LB_ID")]
294 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub quic_lb_id: Option<ServerId>,
296
297 #[arg(
300 id = "server-quic-lb-nonce",
301 long = "server-quic-lb-nonce",
302 requires = "server-quic-lb-id",
303 env = "MOQ_SERVER_QUIC_LB_NONCE"
304 )]
305 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub quic_lb_nonce: Option<usize>,
307
308 #[serde(default, skip_serializing_if = "Option::is_none")]
316 #[arg(id = "server-quic-qlog", long = "server-quic-qlog", env = "MOQ_SERVER_QUIC_QLOG")]
317 pub qlog: Option<PathBuf>,
318}
319
320impl Server {
321 pub(crate) fn validate(&self) -> crate::Result<()> {
323 validate_qlog(self.qlog.as_ref())
324 }
325
326 pub(crate) fn resolve(&self) -> Resolved {
328 Resolved::new(
329 self.max_streams,
330 self.gso,
331 self.idle_timeout,
332 self.keep_alive,
333 self.mtu_discovery,
334 self.congestion_control,
335 self.qlog.clone(),
336 )
337 }
338}
339
340#[derive(Clone, Debug)]
346pub(crate) struct Resolved {
347 pub max_streams: u64,
349 pub gso: Option<bool>,
351 pub idle_timeout: Duration,
353 pub keep_alive: Option<Duration>,
355 pub mtu_discovery: bool,
357 pub congestion_control: Option<CongestionControl>,
360 pub qlog: Option<PathBuf>,
362}
363
364impl Resolved {
365 fn new(
366 max_streams: Option<u64>,
367 gso: Option<bool>,
368 idle_timeout: Option<Duration>,
369 keep_alive: Option<Duration>,
370 mtu_discovery: Option<bool>,
371 congestion_control: Option<CongestionControl>,
372 qlog: Option<PathBuf>,
373 ) -> Self {
374 let keep_alive = match keep_alive {
377 Some(d) if d.is_zero() => None,
378 Some(d) => Some(d),
379 None => Some(DEFAULT_KEEP_ALIVE),
380 };
381
382 Self {
383 max_streams: max_streams.unwrap_or(DEFAULT_MAX_STREAMS),
384 gso,
385 idle_timeout: idle_timeout.unwrap_or(DEFAULT_IDLE_TIMEOUT),
386 keep_alive,
387 mtu_discovery: mtu_discovery.unwrap_or(false),
388 congestion_control,
389 qlog,
390 }
391 }
392
393 #[cfg_attr(not(any(feature = "quinn", feature = "noq", feature = "quiche")), allow(dead_code))]
398 pub(crate) fn qlog_dir(&self) -> Option<&std::path::Path> {
399 self.qlog.as_deref()
400 }
401
402 #[cfg_attr(not(any(feature = "quiche", feature = "iroh")), allow(dead_code))]
408 pub(crate) fn gso_disabled(&self) -> bool {
409 self.gso == Some(false)
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use clap::Parser;
417
418 #[derive(Parser)]
421 struct Both {
422 #[command(flatten)]
423 client: Client,
424 #[command(flatten)]
425 server: Server,
426 }
427
428 fn parse(args: &[&str]) -> Both {
429 let mut full = vec!["test"];
430 full.extend_from_slice(args);
431 Both::parse_from(full)
432 }
433
434 #[test]
435 fn defaults_apply_when_unset() {
436 let quic = Client::default().resolve();
437 assert_eq!(quic.max_streams, DEFAULT_MAX_STREAMS);
438 assert_eq!(quic.idle_timeout, DEFAULT_IDLE_TIMEOUT);
439 assert_eq!(quic.keep_alive, Some(DEFAULT_KEEP_ALIVE));
440 assert!(!quic.mtu_discovery);
441 assert_eq!(quic.gso, None);
442 assert!(!quic.gso_disabled());
443 }
444
445 #[test]
446 fn zero_keep_alive_disables_it() {
447 let disabled = Server {
448 keep_alive: Some(Duration::ZERO),
449 ..Default::default()
450 };
451 assert_eq!(disabled.resolve().keep_alive, None);
452
453 let explicit = Client {
454 keep_alive: Some(Duration::from_secs(2)),
455 ..Default::default()
456 };
457 assert_eq!(explicit.resolve().keep_alive, Some(Duration::from_secs(2)));
458 }
459
460 #[test]
461 fn gso_disabled_only_on_explicit_false() {
462 let off = Client {
463 gso: Some(false),
464 ..Default::default()
465 };
466 assert!(off.resolve().gso_disabled());
467 let on = Client {
468 gso: Some(true),
469 ..Default::default()
470 };
471 assert!(!on.resolve().gso_disabled());
472 }
473
474 #[test]
475 fn client_and_server_flags_are_distinct() {
476 let both = parse(&["--client-quic-max-streams", "5000", "--server-quic-max-streams", "9000"]);
477 assert_eq!(both.client.max_streams, Some(5000));
478 assert_eq!(both.server.max_streams, Some(9000));
479 }
480
481 #[test]
482 fn server_only_knobs_parse() {
483 let both = parse(&["--server-preferred-v4", "192.0.2.1:443", "--server-quic-lb-id", "ab"]);
484 assert_eq!(both.server.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
485 assert!(both.server.quic_lb_id.is_some());
486 assert_eq!(both.client.max_streams, None);
488 }
489
490 #[test]
491 fn deprecated_max_streams_aliases() {
492 let both = parse(&["--client-max-streams", "2048", "--server-max-streams", "4096"]);
493 assert_eq!(both.client.max_streams, Some(2048));
494 assert_eq!(both.server.max_streams, Some(4096));
495 }
496
497 #[test]
498 fn qlog_flags_are_distinct_per_role() {
499 let both = parse(&["--client-quic-qlog", "/tmp/client", "--server-quic-qlog", "/tmp/server"]);
500 assert_eq!(both.client.qlog.as_deref(), Some(std::path::Path::new("/tmp/client")));
501 assert_eq!(both.server.qlog.as_deref(), Some(std::path::Path::new("/tmp/server")));
502
503 assert_eq!(
504 both.client.resolve().qlog_dir(),
505 Some(std::path::Path::new("/tmp/client"))
506 );
507 assert_eq!(Client::default().resolve().qlog_dir(), None);
508 }
509
510 #[test]
513 fn qlog_requires_the_feature() {
514 let unset = Client::default().validate();
515 assert!(unset.is_ok(), "no directory configured is always fine");
516
517 let set = Client {
518 qlog: Some("/tmp/qlog".into()),
519 ..Default::default()
520 };
521
522 if cfg!(feature = "qlog") {
523 assert!(set.validate().is_ok());
524 } else {
525 assert!(matches!(set.validate(), Err(crate::Error::QlogUnsupported)));
526 }
527 }
528
529 #[test]
530 fn toml_round_trips() {
531 let toml = r#"
532 max_streams = 7000
533 gso = false
534 preferred_v4 = "192.0.2.1:443"
535 congestion_control = "delay"
536 qlog = "/tmp/qlog"
537 "#;
538 let quic: Server = toml::from_str(toml).unwrap();
539 assert_eq!(quic.max_streams, Some(7000));
540 assert_eq!(quic.gso, Some(false));
541 assert_eq!(quic.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
542 assert_eq!(quic.congestion_control, Some(CongestionControl::Delay));
543 assert_eq!(quic.qlog.as_deref(), Some(std::path::Path::new("/tmp/qlog")));
544 }
545
546 #[test]
547 fn congestion_control_flags_parse() {
548 let both = parse(&[
549 "--client-quic-congestion-control",
550 "delay",
551 "--server-quic-congestion-control",
552 "loss",
553 ]);
554 assert_eq!(both.client.congestion_control, Some(CongestionControl::Delay));
555 assert_eq!(both.server.congestion_control, Some(CongestionControl::Loss));
556
557 assert_eq!(Client::default().resolve().congestion_control, None);
559 }
560}