1use super::{Error, pool::Origin};
7use blitz_traits::tls_policy::TlsPolicyError;
8use bytes::BytesMut;
9use nago_rustls::{
10 TlsSession,
11 rustls::{ClientConfig, ClientConnection, RootCertStore},
12 rustls_pki_types::ServerName,
13};
14use nagoya::{net::TcpStream, reactor::Handle};
15use ps_rustcrypto::policy;
16use std::{
17 collections::HashMap,
18 sync::{Arc, Mutex, OnceLock},
19};
20
21const READ_CHUNK: usize = 16 * 1024;
22
23pub(super) fn default_config() -> Result<Arc<ClientConfig>, Error> {
24 static CONFIG: OnceLock<Result<Arc<ClientConfig>, String>> = OnceLock::new();
25 match CONFIG.get_or_init(build_config) {
26 Ok(config) => Ok(config.clone()),
27 Err(error) => Err(Error::Tls(error.clone())),
28 }
29}
30
31fn native_roots() -> Result<RootCertStore, String> {
32 let native = rustls_native_certs::load_native_certs();
33 let mut roots = RootCertStore::empty();
34 let (accepted, rejected) = roots.add_parsable_certificates(native.certs);
35 if accepted == 0 {
36 return Err(format!(
37 "no usable native TLS trust anchors: {} loading errors, {rejected} rejected certificates",
38 native.errors.len(),
39 ));
40 }
41 if !native.errors.is_empty() || rejected != 0 {
42 eprintln!(
43 "TLS trust store: {} loading errors, {rejected} rejected certificates",
44 native.errors.len(),
45 );
46 }
47 #[cfg(feature = "sandbox2-witness")]
48 crate::chacha_policy::add_witness_roots(&mut roots)?;
49 Ok(roots)
50}
51
52fn public_roots(native: &RootCertStore) -> RootCertStore {
56 let mut public = RootCertStore::empty();
57 public.roots = native
58 .roots
59 .iter()
60 .filter(|root| {
61 webpki_roots::TLS_SERVER_ROOTS.iter().any(|built_in| {
62 root.subject.as_ref() == built_in.subject.as_ref()
63 && root.subject_public_key_info.as_ref()
64 == built_in.subject_public_key_info.as_ref()
65 })
66 })
67 .cloned()
68 .collect();
69 public
70}
71
72pub(super) fn alpn() -> Vec<Vec<u8>> {
74 vec![b"h2".to_vec(), b"http/1.1".to_vec()]
75}
76
77fn build_config() -> Result<Arc<ClientConfig>, String> {
78 let roots = native_roots()?;
79 let public = public_roots(&roots);
80 let provider = crate::chacha_provider();
81 let mut config = ClientConfig::builder_with_provider(provider)
82 .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])
83 .map_err(|error| error.to_string())?
84 .dangerous()
85 .with_custom_certificate_verifier(crate::certificate_verifier(roots, public)?)
86 .with_no_client_auth();
87 config.alpn_protocols = alpn();
88 #[cfg(feature = "sandbox2-witness")]
89 if std::env::var("SANDBOX2_TLS_DEFECT").ok().as_deref() == Some("chacha-load") {
90 config.alpn_protocols = vec![b"sandbox2-incompatible".to_vec()];
92 }
93 Ok(Arc::new(config))
94}
95
96static HOSTS: Mutex<Option<HashMap<String, HostStats>>> = Mutex::new(None);
98
99#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
101pub struct HostStats {
102 pub connections: u32,
104 pub refusals: u32,
106 pub handshakes: u32,
108 pub last_tls_policy_error: Option<TlsPolicyError>,
110 pub last_alpn: Option<&'static str>,
113}
114
115fn update(host: &str, change: impl FnOnce(&mut HostStats)) -> HostStats {
116 let Ok(mut map) = HOSTS.lock() else {
117 return HostStats::default();
118 };
119 let stats = map
120 .get_or_insert_with(HashMap::new)
121 .entry(host.to_ascii_lowercase())
122 .or_default();
123 change(stats);
124 *stats
125}
126
127pub(super) fn record_alpn(host: &str, alpn: &'static str) {
128 update(host, |stats| stats.last_alpn = Some(alpn));
129}
130
131pub fn connection_stats() -> Vec<(String, HostStats)> {
133 let mut all: Vec<_> = HOSTS
134 .lock()
135 .ok()
136 .and_then(|map| {
137 map.as_ref()
138 .map(|map| map.iter().map(|(h, s)| (h.clone(), *s)).collect())
139 })
140 .unwrap_or_default();
141 all.sort_by(|a, b| a.0.cmp(&b.0));
142 all
143}
144
145pub(super) async fn open(
146 origin: &Origin,
147 handle: &Handle,
148 config: Arc<ClientConfig>,
149 addresses: &[nagoya::reactor::socket::Addr],
150) -> Result<Transport, Error> {
151 if !origin.tls {
152 return Ok(Transport::Plain(connect(origin, handle, addresses).await?));
153 }
154 update(&origin.host, |stats| stats.last_tls_policy_error = None);
155 match handshake(origin, handle, config, addresses).await {
157 Ok(tls) => {
158 update(&origin.host, |stats| stats.handshakes += 1);
159 Ok(Transport::Tls(tls))
160 }
161 Err(error) => {
162 if let Error::TlsPolicy(reason) = &error {
163 update(&origin.host, |stats| {
164 stats.refusals += 1;
165 stats.last_tls_policy_error = Some(*reason);
166 });
167 }
168 info(format_args!("TLS {}: {error}", origin.host));
170 Err(error)
171 }
172 }
173}
174
175fn info(message: std::fmt::Arguments<'_>) {
176 #[cfg(feature = "tracing")]
177 tracing::info!("{message}");
178 #[cfg(not(feature = "tracing"))]
179 eprintln!("info: {message}");
180}
181
182pub(super) async fn resolve_addresses(
183 origin: &Origin,
184 initiator: &blitz_traits::net::Initiator,
185) -> Result<Vec<nagoya::reactor::socket::Addr>, Error> {
186 let host = origin.host.clone();
187 let port = origin.port;
188 let addresses = nagoya::spawn(async move {
189 nagoya::net::resolve(&host, port).map_err(|error| Error::Dns(error.to_string()))
190 })
191 .await
192 .ok_or_else(|| Error::Dns("resolver task was cancelled".to_owned()))??;
193
194 for address in &addresses {
195 use nagoya::reactor::socket::Addr;
196 let ip = match address {
197 Addr::V4(bytes, _) => std::net::IpAddr::from(*bytes),
198 Addr::V6(bytes, _) | Addr::V6Scoped(bytes, _, _) => std::net::IpAddr::from(*bytes),
199 Addr::Path(_) => return Err(Error::Blocked("resource-private-address".into())),
200 };
201 initiator.check_address(ip).map_err(Error::Blocked)?;
202 }
203 Ok(addresses)
204}
205
206async fn connect(
207 origin: &Origin,
208 handle: &Handle,
209 addresses: &[nagoya::reactor::socket::Addr],
210) -> Result<TcpStream, Error> {
211 let stats = update(&origin.host, |stats| stats.connections += 1);
212 info(format_args!(
213 "net {}: new connection #{} (tls {})",
214 origin.host, stats.connections, origin.tls
215 ));
216 nagoya::net::connect_any(addresses, handle)
217 .await
218 .map_err(|error| Error::Io(error.to_string()))
219}
220
221async fn handshake(
223 origin: &Origin,
224 handle: &Handle,
225 config: Arc<ClientConfig>,
226 addresses: &[nagoya::reactor::socket::Addr],
227) -> Result<Box<TlsSession<TcpStream>>, Error> {
228 let stream = connect(origin, handle, addresses).await?;
229 let name = ServerName::try_from(origin.host.to_string())
230 .map_err(|_| Error::InvalidRequest("invalid TLS server name"))?;
231 let session =
232 ClientConnection::new(config, name).map_err(|error| Error::Tls(error.to_string()))?;
233 let mut tls = TlsSession::client(stream, session);
234 if let Err(errno) = tls.handshake().await {
235 return Err(handshake_failure(tls.tls_error(), errno));
236 }
237 if tls
238 .alpn_protocol()
239 .is_some_and(|protocol| protocol != b"http/1.1" && protocol != b"h2")
240 {
241 return Err(Error::Tls(
242 "peer negotiated an unsupported ALPN protocol".to_owned(),
243 ));
244 }
245 #[cfg(feature = "sandbox2-witness")]
246 if std::env::var("SANDBOX2_TLS_DEFECT").ok().as_deref() == Some("chacha-load") {
247 return Err(Error::TlsPolicy(TlsPolicyError::CipherSuite));
249 }
250 Ok(Box::new(tls))
251}
252
253fn handshake_failure(
258 error: Option<&nago_rustls::rustls::Error>,
259 errno: impl std::fmt::Display,
260) -> Error {
261 use nago_rustls::rustls::Error as TlsError;
262 use nago_rustls::rustls::{
263 AlertDescription, CertificateError, PeerIncompatible, PeerMisbehaved,
264 };
265
266 let reason = match error {
267 Some(error) if policy::is_rsa_refusal(error) => TlsPolicyError::RsaCertificateChain,
268 Some(error) if policy::is_key_type_refusal(error) => TlsPolicyError::CertificateKey,
269 Some(TlsError::InvalidCertificate(
270 CertificateError::UnsupportedSignatureAlgorithmContext { .. }
271 | CertificateError::UnsupportedSignatureAlgorithmForPublicKeyContext { .. },
272 )) => TlsPolicyError::CertificateSignature,
273 Some(TlsError::PeerMisbehaved(PeerMisbehaved::SelectedUnofferedCipherSuite)) => {
274 TlsPolicyError::CipherSuite
275 }
276 Some(TlsError::PeerMisbehaved(
277 PeerMisbehaved::SignedHandshakeWithUnadvertisedSigScheme,
278 ))
279 | Some(TlsError::PeerIncompatible(PeerIncompatible::NoSignatureSchemesInCommon)) => {
280 TlsPolicyError::HandshakeSignature
281 }
282 Some(TlsError::AlertReceived(AlertDescription::HandshakeFailure)) => {
283 TlsPolicyError::PeerRejectedAlgorithms { alert: 40 }
284 }
285 Some(TlsError::AlertReceived(AlertDescription::InsufficientSecurity)) => {
286 TlsPolicyError::PeerRejectedAlgorithms { alert: 71 }
287 }
288 Some(error) if policy::ct::is_not_logged(error) => {
289 return Error::Tls("certificate not publicly logged".to_owned());
290 }
291 Some(error) => return Error::Tls(format!("handshake: {error}")),
292 None => return Error::Tls(format!("handshake: {errno}")),
293 };
294 Error::TlsPolicy(reason)
295}
296
297pub(super) enum Transport {
298 Plain(TcpStream),
299 Tls(Box<TlsSession<TcpStream>>),
300}
301
302impl Transport {
303 pub async fn read_into(&mut self, buffer: &mut BytesMut) -> Result<usize, Error> {
304 match self {
305 Self::Plain(stream) => {
306 buffer.reserve(READ_CHUNK);
308 stream
309 .read_buf(buffer)
310 .await
311 .map_err(|error| Error::Io(error.to_string()))
312 }
313 Self::Tls(stream) => {
314 let start = buffer.len();
317 buffer.resize(start + READ_CHUNK, 0);
318 match stream.read(&mut buffer[start..]).await {
319 Ok(read) => {
320 buffer.truncate(start + read);
321 Ok(read)
322 }
323 Err(error) => {
324 buffer.truncate(start);
325 Err(Error::Io(error.to_string()))
326 }
327 }
328 }
329 }
330 }
331
332 pub async fn write_all(&mut self, bytes: &[u8]) -> Result<(), Error> {
333 match self {
334 Self::Plain(stream) => stream
335 .write_all(bytes)
336 .await
337 .map_err(|error| Error::Write(error.to_string())),
338 Self::Tls(stream) => stream
339 .write_all(bytes)
340 .await
341 .map_err(|error| Error::Write(error.to_string())),
342 }
343 }
344}