#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![deny(rust_2018_idioms)]
#![warn(missing_debug_implementations)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(target_os = "linux")]
mod linux {
use std::fs;
use std::os::unix::fs::MetadataExt;
pub(crate) const PROC_PID_INIT_INO: u64 = 0xEFFF_FFFC;
pub(crate) const CGROUP_MARKERS: &[&str] = &[
"docker",
"containerd",
"kubepods",
"lxc",
"podman",
"garden",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum NsInode {
Root,
NonRoot,
Unknown,
}
pub(crate) fn read_pid_ns_inode() -> NsInode {
match fs::metadata("/proc/self/ns/pid") {
Ok(m) if m.ino() == PROC_PID_INIT_INO => NsInode::Root,
Ok(_) => NsInode::NonRoot,
Err(_) => NsInode::Unknown,
}
}
pub(crate) fn is_pid_one() -> bool {
std::process::id() == 1
}
pub(crate) fn is_wsl() -> bool {
match fs::read_to_string("/proc/sys/kernel/osrelease") {
Ok(s) => (s.contains("Microsoft") || s.contains("WSL")) && !s.contains("WSL2"),
Err(_) => false,
}
}
pub(crate) fn cgroup_indicates_container() -> bool {
for path in ["/proc/1/cgroup", "/proc/self/cgroup"] {
if let Ok(s) = fs::read_to_string(path) {
if CGROUP_MARKERS.iter().any(|m| s.contains(m)) {
return true;
}
}
}
false
}
pub(crate) fn detect() -> bool {
let ns = read_pid_ns_inode();
if matches!(ns, NsInode::Root) {
return false;
}
if is_pid_one() {
return true;
}
if is_wsl() {
return true;
}
if cgroup_indicates_container() {
return true;
}
matches!(ns, NsInode::NonRoot)
}
}
#[must_use]
#[inline]
pub fn is_container() -> bool {
#[cfg(target_os = "linux")]
{
use std::sync::atomic::{AtomicU8, Ordering};
static CACHED: AtomicU8 = AtomicU8::new(0);
match CACHED.load(Ordering::Relaxed) {
1 => false,
2 => true,
_ => {
let value = linux::detect();
CACHED.store(if value { 2 } else { 1 }, Ordering::Relaxed);
value
}
}
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
#[cfg(feature = "diagnostics")]
#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))]
pub mod diagnostics {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CheckResult {
pub name: &'static str,
pub description: &'static str,
pub matched: bool,
}
#[derive(Debug, Clone)]
pub struct Report {
pub checks: Vec<CheckResult>,
pub is_container: bool,
}
impl Report {
#[must_use]
pub fn any_matched(&self) -> bool {
self.checks.iter().any(|c| c.matched)
}
}
#[must_use]
pub fn report() -> Report {
#[cfg(target_os = "linux")]
{
use crate::linux::{self, NsInode};
let ns = linux::read_pid_ns_inode();
let pid_one = linux::is_pid_one();
let wsl = linux::is_wsl();
let cgroup = linux::cgroup_indicates_container();
let checks = vec![
CheckResult {
name: "pid_namespace",
description: match ns {
NsInode::Root => {
"/proc/self/ns/pid inode == PROC_PID_INIT_INO (root PID namespace)"
}
NsInode::NonRoot => {
"/proc/self/ns/pid inode != PROC_PID_INIT_INO (child PID namespace)"
}
NsInode::Unknown => {
"/proc/self/ns/pid could not be read (inode unknown)"
}
},
matched: matches!(ns, NsInode::NonRoot),
},
CheckResult {
name: "pid_one",
description: "getpid() == 1 (assumed to mean container init; never use this crate from a real init system)",
matched: pid_one,
},
CheckResult {
name: "wsl",
description: "/proc/sys/kernel/osrelease contains \"Microsoft\" or \"WSL\" but not \"WSL2\" (WSL2 is a VM, not a container)",
matched: wsl,
},
CheckResult {
name: "cgroup",
description: "/proc/1/cgroup or /proc/self/cgroup mentions a known runtime",
matched: cgroup,
},
];
Report {
checks,
is_container: crate::is_container(),
}
}
#[cfg(not(target_os = "linux"))]
{
Report {
checks: vec![CheckResult {
name: "platform",
description: "container detection is only implemented on Linux",
matched: false,
}],
is_container: crate::is_container(),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn callable_and_cached() {
let first = is_container();
let second = is_container();
assert_eq!(first, second);
}
#[cfg(not(target_os = "linux"))]
#[test]
fn always_false_off_linux() {
assert!(!is_container());
}
#[cfg(feature = "diagnostics")]
#[test]
fn report_agrees_with_is_container() {
let report = diagnostics::report();
assert_eq!(report.is_container, is_container());
}
#[cfg(all(feature = "diagnostics", target_os = "linux"))]
#[test]
fn report_runs_every_check() {
let report = diagnostics::report();
assert_eq!(report.checks.len(), 4);
}
}