Skip to main content

datadog_agent_commons/ipc/
config.rs

1//! IPC configuration.
2
3use std::path::{Path, PathBuf};
4
5#[cfg(target_os = "linux")]
6use saluki_error::generic_error;
7use saluki_error::GenericError;
8use tonic::transport::Uri;
9#[cfg(not(target_os = "linux"))]
10use tracing::warn;
11
12use crate::platform::PlatformSettings;
13
14/// Datadog Agent IPC bearer-token and exact shared-certificate mTLS configuration.
15#[derive(Clone, Debug)]
16pub struct IpcAuthConfiguration {
17    /// Path to the Agent authentication token file.
18    ///
19    /// The contents of the file are passed as a bearer token in RPC requests to the IPC endpoint.
20    ///
21    /// Defaults to `<conf dir>/auth_token`, where `<conf dir>` is the platform-specific directory containing the Agent
22    /// configuration.
23    auth_token_file_path: PathBuf,
24
25    /// Path to the shared Agent IPC mTLS identity file.
26    ///
27    /// The PEM file contains one certificate and its private key. IPC peers require exact leaf-certificate DER equality
28    /// rather than CA-based trust, and each peer proves possession of the corresponding private key during the TLS
29    /// handshake. The same identity authenticates both the client and server, so a CA chain cannot broaden it.
30    ///
31    /// Defaults to `ipc_cert.pem` in the same directory as the Agent authentication token file. (for example, if
32    /// `auth_token_file_path` is `/etc/datadog-agent/auth_token`, this will be `/etc/datadog-agent/ipc_cert.pem`.)
33    ipc_cert_file_path: PathBuf,
34}
35
36impl IpcAuthConfiguration {
37    /// Creates an `IpcAuthConfiguration`, resolving empty paths to Agent defaults.
38    ///
39    /// An empty token path selects the platform-specific token path. An empty certificate path selects `ipc_cert.pem`
40    /// beside the resolved token, or in the platform configuration directory when the token has no parent.
41    pub fn new(mut auth_token_file_path: PathBuf, mut ipc_cert_file_path: PathBuf) -> Self {
42        if auth_token_file_path.as_os_str().is_empty() {
43            auth_token_file_path = PlatformSettings::get_auth_token_path();
44        }
45
46        if ipc_cert_file_path.as_os_str().is_empty() {
47            let auth_token_dir = auth_token_file_path
48                .parent()
49                .unwrap_or(PlatformSettings::get_config_dir_path());
50            ipc_cert_file_path = auth_token_dir.join(PlatformSettings::get_ipc_cert_filename());
51        }
52
53        Self {
54            auth_token_file_path,
55            ipc_cert_file_path,
56        }
57    }
58
59    /// Gets the path to the Agent authentication token file from the configuration.
60    pub fn auth_token_file_path(&self) -> &Path {
61        &self.auth_token_file_path
62    }
63
64    /// Gets the shared IPC mTLS identity file path from the configuration.
65    pub fn ipc_cert_file_path(&self) -> &Path {
66        &self.ipc_cert_file_path
67    }
68}
69
70/// Datadog Agent IPC client configuration.
71#[derive(Clone, Debug)]
72pub struct RemoteAgentClientConfiguration {
73    /// Core Agent CMD API port used for remote-agent gRPC IPC on localhost.
74    pub cmd_port: u16,
75
76    /// Authentication configuration for the IPC endpoint.
77    pub auth: IpcAuthConfiguration,
78
79    /// Maximum message size for gRPC messages.
80    pub grpc_max_message_size: usize,
81
82    /// Resolved CID for connecting to the Agent IPC endpoint via AF_VSOCK.
83    #[cfg(target_os = "linux")]
84    pub vsock_cid: Option<u32>,
85}
86
87impl RemoteAgentClientConfiguration {
88    /// Creates a `RemoteAgentClientConfiguration` from its parts, resolving the named vsock address to a CID.
89    ///
90    /// `vsock_addr` follows the Datadog Agent's `vsock_addr` setting: an empty value disables vsock, and `host`,
91    /// `hypervisor`, and `local` select the well-known context identifiers 2, 0, and 3. vsock is only supported on
92    /// Linux; on other platforms, a non-empty value is ignored with a warning.
93    ///
94    /// # Errors
95    ///
96    /// On Linux, returns an error if `vsock_addr` is not empty and is not one of the supported names.
97    pub fn from_parts(
98        cmd_port: u16, auth: IpcAuthConfiguration, grpc_max_message_size: usize, vsock_addr: &str,
99    ) -> Result<Self, GenericError> {
100        #[cfg(target_os = "linux")]
101        let vsock_cid = match vsock_addr {
102            "" => None,
103            "host" => Some(2),
104            "hypervisor" => Some(0),
105            "local" => Some(3),
106            other => {
107                return Err(generic_error!(
108                    "invalid vsock address '{}'; expected one of: host, hypervisor, local",
109                    other
110                ))
111            }
112        };
113
114        #[cfg(not(target_os = "linux"))]
115        if !vsock_addr.is_empty() {
116            warn!("`vsock_addr` is configured but vsock is only supported on Linux. Setting will be ignored.");
117        }
118
119        Ok(Self {
120            cmd_port,
121            auth,
122            grpc_max_message_size,
123            #[cfg(target_os = "linux")]
124            vsock_cid,
125        })
126    }
127
128    /// Returns the Core Agent CMD API gRPC endpoint URI.
129    pub fn endpoint(&self) -> Uri {
130        format!("https://127.0.0.1:{}", self.cmd_port)
131            .parse()
132            .expect("a URI built from a u16 port is valid")
133    }
134
135    /// Returns the vsock address to use for connecting to the IPC endpoint, if configured.
136    #[cfg(target_os = "linux")]
137    pub fn vsock_addr(&self) -> Option<tokio_vsock::VsockAddr> {
138        self.vsock_cid
139            .map(|cid| tokio_vsock::VsockAddr::new(cid, u32::from(self.cmd_port)))
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use std::path::{Path, PathBuf};
146
147    use super::{IpcAuthConfiguration, RemoteAgentClientConfiguration};
148    use crate::platform::PlatformSettings;
149
150    #[test]
151    fn ipc_cert_file_path_empty_config() {
152        let default_auth_token_path = PlatformSettings::get_auth_token_path();
153
154        // When the auth token file path _and_ IPC cert file path are both unset, we should default to looking for the
155        // IPC cert in the same directory as the auth token.
156        let config = IpcAuthConfiguration::new(PathBuf::new(), PathBuf::new());
157        assert_eq!(config.auth_token_file_path(), default_auth_token_path);
158        assert_eq!(
159            config.ipc_cert_file_path().parent(),
160            default_auth_token_path.as_path().parent()
161        );
162        assert_eq!(
163            config.ipc_cert_file_path().file_name().map(Path::new),
164            Some(PlatformSettings::get_ipc_cert_filename())
165        );
166    }
167
168    #[test]
169    fn ipc_cert_file_path_defaults() {
170        let default_auth_token_path = PlatformSettings::get_auth_token_path();
171
172        // When the IPC cert file path is not set, it should default to the same directory as the auth token file using
173        // the default certificate file name.
174        let config = IpcAuthConfiguration::new(default_auth_token_path.clone(), PathBuf::new());
175        assert_eq!(
176            config.ipc_cert_file_path().parent(),
177            default_auth_token_path.as_path().parent()
178        );
179        assert_eq!(
180            config.ipc_cert_file_path().file_name().map(Path::new),
181            Some(PlatformSettings::get_ipc_cert_filename())
182        );
183    }
184
185    #[test]
186    fn ipc_cert_file_path_explicitly_set() {
187        let default_auth_token_path = PlatformSettings::get_auth_token_path();
188        let custom_ipc_cert_path = PathBuf::from("/tmp/custom_ipc_cert.pem");
189
190        // When the IPC cert file path is explicitly set, it should be used.
191        let config = IpcAuthConfiguration::new(default_auth_token_path, custom_ipc_cert_path.clone());
192        assert_eq!(custom_ipc_cert_path, config.ipc_cert_file_path());
193    }
194
195    #[test]
196    fn ipc_cert_file_path_custom_auth_token_path() {
197        let custom_auth_token_path = PathBuf::from("/secret/auth_token");
198
199        // When the IPC cert file path is not set, but there's a custom auth token path (explicitly set, different from the default),
200        // we should still look in the same directory as the auth token file using the default certificate file name.
201        let config = IpcAuthConfiguration::new(custom_auth_token_path.clone(), PathBuf::new());
202        assert_eq!(
203            config.ipc_cert_file_path().parent(),
204            custom_auth_token_path.as_path().parent()
205        );
206        assert_eq!(
207            config.ipc_cert_file_path().file_name().map(Path::new),
208            Some(PlatformSettings::get_ipc_cert_filename())
209        );
210    }
211
212    #[test]
213    fn ipc_cert_file_path_invalid_auth_token_path() {
214        let invalid_auth_token_path = PathBuf::from("/");
215
216        // If the auth token file path is somehow unset or invalid (for example, no parent directory), we should use the same
217        // logic but with the default Datadog Agent configuration directory.
218        let config = IpcAuthConfiguration::new(invalid_auth_token_path, PathBuf::new());
219        assert_eq!(
220            config.ipc_cert_file_path().parent(),
221            Some(PlatformSettings::get_config_dir_path())
222        );
223        assert_eq!(
224            config.ipc_cert_file_path().file_name().map(Path::new),
225            Some(PlatformSettings::get_ipc_cert_filename())
226        );
227    }
228
229    fn remote_agent_config(cmd_port: u16) -> RemoteAgentClientConfiguration {
230        RemoteAgentClientConfiguration {
231            cmd_port,
232            auth: IpcAuthConfiguration::new(PathBuf::new(), PathBuf::new()),
233            grpc_max_message_size: 128 * 1024 * 1024,
234            #[cfg(target_os = "linux")]
235            vsock_cid: None,
236        }
237    }
238
239    #[test]
240    fn endpoint_uses_cmd_port() {
241        for (cmd_port, expected) in [(5001, "https://127.0.0.1:5001/"), (7777, "https://127.0.0.1:7777/")] {
242            assert_eq!(remote_agent_config(cmd_port).endpoint().to_string(), expected);
243        }
244    }
245
246    #[test]
247    fn from_parts_keeps_the_given_parts() {
248        let auth = IpcAuthConfiguration::new("/secret/auth_token".into(), "/secret/ipc_cert.pem".into());
249        let config = RemoteAgentClientConfiguration::from_parts(5001, auth, 4 * 1024 * 1024, "")
250            .expect("an empty vsock address is valid");
251
252        assert_eq!(config.cmd_port, 5001);
253        assert_eq!(config.grpc_max_message_size, 4 * 1024 * 1024);
254        assert_eq!(config.auth.auth_token_file_path(), Path::new("/secret/auth_token"));
255        assert_eq!(config.auth.ipc_cert_file_path(), Path::new("/secret/ipc_cert.pem"));
256    }
257
258    #[cfg(target_os = "linux")]
259    #[test]
260    fn from_parts_resolves_vsock_addresses() {
261        for (value, expected_cid) in [
262            ("", None),
263            ("host", Some(2)),
264            ("hypervisor", Some(0)),
265            ("local", Some(3)),
266        ] {
267            let auth = IpcAuthConfiguration::new(PathBuf::new(), PathBuf::new());
268            let config = RemoteAgentClientConfiguration::from_parts(5001, auth, 4 * 1024 * 1024, value)
269                .expect("valid vsock address");
270            assert_eq!(config.vsock_cid, expected_cid, "input: {value:?}");
271        }
272    }
273
274    #[cfg(target_os = "linux")]
275    #[test]
276    fn from_parts_rejects_invalid_vsock_addresses() {
277        for value in ["invalid", "2", "HOST", "host ", "vm0"] {
278            let auth = IpcAuthConfiguration::new(PathBuf::new(), PathBuf::new());
279            assert!(
280                RemoteAgentClientConfiguration::from_parts(5001, auth, 4 * 1024 * 1024, value).is_err(),
281                "expected error for input: {value:?}",
282            );
283        }
284    }
285
286    #[cfg(not(target_os = "linux"))]
287    #[test]
288    fn from_parts_ignores_vsock_addresses_off_linux() {
289        let auth = IpcAuthConfiguration::new(PathBuf::new(), PathBuf::new());
290        assert!(RemoteAgentClientConfiguration::from_parts(5001, auth, 4 * 1024 * 1024, "host").is_ok());
291    }
292
293    #[cfg(target_os = "linux")]
294    #[test]
295    fn vsock_addr_uses_resolved_cid_and_cmd_port() {
296        let mut config = remote_agent_config(5001);
297        assert_eq!(config.vsock_addr(), None);
298
299        config.vsock_cid = Some(2);
300        let addr = config.vsock_addr().expect("vsock address should be configured");
301        assert_eq!(addr.cid(), 2);
302        assert_eq!(addr.port(), 5001);
303    }
304}