From 7af7bdc16a41d64d58743d15023a63b3eed2102c Mon Sep 17 00:00:00 2001 From: KKRainbow <5665404+KKRainbow@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:22:32 +0800 Subject: [PATCH] build: consolidate dependencies and select rustls ring explicitly (#2648) * build(core): consolidate crypto dependencies and trim features Align AES-GCM, ChaCha20Poly1305, HMAC, SHA2 and HKDF with the versions already required by Snow and STUN. Align base64 with pbjson, and explain the coordinated upgrade constraints beside the manifest entries. Adapt AEAD and HMAC calls without changing packet or key formats, and pin the WireGuard client key derivation with an independent HKDF vector. Limit Snow to the Noise algorithms used by the core. Use crossbeam-utils directly, keep the futures executor in tests, and remove UUID fast-rng from the core while retaining existing features in native consumers. Enable browser entropy and certificate time for the rustls backend. Core default normal/build dependencies fall from 255 to 224 packages; duplicated package names fall from 24 to 5 against upstream 4837468d. Validation: Docker tests pass: 970 core, 33 proto (2 ignored), 5 WireGuard, and TCP/UDP three-node encrypted relays. Clippy passes with warnings denied for core, proto, native and web targets. Browser default/ChaCha20, WASI and minimal native compile checks pass. Workspace formatting passes. * 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 * ci: remove the separate rustls backend regression step Keep the HTTPS discovery regression in the existing test archive and runner. Avoid an extra core test build solely to enable both rustls backends; that combination was verified locally before the TLS fix. * build(web): share reqwest 0.13 with the OIDC client Disable the reqwest 0.12 client bundled with openidconnect/oauth2 and use the official oauth2-reqwest adapter around the workspace reqwest 0.13 client. Pin the pre-release adapter version until its API stabilizes. Keep the existing redirect policy and 30-second timeout. Initialize the ring provider when constructing OIDC configuration, including outside the web application entry point. Exercise discovery, JWKS fetching, token success and OAuth error responses against a local mock provider. Verify redirects are not followed. Refresh Cargo.lock to remove reqwest 0.12 without unrelated upgrades. Validation: Docker OIDC tests (2) and webhook tests (13) pass. Clippy with warnings denied for native/core/proto/web all targets and full features passes, as does workspace formatting. The default core/web dependency graph has one reqwest version and 45 multi-version names instead of 46; the TLS runtime still selects ring only. --- Cargo.lock | 148 +++++------------- Cargo.toml | 13 +- easytier-contrib/easytier-ohrs/Cargo.toml | 2 +- easytier-core/Cargo.toml | 35 +++-- .../src/connectivity/hole_punch/udp/server.rs | 2 +- .../manual/discovery/implementation.rs | 51 +++++- .../src/gateway/dataplane/resource.rs | 2 +- easytier-core/src/gateway/port_forward.rs | 2 +- .../src/gateway/proxy/tcp_proxy_engine.rs | 2 +- easytier-core/src/peers/conn/peer.rs | 2 +- easytier-core/src/peers/conn/peer_conn.rs | 2 +- easytier-core/src/peers/conn/peer_session.rs | 2 +- easytier-core/src/peers/context.rs | 2 +- .../src/peers/peer_center/instance.rs | 2 +- .../src/peers/route/peer_ospf_route.rs | 2 +- easytier-core/src/tunnel/encrypt/aes_gcm.rs | 14 +- easytier-core/src/tunnel/encrypt/chacha20.rs | 11 +- easytier-core/src/tunnel/secure_datagram.rs | 2 +- easytier-gui/src-tauri/src/lib.rs | 1 + easytier-proto/src/peer_rpc.rs | 2 +- easytier-web/Cargo.toml | 12 +- easytier-web/src/main.rs | 1 + easytier-web/src/restful/oidc.rs | 131 +++++++++++++++- easytier-web/src/webhook.rs | 4 + easytier/Cargo.toml | 6 +- easytier/src/core.rs | 1 + easytier/src/tunnel/websocket.rs | 31 ++-- easytier/src/utils/mod.rs | 15 ++ easytier/src/vpn_portal/wireguard.rs | 9 ++ 29 files changed, 326 insertions(+), 183 deletions(-) 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()