saluki_env/features/
mod.rs

1//! Feature detection.
2//!
3//! This module provides helpers for detecting the presence of various "features" in the environment, such as if
4//! containerd is running, and so on. Feature detection is useful for knowing what capabilities are available, and what
5//! code should or shouldn't be run.
6use std::path::{Path, PathBuf};
7#[cfg(unix)]
8use std::{io::ErrorKind, net::Shutdown, os::unix::fs::FileTypeExt as _, time::Duration};
9
10use bitmask_enum::bitmask;
11#[cfg(unix)]
12use socket2::{Domain, SockAddr, Socket, Type};
13use tracing::debug;
14
15mod containerd;
16pub use self::containerd::ContainerdDetector;
17
18mod detector;
19pub use self::detector::FeatureDetector;
20
21const CONTAINER_HOST_MOUNT_PATH: &str = "/host";
22const LEGACY_CGROUPFS_ROOT_MARKER: &str = "/cgroup/memory/memory.stat";
23#[cfg(unix)]
24const SOCKET_CHECK_CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
25
26/// Features are distinct markers or indicators of a particular technology or platform that's present.
27///
28/// In general, features represent the type of environment that the application is running in, such as the Kubernetes
29/// feature being present indicating that the application is running in Kubernetes, and so on.
30#[bitmask(u16)]
31#[bitmask_config(vec_debug)]
32pub enum Feature {
33    /// Host-mapped procfs.
34    ///
35    /// This implies that we're in a containerized environment and the host's procfs (`/proc`) has been mapped into the
36    /// container using a `/host` prefix, resulting in a `/host/proc` path.
37    HostMappedProcfs,
38
39    /// Host-mapped cgroupfs.
40    ///
41    /// This implies that we're in a containerized environment and the host's cgroupfs (`/sys/fs/cgroup`) has been
42    /// mapped into the container using a `/host` prefix, resulting in a `/host/sys/fs/cgroup` path.
43    HostMappedCgroupfs,
44
45    /// Legacy cgroupfs root.
46    ///
47    /// This implies that we're running directly on a host whose cgroups v1 hierarchy sits at `/cgroup` rather than
48    /// `/sys/fs/cgroup`, which is the layout used by older Amazon Linux hosts. This is never detected in a
49    /// containerized environment, where the host's hierarchy is reached through a host-mapped path instead.
50    LegacyCgroupfsRoot,
51
52    /// Containerd.
53    Containerd,
54}
55
56fn get_env_var_or_empty(name: &str) -> String {
57    std::env::var(name).unwrap_or_else(|_| String::new())
58}
59
60fn is_env_var_present(name: &str) -> bool {
61    !get_env_var_or_empty(name).is_empty()
62}
63
64/// Returns whether the process runs in an Amazon ECS Fargate environment.
65pub fn is_ecs_fargate() -> bool {
66    is_env_var_present("ECS_FARGATE") || get_env_var_or_empty("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE"
67}
68
69fn file_exists<P>(path: P) -> bool
70where
71    P: AsRef<Path>,
72{
73    std::fs::metadata(path).is_ok()
74}
75
76#[cfg(unix)]
77fn path_empty<P>(path: P) -> bool
78where
79    P: AsRef<Path>,
80{
81    path.as_ref().as_os_str().is_empty()
82}
83
84#[cfg(unix)]
85fn path_contains<P>(path: P, fragment: &str) -> bool
86where
87    P: AsRef<Path>,
88{
89    path.as_ref().to_string_lossy().contains(fragment)
90}
91
92/// Checks to see if a Unix domain sockets exists, and is reachable, at the given path.
93///
94/// If there is no file at the given path, or if the file isn't of the socket type, `None` is returned. If the socket
95/// at the given path isn't reachable, `Some(false)` is returned. Otherwise, `Some(true)` is returned.
96///
97/// Reachability is determined as either being able to connect or having the permissions to do so. We do this because it
98/// is likely that if no process is _currently_ listening on the socket, one will likely be in the future.
99#[cfg(unix)]
100fn check_unix_socket(path: &Path) -> Option<bool> {
101    // Make sure the path exists and is a socket.
102    let metadata = match std::fs::metadata(path) {
103        Ok(metadata) => metadata,
104        Err(e) => {
105            debug!(socket_path = %path.to_string_lossy(), error = %e, "Failed to get metadata for socket path.");
106            return None;
107        }
108    };
109    if !metadata.file_type().is_socket() {
110        return None;
111    }
112
113    // Check to see if we can connect to the socket.
114    //
115    // We treat every error other than `PermissionDenied` as a non-failure, with the thought being that if the file
116    // exists and is a socket, the process is potentially not listening _yet_, but likely will in the future.
117    let socket = match Socket::new_raw(Domain::UNIX, Type::STREAM, None) {
118        Ok(socket) => socket,
119        Err(e) => {
120            debug!(socket_path = %path.to_string_lossy(), error = %e, "Failed to create socket.");
121            return Some(false);
122        }
123    };
124
125    let socket_addr = match SockAddr::unix(path) {
126        Ok(socket_addr) => socket_addr,
127        Err(e) => {
128            debug!(socket_path = %path.to_string_lossy(), error = %e, "Failed to create socket address.");
129            return Some(false);
130        }
131    };
132
133    Some(
134        match socket.connect_timeout(&socket_addr, SOCKET_CHECK_CONNECT_TIMEOUT) {
135            Ok(_) => {
136                // Shutdown the socket, ignoring any errors.
137                let _ = socket.shutdown(Shutdown::Both);
138
139                true
140            }
141            Err(e) => e.kind() != ErrorKind::PermissionDenied,
142        },
143    )
144}
145
146#[cfg(unix)]
147fn find_first_available_unix_socket<I, P>(socket_paths: I) -> Option<PathBuf>
148where
149    I: IntoIterator<Item = P>,
150    P: AsRef<Path>,
151{
152    for socket_path in socket_paths {
153        let socket_path = socket_path.as_ref();
154        let socket_path_str = socket_path.to_string_lossy();
155
156        match check_unix_socket(socket_path) {
157            None => {
158                debug!(socket_path = %socket_path_str, "No file at socket path or not a socket.");
159                continue;
160            }
161            Some(false) => {
162                debug!(socket_path = %socket_path_str, "Found file at socket path but unreachable. (permissions?)");
163                continue;
164            }
165            Some(true) => {
166                debug!(socket_path = %socket_path_str, "Found reachable socket.");
167                return Some(socket_path.to_path_buf());
168            }
169        }
170    }
171
172    None
173}
174
175fn is_running_inside_container() -> bool {
176    // `DOCKER_DD_AGENT` is set by the official Datadog Agent container image.
177    let is_containerized = is_env_var_present("DOCKER_DD_AGENT");
178    if is_containerized {
179        debug!("Found non-empty DOCKER_DD_AGENT environment variable. Likely running in a container.");
180    } else {
181        debug!("Did not find DOCKER_DD_AGENT environment variable. Likely not running in a container.");
182    }
183    is_containerized
184}
185
186#[cfg(unix)]
187fn is_running_inside_docker() -> bool {
188    // This file is mounted into a container's filesystem by Docker itself, and implies that we're currently _inside_
189    // the Docker runtime. `is_docker_present` detects the presence of the Docker runtime on the host itself, at the OS
190    // level.
191    file_exists("/.dockerenv")
192}
193
194#[cfg(unix)]
195fn with_host_mount_prefixes<I, P>(paths: I) -> Vec<PathBuf>
196where
197    I: IntoIterator<Item = P>,
198    P: AsRef<str>,
199{
200    let mut prefixes = vec![];
201
202    // Add the provided paths as they are, joined with a leading slash, which will absolute-ize them if they are
203    // relative. If we're in a containerized environment, we'll also add a "/host"-anchored version of each path.
204    //
205    // We do this to avoid clobbering paths in the container, such that if we mount a host path into the container, such
206    // as "/var/run", it ends up as "/host/var/run" in the container, which doesn't shadow the existing "/var/run".
207    let root = PathBuf::from("/");
208    let host_root = PathBuf::from(CONTAINER_HOST_MOUNT_PATH);
209    let is_containerized = is_running_inside_container();
210
211    for path in paths {
212        let path = path.as_ref().trim_start_matches('/');
213
214        prefixes.push(root.join(path));
215
216        if is_containerized {
217            prefixes.push(host_root.join(path));
218        }
219    }
220
221    prefixes
222}
223
224fn has_host_mapped_procfs() -> bool {
225    let path = PathBuf::from(CONTAINER_HOST_MOUNT_PATH).join("proc");
226    is_running_inside_container() && file_exists(&path)
227}
228
229fn has_host_mapped_cgroupfs() -> bool {
230    let path = PathBuf::from(CONTAINER_HOST_MOUNT_PATH).join("sys/fs/cgroup");
231    is_running_inside_container() && file_exists(&path)
232}
233
234fn has_legacy_cgroupfs_root() -> bool {
235    // This layout only describes a host we're running on directly. The Datadog Agent probes for it only when it isn't
236    // containerized, and otherwise picks between the host-mapped and local cgroupfs roots without considering
237    // `/cgroup` at all. A `/cgroup` path inside our own container is not the host's hierarchy, so treating it as one
238    // would point the reader at a root with no cgroups under it.
239    if is_running_inside_container() {
240        return false;
241    }
242
243    // The Datadog Agent looks for the `memory` controller's `memory.stat` rather than the `/cgroup` directory itself,
244    // since the directory can exist while empty. Like the Agent, we count a stat that fails for any reason other than
245    // the file being absent: the layout is there, we just can't read the marker.
246    match std::fs::metadata(LEGACY_CGROUPFS_ROOT_MARKER) {
247        Ok(_) => true,
248        Err(e) => e.kind() != std::io::ErrorKind::NotFound,
249    }
250}