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