From 6154b550c1860a145c78b4d8a4894ad3e7c5a1b6 Mon Sep 17 00:00:00 2001 From: KKRainbow <5665404+KKRainbow@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:14:40 +0800 Subject: [PATCH] fix(tls): select ring explicitly across application entry points Building easytier and easytier-web together enabled both ring and aws-lc through reqwest 0.13. HTTPS endpoint discovery used rustls automatic provider selection and could panic before sending a ClientHello. Use reqwest rustls-no-provider to share ring, and explicitly install the process default before starting CLI, GUI and web services. Initialize it for standalone webhook construction too, retaining any provider already selected by the host. Pass ring directly to HTTPS discovery and WebSocket TLS builders so library use is independent of application initialization order. Keep existing certificate verification, SNI and protocol settings. Add a duplex-stream regression that reproduces the original panic with both backends enabled, and run it in CI. Explain provider selection and feature-unification constraints next to the relevant code. Validation: the new regression fails before the fix and passes after it. Docker tests pass: 10 discovery, 13 webhook, 2 WebSocket, WSS three-node AES-GCM relay, and WSS credential connectivity. Clippy with warnings denied and fmt pass. GUI, browser, WASI, minimal native, endpoint-only and WebSocket-only compile checks pass. Linux and Windows release dependency trees contain only the ring runtime backend. Fixes #2607 --- .github/workflows/test.yml | 7 +++ .../manual/discovery/implementation.rs | 51 +++++++++++++++++-- easytier-gui/src-tauri/src/lib.rs | 1 + easytier-web/Cargo.toml | 5 +- easytier-web/src/main.rs | 1 + easytier-web/src/webhook.rs | 4 ++ easytier/Cargo.toml | 1 + easytier/src/core.rs | 1 + easytier/src/tunnel/websocket.rs | 31 +++++------ easytier/src/utils/mod.rs | 15 ++++++ 10 files changed, 98 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 19388d28..b0c8bcb4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -125,6 +125,13 @@ jobs: - uses: taiki-e/install-action@nextest + - name: Test HTTPS discovery with both rustls backends + # Catch provider-selection panics even when release features use only ring. + run: >- + cargo test --locked --package easytier-core --lib + --features rustls/aws_lc_rs + https_fetch_reaches_tls_handshake_without_global_provider + - name: Archive test run: >- cargo nextest archive --archive-file tests.tar.zst diff --git a/easytier-core/src/connectivity/manual/discovery/implementation.rs b/easytier-core/src/connectivity/manual/discovery/implementation.rs index 17a2aafa..6029dfb8 100644 --- a/easytier-core/src/connectivity/manual/discovery/implementation.rs +++ b/easytier-core/src/connectivity/manual/discovery/implementation.rs @@ -187,9 +187,16 @@ where let root_store = rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), }; - let tls_config = rustls::ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); + // A host application can enable another rustls backend through Cargo + // feature unification. Select ring explicitly instead of requiring a + // process-wide provider to have been initialized first (#2607). + let tls_config = rustls::ClientConfig::builder_with_provider(Arc::new( + rustls::crypto::ring::default_provider(), + )) + .with_safe_default_protocol_versions() + .context("selecting HTTPS protocol versions failed")? + .with_root_certificates(root_store) + .with_no_client_auth(); let stream = TlsConnector::from(Arc::new(tls_config)) .connect(server_name, socket) .await @@ -631,6 +638,44 @@ mod tests { assert_eq!(options.bind.context.socket_mark, Some(9)); } + #[tokio::test] + async fn https_fetch_reaches_tls_handshake_without_global_provider() { + // Also run with rustls/aws_lc_rs enabled: feature unification must not + // make HTTPS discovery panic before it can send a ClientHello (#2607). + let (client, mut server) = tokio::io::duplex(8192); + let host = Arc::new(HttpTestHost { + stream: Mutex::new(Some(client)), + connects: Mutex::new(Vec::new()), + }); + let dns = HttpTestDns { + queries: Mutex::new(Vec::new()), + }; + let server_task = tokio::spawn(async move { + let mut record_header = [0; 5]; + server.read_exact(&mut record_header).await.unwrap(); + assert_eq!(record_header[0], 22, "expected a TLS handshake record"); + // Close without replying; discovery should report a handshake + // error rather than panic while selecting a crypto provider. + }); + + let error = fetch_http_discovery( + host, + &dns, + HttpDiscoveryRequest { + url: "https://discovery.example/lookup".parse().unwrap(), + user_agent: "easytier/test".to_owned(), + network_name: "test-network".to_owned(), + timeout: Duration::from_secs(5), + ip_version: IpVersion::V4, + tcp_bind: TcpBindOptions::default(), + }, + ) + .await + .expect_err("server closed during the TLS handshake"); + assert!(format!("{error:#}").contains("HTTPS handshake failed")); + server_task.await.unwrap(); + } + #[test] fn http_discovery_interprets_redirect_and_body_forms() { let query = resolve_http_endpoint(HttpDiscoveryResponse { diff --git a/easytier-gui/src-tauri/src/lib.rs b/easytier-gui/src-tauri/src/lib.rs index 64bdbff6..fd0b106c 100644 --- a/easytier-gui/src-tauri/src/lib.rs +++ b/easytier-gui/src-tauri/src/lib.rs @@ -1443,6 +1443,7 @@ pub fn run_gui() -> std::process::ExitCode { } setup_panic_handler(); + easytier::utils::init_crypto_provider(); let mut builder = tauri::Builder::default(); diff --git a/easytier-web/Cargo.toml b/easytier-web/Cargo.toml index 048fe99c..93be0a3f 100644 --- a/easytier-web/Cargo.toml +++ b/easytier-web/Cargo.toml @@ -75,7 +75,10 @@ uuid = { workspace = true, features = [ chrono = { workspace = true, features = ["serde"] } openidconnect = { version = "4.0", default-features = false, features = ["accept-rfc3339-timestamps", "reqwest"] } -reqwest = { workspace = true, features = ["json", "rustls"] } +# Reuse EasyTier's ring provider. reqwest 0.13's rustls feature adds aws-lc, +# making rustls backend selection ambiguous when core/web are built together. +# Initialize the process provider before constructing a reqwest client. +reqwest = { workspace = true, features = ["json", "rustls-no-provider"] } subtle = "2.6" mimalloc.workspace = true diff --git a/easytier-web/src/main.rs b/easytier-web/src/main.rs index 8db874b9..78d9dbd4 100644 --- a/easytier-web/src/main.rs +++ b/easytier-web/src/main.rs @@ -303,6 +303,7 @@ async fn main() { let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); rust_i18n::set_locale(&locale); setup_panic_handler(); + easytier::utils::init_crypto_provider(); let cli = Cli::parse(); log::init_with_default_console_targets(&cli, false, &["CORE", "easytier_web"]).unwrap(); diff --git a/easytier-web/src/webhook.rs b/easytier-web/src/webhook.rs index 050ba1ba..29859674 100644 --- a/easytier-web/src/webhook.rs +++ b/easytier-web/src/webhook.rs @@ -281,6 +281,10 @@ impl WebhookConfig { web_instance_id: Option, web_instance_api_base_url: Option, ) -> Self { + // This constructor is also used without main (for example in tests). + // reqwest's rustls-no-provider needs a process default even if ring + // is the only compiled backend. + easytier::utils::init_crypto_provider(); WebhookConfig { webhook_url, webhook_secret, diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index fa4e4c07..7e071eb3 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -416,6 +416,7 @@ upnp = [ ] endpoint-discovery = [ "dns-resolver", + "dep:rustls", "easytier-core/endpoint-discovery", ] dhcp-ipv4 = ["easytier-core/dhcp-ipv4"] diff --git a/easytier/src/core.rs b/easytier/src/core.rs index 2dcf3cae..f2adf9e3 100644 --- a/easytier/src/core.rs +++ b/easytier/src/core.rs @@ -1733,6 +1733,7 @@ pub async fn main() -> ExitCode { let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); rust_i18n::set_locale(&locale); setup_panic_handler(); + crate::utils::init_crypto_provider(); #[cfg(target_os = "windows")] match windows_service::service_dispatcher::start(String::new(), ffi_service_main) { diff --git a/easytier/src/tunnel/websocket.rs b/easytier/src/tunnel/websocket.rs index 8cc7c7f3..bfd80aa7 100644 --- a/easytier/src/tunnel/websocket.rs +++ b/easytier/src/tunnel/websocket.rs @@ -182,17 +182,15 @@ impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { } } -fn init_crypto_provider() { - let _ = - rustls::crypto::CryptoProvider::install_default(rustls::crypto::ring::default_provider()); -} - fn get_insecure_tls_client_config() -> rustls::ClientConfig { - init_crypto_provider(); - let provider = rustls::crypto::CryptoProvider::get_default().unwrap(); - let mut config = rustls::ClientConfig::builder() + // Library callers may not have initialized a process-wide provider. + // Use the same explicit backend for TLS and signature verification. + let provider = Arc::new(rustls::crypto::ring::default_provider()); + let mut config = rustls::ClientConfig::builder_with_provider(provider.clone()) + .with_safe_default_protocol_versions() + .expect("ring supports the default TLS protocol versions") .dangerous() - .with_custom_certificate_verifier(SkipServerVerification::new(provider.clone())) + .with_custom_certificate_verifier(SkipServerVerification::new(provider)) .with_no_client_auth(); config.enable_sni = true; config.enable_early_data = false; @@ -220,12 +218,16 @@ where let peer_addr = stream.peer_addr()?; let mut remote_url = socket_url(local_url.scheme(), peer_addr); let stream = if is_wss(&local_url)? { - init_crypto_provider(); let (certificates, private_key) = get_insecure_tls_cert(); - let config = rustls::ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(certificates, private_key) - .with_context(|| "Failed to create server config")?; + // Do not rely on another tunnel initializing the global provider. + let config = rustls::ServerConfig::builder_with_provider(Arc::new( + rustls::crypto::ring::default_provider(), + )) + .with_safe_default_protocol_versions() + .with_context(|| "Failed to select TLS protocol versions")? + .with_no_client_auth() + .with_single_cert(certificates, private_key) + .with_context(|| "Failed to create server config")?; Either::Left(TlsAcceptor::from(Arc::new(config)).accept(stream).await?) } else { Either::Right(stream) @@ -390,7 +392,6 @@ where let client = ClientBuilder::from_uri(http::Uri::try_from(remote_url.to_string()).unwrap()) .max_headers(128); let stream: MaybeTlsStream = if is_wss { - init_crypto_provider(); let tls = tokio_rustls::TlsConnector::from(Arc::new(get_insecure_tls_client_config())); let sni = remote_url.domain().unwrap_or("localhost").to_owned(); let server_name = rustls::pki_types::ServerName::try_from(sni) diff --git a/easytier/src/utils/mod.rs b/easytier/src/utils/mod.rs index d9deb752..ceb6085a 100644 --- a/easytier/src/utils/mod.rs +++ b/easytier/src/utils/mod.rs @@ -9,6 +9,21 @@ use std::sync::{Arc, Weak}; #[cfg(feature = "management")] pub type PeerRoutePair = crate::proto::api::instance::PeerRoutePair; +/// Select the process-wide TLS backend before starting application services. +pub fn init_crypto_provider() { + #[cfg(any( + feature = "endpoint-discovery", + feature = "quic", + feature = "websocket" + ))] + { + // Cargo features are additive: another dependency can enable aws-lc + // alongside ring, making rustls's automatic selection panic (#2607). + // Keep an existing provider chosen by an embedding application. + let _ = rustls::crypto::ring::default_provider().install_default(); + } +} + pub fn check_tcp_available(port: u16) -> bool { let s = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port); TcpListener::bind(s).is_ok()