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}