-
Notifications
You must be signed in to change notification settings - Fork 21
feat(machine id): Add helpers in ddcommon to fetch the machine UUID l… #2163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ea0b993
e17eaed
b480793
d1ddfc2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||||||
|
|
||||||||||
| 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 { | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| 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" | ||||||||||
| ); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| 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 | ||||||||
|
|
||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| /// 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 { | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| tv_sec: 0, | ||||||||
| tv_nsec: 0, | ||||||||
| }; | ||||||||
| let rc = unsafe { libc::gethostuuid(uuid.as_mut_ptr(), &wait) }; | ||||||||
| if rc != 0 { | ||||||||
| return String::new(); | ||||||||
| } | ||||||||
| format!( | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| "{: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], "-"); | ||||||||
| } | ||||||||
| } | ||||||||
| 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"), ""); | ||
| } | ||
| } |
| 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 | ||||||||||||||||
|
|
||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
| 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() { | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Do you need
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
| 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. | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
| 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)]; | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
| let mut actual_len = data_len; | ||||||||||||||||
| // SAFETY: buf has the capacity returned by the size-query call above. | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
| 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) { | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
| buf.pop(); | ||||||||||||||||
| } | ||||||||||||||||
| String::from_utf16(&buf) | ||||||||||||||||
| .unwrap_or_default() | ||||||||||||||||
| .trim() | ||||||||||||||||
| .to_owned() | ||||||||||||||||
| } | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.