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