datadog_agent_commons/ipc/
config.rs

1//! IPC configuration.
2
3use std::{path::PathBuf, time::Duration};
4
5use backon::{BackoffBuilder, ConstantBuilder};
6use saluki_config::GenericConfiguration;
7use saluki_error::{ErrorContext as _, GenericError};
8use serde::Deserialize;
9use tonic::transport::Uri;
10#[cfg(not(target_os = "linux"))]
11use tracing::warn;
12
13use crate::platform::PlatformSettings;
14
15const DEFAULT_CMD_PORT: u16 = 5001;
16
17const fn default_cmd_port() -> u16 {
18    DEFAULT_CMD_PORT
19}
20
21const fn default_connect_retry_attempts() -> usize {
22    10
23}
24
25const fn default_grpc_max_message_size() -> usize {
26    128 * 1024 * 1024
27}
28
29const fn default_connect_retry_backoff() -> Duration {
30    Duration::from_secs(2)
31}
32
33/// Datadog Agent IPC authentication configuration.
34#[derive(Deserialize)]
35#[serde(default)]
36pub struct IpcAuthConfiguration {
37    /// Path to the Agent authentication token file.
38    ///
39    /// The contents of the file are passed as a bearer token in RPC requests to the IPC endpoint.
40    ///
41    /// Defaults to `<conf dir>/auth_token`, where `<conf dir>` is the platform-specific directory containing the Agent
42    /// configuration.
43    auth_token_file_path: PathBuf,
44
45    /// Path to the Agent IPC TLS certificate file.
46    ///
47    /// The file is expected to be PEM-encoded, containing both a certificate and private key. The certificate will be
48    /// used to verify the TLS server certificate presented by the Agent, and the certificate and private key will be
49    /// used together to provide client authentication _to_ the Agent.
50    ///
51    /// Defaults to `ipc_cert.pem` in the same directory as the Agent authentication token file. (for example, if
52    /// `auth_token_file_path` is `/etc/datadog-agent/auth_token`, this will be `/etc/datadog-agent/ipc_cert.pem`.)
53    ipc_cert_file_path: Option<PathBuf>,
54}
55
56impl IpcAuthConfiguration {
57    // Creates a new `IpcAuthConfiguration` from the given configuration.
58    ///
59    /// ## Errors
60    ///
61    /// If the configuration is invalid, an error is returned.
62    pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
63        config
64            .as_typed::<Self>()
65            .error_context("Failed to parse Datadog Agent IPC authentication configuration.")
66    }
67
68    /// Gets the path to the Agent authentication token file from the configuration.
69    pub fn auth_token_file_path(&self) -> PathBuf {
70        if self.auth_token_file_path.as_os_str().is_empty() {
71            return PlatformSettings::get_auth_token_path();
72        }
73
74        self.auth_token_file_path.clone()
75    }
76
77    /// Gets the IPC certificate file path from the configuration.
78    pub fn ipc_cert_file_path(&self) -> PathBuf {
79        // If the IPC cert file path is set explicitly, we always prefer that.
80        if let Some(path) = self.ipc_cert_file_path.as_ref() {
81            if !path.as_os_str().is_empty() {
82                return path.clone();
83            }
84        }
85
86        // Otherwise, we default to the same directory as the auth token file with the default certificate file name.
87        let auth_token_dir = if self.auth_token_file_path.as_os_str().is_empty() {
88            PlatformSettings::get_config_dir_path()
89        } else {
90            self.auth_token_file_path
91                .parent()
92                .unwrap_or(PlatformSettings::get_config_dir_path())
93        };
94
95        auth_token_dir.join(PlatformSettings::get_ipc_cert_filename())
96    }
97}
98
99impl Default for IpcAuthConfiguration {
100    fn default() -> Self {
101        Self {
102            auth_token_file_path: PlatformSettings::get_auth_token_path(),
103            ipc_cert_file_path: None,
104        }
105    }
106}
107
108/// Datadog Agent IPC client configuration.
109#[derive(Deserialize)]
110pub struct RemoteAgentClientConfiguration {
111    /// Core Agent CMD API port used for remote-agent gRPC IPC on localhost.
112    ///
113    /// Defaults to `5001`.
114    #[serde(default = "default_cmd_port")]
115    cmd_port: u16,
116
117    /// Authentication configuration for the IPC endpoint.
118    #[serde(flatten, default)]
119    auth: IpcAuthConfiguration,
120
121    /// Number of allowed retry attempts when initially connecting.
122    ///
123    /// Defaults to `10`.
124    #[serde(default = "default_connect_retry_attempts")]
125    connect_retry_attempts: usize,
126
127    /// Amount of time to wait between connection attempts when initially connecting.
128    ///
129    /// Defaults to 2 seconds.
130    #[serde(default = "default_connect_retry_backoff")]
131    connect_retry_backoff: Duration,
132
133    /// Maximum message size for gRPC messages.
134    ///
135    /// Defaults to `128 * 1024 * 1024` (128 MB).
136    #[serde(
137        rename = "agent_ipc_grpc_max_message_size",
138        default = "default_grpc_max_message_size"
139    )]
140    grpc_max_message_size: usize,
141
142    /// vsock address for connecting to the Agent IPC endpoint via AF_VSOCK.
143    ///
144    /// When set, the IPC client connects over a vsock socket using the resolved CID with the port
145    /// taken from the configured endpoint. This mirrors the Datadog Agent's `vsock_addr`
146    /// configuration, enabling communication from within a guest VM (for example, Nitro Enclaves)
147    /// to an Agent process running on the host or hypervisor.
148    ///
149    /// Accepted values:
150    /// - `host`: connect to the host (CID 2, `VMADDR_CID_HOST`)
151    /// - `hypervisor`: connect to the hypervisor (CID 0, `VMADDR_CID_HYPERVISOR`)
152    /// - `local`: connect to the local VM (CID 3, `VMADDR_CID_LOCAL`)
153    ///
154    /// Defaults to unset (TCP connection).
155    #[cfg(target_os = "linux")]
156    #[serde(default, deserialize_with = "deserialize_vsock_addr")]
157    vsock_addr: Option<u32>,
158
159    // Non-Linux: capture raw value solely to emit a warning when configured.
160    #[cfg(not(target_os = "linux"))]
161    #[serde(default)]
162    vsock_addr: String,
163}
164
165#[cfg(target_os = "linux")]
166fn deserialize_vsock_addr<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
167where
168    D: serde::Deserializer<'de>,
169{
170    use serde::de::Error as _;
171    match Option::<String>::deserialize(deserializer)?.as_deref() {
172        None | Some("") => Ok(None),
173        Some("host") => Ok(Some(2)),       // VMADDR_CID_HOST
174        Some("hypervisor") => Ok(Some(0)), // VMADDR_CID_HYPERVISOR
175        Some("local") => Ok(Some(3)),      // VMADDR_CID_LOCAL
176        Some(other) => Err(D::Error::custom(format!(
177            "invalid vsock address '{}'; expected one of: host, hypervisor, local",
178            other
179        ))),
180    }
181}
182
183impl RemoteAgentClientConfiguration {
184    /// Creates a new `RemoteAgentClientConfiguration` from the given configuration.
185    ///
186    /// ## Errors
187    ///
188    /// If the configuration is invalid, an error is returned.
189    pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
190        let this = config
191            .as_typed::<Self>()
192            .error_context("Failed to parse Datadog Agent IPC client configuration.")?;
193
194        #[cfg(not(target_os = "linux"))]
195        if !this.vsock_addr.is_empty() {
196            warn!("`vsock_addr` is configured but vsock is only supported on Linux. Setting will be ignored.");
197        }
198
199        Ok(this)
200    }
201
202    /// Returns a reference to the authentication configuration for the Remote Agent client.
203    pub fn auth(&self) -> &IpcAuthConfiguration {
204        &self.auth
205    }
206
207    /// Returns the Core Agent CMD API gRPC endpoint URI.
208    pub fn endpoint(&self) -> Result<Uri, GenericError> {
209        format!("https://127.0.0.1:{}", self.cmd_port)
210            .parse::<Uri>()
211            .with_error_context(|| format!("failed to build URI from cmd_port {}", self.cmd_port))
212    }
213
214    /// Returns the maximum message size for gRPC.
215    pub fn grpc_max_message_size(&self) -> usize {
216        self.grpc_max_message_size
217    }
218
219    /// Returns the vsock address to use for connecting to the IPC endpoint, if configured.
220    ///
221    /// Combines the CID from `vsock_addr` with the port resolved from `endpoint()`. Returns
222    /// an error if `vsock_addr` is set but the endpoint has no explicit port.
223    ///
224    /// # Errors
225    ///
226    /// If the configured endpoint has no explicit port.
227    #[cfg(target_os = "linux")]
228    pub fn vsock_addr(&self) -> Result<Option<tokio_vsock::VsockAddr>, GenericError> {
229        let Some(cid) = self.vsock_addr else {
230            return Ok(None);
231        };
232        let port = self
233            .endpoint()?
234            .port_u16()
235            .map(u32::from)
236            .ok_or_else(|| saluki_error::generic_error!("vsock requires an explicit port in the IPC endpoint"))?;
237        Ok(Some(tokio_vsock::VsockAddr::new(cid, port)))
238    }
239}
240
241impl BackoffBuilder for &RemoteAgentClientConfiguration {
242    type Backoff = <ConstantBuilder as BackoffBuilder>::Backoff;
243
244    fn build(self) -> Self::Backoff {
245        ConstantBuilder::default()
246            .with_delay(self.connect_retry_backoff)
247            .with_max_times(self.connect_retry_attempts)
248            .build()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use std::path::{Path, PathBuf};
255
256    use saluki_config::ConfigurationLoader;
257
258    use super::RemoteAgentClientConfiguration;
259    use crate::platform::PlatformSettings;
260
261    async fn get_remote_agent_config(
262        ipc_cert_file_path: Option<&Path>, auth_token_file_path: Option<&Path>,
263    ) -> RemoteAgentClientConfiguration {
264        // Set the values in the config map if provided, then defer to the shared loader helper so both
265        // entry points build the configuration the exact same way.
266        let mut values = serde_json::Map::new();
267        if let Some(path) = ipc_cert_file_path {
268            values.insert(
269                "ipc_cert_file_path".to_string(),
270                path.to_string_lossy().into_owned().into(),
271            );
272        }
273        if let Some(path) = auth_token_file_path {
274            values.insert(
275                "auth_token_file_path".to_string(),
276                path.to_string_lossy().into_owned().into(),
277            );
278        }
279
280        config_from_values(values).await
281    }
282
283    #[tokio::test]
284    async fn ipc_cert_file_path_empty_config() {
285        let default_auth_token_path = PlatformSettings::get_auth_token_path();
286
287        // When the auth token file path _and_ IPC cert file path are both unset, we should default to looking for the
288        // IPC cert in the same directory as the auth token.
289        let config = get_remote_agent_config(None, None).await;
290        assert_eq!(
291            config.auth().ipc_cert_file_path().parent(),
292            default_auth_token_path.as_path().parent()
293        );
294        assert_eq!(
295            config.auth().ipc_cert_file_path().file_name().map(Path::new),
296            Some(PlatformSettings::get_ipc_cert_filename())
297        );
298    }
299
300    #[tokio::test]
301    async fn ipc_cert_file_path_defaults() {
302        let default_auth_token_path = PlatformSettings::get_auth_token_path();
303
304        // When the IPC cert file path is not set, it should default to the same directory as the auth token file using
305        // the default certificate file name.
306        let config = get_remote_agent_config(None, Some(&default_auth_token_path)).await;
307        assert_eq!(
308            config.auth().ipc_cert_file_path().parent(),
309            default_auth_token_path.as_path().parent()
310        );
311        assert_eq!(
312            config.auth().ipc_cert_file_path().file_name().map(Path::new),
313            Some(PlatformSettings::get_ipc_cert_filename())
314        );
315    }
316
317    #[tokio::test]
318    async fn ipc_cert_file_path_explicitly_set() {
319        let default_auth_token_path = PlatformSettings::get_auth_token_path();
320        let custom_ipc_cert_path = PathBuf::from("/tmp/custom_ipc_cert.pem");
321
322        // When the IPC cert file path is explicitly set, it should be used.
323        let config = get_remote_agent_config(Some(&custom_ipc_cert_path), Some(&default_auth_token_path)).await;
324        assert_eq!(custom_ipc_cert_path, config.auth().ipc_cert_file_path());
325    }
326
327    #[tokio::test]
328    async fn ipc_cert_file_path_custom_auth_token_path() {
329        let custom_auth_token_path = PathBuf::from("/secret/auth_token");
330
331        // When the IPC cert file path is not set, but there's a custom auth token path (explicitly set, different from the default),
332        // we should still look in the same directory as the auth token file using the default certificate file name.
333        let config = get_remote_agent_config(None, Some(&custom_auth_token_path)).await;
334        assert_eq!(
335            config.auth().ipc_cert_file_path().parent(),
336            custom_auth_token_path.as_path().parent()
337        );
338        assert_eq!(
339            config.auth().ipc_cert_file_path().file_name().map(Path::new),
340            Some(PlatformSettings::get_ipc_cert_filename())
341        );
342    }
343
344    #[tokio::test]
345    async fn ipc_cert_file_path_invalid_auth_token_path() {
346        let invalid_auth_token_path = PathBuf::from("/");
347
348        // If the auth token file path is somehow unset or invalid (for example, no parent directory), we should use the same
349        // logic but with the default Datadog Agent configuration directory.
350        let config = get_remote_agent_config(None, Some(&invalid_auth_token_path)).await;
351        assert_eq!(
352            config.auth().ipc_cert_file_path().parent(),
353            Some(PlatformSettings::get_config_dir_path())
354        );
355        assert_eq!(
356            config.auth().ipc_cert_file_path().file_name().map(Path::new),
357            Some(PlatformSettings::get_ipc_cert_filename())
358        );
359    }
360
361    async fn config_from_values(values: serde_json::Map<String, serde_json::Value>) -> RemoteAgentClientConfiguration {
362        let (base_config, _) =
363            ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
364        RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
365    }
366
367    #[tokio::test]
368    async fn endpoint_defaults_to_port_5001() {
369        let config = config_from_values(serde_json::Map::new()).await;
370        assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:5001/");
371    }
372
373    #[tokio::test]
374    async fn endpoint_uses_cmd_port() {
375        let mut values = serde_json::Map::new();
376        values.insert("cmd_port".to_string(), 7777.into());
377        let config = config_from_values(values).await;
378        assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:7777/");
379    }
380
381    #[cfg(target_os = "linux")]
382    #[tokio::test]
383    async fn vsock_addr_valid_values() {
384        // (vsock_addr input, expected CID — port always comes from cmd_port=5001)
385        let cases: &[(&str, Option<u32>)] = &[
386            ("", None),
387            ("host", Some(2)),
388            ("hypervisor", Some(0)),
389            ("local", Some(3)),
390        ];
391
392        for (input, expected_cid) in cases {
393            let mut values = serde_json::Map::new();
394            values.insert("vsock_addr".to_string(), (*input).into());
395            values.insert("cmd_port".to_string(), 5001u16.into());
396            let config = config_from_values(values).await;
397            let result = config
398                .vsock_addr()
399                .expect("vsock_addr() should not error with cmd_port set");
400            assert_eq!(result.map(|a| a.cid()), *expected_cid, "input: {input:?}");
401        }
402    }
403
404    #[cfg(target_os = "linux")]
405    #[tokio::test]
406    async fn vsock_addr_invalid_values() {
407        let cases = &["invalid", "2", "HOST", "host ", "vm0"];
408
409        for input in cases {
410            let mut values = serde_json::Map::new();
411            values.insert("vsock_addr".to_string(), (*input).into());
412            let (base_config, _) =
413                ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
414            assert!(
415                RemoteAgentClientConfiguration::from_configuration(&base_config).is_err(),
416                "expected error for input: {input:?}",
417            );
418        }
419    }
420}