1use crate::crypto;
2use crate::{Backoff, QuicBackend, Reconnect};
3use anyhow::Context;
4use std::path::PathBuf;
5use std::{net, sync::Arc};
6use url::Url;
7
8#[serde_with::serde_as]
10#[derive(Clone, Default, Debug, clap::Args, serde::Serialize, serde::Deserialize)]
11#[serde(default, deny_unknown_fields)]
12#[non_exhaustive]
13pub struct ClientTls {
14 #[serde(skip_serializing_if = "Vec::is_empty")]
20 #[arg(id = "tls-root", long = "tls-root", env = "MOQ_CLIENT_TLS_ROOT")]
21 #[serde_as(as = "serde_with::OneOrMany<_>")]
22 pub root: Vec<PathBuf>,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
29 #[arg(id = "client-tls-cert", long = "client-tls-cert", env = "MOQ_CLIENT_TLS_CERT")]
30 pub cert: Option<PathBuf>,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
37 #[arg(id = "client-tls-key", long = "client-tls-key", env = "MOQ_CLIENT_TLS_KEY")]
38 pub key: Option<PathBuf>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
44 #[arg(
45 id = "tls-disable-verify",
46 long = "tls-disable-verify",
47 env = "MOQ_CLIENT_TLS_DISABLE_VERIFY",
48 default_missing_value = "true",
49 num_args = 0..=1,
50 require_equals = true,
51 value_parser = clap::value_parser!(bool),
52 )]
53 pub disable_verify: Option<bool>,
54}
55
56#[derive(Clone, Debug, clap::Parser, serde::Serialize, serde::Deserialize)]
58#[serde(deny_unknown_fields, default)]
59#[non_exhaustive]
60pub struct ClientConfig {
61 #[arg(
63 id = "client-bind",
64 long = "client-bind",
65 default_value = "[::]:0",
66 env = "MOQ_CLIENT_BIND"
67 )]
68 pub bind: net::SocketAddr,
69
70 #[arg(id = "client-backend", long = "client-backend", env = "MOQ_CLIENT_BACKEND")]
73 pub backend: Option<QuicBackend>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
77 #[arg(
78 id = "client-max-streams",
79 long = "client-max-streams",
80 env = "MOQ_CLIENT_MAX_STREAMS"
81 )]
82 pub max_streams: Option<u64>,
83
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
92 #[arg(id = "client-version", long = "client-version", env = "MOQ_CLIENT_VERSION")]
93 pub version: Vec<moq_net::Version>,
94
95 #[command(flatten)]
96 #[serde(default)]
97 pub tls: ClientTls,
98
99 #[command(flatten)]
100 #[serde(default)]
101 pub backoff: Backoff,
102
103 #[cfg(feature = "websocket")]
104 #[command(flatten)]
105 #[serde(default)]
106 pub websocket: super::ClientWebSocket,
107}
108
109impl ClientTls {
110 pub fn build(&self) -> anyhow::Result<rustls::ClientConfig> {
116 use rustls::pki_types::CertificateDer;
117
118 let provider = crypto::provider();
119
120 let mut roots = rustls::RootCertStore::empty();
121 if self.root.is_empty() {
122 let native = rustls_native_certs::load_native_certs();
123 for err in native.errors {
124 tracing::warn!(%err, "failed to load root cert");
125 }
126 for cert in native.certs {
127 roots.add(cert).context("failed to add root cert")?;
128 }
129 } else {
130 for root in &self.root {
131 let file = std::fs::File::open(root).context("failed to open root cert file")?;
132 let mut reader = std::io::BufReader::new(file);
133 let cert = rustls_pemfile::certs(&mut reader)
134 .next()
135 .context("no roots found")?
136 .context("failed to read root cert")?;
137 roots.add(cert).context("failed to add root cert")?;
138 }
139 }
140
141 let builder = rustls::ClientConfig::builder_with_provider(provider.clone())
144 .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])?
145 .with_root_certificates(roots);
146
147 let mut tls = match (&self.cert, &self.key) {
148 (Some(cert_path), Some(key_path)) => {
149 let cert_pem = std::fs::read(cert_path).context("failed to read client certificate")?;
150 let chain: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_pem.as_slice())
151 .collect::<Result<_, _>>()
152 .context("failed to parse client certificate")?;
153 anyhow::ensure!(!chain.is_empty(), "no certificates found in client certificate");
154 let key_pem = std::fs::read(key_path).context("failed to read client key")?;
155 let key = rustls_pemfile::private_key(&mut key_pem.as_slice())
156 .context("failed to parse client key")?
157 .context("no private key found in client key")?;
158 builder
159 .with_client_auth_cert(chain, key)
160 .context("failed to configure client certificate")?
161 }
162 (None, None) => builder.with_no_client_auth(),
163 _ => anyhow::bail!("both --client-tls-cert and --client-tls-key must be provided"),
164 };
165
166 if self.disable_verify.unwrap_or_default() {
167 tracing::warn!("TLS server certificate verification is disabled; A man-in-the-middle attack is possible.");
168 let noop = NoCertificateVerification(provider);
169 tls.dangerous().set_certificate_verifier(Arc::new(noop));
170 }
171
172 Ok(tls)
173 }
174}
175
176impl ClientConfig {
177 pub fn init(self) -> anyhow::Result<Client> {
178 Client::new(self)
179 }
180
181 pub fn versions(&self) -> moq_net::Versions {
183 if self.version.is_empty() {
184 moq_net::Versions::all()
185 } else {
186 moq_net::Versions::from(self.version.clone())
187 }
188 }
189}
190
191impl Default for ClientConfig {
192 fn default() -> Self {
193 Self {
194 bind: "[::]:0".parse().unwrap(),
195 backend: None,
196 max_streams: None,
197 version: Vec::new(),
198 tls: ClientTls::default(),
199 backoff: Backoff::default(),
200 #[cfg(feature = "websocket")]
201 websocket: super::ClientWebSocket::default(),
202 }
203 }
204}
205
206#[derive(Clone)]
210pub struct Client {
211 moq: moq_net::Client,
212 versions: moq_net::Versions,
213 backoff: Backoff,
214 #[cfg(feature = "websocket")]
215 websocket: super::ClientWebSocket,
216 tls: rustls::ClientConfig,
217 #[cfg(feature = "noq")]
218 noq: Option<crate::noq::NoqClient>,
219 #[cfg(feature = "quinn")]
220 quinn: Option<crate::quinn::QuinnClient>,
221 #[cfg(feature = "quiche")]
222 quiche: Option<crate::quiche::QuicheClient>,
223 #[cfg(feature = "iroh")]
224 iroh: Option<web_transport_iroh::iroh::Endpoint>,
225 #[cfg(feature = "iroh")]
226 iroh_addrs: Vec<std::net::SocketAddr>,
227}
228
229impl Client {
230 #[cfg(not(any(feature = "noq", feature = "quinn", feature = "quiche", feature = "websocket")))]
231 pub fn new(_config: ClientConfig) -> anyhow::Result<Self> {
232 anyhow::bail!("no QUIC or WebSocket backend compiled; enable noq, quinn, quiche, or websocket feature");
233 }
234
235 #[cfg(any(feature = "noq", feature = "quinn", feature = "quiche", feature = "websocket"))]
237 pub fn new(config: ClientConfig) -> anyhow::Result<Self> {
238 #[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
239 let backend = config.backend.clone().unwrap_or({
240 #[cfg(feature = "quinn")]
241 {
242 QuicBackend::Quinn
243 }
244 #[cfg(all(feature = "noq", not(feature = "quinn")))]
245 {
246 QuicBackend::Noq
247 }
248 #[cfg(all(feature = "quiche", not(feature = "quinn"), not(feature = "noq")))]
249 {
250 QuicBackend::Quiche
251 }
252 #[cfg(all(not(feature = "quiche"), not(feature = "quinn"), not(feature = "noq")))]
253 panic!("no QUIC backend compiled; enable noq, quinn, or quiche feature");
254 });
255
256 let tls = config.tls.build()?;
257
258 #[cfg(feature = "noq")]
259 #[allow(unreachable_patterns)]
260 let noq = match backend {
261 QuicBackend::Noq => Some(crate::noq::NoqClient::new(&config)?),
262 _ => None,
263 };
264
265 #[cfg(feature = "quinn")]
266 #[allow(unreachable_patterns)]
267 let quinn = match backend {
268 QuicBackend::Quinn => Some(crate::quinn::QuinnClient::new(&config)?),
269 _ => None,
270 };
271
272 #[cfg(feature = "quiche")]
273 let quiche = match backend {
274 QuicBackend::Quiche => Some(crate::quiche::QuicheClient::new(&config)?),
275 _ => None,
276 };
277
278 let versions = config.versions();
279 Ok(Self {
280 moq: moq_net::Client::new().with_versions(versions.clone()),
281 versions,
282 backoff: config.backoff,
283 #[cfg(feature = "websocket")]
284 websocket: config.websocket,
285 tls,
286 #[cfg(feature = "noq")]
287 noq,
288 #[cfg(feature = "quinn")]
289 quinn,
290 #[cfg(feature = "quiche")]
291 quiche,
292 #[cfg(feature = "iroh")]
293 iroh: None,
294 #[cfg(feature = "iroh")]
295 iroh_addrs: Vec::new(),
296 })
297 }
298
299 #[cfg(feature = "iroh")]
300 pub fn with_iroh(mut self, iroh: Option<web_transport_iroh::iroh::Endpoint>) -> Self {
301 self.iroh = iroh;
302 self
303 }
304
305 #[cfg(feature = "iroh")]
310 pub fn with_iroh_addrs(mut self, addrs: Vec<std::net::SocketAddr>) -> Self {
311 self.iroh_addrs = addrs;
312 self
313 }
314
315 pub fn with_publish(mut self, publish: impl Into<Option<moq_net::OriginConsumer>>) -> Self {
316 self.moq = self.moq.with_publish(publish);
317 self
318 }
319
320 pub fn with_consume(mut self, consume: impl Into<Option<moq_net::OriginProducer>>) -> Self {
321 self.moq = self.moq.with_consume(consume);
322 self
323 }
324
325 pub fn with_stats(mut self, stats: moq_net::StatsHandle) -> Self {
327 self.moq = self.moq.with_stats(stats);
328 self
329 }
330
331 pub fn reconnect(&self, url: Url) -> Reconnect {
336 Reconnect::new(self.clone(), url, self.backoff.clone())
337 }
338
339 #[cfg(not(any(
340 feature = "noq",
341 feature = "quinn",
342 feature = "quiche",
343 feature = "iroh",
344 feature = "websocket"
345 )))]
346 pub async fn connect(&self, _url: Url) -> anyhow::Result<moq_net::Session> {
347 anyhow::bail!("no backend compiled; enable noq, quinn, quiche, iroh, or websocket feature");
348 }
349
350 #[cfg(any(
351 feature = "noq",
352 feature = "quinn",
353 feature = "quiche",
354 feature = "iroh",
355 feature = "websocket"
356 ))]
357 pub async fn connect(&self, url: Url) -> anyhow::Result<moq_net::Session> {
358 let session = self.connect_inner(url).await?;
359 tracing::info!(version = %session.version(), "connected");
360 Ok(session)
361 }
362
363 #[cfg(any(
364 feature = "noq",
365 feature = "quinn",
366 feature = "quiche",
367 feature = "iroh",
368 feature = "websocket"
369 ))]
370 async fn connect_inner(&self, url: Url) -> anyhow::Result<moq_net::Session> {
371 #[cfg(feature = "iroh")]
372 if url.scheme() == "iroh" {
373 let endpoint = self.iroh.as_ref().context("Iroh support is not enabled")?;
374 let session = crate::iroh::connect(endpoint, url, self.iroh_addrs.iter().copied()).await?;
375 let session = self.moq.connect(session).await?;
376 return Ok(session);
377 }
378
379 #[cfg(feature = "noq")]
380 if let Some(noq) = self.noq.as_ref() {
381 let tls = self.tls.clone();
382 let quic_url = url.clone();
383 let quic_handle = async {
384 let res = noq.connect(&tls, quic_url).await;
385 if let Err(err) = &res {
386 tracing::warn!(%err, "QUIC connection failed");
387 }
388 res
389 };
390
391 #[cfg(feature = "websocket")]
392 {
393 let alpns = self.versions.alpns();
394 let ws_handle = crate::websocket::race_handle(&self.websocket, &self.tls, url, &alpns);
395
396 return Ok(tokio::select! {
397 Ok(quic) = quic_handle => self.moq.connect(quic).await?,
398 Some(Ok(ws)) = ws_handle => self.moq.connect(ws).await?,
399 else => anyhow::bail!("failed to connect to server"),
400 });
401 }
402
403 #[cfg(not(feature = "websocket"))]
404 {
405 let session = quic_handle.await?;
406 return Ok(self.moq.connect(session).await?);
407 }
408 }
409
410 #[cfg(feature = "quinn")]
411 if let Some(quinn) = self.quinn.as_ref() {
412 let tls = self.tls.clone();
413 let quic_url = url.clone();
414 let quic_handle = async {
415 let res = quinn.connect(&tls, quic_url).await;
416 if let Err(err) = &res {
417 tracing::warn!(%err, "QUIC connection failed");
418 }
419 res
420 };
421
422 #[cfg(feature = "websocket")]
423 {
424 let alpns = self.versions.alpns();
425 let ws_handle = crate::websocket::race_handle(&self.websocket, &self.tls, url, &alpns);
426
427 return Ok(tokio::select! {
428 Ok(quic) = quic_handle => self.moq.connect(quic).await?,
429 Some(Ok(ws)) = ws_handle => self.moq.connect(ws).await?,
430 else => anyhow::bail!("failed to connect to server"),
431 });
432 }
433
434 #[cfg(not(feature = "websocket"))]
435 {
436 let session = quic_handle.await?;
437 return Ok(self.moq.connect(session).await?);
438 }
439 }
440
441 #[cfg(feature = "quiche")]
442 if let Some(quiche) = self.quiche.as_ref() {
443 let quic_url = url.clone();
444 let quic_handle = async {
445 let res = quiche.connect(quic_url).await;
446 if let Err(err) = &res {
447 tracing::warn!(%err, "QUIC connection failed");
448 }
449 res
450 };
451
452 #[cfg(feature = "websocket")]
453 {
454 let alpns = self.versions.alpns();
455 let ws_handle = crate::websocket::race_handle(&self.websocket, &self.tls, url, &alpns);
456
457 return Ok(tokio::select! {
458 Ok(quic) = quic_handle => self.moq.connect(quic).await?,
459 Some(Ok(ws)) = ws_handle => self.moq.connect(ws).await?,
460 else => anyhow::bail!("failed to connect to server"),
461 });
462 }
463
464 #[cfg(not(feature = "websocket"))]
465 {
466 let session = quic_handle.await?;
467 return Ok(self.moq.connect(session).await?);
468 }
469 }
470
471 #[cfg(feature = "websocket")]
472 {
473 let alpns = self.versions.alpns();
474 let session = crate::websocket::connect(&self.websocket, &self.tls, url, &alpns).await?;
475 return Ok(self.moq.connect(session).await?);
476 }
477
478 #[cfg(not(feature = "websocket"))]
479 anyhow::bail!("no QUIC backend matched; this should not happen");
480 }
481}
482
483use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
484
485#[derive(Debug)]
486struct NoCertificateVerification(crypto::Provider);
487
488impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
489 fn verify_server_cert(
490 &self,
491 _end_entity: &CertificateDer<'_>,
492 _intermediates: &[CertificateDer<'_>],
493 _server_name: &ServerName<'_>,
494 _ocsp: &[u8],
495 _now: UnixTime,
496 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
497 Ok(rustls::client::danger::ServerCertVerified::assertion())
498 }
499
500 fn verify_tls12_signature(
501 &self,
502 message: &[u8],
503 cert: &CertificateDer<'_>,
504 dss: &rustls::DigitallySignedStruct,
505 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
506 rustls::crypto::verify_tls12_signature(message, cert, dss, &self.0.signature_verification_algorithms)
507 }
508
509 fn verify_tls13_signature(
510 &self,
511 message: &[u8],
512 cert: &CertificateDer<'_>,
513 dss: &rustls::DigitallySignedStruct,
514 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
515 rustls::crypto::verify_tls13_signature(message, cert, dss, &self.0.signature_verification_algorithms)
516 }
517
518 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
519 self.0.signature_verification_algorithms.supported_schemes()
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use clap::Parser;
527
528 #[test]
529 fn test_toml_disable_verify_survives_update_from() {
530 let toml = r#"
531 tls.disable_verify = true
532 "#;
533
534 let mut config: ClientConfig = toml::from_str(toml).unwrap();
535 assert_eq!(config.tls.disable_verify, Some(true));
536
537 config.update_from(["test"]);
539 assert_eq!(config.tls.disable_verify, Some(true));
540 }
541
542 #[test]
543 fn test_cli_disable_verify_flag() {
544 let config = ClientConfig::parse_from(["test", "--tls-disable-verify"]);
545 assert_eq!(config.tls.disable_verify, Some(true));
546 }
547
548 #[test]
549 fn test_cli_disable_verify_explicit_false() {
550 let config = ClientConfig::parse_from(["test", "--tls-disable-verify=false"]);
551 assert_eq!(config.tls.disable_verify, Some(false));
552 }
553
554 #[test]
555 fn test_cli_disable_verify_explicit_true() {
556 let config = ClientConfig::parse_from(["test", "--tls-disable-verify=true"]);
557 assert_eq!(config.tls.disable_verify, Some(true));
558 }
559
560 #[test]
561 fn test_cli_no_disable_verify() {
562 let config = ClientConfig::parse_from(["test"]);
563 assert_eq!(config.tls.disable_verify, None);
564 }
565
566 #[test]
567 fn test_toml_version_survives_update_from() {
568 let toml = r#"
569 version = ["moq-lite-02"]
570 "#;
571
572 let mut config: ClientConfig = toml::from_str(toml).unwrap();
573 assert_eq!(config.version, vec!["moq-lite-02".parse::<moq_net::Version>().unwrap()]);
574
575 config.update_from(["test"]);
577 assert_eq!(config.version, vec!["moq-lite-02".parse::<moq_net::Version>().unwrap()]);
578 }
579
580 #[test]
581 fn test_cli_version() {
582 let config = ClientConfig::parse_from(["test", "--client-version", "moq-lite-03"]);
583 assert_eq!(config.version, vec!["moq-lite-03".parse::<moq_net::Version>().unwrap()]);
584 }
585
586 #[test]
587 fn test_cli_no_version_defaults_to_all() {
588 let config = ClientConfig::parse_from(["test"]);
589 assert!(config.version.is_empty());
590 assert_eq!(config.versions().alpns().len(), moq_net::ALPNS.len());
592 }
593}