Skip to main content

webtrans_quinn/
client.rs

1//! Client-side helpers for WebTransport over Quinn.
2
3use std::net::{IpAddr, SocketAddr};
4use std::sync::Arc;
5use std::time::Duration;
6
7#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
8use quinn::crypto::rustls::QuicClientConfig;
9use rustls::{client::danger::ServerCertVerifier, pki_types::CertificateDer};
10use tokio::net::lookup_host;
11use url::{Host, Url};
12
13#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
14use crate::ALPN;
15use crate::crypto;
16use crate::{ClientError, Session};
17
18/// Congestion control algorithm to use for the connection.
19///
20/// Different algorithms make different tradeoffs between throughput and latency.
21pub enum CongestionControl {
22    /// Use the default congestion control algorithm (typically CUBIC).
23    Default,
24    /// Optimize for throughput (BBR).
25    Throughput,
26    /// Optimize for low latency (NewReno).
27    LowLatency,
28}
29
30#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
31/// Construct a WebTransport [Client] using sensible defaults.
32///
33/// This is optional; advanced users may use [Client::new] directly.
34pub struct ClientBuilder {
35    provider: crypto::Provider,
36    transport: quinn::TransportConfig,
37    dns_timeout: Option<Duration>,
38    handshake_timeout: Option<Duration>,
39}
40
41#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
42impl ClientBuilder {
43    /// Create a client builder, which can establish multiple [Session]s.
44    pub fn new() -> Self {
45        Self {
46            provider: crypto::default_provider(),
47            transport: quinn::TransportConfig::default(),
48            dns_timeout: None,
49            handshake_timeout: None,
50        }
51    }
52
53    /// Enable the specified congestion controller.
54    pub fn with_congestion_control(mut self, algorithm: CongestionControl) -> Self {
55        match algorithm {
56            CongestionControl::LowLatency => self.transport.congestion_controller_factory(
57                Arc::new(quinn::congestion::NewRenoConfig::default()),
58            ),
59            CongestionControl::Throughput => self
60                .transport
61                .congestion_controller_factory(Arc::new(quinn::congestion::BbrConfig::default())),
62            CongestionControl::Default => self
63                .transport
64                .congestion_controller_factory(Arc::new(quinn::congestion::CubicConfig::default())),
65        };
66
67        self
68    }
69
70    /// Replace the QUIC transport configuration.
71    ///
72    /// Use this to configure idle timeouts, receive windows, concurrent stream
73    /// limits, datagram buffers, and other transport resource limits.
74    pub fn with_transport_config(mut self, transport: quinn::TransportConfig) -> Self {
75        self.transport = transport;
76        self
77    }
78
79    /// Limit how long DNS resolution may take.
80    pub fn with_dns_timeout(mut self, timeout: Duration) -> Self {
81        self.dns_timeout = Some(timeout);
82        self
83    }
84
85    /// Limit the combined QUIC, HTTP/3 SETTINGS, and CONNECT handshake duration.
86    pub fn with_handshake_timeout(mut self, timeout: Duration) -> Self {
87        self.handshake_timeout = Some(timeout);
88        self
89    }
90
91    /// Accept certificates from servers chained to known root CAs.
92    pub fn with_system_roots(self) -> Result<Client, ClientError> {
93        let mut roots = rustls::RootCertStore::empty();
94
95        let native = rustls_native_certs::load_native_certs();
96
97        // Log any errors encountered while loading native root certificates.
98        for err in native.errors {
99            tracing::warn!("failed to load root cert: {err:?}");
100        }
101
102        // Add the platform's native root certificates.
103        for cert in native.certs {
104            if let Err(err) = roots.add(cert) {
105                tracing::warn!("failed to add root cert: {err:?}");
106            }
107        }
108
109        let crypto = self
110            .builder()?
111            .with_root_certificates(roots)
112            .with_no_client_auth();
113
114        self.build(crypto)
115    }
116
117    /// Supply certificates for accepted servers instead of using root CAs.
118    pub fn with_server_certificates(
119        self,
120        certs: Vec<CertificateDer>,
121    ) -> Result<Client, ClientError> {
122        let hashes = certs.iter().map({
123            let provider = self.provider.clone();
124            move |cert| crypto::sha256(&provider, cert).as_ref().to_vec()
125        });
126
127        self.with_server_certificate_hashes(hashes.collect())
128    }
129
130    /// Supply SHA-256 hashes for accepted certificates instead of using root CAs.
131    pub fn with_server_certificate_hashes(
132        self,
133        hashes: Vec<Vec<u8>>,
134    ) -> Result<Client, ClientError> {
135        // Use a custom fingerprint verifier.
136        let fingerprints = Arc::new(ServerFingerprints {
137            provider: self.provider.clone(),
138            fingerprints: hashes,
139        });
140
141        // Configure the crypto client.
142        let crypto = self
143            .builder()?
144            .dangerous()
145            .with_custom_certificate_verifier(fingerprints.clone())
146            .with_no_client_auth();
147
148        self.build(crypto)
149    }
150
151    /// Access dangerous configuration options.
152    ///
153    /// This method returns a builder that provides access to potentially insecure
154    /// TLS configurations. These options are opt-in and require explicit acknowledgment
155    /// through the builder pattern, making the security implications clear at the call site.
156    pub fn dangerous(self) -> DangerousClientBuilder {
157        DangerousClientBuilder { inner: self }
158    }
159
160    fn builder(
161        &self,
162    ) -> Result<rustls::ConfigBuilder<rustls::ClientConfig, rustls::WantsVerifier>, ClientError>
163    {
164        rustls::ClientConfig::builder_with_provider(self.provider.clone())
165            .with_protocol_versions(&[&rustls::version::TLS13])
166            .map_err(Into::into)
167    }
168
169    fn build(self, mut crypto: rustls::ClientConfig) -> Result<Client, ClientError> {
170        crypto.alpn_protocols = vec![ALPN.as_bytes().to_vec()];
171
172        let client_config = QuicClientConfig::try_from(crypto)
173            .map_err(|_| ClientError::InvalidCryptoConfiguration)?;
174        let mut client_config = quinn::ClientConfig::new(Arc::new(client_config));
175
176        client_config.transport_config(Arc::new(self.transport));
177
178        let client = quinn::Endpoint::client(SocketAddr::from(([0_u16; 8], 0)))
179            .map_err(|error| ClientError::Io(Arc::new(error)))?;
180        Ok(Client {
181            endpoint: client,
182            config: client_config,
183            dns_timeout: self.dns_timeout,
184            handshake_timeout: self.handshake_timeout,
185        })
186    }
187}
188
189#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
190impl Default for ClientBuilder {
191    fn default() -> Self {
192        Self::new()
193    }
194}
195
196#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
197/// Builder for dangerous TLS configuration options.
198///
199/// This builder exposes potentially insecure TLS settings. Use only when you
200/// understand the security implications, such as in local development or over
201/// a secure VPN connection.
202pub struct DangerousClientBuilder {
203    inner: ClientBuilder,
204}
205
206#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
207impl DangerousClientBuilder {
208    /// Disable certificate verification entirely.
209    ///
210    /// This makes the connection vulnerable to man-in-the-middle attacks.
211    /// Only use this in secure environments, such as local development or over a VPN.
212    ///
213    /// This method is memory-safe, but dangerous from a security perspective, hence
214    /// the explicit `dangerous()` builder requirement.
215    pub fn with_no_certificate_verification(self) -> Result<Client, ClientError> {
216        let noop = NoCertificateVerification(self.inner.provider.clone());
217
218        let crypto = self
219            .inner
220            .builder()?
221            .dangerous()
222            .with_custom_certificate_verifier(Arc::new(noop))
223            .with_no_client_auth();
224
225        self.inner.build(crypto)
226    }
227}
228
229/// A client for connecting to a WebTransport server.
230#[derive(Clone, Debug)]
231pub struct Client {
232    endpoint: quinn::Endpoint,
233    config: quinn::ClientConfig,
234    dns_timeout: Option<Duration>,
235    handshake_timeout: Option<Duration>,
236}
237
238impl Client {
239    /// Manually create a client via a Quinn endpoint and config.
240    ///
241    /// The ALPN must be set to [ALPN].
242    pub fn new(endpoint: quinn::Endpoint, config: quinn::ClientConfig) -> Self {
243        Self {
244            endpoint,
245            config,
246            dns_timeout: None,
247            handshake_timeout: None,
248        }
249    }
250
251    /// Connect to the server.
252    pub async fn connect(&self, url: Url) -> Result<Session, ClientError> {
253        validate_url(&url)?;
254        let port = url.port().unwrap_or(443);
255
256        let (host, remote) = match url
257            .host()
258            .ok_or_else(|| ClientError::InvalidDnsName("".to_string()))?
259        {
260            Host::Domain(domain) => {
261                let domain = domain.to_string();
262                // Look up the DNS entry.
263                let lookup = lookup_host((domain.clone(), port));
264                let result = match self.dns_timeout {
265                    Some(timeout) => tokio::time::timeout(timeout, lookup)
266                        .await
267                        .map_err(|_| ClientError::DnsTimeout)?,
268                    None => lookup.await,
269                };
270                let mut remotes = match result {
271                    Ok(remotes) => remotes,
272                    Err(_) => return Err(ClientError::InvalidDnsName(domain)),
273                };
274
275                // Use the first resolved address.
276                let remote = match remotes.next() {
277                    Some(remote) => remote,
278                    None => return Err(ClientError::InvalidDnsName(domain)),
279                };
280
281                (domain, remote)
282            }
283            Host::Ipv4(ipv4) => (ipv4.to_string(), SocketAddr::new(IpAddr::V4(ipv4), port)),
284            Host::Ipv6(ipv6) => (ipv6.to_string(), SocketAddr::new(IpAddr::V6(ipv6), port)),
285        };
286
287        // Connect to the server using the resolved address.
288        let connecting = self
289            .endpoint
290            .connect_with(self.config.clone(), remote, &host)?;
291        let establish = async move {
292            let conn = connecting.await?;
293            Session::connect(conn, url).await
294        };
295
296        match self.handshake_timeout {
297            Some(timeout) => tokio::time::timeout(timeout, establish)
298                .await
299                .map_err(|_| ClientError::HandshakeTimeout)?,
300            None => establish.await,
301        }
302    }
303}
304
305fn validate_url(url: &Url) -> Result<(), ClientError> {
306    if url.scheme() != "https" {
307        return Err(ClientError::InvalidUrl(
308            "WebTransport requires an https URL".to_string(),
309        ));
310    }
311    if !url.username().is_empty() || url.password().is_some() {
312        return Err(ClientError::InvalidUrl(
313            "userinfo is not supported in the authority".to_string(),
314        ));
315    }
316    if url.fragment().is_some() {
317        return Err(ClientError::InvalidUrl(
318            "URL fragments are not sent in HTTP request targets".to_string(),
319        ));
320    }
321    if url.cannot_be_a_base() || url.host().is_none() {
322        return Err(ClientError::InvalidUrl(
323            "URL must contain a valid authority and path".to_string(),
324        ));
325    }
326    Ok(())
327}
328
329#[cfg_attr(not(any(feature = "ring", feature = "aws-lc-rs")), allow(dead_code))]
330#[derive(Debug)]
331struct ServerFingerprints {
332    provider: crypto::Provider,
333    fingerprints: Vec<Vec<u8>>,
334}
335
336impl ServerCertVerifier for ServerFingerprints {
337    fn verify_server_cert(
338        &self,
339        end_entity: &rustls::pki_types::CertificateDer<'_>,
340        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
341        _server_name: &rustls::pki_types::ServerName<'_>,
342        _ocsp_response: &[u8],
343        _now: rustls::pki_types::UnixTime,
344    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
345        let cert_hash = crypto::sha256(&self.provider, end_entity);
346        if self
347            .fingerprints
348            .iter()
349            .any(|fingerprint| fingerprint == cert_hash.as_ref())
350        {
351            return Ok(rustls::client::danger::ServerCertVerified::assertion());
352        }
353
354        Err(rustls::Error::InvalidCertificate(
355            rustls::CertificateError::UnknownIssuer,
356        ))
357    }
358
359    fn verify_tls12_signature(
360        &self,
361        message: &[u8],
362        cert: &rustls::pki_types::CertificateDer<'_>,
363        dss: &rustls::DigitallySignedStruct,
364    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
365        rustls::crypto::verify_tls12_signature(
366            message,
367            cert,
368            dss,
369            &self.provider.signature_verification_algorithms,
370        )
371    }
372
373    fn verify_tls13_signature(
374        &self,
375        message: &[u8],
376        cert: &rustls::pki_types::CertificateDer<'_>,
377        dss: &rustls::DigitallySignedStruct,
378    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
379        rustls::crypto::verify_tls13_signature(
380            message,
381            cert,
382            dss,
383            &self.provider.signature_verification_algorithms,
384        )
385    }
386
387    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
388        self.provider
389            .signature_verification_algorithms
390            .supported_schemes()
391    }
392}
393
394#[derive(Debug)]
395/// Certificate verifier that disables all chain and hostname validation.
396///
397/// Use only in controlled environments such as local development.
398pub struct NoCertificateVerification(Arc<rustls::crypto::CryptoProvider>);
399
400impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
401    fn verify_server_cert(
402        &self,
403        _end_entity: &CertificateDer<'_>,
404        _intermediates: &[CertificateDer<'_>],
405        _server_name: &rustls::pki_types::ServerName<'_>,
406        _ocsp: &[u8],
407        _now: rustls::pki_types::UnixTime,
408    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
409        Ok(rustls::client::danger::ServerCertVerified::assertion())
410    }
411
412    fn verify_tls12_signature(
413        &self,
414        message: &[u8],
415        cert: &CertificateDer<'_>,
416        dss: &rustls::DigitallySignedStruct,
417    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
418        rustls::crypto::verify_tls12_signature(
419            message,
420            cert,
421            dss,
422            &self.0.signature_verification_algorithms,
423        )
424    }
425
426    fn verify_tls13_signature(
427        &self,
428        message: &[u8],
429        cert: &CertificateDer<'_>,
430        dss: &rustls::DigitallySignedStruct,
431    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
432        rustls::crypto::verify_tls13_signature(
433            message,
434            cert,
435            dss,
436            &self.0.signature_verification_algorithms,
437        )
438    }
439
440    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
441        self.0.signature_verification_algorithms.supported_schemes()
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448
449    #[test]
450    fn validates_webtransport_urls() {
451        assert!(validate_url(&Url::parse("https://example.com/chat").unwrap()).is_ok());
452        assert!(validate_url(&Url::parse("http://example.com/chat").unwrap()).is_err());
453        assert!(validate_url(&Url::parse("https://user@example.com/chat").unwrap()).is_err());
454        assert!(validate_url(&Url::parse("https://example.com/chat#fragment").unwrap()).is_err());
455    }
456
457    #[test]
458    fn builder_records_operation_timeouts() {
459        let builder = ClientBuilder::new()
460            .with_dns_timeout(Duration::from_secs(2))
461            .with_handshake_timeout(Duration::from_secs(7));
462        assert_eq!(builder.dns_timeout, Some(Duration::from_secs(2)));
463        assert_eq!(builder.handshake_timeout, Some(Duration::from_secs(7)));
464    }
465}