Skip to main content

moq_native/
quinn.rs

1//! The quinn QUIC backend, used for both WebTransport (`https://`) and raw QUIC (`moqt://`, `moql://`).
2
3use crate::client::ClientConfig;
4use crate::quic::CongestionControl;
5use crate::quic::Resolved;
6use crate::quic::ServerId;
7use crate::server::ServerConfig;
8use crate::tls::{FingerprintVerifier, ServeCerts};
9use std::net;
10use std::sync::Arc;
11use std::time::Duration;
12use url::Url;
13
14pub use web_transport_quinn;
15
16/// Attach a qlog stream writing into `dir`, if one was configured.
17///
18/// quinn's [`quinn::QlogStream`] is a shared handle: every connection on the endpoint
19/// writes into it, tagged with its own qlog `group_id`. So this is one file per
20/// endpoint rather than per connection, unlike the noq and quiche backends.
21fn apply_qlog(transport: &mut quinn::TransportConfig, quic: &Resolved, role: &str) -> Result<()> {
22	// `Client::validate` already rejected a directory this build can't honor, so the
23	// block below is only reached where capture actually works.
24	let Some(dir) = quic.qlog_dir() else {
25		return Ok(());
26	};
27
28	#[cfg(feature = "qlog")]
29	{
30		// The pid keeps concurrent processes sharing a directory from clobbering each other.
31		let path = dir.join(format!("moq-{role}-{}.sqlog", std::process::id()));
32		let file = std::fs::File::create(&path).map_err(Error::CreateQlog)?;
33
34		// Deliberately unbuffered: qlog's streamer only flushes on `finish_log`, which
35		// quinn never calls per event, so a BufWriter would hold every trace in memory
36		// until the endpoint drops and lose the lot if the process is killed. Killing a
37		// stuck process is exactly when these traces are worth having.
38		let mut config = quinn::QlogConfig::default();
39		config.writer(Box::new(file)).title(Some(format!("moq-native {role}")));
40
41		transport.qlog_stream(config.into_stream());
42		tracing::info!(path = %path.display(), "writing qlog");
43	}
44
45	#[cfg(not(feature = "qlog"))]
46	let _ = (transport, dir, role);
47
48	Ok(())
49}
50
51/// Apply the resolved quic knobs to a quinn transport config.
52fn apply_transport(transport: &mut quinn::TransportConfig, quic: &Resolved) {
53	transport.max_idle_timeout(Some(quic.idle_timeout.try_into().expect("idle timeout out of range")));
54	transport.keep_alive_interval(quic.keep_alive);
55
56	// quinn enables MTU discovery by default; disable it unless asked.
57	if !quic.mtu_discovery {
58		transport.mtu_discovery_config(None);
59	}
60
61	let max_streams = quinn::VarInt::from_u64(quic.max_streams).unwrap_or(quinn::VarInt::MAX);
62	transport.max_concurrent_bidi_streams(max_streams);
63	transport.max_concurrent_uni_streams(max_streams);
64
65	// GSO is on by default; only the quinn/noq backends can turn it off.
66	if let Some(gso) = quic.gso {
67		transport.enable_segmentation_offload(gso);
68	}
69
70	transport.congestion_controller_factory(congestion_factory(congestion_control(quic)));
71}
72
73/// The congestion control family to install, defaulting to delay-based.
74///
75/// Live media wants a steady send rate an encoder can track, not CUBIC's sawtooth,
76/// so override quinn's own CUBIC default.
77fn congestion_control(quic: &Resolved) -> CongestionControl {
78	quic.congestion_control.unwrap_or(CongestionControl::Delay)
79}
80
81/// The quinn controller factory for a congestion control family. quinn's BBR is v1.
82fn congestion_factory(family: CongestionControl) -> Arc<dyn quinn::congestion::ControllerFactory + Send + Sync> {
83	match family {
84		CongestionControl::Loss => Arc::new(quinn::congestion::CubicConfig::default()),
85		CongestionControl::Delay => Arc::new(quinn::congestion::BbrConfig::default()),
86	}
87}
88
89/// Errors specific to the quinn QUIC backend.
90#[derive(Debug, thiserror::Error)]
91#[non_exhaustive]
92pub enum Error {
93	/// The UDP socket couldn't be bound, usually because the address is already in use.
94	#[error("failed to bind UDP socket")]
95	BindSocket(#[source] std::io::Error),
96
97	/// The bound socket couldn't be turned into a QUIC endpoint.
98	#[error("failed to create QUIC endpoint")]
99	CreateEndpoint(#[source] std::io::Error),
100
101	/// The qlog trace file couldn't be created, usually a missing directory.
102	#[error("failed to create qlog file")]
103	CreateQlog(#[source] std::io::Error),
104
105	/// Quinn found no async runtime. Construct the client or server from within a tokio context.
106	#[error("no async runtime")]
107	NoRuntime,
108
109	/// The endpoint's local address couldn't be read back from the OS.
110	#[error("failed to get local address")]
111	LocalAddr(#[source] std::io::Error),
112
113	/// The server's configured bind address couldn't be resolved.
114	#[error("failed to resolve bind address")]
115	ResolveBind(#[source] std::io::Error),
116
117	/// The URL has no host to connect to.
118	#[error("invalid DNS name")]
119	InvalidDnsName,
120
121	/// Resolving the URL's host failed.
122	#[error("failed DNS lookup")]
123	DnsLookup(#[source] std::io::Error),
124
125	/// DNS resolved the host to no addresses at all.
126	#[error("no DNS entries")]
127	NoDnsEntries,
128
129	/// The insecure `http://` bootstrap couldn't fetch `/certificate.sha256`.
130	#[error("failed to fetch fingerprint")]
131	FetchFingerprint(#[source] reqwest::Error),
132
133	/// The `/certificate.sha256` fetch returned a non-success status.
134	#[error("fingerprint request failed")]
135	FingerprintStatus(#[source] reqwest::Error),
136
137	/// The fingerprint response body couldn't be read.
138	#[error("failed to read fingerprint")]
139	ReadFingerprint(#[source] reqwest::Error),
140
141	/// The fetched fingerprint wasn't valid hex.
142	#[error("invalid fingerprint")]
143	InvalidFingerprint(#[from] hex::FromHexError),
144
145	/// The URL scheme isn't one this backend can dial.
146	#[error("url scheme must be 'https', 'moqt', or 'moql'")]
147	InvalidScheme,
148
149	/// The URL scheme passed the initial check but has no session type, which means it slipped through a scheme list.
150	#[error("unsupported URL scheme: {0}")]
151	UnsupportedScheme(String),
152
153	/// The connection came up without TLS handshake data, so the negotiated ALPN can't be read.
154	#[error("missing handshake data")]
155	MissingHandshake,
156
157	/// TLS negotiated no ALPN, so there's no protocol to speak.
158	#[error("missing ALPN")]
159	MissingAlpn,
160
161	/// The negotiated ALPN wasn't valid UTF-8.
162	#[error("failed to decode ALPN")]
163	DecodeAlpn(#[from] std::string::FromUtf8Error),
164
165	/// The peer negotiated an ALPN this endpoint doesn't handle.
166	#[error("unsupported ALPN: {0}")]
167	UnsupportedAlpn(String),
168
169	/// A raw QUIC client connected without SNI, so the server can't tell which host it wanted.
170	#[error("missing server name for raw QUIC connection")]
171	MissingServerName,
172
173	/// The client's SNI hostname didn't form a valid URL.
174	#[error("failed to construct URL from server name")]
175	BuildUrl(#[source] url::ParseError),
176
177	/// The configured QUIC-LB nonce is too short to be unguessable.
178	#[error("quic_lb_nonce must be at least 4")]
179	QuicLbNonceTooSmall,
180
181	/// The QUIC-LB server ID plus nonce doesn't fit in a connection ID. Shorten one of them.
182	#[error("connection ID length ({0}) exceeds maximum of 20")]
183	QuicLbCidTooLong(usize),
184
185	/// The mTLS client verifier couldn't be built from the configured roots.
186	#[error("failed to build client certificate verifier")]
187	ClientVerifier(#[source] rustls::server::VerifierBuilderError),
188
189	/// The rustls crypto provider offers no cipher suite usable for QUIC initial packets.
190	#[error(transparent)]
191	NoInitialCipherSuite(#[from] quinn::crypto::rustls::NoInitialCipherSuite),
192
193	/// Quinn refused to start the connection, before any packet was sent.
194	#[error(transparent)]
195	Connect(#[from] quinn::ConnectError),
196
197	/// The QUIC connection failed or was closed by the peer.
198	#[error(transparent)]
199	Connection(#[from] quinn::ConnectionError),
200
201	/// The WebTransport client handshake failed.
202	#[error(transparent)]
203	Client(#[from] web_transport_quinn::ClientError),
204
205	/// The server answered the WebTransport CONNECT with a rejection status.
206	#[error(transparent)]
207	ConnectRejected(#[from] crate::ConnectError),
208
209	/// The WebTransport server handshake failed while responding.
210	#[error(transparent)]
211	Server(#[from] web_transport_quinn::ServerError),
212
213	/// The QUIC handshake didn't complete for an incoming connection.
214	#[error("failed to establish QUIC connection")]
215	Establish(#[source] quinn::ConnectionError),
216
217	/// The client never sent a usable WebTransport CONNECT request.
218	#[error("failed to receive WebTransport request")]
219	RecvRequest(#[source] web_transport_quinn::ServerError),
220
221	/// The TLS configuration or certificates couldn't be loaded.
222	#[error(transparent)]
223	Tls(#[from] crate::tls::Error),
224
225	/// Two or more addresses were raced and every attempt failed, each paired
226	/// with its own error in dial order. All of them are kept: picking one to
227	/// report would bury a rejected certificate or a refused port behind
228	/// whichever address happened to be unroutable or to blackhole until its
229	/// timeout. A host with a single address reports that error directly instead.
230	#[error("all {} connection attempts failed: {}", .0.len(), crate::failover::describe(.0))]
231	Failover(Vec<crate::failover::Failure<Error>>),
232}
233
234impl crate::failover::Aggregate for Error {
235	fn aggregate(failures: Vec<crate::failover::Failure<Self>>) -> Self {
236		Self::Failover(failures)
237	}
238
239	fn resolve(error: Option<std::io::Error>) -> Self {
240		match error {
241			Some(error) => Self::DnsLookup(error),
242			None => Self::NoDnsEntries,
243		}
244	}
245}
246
247type Result<T> = std::result::Result<T, Error>;
248
249// ── Client ──────────────────────────────────────────────────────────
250
251#[derive(Clone)]
252pub(crate) struct QuinnClient {
253	pub quic: quinn::Endpoint,
254	pub transport: Arc<quinn::TransportConfig>,
255	/// Whether an `http://` URL may bootstrap a pin (see [crate::tls::Client::allows_http_bootstrap]).
256	pub http_bootstrap: bool,
257	/// Optional TLS SNI / verification hostname override (from config).
258	pub host_name: Option<String>,
259	/// Stagger between Happy Eyeballs connection attempts (see [`crate::failover`]).
260	pub failover_delay: Duration,
261	/// How long the first candidate waits for the full DNS answer, RFC 8305's
262	/// Resolution Delay (see [`crate::ClientConfig::resolution_delay`]).
263	pub resolution_delay: Duration,
264	/// Whether the bound socket really came back dual-stack, which decides
265	/// whether an IPv4 destination is reachable at all. Captured here because the
266	/// endpoint owns the socket from here on and `local_addr` can't tell us.
267	dual_stack: bool,
268}
269
270impl QuinnClient {
271	pub fn new(config: &ClientConfig) -> Result<Self> {
272		let socket = crate::bind::udp(config.bind).map_err(Error::BindSocket)?;
273		let dual_stack = crate::bind::udp_is_dual_stack(&socket);
274
275		let quic = config.quic.resolve();
276		let mut transport = quinn::TransportConfig::default();
277		apply_transport(&mut transport, &quic);
278		apply_qlog(&mut transport, &quic, "client")?;
279		let transport = Arc::new(transport);
280
281		// There's a bit more boilerplate to make a generic endpoint.
282		let runtime = quinn::default_runtime().ok_or(Error::NoRuntime)?;
283		let endpoint_config = quinn::EndpointConfig::default();
284
285		// Create the generic QUIC endpoint.
286		let quic = quinn::Endpoint::new(endpoint_config, None, socket, runtime).map_err(Error::CreateEndpoint)?;
287
288		Ok(Self {
289			quic,
290			transport,
291			http_bootstrap: config.tls.allows_http_bootstrap(),
292			host_name: config.tls.host_name.clone(),
293			failover_delay: config.resolved_failover_delay(),
294			resolution_delay: config.resolved_resolution_delay(),
295			dual_stack,
296		})
297	}
298
299	pub async fn connect(
300		&self,
301		tls: &rustls::ClientConfig,
302		url: Url,
303		versions: &moq_net::Versions,
304	) -> Result<web_transport_quinn::Session> {
305		let mut url = url;
306		let mut config = tls.clone();
307
308		let target = url.host().ok_or(Error::InvalidDnsName)?;
309		let host = target.to_string();
310		let port = url.port().unwrap_or(443);
311
312		// Resolve, adapted to the local socket's family; the dial below races the
313		// answers Happy Eyeballs style as they land, so neither a broken family nor a
314		// lookup still waiting on its AAAA record can stall the connect.
315		let local = self.quic.local_addr().map_err(Error::LocalAddr)?;
316		let candidates =
317			crate::resolve::Candidates::resolve(target, port, self.resolution_delay).with_local(local, self.dual_stack);
318
319		if url.scheme() == "http" {
320			// Insecure per-connection bootstrap: only honored when no stronger
321			// verification is configured, so an attacker controlling the plaintext
322			// fetch can't weaken an explicit pin or re-enable disabled verification.
323			if self.http_bootstrap {
324				// Perform a HTTP request to fetch the certificate fingerprint.
325				let mut fingerprint = url.clone();
326				fingerprint.set_path("/certificate.sha256");
327				fingerprint.set_query(None);
328				fingerprint.set_fragment(None);
329
330				tracing::warn!(url = %fingerprint, "performing insecure HTTP request for certificate");
331
332				let resp = reqwest::get(fingerprint.as_str())
333					.await
334					.map_err(Error::FetchFingerprint)?
335					.error_for_status()
336					.map_err(Error::FingerprintStatus)?;
337
338				let fingerprint = resp.text().await.map_err(Error::ReadFingerprint)?;
339				let fingerprint = hex::decode(fingerprint.trim())?;
340
341				let verifier = FingerprintVerifier::new(config.crypto_provider().clone(), vec![fingerprint]);
342				config.dangerous().set_certificate_verifier(Arc::new(verifier));
343			} else {
344				tracing::warn!(
345					"ignoring insecure http:// fingerprint bootstrap; using the configured TLS verification"
346				);
347			}
348
349			url.set_scheme("https").expect("failed to set scheme");
350		}
351
352		let alpns: Vec<Vec<u8>> = match url.scheme() {
353			"https" => vec![web_transport_quinn::ALPN.as_bytes().to_vec()],
354			"moqt" | "moql" => versions.alpns().iter().map(|alpn| alpn.as_bytes().to_vec()).collect(),
355			_ => return Err(Error::InvalidScheme),
356		};
357
358		config.alpn_protocols = alpns;
359		config.key_log = Arc::new(rustls::KeyLogFile::new());
360
361		let config: quinn::crypto::rustls::QuicClientConfig = config.try_into()?;
362		let mut config = quinn::ClientConfig::new(Arc::new(config));
363		config.transport_config(self.transport.clone());
364
365		tracing::debug!(%url, "connecting");
366
367		// Use the configured host_name override for SNI + cert verification, else the URL host.
368		let host_name = self.host_name.clone().unwrap_or(host);
369
370		// Race only the QUIC handshake: the winner alone performs the WebTransport
371		// CONNECT below, so the server sees a single request no matter how many
372		// addresses were dialed.
373		let connection = crate::failover::race(candidates, self.failover_delay, |addr| {
374			let endpoint = self.quic.clone();
375			let config = config.clone();
376			let host_name = host_name.clone();
377			async move { Ok::<_, Error>(endpoint.connect_with(config, addr, &host_name)?.await?) }
378		})
379		.await?;
380		tracing::Span::current().record("id", connection.stable_id());
381
382		let mut request = web_transport_quinn::proto::ConnectRequest::new(url.clone());
383		for alpn in versions.alpns() {
384			request = request.with_protocol(alpn.to_string());
385		}
386
387		let session = match url.scheme() {
388			"https" => web_transport_quinn::Session::connect(connection, request)
389				.await
390				.map_err(map_client_error)?,
391			"moqt" | "moql" => {
392				let handshake = connection
393					.handshake_data()
394					.ok_or(Error::MissingHandshake)?
395					.downcast::<quinn::crypto::rustls::HandshakeData>()
396					.unwrap();
397
398				let alpn = handshake.protocol.ok_or(Error::MissingAlpn)?;
399				let alpn = String::from_utf8(alpn)?;
400
401				let response = web_transport_quinn::proto::ConnectResponse::OK.with_protocol(alpn);
402				web_transport_quinn::Session::raw(connection, request, response)
403			}
404			_ => return Err(Error::UnsupportedScheme(url.scheme().to_string())),
405		};
406
407		Ok(session)
408	}
409}
410
411impl Error {
412	pub(crate) fn connect_error(&self) -> Option<crate::ConnectError> {
413		match self {
414			Self::ConnectRejected(err) => Some(*err),
415			Self::Client(err) => classify_client_error(err),
416			Self::Failover(failures) => failures.iter().find_map(|failure| failure.error.connect_error()),
417			_ => None,
418		}
419	}
420
421	/// The HTTP status a server answered with, if it answered with one at all.
422	///
423	/// Two places see a real status: the insecure `http://` fingerprint bootstrap, and the
424	/// WebTransport CONNECT response. See [`crate::Error::status`].
425	pub(crate) fn status(&self) -> Option<u16> {
426		match self {
427			Self::FetchFingerprint(err) | Self::FingerprintStatus(err) | Self::ReadFingerprint(err) => {
428				err.status().map(|status| status.as_u16())
429			}
430			Self::Client(err) => client_status(err),
431			// Every raced address has to have answered, and answered with something not worth
432			// repeating, before the set counts as settled: one address refusing says nothing about
433			// the others, which may simply have been unroutable.
434			Self::Failover(failures) => {
435				let mut settled = None;
436				for failure in failures {
437					match failure.error.status() {
438						Some(status) if !crate::error::status_retryable(status) => settled = Some(status),
439						_ => return None,
440					}
441				}
442				settled
443			}
444			_ => None,
445		}
446	}
447}
448
449fn map_client_error(err: web_transport_quinn::ClientError) -> Error {
450	if let Some(err) = classify_client_error(&err) {
451		return err.into();
452	}
453
454	err.into()
455}
456
457fn classify_client_error(err: &web_transport_quinn::ClientError) -> Option<crate::ConnectError> {
458	client_status(err).and_then(crate::ConnectError::from_status_u16)
459}
460
461/// The HTTP status the server answered the WebTransport CONNECT with, when it answered with one at
462/// all (as opposed to the connection failing underneath the request).
463///
464/// Both classifications read this: [`classify_client_error`] turns an auth status into a
465/// [`crate::ConnectError`], and [`Error::status`] hands it to the caller, whose backoff consults
466/// the status. A `404` or `405` is the server's settled answer, so retrying
467/// it just burns the reconnect budget on a URL that will never work.
468fn client_status(err: &web_transport_quinn::ClientError) -> Option<u16> {
469	match err {
470		web_transport_quinn::ClientError::HttpError(err) => connect_status(err),
471		_ => None,
472	}
473}
474
475fn connect_status(err: &web_transport_quinn::ConnectError) -> Option<u16> {
476	match err {
477		web_transport_quinn::ConnectError::ErrorStatus(status) => Some(status.as_u16()),
478		web_transport_quinn::ConnectError::ProtoError(err) => proto_status(err),
479		_ => None,
480	}
481}
482
483fn proto_status(err: &web_transport_quinn::proto::ConnectError) -> Option<u16> {
484	match err {
485		web_transport_quinn::proto::ConnectError::ErrorStatus(status)
486		| web_transport_quinn::proto::ConnectError::WrongStatus(Some(status)) => Some(status.as_u16()),
487		_ => None,
488	}
489}
490
491// ── Server ──────────────────────────────────────────────────────────
492
493pub(crate) struct QuinnServer {
494	pub quic: quinn::Endpoint,
495	pub certs: Arc<ServeCerts>,
496}
497
498impl QuinnServer {
499	pub fn new(config: ServerConfig) -> Result<Self> {
500		let quic = config.quic.resolve();
501		let mut transport = quinn::TransportConfig::default();
502		apply_transport(&mut transport, &quic);
503		apply_qlog(&mut transport, &quic, "server")?;
504		let transport = Arc::new(transport);
505
506		let provider = crate::crypto::provider();
507
508		let certs = ServeCerts::new(provider.clone());
509		certs.load_certs(&config.tls)?;
510		let certs = Arc::new(certs);
511
512		let tls_builder = rustls::ServerConfig::builder_with_provider(provider.clone())
513			.with_protocol_versions(&[&rustls::version::TLS13])
514			.map_err(crate::tls::Error::from)?;
515
516		let mut tls = if config.tls.root.is_empty() {
517			tls_builder.with_no_client_auth().with_cert_resolver(certs.clone())
518		} else {
519			let roots = config.tls.load_roots()?;
520			let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider)
521				.allow_unauthenticated()
522				.build()
523				.map_err(Error::ClientVerifier)?;
524			tls_builder
525				.with_client_cert_verifier(verifier)
526				.with_cert_resolver(certs.clone())
527		};
528
529		// H3 is last because it requires WebTransport framing which not all H3 endpoints support.
530		let mut alpns: Vec<Vec<u8>> = config
531			.versions()
532			.alpns()
533			.iter()
534			.map(|alpn| alpn.as_bytes().to_vec())
535			.collect();
536		alpns.push(web_transport_quinn::ALPN.as_bytes().to_vec());
537
538		tls.alpn_protocols = alpns;
539		tls.key_log = Arc::new(rustls::KeyLogFile::new());
540
541		let tls: quinn::crypto::rustls::QuicServerConfig = tls.try_into()?;
542		let mut tls = quinn::ServerConfig::with_crypto(Arc::new(tls));
543		tls.transport_config(transport);
544
545		// Advertise the preferred_address transport parameter (RFC 9000 §9.6).
546		// Quinn allocates a fresh CID + reset token for the address during the handshake.
547		if let Some(addr) = config.quic.preferred_v4 {
548			tls.preferred_address_v4(Some(addr));
549		}
550		if let Some(addr) = config.quic.preferred_v6 {
551			tls.preferred_address_v6(Some(addr));
552		}
553
554		// There's a bit more boilerplate to make a generic endpoint.
555		let runtime = quinn::default_runtime().ok_or(Error::NoRuntime)?;
556
557		let listen =
558			crate::util::resolve(config.bind.as_deref(), crate::server::DEFAULT_BIND).map_err(Error::ResolveBind)?;
559
560		// Configure connection ID generator with server ID if provided
561		let mut endpoint_config = quinn::EndpointConfig::default();
562		if let Some(server_id) = config.quic.quic_lb_id {
563			let nonce_len = config.quic.quic_lb_nonce.unwrap_or(8);
564			if nonce_len < 4 {
565				return Err(Error::QuicLbNonceTooSmall);
566			}
567
568			let cid_len = 1 + server_id.len() + nonce_len;
569			if cid_len > 20 {
570				return Err(Error::QuicLbCidTooLong(cid_len));
571			}
572
573			tracing::info!(
574				?server_id,
575				nonce_len,
576				"using QUIC-LB compatible connection ID generation"
577			);
578			endpoint_config.cid_generator(move || Box::new(ServerIdGenerator::new(server_id.clone(), nonce_len)));
579		}
580
581		let socket = crate::bind::udp(listen).map_err(Error::BindSocket)?;
582
583		// Create the generic QUIC endpoint.
584		let quic = quinn::Endpoint::new(endpoint_config, Some(tls), socket, runtime).map_err(Error::CreateEndpoint)?;
585
586		// Spawn the cert reload watcher only after endpoint creation succeeds,
587		// so we don't leave a dangling watcher on failure.
588		tokio::spawn(crate::tls::reload_certs(certs.clone(), config.tls.clone()));
589
590		Ok(Self { quic, certs })
591	}
592
593	pub fn accept(&self) -> impl std::future::Future<Output = Option<quinn::Incoming>> + '_ {
594		self.quic.accept()
595	}
596
597	pub fn certificates(&self) -> crate::tls::Certificates {
598		crate::tls::Certificates::new(self.certs.info.clone())
599	}
600
601	pub fn local_addr(&self) -> Result<net::SocketAddr> {
602		self.quic.local_addr().map_err(Error::LocalAddr)
603	}
604
605	pub fn close(&self) {
606		self.quic.close(quinn::VarInt::from_u32(0), b"server shutdown");
607	}
608}
609
610// ── QuinnRequest ────────────────────────────────────────────────────
611
612/// Accept a QUIC connection, negotiate WebTransport or raw moq, and complete the
613/// handshake (a `200 OK` for WebTransport). Returns the established session plus the
614/// request URL and validated mTLS identity, both captured before the response consumes
615/// the request. Raw QUIC carries no request URL (the path rides the SETUP instead).
616pub(crate) async fn accept(
617	conn: quinn::Incoming,
618	alpns: Vec<&'static str>,
619) -> Result<(
620	web_transport_quinn::Session,
621	Option<Url>,
622	Option<crate::tls::PeerIdentity>,
623)> {
624	let mut conn = conn.accept()?;
625
626	let handshake = conn
627		.handshake_data()
628		.await?
629		.downcast::<quinn::crypto::rustls::HandshakeData>()
630		.unwrap();
631
632	let alpn = handshake.protocol.ok_or(Error::MissingAlpn)?;
633	let alpn = String::from_utf8(alpn)?;
634	let host = handshake.server_name.unwrap_or_default();
635
636	tracing::debug!(%host, ip = %conn.remote_address(), %alpn, "accepting");
637
638	// Wait for the QUIC connection to be established.
639	let conn = conn.await.map_err(Error::Establish)?;
640
641	let span = tracing::Span::current();
642	span.record("id", conn.stable_id()); // TODO can we get this earlier?
643	tracing::debug!(%host, ip = %conn.remote_address(), %alpn, "accepted");
644
645	match alpn.as_str() {
646		web_transport_quinn::ALPN => {
647			// Wait for the CONNECT request, then capture its URL and mTLS identity before
648			// the response consumes it.
649			let request = web_transport_quinn::Request::accept(conn)
650				.await
651				.map_err(Error::RecvRequest)?;
652			let url = Some(request.url.clone());
653			let identity = crate::tls::PeerIdentity::from_any(request.conn().peer_identity());
654
655			let mut response = web_transport_quinn::proto::ConnectResponse::OK;
656			// Pick the first sub-protocol that we actually support.
657			// This is the WebTransport equivalent of ALPN negotiation.
658			// If no match is found, we default to no sub-protocol to support older
659			// clients that don't use ALPN. We assume moq-transport-14/moq-lite-02
660			// and perform the SETUP_x exchange instead.
661			if let Some(protocol) = request.protocols.iter().find(|p| alpns.contains(&p.as_str())) {
662				response = response.with_protocol(protocol);
663			}
664			let session = request.respond(response).await.map_err(Error::Server)?;
665			Ok((session, url, identity))
666		}
667		// Recognize any moq ALPN this server actually offered (its configured versions),
668		// not the global default set. rustls only negotiates an ALPN the server offered, so
669		// this covers opt-in / work-in-progress versions (e.g. moq-lite-06-wip) that are
670		// deliberately absent from `moq_net::ALPNS`.
671		alpn if alpns.contains(&alpn) => {
672			// Raw QUIC carries no in-band request URL like WebTransport's CONNECT, so the TLS
673			// SNI is the only authority the client can offer, and it's optional. A client dialing
674			// a bare IP sends no SNI (RFC 6066 forbids IP literals), leaving `host` empty; the
675			// resulting hostless `moqt://` routes to the root path, exactly like a URL-less stream
676			// transport. `url()` returns `None` for the raw variant either way.
677			let host_str = if host.contains(':') {
678				format!("[{}]", host)
679			} else {
680				host.clone()
681			};
682			let url = format!("moqt://{}", host_str).parse::<Url>().map_err(Error::BuildUrl)?;
683			let request = web_transport_quinn::proto::ConnectRequest::new(url);
684			let response = web_transport_quinn::proto::ConnectResponse::OK.with_protocol(alpn);
685			let identity = crate::tls::PeerIdentity::from_any(conn.peer_identity());
686			// Raw QUIC carries no request URL; the path rides the SETUP.
687			let session = web_transport_quinn::Session::raw(conn, request, response);
688			Ok((session, None, identity))
689		}
690		_ => Err(Error::UnsupportedAlpn(alpn)),
691	}
692}
693
694// ── ServerIdGenerator ───────────────────────────────────────────────
695
696struct ServerIdGenerator {
697	server_id: ServerId,
698	nonce_len: usize,
699}
700
701impl ServerIdGenerator {
702	fn new(server_id: ServerId, nonce_len: usize) -> Self {
703		Self { server_id, nonce_len }
704	}
705}
706
707impl quinn::ConnectionIdGenerator for ServerIdGenerator {
708	fn generate_cid(&mut self) -> quinn::ConnectionId {
709		use rand::RngExt;
710		let cid_len = self.cid_len();
711		let mut cid = Vec::with_capacity(cid_len);
712		// First byte has "self-encoded length" of server ID + nonce
713		cid.push((cid_len - 1) as u8);
714		cid.extend(self.server_id.0.iter());
715		cid.extend(rand::rng().random_iter::<u8>().take(self.nonce_len));
716		quinn::ConnectionId::new(cid.as_slice())
717	}
718
719	fn cid_len(&self) -> usize {
720		1 + self.server_id.len() + self.nonce_len
721	}
722
723	fn cid_lifetime(&self) -> Option<Duration> {
724		None
725	}
726}
727
728#[cfg(test)]
729mod tests {
730	use super::*;
731
732	fn connect_rejected(status: u16) -> Error {
733		Error::Client(web_transport_quinn::ClientError::HttpError(
734			web_transport_quinn::ConnectError::ErrorStatus(
735				web_transport_quinn::http::StatusCode::from_u16(status).unwrap(),
736			),
737		))
738	}
739
740	/// A CONNECT the relay answered carries its status through to the caller, so a wrong path or an
741	/// endpoint that doesn't speak WebTransport can surface immediately rather than after the whole
742	/// reconnect budget.
743	#[test]
744	fn a_rejected_connect_reports_its_status() {
745		for status in [400, 404, 405, 410, 501] {
746			assert_eq!(connect_rejected(status).status(), Some(status));
747			assert!(
748				!crate::error::status_retryable(status),
749				"{status} should stop the reconnect loop"
750			);
751		}
752
753		for status in [408, 429, 502, 503, 504] {
754			assert_eq!(connect_rejected(status).status(), Some(status));
755			assert!(crate::error::status_retryable(status), "{status} should be retried");
756		}
757
758		// Auth is peeled off into its own variant before reaching the generic client arm.
759		assert_eq!(
760			connect_rejected(401).connect_error(),
761			Some(crate::ConnectError::Unauthorized)
762		);
763		assert_eq!(
764			connect_rejected(403).connect_error(),
765			Some(crate::ConnectError::Forbidden)
766		);
767	}
768
769	/// Build a controller from each family's factory and downcast it to the
770	/// concrete quinn implementation it must map to.
771	#[test]
772	fn congestion_factory_maps_each_family() {
773		let now = std::time::Instant::now();
774		let mtu = 1200;
775
776		let loss = congestion_factory(CongestionControl::Loss).build(now, mtu);
777		assert!(loss.into_any().downcast::<quinn::congestion::Cubic>().is_ok());
778
779		let delay = congestion_factory(CongestionControl::Delay).build(now, mtu);
780		assert!(delay.into_any().downcast::<quinn::congestion::Bbr>().is_ok());
781	}
782
783	/// An unset knob must land on BBR rather than quinn's own CUBIC default.
784	#[test]
785	fn congestion_control_defaults_to_delay() {
786		let mut quic = crate::quic::Client::default();
787		assert_eq!(congestion_control(&quic.resolve()), CongestionControl::Delay);
788
789		// An explicit request still gets through.
790		quic.congestion_control = Some(CongestionControl::Loss);
791		assert_eq!(congestion_control(&quic.resolve()), CongestionControl::Loss);
792	}
793
794	/// Loopback regression test: a config selecting BBR must produce live
795	/// connections that actually run quinn's BBR controller, on both ends.
796	#[tokio::test]
797	async fn delay_reaches_the_live_connection() {
798		let server_config = ServerConfig {
799			bind: Some("127.0.0.1:0".to_string()),
800			tls: crate::tls::Server {
801				generate: vec!["localhost".into()],
802				..Default::default()
803			},
804			quic: crate::quic::Server {
805				congestion_control: Some(CongestionControl::Delay),
806				..Default::default()
807			},
808			..Default::default()
809		};
810
811		let server = QuinnServer::new(server_config).expect("server init");
812		let addr = server.local_addr().expect("local addr");
813
814		let accepted = tokio::spawn(async move {
815			let incoming = server.accept().await.expect("no incoming connection");
816			let conn = incoming.accept().expect("accept").await.expect("handshake");
817			conn.congestion_state()
818				.into_any()
819				.downcast::<quinn::congestion::Bbr>()
820				.is_ok()
821		});
822
823		// tls::Client has a private field, so it can't be built with a struct literal.
824		let mut tls_config = crate::tls::Client::default();
825		tls_config.disable_verify = Some(true);
826
827		let client_config = ClientConfig {
828			bind: "127.0.0.1:0".parse().unwrap(),
829			tls: tls_config,
830			quic: crate::quic::Client {
831				congestion_control: Some(CongestionControl::Delay),
832				..Default::default()
833			},
834			..Default::default()
835		};
836
837		let tls = client_config.tls.build().expect("tls config");
838		let client = QuinnClient::new(&client_config).expect("client init");
839		// Dial the loopback IP directly so the system resolver is never involved.
840		let url: Url = format!("moqt://127.0.0.1:{}", addr.port()).parse().unwrap();
841
842		// Bound the whole connect + accept + assert flow so a handshake
843		// regression fails fast instead of stalling CI.
844		tokio::time::timeout(Duration::from_secs(5), async move {
845			let session = client
846				.connect(&tls, url, &moq_net::Versions::default())
847				.await
848				.expect("connect failed");
849
850			// web_transport_quinn::Session derefs to the quinn connection.
851			assert!(
852				session
853					.congestion_state()
854					.into_any()
855					.downcast::<quinn::congestion::Bbr>()
856					.is_ok(),
857				"client connection is not running BBR"
858			);
859			assert!(
860				accepted.await.expect("server task panicked"),
861				"server connection is not running BBR"
862			);
863		})
864		.await
865		.expect("test timed out");
866	}
867}