diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 7a95a98..78476a0 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,3 @@ github: EpicEric +ko_fi: epiceric liberapay: EpicEric diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5160a..1007677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Add `--no-domain` CLI flag. +### Fixed + +- Improved general performance. + ## 0.8.7 (2026-01-15) ### Added diff --git a/Cargo.lock b/Cargo.lock index c5aa66f..2575117 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3838,9 +3838,9 @@ dependencies = [ [[package]] name = "russh" -version = "0.56.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdbb7dcdd62c17ac911307ff693f55b3ec6712004d2d66ffdb8c0fa00269fd66" +checksum = "01fe22d10a0e39c1134a971d5b8db8a40357b48ef22d81fa8d6eac22202dd782" dependencies = [ "aes", "aws-lc-rs", @@ -3880,8 +3880,8 @@ dependencies = [ "pkcs1", "pkcs5", "pkcs8 0.10.2", - "rand 0.8.5", - "rand_core 0.6.4", + "rand 0.9.2", + "rand_core 0.10.0-rc-3", "rsa", "russh-cryptovec", "russh-util", diff --git a/Cargo.toml b/Cargo.toml index bdb4bed..4de67ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ reqwest = { version = "0.13.1", optional = true, default-features = false, featu "json", "rustls", ] } -russh = "0.56.0" +russh = "0.57.0" rustls = "0.23.36" rustls-acme = { version = "0.15.0", optional = true, default-features = false, features = [ "tokio", diff --git a/docker-compose-example/sandhole/compose.yml b/docker-compose-example/sandhole/compose.yml index cea9f91..452894f 100644 --- a/docker-compose-example/sandhole/compose.yml +++ b/docker-compose-example/sandhole/compose.yml @@ -12,10 +12,12 @@ services: - sh - -c - > - agnos-generate-accounts-keys --key-size 4096 --no-confirm config.toml - && agnos --no-staging config.toml - && echo 'Retrying in one hour...' - && sleep 3600 + while true; do + agnos-generate-accounts-keys --key-size 4096 --no-confirm config.toml; + agnos --no-staging config.toml; + echo 'Retrying in one hour...'; + sleep 3600; + done sandhole: image: docker.io/epiceric/sandhole:latest diff --git a/nixos/module.nix b/nixos/module.nix index 4e8a42a..32813c5 100644 --- a/nixos/module.nix +++ b/nixos/module.nix @@ -76,6 +76,7 @@ in # Default values for the CLI default = { domain = null; + no-domain = false; ssh-port = 2222; http-port = 80; https-port = 443; diff --git a/src/connections.rs b/src/connections.rs index e10764c..a9feee6 100644 --- a/src/connections.rs +++ b/src/connections.rs @@ -153,6 +153,7 @@ where let slice = value.0.as_slice(); let entry = match self.strategy { LoadBalancingStrategy::Replace | LoadBalancingStrategy::Deny => slice.first(), + LoadBalancingStrategy::Allow if slice.len() <= 1 => slice.first(), LoadBalancingStrategy::Allow => match self.algorithm { LoadBalancingAlgorithm::IpHash => { let mut hash = SipHasher::default(); diff --git a/src/entrypoint.rs b/src/entrypoint.rs index 66c0abf..bf249af 100644 --- a/src/entrypoint.rs +++ b/src/entrypoint.rs @@ -32,7 +32,7 @@ use tokio::{ io::copy_bidirectional_with_sizes, net::TcpStream, pin, - time::{sleep, timeout}, + time::{interval, timeout}, }; use tokio_rustls::TlsAcceptor; use tokio_util::sync::CancellationToken; @@ -119,10 +119,10 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> { } let http_request_timeout = config.http_request_timeout; let tcp_connection_timeout = config.tcp_connection_timeout; - let buffer_size = config - .buffer_size - .try_into() - .with_context(|| "Cannot convert buffer size to usize")?; + let buffer_size = usize::try_from(config.buffer_size) + .with_context(|| "Cannot convert buffer size to usize")? + .checked_shl(1) + .expect("TEST: small buffer size"); // Initialize crypto and credentials let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); // Find the private SSH key for Sandhole or create a new one. @@ -378,8 +378,10 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> { let telemetry_clone = Arc::clone(&telemetry); // Periodically update SSH data, based on the connection map. tokio::spawn(async move { + let mut refresh_interval = interval(Duration::from_millis(3_000)); + refresh_interval.tick().await; loop { - sleep(Duration::from_millis(3_000)).await; + refresh_interval.tick().await; let data = connections_clone.data(); let telemetry_per_minute = telemetry_clone.get_ssh_connections_per_minute(); let telemetry_current = telemetry_clone.get_current_ssh_connections(); @@ -410,8 +412,10 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> { let telemetry_clone = Arc::clone(&telemetry); // Periodically update HTTP data, based on the connection map and the telemetry counters. tokio::spawn(async move { + let mut refresh_interval = interval(Duration::from_millis(3_000)); + refresh_interval.tick().await; loop { - sleep(Duration::from_millis(3_000)).await; + refresh_interval.tick().await; let data = connections_clone.data(); let telemetry = telemetry_clone.get_http_requests_per_minute(); let data = data @@ -431,8 +435,10 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> { let telemetry_clone = Arc::clone(&telemetry); // Periodically update SNI data, based on the connection map and the telemetry counters. tokio::spawn(async move { + let mut refresh_interval = interval(Duration::from_millis(3_000)); + refresh_interval.tick().await; loop { - sleep(Duration::from_millis(3_000)).await; + refresh_interval.tick().await; let data = connections_clone.data(); let telemetry_per_minute = telemetry_clone.get_sni_connections_per_minute(); let telemetry_current = telemetry_clone.get_current_sni_connections(); @@ -465,8 +471,10 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> { let telemetry_clone = Arc::clone(&telemetry); // Periodically update TCP data, based on the connection map. tokio::spawn(async move { + let mut refresh_interval = interval(Duration::from_millis(3_000)); + refresh_interval.tick().await; loop { - sleep(Duration::from_millis(3_000)).await; + refresh_interval.tick().await; let data = connections_clone.data(); let telemetry_per_minute = telemetry_clone.get_tcp_connections_per_minute(); let telemetry_current = telemetry_clone.get_current_tcp_connections(); @@ -495,8 +503,10 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> { let telemetry_clone = Arc::clone(&telemetry); // Periodically update alias data, based on the connection map. tokio::spawn(async move { + let mut refresh_interval = interval(Duration::from_millis(3_000)); + refresh_interval.tick().await; loop { - sleep(Duration::from_millis(3_000)).await; + refresh_interval.tick().await; let alias_data = alias_connections_clone.data(); let admin_data = admin_connections_clone.data(); let telemetry_alias_per_minute = telemetry_clone.get_alias_connections_per_minute(); @@ -549,8 +559,10 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> { .with_memory(MemoryRefreshKind::nothing().with_ram()); let mut system = System::new_with_specifics(system_refresh); let mut networks = Networks::new_with_refreshed_list(); + let mut system_data_interval = interval(Duration::from_millis(1_000)); + system_data_interval.tick().await; loop { - sleep(Duration::from_millis(1_000)).await; + system_data_interval.tick().await; system.refresh_specifics(system_refresh); networks.refresh(true); let (network_tx, network_rx) = match networks @@ -768,10 +780,12 @@ pub async fn entrypoint(config: ApplicationConfig) -> color_eyre::Result<()> { http11_server_config .alpn_protocols .extend_from_slice(&[b"http/1.1".to_vec()]); + http11_server_config.max_early_data_size = 1024; let http11_server_config = Arc::new(http11_server_config); http2_server_config .alpn_protocols .extend_from_slice(&[b"h2".to_vec(), b"http/1.1".to_vec()]); + http2_server_config.max_early_data_size = 1024; let http2_server_config = Arc::new(http2_server_config); let ip_filter_clone = Arc::clone(&ip_filter); let https_proxy_data = Arc::new( @@ -985,7 +999,8 @@ async fn handle_https_connection( let service = service_fn(move |req: Request| { proxy_handler(req, address, None, Arc::clone(&proxy_data)) }); - let server = auto::Builder::new(TokioExecutor::new()); + let mut server = auto::Builder::new(TokioExecutor::new()); + server.http1().pipeline_flush(true); let conn = server.serve_connection_with_upgrades(io, service); match sandhole.tcp_connection_timeout { Some(duration) => { diff --git a/src/http.rs b/src/http.rs index ff8d738..48b7436 100644 --- a/src/http.rs +++ b/src/http.rs @@ -5,18 +5,21 @@ use std::{ net::SocketAddr, pin::Pin, str::FromStr, - sync::Arc, + sync::{Arc, LazyLock}, task::{Context, Poll}, time::{Duration, Instant}, }; use crate::{ connection_handler::ConnectionHandler, - telemetry::{TELEMETRY_HISTOGRAM_HTTP_ELAPSED_TIME, TELEMETRY_KEY_HOSTNAME}, + connections::ConnectionGetByHttpHost, + ssh::ServerHandlerSender, + tcp_alias::BorrowedTcpAlias, + telemetry::{ + TELEMETRY_COUNTER_ALIAS_CONNECTIONS, TELEMETRY_COUNTER_HTTP_REQUESTS, + TELEMETRY_HISTOGRAM_HTTP_ELAPSED_TIME, TELEMETRY_KEY_ALIAS, TELEMETRY_KEY_HOSTNAME, + }, }; -use crate::{connections::ConnectionGetByHttpHost, telemetry::TELEMETRY_KEY_ALIAS}; -use crate::{ssh::ServerHandlerSender, telemetry::TELEMETRY_COUNTER_HTTP_REQUESTS}; -use crate::{tcp_alias::TcpAlias, telemetry::TELEMETRY_COUNTER_ALIAS_CONNECTIONS}; use axum::{ body::Body as AxumBody, @@ -30,7 +33,7 @@ use http::{ use http::{header::COOKIE, uri::InvalidUriParts}; use hyper::{ Request, Response, StatusCode, - body::Body, + body::{Body, Incoming}, header::{HOST, UPGRADE}, }; use hyper_util::rt::{TokioExecutor, TokioIo}; @@ -42,18 +45,27 @@ use tokio::{ time::timeout, }; -const X_FORWARDED_FOR: &str = "X-Forwarded-For"; -const X_FORWARDED_HOST: &str = "X-Forwarded-Host"; -const X_FORWARDED_PROTO: &str = "X-Forwarded-Proto"; -const X_FORWARDED_PORT: &str = "X-Forwarded-Port"; +static X_FORWARDED_FOR: LazyLock = + LazyLock::new(|| HeaderName::from_str("X-Forwarded-For").expect("valid header name")); +static X_FORWARDED_HOST: LazyLock = + LazyLock::new(|| HeaderName::from_str("X-Forwarded-Host").expect("valid header name")); +static X_FORWARDED_PROTO: LazyLock = + LazyLock::new(|| HeaderName::from_str("X-Forwarded-Proto").expect("valid header name")); +static X_FORWARDED_PORT: LazyLock = + LazyLock::new(|| HeaderName::from_str("X-Forwarded-Port").expect("valid header name")); + +enum ProxyResponse { + Axum(Response), + Proxy(TimedResponse), +} struct TimedResponse { - response: Response, + response: Response, log: Option>, } struct TimedResponseBody { - body: AxumBody, + body: Incoming, log: Option>, } @@ -73,7 +85,7 @@ impl IntoResponse for TimedResponse { impl Body for TimedResponseBody { type Data = bytes::Bytes; - type Error = axum::Error; + type Error = hyper::Error; #[inline] fn poll_frame( @@ -164,12 +176,11 @@ fn http_log(data: HttpLog, tx: Option, disable_http_logs: b } } -fn append_to_header(headers: &mut HeaderMap, new_header_name: &str, new_value: String) { - let header_name: HeaderName = HeaderName::from_str(new_header_name).expect("valid header name"); - +// Append the bytes to the given comma-separated entry of HeaderMap +fn append_to_header(headers: &mut HeaderMap, header_name: &HeaderName, new_value: &[u8]) { match headers.entry(header_name) { http::header::Entry::Vacant(entry) => { - entry.insert(HeaderValue::from_str(new_value.as_str()).expect("valid header value")); + entry.insert(HeaderValue::from_bytes(new_value).expect("valid header value")); } http::header::Entry::Occupied(mut entry) => { let existing = entry.get().as_bytes(); @@ -178,7 +189,7 @@ fn append_to_header(headers: &mut HeaderMap, new_header_name: &str, new_value: S combined_bytes.extend_from_slice(existing); combined_bytes.extend_from_slice(b", "); - combined_bytes.extend_from_slice(new_value.as_bytes()); + combined_bytes.extend_from_slice(new_value); if let Ok(new_val) = HeaderValue::from_bytes(&combined_bytes) { entry.insert(new_val); @@ -307,7 +318,10 @@ where ::Error: Error + Send + Sync + 'static, { match proxy_handler_inner(request, tcp_address, fingerprint, proxy_data).await { - Ok(response) => Ok(response.into_response()), + Ok(response) => Ok(match response { + ProxyResponse::Axum(response) => response, + ProxyResponse::Proxy(response) => response.into_response(), + }), Err(error) => Ok(error.into_response()), } } @@ -321,7 +335,7 @@ async fn proxy_handler_inner( tcp_address: SocketAddr, fingerprint: Option, proxy_data: Arc>, -) -> Result +) -> Result where M: ConnectionGetByHttpHost>, H: ConnectionHandler, @@ -353,13 +367,9 @@ where let host = host.to_owned(); let ip = tcp_address.ip().to_canonical(); let ip_string = ip.to_string(); - let method = request.method().to_owned(); - let uri = request.uri().to_owned(); let http_log_builder = HttpLog::builder() - .ip(ip_string.clone()) - .host(host.clone()) - .uri(uri.path().into()) - .method(method.as_str().into()); + .uri(request.uri().path().to_string()) + .method(request.method().to_string()); // Find the HTTP handler for the given host let Some(handler) = conn_manager.get_by_http_host(&host, ip) else { // If no handler was found, check if this is a request to the root domain @@ -368,17 +378,17 @@ where { // If so, redirect to the configured URL let response = Redirect::to(&redirect.to).into_response(); - let http_log_builder = http_log_builder.status(response.status().as_u16()); - return Ok(TimedResponse { - response, - log: Some(Box::new(move || { - http_log( - http_log_builder.elapsed_time(timer.elapsed()).build(), - None, - disable_http_logs, - ) - })), - }); + http_log( + http_log_builder + .host(host) + .ip(ip_string) + .status(response.status().as_u16()) + .elapsed_time(timer.elapsed()) + .build(), + None, + disable_http_logs, + ); + return Ok(ProxyResponse::Axum(response)); } // No handler was found, return 404 return Err(HttpError::HandlerNotFound); @@ -413,17 +423,17 @@ where .as_str(), ) .into_response(); - let http_log_builder = http_log_builder.status(response.status().as_u16()); - return Ok(TimedResponse { - response, - log: Some(Box::new(move || { - http_log( - http_log_builder.elapsed_time(timer.elapsed()).build(), - None, - disable_http_logs, - ) - })), - }); + http_log( + http_log_builder + .host(host) + .ip(ip_string) + .status(response.status().as_u16()) + .elapsed_time(timer.elapsed()) + .build(), + None, + disable_http_logs, + ); + return Ok(ProxyResponse::Axum(response)); } (Protocol::Http { port }, _, _) | (Protocol::TlsRedirect { from: port, .. }, _, ProxyType::Aliasing) => ("http", *port), @@ -431,13 +441,14 @@ where }; // Add proxied info to the proper headers, but don't overwrite any existing proxy headers let headers = request.headers_mut(); - append_to_header(headers, X_FORWARDED_FOR, ip_string.clone()); - append_to_header(headers, X_FORWARDED_HOST, host.clone()); - append_to_header(headers, X_FORWARDED_PROTO, proto.to_string()); - append_to_header(headers, X_FORWARDED_PORT, port.to_string()); + append_to_header(headers, &X_FORWARDED_FOR, ip_string.as_bytes()); + append_to_header(headers, &X_FORWARDED_HOST, host.as_bytes()); + append_to_header(headers, &X_FORWARDED_PROTO, proto.as_bytes()); + append_to_header(headers, &X_FORWARDED_PORT, port.to_string().as_bytes()); + let http_log_builder = http_log_builder.host(host.clone()).ip(ip_string); // Add this request to the telemetry for the host if http_data.as_ref().is_some_and(|data| data.is_aliasing) { - counter!(TELEMETRY_COUNTER_ALIAS_CONNECTIONS, TELEMETRY_KEY_ALIAS => TcpAlias(host.clone(), port).to_string()) + counter!(TELEMETRY_COUNTER_ALIAS_CONNECTIONS, TELEMETRY_KEY_ALIAS => BorrowedTcpAlias(&host, &port).to_string()) .increment(1); } else { counter!(TELEMETRY_COUNTER_HTTP_REQUESTS, TELEMETRY_KEY_HOSTNAME => host.clone()) @@ -464,8 +475,8 @@ where let is_http2 = http_data.as_ref().map(|data| data.http2).unwrap_or(false); let request_host = http_data .as_ref() - .and_then(|data| data.host.clone()) - .unwrap_or(host); + .and_then(|data| data.host.as_deref()) + .unwrap_or(host.as_str()); match request.version() { Version::HTTP_2 if is_http2 => { // Create an HTTP/2 handshake over the selected channel @@ -474,7 +485,7 @@ where *authority = Authority::from_maybe_shared( authority .as_str() - .replace(authority.host(), &request_host) + .replace(authority.host(), request_host) .into_bytes(), )?; *request.uri_mut() = Uri::from_parts(uri_parts)?; @@ -508,8 +519,8 @@ where None => sender.send_request(request).await?, }; let http_log_builder = http_log_builder.status(response.status().as_u16()); - Ok(TimedResponse { - response: response.into_response(), + Ok(ProxyResponse::Proxy(TimedResponse { + response, log: Some(Box::new(move || { http_log( http_log_builder.elapsed_time(timer.elapsed()).build(), @@ -517,7 +528,7 @@ where disable_http_logs, ) })), - }) + })) } Version::HTTP_11 | Version::HTTP_2 => { // Ensure best-effort compatibility of proxy request with HTTP/1.1 format @@ -634,8 +645,8 @@ where } // Return the response to the client let http_log_builder = http_log_builder.status(response.status().as_u16()); - Ok(TimedResponse { - response: response.into_response(), + Ok(ProxyResponse::Proxy(TimedResponse { + response, log: Some(Box::new(move || { http_log( http_log_builder.elapsed_time(timer.elapsed()).build(), @@ -643,12 +654,12 @@ where disable_http_logs, ) })), - }) + })) } _ => { let http_log_builder = http_log_builder.status(response.status().as_u16()); - Ok(TimedResponse { - response: response.into_response(), + Ok(ProxyResponse::Proxy(TimedResponse { + response, log: Some(Box::new(move || { http_log( http_log_builder.elapsed_time(timer.elapsed()).build(), @@ -656,7 +667,7 @@ where disable_http_logs, ) })), - }) + })) } } } else { @@ -690,8 +701,8 @@ where }; // Return the received response to the client let http_log_builder = http_log_builder.status(response.status().as_u16()); - Ok(TimedResponse { - response: response.into_response(), + Ok(ProxyResponse::Proxy(TimedResponse { + response, log: Some(Box::new(move || { http_log( http_log_builder.elapsed_time(timer.elapsed()).build(), @@ -699,7 +710,7 @@ where disable_http_logs, ) })), - }) + })) } } version => Err(HttpError::InvalidHttpVersion(version)), diff --git a/src/tcp_alias.rs b/src/tcp_alias.rs index ac987bd..1ea063c 100644 --- a/src/tcp_alias.rs +++ b/src/tcp_alias.rs @@ -32,6 +32,12 @@ impl FromStr for TcpAlias { #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub(crate) struct BorrowedTcpAlias<'a>(pub(crate) &'a str, pub(crate) &'a u16); +impl Display for BorrowedTcpAlias<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.0, self.1) + } +} + impl<'a> Borrow for TcpAlias { fn borrow(&self) -> &(dyn TcpAliasKey + 'a) { self diff --git a/src/tls.rs b/src/tls.rs index 6d37dbd..79726d8 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -1,3 +1,4 @@ +use tokio::io::{empty, join}; use tokio_rustls::LazyConfigAcceptor; pub(crate) struct TlsPeekData { @@ -7,10 +8,9 @@ pub(crate) struct TlsPeekData { // Get the SNI and ALPN from a peeked ClientHello if it's valid. pub(crate) async fn peek_sni_and_alpn(buf: &[u8]) -> Option { - let handshake = - LazyConfigAcceptor::new(Default::default(), tokio::io::join(buf, tokio::io::empty())) - .await - .ok()?; + let handshake = LazyConfigAcceptor::new(Default::default(), join(buf, empty())) + .await + .ok()?; let client_hello = handshake.client_hello(); client_hello.server_name().map(|sni| TlsPeekData { sni: sni.to_owned(), diff --git a/tests/integration/https_multi_stream_download.rs b/tests/integration/https_multi_stream_download.rs index 9f8cac7..792e087 100644 --- a/tests/integration/https_multi_stream_download.rs +++ b/tests/integration/https_multi_stream_download.rs @@ -123,7 +123,7 @@ async fn https_multi_stream_download() { .await .expect("tcpip_forward failed"); - // 3. Connect to the HTTPS port of our proxy with out multiple streams + // 3. Connect to the HTTPS port of our proxy with multiple streams let mut root_store = RootCertStore::empty(); root_store.add_parsable_certificates( CertificateDer::pem_file_iter(concat!( diff --git a/tests/integration/https_multi_stream_upload.rs b/tests/integration/https_multi_stream_upload.rs index bf0831d..c31a738 100644 --- a/tests/integration/https_multi_stream_upload.rs +++ b/tests/integration/https_multi_stream_upload.rs @@ -119,7 +119,7 @@ async fn https_multi_stream_upload() { .await .expect("tcpip_forward failed"); - // 3. Connect to the HTTPS port of our proxy with out multiple streams + // 3. Connect to the HTTPS port of our proxy with multiple streams let mut root_store = RootCertStore::empty(); root_store.add_parsable_certificates( CertificateDer::pem_file_iter(concat!( diff --git a/tests/integration/https_single_stream_download.rs b/tests/integration/https_single_stream_download.rs index 5a6ead9..4946036 100644 --- a/tests/integration/https_single_stream_download.rs +++ b/tests/integration/https_single_stream_download.rs @@ -124,7 +124,7 @@ async fn https_single_stream_download() { .await .expect("tcpip_forward failed"); - // 3. Connect to the HTTPS port of our proxy with out multiple streams + // 3. Connect to the HTTPS port of our proxy with a single stream let mut root_store = RootCertStore::empty(); root_store.add_parsable_certificates( CertificateDer::pem_file_iter(concat!( diff --git a/tests/integration/https_single_stream_upload.rs b/tests/integration/https_single_stream_upload.rs index 978a65a..5ffc51a 100644 --- a/tests/integration/https_single_stream_upload.rs +++ b/tests/integration/https_single_stream_upload.rs @@ -120,7 +120,7 @@ async fn https_single_stream_upload() { .await .expect("tcpip_forward failed"); - // 3. Connect to the HTTPS port of our proxy with out multiple streams + // 3. Connect to the HTTPS port of our proxy with a single stream let mut root_store = RootCertStore::empty(); root_store.add_parsable_certificates( CertificateDer::pem_file_iter(concat!(