Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libdd-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ features = [
"Win32_Foundation",
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_Performance",
"Win32_System_Registry",
"Win32_System_Threading",
]

Expand Down
1 change: 1 addition & 0 deletions libdd-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod connector;
#[cfg(feature = "reqwest")]
pub mod dump_server;
pub mod entity_id;
pub mod machine_id;
pub mod regex_engine;
#[macro_use]
pub mod cstr;
Expand Down
121 changes: 121 additions & 0 deletions libdd-common/src/machine_id/linux.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//! Linux host machine id, mirroring gopsutil's fallback order:
//! `/sys/class/dmi/id/product_uuid` (root-only, usually empty otherwise) →
//! `/etc/machine-id` → `/proc/sys/kernel/random/boot_id`.

use std::path::Path;

fn read_trimmed(path: &Path) -> Option<String> {
let s = std::fs::read_to_string(path).ok()?;
let s = s.trim().to_owned();
if s.is_empty() {
None
} else {
Some(s)
}
}

pub fn get_machine_id_impl_paths(dmi_path: &Path, etc_path: &Path, boot_path: &Path) -> String {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn get_machine_id_impl_paths(dmi_path: &Path, etc_path: &Path, boot_path: &Path) -> String {
/// Resolves the id from the three candidate paths in priority order. Paths are
/// parameters so the fallback logic is unit-testable without real `/sys`/`/etc`.
pub fn get_machine_id_impl_paths(dmi_path: &Path, etc_path: &Path, boot_path: &Path) -> String {

if let Some(id) = read_trimmed(dmi_path) {
return id;
}
// agent compatibility:
// gopsutil only accepts /etc/machine-id when it's exactly 32 chars (bare hex)
if let Some(id) = read_trimmed(etc_path) {
if id.len() == 32 {
return id;
}
}
read_trimmed(boot_path).unwrap_or_default()
}

pub fn get_machine_id_impl() -> String {
get_machine_id_impl_paths(
Path::new("/sys/class/dmi/id/product_uuid"),
Path::new("/etc/machine-id"),
Path::new("/proc/sys/kernel/random/boot_id"),
)
}

#[cfg(test)]
mod tests {
use super::*;

fn write(path: &Path, content: &[u8]) {
std::fs::write(path, content).unwrap();
}

fn tmp_paths(
dir: &tempfile::TempDir,
) -> (std::path::PathBuf, std::path::PathBuf, std::path::PathBuf) {
(
dir.path().join("product_uuid"),
dir.path().join("machine_id"),
dir.path().join("boot_id"),
)
}

#[test]
fn level1_dmi_wins_when_present() {
let dir = tempfile::tempdir().unwrap();
let (dmi, etc, boot) = tmp_paths(&dir);
write(&dmi, b"B08FA8A2-B01A-4D2B-BD95-FEC7E30C5AEC\n");
write(&etc, b"aabbccddaabbccddaabbccddaabbccdd\n");
write(&boot, b"cccccccccccccccccccccccccccccccc\n");
assert_eq!(
get_machine_id_impl_paths(&dmi, &etc, &boot),
"B08FA8A2-B01A-4D2B-BD95-FEC7E30C5AEC"
);
}

#[test]
fn level2_etc_used_when_dmi_absent() {
let dir = tempfile::tempdir().unwrap();
let (dmi, etc, boot) = tmp_paths(&dir);
write(&etc, b"aabbccddaabbccddaabbccddaabbccdd\n");
write(&boot, b"cccccccccccccccccccccccccccccccc\n");
assert_eq!(
get_machine_id_impl_paths(&dmi, &etc, &boot),
"aabbccddaabbccddaabbccddaabbccdd"
);
}

#[test]
fn level2_skipped_when_etc_not_32_chars() {
let dir = tempfile::tempdir().unwrap();
let (dmi, etc, boot) = tmp_paths(&dir);
write(&etc, b"aabbccdd-aabb-ccdd-aabb-ccddaabbccdd\n");
write(&boot, b"dddddddddddddddddddddddddddddddd\n");
assert_eq!(
get_machine_id_impl_paths(&dmi, &etc, &boot),
"dddddddddddddddddddddddddddddddd"
);
}

#[test]
fn level3_boot_id_as_last_resort() {
let dir = tempfile::tempdir().unwrap();
let (dmi, etc, boot) = tmp_paths(&dir);
write(&boot, b"cccccccccccccccccccccccccccccccc\n");
assert_eq!(
get_machine_id_impl_paths(&dmi, &etc, &boot),
"cccccccccccccccccccccccccccccccc"
);
}

#[test]
fn all_absent_returns_empty() {
let dir = tempfile::tempdir().unwrap();
let (dmi, etc, boot) = tmp_paths(&dir);
assert_eq!(get_machine_id_impl_paths(&dmi, &etc, &boot), "");
}

#[test]
fn trims_whitespace() {
let dir = tempfile::tempdir().unwrap();
let (dmi, etc, boot) = tmp_paths(&dir);
write(&etc, b" aabbccddaabbccddaabbccddaabbccdd \n");
assert_eq!(
get_machine_id_impl_paths(&dmi, &etc, &boot),
"aabbccddaabbccddaabbccddaabbccdd"
);
}
}
39 changes: 39 additions & 0 deletions libdd-common/src/machine_id/macos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//! macOS host machine id via `gethostuuid(3)`, returning `IOPlatformUUID`.

/// Returns `IOPlatformUUID` via `gethostuuid(3)`, which avoids a fork+exec of `ioreg`.
pub fn get_machine_id_impl() -> String {
let mut uuid = [0u8; 16];
let wait = libc::timespec {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let wait = libc::timespec {
// Zero timeout: the host UUID is static, so there's nothing to wait for.
let wait = libc::timespec {

tv_sec: 0,
tv_nsec: 0,
};
let rc = unsafe { libc::gethostuuid(uuid.as_mut_ptr(), &wait) };
if rc != 0 {
return String::new();
}
format!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
format!(
// Assemble the 16 raw bytes into the canonical 8-4-4-4-12 hyphenated UUID.
format!(

"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
uuid[0], uuid[1], uuid[2], uuid[3],
uuid[4], uuid[5],
uuid[6], uuid[7],
uuid[8], uuid[9],
uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15],
)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn returns_nonempty_uuid() {
let id = get_machine_id_impl();
assert!(!id.is_empty());
assert_eq!(id.len(), 36);
assert_eq!(&id[8..9], "-");
assert_eq!(&id[13..14], "-");
assert_eq!(&id[18..19], "-");
assert_eq!(&id[23..24], "-");
}
}
135 changes: 135 additions & 0 deletions libdd-common/src/machine_id/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

//! Host machine identifier, mirroring `pkg/util/uuid.GetUUID()` in the Go agent.
//!
//! | Platform | Source |
//! |----------|--------|
//! | Linux | `/sys/class/dmi/id/product_uuid` then `/etc/machine-id` → `/proc/sys/kernel/random/boot_id` |
//! | macOS | `gethostuuid(3)` |
//! | Windows | `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` |
//! | Other | `""` |
//!
//! All values are normalised to lowercase `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
//! Returns `""` on failure rather than a random UUID — the backend can detect
//! a missing value but not a wrong one.

use std::sync::LazyLock;

#[cfg(target_os = "linux")]
mod linux;

#[cfg(target_os = "macos")]
mod macos;

#[cfg(windows)]
mod windows;

/// Normalise a raw OS machine-id to a lowercase hyphenated UUID string.
/// Strips hyphens, filters to hex digits, lowercases, then re-inserts hyphens.
/// Returns `""` if the result is not exactly 32 hex digits.
pub(crate) fn normalize_uuid(raw: &str) -> String {
let hex: String = raw
.chars()
.filter(|c| c.is_ascii_hexdigit())
.flat_map(char::to_lowercase)
.collect();

if hex.len() != 32 {
return String::new();
}

format!(
"{}-{}-{}-{}-{}",
&hex[0..8],
&hex[8..12],
&hex[12..16],
&hex[16..20],
&hex[20..32],
)
}

static MACHINE_ID: LazyLock<String> = LazyLock::new(|| {
let raw = {
#[cfg(target_os = "linux")]
{
linux::get_machine_id_impl()
}
#[cfg(target_os = "macos")]
{
macos::get_machine_id_impl()
}
#[cfg(windows)]
{
windows::get_machine_id_impl()
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
{
String::new()
}
};
normalize_uuid(&raw)
});

/// Returns the host machine ID as a lowercase hyphenated UUID, cached for the process lifetime.
/// Returns `""` on failure or unsupported platforms.
pub fn get_machine_id() -> &'static str {
MACHINE_ID.as_str()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn cached_value_is_stable() {
assert_eq!(get_machine_id(), get_machine_id());
}

#[test]
fn value_has_uuid_shape_if_nonempty() {
let id = get_machine_id();
if id.is_empty() {
return;
}
assert_eq!(id.len(), 36);
for (i, c) in id.chars().enumerate() {
if [8, 13, 18, 23].contains(&i) {
assert_eq!(c, '-');
} else {
assert!(c.is_ascii_hexdigit() && !c.is_ascii_uppercase());
}
}
}

#[test]
fn normalize_bare_hex_inserts_hyphens() {
assert_eq!(
normalize_uuid("b08fa8a2b01a4d2bbd95fec7e30c5aec"),
"b08fa8a2-b01a-4d2b-bd95-fec7e30c5aec"
);
}

#[test]
fn normalize_uppercase_uuid_lowercased() {
assert_eq!(
normalize_uuid("B08FA8A2-B01A-4D2B-BD95-FEC7E30C5AEC"),
"b08fa8a2-b01a-4d2b-bd95-fec7e30c5aec"
);
}

#[test]
fn normalize_lowercase_uuid_unchanged() {
assert_eq!(
normalize_uuid("b08fa8a2-b01a-4d2b-bd95-fec7e30c5aec"),
"b08fa8a2-b01a-4d2b-bd95-fec7e30c5aec"
);
}

#[test]
fn normalize_invalid_returns_empty() {
assert_eq!(normalize_uuid(""), "");
assert_eq!(normalize_uuid("b08fa8a2"), "");
assert_eq!(normalize_uuid("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"), "");
}
}
85 changes: 85 additions & 0 deletions libdd-common/src/machine_id/windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//! Windows host machine id: reads `MachineGuid` from //! `HKLM\SOFTWARE\Microsoft\Cryptography` via the raw Win32 registry API.
//!
//! `MachineGuid` lives in the 64-bit registry view, so a 32-bit process under
//! WOW64 must pass `KEY_WOW64_64KEY` or it gets redirected to the (empty)
//! `WOW6432Node` copy. The value is read with the standard two-call Win32
//! pattern: query the byte size, then read into a right-sized buffer.

use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::System::Registry::{
RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY, HKEY_LOCAL_MACHINE, KEY_READ,
KEY_WOW64_64KEY, REG_SZ,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, IsWow64Process};

fn to_wide_null(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0u16)).collect()
}

fn is_wow64() -> bool {
let mut result: i32 = 0;
let ok = unsafe { IsWow64Process(GetCurrentProcess(), &mut result) };
ok != 0 && result != 0
}

pub fn get_machine_id_impl() -> String {
let access = if cfg!(target_pointer_width = "32") && is_wow64() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Do you need is_wow64()? The agent always does KEY_READ_ | KEY_WOW64_64KEY Won't KEY_WOW64_64KEY be a no-op on 64-bit?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let access = if cfg!(target_pointer_width = "32") && is_wow64() {
// MachineGuid is in the 64-bit view; a 32-bit process under WOW64 is
// redirected to WOW6432Node by default, so force the 64-bit view there.
let access = if cfg!(target_pointer_width = "32") && is_wow64() {

KEY_READ | KEY_WOW64_64KEY
} else {
KEY_READ
};

let mut hkey: HKEY = 0;
// SAFETY: all pointers are valid.
let subkey = to_wide_null("SOFTWARE\\Microsoft\\Cryptography");
let status =
unsafe { RegOpenKeyExW(HKEY_LOCAL_MACHINE, subkey.as_ptr(), 0, access, &mut hkey) };
if status != ERROR_SUCCESS {
return String::new();
}

let value_wide = to_wide_null("MachineGuid");

let mut data_type: u32 = 0;
let mut data_len: u32 = 0;
// SAFETY: null data pointer is valid for a size-query call.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SAFETY: null data pointer is valid for a size-query call.
// SAFETY: null data pointer is valid for a size-query call.
// First call (null data pointer) returns the value's size in bytes.

let status = unsafe {
RegQueryValueExW(
hkey,
value_wide.as_ptr(),
std::ptr::null_mut(),
&mut data_type,
std::ptr::null_mut(),
&mut data_len,
)
};
if status != ERROR_SUCCESS || data_type != REG_SZ {
// SAFETY: hkey is a valid open handle.
unsafe { RegCloseKey(hkey) };
return String::new();
}

let mut buf: Vec<u16> = vec![0u16; (data_len as usize).div_ceil(2)];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let mut buf: Vec<u16> = vec![0u16; (data_len as usize).div_ceil(2)];
// data_len is bytes; REG_SZ holds UTF-16, so the u16 count is bytes/2 (round up).
let mut buf: Vec<u16> = vec![0u16; (data_len as usize).div_ceil(2)];

let mut actual_len = data_len;
// SAFETY: buf has the capacity returned by the size-query call above.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SAFETY: buf has the capacity returned by the size-query call above.
// SAFETY: buf has the capacity returned by the size-query call above.
// Second call reads the value into the size-query'd buffer.

let status = unsafe {
RegQueryValueExW(
hkey,
value_wide.as_ptr(),
std::ptr::null_mut(),
&mut data_type,
buf.as_mut_ptr().cast(),
&mut actual_len,
)
};
// SAFETY: hkey is a valid open handle.
unsafe { RegCloseKey(hkey) };

if status != ERROR_SUCCESS {
return String::new();
}

while buf.last() == Some(&0u16) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
while buf.last() == Some(&0u16) {
// REG_SZ data includes a trailing NUL (possibly padded with more); drop them.
while buf.last() == Some(&0u16) {

buf.pop();
}
String::from_utf16(&buf)
.unwrap_or_default()
.trim()
.to_owned()
}
Loading