saluki_io/net/
ipc.rs

1use std::{
2    path::{Path, PathBuf},
3    sync::Arc,
4    time::Duration,
5};
6
7use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
8use hyper_util::client::legacy::connect::HttpConnector;
9use rustls::{
10    client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
11    crypto::CryptoProvider,
12    pki_types::{CertificateDer, ServerName, UnixTime},
13    version::TLS13,
14    CertificateError, ClientConfig, DigitallySignedStruct, ServerConfig, SignatureScheme,
15};
16use rustls_pki_types::{pem::PemObject as _, PrivateKeyDer};
17use saluki_error::{generic_error, ErrorContext as _, GenericError};
18
19const DEFAULT_DATADOG_AGENT_CONFIG_DIR: &str = "/etc/datadog-agent";
20const DEFAULT_IPC_CERT_FILE_NAME: &str = "ipc_cert.pem";
21const DEFAULT_CERT_READ_TIMEOUT: Duration = Duration::from_secs(20);
22const DEFAULT_CERT_READ_INTERVAL: Duration = Duration::from_millis(100);
23
24/// Gets the IPC certificate file path from the configuration.
25///
26/// This function uses the same logic as the RemoteAgentClient to determine the certificate path,
27/// ensuring consistency between the client and server TLS configurations.
28pub fn get_ipc_cert_file_path(ipc_cert_file_path: Option<&PathBuf>, auth_token_file_path: &Path) -> PathBuf {
29    // If the IPC cert file path is set explicitly, we always prefer that.
30    if let Some(path) = ipc_cert_file_path {
31        if !path.as_os_str().is_empty() {
32            return path.clone();
33        }
34    }
35
36    // Otherwise, we default to the same directory as the auth token file with the default certificate file name.
37    let mut cert_path = auth_token_file_path
38        .parent()
39        .map(|p| p.to_path_buf())
40        .unwrap_or_else(|| PathBuf::from(DEFAULT_DATADOG_AGENT_CONFIG_DIR));
41
42    cert_path.push(DEFAULT_IPC_CERT_FILE_NAME);
43    cert_path
44}
45
46/// Builds an HTTPS connector for connecting to the Datadog Agent's IPC endpoint.
47///
48/// This connector is configured to load a bundled TLS certificate/private key file, generated by the Datadog Agent, and
49/// use it for both server verification and client authentication. The given certificate path is expected to be a
50/// PEM-encoded file containing both the certificate and private key. The private key is only used for client
51/// authentication.
52pub async fn build_datadog_agent_ipc_https_connector<P: AsRef<Path>>(
53    cert_path: P,
54) -> Result<HttpsConnector<HttpConnector>, GenericError> {
55    let tls_client_config = build_datadog_agent_client_ipc_tls_config(cert_path).await?;
56    let mut http_connector = HttpConnector::new();
57    http_connector.enforce_http(false);
58
59    Ok(HttpsConnectorBuilder::new()
60        .with_tls_config(tls_client_config)
61        .https_only()
62        .enable_http2()
63        .wrap_connector(http_connector))
64}
65
66#[derive(Debug)]
67struct DatadogAgentServerCertVerifier {
68    cert: CertificateDer<'static>,
69    provider: Arc<CryptoProvider>,
70}
71
72impl DatadogAgentServerCertVerifier {
73    fn from_certificate_and_provider(cert: CertificateDer<'static>, provider: Arc<CryptoProvider>) -> Self {
74        Self { cert, provider }
75    }
76}
77
78impl ServerCertVerifier for DatadogAgentServerCertVerifier {
79    fn verify_server_cert(
80        &self, end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &ServerName<'_>,
81        _ocsp_response: &[u8], _now: UnixTime,
82    ) -> Result<ServerCertVerified, rustls::Error> {
83        // We only care about if the server certificate matches the one we have.
84        //
85        // This explicitly ignores things like the server using a CA certificate as an end-entity certificate and all of
86        // that. We just want to verify that the server certificate is the one we expect.
87        if end_entity != &self.cert {
88            return Err(rustls::Error::InvalidCertificate(CertificateError::UnknownIssuer));
89        }
90
91        Ok(ServerCertVerified::assertion())
92    }
93
94    fn verify_tls12_signature(
95        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
96    ) -> Result<HandshakeSignatureValid, rustls::Error> {
97        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
98    }
99
100    fn verify_tls13_signature(
101        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
102    ) -> Result<HandshakeSignatureValid, rustls::Error> {
103        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
104    }
105
106    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
107        self.provider.signature_verification_algorithms.supported_schemes()
108    }
109}
110
111pub async fn build_datadog_agent_client_ipc_tls_config<P: AsRef<Path>>(
112    cert_path: P,
113) -> Result<ClientConfig, GenericError> {
114    // Read the certificate file, and extract the certificate and private key from it.
115    let raw_cert_data = read_cert_file(
116        cert_path.as_ref(),
117        DEFAULT_CERT_READ_TIMEOUT,
118        DEFAULT_CERT_READ_INTERVAL,
119    )
120    .await?;
121
122    let parsed_cert = CertificateDer::from_pem_slice(&raw_cert_data[..])
123        .with_error_context(|| format!("Failed to parse certificate file '{}'.", cert_path.as_ref().display()))?;
124
125    let parsed_key = PrivateKeyDer::from_pem_slice(&raw_cert_data[..])
126        .with_error_context(|| format!("Failed to parse private key file '{}'.", cert_path.as_ref().display()))?;
127
128    // Build our client TLS configuration to use the parsed certificate for server verification, and then to send it for
129    // client authentication.
130    let crypto_provider = rustls::crypto::CryptoProvider::get_default()
131        .map(Arc::clone)
132        .ok_or_else(|| generic_error!("Default cryptography provider not yet installed."))?;
133    let agent_cert_verifier = Arc::new(DatadogAgentServerCertVerifier::from_certificate_and_provider(
134        parsed_cert.clone(),
135        crypto_provider,
136    ));
137
138    let tls_client_config = ClientConfig::builder_with_protocol_versions(&[&TLS13])
139        .dangerous()
140        .with_custom_certificate_verifier(agent_cert_verifier)
141        .with_client_auth_cert(vec![parsed_cert], parsed_key)
142        .with_error_context(|| {
143            format!(
144                "Failed to configure TLS client authentication with certificate/private key from '{}'.",
145                cert_path.as_ref().display()
146            )
147        })?;
148    Ok(tls_client_config)
149}
150
151/// Builds a server TLS configuration for the Datadog Agent's IPC endpoint.
152///
153/// This function reads the certificate file, parses the certificate and private key,
154/// and creates a server TLS configuration. The certificate path is expected to be a
155/// PEM-encoded file containing both the certificate and private key.
156pub async fn build_datadog_agent_server_tls_config<P: AsRef<Path>>(cert_path: P) -> Result<ServerConfig, GenericError> {
157    // Read the certificate file, and extract the certificate and private key from it.
158    let raw_cert_data = read_cert_file(
159        cert_path.as_ref(),
160        DEFAULT_CERT_READ_TIMEOUT,
161        DEFAULT_CERT_READ_INTERVAL,
162    )
163    .await?;
164
165    let parsed_cert = CertificateDer::from_pem_slice(&raw_cert_data[..])
166        .with_error_context(|| format!("Failed to parse certificate file '{}'.", cert_path.as_ref().display()))?;
167
168    let parsed_key = PrivateKeyDer::from_pem_slice(&raw_cert_data[..])
169        .with_error_context(|| format!("Failed to parse private key file '{}'.", cert_path.as_ref().display()))?;
170
171    // Build the server TLS configuration
172    let tls_server_config = ServerConfig::builder()
173        .with_no_client_auth()
174        .with_single_cert(vec![parsed_cert], parsed_key)
175        .map_err(|e| generic_error!("Failed to configure TLS server: {}", e))?;
176
177    Ok(tls_server_config)
178}
179
180/// Reads a certificate file and retries up to a certain number of times with a certain wait duration between attempts.
181async fn read_cert_file(cert_path: &Path, timeout: Duration, interval: Duration) -> Result<Vec<u8>, GenericError> {
182    if timeout < interval {
183        return Err(generic_error!(
184            "Timeout is less than interval. Timeout: {}, Interval: {}",
185            timeout.as_secs(),
186            interval.as_secs()
187        ));
188    }
189
190    let start_time = std::time::Instant::now();
191    let mut last_error: String = String::new();
192    while start_time.elapsed() < timeout {
193        match tokio::fs::read(cert_path).await {
194            Ok(data) => return Ok(data),
195            Err(e) => {
196                last_error = e.to_string();
197                tokio::time::sleep(interval).await;
198            }
199        }
200    }
201    Err(generic_error!(
202        "Failed to read certificate file '{}' after {} seconds: {}",
203        cert_path.display(),
204        timeout.as_secs(),
205        last_error
206    ))
207}
208
209#[cfg(test)]
210mod tests {
211    use std::path::{Path, PathBuf};
212
213    use super::get_ipc_cert_file_path;
214
215    fn default_agent_auth_token_file_path() -> PathBuf {
216        PathBuf::from("/etc/datadog-agent/auth_token")
217    }
218
219    #[test]
220    fn ipc_cert_file_path_defaults() {
221        let default_auth_token_path = default_agent_auth_token_file_path();
222        let custom_auth_token_path = PathBuf::from("/secret/auth_token");
223        let invalid_auth_token_path = PathBuf::from("/");
224        let custom_ipc_cert_path = PathBuf::from("/tmp/custom_ipc_cert.pem");
225
226        // When the IPC cert file path is explicitly set, it should be used.
227        let result = get_ipc_cert_file_path(Some(&custom_ipc_cert_path), &default_auth_token_path);
228        assert_eq!(result, custom_ipc_cert_path);
229
230        // When the IPC cert file path is not set, it should default to the same directory as the auth token file using
231        // the default certificate file name.
232        let result = get_ipc_cert_file_path(None, &default_auth_token_path);
233        assert_eq!(result.parent(), default_auth_token_path.as_path().parent());
234        assert_eq!(result.file_name().and_then(|s| s.to_str()), Some("ipc_cert.pem"));
235
236        // This should hold when using a custom auth token file path as well.
237        let result = get_ipc_cert_file_path(None, &custom_auth_token_path);
238        assert_eq!(result.parent(), custom_auth_token_path.as_path().parent());
239        assert_eq!(result.file_name().and_then(|s| s.to_str()), Some("ipc_cert.pem"));
240
241        // If the auth token file path is somehow unset or invalid (e.g., no parent directory), we should use the same
242        // logic but with the default Datadog Agent configuration directory.
243        let result = get_ipc_cert_file_path(None, &invalid_auth_token_path);
244        assert_eq!(result.parent(), Some(Path::new("/etc/datadog-agent")));
245        assert_eq!(result.file_name().and_then(|s| s.to_str()), Some("ipc_cert.pem"));
246    }
247}