Skip to main content

trillium_quinn/
client.rs

1use crate::{
2    config::QuinnEndpoint,
3    runtime::{SocketTransport, TrilliumRuntime},
4};
5use std::{
6    fmt::{self, Debug, Formatter},
7    io,
8    net::SocketAddr,
9    sync::Arc,
10};
11use trillium_server_common::{Connector, QuicClientConfig};
12
13/// Client-side QUIC configuration for HTTP/3, backed by quinn.
14///
15/// This is a thin factory that creates [`QuinnEndpoint`]s bound to local addresses.
16/// The resulting endpoints can both accept and initiate QUIC connections.
17///
18/// # Construction
19///
20/// ```rust,ignore
21/// use trillium_tokio::ClientConfig;
22/// use trillium_quinn::ClientQuicConfig;
23///
24/// let client = trillium_client::Client::new_with_quic(
25///     ClientConfig::default(),
26///     ClientQuicConfig::with_webpki_roots(),
27/// );
28/// ```
29pub struct ClientQuicConfig {
30    client_config: quinn::ClientConfig,
31    /// Retained so the bound endpoint can rebuild a per-connection `quinn::ClientConfig` with a
32    /// different ALPN (e.g. `doq`) — ALPN is baked into `QuicClientConfig` at construction and
33    /// can't be changed on the assembled `client_config`. `None` when built from a pre-assembled
34    /// `quinn::ClientConfig` (no rustls config to rebuild from), which disables per-connection
35    /// ALPN.
36    base_tls: Option<Arc<rustls::ClientConfig>>,
37}
38
39impl Debug for ClientQuicConfig {
40    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
41        f.debug_struct("ClientQuicConfig").finish_non_exhaustive()
42    }
43}
44
45impl ClientQuicConfig {
46    /// Build a `ClientQuicConfig` trusting the [WebPKI](https://github.com/rustls/webpki-roots)
47    /// root certificates.
48    ///
49    /// Suitable for connecting to publicly trusted servers. For custom CA trust or client
50    /// authentication, use [`from_rustls_client_config`](Self::from_rustls_client_config).
51    ///
52    /// Requires the `webpki-roots` crate feature.
53    #[cfg(feature = "webpki-roots")]
54    pub fn with_webpki_roots() -> Self {
55        let root_store =
56            rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
57        let crypto =
58            rustls::ClientConfig::builder_with_provider(crate::crypto_provider::crypto_provider())
59                .with_safe_default_protocol_versions()
60                .expect("building TLS config with protocol versions")
61                .with_root_certificates(root_store)
62                .with_no_client_auth();
63
64        Self::from_rustls_client_config(crypto)
65    }
66
67    /// Build from a pre-built [`rustls::ClientConfig`].
68    ///
69    /// `h3` ALPN is added automatically if not already present.
70    pub fn from_rustls_client_config(mut tls: rustls::ClientConfig) -> Self {
71        if !tls.alpn_protocols.contains(&b"h3".to_vec()) {
72            tls.alpn_protocols.push(b"h3".to_vec());
73        }
74        let tls = Arc::new(tls);
75        let quic_tls = quinn::crypto::rustls::QuicClientConfig::try_from(tls.clone())
76            .expect("building QUIC client TLS config");
77        Self {
78            client_config: quinn::ClientConfig::new(Arc::new(quic_tls)),
79            base_tls: Some(tls),
80        }
81    }
82
83    /// Build from a pre-built [`quinn::ClientConfig`].
84    ///
85    /// Use this when you need full control over transport parameters or TLS. The caller is
86    /// responsible for including `h3` in ALPN protocols. Per-connection ALPN override (used by
87    /// DNS-over-QUIC) is unavailable on configs built this way — there is no rustls config to
88    /// derive an alternate-ALPN config from.
89    pub fn from_quinn_client_config(config: quinn::ClientConfig) -> Self {
90        Self {
91            client_config: config,
92            base_tls: None,
93        }
94    }
95
96    /// Override the quinn [`TransportConfig`](quinn::TransportConfig) governing flow-control
97    /// windows, congestion control, GSO, and related transport parameters.
98    ///
99    /// Composes with any of the `from_*` constructors — construct with TLS credentials, then call
100    /// this to replace quinn's default transport configuration. The mirror of
101    /// [`QuicConfig::with_transport_config`](crate::QuicConfig::with_transport_config) on the
102    /// client side.
103    #[must_use]
104    pub fn with_transport_config(mut self, transport: Arc<quinn::TransportConfig>) -> Self {
105        self.client_config.transport_config(transport);
106        self
107    }
108}
109
110impl<C> QuicClientConfig<C> for ClientQuicConfig
111where
112    C: Connector,
113    C::Runtime: Unpin,
114    C::Udp: SocketTransport,
115{
116    type Endpoint = QuinnEndpoint;
117
118    fn bind(&self, addr: SocketAddr, runtime: &C::Runtime) -> io::Result<Self::Endpoint> {
119        let socket = std::net::UdpSocket::bind(addr)?;
120        let quinn_runtime = TrilliumRuntime::<C::Runtime, C::Udp>::new(runtime.clone());
121        let mut endpoint = quinn::Endpoint::new(
122            quinn::EndpointConfig::default(),
123            None, // client-only, no server config
124            socket,
125            quinn_runtime,
126        )?;
127        endpoint.set_default_client_config(self.client_config.clone());
128        Ok(QuinnEndpoint::new_client(endpoint, self.base_tls.clone()))
129    }
130}