saluki_io/net/
ipc.rs

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