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")]
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")]
258 #[arg(
259 id = "server-quic-congestion-control",
260 long = "server-quic-congestion-control",
261 env = "MOQ_SERVER_QUIC_CONGESTION_CONTROL",
262 value_enum
263 )]
264 pub congestion_control: Option<CongestionControl>,
265
266 #[arg(
274 id = "server-preferred-v4",
275 long = "server-preferred-v4",
276 env = "MOQ_SERVER_PREFERRED_V4"
277 )]
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub preferred_v4: Option<net::SocketAddrV4>,
280
281 #[arg(
283 id = "server-preferred-v6",
284 long = "server-preferred-v6",
285 env = "MOQ_SERVER_PREFERRED_V6"
286 )]
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub preferred_v6: Option<net::SocketAddrV6>,
289
290 #[arg(id = "server-quic-lb-id", long = "server-quic-lb-id", env = "MOQ_SERVER_QUIC_LB_ID")]
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub quic_lb_id: Option<ServerId>,
295
296 #[arg(
299 id = "server-quic-lb-nonce",
300 long = "server-quic-lb-nonce",
301 requires = "server-quic-lb-id",
302 env = "MOQ_SERVER_QUIC_LB_NONCE"
303 )]
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub quic_lb_nonce: Option<usize>,
306
307 #[serde(default, skip_serializing_if = "Option::is_none")]
315 #[arg(id = "server-quic-qlog", long = "server-quic-qlog", env = "MOQ_SERVER_QUIC_QLOG")]
316 pub qlog: Option<PathBuf>,
317}
318
319impl Server {
320 pub(crate) fn validate(&self) -> crate::Result<()> {
322 validate_qlog(self.qlog.as_ref())
323 }
324
325 pub(crate) fn resolve(&self) -> Resolved {
327 Resolved::new(
328 self.max_streams,
329 self.gso,
330 self.idle_timeout,
331 self.keep_alive,
332 self.mtu_discovery,
333 self.congestion_control,
334 self.qlog.clone(),
335 )
336 }
337}
338
339#[derive(Clone, Debug)]
345pub(crate) struct Resolved {
346 pub max_streams: u64,
348 pub gso: Option<bool>,
350 pub idle_timeout: Duration,
352 pub keep_alive: Option<Duration>,
354 pub mtu_discovery: bool,
356 pub congestion_control: Option<CongestionControl>,
359 pub qlog: Option<PathBuf>,
361}
362
363impl Resolved {
364 fn new(
365 max_streams: Option<u64>,
366 gso: Option<bool>,
367 idle_timeout: Option<Duration>,
368 keep_alive: Option<Duration>,
369 mtu_discovery: Option<bool>,
370 congestion_control: Option<CongestionControl>,
371 qlog: Option<PathBuf>,
372 ) -> Self {
373 let keep_alive = match keep_alive {
376 Some(d) if d.is_zero() => None,
377 Some(d) => Some(d),
378 None => Some(DEFAULT_KEEP_ALIVE),
379 };
380
381 Self {
382 max_streams: max_streams.unwrap_or(DEFAULT_MAX_STREAMS),
383 gso,
384 idle_timeout: idle_timeout.unwrap_or(DEFAULT_IDLE_TIMEOUT),
385 keep_alive,
386 mtu_discovery: mtu_discovery.unwrap_or(false),
387 congestion_control,
388 qlog,
389 }
390 }
391
392 #[cfg_attr(not(any(feature = "quinn", feature = "noq", feature = "quiche")), allow(dead_code))]
397 pub(crate) fn qlog_dir(&self) -> Option<&std::path::Path> {
398 self.qlog.as_deref()
399 }
400
401 #[cfg_attr(not(feature = "iroh"), allow(dead_code))]
406 pub(crate) fn gso_disabled(&self) -> bool {
407 self.gso == Some(false)
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use clap::Parser;
415
416 #[derive(Parser)]
419 struct Both {
420 #[command(flatten)]
421 client: Client,
422 #[command(flatten)]
423 server: Server,
424 }
425
426 fn parse(args: &[&str]) -> Both {
427 let mut full = vec!["test"];
428 full.extend_from_slice(args);
429 Both::parse_from(full)
430 }
431
432 #[test]
433 fn defaults_apply_when_unset() {
434 let quic = Client::default().resolve();
435 assert_eq!(quic.max_streams, DEFAULT_MAX_STREAMS);
436 assert_eq!(quic.idle_timeout, DEFAULT_IDLE_TIMEOUT);
437 assert_eq!(quic.keep_alive, Some(DEFAULT_KEEP_ALIVE));
438 assert!(!quic.mtu_discovery);
439 assert_eq!(quic.gso, None);
440 assert!(!quic.gso_disabled());
441 }
442
443 #[test]
444 fn zero_keep_alive_disables_it() {
445 let disabled = Server {
446 keep_alive: Some(Duration::ZERO),
447 ..Default::default()
448 };
449 assert_eq!(disabled.resolve().keep_alive, None);
450
451 let explicit = Client {
452 keep_alive: Some(Duration::from_secs(2)),
453 ..Default::default()
454 };
455 assert_eq!(explicit.resolve().keep_alive, Some(Duration::from_secs(2)));
456 }
457
458 #[test]
459 fn gso_disabled_only_on_explicit_false() {
460 let off = Client {
461 gso: Some(false),
462 ..Default::default()
463 };
464 assert!(off.resolve().gso_disabled());
465 let on = Client {
466 gso: Some(true),
467 ..Default::default()
468 };
469 assert!(!on.resolve().gso_disabled());
470 }
471
472 #[test]
473 fn client_and_server_flags_are_distinct() {
474 let both = parse(&["--client-quic-max-streams", "5000", "--server-quic-max-streams", "9000"]);
475 assert_eq!(both.client.max_streams, Some(5000));
476 assert_eq!(both.server.max_streams, Some(9000));
477 }
478
479 #[test]
480 fn server_only_knobs_parse() {
481 let both = parse(&["--server-preferred-v4", "192.0.2.1:443", "--server-quic-lb-id", "ab"]);
482 assert_eq!(both.server.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
483 assert!(both.server.quic_lb_id.is_some());
484 assert_eq!(both.client.max_streams, None);
486 }
487
488 #[test]
489 fn deprecated_max_streams_aliases() {
490 let both = parse(&["--client-max-streams", "2048", "--server-max-streams", "4096"]);
491 assert_eq!(both.client.max_streams, Some(2048));
492 assert_eq!(both.server.max_streams, Some(4096));
493 }
494
495 #[test]
496 fn qlog_flags_are_distinct_per_role() {
497 let both = parse(&["--client-quic-qlog", "/tmp/client", "--server-quic-qlog", "/tmp/server"]);
498 assert_eq!(both.client.qlog.as_deref(), Some(std::path::Path::new("/tmp/client")));
499 assert_eq!(both.server.qlog.as_deref(), Some(std::path::Path::new("/tmp/server")));
500
501 assert_eq!(
502 both.client.resolve().qlog_dir(),
503 Some(std::path::Path::new("/tmp/client"))
504 );
505 assert_eq!(Client::default().resolve().qlog_dir(), None);
506 }
507
508 #[test]
511 fn qlog_requires_the_feature() {
512 let unset = Client::default().validate();
513 assert!(unset.is_ok(), "no directory configured is always fine");
514
515 let set = Client {
516 qlog: Some("/tmp/qlog".into()),
517 ..Default::default()
518 };
519
520 if cfg!(feature = "qlog") {
521 assert!(set.validate().is_ok());
522 } else {
523 assert!(matches!(set.validate(), Err(crate::Error::QlogUnsupported)));
524 }
525 }
526
527 #[test]
528 fn toml_round_trips() {
529 let toml = r#"
530 max_streams = 7000
531 gso = false
532 preferred_v4 = "192.0.2.1:443"
533 congestion_control = "delay"
534 qlog = "/tmp/qlog"
535 "#;
536 let quic: Server = toml::from_str(toml).unwrap();
537 assert_eq!(quic.max_streams, Some(7000));
538 assert_eq!(quic.gso, Some(false));
539 assert_eq!(quic.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
540 assert_eq!(quic.congestion_control, Some(CongestionControl::Delay));
541 assert_eq!(quic.qlog.as_deref(), Some(std::path::Path::new("/tmp/qlog")));
542 }
543
544 #[test]
545 fn congestion_control_flags_parse() {
546 let both = parse(&[
547 "--client-quic-congestion-control",
548 "delay",
549 "--server-quic-congestion-control",
550 "loss",
551 ]);
552 assert_eq!(both.client.congestion_control, Some(CongestionControl::Delay));
553 assert_eq!(both.server.congestion_control, Some(CongestionControl::Loss));
554
555 assert_eq!(Client::default().resolve().congestion_control, None);
557 }
558}