Skip to main content

moq_native/
quinn.rs

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/// Errors specific to the quinn QUIC backend.
12#[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// ── Client ──────────────────────────────────────────────────────────
115
116#[derive(Clone)]
117pub(crate) struct QuinnClient {
118	pub quic: quinn::Endpoint,
119	pub transport: Arc<quinn::TransportConfig>,
120	/// Whether an `http://` URL may bootstrap a pin (see [crate::tls::Client::allows_http_bootstrap]).
121	pub http_bootstrap: bool,
122	pub versions: moq_net::Versions,
123}
124
125impl QuinnClient {
126	pub fn new(config: &ClientConfig) -> Result<Self> {
127		let socket = crate::bind::udp(config.bind).map_err(Error::BindSocket)?;
128
129		// TODO Validate the BBR implementation before enabling it
130		let mut transport = quinn::TransportConfig::default();
131		transport.max_idle_timeout(Some(time::Duration::from_secs(30).try_into().unwrap()));
132		transport.keep_alive_interval(Some(time::Duration::from_secs(5)));
133		transport.mtu_discovery_config(None); // Disable MTU discovery
134
135		let max_streams = config.max_streams.unwrap_or(crate::DEFAULT_MAX_STREAMS);
136		let max_streams = quinn::VarInt::from_u64(max_streams).unwrap_or(quinn::VarInt::MAX);
137		transport.max_concurrent_bidi_streams(max_streams);
138		transport.max_concurrent_uni_streams(max_streams);
139
140		let transport = Arc::new(transport);
141
142		// There's a bit more boilerplate to make a generic endpoint.
143		let runtime = quinn::default_runtime().ok_or(Error::NoRuntime)?;
144		let endpoint_config = quinn::EndpointConfig::default();
145
146		// Create the generic QUIC endpoint.
147		let quic = quinn::Endpoint::new(endpoint_config, None, socket, runtime).map_err(Error::CreateEndpoint)?;
148
149		Ok(Self {
150			quic,
151			transport,
152			http_bootstrap: config.tls.allows_http_bootstrap(),
153			versions: config.versions(),
154		})
155	}
156
157	pub async fn connect(&self, tls: &rustls::ClientConfig, url: Url) -> Result<web_transport_quinn::Session> {
158		let mut url = url;
159		let mut config = tls.clone();
160
161		let host = url.host().ok_or(Error::InvalidDnsName)?.to_string();
162		let port = url.port().unwrap_or(443);
163
164		// Look up the DNS entry.
165		// Quinn doesn't support happy eyeballs, so we pick a single address,
166		// preferring one whose family matches the local socket so the OS
167		// doesn't reject it (notably on Windows, where IPv6 sockets aren't
168		// dual-stack by default).
169		let local = self.quic.local_addr().map_err(Error::LocalAddr)?;
170		let addrs = tokio::net::lookup_host((host.clone(), port))
171			.await
172			.map_err(Error::DnsLookup)?;
173		let ip = crate::util::pick_addr(addrs, local).ok_or(Error::NoDnsEntries)?;
174
175		if url.scheme() == "http" {
176			// Insecure per-connection bootstrap: only honored when no stronger
177			// verification is configured, so an attacker controlling the plaintext
178			// fetch can't weaken an explicit pin or re-enable disabled verification.
179			if self.http_bootstrap {
180				// Perform a HTTP request to fetch the certificate fingerprint.
181				let mut fingerprint = url.clone();
182				fingerprint.set_path("/certificate.sha256");
183				fingerprint.set_query(None);
184				fingerprint.set_fragment(None);
185
186				tracing::warn!(url = %fingerprint, "performing insecure HTTP request for certificate");
187
188				let resp = reqwest::get(fingerprint.as_str())
189					.await
190					.map_err(Error::FetchFingerprint)?
191					.error_for_status()
192					.map_err(Error::FingerprintStatus)?;
193
194				let fingerprint = resp.text().await.map_err(Error::ReadFingerprint)?;
195				let fingerprint = hex::decode(fingerprint.trim())?;
196
197				let verifier = FingerprintVerifier::new(config.crypto_provider().clone(), vec![fingerprint]);
198				config.dangerous().set_certificate_verifier(Arc::new(verifier));
199			} else {
200				tracing::warn!(
201					"ignoring insecure http:// fingerprint bootstrap; using the configured TLS verification"
202				);
203			}
204
205			url.set_scheme("https").expect("failed to set scheme");
206		}
207
208		let alpns: Vec<Vec<u8>> = match url.scheme() {
209			"https" => vec![web_transport_quinn::ALPN.as_bytes().to_vec()],
210			"moqt" | "moql" => self
211				.versions
212				.alpns()
213				.iter()
214				.map(|alpn| alpn.as_bytes().to_vec())
215				.collect(),
216			_ => return Err(Error::InvalidScheme),
217		};
218
219		config.alpn_protocols = alpns;
220		config.key_log = Arc::new(rustls::KeyLogFile::new());
221
222		let config: quinn::crypto::rustls::QuicClientConfig = config.try_into()?;
223		let mut config = quinn::ClientConfig::new(Arc::new(config));
224		config.transport_config(self.transport.clone());
225
226		tracing::debug!(%url, %ip, "connecting");
227
228		let connection = self.quic.connect_with(config, ip, &host)?.await?;
229		tracing::Span::current().record("id", connection.stable_id());
230
231		let mut request = web_transport_quinn::proto::ConnectRequest::new(url.clone());
232		for alpn in self.versions.alpns() {
233			request = request.with_protocol(alpn.to_string());
234		}
235
236		let session = match url.scheme() {
237			"https" => web_transport_quinn::Session::connect(connection, request)
238				.await
239				.map_err(map_client_error)?,
240			"moqt" | "moql" => {
241				let handshake = connection
242					.handshake_data()
243					.ok_or(Error::MissingHandshake)?
244					.downcast::<quinn::crypto::rustls::HandshakeData>()
245					.unwrap();
246
247				let alpn = handshake.protocol.ok_or(Error::MissingAlpn)?;
248				let alpn = String::from_utf8(alpn)?;
249
250				let response = web_transport_quinn::proto::ConnectResponse::OK.with_protocol(alpn);
251				web_transport_quinn::Session::raw(connection, request, response)
252			}
253			_ => return Err(Error::UnsupportedScheme(url.scheme().to_string())),
254		};
255
256		Ok(session)
257	}
258}
259
260impl Error {
261	pub(crate) fn connect_error(&self) -> Option<crate::ConnectError> {
262		match self {
263			Self::ConnectRejected(err) => Some(*err),
264			Self::Client(err) => classify_client_error(err),
265			_ => None,
266		}
267	}
268}
269
270fn map_client_error(err: web_transport_quinn::ClientError) -> Error {
271	if let Some(err) = classify_client_error(&err) {
272		return err.into();
273	}
274
275	err.into()
276}
277
278fn classify_client_error(err: &web_transport_quinn::ClientError) -> Option<crate::ConnectError> {
279	match err {
280		web_transport_quinn::ClientError::HttpError(err) => classify_connect_error(err),
281		_ => None,
282	}
283}
284
285fn classify_connect_error(err: &web_transport_quinn::ConnectError) -> Option<crate::ConnectError> {
286	match err {
287		web_transport_quinn::ConnectError::ErrorStatus(status) => crate::ConnectError::from_status_u16(status.as_u16()),
288		web_transport_quinn::ConnectError::ProtoError(err) => classify_proto_error(err),
289		_ => None,
290	}
291}
292
293fn classify_proto_error(err: &web_transport_quinn::proto::ConnectError) -> Option<crate::ConnectError> {
294	match err {
295		web_transport_quinn::proto::ConnectError::ErrorStatus(status)
296		| web_transport_quinn::proto::ConnectError::WrongStatus(Some(status)) => {
297			crate::ConnectError::from_status_u16(status.as_u16())
298		}
299		_ => None,
300	}
301}
302
303// ── Server ──────────────────────────────────────────────────────────
304
305pub(crate) struct QuinnServer {
306	pub quic: quinn::Endpoint,
307	pub certs: Arc<ServeCerts>,
308}
309
310impl QuinnServer {
311	pub fn new(config: ServerConfig) -> Result<Self> {
312		// Enable BBR congestion control
313		// TODO Validate the BBR implementation before enabling it
314		let mut transport = quinn::TransportConfig::default();
315		transport.max_idle_timeout(Some(Duration::from_secs(30).try_into().unwrap()));
316		transport.keep_alive_interval(Some(Duration::from_secs(5)));
317		transport.mtu_discovery_config(None); // Disable MTU discovery
318
319		let max_streams = config.max_streams.unwrap_or(crate::DEFAULT_MAX_STREAMS);
320		let max_streams = quinn::VarInt::from_u64(max_streams).unwrap_or(quinn::VarInt::MAX);
321		transport.max_concurrent_bidi_streams(max_streams);
322		transport.max_concurrent_uni_streams(max_streams);
323
324		let transport = Arc::new(transport);
325
326		let provider = crate::crypto::provider();
327
328		let certs = ServeCerts::new(provider.clone());
329		certs.load_certs(&config.tls)?;
330		let certs = Arc::new(certs);
331
332		let tls_builder = rustls::ServerConfig::builder_with_provider(provider.clone())
333			.with_protocol_versions(&[&rustls::version::TLS13])
334			.map_err(crate::tls::Error::from)?;
335
336		let mut tls = if config.tls.root.is_empty() {
337			tls_builder.with_no_client_auth().with_cert_resolver(certs.clone())
338		} else {
339			let roots = config.tls.load_roots()?;
340			let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider)
341				.allow_unauthenticated()
342				.build()
343				.map_err(Error::ClientVerifier)?;
344			tls_builder
345				.with_client_cert_verifier(verifier)
346				.with_cert_resolver(certs.clone())
347		};
348
349		// H3 is last because it requires WebTransport framing which not all H3 endpoints support.
350		let mut alpns: Vec<Vec<u8>> = config
351			.versions()
352			.alpns()
353			.iter()
354			.map(|alpn| alpn.as_bytes().to_vec())
355			.collect();
356		alpns.push(web_transport_quinn::ALPN.as_bytes().to_vec());
357
358		tls.alpn_protocols = alpns;
359		tls.key_log = Arc::new(rustls::KeyLogFile::new());
360
361		let tls: quinn::crypto::rustls::QuicServerConfig = tls.try_into()?;
362		let mut tls = quinn::ServerConfig::with_crypto(Arc::new(tls));
363		tls.transport_config(transport);
364
365		// Advertise the preferred_address transport parameter (RFC 9000 §9.6).
366		// Quinn allocates a fresh CID + reset token for the address during the handshake.
367		if let Some(addr) = config.preferred_v4 {
368			tls.preferred_address_v4(Some(addr));
369		}
370		if let Some(addr) = config.preferred_v6 {
371			tls.preferred_address_v6(Some(addr));
372		}
373
374		// There's a bit more boilerplate to make a generic endpoint.
375		let runtime = quinn::default_runtime().ok_or(Error::NoRuntime)?;
376
377		let listen =
378			crate::util::resolve(config.bind.as_deref(), crate::server::DEFAULT_BIND).map_err(Error::ResolveBind)?;
379
380		// Configure connection ID generator with server ID if provided
381		let mut endpoint_config = quinn::EndpointConfig::default();
382		if let Some(server_id) = config.quic_lb_id {
383			let nonce_len = config.quic_lb_nonce.unwrap_or(8);
384			if nonce_len < 4 {
385				return Err(Error::QuicLbNonceTooSmall);
386			}
387
388			let cid_len = 1 + server_id.len() + nonce_len;
389			if cid_len > 20 {
390				return Err(Error::QuicLbCidTooLong(cid_len));
391			}
392
393			tracing::info!(
394				?server_id,
395				nonce_len,
396				"using QUIC-LB compatible connection ID generation"
397			);
398			endpoint_config.cid_generator(move || Box::new(ServerIdGenerator::new(server_id.clone(), nonce_len)));
399		}
400
401		let socket = crate::bind::udp(listen).map_err(Error::BindSocket)?;
402
403		// Create the generic QUIC endpoint.
404		let quic = quinn::Endpoint::new(endpoint_config, Some(tls), socket, runtime).map_err(Error::CreateEndpoint)?;
405
406		// Spawn the cert reload watcher only after endpoint creation succeeds,
407		// so we don't leave a dangling watcher on failure.
408		tokio::spawn(crate::tls::reload_certs(certs.clone(), config.tls.clone()));
409
410		Ok(Self { quic, certs })
411	}
412
413	pub fn accept(&self) -> impl std::future::Future<Output = Option<quinn::Incoming>> + '_ {
414		self.quic.accept()
415	}
416
417	pub fn tls_info(&self) -> Arc<RwLock<crate::tls::Info>> {
418		self.certs.info.clone()
419	}
420
421	pub fn local_addr(&self) -> Result<net::SocketAddr> {
422		self.quic.local_addr().map_err(Error::LocalAddr)
423	}
424
425	pub fn close(&self) {
426		self.quic.close(quinn::VarInt::from_u32(0), b"server shutdown");
427	}
428}
429
430// ── QuinnRequest ────────────────────────────────────────────────────
431
432/// A raw QUIC connection request without WebTransport framing (quinn backend).
433pub(crate) enum QuinnRequest {
434	Raw {
435		request: web_transport_quinn::proto::ConnectRequest,
436		response: web_transport_quinn::proto::ConnectResponse,
437		connection: quinn::Connection,
438	},
439	WebTransport {
440		request: web_transport_quinn::Request,
441		alpns: Vec<&'static str>,
442	},
443}
444
445impl QuinnRequest {
446	pub async fn accept(conn: quinn::Incoming, alpns: Vec<&'static str>) -> Result<Self> {
447		let mut conn = conn.accept()?;
448
449		let handshake = conn
450			.handshake_data()
451			.await?
452			.downcast::<quinn::crypto::rustls::HandshakeData>()
453			.unwrap();
454
455		let alpn = handshake.protocol.ok_or(Error::MissingAlpn)?;
456		let alpn = String::from_utf8(alpn)?;
457		let host = handshake.server_name.unwrap_or_default();
458
459		tracing::debug!(%host, ip = %conn.remote_address(), %alpn, "accepting");
460
461		// Wait for the QUIC connection to be established.
462		let conn = conn.await.map_err(Error::Establish)?;
463
464		let span = tracing::Span::current();
465		span.record("id", conn.stable_id()); // TODO can we get this earlier?
466		tracing::debug!(%host, ip = %conn.remote_address(), %alpn, "accepted");
467
468		match alpn.as_str() {
469			web_transport_quinn::ALPN => {
470				// Wait for the CONNECT request.
471				let request = web_transport_quinn::Request::accept(conn)
472					.await
473					.map_err(Error::RecvRequest)?;
474				Ok(Self::WebTransport { request, alpns })
475			}
476			alpn if moq_net::ALPNS.contains(&alpn) => {
477				if host.is_empty() {
478					return Err(Error::MissingServerName);
479				}
480				let host_str = if host.contains(':') {
481					format!("[{}]", host)
482				} else {
483					host.clone()
484				};
485				let url = format!("moqt://{}", host_str).parse::<Url>().map_err(Error::BuildUrl)?;
486				let request = web_transport_quinn::proto::ConnectRequest::new(url);
487				let response = web_transport_quinn::proto::ConnectResponse::OK.with_protocol(alpn);
488				Ok(Self::Raw {
489					connection: conn,
490					request,
491					response,
492				})
493			}
494			_ => Err(Error::UnsupportedAlpn(alpn)),
495		}
496	}
497
498	/// Accept the session, returning a 200 OK if using WebTransport.
499	pub async fn ok(self) -> std::result::Result<web_transport_quinn::Session, web_transport_quinn::ServerError> {
500		match self {
501			QuinnRequest::Raw {
502				connection,
503				request,
504				response,
505			} => Ok(web_transport_quinn::Session::raw(connection, request, response)),
506			QuinnRequest::WebTransport { request, alpns } => {
507				let mut response = web_transport_quinn::proto::ConnectResponse::OK;
508				// Pick the first sub-protocol that we actually support.
509				// This is the WebTransport equivalent of ALPN negotiation.
510				// If no match is found, we default to no sub-protocol to support older
511				// clients that don't use ALPN. We assume moq-transport-14/moq-lite-02
512				// and perform the SETUP_x exchange instead.
513				if let Some(protocol) = request.protocols.iter().find(|p| alpns.contains(&p.as_str())) {
514					response = response.with_protocol(protocol);
515				}
516				request.respond(response).await
517			}
518		}
519	}
520
521	/// Returns the URL provided by the client.
522	pub fn url(&self) -> Option<&Url> {
523		match self {
524			QuinnRequest::Raw { .. } => None,
525			QuinnRequest::WebTransport { request, .. } => Some(&request.url),
526		}
527	}
528
529	/// The client certificate chain the peer presented, if any, validated by
530	/// rustls against the configured `tls.root` during the handshake.
531	pub fn peer_identity(&self) -> Option<crate::tls::PeerIdentity> {
532		let conn = match self {
533			QuinnRequest::Raw { connection, .. } => connection,
534			QuinnRequest::WebTransport { request, .. } => request.conn(),
535		};
536		crate::tls::PeerIdentity::from_any(conn.peer_identity())
537	}
538
539	/// Reject the session with a status code.
540	pub async fn close(
541		self,
542		status: web_transport_quinn::http::StatusCode,
543	) -> std::result::Result<(), web_transport_quinn::ServerError> {
544		match self {
545			QuinnRequest::Raw { connection, .. } => {
546				connection.close(status.as_u16().into(), status.as_str().as_bytes());
547				Ok(())
548			}
549			QuinnRequest::WebTransport { request, alpns: _, .. } => request.reject(status).await,
550		}
551	}
552}
553
554// ── ServerIdGenerator ───────────────────────────────────────────────
555
556struct ServerIdGenerator {
557	server_id: ServerId,
558	nonce_len: usize,
559}
560
561impl ServerIdGenerator {
562	fn new(server_id: ServerId, nonce_len: usize) -> Self {
563		Self { server_id, nonce_len }
564	}
565}
566
567impl quinn::ConnectionIdGenerator for ServerIdGenerator {
568	fn generate_cid(&mut self) -> quinn::ConnectionId {
569		use rand::RngExt;
570		let cid_len = self.cid_len();
571		let mut cid = Vec::with_capacity(cid_len);
572		// First byte has "self-encoded length" of server ID + nonce
573		cid.push((cid_len - 1) as u8);
574		cid.extend(self.server_id.0.iter());
575		cid.extend(rand::rng().random_iter::<u8>().take(self.nonce_len));
576		quinn::ConnectionId::new(cid.as_slice())
577	}
578
579	fn cid_len(&self) -> usize {
580		1 + self.server_id.len() + self.nonce_len
581	}
582
583	fn cid_lifetime(&self) -> Option<Duration> {
584		None
585	}
586}