use std::path::{Path, PathBuf};
use crate::error::Error;
use crate::source::{Probe, Source, SourceKind};
use crate::sources::util::{NormalizeOutcome, classify, read_capped};
macro_rules! file_source {
($name:ident, $kind:expr, $default:expr, $doc:literal) => {
#[doc = $doc]
#[derive(Debug, Clone)]
pub struct $name {
path: PathBuf,
}
impl $name {
#[doc = concat!("Read from the standard path (`", $default, "`).")]
#[must_use]
pub fn new() -> Self {
Self {
path: PathBuf::from($default),
}
}
#[must_use]
pub fn at(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
impl Default for $name {
fn default() -> Self {
Self::new()
}
}
impl Source for $name {
fn kind(&self) -> SourceKind {
$kind
}
fn probe(&self) -> Result<Option<Probe>, Error> {
read_id_file($kind, &self.path)
}
}
};
}
file_source!(
MachineIdFile,
SourceKind::MachineId,
"/etc/machine-id",
"`/etc/machine-id` — the systemd-managed primary host identifier on modern Linux."
);
file_source!(
DbusMachineIdFile,
SourceKind::DbusMachineId,
"/var/lib/dbus/machine-id",
"`/var/lib/dbus/machine-id` — D-Bus machine ID. Often a symlink to `/etc/machine-id` \
but present on its own on some minimal images."
);
#[derive(Debug, Clone)]
pub struct DmiProductUuid {
path: PathBuf,
}
impl DmiProductUuid {
#[must_use]
pub fn new() -> Self {
Self {
path: PathBuf::from("/sys/class/dmi/id/product_uuid"),
}
}
#[must_use]
pub fn at(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
impl Default for DmiProductUuid {
fn default() -> Self {
Self::new()
}
}
impl Source for DmiProductUuid {
fn kind(&self) -> SourceKind {
SourceKind::Dmi
}
fn probe(&self) -> Result<Option<Probe>, Error> {
read_dmi_file(&self.path)
}
}
const DMI_PLACEHOLDER_UUIDS: &[&str] = &[
"03000200-0400-0500-0006-000700080009",
];
fn is_dmi_garbage(value: &str) -> bool {
let lower = value.to_ascii_lowercase();
if DMI_PLACEHOLDER_UUIDS.iter().any(|p| *p == lower) {
return true;
}
is_all_same_nibble_uuid(&lower)
}
fn is_all_same_nibble_uuid(value: &str) -> bool {
let mut chars = value.chars().filter(|c| *c != '-');
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_hexdigit() {
return false;
}
let mut count = 1usize;
for c in chars {
if c != first {
return false;
}
count += 1;
}
count == 32
}
fn read_dmi_file(path: &Path) -> Result<Option<Probe>, Error> {
match read_id_file(SourceKind::Dmi, path)? {
Some(probe) if is_dmi_garbage(probe.value()) => {
log::debug!(
"host-identity: DMI product_uuid {} matches a known vendor-placeholder; \
falling through",
probe.value()
);
Ok(None)
}
other => Ok(other),
}
}
fn read_id_file(kind: SourceKind, path: &Path) -> Result<Option<Probe>, Error> {
match read_capped(path) {
Ok(content) => match classify(&content) {
NormalizeOutcome::Usable(value) => Ok(Some(Probe::new(kind, value))),
NormalizeOutcome::Sentinel => Err(Error::Uninitialized {
source_kind: kind,
path: PathBuf::from(path),
}),
NormalizeOutcome::Empty => Ok(None),
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
log::debug!(
"host-identity: permission denied reading {}",
path.display()
);
Ok(None)
}
Err(source) => Err(Error::Io {
source_kind: kind,
path: PathBuf::from(path),
source,
}),
}
}
#[must_use]
pub(crate) fn in_container() -> bool {
const MARKERS: &[&str] = &["docker", "kubepods", "containerd", "podman", "lxc", "crio"];
Path::new("/.dockerenv").exists()
|| std::fs::read_to_string("/proc/1/cgroup").is_ok_and(|cgroup| {
cgroup
.split(['/', ':', '-', '.', '_', '\n'])
.any(|seg| MARKERS.contains(&seg))
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::{NamedTempFile, TempDir};
#[test]
fn machine_id_file_rejects_uninitialized_sentinel() {
let mut f = NamedTempFile::new().unwrap();
writeln!(f, "uninitialized").unwrap();
let err = read_id_file(SourceKind::MachineId, f.path()).expect_err("sentinel must error");
match err {
Error::Uninitialized { path, source_kind } => {
assert_eq!(path, f.path());
assert_eq!(source_kind, SourceKind::MachineId);
}
other => panic!("expected Uninitialized, got {other:?}"),
}
}
#[test]
fn machine_id_file_accepts_normal_value() {
let mut f = NamedTempFile::new().unwrap();
writeln!(f, "abc123").unwrap();
let probe = read_id_file(SourceKind::MachineId, f.path())
.unwrap()
.unwrap();
assert_eq!(probe.value(), "abc123");
}
#[test]
fn machine_id_file_missing_is_none() {
let dir = TempDir::new().unwrap();
let missing = dir.path().join("definitely-not-there");
let probe = read_id_file(SourceKind::MachineId, &missing).unwrap();
assert!(probe.is_none());
}
#[test]
fn machine_id_file_empty_is_none() {
let f = NamedTempFile::new().unwrap();
let probe = read_id_file(SourceKind::MachineId, f.path()).unwrap();
assert!(probe.is_none());
}
#[test]
fn machine_id_file_whitespace_only_is_none() {
let mut f = NamedTempFile::new().unwrap();
write!(f, " \n\t ").unwrap();
let probe = read_id_file(SourceKind::MachineId, f.path()).unwrap();
assert!(probe.is_none());
}
#[test]
fn machine_id_file_reports_io_error_for_directory() {
let dir = TempDir::new().unwrap();
let err = read_id_file(SourceKind::MachineId, dir.path())
.expect_err("reading a directory must error");
match err {
Error::Io { path, .. } => assert_eq!(path, dir.path()),
other => panic!("expected Io, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn machine_id_file_permission_denied_is_none() {
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
struct PermGuard(PathBuf);
impl Drop for PermGuard {
fn drop(&mut self) {
let _ = std::fs::set_permissions(&self.0, std::fs::Permissions::from_mode(0o600));
}
}
if nix_is_root() {
return;
}
let mut f = NamedTempFile::new().unwrap();
writeln!(f, "abc123").unwrap();
let path: &Path = f.path();
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o000)).unwrap();
let _guard = PermGuard(path.to_path_buf());
let probe = read_id_file(SourceKind::MachineId, path)
.expect("permission denied should be swallowed to Ok(None)");
assert!(probe.is_none());
}
fn dmi_tempfile(body: &str) -> NamedTempFile {
let mut f = NamedTempFile::new().unwrap();
write!(f, "{body}").unwrap();
f
}
fn dmi_probe(body: &str) -> Option<Probe> {
let f = dmi_tempfile(body);
read_dmi_file(f.path()).unwrap()
}
#[test]
fn dmi_rejects_all_zero_uuid() {
assert!(dmi_probe("00000000-0000-0000-0000-000000000000\n").is_none());
}
#[test]
fn dmi_rejects_all_f_uuid_lower() {
assert!(dmi_probe("ffffffff-ffff-ffff-ffff-ffffffffffff\n").is_none());
}
#[test]
fn dmi_rejects_all_f_uuid_upper() {
assert!(dmi_probe("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF\n").is_none());
}
#[test]
fn dmi_rejects_all_same_nibble_1() {
assert!(dmi_probe("11111111-1111-1111-1111-111111111111\n").is_none());
}
#[test]
fn dmi_rejects_all_same_nibble_a() {
assert!(dmi_probe("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\n").is_none());
}
#[test]
fn dmi_rejects_supermicro_ami_placeholder() {
assert!(dmi_probe("03000200-0400-0500-0006-000700080009\n").is_none());
}
#[test]
fn dmi_rejects_supermicro_ami_placeholder_uppercase() {
assert!(
dmi_probe(
"03000200-0400-0500-0006-000700080009"
.to_ascii_uppercase()
.as_str()
)
.is_none()
);
}
#[test]
fn dmi_rejects_garbage_with_trailing_whitespace() {
assert!(dmi_probe(" 00000000-0000-0000-0000-000000000000 \n\t").is_none());
}
#[test]
fn dmi_accepts_plausible_real_uuid() {
let probe = dmi_probe("4c4c4544-0039-5710-8052-b4c04f384833\n").unwrap();
assert_eq!(probe.value(), "4c4c4544-0039-5710-8052-b4c04f384833");
}
#[test]
fn dmi_accepts_non_uuid_shape() {
let probe = dmi_probe("abcdef\n").unwrap();
assert_eq!(probe.value(), "abcdef");
}
#[test]
fn machine_id_file_accepts_all_zero_uuid() {
let f = dmi_tempfile("00000000-0000-0000-0000-000000000000\n");
let probe = read_id_file(SourceKind::MachineId, f.path())
.unwrap()
.unwrap();
assert_eq!(probe.value(), "00000000-0000-0000-0000-000000000000");
}
#[cfg(unix)]
fn nix_is_root() -> bool {
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|s| {
s.lines()
.find_map(|l| l.strip_prefix("Uid:"))
.and_then(|l| l.split_whitespace().next().map(str::to_owned))
})
.is_some_and(|uid| uid == "0")
}
}