saluki_io/net/
ipc.rs

1use std::{
2    io::Cursor,
3    path::{Path, PathBuf},
4    sync::Arc,
5    time::Duration,
6};
7
8use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
9use hyper_util::client::legacy::connect::HttpConnector;
10use rustls::{
11    client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
12    crypto::CryptoProvider,
13    pki_types::{CertificateDer, ServerName, UnixTime},
14    version::TLS13,
15    CertificateError, ClientConfig, DigitallySignedStruct, ServerConfig, SignatureScheme,
16};
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 mut cert_reader = Cursor::new(&raw_cert_data);
123    let parsed_cert = rustls_pemfile::certs(&mut cert_reader)
124        .next()
125        .ok_or_else(|| generic_error!("No certificate found in file."))?
126        .with_error_context(|| format!("Failed to parse certificate file '{}'.", cert_path.as_ref().display()))?;
127
128    let mut key_reader = Cursor::new(&raw_cert_data);
129    let parsed_key = rustls_pemfile::private_key(&mut key_reader)
130        .with_error_context(|| format!("Failed to parse private key file '{}'.", cert_path.as_ref().display()))?
131        .ok_or_else(|| generic_error!("No private key found in file."))?;
132
133    // Build our client TLS configuration to use the parsed certificate for server verification, and then to send it for
134    // client authentication.
135    let crypto_provider = rustls::crypto::CryptoProvider::get_default()
136        .map(Arc::clone)
137        .ok_or_else(|| generic_error!("Default cryptography provider not yet installed."))?;
138    let agent_cert_verifier = Arc::new(DatadogAgentServerCertVerifier::from_certificate_and_provider(
139        parsed_cert.clone(),
140        crypto_provider,
141    ));
142
143    let tls_client_config = ClientConfig::builder_with_protocol_versions(&[&TLS13])
144        .dangerous()
145        .with_custom_certificate_verifier(agent_cert_verifier)
146        .with_client_auth_cert(vec![parsed_cert], parsed_key)
147        .with_error_context(|| {
148            format!(
149                "Failed to configure TLS client authentication with certificate/private key from '{}'.",
150                cert_path.as_ref().display()
151            )
152        })?;
153    Ok(tls_client_config)
154}
155
156/// Builds a server TLS configuration for the Datadog Agent's IPC endpoint.
157///
158/// This function reads the certificate file, parses the certificate and private key,
159/// and creates a server TLS configuration. The certificate path is expected to be a
160/// PEM-encoded file containing both the certificate and private key.
161pub async fn build_datadog_agent_server_tls_config<P: AsRef<Path>>(cert_path: P) -> Result<ServerConfig, GenericError> {
162    // Read the certificate file, and extract the certificate and private key from it.
163    let raw_cert_data = read_cert_file(
164        cert_path.as_ref(),
165        DEFAULT_CERT_READ_TIMEOUT,
166        DEFAULT_CERT_READ_INTERVAL,
167    )
168    .await?;
169
170    let mut cert_reader = Cursor::new(&raw_cert_data);
171    let parsed_cert = rustls_pemfile::certs(&mut cert_reader)
172        .next()
173        .ok_or_else(|| generic_error!("No certificate found in file."))?
174        .with_error_context(|| format!("Failed to parse certificate file '{}'.", cert_path.as_ref().display()))?;
175
176    let mut key_reader = Cursor::new(&raw_cert_data);
177    let parsed_key = rustls_pemfile::private_key(&mut key_reader)
178        .with_error_context(|| format!("Failed to parse private key file '{}'.", cert_path.as_ref().display()))?
179        .ok_or_else(|| generic_error!("No private key found in file."))?;
180
181    // Build the server TLS configuration
182    let tls_server_config = ServerConfig::builder()
183        .with_no_client_auth()
184        .with_single_cert(vec![parsed_cert], parsed_key)
185        .map_err(|e| generic_error!("Failed to configure TLS server: {}", e))?;
186
187    Ok(tls_server_config)
188}
189
190/// Reads a certificate file and retries up to a certain number of times with a certain wait duration between attempts.
191async fn read_cert_file(cert_path: &Path, timeout: Duration, interval: Duration) -> Result<Vec<u8>, GenericError> {
192    if timeout < interval {
193        return Err(generic_error!(
194            "Timeout is less than interval. Timeout: {}, Interval: {}",
195            timeout.as_secs(),
196            interval.as_secs()
197        ));
198    }
199
200    let start_time = std::time::Instant::now();
201    let mut last_error: String = String::new();
202    while start_time.elapsed() < timeout {
203        match tokio::fs::read(cert_path).await {
204            Ok(data) => return Ok(data),
205            Err(e) => {
206                last_error = e.to_string();
207                tokio::time::sleep(interval).await;
208            }
209        }
210    }
211    Err(generic_error!(
212        "Failed to read certificate file '{}' after {} seconds: {}",
213        cert_path.display(),
214        timeout.as_secs(),
215        last_error
216    ))
217}
218
219#[cfg(test)]
220mod tests {
221    use std::path::{Path, PathBuf};
222
223    use super::get_ipc_cert_file_path;
224
225    fn default_agent_auth_token_file_path() -> PathBuf {
226        PathBuf::from("/etc/datadog-agent/auth_token")
227    }
228
229    #[test]
230    fn ipc_cert_file_path_defaults() {
231        let default_auth_token_path = default_agent_auth_token_file_path();
232        let custom_auth_token_path = PathBuf::from("/secret/auth_token");
233        let invalid_auth_token_path = PathBuf::from("/");
234        let custom_ipc_cert_path = PathBuf::from("/tmp/custom_ipc_cert.pem");
235
236        // When the IPC cert file path is explicitly set, it should be used.
237        let result = get_ipc_cert_file_path(Some(&custom_ipc_cert_path), &default_auth_token_path);
238        assert_eq!(result, custom_ipc_cert_path);
239
240        // When the IPC cert file path is not set, it should default to the same directory as the auth token file using
241        // the default certificate file name.
242        let result = get_ipc_cert_file_path(None, &default_auth_token_path);
243        assert_eq!(result.parent(), default_auth_token_path.as_path().parent());
244        assert_eq!(result.file_name().and_then(|s| s.to_str()), Some("ipc_cert.pem"));
245
246        // This should hold when using a custom auth token file path as well.
247        let result = get_ipc_cert_file_path(None, &custom_auth_token_path);
248        assert_eq!(result.parent(), custom_auth_token_path.as_path().parent());
249        assert_eq!(result.file_name().and_then(|s| s.to_str()), Some("ipc_cert.pem"));
250
251        // If the auth token file path is somehow unset or invalid (e.g., no parent directory), we should use the same
252        // logic but with the default Datadog Agent configuration directory.
253        let result = get_ipc_cert_file_path(None, &invalid_auth_token_path);
254        assert_eq!(result.parent(), Some(Path::new("/etc/datadog-agent")));
255        assert_eq!(result.file_name().and_then(|s| s.to_str()), Some("ipc_cert.pem"));
256    }
257}