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
12fn 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 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 if let Some(gso) = quic.gso {
28 transport.enable_segmentation_offload(gso);
29 }
30}
31
32#[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#[derive(Clone)]
138pub(crate) struct QuinnClient {
139 pub quic: quinn::Endpoint,
140 pub transport: Arc<quinn::TransportConfig>,
141 pub http_bootstrap: bool,
143 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 let mut transport = quinn::TransportConfig::default();
153 apply_transport(&mut transport, config.quic.resolve());
154 let transport = Arc::new(transport);
155
156 let runtime = quinn::default_runtime().ok_or(Error::NoRuntime)?;
158 let endpoint_config = quinn::EndpointConfig::default();
159
160 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 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 if self.http_bootstrap {
199 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 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
320pub(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 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 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 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 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 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 let quic = quinn::Endpoint::new(endpoint_config, Some(tls), socket, runtime).map_err(Error::CreateEndpoint)?;
414
415 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
439pub(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 let conn = conn.await.map_err(Error::Establish)?;
472
473 let span = tracing::Span::current();
474 span.record("id", conn.stable_id()); tracing::debug!(%host, ip = %conn.remote_address(), %alpn, "accepted");
476
477 match alpn.as_str() {
478 web_transport_quinn::ALPN => {
479 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 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 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 pub fn url(&self) -> Option<&Url> {
532 match self {
533 QuinnRequest::Raw { .. } => None,
534 QuinnRequest::WebTransport { request, .. } => Some(&request.url),
535 }
536 }
537
538 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 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
563struct 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 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}