1use std::net;
13use std::time::Duration;
14
15use crate::ServerId;
16
17pub(crate) const DEFAULT_MAX_STREAMS: u64 = 1024;
19
20pub(crate) const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
22
23pub(crate) const DEFAULT_KEEP_ALIVE: Duration = Duration::from_secs(5);
25
26#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
28#[serde(deny_unknown_fields, default)]
29#[non_exhaustive]
30pub struct Client {
31 #[serde(skip_serializing_if = "Option::is_none")]
34 #[arg(
35 id = "client-quic-max-streams",
36 long = "client-quic-max-streams",
37 alias = "client-max-streams",
38 env = "MOQ_CLIENT_QUIC_MAX_STREAMS"
39 )]
40 pub max_streams: Option<u64>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
48 #[arg(
49 id = "client-quic-gso",
50 long = "client-quic-gso",
51 env = "MOQ_CLIENT_QUIC_GSO",
52 default_missing_value = "true",
53 num_args = 0..=1,
54 require_equals = true,
55 value_parser = clap::value_parser!(bool),
56 )]
57 pub gso: Option<bool>,
58
59 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
61 #[arg(
62 id = "client-quic-idle-timeout",
63 long = "client-quic-idle-timeout",
64 env = "MOQ_CLIENT_QUIC_IDLE_TIMEOUT",
65 value_parser = humantime::parse_duration,
66 )]
67 pub idle_timeout: Option<Duration>,
68
69 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
72 #[arg(
73 id = "client-quic-keep-alive",
74 long = "client-quic-keep-alive",
75 env = "MOQ_CLIENT_QUIC_KEEP_ALIVE",
76 value_parser = humantime::parse_duration,
77 )]
78 pub keep_alive: Option<Duration>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 #[arg(
83 id = "client-quic-mtu-discovery",
84 long = "client-quic-mtu-discovery",
85 env = "MOQ_CLIENT_QUIC_MTU_DISCOVERY",
86 default_missing_value = "true",
87 num_args = 0..=1,
88 require_equals = true,
89 value_parser = clap::value_parser!(bool),
90 )]
91 pub mtu_discovery: Option<bool>,
92}
93
94impl Client {
95 pub(crate) fn resolve(&self) -> Resolved {
97 Resolved::new(
98 self.max_streams,
99 self.gso,
100 self.idle_timeout,
101 self.keep_alive,
102 self.mtu_discovery,
103 )
104 }
105}
106
107#[derive(Clone, Debug, Default, clap::Args, serde::Serialize, serde::Deserialize)]
112#[serde(deny_unknown_fields, default)]
113#[non_exhaustive]
114pub struct Server {
115 #[serde(skip_serializing_if = "Option::is_none")]
118 #[arg(
119 id = "server-quic-max-streams",
120 long = "server-quic-max-streams",
121 alias = "server-max-streams",
122 env = "MOQ_SERVER_QUIC_MAX_STREAMS"
123 )]
124 pub max_streams: Option<u64>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
128 #[arg(
129 id = "server-quic-gso",
130 long = "server-quic-gso",
131 env = "MOQ_SERVER_QUIC_GSO",
132 default_missing_value = "true",
133 num_args = 0..=1,
134 require_equals = true,
135 value_parser = clap::value_parser!(bool),
136 )]
137 pub gso: Option<bool>,
138
139 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
141 #[arg(
142 id = "server-quic-idle-timeout",
143 long = "server-quic-idle-timeout",
144 env = "MOQ_SERVER_QUIC_IDLE_TIMEOUT",
145 value_parser = humantime::parse_duration,
146 )]
147 pub idle_timeout: Option<Duration>,
148
149 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
152 #[arg(
153 id = "server-quic-keep-alive",
154 long = "server-quic-keep-alive",
155 env = "MOQ_SERVER_QUIC_KEEP_ALIVE",
156 value_parser = humantime::parse_duration,
157 )]
158 pub keep_alive: Option<Duration>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
162 #[arg(
163 id = "server-quic-mtu-discovery",
164 long = "server-quic-mtu-discovery",
165 env = "MOQ_SERVER_QUIC_MTU_DISCOVERY",
166 default_missing_value = "true",
167 num_args = 0..=1,
168 require_equals = true,
169 value_parser = clap::value_parser!(bool),
170 )]
171 pub mtu_discovery: Option<bool>,
172
173 #[arg(
181 id = "server-preferred-v4",
182 long = "server-preferred-v4",
183 env = "MOQ_SERVER_PREFERRED_V4"
184 )]
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub preferred_v4: Option<net::SocketAddrV4>,
187
188 #[arg(
190 id = "server-preferred-v6",
191 long = "server-preferred-v6",
192 env = "MOQ_SERVER_PREFERRED_V6"
193 )]
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub preferred_v6: Option<net::SocketAddrV6>,
196
197 #[arg(id = "server-quic-lb-id", long = "server-quic-lb-id", env = "MOQ_SERVER_QUIC_LB_ID")]
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub quic_lb_id: Option<ServerId>,
202
203 #[arg(
206 id = "server-quic-lb-nonce",
207 long = "server-quic-lb-nonce",
208 requires = "server-quic-lb-id",
209 env = "MOQ_SERVER_QUIC_LB_NONCE"
210 )]
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub quic_lb_nonce: Option<usize>,
213}
214
215impl Server {
216 pub(crate) fn resolve(&self) -> Resolved {
218 Resolved::new(
219 self.max_streams,
220 self.gso,
221 self.idle_timeout,
222 self.keep_alive,
223 self.mtu_discovery,
224 )
225 }
226}
227
228#[derive(Clone, Copy, Debug)]
234pub(crate) struct Resolved {
235 pub max_streams: u64,
237 pub gso: Option<bool>,
239 pub idle_timeout: Duration,
241 pub keep_alive: Option<Duration>,
243 pub mtu_discovery: bool,
245}
246
247impl Resolved {
248 fn new(
249 max_streams: Option<u64>,
250 gso: Option<bool>,
251 idle_timeout: Option<Duration>,
252 keep_alive: Option<Duration>,
253 mtu_discovery: Option<bool>,
254 ) -> Self {
255 let keep_alive = match keep_alive {
258 Some(d) if d.is_zero() => None,
259 Some(d) => Some(d),
260 None => Some(DEFAULT_KEEP_ALIVE),
261 };
262
263 Self {
264 max_streams: max_streams.unwrap_or(DEFAULT_MAX_STREAMS),
265 gso,
266 idle_timeout: idle_timeout.unwrap_or(DEFAULT_IDLE_TIMEOUT),
267 keep_alive,
268 mtu_discovery: mtu_discovery.unwrap_or(false),
269 }
270 }
271
272 pub(crate) fn gso_disabled(&self) -> bool {
274 self.gso == Some(false)
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use clap::Parser;
282
283 #[derive(Parser)]
286 struct Both {
287 #[command(flatten)]
288 client: Client,
289 #[command(flatten)]
290 server: Server,
291 }
292
293 fn parse(args: &[&str]) -> Both {
294 let mut full = vec!["test"];
295 full.extend_from_slice(args);
296 Both::parse_from(full)
297 }
298
299 #[test]
300 fn defaults_apply_when_unset() {
301 let quic = Client::default().resolve();
302 assert_eq!(quic.max_streams, DEFAULT_MAX_STREAMS);
303 assert_eq!(quic.idle_timeout, DEFAULT_IDLE_TIMEOUT);
304 assert_eq!(quic.keep_alive, Some(DEFAULT_KEEP_ALIVE));
305 assert!(!quic.mtu_discovery);
306 assert_eq!(quic.gso, None);
307 assert!(!quic.gso_disabled());
308 }
309
310 #[test]
311 fn zero_keep_alive_disables_it() {
312 let disabled = Server {
313 keep_alive: Some(Duration::ZERO),
314 ..Default::default()
315 };
316 assert_eq!(disabled.resolve().keep_alive, None);
317
318 let explicit = Client {
319 keep_alive: Some(Duration::from_secs(2)),
320 ..Default::default()
321 };
322 assert_eq!(explicit.resolve().keep_alive, Some(Duration::from_secs(2)));
323 }
324
325 #[test]
326 fn gso_disabled_only_on_explicit_false() {
327 let off = Client {
328 gso: Some(false),
329 ..Default::default()
330 };
331 assert!(off.resolve().gso_disabled());
332 let on = Client {
333 gso: Some(true),
334 ..Default::default()
335 };
336 assert!(!on.resolve().gso_disabled());
337 }
338
339 #[test]
340 fn client_and_server_flags_are_distinct() {
341 let both = parse(&["--client-quic-max-streams", "5000", "--server-quic-max-streams", "9000"]);
342 assert_eq!(both.client.max_streams, Some(5000));
343 assert_eq!(both.server.max_streams, Some(9000));
344 }
345
346 #[test]
347 fn server_only_knobs_parse() {
348 let both = parse(&["--server-preferred-v4", "192.0.2.1:443", "--server-quic-lb-id", "ab"]);
349 assert_eq!(both.server.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
350 assert!(both.server.quic_lb_id.is_some());
351 assert_eq!(both.client.max_streams, None);
353 }
354
355 #[test]
356 fn deprecated_max_streams_aliases() {
357 let both = parse(&["--client-max-streams", "2048", "--server-max-streams", "4096"]);
358 assert_eq!(both.client.max_streams, Some(2048));
359 assert_eq!(both.server.max_streams, Some(4096));
360 }
361
362 #[test]
363 fn toml_round_trips() {
364 let toml = r#"
365 max_streams = 7000
366 gso = false
367 preferred_v4 = "192.0.2.1:443"
368 "#;
369 let quic: Server = toml::from_str(toml).unwrap();
370 assert_eq!(quic.max_streams, Some(7000));
371 assert_eq!(quic.gso, Some(false));
372 assert_eq!(quic.preferred_v4, Some("192.0.2.1:443".parse().unwrap()));
373 }
374}