1use std::net;
13use std::time::Duration;
14
15#[serde_with::serde_as]
20#[derive(Clone, serde::Serialize, serde::Deserialize)]
21pub struct ServerId(#[serde_as(as = "serde_with::hex::Hex")] pub(crate) Vec<u8>);
22
23impl ServerId {
24 #[allow(dead_code)]
25 pub(crate) fn len(&self) -> usize {
26 self.0.len()
27 }
28}
29
30impl std::fmt::Debug for ServerId {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.debug_tuple("ServerId").field(&hex::encode(&self.0)).finish()
33 }
34}
35
36impl std::str::FromStr for ServerId {
37 type Err = hex::FromHexError;
38
39 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
40 hex::decode(s).map(Self)
41 }
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
50#[serde(rename_all = "kebab-case")]
51#[non_exhaustive]
52pub enum CongestionControl {
53 Loss,
56 Delay,
60}
61
62pub(crate) const DEFAULT_MAX_STREAMS: u64 = 1024;
64
65pub(crate) const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
67
68pub(crate) const DEFAULT_KEEP_ALIVE: Duration = Duration::from_secs(5);
70
71#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
73#[serde(deny_unknown_fields, default)]
74#[non_exhaustive]
75pub struct Client {
76 #[serde(skip_serializing_if = "Option::is_none")]
79 #[arg(
80 id = "client-quic-max-streams",
81 long = "client-quic-max-streams",
82 alias = "client-max-streams",
83 env = "MOQ_CLIENT_QUIC_MAX_STREAMS"
84 )]
85 pub max_streams: Option<u64>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
93 #[arg(
94 id = "client-quic-gso",
95 long = "client-quic-gso",
96 env = "MOQ_CLIENT_QUIC_GSO",
97 default_missing_value = "true",
98 num_args = 0..=1,
99 require_equals = true,
100 value_parser = clap::value_parser!(bool),
101 )]
102 pub gso: Option<bool>,
103
104 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
106 #[arg(
107 id = "client-quic-idle-timeout",
108 long = "client-quic-idle-timeout",
109 env = "MOQ_CLIENT_QUIC_IDLE_TIMEOUT",
110 value_parser = humantime::parse_duration,
111 )]
112 pub idle_timeout: Option<Duration>,
113
114 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
117 #[arg(
118 id = "client-quic-keep-alive",
119 long = "client-quic-keep-alive",
120 env = "MOQ_CLIENT_QUIC_KEEP_ALIVE",
121 value_parser = humantime::parse_duration,
122 )]
123 pub keep_alive: Option<Duration>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 #[arg(
128 id = "client-quic-mtu-discovery",
129 long = "client-quic-mtu-discovery",
130 env = "MOQ_CLIENT_QUIC_MTU_DISCOVERY",
131 default_missing_value = "true",
132 num_args = 0..=1,
133 require_equals = true,
134 value_parser = clap::value_parser!(bool),
135 )]
136 pub mtu_discovery: Option<bool>,
137
138 #[serde(skip_serializing_if = "Option::is_none")]
141 #[arg(
142 id = "client-quic-congestion-control",
143 long = "client-quic-congestion-control",
144 env = "MOQ_CLIENT_QUIC_CONGESTION_CONTROL",
145 value_enum
146 )]
147 pub congestion_control: Option<CongestionControl>,
148}
149
150impl Client {
151 pub(crate) fn resolve(&self) -> Resolved {
153 Resolved::new(
154 self.max_streams,
155 self.gso,
156 self.idle_timeout,
157 self.keep_alive,
158 self.mtu_discovery,
159 self.congestion_control,
160 )
161 }
162}
163
164#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
169#[serde(deny_unknown_fields, default)]
170#[non_exhaustive]
171pub struct Server {
172 #[serde(skip_serializing_if = "Option::is_none")]
175 #[arg(
176 id = "server-quic-max-streams",
177 long = "server-quic-max-streams",
178 alias = "server-max-streams",
179 env = "MOQ_SERVER_QUIC_MAX_STREAMS"
180 )]
181 pub max_streams: Option<u64>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 #[arg(
186 id = "server-quic-gso",
187 long = "server-quic-gso",
188 env = "MOQ_SERVER_QUIC_GSO",
189 default_missing_value = "true",
190 num_args = 0..=1,
191 require_equals = true,
192 value_parser = clap::value_parser!(bool),
193 )]
194 pub gso: Option<bool>,
195
196 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
198 #[arg(
199 id = "server-quic-idle-timeout",
200 long = "server-quic-idle-timeout",
201 env = "MOQ_SERVER_QUIC_IDLE_TIMEOUT",
202 value_parser = humantime::parse_duration,
203 )]
204 pub idle_timeout: Option<Duration>,
205
206 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
209 #[arg(
210 id = "server-quic-keep-alive",
211 long = "server-quic-keep-alive",
212 env = "MOQ_SERVER_QUIC_KEEP_ALIVE",
213 value_parser = humantime::parse_duration,
214 )]
215 pub keep_alive: Option<Duration>,
216
217 #[serde(skip_serializing_if = "Option::is_none")]
219 #[arg(
220 id = "server-quic-mtu-discovery",
221 long = "server-quic-mtu-discovery",
222 env = "MOQ_SERVER_QUIC_MTU_DISCOVERY",
223 default_missing_value = "true",
224 num_args = 0..=1,
225 require_equals = true,
226 value_parser = clap::value_parser!(bool),
227 )]
228 pub mtu_discovery: Option<bool>,
229
230 #[serde(skip_serializing_if = "Option::is_none")]
233 #[arg(
234 id = "server-quic-congestion-control",
235 long = "server-quic-congestion-control",
236 env = "MOQ_SERVER_QUIC_CONGESTION_CONTROL",
237 value_enum
238 )]
239 pub congestion_control: Option<CongestionControl>,
240
241 #[arg(
249 id = "server-preferred-v4",
250 long = "server-preferred-v4",
251 env = "MOQ_SERVER_PREFERRED_V4"
252 )]
253 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub preferred_v4: Option<net::SocketAddrV4>,
255
256 #[arg(
258 id = "server-preferred-v6",
259 long = "server-preferred-v6",
260 env = "MOQ_SERVER_PREFERRED_V6"
261 )]
262 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub preferred_v6: Option<net::SocketAddrV6>,
264
265 #[arg(id = "server-quic-lb-id", long = "server-quic-lb-id", env = "MOQ_SERVER_QUIC_LB_ID")]
268 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub quic_lb_id: Option<ServerId>,
270
271 #[arg(
274 id = "server-quic-lb-nonce",
275 long = "server-quic-lb-nonce",
276 requires = "server-quic-lb-id",
277 env = "MOQ_SERVER_QUIC_LB_NONCE"
278 )]
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub quic_lb_nonce: Option<usize>,
281}
282
283impl Server {
284 pub(crate) fn resolve(&self) -> Resolved {
286 Resolved::new(
287 self.max_streams,
288 self.gso,
289 self.idle_timeout,
290 self.keep_alive,
291 self.mtu_discovery,
292 self.congestion_control,
293 )
294 }
295}
296
297#[derive(Clone, Copy, Debug)]
303pub(crate) struct Resolved {
304 pub max_streams: u64,
306 pub gso: Option<bool>,
308 pub idle_timeout: Duration,
310 pub keep_alive: Option<Duration>,
312 pub mtu_discovery: bool,
314 pub congestion_control: Option<CongestionControl>,
316}
317
318impl Resolved {
319 fn new(
320 max_streams: Option<u64>,
321 gso: Option<bool>,
322 idle_timeout: Option<Duration>,
323 keep_alive: Option<Duration>,
324 mtu_discovery: Option<bool>,
325 congestion_control: Option<CongestionControl>,
326 ) -> Self {
327 let keep_alive = match keep_alive {
330 Some(d) if d.is_zero() => None,
331 Some(d) => Some(d),
332 None => Some(DEFAULT_KEEP_ALIVE),
333 };
334
335 Self {
336 max_streams: max_streams.unwrap_or(DEFAULT_MAX_STREAMS),
337 gso,
338 idle_timeout: idle_timeout.unwrap_or(DEFAULT_IDLE_TIMEOUT),
339 keep_alive,
340 mtu_discovery: mtu_discovery.unwrap_or(false),
341 congestion_control,
342 }
343 }
344
345 #[cfg_attr(not(any(feature = "quiche", feature = "iroh")), allow(dead_code))]
351 pub(crate) fn gso_disabled(&self) -> bool {
352 self.gso == Some(false)
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use clap::Parser;
360
361 #[derive(Parser)]
364 struct Both {
365 #[command(flatten)]
366 client: Client,
367 #[command(flatten)]
368 server: Server,
369 }
370
371 fn parse(args: &[&str]) -> Both {
372 let mut full = vec!["test"];
373 full.extend_from_slice(args);
374 Both::parse_from(full)
375 }
376
377 #[test]
378 fn defaults_apply_when_unset() {
379 let quic = Client::default().resolve();
380 assert_eq!(quic.max_streams, DEFAULT_MAX_STREAMS);
381 assert_eq!(quic.idle_timeout, DEFAULT_IDLE_TIMEOUT);
382 assert_eq!(quic.keep_alive, Some(DEFAULT_KEEP_ALIVE));
383 assert!(!quic.mtu_discovery);
384 assert_eq!(quic.gso, None);
385 assert!(!quic.gso_disabled());
386 }
387
388 #[test]
389 fn zero_keep_alive_disables_it() {
390 let disabled = Server {
391 keep_alive: Some(Duration::ZERO),
392 ..Default::default()
393 };
394 assert_eq!(disabled.resolve().keep_alive, None);
395
396 let explicit = Client {
397 keep_alive: Some(Duration::from_secs(2)),
398 ..Default::default()
399 };
400 assert_eq!(explicit.resolve().keep_alive, Some(Duration::from_secs(2)));
401 }
402
403 #[test]
404 fn gso_disabled_only_on_explicit_false() {
405 let off = Client {
406 gso: Some(false),
407 ..Default::default()
408 };
409 assert!(off.resolve().gso_disabled());
410 let on = Client {
411 gso: Some(true),
412 ..Default::default()
413 };
414 assert!(!on.resolve().gso_disabled());
415 }
416
417 #[test]
418 fn client_and_server_flags_are_distinct() {
419 let both = parse(&["--client-quic-max-streams", "5000", "--server-quic-max-streams", "9000"]);
420 assert_eq!(both.client.max_streams, Some(5000));
421 assert_eq!(both.server.max_streams, Some(9000));
422 }
423
424 #[test]
425 fn server_only_knobs_parse() {
426 let both = parse(&["--server-preferred-v4", "192.0.2.1:443", "--server-quic-lb-id", "ab"]);
427 assert_eq!(both.server.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
428 assert!(both.server.quic_lb_id.is_some());
429 assert_eq!(both.client.max_streams, None);
431 }
432
433 #[test]
434 fn deprecated_max_streams_aliases() {
435 let both = parse(&["--client-max-streams", "2048", "--server-max-streams", "4096"]);
436 assert_eq!(both.client.max_streams, Some(2048));
437 assert_eq!(both.server.max_streams, Some(4096));
438 }
439
440 #[test]
441 fn toml_round_trips() {
442 let toml = r#"
443 max_streams = 7000
444 gso = false
445 preferred_v4 = "192.0.2.1:443"
446 congestion_control = "delay"
447 "#;
448 let quic: Server = toml::from_str(toml).unwrap();
449 assert_eq!(quic.max_streams, Some(7000));
450 assert_eq!(quic.gso, Some(false));
451 assert_eq!(quic.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
452 assert_eq!(quic.congestion_control, Some(CongestionControl::Delay));
453 }
454
455 #[test]
456 fn congestion_control_flags_parse() {
457 let both = parse(&[
458 "--client-quic-congestion-control",
459 "delay",
460 "--server-quic-congestion-control",
461 "loss",
462 ]);
463 assert_eq!(both.client.congestion_control, Some(CongestionControl::Delay));
464 assert_eq!(both.server.congestion_control, Some(CongestionControl::Loss));
465
466 assert_eq!(Client::default().resolve().congestion_control, None);
468 }
469}