diff --git a/Cargo.lock b/Cargo.lock index 8c15feda34..67e0a75474 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3238,6 +3238,8 @@ dependencies = [ "rand 0.8.5", "reqwest", "rustc-hash 1.1.0", + "rustls", + "rustls-platform-verifier", "serde", "serde_json", "strum", diff --git a/LICENSE-3rdparty.yml b/LICENSE-3rdparty.yml index 5634008119..1425aa8508 100644 --- a/LICENSE-3rdparty.yml +++ b/LICENSE-3rdparty.yml @@ -32028,9 +32028,9 @@ third_party_libraries: - package_name: stringmetrics package_version: 2.2.2 repository: https://github.com/pluots/stringmetrics - license: License specified in file ($CARGO_HOME/registry/src/index.crates.io-6f17d22bba15001f/stringmetrics-2.2.2/LICENSE) + license: License specified in file ($CARGO_HOME/registry/src/github.com-25cdd57fae9f0462/stringmetrics-2.2.2/LICENSE) licenses: - - license: License specified in file ($CARGO_HOME/registry/src/index.crates.io-6f17d22bba15001f/stringmetrics-2.2.2/LICENSE) + - license: License specified in file ($CARGO_HOME/registry/src/github.com-25cdd57fae9f0462/stringmetrics-2.2.2/LICENSE) text: | Copyright 2022 Trevor Gross diff --git a/libdd-profiling/Cargo.toml b/libdd-profiling/Cargo.toml index 53259d6215..2db00d782c 100644 --- a/libdd-profiling/Cargo.toml +++ b/libdd-profiling/Cargo.toml @@ -54,6 +54,7 @@ rand = "0.8" # The default resolver can hold locks or other global state that can cause deadlocks # or corruption when the process forks (e.g., in PHP-FPM or other forking environments). reqwest = { version = "0.13", features = ["multipart", "rustls", "hickory-dns"], default-features = false} +rustls-platform-verifier = "0.6" rustc-hash = { version = "1.1", default-features = false } serde = {version = "1.0", features = ["derive"]} serde_json = {version = "1.0"} @@ -63,6 +64,12 @@ tokio = {version = "1.23", features = ["rt", "macros", "net", "io-util", "fs"]} tokio-util = { version = "0.7.1", default-features = false } zstd = { version = "0.13", default-features = false } +# aws-lc-rs is preferred on Unix; ring is used on Windows where aws-lc-rs has build issues. +[target.'cfg(unix)'.dependencies] +rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs"] } + +[target.'cfg(not(unix))'.dependencies] +rustls = { version = "0.23", default-features = false, features = ["ring"] } [dev-dependencies] bolero = "0.13" diff --git a/libdd-profiling/src/exporter/mod.rs b/libdd-profiling/src/exporter/mod.rs index e2dce20b3b..14ca6b8201 100644 --- a/libdd-profiling/src/exporter/mod.rs +++ b/libdd-profiling/src/exporter/mod.rs @@ -5,6 +5,7 @@ pub mod config; mod errors; pub mod exporter_manager; mod profile_exporter; +mod tls; pub use errors::SendError; pub use exporter_manager::ExporterManager; diff --git a/libdd-profiling/src/exporter/profile_exporter.rs b/libdd-profiling/src/exporter/profile_exporter.rs index 7a7e0522c7..1cc812839b 100644 --- a/libdd-profiling/src/exporter/profile_exporter.rs +++ b/libdd-profiling/src/exporter/profile_exporter.rs @@ -65,6 +65,11 @@ impl ProfileExporter { /// The exporter can be used from any thread, but if using `send_blocking()`, the exporter /// should remain on the same thread for all blocking calls. See [`send_blocking`] for details. /// + /// # Performance + /// + /// TLS configuration is cached globally and reused across exporter + /// instances, avoiding repeated root store loading on Linux. + /// /// [`send_blocking`]: ProfileExporter::send_blocking pub fn new( profiling_library_name: &str, @@ -73,8 +78,7 @@ impl ProfileExporter { mut tags: Vec, endpoint: Endpoint, ) -> anyhow::Result { - let (builder, request_url) = endpoint.to_reqwest_client_builder()?; - + let tls_config = super::tls::cached_tls_config()?; // Pre-build all static headers let mut headers = reqwest::header::HeaderMap::new(); @@ -118,6 +122,9 @@ impl ProfileExporter { // Precompute the base tags string (includes configured tags + Azure App Services tags) let base_tags_string: String = tags.iter().flat_map(|tag| [tag.as_ref(), ","]).collect(); + let (builder, request_url) = endpoint.to_reqwest_client_builder()?; + let builder = builder.tls_backend_preconfigured(tls_config.0); + Ok(Self { client: builder.build()?, family: family.to_string(), diff --git a/libdd-profiling/src/exporter/tls.rs b/libdd-profiling/src/exporter/tls.rs new file mode 100644 index 0000000000..a9c4769d95 --- /dev/null +++ b/libdd-profiling/src/exporter/tls.rs @@ -0,0 +1,82 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Pre-initialized TLS configuration for high-performance profile export. +//! +//! On Linux, [`TlsConfig::new`] eagerly loads native root certificates via the +//! platform verifier, avoiding repeated expensive disk I/O on every +//! [`ProfileExporter`] creation. +//! +//! On macOS, the platform verifier's `Verifier::new()` is cheap (no cert +//! loading), but the actual Security.framework work happens lazily during each +//! TLS handshake. Creating a [`TlsConfig`] still avoids redundant `reqwest` +//! client setup on every exporter creation. +//! +//! # Fork Safety +//! +//! `TlsConfig` does **not** call Security.framework APIs directly, so it is +//! safe to create before `fork()`. Security.framework work is deferred to +//! each child's first TLS handshake. +//! +//! [`ProfileExporter`]: super::ProfileExporter + +/// Wraps a [`rustls::ClientConfig`] that has been pre-configured with the +/// platform certificate verifier. Clone is cheap (inner `Arc`). +#[derive(Clone)] +pub(crate) struct TlsConfig(pub(crate) rustls::ClientConfig); + +impl TlsConfig { + /// Create a new TLS configuration using the platform certificate verifier. + /// + /// On Linux, this eagerly loads the native root certificate store, which is + /// the expensive operation that was previously repeated on every + /// `ProfileExporter::new` call. + /// + /// On macOS, this is lightweight; the platform verifier defers + /// Security.framework calls to the first TLS handshake. + pub fn new() -> Result { + use rustls_platform_verifier::BuilderVerifierExt; + + // Use an explicit CryptoProvider rather than relying on + // `CryptoProvider::get_default_or_install_from_crate_features()`. + // Feature unification can enable both `aws-lc-rs` and `ring` in the + // same build (reqwest enables aws-lc-rs while libdd-common enables + // ring on Windows), which causes the automatic detection to panic. + let provider = rustls::crypto::CryptoProvider::get_default() + .cloned() + .unwrap_or_else(|| std::sync::Arc::new(Self::default_crypto_provider())); + + let config = rustls::ClientConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions()? + .with_platform_verifier()? + .with_no_client_auth(); + Ok(Self(config)) + } + + /// Returns the platform-appropriate default crypto provider. + /// + /// Matches the convention used by `libdd-common`: `aws-lc-rs` on Unix, + /// `ring` on Windows (where `aws-lc-rs` has issues). + fn default_crypto_provider() -> rustls::crypto::CryptoProvider { + #[cfg(unix)] + { + rustls::crypto::aws_lc_rs::default_provider() + } + #[cfg(not(unix))] + { + rustls::crypto::ring::default_provider() + } + } +} + +static TLS_CONFIG: std::sync::LazyLock> = + std::sync::LazyLock::new(|| { + TlsConfig::new().map_err(|err| format!("failed to initialize TLS configuration: {err}")) + }); + +pub(crate) fn cached_tls_config() -> anyhow::Result { + TLS_CONFIG + .as_ref() + .map(Clone::clone) + .map_err(|err| anyhow::anyhow!("{err}")) +}