#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
mod atomic;
pub mod engine;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UsbId {
pub vid: u16,
pub pid: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MacQuirk {
pub vid: u16,
pub pid: Option<u16>,
pub label: &'static str,
}
pub const MAC_QUIRKS: &[MacQuirk] = &[MacQuirk {
vid: 0xa69c,
pid: None,
label: "AICSemi AIC8800-family USB WiFi (no efuse MAC)",
}];
pub const KNOWN_STABLE_EFUSE: &[MacQuirk] = &[MacQuirk {
vid: 0x0bda,
pid: None,
label: "Realtek RTL88xx USB WiFi (efuse MAC)",
}];
fn table_match(table: &[MacQuirk], id: UsbId) -> Option<&'static str> {
table.iter().find_map(|q| {
if q.vid == id.vid && q.pid.map(|p| p == id.pid).unwrap_or(true) {
Some(q.label)
} else {
None
}
})
}
pub fn is_quirk_randomizer(id: UsbId) -> Option<&'static str> {
table_match(MAC_QUIRKS, id)
}
pub fn is_known_stable_efuse(id: UsbId) -> bool {
table_match(KNOWN_STABLE_EFUSE, id).is_some()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MacAddr(pub [u8; 6]);
impl MacAddr {
pub fn is_unicast(&self) -> bool {
self.0[0] & 0x01 == 0
}
pub fn is_locally_administered(&self) -> bool {
self.0[0] & 0x02 != 0
}
pub fn parse(s: &str) -> Option<MacAddr> {
let mut bytes = [0u8; 6];
let mut n = 0;
for part in s.split([':', '-']) {
if n >= 6 {
return None;
}
bytes[n] = u8::from_str_radix(part.trim(), 16).ok()?;
n += 1;
}
if n == 6 {
Some(MacAddr(bytes))
} else {
None
}
}
}
impl std::fmt::Display for MacAddr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let b = self.0;
write!(
f,
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
b[0], b[1], b[2], b[3], b[4], b[5]
)
}
}
impl Serialize for MacAddr {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for MacAddr {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
MacAddr::parse(&s).ok_or_else(|| serde::de::Error::custom("invalid MAC address"))
}
}
pub fn derive_pinned_mac(machine_id: &str, salt: &str) -> Option<MacAddr> {
if machine_id.trim().is_empty() {
return None;
}
let mut hasher = Sha256::new();
if salt.is_empty() {
hasher.update(machine_id.as_bytes());
} else {
hasher.update(machine_id.as_bytes());
hasher.update(b":");
hasher.update(salt.as_bytes());
}
let digest = hasher.finalize();
Some(MacAddr([
0x02, digest[0], digest[1], digest[2], digest[3], digest[4],
]))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AdapterState {
Stable,
Pinned,
Candidate,
Deferred,
Disabled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AdapterSource {
Quirk,
Learned,
Override,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdapterVerdict {
pub name: String,
pub vidpid: String,
pub usb_path: String,
pub state: AdapterState,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<AdapterSource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pinned_mac: Option<MacAddr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_seen_mac: Option<MacAddr>,
#[serde(default)]
pub applied_live: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub link_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deferred_reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LearnerRecord {
pub vidpid: String,
pub usb_path: String,
pub last_mac: MacAddr,
pub first_seen: u64,
pub boot_count: u32,
pub mac_change_count: u32,
}
pub const MAC_PINS_STATE_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MacPinsState {
pub version: u32,
#[serde(default)]
pub adapters: Vec<AdapterVerdict>,
#[serde(default)]
pub learner: Vec<LearnerRecord>,
#[serde(default)]
pub updated_at: u64,
}
impl Default for MacPinsState {
fn default() -> Self {
MacPinsState {
version: MAC_PINS_STATE_VERSION,
adapters: Vec::new(),
learner: Vec::new(),
updated_at: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quirk_table_matches_aic_vendor_family() {
assert!(is_quirk_randomizer(UsbId {
vid: 0xa69c,
pid: 0x8d81
})
.is_some());
assert!(is_quirk_randomizer(UsbId {
vid: 0xa69c,
pid: 0x0001
})
.is_some());
assert!(is_quirk_randomizer(UsbId {
vid: 0x0bda,
pid: 0xa81a
})
.is_none());
assert!(is_quirk_randomizer(UsbId {
vid: 0x1234,
pid: 0x5678
})
.is_none());
}
#[test]
fn efuse_allowlist_protects_the_rtl_radio() {
assert!(is_known_stable_efuse(UsbId {
vid: 0x0bda,
pid: 0xa81a
}));
assert!(is_known_stable_efuse(UsbId {
vid: 0x0bda,
pid: 0x8812
}));
assert!(!is_known_stable_efuse(UsbId {
vid: 0xa69c,
pid: 0x8d81
}));
}
#[test]
fn derive_matches_the_proven_rig_value() {
let mac = derive_pinned_mac("03851cd61fc642d781d3f93a00e624cd", "").unwrap();
assert_eq!(mac.to_string(), "02:c6:75:83:1a:3e");
}
#[test]
fn derived_mac_is_unicast_and_locally_administered() {
let mac = derive_pinned_mac("any-machine-id", "").unwrap();
assert!(mac.is_unicast());
assert!(mac.is_locally_administered());
assert_eq!(mac.0[0], 0x02);
}
#[test]
fn salt_distinguishes_two_adapters_on_one_box() {
let a = derive_pinned_mac("mid", "5-1.3").unwrap();
let b = derive_pinned_mac("mid", "5-1.4").unwrap();
assert_ne!(a, b);
let single = derive_pinned_mac("mid", "").unwrap();
assert_ne!(a, single);
}
#[test]
fn empty_machine_id_skips() {
assert!(derive_pinned_mac("", "").is_none());
assert!(derive_pinned_mac(" ", "5-1.3").is_none());
}
#[test]
fn mac_parse_round_trips() {
let m = MacAddr::parse("02:C6:75:83:1A:3E").unwrap();
assert_eq!(m.to_string(), "02:c6:75:83:1a:3e");
assert_eq!(MacAddr::parse("02-c6-75-83-1a-3e").unwrap(), m);
assert!(MacAddr::parse("not-a-mac").is_none());
assert!(MacAddr::parse("02:c6:75:83:1a").is_none());
}
#[test]
fn state_file_serializes_to_the_expected_shape() {
let st = MacPinsState {
version: MAC_PINS_STATE_VERSION,
adapters: vec![AdapterVerdict {
name: "wlan0".into(),
vidpid: "a69c:8d81".into(),
usb_path: "5-1.3".into(),
state: AdapterState::Pinned,
source: Some(AdapterSource::Quirk),
pinned_mac: MacAddr::parse("02:c6:75:83:1a:3e"),
last_seen_mac: MacAddr::parse("88:00:33:77:f3:bd"),
applied_live: false,
link_file: Some("/etc/systemd/network/10-ados-mac-wlan0.link".into()),
deferred_reason: None,
}],
learner: Vec::new(),
updated_at: 1_700_000_000,
};
let json = serde_json::to_string(&st).unwrap();
assert!(json.contains("\"state\":\"pinned\""));
assert!(json.contains("\"source\":\"quirk\""));
assert!(json.contains("\"pinned_mac\":\"02:c6:75:83:1a:3e\""));
let back: MacPinsState = serde_json::from_str(&json).unwrap();
assert_eq!(back, st);
}
}