Skip to main content

moq_native/
quinn.rs

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