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	/// Optional TLS SNI / verification hostname override (from config).
123	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		// TODO Validate the BBR implementation before enabling it
131		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); // Disable MTU discovery
135
136		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		// There's a bit more boilerplate to make a generic endpoint.
144		let runtime = quinn::default_runtime().ok_or(Error::NoRuntime)?;
145		let endpoint_config = quinn::EndpointConfig::default();
146
147		// Create the generic QUIC endpoint.
148		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		// Look up the DNS entry.
171		// Quinn doesn't support happy eyeballs, so we pick a single address,
172		// preferring one whose family matches the local socket so the OS
173		// doesn't reject it (notably on Windows, where IPv6 sockets aren't
174		// dual-stack by default).
175		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			// Insecure per-connection bootstrap: only honored when no stronger
183			// verification is configured, so an attacker controlling the plaintext
184			// fetch can't weaken an explicit pin or re-enable disabled verification.
185			if self.http_bootstrap {
186				// Perform a HTTP request to fetch the certificate fingerprint.
187				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		// Use the configured host_name override for SNI + cert verification, else the URL host.
230		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
307// ── Server ──────────────────────────────────────────────────────────
308
309pub(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		// Enable BBR congestion control
317		// TODO Validate the BBR implementation before enabling it
318		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); // Disable MTU discovery
322
323		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		// H3 is last because it requires WebTransport framing which not all H3 endpoints support.
354		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		// Advertise the preferred_address transport parameter (RFC 9000 §9.6).
370		// Quinn allocates a fresh CID + reset token for the address during the handshake.
371		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		// There's a bit more boilerplate to make a generic endpoint.
379		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		// Configure connection ID generator with server ID if provided
385		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		// Create the generic QUIC endpoint.
408		let quic = quinn::Endpoint::new(endpoint_config, Some(tls), socket, runtime).map_err(Error::CreateEndpoint)?;
409
410		// Spawn the cert reload watcher only after endpoint creation succeeds,
411		// so we don't leave a dangling watcher on failure.
412		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
434// ── QuinnRequest ────────────────────────────────────────────────────
435
436/// A raw QUIC connection request without WebTransport framing (quinn backend).
437pub(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		// Wait for the QUIC connection to be established.
466		let conn = conn.await.map_err(Error::Establish)?;
467
468		let span = tracing::Span::current();
469		span.record("id", conn.stable_id()); // TODO can we get this earlier?
470		tracing::debug!(%host, ip = %conn.remote_address(), %alpn, "accepted");
471
472		match alpn.as_str() {
473			web_transport_quinn::ALPN => {
474				// Wait for the CONNECT request.
475				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	/// Accept the session, returning a 200 OK if using WebTransport.
503	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				// Pick the first sub-protocol that we actually support.
513				// This is the WebTransport equivalent of ALPN negotiation.
514				// If no match is found, we default to no sub-protocol to support older
515				// clients that don't use ALPN. We assume moq-transport-14/moq-lite-02
516				// and perform the SETUP_x exchange instead.
517				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	/// Returns the URL provided by the client.
526	pub fn url(&self) -> Option<&Url> {
527		match self {
528			QuinnRequest::Raw { .. } => None,
529			QuinnRequest::WebTransport { request, .. } => Some(&request.url),
530		}
531	}
532
533	/// The client certificate chain the peer presented, if any, validated by
534	/// rustls against the configured `tls.root` during the handshake.
535	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	/// Reject the session with a status code.
544	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
558// ── ServerIdGenerator ───────────────────────────────────────────────
559
560struct 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		// First byte has "self-encoded length" of server ID + nonce
577		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}