1use crate::client::ClientConfig;
2use crate::server::{ServerConfig, ServerId};
3use crate::tls::{FingerprintVerifier, ServeCerts};
4use std::sync::{Arc, RwLock};
5use std::time::Duration;
6use std::{net, time};
7use url::Url;
8
9pub use web_transport_quinn;
10
11#[derive(Debug, thiserror::Error)]
13#[non_exhaustive]
14pub enum Error {
15 #[error("failed to bind UDP socket")]
16 BindSocket(#[source] std::io::Error),
17
18 #[error("failed to create QUIC endpoint")]
19 CreateEndpoint(#[source] std::io::Error),
20
21 #[error("no async runtime")]
22 NoRuntime,
23
24 #[error("failed to get local address")]
25 LocalAddr(#[source] std::io::Error),
26
27 #[error("failed to resolve bind address")]
28 ResolveBind(#[source] std::io::Error),
29
30 #[error("invalid DNS name")]
31 InvalidDnsName,
32
33 #[error("failed DNS lookup")]
34 DnsLookup(#[source] std::io::Error),
35
36 #[error("no DNS entries")]
37 NoDnsEntries,
38
39 #[error("failed to fetch fingerprint")]
40 FetchFingerprint(#[source] reqwest::Error),
41
42 #[error("fingerprint request failed")]
43 FingerprintStatus(#[source] reqwest::Error),
44
45 #[error("failed to read fingerprint")]
46 ReadFingerprint(#[source] reqwest::Error),
47
48 #[error("invalid fingerprint")]
49 InvalidFingerprint(#[from] hex::FromHexError),
50
51 #[error("url scheme must be 'https', 'moqt', or 'moql'")]
52 InvalidScheme,
53
54 #[error("unsupported URL scheme: {0}")]
55 UnsupportedScheme(String),
56
57 #[error("missing handshake data")]
58 MissingHandshake,
59
60 #[error("missing ALPN")]
61 MissingAlpn,
62
63 #[error("failed to decode ALPN")]
64 DecodeAlpn(#[from] std::string::FromUtf8Error),
65
66 #[error("unsupported ALPN: {0}")]
67 UnsupportedAlpn(String),
68
69 #[error("missing server name for raw QUIC connection")]
70 MissingServerName,
71
72 #[error("failed to construct URL from server name")]
73 BuildUrl(#[source] url::ParseError),
74
75 #[error("quic_lb_nonce must be at least 4")]
76 QuicLbNonceTooSmall,
77
78 #[error("connection ID length ({0}) exceeds maximum of 20")]
79 QuicLbCidTooLong(usize),
80
81 #[error("failed to build client certificate verifier")]
82 ClientVerifier(#[source] rustls::server::VerifierBuilderError),
83
84 #[error(transparent)]
85 NoInitialCipherSuite(#[from] quinn::crypto::rustls::NoInitialCipherSuite),
86
87 #[error(transparent)]
88 Connect(#[from] quinn::ConnectError),
89
90 #[error(transparent)]
91 Connection(#[from] quinn::ConnectionError),
92
93 #[error(transparent)]
94 Client(#[from] web_transport_quinn::ClientError),
95
96 #[error(transparent)]
97 ConnectRejected(#[from] crate::ConnectError),
98
99 #[error(transparent)]
100 Server(#[from] web_transport_quinn::ServerError),
101
102 #[error("failed to establish QUIC connection")]
103 Establish(#[source] quinn::ConnectionError),
104
105 #[error("failed to receive WebTransport request")]
106 RecvRequest(#[source] web_transport_quinn::ServerError),
107
108 #[error(transparent)]
109 Tls(#[from] crate::tls::Error),
110}
111
112type Result<T> = std::result::Result<T, Error>;
113
114#[derive(Clone)]
117pub(crate) struct QuinnClient {
118 pub quic: quinn::Endpoint,
119 pub transport: Arc<quinn::TransportConfig>,
120 pub http_bootstrap: bool,
122 pub host_name: Option<String>,
124}
125
126impl QuinnClient {
127 pub fn new(config: &ClientConfig) -> Result<Self> {
128 let socket = crate::bind::udp(config.bind).map_err(Error::BindSocket)?;
129
130 let mut transport = quinn::TransportConfig::default();
132 transport.max_idle_timeout(Some(time::Duration::from_secs(30).try_into().unwrap()));
133 transport.keep_alive_interval(Some(time::Duration::from_secs(5)));
134 transport.mtu_discovery_config(None); let max_streams = config.max_streams.unwrap_or(crate::DEFAULT_MAX_STREAMS);
137 let max_streams = quinn::VarInt::from_u64(max_streams).unwrap_or(quinn::VarInt::MAX);
138 transport.max_concurrent_bidi_streams(max_streams);
139 transport.max_concurrent_uni_streams(max_streams);
140
141 let transport = Arc::new(transport);
142
143 let runtime = quinn::default_runtime().ok_or(Error::NoRuntime)?;
145 let endpoint_config = quinn::EndpointConfig::default();
146
147 let quic = quinn::Endpoint::new(endpoint_config, None, socket, runtime).map_err(Error::CreateEndpoint)?;
149
150 Ok(Self {
151 quic,
152 transport,
153 http_bootstrap: config.tls.allows_http_bootstrap(),
154 host_name: config.tls.host_name.clone(),
155 })
156 }
157
158 pub async fn connect(
159 &self,
160 tls: &rustls::ClientConfig,
161 url: Url,
162 versions: &moq_net::Versions,
163 ) -> Result<web_transport_quinn::Session> {
164 let mut url = url;
165 let mut config = tls.clone();
166
167 let host = url.host().ok_or(Error::InvalidDnsName)?.to_string();
168 let port = url.port().unwrap_or(443);
169
170 let local = self.quic.local_addr().map_err(Error::LocalAddr)?;
176 let addrs = tokio::net::lookup_host((host.clone(), port))
177 .await
178 .map_err(Error::DnsLookup)?;
179 let ip = crate::util::pick_addr(addrs, local).ok_or(Error::NoDnsEntries)?;
180
181 if url.scheme() == "http" {
182 if self.http_bootstrap {
186 let mut fingerprint = url.clone();
188 fingerprint.set_path("/certificate.sha256");
189 fingerprint.set_query(None);
190 fingerprint.set_fragment(None);
191
192 tracing::warn!(url = %fingerprint, "performing insecure HTTP request for certificate");
193
194 let resp = reqwest::get(fingerprint.as_str())
195 .await
196 .map_err(Error::FetchFingerprint)?
197 .error_for_status()
198 .map_err(Error::FingerprintStatus)?;
199
200 let fingerprint = resp.text().await.map_err(Error::ReadFingerprint)?;
201 let fingerprint = hex::decode(fingerprint.trim())?;
202
203 let verifier = FingerprintVerifier::new(config.crypto_provider().clone(), vec![fingerprint]);
204 config.dangerous().set_certificate_verifier(Arc::new(verifier));
205 } else {
206 tracing::warn!(
207 "ignoring insecure http:// fingerprint bootstrap; using the configured TLS verification"
208 );
209 }
210
211 url.set_scheme("https").expect("failed to set scheme");
212 }
213
214 let alpns: Vec<Vec<u8>> = match url.scheme() {
215 "https" => vec![web_transport_quinn::ALPN.as_bytes().to_vec()],
216 "moqt" | "moql" => versions.alpns().iter().map(|alpn| alpn.as_bytes().to_vec()).collect(),
217 _ => return Err(Error::InvalidScheme),
218 };
219
220 config.alpn_protocols = alpns;
221 config.key_log = Arc::new(rustls::KeyLogFile::new());
222
223 let config: quinn::crypto::rustls::QuicClientConfig = config.try_into()?;
224 let mut config = quinn::ClientConfig::new(Arc::new(config));
225 config.transport_config(self.transport.clone());
226
227 tracing::debug!(%url, %ip, "connecting");
228
229 let host_name = self.host_name.clone().unwrap_or(host);
231
232 let connection = self.quic.connect_with(config, ip, &host_name)?.await?;
233 tracing::Span::current().record("id", connection.stable_id());
234
235 let mut request = web_transport_quinn::proto::ConnectRequest::new(url.clone());
236 for alpn in versions.alpns() {
237 request = request.with_protocol(alpn.to_string());
238 }
239
240 let session = match url.scheme() {
241 "https" => web_transport_quinn::Session::connect(connection, request)
242 .await
243 .map_err(map_client_error)?,
244 "moqt" | "moql" => {
245 let handshake = connection
246 .handshake_data()
247 .ok_or(Error::MissingHandshake)?
248 .downcast::<quinn::crypto::rustls::HandshakeData>()
249 .unwrap();
250
251 let alpn = handshake.protocol.ok_or(Error::MissingAlpn)?;
252 let alpn = String::from_utf8(alpn)?;
253
254 let response = web_transport_quinn::proto::ConnectResponse::OK.with_protocol(alpn);
255 web_transport_quinn::Session::raw(connection, request, response)
256 }
257 _ => return Err(Error::UnsupportedScheme(url.scheme().to_string())),
258 };
259
260 Ok(session)
261 }
262}
263
264impl Error {
265 pub(crate) fn connect_error(&self) -> Option<crate::ConnectError> {
266 match self {
267 Self::ConnectRejected(err) => Some(*err),
268 Self::Client(err) => classify_client_error(err),
269 _ => None,
270 }
271 }
272}
273
274fn map_client_error(err: web_transport_quinn::ClientError) -> Error {
275 if let Some(err) = classify_client_error(&err) {
276 return err.into();
277 }
278
279 err.into()
280}
281
282fn classify_client_error(err: &web_transport_quinn::ClientError) -> Option<crate::ConnectError> {
283 match err {
284 web_transport_quinn::ClientError::HttpError(err) => classify_connect_error(err),
285 _ => None,
286 }
287}
288
289fn classify_connect_error(err: &web_transport_quinn::ConnectError) -> Option<crate::ConnectError> {
290 match err {
291 web_transport_quinn::ConnectError::ErrorStatus(status) => crate::ConnectError::from_status_u16(status.as_u16()),
292 web_transport_quinn::ConnectError::ProtoError(err) => classify_proto_error(err),
293 _ => None,
294 }
295}
296
297fn classify_proto_error(err: &web_transport_quinn::proto::ConnectError) -> Option<crate::ConnectError> {
298 match err {
299 web_transport_quinn::proto::ConnectError::ErrorStatus(status)
300 | web_transport_quinn::proto::ConnectError::WrongStatus(Some(status)) => {
301 crate::ConnectError::from_status_u16(status.as_u16())
302 }
303 _ => None,
304 }
305}
306
307pub(crate) struct QuinnServer {
310 pub quic: quinn::Endpoint,
311 pub certs: Arc<ServeCerts>,
312}
313
314impl QuinnServer {
315 pub fn new(config: ServerConfig) -> Result<Self> {
316 let mut transport = quinn::TransportConfig::default();
319 transport.max_idle_timeout(Some(Duration::from_secs(30).try_into().unwrap()));
320 transport.keep_alive_interval(Some(Duration::from_secs(5)));
321 transport.mtu_discovery_config(None); let max_streams = config.max_streams.unwrap_or(crate::DEFAULT_MAX_STREAMS);
324 let max_streams = quinn::VarInt::from_u64(max_streams).unwrap_or(quinn::VarInt::MAX);
325 transport.max_concurrent_bidi_streams(max_streams);
326 transport.max_concurrent_uni_streams(max_streams);
327
328 let transport = Arc::new(transport);
329
330 let provider = crate::crypto::provider();
331
332 let certs = ServeCerts::new(provider.clone());
333 certs.load_certs(&config.tls)?;
334 let certs = Arc::new(certs);
335
336 let tls_builder = rustls::ServerConfig::builder_with_provider(provider.clone())
337 .with_protocol_versions(&[&rustls::version::TLS13])
338 .map_err(crate::tls::Error::from)?;
339
340 let mut tls = if config.tls.root.is_empty() {
341 tls_builder.with_no_client_auth().with_cert_resolver(certs.clone())
342 } else {
343 let roots = config.tls.load_roots()?;
344 let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider)
345 .allow_unauthenticated()
346 .build()
347 .map_err(Error::ClientVerifier)?;
348 tls_builder
349 .with_client_cert_verifier(verifier)
350 .with_cert_resolver(certs.clone())
351 };
352
353 let mut alpns: Vec<Vec<u8>> = config
355 .versions()
356 .alpns()
357 .iter()
358 .map(|alpn| alpn.as_bytes().to_vec())
359 .collect();
360 alpns.push(web_transport_quinn::ALPN.as_bytes().to_vec());
361
362 tls.alpn_protocols = alpns;
363 tls.key_log = Arc::new(rustls::KeyLogFile::new());
364
365 let tls: quinn::crypto::rustls::QuicServerConfig = tls.try_into()?;
366 let mut tls = quinn::ServerConfig::with_crypto(Arc::new(tls));
367 tls.transport_config(transport);
368
369 if let Some(addr) = config.preferred_v4 {
372 tls.preferred_address_v4(Some(addr));
373 }
374 if let Some(addr) = config.preferred_v6 {
375 tls.preferred_address_v6(Some(addr));
376 }
377
378 let runtime = quinn::default_runtime().ok_or(Error::NoRuntime)?;
380
381 let listen =
382 crate::util::resolve(config.bind.as_deref(), crate::server::DEFAULT_BIND).map_err(Error::ResolveBind)?;
383
384 let mut endpoint_config = quinn::EndpointConfig::default();
386 if let Some(server_id) = config.quic_lb_id {
387 let nonce_len = config.quic_lb_nonce.unwrap_or(8);
388 if nonce_len < 4 {
389 return Err(Error::QuicLbNonceTooSmall);
390 }
391
392 let cid_len = 1 + server_id.len() + nonce_len;
393 if cid_len > 20 {
394 return Err(Error::QuicLbCidTooLong(cid_len));
395 }
396
397 tracing::info!(
398 ?server_id,
399 nonce_len,
400 "using QUIC-LB compatible connection ID generation"
401 );
402 endpoint_config.cid_generator(move || Box::new(ServerIdGenerator::new(server_id.clone(), nonce_len)));
403 }
404
405 let socket = crate::bind::udp(listen).map_err(Error::BindSocket)?;
406
407 let quic = quinn::Endpoint::new(endpoint_config, Some(tls), socket, runtime).map_err(Error::CreateEndpoint)?;
409
410 tokio::spawn(crate::tls::reload_certs(certs.clone(), config.tls.clone()));
413
414 Ok(Self { quic, certs })
415 }
416
417 pub fn accept(&self) -> impl std::future::Future<Output = Option<quinn::Incoming>> + '_ {
418 self.quic.accept()
419 }
420
421 pub fn tls_info(&self) -> Arc<RwLock<crate::tls::Info>> {
422 self.certs.info.clone()
423 }
424
425 pub fn local_addr(&self) -> Result<net::SocketAddr> {
426 self.quic.local_addr().map_err(Error::LocalAddr)
427 }
428
429 pub fn close(&self) {
430 self.quic.close(quinn::VarInt::from_u32(0), b"server shutdown");
431 }
432}
433
434pub(crate) enum QuinnRequest {
438 Raw {
439 request: web_transport_quinn::proto::ConnectRequest,
440 response: web_transport_quinn::proto::ConnectResponse,
441 connection: quinn::Connection,
442 },
443 WebTransport {
444 request: web_transport_quinn::Request,
445 alpns: Vec<&'static str>,
446 },
447}
448
449impl QuinnRequest {
450 pub async fn accept(conn: quinn::Incoming, alpns: Vec<&'static str>) -> Result<Self> {
451 let mut conn = conn.accept()?;
452
453 let handshake = conn
454 .handshake_data()
455 .await?
456 .downcast::<quinn::crypto::rustls::HandshakeData>()
457 .unwrap();
458
459 let alpn = handshake.protocol.ok_or(Error::MissingAlpn)?;
460 let alpn = String::from_utf8(alpn)?;
461 let host = handshake.server_name.unwrap_or_default();
462
463 tracing::debug!(%host, ip = %conn.remote_address(), %alpn, "accepting");
464
465 let conn = conn.await.map_err(Error::Establish)?;
467
468 let span = tracing::Span::current();
469 span.record("id", conn.stable_id()); tracing::debug!(%host, ip = %conn.remote_address(), %alpn, "accepted");
471
472 match alpn.as_str() {
473 web_transport_quinn::ALPN => {
474 let request = web_transport_quinn::Request::accept(conn)
476 .await
477 .map_err(Error::RecvRequest)?;
478 Ok(Self::WebTransport { request, alpns })
479 }
480 alpn if moq_net::ALPNS.contains(&alpn) => {
481 if host.is_empty() {
482 return Err(Error::MissingServerName);
483 }
484 let host_str = if host.contains(':') {
485 format!("[{}]", host)
486 } else {
487 host.clone()
488 };
489 let url = format!("moqt://{}", host_str).parse::<Url>().map_err(Error::BuildUrl)?;
490 let request = web_transport_quinn::proto::ConnectRequest::new(url);
491 let response = web_transport_quinn::proto::ConnectResponse::OK.with_protocol(alpn);
492 Ok(Self::Raw {
493 connection: conn,
494 request,
495 response,
496 })
497 }
498 _ => Err(Error::UnsupportedAlpn(alpn)),
499 }
500 }
501
502 pub async fn ok(self) -> std::result::Result<web_transport_quinn::Session, web_transport_quinn::ServerError> {
504 match self {
505 QuinnRequest::Raw {
506 connection,
507 request,
508 response,
509 } => Ok(web_transport_quinn::Session::raw(connection, request, response)),
510 QuinnRequest::WebTransport { request, alpns } => {
511 let mut response = web_transport_quinn::proto::ConnectResponse::OK;
512 if let Some(protocol) = request.protocols.iter().find(|p| alpns.contains(&p.as_str())) {
518 response = response.with_protocol(protocol);
519 }
520 request.respond(response).await
521 }
522 }
523 }
524
525 pub fn url(&self) -> Option<&Url> {
527 match self {
528 QuinnRequest::Raw { .. } => None,
529 QuinnRequest::WebTransport { request, .. } => Some(&request.url),
530 }
531 }
532
533 pub fn peer_identity(&self) -> Option<crate::tls::PeerIdentity> {
536 let conn = match self {
537 QuinnRequest::Raw { connection, .. } => connection,
538 QuinnRequest::WebTransport { request, .. } => request.conn(),
539 };
540 crate::tls::PeerIdentity::from_any(conn.peer_identity())
541 }
542
543 pub async fn close(
545 self,
546 status: web_transport_quinn::http::StatusCode,
547 ) -> std::result::Result<(), web_transport_quinn::ServerError> {
548 match self {
549 QuinnRequest::Raw { connection, .. } => {
550 connection.close(status.as_u16().into(), status.as_str().as_bytes());
551 Ok(())
552 }
553 QuinnRequest::WebTransport { request, alpns: _, .. } => request.reject(status).await,
554 }
555 }
556}
557
558struct ServerIdGenerator {
561 server_id: ServerId,
562 nonce_len: usize,
563}
564
565impl ServerIdGenerator {
566 fn new(server_id: ServerId, nonce_len: usize) -> Self {
567 Self { server_id, nonce_len }
568 }
569}
570
571impl quinn::ConnectionIdGenerator for ServerIdGenerator {
572 fn generate_cid(&mut self) -> quinn::ConnectionId {
573 use rand::RngExt;
574 let cid_len = self.cid_len();
575 let mut cid = Vec::with_capacity(cid_len);
576 cid.push((cid_len - 1) as u8);
578 cid.extend(self.server_id.0.iter());
579 cid.extend(rand::rng().random_iter::<u8>().take(self.nonce_len));
580 quinn::ConnectionId::new(cid.as_slice())
581 }
582
583 fn cid_len(&self) -> usize {
584 1 + self.server_id.len() + self.nonce_len
585 }
586
587 fn cid_lifetime(&self) -> Option<Duration> {
588 None
589 }
590}