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()