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