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
15fn default_agent_ipc_endpoint() -> Uri {
16    Uri::from_static("https://127.0.0.1:5001")
17}
18
19const fn default_connect_retry_attempts() -> usize {
20    10
21}
22
23const fn default_grpc_max_message_size() -> usize {
24    128 * 1024 * 1024
25}
26
27const fn default_connect_retry_backoff() -> Duration {
28    Duration::from_secs(2)
29}
30
31/// Datadog Agent IPC authentication configuration.
32#[derive(Deserialize)]
33#[serde(default)]
34pub struct IpcAuthConfiguration {
35    /// Path to the Agent authentication token file.
36    ///
37    /// The contents of the file are passed as a bearer token in RPC requests to the IPC endpoint.
38    ///
39    /// Defaults to `<conf dir>/auth_token`, where `<conf dir>` is the platform-specific directory containing the Agent
40    /// configuration.
41    auth_token_file_path: PathBuf,
42
43    /// Path to the Agent IPC TLS certificate file.
44    ///
45    /// The file is expected to be PEM-encoded, containing both a certificate and private key. The certificate will be
46    /// used to verify the TLS server certificate presented by the Agent, and the certificate and private key will be
47    /// used together to provide client authentication _to_ the Agent.
48    ///
49    /// Defaults to `ipc_cert.pem` in the same directory as the Agent authentication token file. (for example, if
50    /// `auth_token_file_path` is `/etc/datadog-agent/auth_token`, this will be `/etc/datadog-agent/ipc_cert.pem`.)
51    ipc_cert_file_path: Option<PathBuf>,
52}
53
54impl IpcAuthConfiguration {
55    // Creates a new `IpcAuthConfiguration` from the given configuration.
56    ///
57    /// ## Errors
58    ///
59    /// If the configuration is invalid, an error is returned.
60    pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
61        config
62            .as_typed::<Self>()
63            .error_context("Failed to parse Datadog Agent IPC authentication configuration.")
64    }
65
66    /// Gets the path to the Agent authentication token file from the configuration.
67    pub fn auth_token_file_path(&self) -> PathBuf {
68        if self.auth_token_file_path.as_os_str().is_empty() {
69            return PlatformSettings::get_auth_token_path();
70        }
71
72        self.auth_token_file_path.clone()
73    }
74
75    /// Gets the IPC certificate file path from the configuration.
76    pub fn ipc_cert_file_path(&self) -> PathBuf {
77        // If the IPC cert file path is set explicitly, we always prefer that.
78        if let Some(path) = self.ipc_cert_file_path.as_ref() {
79            if !path.as_os_str().is_empty() {
80                return path.clone();
81            }
82        }
83
84        // Otherwise, we default to the same directory as the auth token file with the default certificate file name.
85        let auth_token_dir = if self.auth_token_file_path.as_os_str().is_empty() {
86            PlatformSettings::get_config_dir_path()
87        } else {
88            self.auth_token_file_path
89                .parent()
90                .unwrap_or(PlatformSettings::get_config_dir_path())
91        };
92
93        auth_token_dir.join(PlatformSettings::get_ipc_cert_filename())
94    }
95}
96
97impl Default for IpcAuthConfiguration {
98    fn default() -> Self {
99        Self {
100            auth_token_file_path: PlatformSettings::get_auth_token_path(),
101            ipc_cert_file_path: None,
102        }
103    }
104}
105
106/// Datadog Agent IPC client configuration.
107#[derive(Deserialize)]
108pub struct RemoteAgentClientConfiguration {
109    /// Datadog Agent IPC endpoint to connect to.
110    ///
111    /// Caution/weird: This is configuration is only available on agent-data-plane, and would allow
112    /// one to connect to an Agent at a URI other than localhost/127.0.0.1. However, the Datadog
113    /// configuration schema doesn't account for this and instead provides `cmd_port`. Therefore,
114    ///
115    /// **CAUTION**: if `cmd_port` is set, then `ipc_endpoint` is ignored.
116    ///
117    /// Defaults to `https://127.0.0.1:5001`.
118    #[serde(
119        rename = "agent_ipc_endpoint",
120        with = "http_serde_ext::uri",
121        default = "default_agent_ipc_endpoint"
122    )]
123    ipc_endpoint: Uri,
124
125    /// The port that will be used to connect to the Datadog Agent IPC on the local host.
126    ///
127    /// Takes precedence over `ipc_endpoint` if set.
128    cmd_port: Option<u16>,
129
130    /// Authentication configuration for the IPC endpoint.
131    #[serde(flatten, default)]
132    auth: IpcAuthConfiguration,
133
134    /// Number of allowed retry attempts when initially connecting.
135    ///
136    /// Defaults to `10`.
137    #[serde(default = "default_connect_retry_attempts")]
138    connect_retry_attempts: usize,
139
140    /// Amount of time to wait between connection attempts when initially connecting.
141    ///
142    /// Defaults to 2 seconds.
143    #[serde(default = "default_connect_retry_backoff")]
144    connect_retry_backoff: Duration,
145
146    /// Maximum message size for gRPC messages.
147    ///
148    /// Defaults to `128 * 1024 * 1024` (128 MB).
149    #[serde(
150        rename = "agent_ipc_grpc_max_message_size",
151        default = "default_grpc_max_message_size"
152    )]
153    grpc_max_message_size: usize,
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 IPC endpoint URI.
221    ///
222    /// If `cmd_port` is set, the endpoint is built from it, ignoring `ipc_endpoint`.
223    pub fn endpoint(&self) -> Result<Uri, GenericError> {
224        if let Some(cmd_port) = self.cmd_port {
225            format!("https://127.0.0.1:{}", cmd_port)
226                .parse::<Uri>()
227                .with_error_context(|| format!("failed to build URI from cmd_port {cmd_port}"))
228        } else {
229            Ok(self.ipc_endpoint.clone())
230        }
231    }
232
233    /// Returns the maximum message size for gRPC.
234    pub fn grpc_max_message_size(&self) -> usize {
235        self.grpc_max_message_size
236    }
237
238    /// Returns the vsock address to use for connecting to the IPC endpoint, if configured.
239    ///
240    /// Combines the CID from `vsock_addr` with the port resolved from `endpoint()`. Returns
241    /// an error if `vsock_addr` is set but the endpoint has no explicit port.
242    ///
243    /// # Errors
244    ///
245    /// If the configured endpoint has no explicit port.
246    #[cfg(target_os = "linux")]
247    pub fn vsock_addr(&self) -> Result<Option<tokio_vsock::VsockAddr>, GenericError> {
248        let Some(cid) = self.vsock_addr else {
249            return Ok(None);
250        };
251        let port = self
252            .endpoint()?
253            .port_u16()
254            .map(u32::from)
255            .ok_or_else(|| saluki_error::generic_error!("vsock requires an explicit port in the IPC endpoint"))?;
256        Ok(Some(tokio_vsock::VsockAddr::new(cid, port)))
257    }
258}
259
260impl BackoffBuilder for &RemoteAgentClientConfiguration {
261    type Backoff = <ConstantBuilder as BackoffBuilder>::Backoff;
262
263    fn build(self) -> Self::Backoff {
264        ConstantBuilder::default()
265            .with_delay(self.connect_retry_backoff)
266            .with_max_times(self.connect_retry_attempts)
267            .build()
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use std::path::{Path, PathBuf};
274
275    use saluki_config::ConfigurationLoader;
276
277    use super::RemoteAgentClientConfiguration;
278    use crate::platform::PlatformSettings;
279
280    async fn get_remote_agent_config(
281        ipc_cert_file_path: Option<&Path>, auth_token_file_path: Option<&Path>,
282    ) -> RemoteAgentClientConfiguration {
283        // Set the values in the config map if provided.
284        let mut values = serde_json::Map::new();
285        if let Some(path) = ipc_cert_file_path {
286            values.insert(
287                "ipc_cert_file_path".to_string(),
288                path.to_string_lossy().into_owned().into(),
289            );
290        }
291        if let Some(path) = auth_token_file_path {
292            values.insert(
293                "auth_token_file_path".to_string(),
294                path.to_string_lossy().into_owned().into(),
295            );
296        }
297
298        let (base_config, _) =
299            ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
300        RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
301    }
302
303    #[tokio::test]
304    async fn ipc_cert_file_path_empty_config() {
305        let default_auth_token_path = PlatformSettings::get_auth_token_path();
306
307        // When the auth token file path _and_ IPC cert file path are both unset, we should default to looking for the
308        // IPC cert in the same directory as the auth token.
309        let config = get_remote_agent_config(None, None).await;
310        assert_eq!(
311            config.auth().ipc_cert_file_path().parent(),
312            default_auth_token_path.as_path().parent()
313        );
314        assert_eq!(
315            config.auth().ipc_cert_file_path().file_name().map(Path::new),
316            Some(PlatformSettings::get_ipc_cert_filename())
317        );
318    }
319
320    #[tokio::test]
321    async fn ipc_cert_file_path_defaults() {
322        let default_auth_token_path = PlatformSettings::get_auth_token_path();
323
324        // When the IPC cert file path is not set, it should default to the same directory as the auth token file using
325        // the default certificate file name.
326        let config = get_remote_agent_config(None, Some(&default_auth_token_path)).await;
327        assert_eq!(
328            config.auth().ipc_cert_file_path().parent(),
329            default_auth_token_path.as_path().parent()
330        );
331        assert_eq!(
332            config.auth().ipc_cert_file_path().file_name().map(Path::new),
333            Some(PlatformSettings::get_ipc_cert_filename())
334        );
335    }
336
337    #[tokio::test]
338    async fn ipc_cert_file_path_explicitly_set() {
339        let default_auth_token_path = PlatformSettings::get_auth_token_path();
340        let custom_ipc_cert_path = PathBuf::from("/tmp/custom_ipc_cert.pem");
341
342        // When the IPC cert file path is explicitly set, it should be used.
343        let config = get_remote_agent_config(Some(&custom_ipc_cert_path), Some(&default_auth_token_path)).await;
344        assert_eq!(custom_ipc_cert_path, config.auth().ipc_cert_file_path());
345    }
346
347    #[tokio::test]
348    async fn ipc_cert_file_path_custom_auth_token_path() {
349        let custom_auth_token_path = PathBuf::from("/secret/auth_token");
350
351        // When the IPC cert file path is not set, but there's a custom auth token path (explicitly set, different from the default),
352        // we should still look in the same directory as the auth token file using the default certificate file name.
353        let config = get_remote_agent_config(None, Some(&custom_auth_token_path)).await;
354        assert_eq!(
355            config.auth().ipc_cert_file_path().parent(),
356            custom_auth_token_path.as_path().parent()
357        );
358        assert_eq!(
359            config.auth().ipc_cert_file_path().file_name().map(Path::new),
360            Some(PlatformSettings::get_ipc_cert_filename())
361        );
362    }
363
364    #[tokio::test]
365    async fn ipc_cert_file_path_invalid_auth_token_path() {
366        let invalid_auth_token_path = PathBuf::from("/");
367
368        // If the auth token file path is somehow unset or invalid (for example, no parent directory), we should use the same
369        // logic but with the default Datadog Agent configuration directory.
370        let config = get_remote_agent_config(None, Some(&invalid_auth_token_path)).await;
371        assert_eq!(
372            config.auth().ipc_cert_file_path().parent(),
373            Some(PlatformSettings::get_config_dir_path())
374        );
375        assert_eq!(
376            config.auth().ipc_cert_file_path().file_name().map(Path::new),
377            Some(PlatformSettings::get_ipc_cert_filename())
378        );
379    }
380
381    async fn config_from_values(values: serde_json::Map<String, serde_json::Value>) -> RemoteAgentClientConfiguration {
382        let (base_config, _) =
383            ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
384        RemoteAgentClientConfiguration::from_configuration(&base_config).unwrap()
385    }
386
387    #[tokio::test]
388    async fn endpoint_defaults_to_port_5001() {
389        let config = config_from_values(serde_json::Map::new()).await;
390        assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:5001/");
391    }
392
393    #[tokio::test]
394    async fn endpoint_uses_cmd_port() {
395        let mut values = serde_json::Map::new();
396        values.insert("cmd_port".to_string(), 7777.into());
397        let config = config_from_values(values).await;
398        assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:7777/");
399    }
400
401    #[tokio::test]
402    async fn cmd_port_takes_precedence_over_ipc_endpoint() {
403        let mut values = serde_json::Map::new();
404        values.insert("cmd_port".to_string(), 8888.into());
405        values.insert("agent_ipc_endpoint".to_string(), "https://10.0.0.1:3333".into());
406        let config = config_from_values(values).await;
407        assert_eq!(config.endpoint().unwrap().to_string(), "https://127.0.0.1:8888/");
408    }
409
410    #[tokio::test]
411    async fn ipc_endpoint_used_when_no_cmd_port() {
412        let mut values = serde_json::Map::new();
413        values.insert("agent_ipc_endpoint".to_string(), "https://10.0.0.1:3333".into());
414        let config = config_from_values(values).await;
415        assert_eq!(config.endpoint().unwrap().to_string(), "https://10.0.0.1:3333/");
416    }
417
418    #[cfg(target_os = "linux")]
419    #[tokio::test]
420    async fn vsock_addr_valid_values() {
421        // (vsock_addr input, expected CID — port always comes from cmd_port=5001)
422        let cases: &[(&str, Option<u32>)] = &[
423            ("", None),
424            ("host", Some(2)),
425            ("hypervisor", Some(0)),
426            ("local", Some(3)),
427        ];
428
429        for (input, expected_cid) in cases {
430            let mut values = serde_json::Map::new();
431            values.insert("vsock_addr".to_string(), (*input).into());
432            values.insert("cmd_port".to_string(), 5001u16.into());
433            let config = config_from_values(values).await;
434            let result = config
435                .vsock_addr()
436                .expect("vsock_addr() should not error with cmd_port set");
437            assert_eq!(result.map(|a| a.cid()), *expected_cid, "input: {input:?}");
438        }
439    }
440
441    #[cfg(target_os = "linux")]
442    #[tokio::test]
443    async fn vsock_addr_invalid_values() {
444        let cases = &["invalid", "2", "HOST", "host ", "vm0"];
445
446        for input in cases {
447            let mut values = serde_json::Map::new();
448            values.insert("vsock_addr".to_string(), (*input).into());
449            let (base_config, _) =
450                ConfigurationLoader::for_tests(Some(serde_json::Value::Object(values)), None, false).await;
451            assert!(
452                RemoteAgentClientConfiguration::from_configuration(&base_config).is_err(),
453                "expected error for input: {input:?}",
454            );
455        }
456    }
457}