diff --git a/Cargo.lock b/Cargo.lock index 2ac9903c..24187a03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,25 +75,11 @@ dependencies = [ "aead 0.5.2", "aes 0.8.4", "cipher 0.4.4", - "ctr 0.9.2", - "ghash 0.5.1", + "ctr", + "ghash", "subtle", ] -[[package]] -name = "aes-gcm" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f" -dependencies = [ - "aead 0.6.1", - "aes 0.9.3", - "cipher 0.5.2", - "ctr 0.10.1", - "ctutils", - "ghash 0.6.0", -] - [[package]] name = "ahash" version = "0.8.12" @@ -1957,15 +1943,6 @@ dependencies = [ "cipher 0.4.4", ] -[[package]] -name = "ctr" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" -dependencies = [ - "cipher 0.5.2", -] - [[package]] name = "ctutils" version = "0.4.2" @@ -2537,7 +2514,7 @@ dependencies = [ "atomic-shim", "atomic-write-file", "atomic_refcell", - "base64 0.23.1", + "base64 0.22.1", "bon", "boringtun-easytier", "bytecodec", @@ -2570,7 +2547,7 @@ dependencies = [ "hickory-proto", "hickory-resolver", "hickory-server", - "hkdf 0.13.0", + "hkdf", "http", "http-body-util", "humansize", @@ -2609,7 +2586,7 @@ dependencies = [ "serde_json", "serial_test", "service-manager", - "sha2 0.11.0", + "sha2 0.10.9", "shellexpand", "smoltcp", "socket2", @@ -2663,7 +2640,7 @@ dependencies = [ name = "easytier-core" version = "2.7.0" dependencies = [ - "aes-gcm 0.11.1", + "aes-gcm", "anyhow", "arc-swap", "ariadne", @@ -2671,22 +2648,22 @@ dependencies = [ "async-trait", "atomic-shim", "auto_impl", - "base64 0.23.1", + "base64 0.22.1", "bitflags 2.13.2", "bon", "bytecodec", "bytes", - "chacha20poly1305 0.11.0", + "chacha20poly1305 0.10.1", "chrono", "cidr", - "crossbeam", + "crossbeam-utils", "dashmap", "easytier-proto", "futures", "getrandom 0.2.17", "getrandom 0.3.4", "guarden", - "hmac 0.13.0", + "hmac 0.12.1", "http-body-util", "hyper", "hyper-util", @@ -2704,9 +2681,10 @@ dependencies = [ "rand 0.8.8", "ring", "rustls", + "rustls-pki-types", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.10.9", "smoltcp", "snow", "strum 0.28.0", @@ -2811,7 +2789,7 @@ dependencies = [ name = "easytier-ohos-features" version = "0.1.0" dependencies = [ - "base64 0.23.1", + "base64 0.22.1", "easytier", "flate2", "gethostname", @@ -2858,11 +2836,11 @@ dependencies = [ "anyhow", "async-trait", "auto_impl", - "base64 0.23.1", + "base64 0.22.1", "bytes", "chrono", "cidr", - "hmac 0.13.0", + "hmac 0.12.1", "indoc", "pbjson", "pbjson-build", @@ -2872,10 +2850,10 @@ dependencies = [ "prost-types 0.14.4", "prost-wkt-types", "quote", - "reqwest 0.13.5", + "reqwest", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.10.9", "thiserror 2.0.20", "tokio", "url", @@ -2924,7 +2902,7 @@ dependencies = [ "axum-embed", "axum-login", "axum-messages", - "base64 0.23.1", + "base64 0.22.1", "chrono", "cidr", "clap", @@ -2937,18 +2915,19 @@ dependencies = [ "imageproc", "maxminddb", "mimalloc", + "oauth2-reqwest", "openidconnect", "password-auth", "prost 0.14.4", "rand 0.8.8", - "reqwest 0.13.5", + "reqwest", "rust-embed", "rust-i18n", "sea-orm", "sea-orm-migration", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.10.9", "sqlx", "subtle", "sys-locale", @@ -3025,7 +3004,7 @@ dependencies = [ "ff", "generic-array", "group", - "hkdf 0.12.4", + "hkdf", "pem-rfc7468", "pkcs8", "rand_core 0.6.4", @@ -3778,16 +3757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", - "polyval 0.6.2", -] - -[[package]] -name = "ghash" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" -dependencies = [ - "polyval 0.7.3", + "polyval", ] [[package]] @@ -4297,15 +4267,6 @@ dependencies = [ "hmac 0.12.1", ] -[[package]] -name = "hkdf" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" -dependencies = [ - "hmac 0.13.0", -] - [[package]] name = "hmac" version = "0.12.1" @@ -6070,7 +6031,6 @@ dependencies = [ "getrandom 0.2.17", "http", "rand 0.8.8", - "reqwest 0.12.28", "serde", "serde_json", "serde_path_to_error", @@ -6079,6 +6039,16 @@ dependencies = [ "url", ] +[[package]] +name = "oauth2-reqwest" +version = "0.1.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234fb5c965bbce983ee5de636a7a51d6a3223da8067ea02f9ab2d2d78ac08be2" +dependencies = [ + "oauth2", + "reqwest", +] + [[package]] name = "objc2" version = "0.6.4" @@ -7001,17 +6971,6 @@ dependencies = [ "universal-hash 0.5.1", ] -[[package]] -name = "polyval" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" -dependencies = [ - "cpubits", - "cpufeatures 0.3.1", - "universal-hash 0.6.1", -] - [[package]] name = "portable-atomic" version = "1.15.0" @@ -7743,38 +7702,6 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-http 0.6.11", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "reqwest" version = "0.13.5" @@ -8979,12 +8906,9 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "599b506ccc4aff8cf7844bc42cf783009a434c1e26c964432560fb6d6ad02d82" dependencies = [ - "aes-gcm 0.10.3", - "blake2 0.10.6", "chacha20poly1305 0.10.1", "curve25519-dalek 4.1.3", "getrandom 0.3.4", - "ring", "rustc_version", "sha2 0.10.9", "subtle", @@ -9182,7 +9106,7 @@ dependencies = [ "futures-util", "generic-array", "hex", - "hkdf 0.12.4", + "hkdf", "hmac 0.12.1", "itoa", "log", @@ -9225,7 +9149,7 @@ dependencies = [ "futures-core", "futures-util", "hex", - "hkdf 0.12.4", + "hkdf", "hmac 0.12.1", "home", "itoa", @@ -9633,7 +9557,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest 0.13.5", + "reqwest", "serde", "serde_json", "serde_repr", diff --git a/Cargo.toml b/Cargo.toml index c56c03dd..55ed4580 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,8 @@ async-trait = "0.1.92" atomic-shim = "0.2.0" auto_impl = "1.3.0" axum = "0.8" -base64 = "0.23" +# Match pbjson 0.9's base64 dependency; upgrade together to avoid two codecs. +base64 = "0.22.1" bon = "3.10.1" bytecodec = "0.5.0" bytes = "1.12.1" @@ -37,6 +38,7 @@ chrono = "0.4.45" cidr = "0.3.2" clap = "4.6.7" crossbeam = "0.8.5" +crossbeam-utils = "0.8.23" dashmap = "6.2.1" easytier = { version = "2.7.0", path = "easytier", default-features = false } easytier-core = { version = "2.7.0", path = "easytier-core", default-features = false } @@ -44,10 +46,12 @@ easytier-ffi = { version = "0.1.0", path = "easytier-contrib/easytier-ffi", defa easytier-ohos-core = { version = "0.1.0", path = "easytier-contrib/easytier-ohrs/crates/easytier-ohos-core", default-features = false } easytier-ohos-features = { version = "0.1.0", path = "easytier-contrib/easytier-ohrs/crates/easytier-ohos-features", default-features = false } easytier-proto = { version = "2.7.0", path = "easytier-proto", default-features = false } -futures = "0.3" +futures = { version = "0.3", default-features = false } gethostname = "1.1" guarden = "0.3" -hmac = "0.13.0" +# Keep HMAC/SHA2 on digest 0.10, shared by stun_codec 0.4 and snow 0.10. +# Upgrade this crypto family together when those upstream constraints move. +hmac = "0.12.1" http-body-util = "0.1" hyper = { version = "1", default-features = false } hyper-util = { version = "0.1", default-features = false } @@ -71,7 +75,8 @@ sea-orm = "1.1.20" sea-orm-migration = "1.1.20" serde = "1.0.229" serde_json = "1.0" -sha2 = "0.11.0" +# See the HMAC constraint above; SHA2 must use the same digest generation. +sha2 = "0.10.9" smoltcp = { version = "0.14.0", default-features = false } sqlx = "0.8.6" strum = "0.28.0" diff --git a/easytier-contrib/easytier-ohrs/Cargo.toml b/easytier-contrib/easytier-ohrs/Cargo.toml index 342ba877..ec57793a 100644 --- a/easytier-contrib/easytier-ohrs/Cargo.toml +++ b/easytier-contrib/easytier-ohrs/Cargo.toml @@ -20,7 +20,7 @@ easytier-proto = { workspace = true, features = [ "core", "json-rpc", ] } -futures.workspace = true +futures = { workspace = true, features = ["default"] } napi-derive-ohos = "1.1" napi-ohos = { version = "1.1", default-features = false, features = [ "serde-json", diff --git a/easytier-core/Cargo.toml b/easytier-core/Cargo.toml index 19cf1705..fde05de2 100644 --- a/easytier-core/Cargo.toml +++ b/easytier-core/Cargo.toml @@ -30,11 +30,11 @@ bytecodec.workspace = true bytes.workspace = true chrono = { workspace = true, features = ["clock"] } cidr = { workspace = true, features = ["serde"] } -crossbeam.workspace = true +crossbeam-utils.workspace = true dashmap.workspace = true bon.workspace = true easytier-proto = { workspace = true, features = ["core"] } -futures.workspace = true +futures = { workspace = true, features = ["std", "async-await"] } guarden.workspace = true hmac.workspace = true http-body-util = { workspace = true, optional = true } @@ -58,6 +58,14 @@ serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sha2.workspace = true smoltcp = { workspace = true, optional = true } +# All core Noise handshakes use 25519_ChaChaPoly_SHA256. Snow's std feature +# also enables unused ring and BLAKE2 dependencies, so use its alloc support. +snow = { version = "0.10.0", default-features = false, features = [ + "use-chacha20poly1305", + "use-sha2", + "use-curve25519", + "use-getrandom", +] } stun_codec.workspace = true thiserror.workspace = true tracing.workspace = true @@ -74,25 +82,25 @@ tokio-util = { workspace = true, features = ["io", "rt"] } tokio-rustls = { workspace = true, optional = true } url = { workspace = true, features = ["serde"] } wildmatch = "2.6.1" -uuid = { workspace = true, features = ["v4", "fast-rng", "serde"] } +uuid = { workspace = true, features = ["v4", "serde"] } webpki-roots = { version = "1.0", optional = true } x25519-dalek = { workspace = true, features = ["static_secrets"] } zerocopy = { workspace = true, features = ["derive", "simd"] } zstd = { version = "0.14", optional = true } -aes-gcm = { version = "0.11.1", optional = true } -chacha20poly1305 = { version = "0.11.0", optional = true } +# Match snow 0.10's AEAD generation to share aead, cipher and crypto-common. +# Upgrade together with snow rather than pulling in a second crypto stack. +aes-gcm = { version = "0.10.3", optional = true } +chacha20poly1305 = { version = "0.10.1", optional = true } openssl = { version = "0.10", optional = true, features = ["vendored"] } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] getrandom-02 = { package = "getrandom", version = "0.2.17", features = ["js"] } getrandom-03 = { package = "getrandom", version = "0.3.4", features = ["wasm_js"] } -snow = { version = "0.10.0", default-features = false, features = ["default-resolver", "default-resolver-crypto"] } +ring = { version = "0.17", features = ["wasm32_unknown_unknown_js"], optional = true } +rustls-pki-types = { version = "1.15.1", features = ["web"], optional = true } uuid = { workspace = true, features = ["js"] } wasm-bindgen = "0.2" -[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] -snow = "0.10.0" - [features] default = ["aes-gcm", "endpoint-discovery", "extended-services", "management", "tcp-hole-punch"] aes-gcm = ["dep:aes-gcm"] @@ -106,7 +114,9 @@ endpoint-discovery = [ "dep:http-body-util", "dep:hyper", "dep:hyper-util", + "dep:ring", "dep:rustls", + "dep:rustls-pki-types", "dep:tokio-rustls", "dep:webpki-roots", ] @@ -152,12 +162,17 @@ test-utils = [] tracing-log = ["tracing/log"] zstd = ["dep:zstd"] +[dev-dependencies] +futures = { workspace = true, features = ["executor"] } + [target.'cfg(not(target_os = "wasi"))'.dev-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "test-util"] } [package.metadata.cargo-machete] ignored = [ - # Enable browser entropy backends for transitive rand/snow dependencies. + # Enable browser entropy backends for transitive rand/snow/AEAD dependencies. "getrandom-02", "getrandom-03", + # Enable browser time for rustls certificate validation. + "rustls-pki-types", ] diff --git a/easytier-core/src/connectivity/hole_punch/udp/server.rs b/easytier-core/src/connectivity/hole_punch/udp/server.rs index 5da74fb8..082266ee 100644 --- a/easytier-core/src/connectivity/hole_punch/udp/server.rs +++ b/easytier-core/src/connectivity/hole_punch/udp/server.rs @@ -8,7 +8,7 @@ use std::{ }; use anyhow::Context; -use crossbeam::atomic::AtomicCell; +use crossbeam_utils::atomic::AtomicCell; use quanta::Instant; use rand::{Rng, seq::SliceRandom as _}; use tokio::{ 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-core/src/gateway/dataplane/resource.rs b/easytier-core/src/gateway/dataplane/resource.rs index a06b9e95..7f8ca459 100644 --- a/easytier-core/src/gateway/dataplane/resource.rs +++ b/easytier-core/src/gateway/dataplane/resource.rs @@ -6,7 +6,7 @@ use std::{ sync::{Arc, Mutex}, }; -use crossbeam::atomic::AtomicCell; +use crossbeam_utils::atomic::AtomicCell; use tokio::sync::Notify; use tokio_util::sync::CancellationToken; diff --git a/easytier-core/src/gateway/port_forward.rs b/easytier-core/src/gateway/port_forward.rs index 00b10be3..eda8df7c 100644 --- a/easytier-core/src/gateway/port_forward.rs +++ b/easytier-core/src/gateway/port_forward.rs @@ -9,7 +9,7 @@ use std::{ time::Duration, }; -use crossbeam::atomic::AtomicCell; +use crossbeam_utils::atomic::AtomicCell; use dashmap::DashMap; use quanta::Instant; use tokio::{ diff --git a/easytier-core/src/gateway/proxy/tcp_proxy_engine.rs b/easytier-core/src/gateway/proxy/tcp_proxy_engine.rs index b4cd785d..298fe9e1 100644 --- a/easytier-core/src/gateway/proxy/tcp_proxy_engine.rs +++ b/easytier-core/src/gateway/proxy/tcp_proxy_engine.rs @@ -8,7 +8,7 @@ use std::{ }; use cidr::Ipv4Inet; -use crossbeam::atomic::AtomicCell; +use crossbeam_utils::atomic::AtomicCell; use dashmap::{DashMap, mapref::entry::Entry}; use smoltcp::wire::{IpAddress, IpProtocol, Ipv4Packet, TcpPacket}; diff --git a/easytier-core/src/peers/conn/peer.rs b/easytier-core/src/peers/conn/peer.rs index 7ab3435d..d60fd693 100644 --- a/easytier-core/src/peers/conn/peer.rs +++ b/easytier-core/src/peers/conn/peer.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use arc_swap::ArcSwapOption; -use crossbeam::atomic::AtomicCell; +use crossbeam_utils::atomic::AtomicCell; use dashmap::{DashMap, DashSet}; use parking_lot::{Mutex, RwLock}; diff --git a/easytier-core/src/peers/conn/peer_conn.rs b/easytier-core/src/peers/conn/peer_conn.rs index 072e8707..f03bc217 100644 --- a/easytier-core/src/peers/conn/peer_conn.rs +++ b/easytier-core/src/peers/conn/peer_conn.rs @@ -1,5 +1,5 @@ use arc_swap::ArcSwapOption; -use crossbeam::atomic::AtomicCell; +use crossbeam_utils::atomic::AtomicCell; use futures::{StreamExt, TryFutureExt}; use std::{ any::Any, diff --git a/easytier-core/src/peers/conn/peer_session.rs b/easytier-core/src/peers/conn/peer_session.rs index 2c822f75..cd8c57ca 100644 --- a/easytier-core/src/peers/conn/peer_session.rs +++ b/easytier-core/src/peers/conn/peer_session.rs @@ -5,7 +5,7 @@ use std::sync::{ use std::time::{Duration, Instant}; use anyhow::anyhow; -use crossbeam::atomic::AtomicCell; +use crossbeam_utils::atomic::AtomicCell; use dashmap::DashMap; use crate::peers::util::shrink_dashmap; diff --git a/easytier-core/src/peers/context.rs b/easytier-core/src/peers/context.rs index e1757af4..a259730e 100644 --- a/easytier-core/src/peers/context.rs +++ b/easytier-core/src/peers/context.rs @@ -16,7 +16,7 @@ use easytier_proto::{ common::{FlagsInConfig, PeerFeatureFlag, SecureModeConfig, StunInfo, TunnelInfo}, peer_rpc::{PeerGroupInfo, TrustedCredentialPubkeyProof}, }; -use hmac::{Hmac, KeyInit, Mac}; +use hmac::{Hmac, Mac}; use sha2::Sha256; pub use crate::config::{NetworkIdentity, NetworkSecretDigest}; diff --git a/easytier-core/src/peers/peer_center/instance.rs b/easytier-core/src/peers/peer_center/instance.rs index 2e8525d0..89293311 100644 --- a/easytier-core/src/peers/peer_center/instance.rs +++ b/easytier-core/src/peers/peer_center/instance.rs @@ -4,7 +4,7 @@ use std::{ time::{Duration, Instant}, }; -use crossbeam::atomic::AtomicCell; +use crossbeam_utils::atomic::AtomicCell; use futures::Future; use std::sync::RwLock; use tokio::sync::Mutex; diff --git a/easytier-core/src/peers/route/peer_ospf_route.rs b/easytier-core/src/peers/route/peer_ospf_route.rs index e3b669fa..9973c65e 100644 --- a/easytier-core/src/peers/route/peer_ospf_route.rs +++ b/easytier-core/src/peers/route/peer_ospf_route.rs @@ -12,7 +12,7 @@ use std::{ use arc_swap::ArcSwap; use atomic_shim::AtomicU64; use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr, Ipv6Inet}; -use crossbeam::atomic::AtomicCell; +use crossbeam_utils::atomic::AtomicCell; use dashmap::DashMap; use ordered_hash_map::OrderedHashMap; use parking_lot::{RwLock, lock_api::RwLockUpgradableReadGuard}; diff --git a/easytier-core/src/tunnel/encrypt/aes_gcm.rs b/easytier-core/src/tunnel/encrypt/aes_gcm.rs index 3297dc54..bf914ac1 100644 --- a/easytier-core/src/tunnel/encrypt/aes_gcm.rs +++ b/easytier-core/src/tunnel/encrypt/aes_gcm.rs @@ -1,4 +1,4 @@ -use aes_gcm::{AeadInOut, Aes128Gcm, Aes256Gcm, Key, KeyInit}; +use aes_gcm::{AeadInPlace, Aes128Gcm, Aes256Gcm, Key, KeyInit}; use rand::{RngCore, rngs::OsRng}; use zerocopy::{AsBytes, FromBytes}; @@ -54,16 +54,16 @@ impl Encryptor for AesGcmCipher { let tag = aes_tail.tag.into(); let rs = match &self.cipher { - AesGcmEnum::AES128GCM(aes_gcm) => aes_gcm.decrypt_inout_detached( + AesGcmEnum::AES128GCM(aes_gcm) => aes_gcm.decrypt_in_place_detached( &nonce, &[], - (&mut zc_packet.mut_payload()[..text_len]).into(), + &mut zc_packet.mut_payload()[..text_len], &tag, ), - AesGcmEnum::AES256GCM(aes_gcm) => aes_gcm.decrypt_inout_detached( + AesGcmEnum::AES256GCM(aes_gcm) => aes_gcm.decrypt_in_place_detached( &nonce, &[], - (&mut zc_packet.mut_payload()[..text_len]).into(), + &mut zc_packet.mut_payload()[..text_len], &tag, ), }; @@ -113,7 +113,7 @@ impl Encryptor for AesGcmCipher { nonce.into() }); ( - aes_gcm.encrypt_inout_detached(&nonce, &[], zc_packet.mut_payload().into()), + aes_gcm.encrypt_in_place_detached(&nonce, &[], zc_packet.mut_payload()), nonce, ) } @@ -124,7 +124,7 @@ impl Encryptor for AesGcmCipher { nonce.into() }); ( - aes_gcm.encrypt_inout_detached(&nonce, &[], zc_packet.mut_payload().into()), + aes_gcm.encrypt_in_place_detached(&nonce, &[], zc_packet.mut_payload()), nonce, ) } diff --git a/easytier-core/src/tunnel/encrypt/chacha20.rs b/easytier-core/src/tunnel/encrypt/chacha20.rs index 9da04ab8..6c50e717 100644 --- a/easytier-core/src/tunnel/encrypt/chacha20.rs +++ b/easytier-core/src/tunnel/encrypt/chacha20.rs @@ -1,4 +1,4 @@ -use chacha20poly1305::{AeadInOut, ChaCha20Poly1305, Key, KeyInit}; +use chacha20poly1305::{AeadInPlace, ChaCha20Poly1305, Key, KeyInit}; use rand::{RngCore, rngs::OsRng}; use zerocopy::{AsBytes, FromBytes}; @@ -42,12 +42,7 @@ impl Encryptor for ChaCha20Cipher { let tag = tail.tag.into(); self.cipher - .decrypt_inout_detached( - &nonce, - &[], - (&mut zc_packet.mut_payload()[..text_len]).into(), - &tag, - ) + .decrypt_in_place_detached(&nonce, &[], &mut zc_packet.mut_payload()[..text_len], &tag) .map_err(|_| Error::DecryptionFailed)?; let pm_header = zc_packet.mut_peer_manager_header().unwrap(); @@ -89,7 +84,7 @@ impl Encryptor for ChaCha20Cipher { let tag = self .cipher - .encrypt_inout_detached(&nonce, &[], zc_packet.mut_payload().into()) + .encrypt_in_place_detached(&nonce, &[], zc_packet.mut_payload()) .map_err(|_| Error::EncryptionFailed)?; let tail = StandardAeadTail { diff --git a/easytier-core/src/tunnel/secure_datagram.rs b/easytier-core/src/tunnel/secure_datagram.rs index ba20bb65..db118a07 100644 --- a/easytier-core/src/tunnel/secure_datagram.rs +++ b/easytier-core/src/tunnel/secure_datagram.rs @@ -8,7 +8,7 @@ use std::{ use anyhow::anyhow; use atomic_shim::AtomicU64; -use hmac::{Hmac, KeyInit as _, Mac as _}; +use hmac::{Hmac, Mac as _}; use rand::RngCore as _; use sha2::Sha256; use zerocopy::FromBytes; 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-proto/src/peer_rpc.rs b/easytier-proto/src/peer_rpc.rs index 79252d48..93a8dae1 100644 --- a/easytier-proto/src/peer_rpc.rs +++ b/easytier-proto/src/peer_rpc.rs @@ -1,4 +1,4 @@ -use hmac::{Hmac, KeyInit, Mac}; +use hmac::{Hmac, Mac}; use prost::Message; use sha2::Sha256; #[cfg(feature = "api")] diff --git a/easytier-web/Cargo.toml b/easytier-web/Cargo.toml index f158ba47..14231dc3 100644 --- a/easytier-web/Cargo.toml +++ b/easytier-web/Cargo.toml @@ -16,7 +16,7 @@ tokio-util = { workspace = true, features = ["rt"] } dashmap.workspace = true url.workspace = true async-trait.workspace = true -futures.workspace = true +futures = { workspace = true, features = ["default"] } prost.workspace = true maxminddb = "0.32" @@ -74,8 +74,14 @@ 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"] } +# Keep the bundled reqwest 0.12 client disabled; use the official 0.13 adapter. +openidconnect = { version = "4.0", default-features = false, features = ["accept-rfc3339-timestamps"] } +# Pin the adapter until its API stabilizes. +oauth2-reqwest = "=0.1.0-alpha.3" +# 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/restful/oidc.rs b/easytier-web/src/restful/oidc.rs index 3229c42a..b111c715 100644 --- a/easytier-web/src/restful/oidc.rs +++ b/easytier-web/src/restful/oidc.rs @@ -1,4 +1,4 @@ -use openidconnect::reqwest; +use oauth2_reqwest::ReqwestClient; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; @@ -180,7 +180,7 @@ pub struct OidcConfig { pub scopes: Vec, pub pkce_enabled: bool, pub frontend_base_url: Option, - pub http_client: Option, + pub http_client: Option, cached_client: Option>, } @@ -224,10 +224,14 @@ impl OidcConfig { if oidc_username_claim.trim().is_empty() { return Err(anyhow::anyhow!("--oidc-username-claim cannot be empty")); } - let http_client = reqwest::ClientBuilder::new() - .redirect(reqwest::redirect::Policy::none()) - .timeout(Duration::from_secs(30)) - .build()?; + // OIDC configuration can also be created outside the web entry point. + easytier::utils::init_crypto_provider(); + let http_client = ReqwestClient::from( + reqwest::ClientBuilder::new() + .redirect(reqwest::redirect::Policy::none()) + .timeout(Duration::from_secs(30)) + .build()?, + ); let issuer_url = oidc_issuer_url.ok_or_else(|| { anyhow::anyhow!("--oidc-issuer-url is required when using OIDC authentication") @@ -697,6 +701,121 @@ mod route { mod tests { use super::*; + #[tokio::test] + async fn reqwest_adapter_supports_discovery_and_token_exchange() { + use axum::{ + Form, Json, + http::{HeaderMap, StatusCode}, + response::{IntoResponse, Redirect}, + routing::post, + }; + use openidconnect::{AuthorizationCode, OAuth2TokenResponse, RequestTokenError}; + use serde_json::json; + use std::sync::atomic::{AtomicUsize, Ordering}; + use tokio_util::task::AbortOnDropHandle; + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let issuer = format!("http://{}", listener.local_addr().unwrap()); + let metadata = json!({ + "issuer": issuer, + "authorization_endpoint": format!("{issuer}/authorize"), + "token_endpoint": format!("{issuer}/token"), + "jwks_uri": format!("{issuer}/jwks"), + "response_types_supported": ["code"], + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": ["RS256"], + }); + let redirect_hits = Arc::new(AtomicUsize::new(0)); + let hits = redirect_hits.clone(); + let app = Router::new() + .route( + "/.well-known/openid-configuration", + get(move || async move { Json(metadata) }), + ) + .route("/jwks", get(|| async { Json(json!({ "keys": [] })) })) + .route( + "/token", + post( + |headers: HeaderMap, Form(form): Form>| async move { + assert_eq!( + headers["authorization"], + "Basic dGVzdC1jbGllbnQ6dGVzdC1zZWNyZXQ=" + ); + assert_eq!(form["grant_type"], "authorization_code"); + assert_eq!(form["redirect_uri"], "http://localhost/callback"); + match form["code"].as_str() { + "valid" => Json(json!({ + "access_token": "test-access-token", + "token_type": "Bearer", + "expires_in": 3600, + })) + .into_response(), + "redirect" => Redirect::temporary("/unexpected").into_response(), + _ => ( + StatusCode::BAD_REQUEST, + Json(json!({ "error": "invalid_grant" })), + ) + .into_response(), + } + }, + ), + ) + .route( + "/unexpected", + post(move || async move { + hits.fetch_add(1, Ordering::SeqCst); + Json(json!({ "access_token": "unexpected", "token_type": "Bearer" })) + }), + ); + let _server = AbortOnDropHandle::new(tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + })); + + let config = OidcConfig::from_params(OidcOptions { + oidc_issuer_url: Some(issuer), + oidc_client_id: Some("test-client".to_owned()), + oidc_client_secret: Some("test-secret".to_owned()), + oidc_username_claim: "preferred_username".to_owned(), + oidc_scopes: vec!["openid".to_owned()], + oidc_redirect_url: Some("http://localhost/callback".to_owned()), + oidc_disable_pkce: false, + oidc_frontend_base_url: None, + }) + .await + .unwrap(); + let client = config.client().unwrap(); + let http_client = config.http_client.as_ref().unwrap(); + let token = client + .exchange_code(AuthorizationCode::new("valid".to_owned())) + .unwrap() + .request_async(http_client) + .await + .unwrap(); + assert_eq!(token.access_token().secret(), "test-access-token"); + assert_eq!(token.expires_in(), Some(Duration::from_secs(3600))); + + let error = client + .exchange_code(AuthorizationCode::new("invalid".to_owned())) + .unwrap() + .request_async(http_client) + .await + .unwrap_err(); + assert!( + matches!(error, RequestTokenError::ServerResponse(ref response) + if response.error() == &CoreErrorResponseType::InvalidGrant) + ); + + assert!( + client + .exchange_code(AuthorizationCode::new("redirect".to_owned())) + .unwrap() + .request_async(http_client) + .await + .is_err() + ); + assert_eq!(redirect_hits.load(Ordering::SeqCst), 0); + } + #[test] fn test_dot_path_to_json_pointer() { use serde_json::json; 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 d04a3a3d..7e071eb3 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -61,7 +61,7 @@ strum = { workspace = true, features = ["derive"] } gethostname.workspace = true -futures = { workspace = true, features = ["bilock", "unstable"] } +futures = { workspace = true, features = ["default", "bilock", "unstable"] } tokio = { workspace = true, features = [ "fs", @@ -178,7 +178,8 @@ network-interface = "2.0.5" # for wireguard boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true } -hkdf = { version = "0.13", optional = true } +# Share the workspace HMAC/SHA2 digest generation for WireGuard key derivation. +hkdf = { version = "0.12.4", optional = true } sha2 = { workspace = true, optional = true } # for cli @@ -415,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() diff --git a/easytier/src/vpn_portal/wireguard.rs b/easytier/src/vpn_portal/wireguard.rs index 768466a3..5fa52c1f 100644 --- a/easytier/src/vpn_portal/wireguard.rs +++ b/easytier/src/vpn_portal/wireguard.rs @@ -395,6 +395,15 @@ mod tests { fn named_wireguard_keys_are_stable_and_client_scoped() { let master = [7; 32]; let client = derive_named_key(&master, b"wireguard-client", "laptop").unwrap(); + // Pin the derived key across crypto dependency upgrades/downgrades. + // Independently calculated with RFC 5869 HKDF-SHA256. + assert_eq!( + client, + [ + 5, 98, 244, 32, 245, 111, 41, 24, 163, 149, 201, 218, 22, 228, 8, 224, 134, 16, + 173, 29, 62, 138, 202, 41, 172, 230, 189, 237, 207, 100, 51, 236, + ] + ); assert_eq!( client, derive_named_key(&master, b"wireguard-client", "laptop").unwrap()