Skip to main content

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