Skip to main content

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.
265        let mut values = serde_json::Map::new();
266        if let Some(path) = ipc_cert_file_path {
267            values.insert(
268                "ipc_cert_file_path".to_string(),
269                path.to_string_lossy().into_owned().into(),
270            );
271        }
272        if let Some(path) = auth_token_file_path {
273            values.insert(
274                "auth_token_file_path".to_string(),
275                path.to_string_lossy().into_owned().into(),
276            );
277        }
278
279        let (base_config, _) =
280            ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
281        RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
282    }
283
284    #[tokio::test]
285    async fn ipc_cert_file_path_empty_config() {
286        let default_auth_token_path = PlatformSettings::get_auth_token_path();
287
288        // When the auth token file path _and_ IPC cert file path are both unset, we should default to looking for the
289        // IPC cert in the same directory as the auth token.
290        let config = get_remote_agent_config(None, None).await;
291        assert_eq!(
292            config.auth().ipc_cert_file_path().parent(),
293            default_auth_token_path.as_path().parent()
294        );
295        assert_eq!(
296            config.auth().ipc_cert_file_path().file_name().map(Path::new),
297            Some(PlatformSettings::get_ipc_cert_filename())
298        );
299    }
300
301    #[tokio::test]
302    async fn ipc_cert_file_path_defaults() {
303        let default_auth_token_path = PlatformSettings::get_auth_token_path();
304
305        // When the IPC cert file path is not set, it should default to the same directory as the auth token file using
306        // the default certificate file name.
307        let config = get_remote_agent_config(None, Some(&default_auth_token_path)).await;
308        assert_eq!(
309            config.auth().ipc_cert_file_path().parent(),
310            default_auth_token_path.as_path().parent()
311        );
312        assert_eq!(
313            config.auth().ipc_cert_file_path().file_name().map(Path::new),
314            Some(PlatformSettings::get_ipc_cert_filename())
315        );
316    }
317
318    #[tokio::test]
319    async fn ipc_cert_file_path_explicitly_set() {
320        let default_auth_token_path = PlatformSettings::get_auth_token_path();
321        let custom_ipc_cert_path = PathBuf::from("/tmp/custom_ipc_cert.pem");
322
323        // When the IPC cert file path is explicitly set, it should be used.
324        let config = get_remote_agent_config(Some(&custom_ipc_cert_path), Some(&default_auth_token_path)).await;
325        assert_eq!(custom_ipc_cert_path, config.auth().ipc_cert_file_path());
326    }
327
328    #[tokio::test]
329    async fn ipc_cert_file_path_custom_auth_token_path() {
330        let custom_auth_token_path = PathBuf::from("/secret/auth_token");
331
332        // When the IPC cert file path is not set, but there's a custom auth token path (explicitly set, different from the default),
333        // we should still look in the same directory as the auth token file using the default certificate file name.
334        let config = get_remote_agent_config(None, Some(&custom_auth_token_path)).await;
335        assert_eq!(
336            config.auth().ipc_cert_file_path().parent(),
337            custom_auth_token_path.as_path().parent()
338        );
339        assert_eq!(
340            config.auth().ipc_cert_file_path().file_name().map(Path::new),
341            Some(PlatformSettings::get_ipc_cert_filename())
342        );
343    }
344
345    #[tokio::test]
346    async fn ipc_cert_file_path_invalid_auth_token_path() {
347        let invalid_auth_token_path = PathBuf::from("/");
348
349        // If the auth token file path is somehow unset or invalid (for example, no parent directory), we should use the same
350        // logic but with the default Datadog Agent configuration directory.
351        let config = get_remote_agent_config(None, Some(&invalid_auth_token_path)).await;
352        assert_eq!(
353            config.auth().ipc_cert_file_path().parent(),
354            Some(PlatformSettings::get_config_dir_path())
355        );
356        assert_eq!(
357            config.auth().ipc_cert_file_path().file_name().map(Path::new),
358            Some(PlatformSettings::get_ipc_cert_filename())
359        );
360    }
361
362    async fn config_from_values(values: serde_json::Map<String, serde_json::Value>) -> RemoteAgentClientConfiguration {
363        let (base_config, _) =
364            ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
365        RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
366    }
367
368    #[tokio::test]
369    async fn endpoint_defaults_to_port_5001() {
370        let config = config_from_values(serde_json::Map::new()).await;
371        assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:5001/");
372    }
373
374    #[tokio::test]
375    async fn endpoint_uses_cmd_port() {
376        let mut values = serde_json::Map::new();
377        values.insert("cmd_port".to_string(), 7777.into());
378        let config = config_from_values(values).await;
379        assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:7777/");
380    }
381
382    #[cfg(target_os = "linux")]
383    #[tokio::test]
384    async fn vsock_addr_valid_values() {
385        // (vsock_addr input, expected CID — port always comes from cmd_port=5001)
386        let cases: &[(&str, Option<u32>)] = &[
387            ("", None),
388            ("host", Some(2)),
389            ("hypervisor", Some(0)),
390            ("local", Some(3)),
391        ];
392
393        for (input, expected_cid) in cases {
394            let mut values = serde_json::Map::new();
395            values.insert("vsock_addr".to_string(), (*input).into());
396            values.insert("cmd_port".to_string(), 5001u16.into());
397            let config = config_from_values(values).await;
398            let result = config
399                .vsock_addr()
400                .expect("vsock_addr() should not error with cmd_port set");
401            assert_eq!(result.map(|a| a.cid()), *expected_cid, "input: {input:?}");
402        }
403    }
404
405    #[cfg(target_os = "linux")]
406    #[tokio::test]
407    async fn vsock_addr_invalid_values() {
408        let cases = &["invalid", "2", "HOST", "host ", "vm0"];
409
410        for input in cases {
411            let mut values = serde_json::Map::new();
412            values.insert("vsock_addr".to_string(), (*input).into());
413            let (base_config, _) =
414                ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
415            assert!(
416                RemoteAgentClientConfiguration::from_configuration(&base_config).is_err(),
417                "expected error for input: {input:?}",
418            );
419        }
420    }
421}