1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6
7mod atomic;
8pub mod engine;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct UsbId {
13 pub vid: u16,
15 pub pid: u16,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct MacQuirk {
23 pub vid: u16,
25 pub pid: Option<u16>,
27 pub label: &'static str,
29}
30
31pub const MAC_QUIRKS: &[MacQuirk] = &[MacQuirk {
41 vid: 0xa69c,
42 pid: None,
43 label: "AICSemi AIC8800-family USB WiFi (no efuse MAC)",
44}];
45
46pub const KNOWN_STABLE_EFUSE: &[MacQuirk] = &[MacQuirk {
52 vid: 0x0bda,
53 pid: None,
54 label: "Realtek RTL88xx USB WiFi (efuse MAC)",
55}];
56
57fn table_match(table: &[MacQuirk], id: UsbId) -> Option<&'static str> {
58 table.iter().find_map(|q| {
59 if q.vid == id.vid && q.pid.map(|p| p == id.pid).unwrap_or(true) {
60 Some(q.label)
61 } else {
62 None
63 }
64 })
65}
66
67pub fn is_quirk_randomizer(id: UsbId) -> Option<&'static str> {
69 table_match(MAC_QUIRKS, id)
70}
71
72pub fn is_known_stable_efuse(id: UsbId) -> bool {
74 table_match(KNOWN_STABLE_EFUSE, id).is_some()
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct MacAddr(pub [u8; 6]);
80
81impl MacAddr {
82 pub fn is_unicast(&self) -> bool {
84 self.0[0] & 0x01 == 0
85 }
86
87 pub fn is_locally_administered(&self) -> bool {
90 self.0[0] & 0x02 != 0
91 }
92
93 pub fn parse(s: &str) -> Option<MacAddr> {
95 let mut bytes = [0u8; 6];
96 let mut n = 0;
97 for part in s.split([':', '-']) {
98 if n >= 6 {
99 return None;
100 }
101 bytes[n] = u8::from_str_radix(part.trim(), 16).ok()?;
102 n += 1;
103 }
104 if n == 6 {
105 Some(MacAddr(bytes))
106 } else {
107 None
108 }
109 }
110}
111
112impl std::fmt::Display for MacAddr {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 let b = self.0;
115 write!(
116 f,
117 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
118 b[0], b[1], b[2], b[3], b[4], b[5]
119 )
120 }
121}
122
123impl Serialize for MacAddr {
124 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
125 s.serialize_str(&self.to_string())
126 }
127}
128
129impl<'de> Deserialize<'de> for MacAddr {
130 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
131 let s = String::deserialize(d)?;
132 MacAddr::parse(&s).ok_or_else(|| serde::de::Error::custom("invalid MAC address"))
133 }
134}
135
136pub fn derive_pinned_mac(machine_id: &str, salt: &str) -> Option<MacAddr> {
150 if machine_id.trim().is_empty() {
151 return None;
152 }
153 let mut hasher = Sha256::new();
154 if salt.is_empty() {
155 hasher.update(machine_id.as_bytes());
156 } else {
157 hasher.update(machine_id.as_bytes());
158 hasher.update(b":");
159 hasher.update(salt.as_bytes());
160 }
161 let digest = hasher.finalize();
162 Some(MacAddr([
163 0x02, digest[0], digest[1], digest[2], digest[3], digest[4],
164 ]))
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum AdapterState {
171 Stable,
173 Pinned,
175 Candidate,
178 Deferred,
181 Disabled,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "snake_case")]
188pub enum AdapterSource {
189 Quirk,
191 Learned,
193 Override,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct AdapterVerdict {
200 pub name: String,
202 pub vidpid: String,
204 pub usb_path: String,
206 pub state: AdapterState,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub source: Option<AdapterSource>,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub pinned_mac: Option<MacAddr>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub last_seen_mac: Option<MacAddr>,
217 #[serde(default)]
219 pub applied_live: bool,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub link_file: Option<String>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub deferred_reason: Option<String>,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct LearnerRecord {
233 pub vidpid: String,
235 pub usb_path: String,
237 pub last_mac: MacAddr,
239 pub first_seen: u64,
241 pub boot_count: u32,
243 pub mac_change_count: u32,
245}
246
247pub const MAC_PINS_STATE_VERSION: u32 = 1;
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253pub struct MacPinsState {
254 pub version: u32,
256 #[serde(default)]
258 pub adapters: Vec<AdapterVerdict>,
259 #[serde(default)]
260 pub learner: Vec<LearnerRecord>,
262 #[serde(default)]
264 pub updated_at: u64,
265}
266
267impl Default for MacPinsState {
268 fn default() -> Self {
269 MacPinsState {
270 version: MAC_PINS_STATE_VERSION,
271 adapters: Vec::new(),
272 learner: Vec::new(),
273 updated_at: 0,
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn quirk_table_matches_aic_vendor_family() {
284 assert!(is_quirk_randomizer(UsbId {
285 vid: 0xa69c,
286 pid: 0x8d81
287 })
288 .is_some());
289 assert!(is_quirk_randomizer(UsbId {
290 vid: 0xa69c,
291 pid: 0x0001
292 })
293 .is_some());
294 assert!(is_quirk_randomizer(UsbId {
295 vid: 0x0bda,
296 pid: 0xa81a
297 })
298 .is_none());
299 assert!(is_quirk_randomizer(UsbId {
300 vid: 0x1234,
301 pid: 0x5678
302 })
303 .is_none());
304 }
305
306 #[test]
307 fn efuse_allowlist_protects_the_rtl_radio() {
308 assert!(is_known_stable_efuse(UsbId {
309 vid: 0x0bda,
310 pid: 0xa81a
311 }));
312 assert!(is_known_stable_efuse(UsbId {
313 vid: 0x0bda,
314 pid: 0x8812
315 }));
316 assert!(!is_known_stable_efuse(UsbId {
317 vid: 0xa69c,
318 pid: 0x8d81
319 }));
320 }
321
322 #[test]
323 fn derive_matches_the_proven_rig_value() {
324 let mac = derive_pinned_mac("03851cd61fc642d781d3f93a00e624cd", "").unwrap();
328 assert_eq!(mac.to_string(), "02:c6:75:83:1a:3e");
329 }
330
331 #[test]
332 fn derived_mac_is_unicast_and_locally_administered() {
333 let mac = derive_pinned_mac("any-machine-id", "").unwrap();
334 assert!(mac.is_unicast());
335 assert!(mac.is_locally_administered());
336 assert_eq!(mac.0[0], 0x02);
337 }
338
339 #[test]
340 fn salt_distinguishes_two_adapters_on_one_box() {
341 let a = derive_pinned_mac("mid", "5-1.3").unwrap();
342 let b = derive_pinned_mac("mid", "5-1.4").unwrap();
343 assert_ne!(a, b);
344 let single = derive_pinned_mac("mid", "").unwrap();
345 assert_ne!(a, single);
346 }
347
348 #[test]
349 fn empty_machine_id_skips() {
350 assert!(derive_pinned_mac("", "").is_none());
351 assert!(derive_pinned_mac(" ", "5-1.3").is_none());
352 }
353
354 #[test]
355 fn mac_parse_round_trips() {
356 let m = MacAddr::parse("02:C6:75:83:1A:3E").unwrap();
357 assert_eq!(m.to_string(), "02:c6:75:83:1a:3e");
358 assert_eq!(MacAddr::parse("02-c6-75-83-1a-3e").unwrap(), m);
359 assert!(MacAddr::parse("not-a-mac").is_none());
360 assert!(MacAddr::parse("02:c6:75:83:1a").is_none());
361 }
362
363 #[test]
364 fn state_file_serializes_to_the_expected_shape() {
365 let st = MacPinsState {
366 version: MAC_PINS_STATE_VERSION,
367 adapters: vec![AdapterVerdict {
368 name: "wlan0".into(),
369 vidpid: "a69c:8d81".into(),
370 usb_path: "5-1.3".into(),
371 state: AdapterState::Pinned,
372 source: Some(AdapterSource::Quirk),
373 pinned_mac: MacAddr::parse("02:c6:75:83:1a:3e"),
374 last_seen_mac: MacAddr::parse("88:00:33:77:f3:bd"),
375 applied_live: false,
376 link_file: Some("/etc/systemd/network/10-ados-mac-wlan0.link".into()),
377 deferred_reason: None,
378 }],
379 learner: Vec::new(),
380 updated_at: 1_700_000_000,
381 };
382 let json = serde_json::to_string(&st).unwrap();
383 assert!(json.contains("\"state\":\"pinned\""));
384 assert!(json.contains("\"source\":\"quirk\""));
385 assert!(json.contains("\"pinned_mac\":\"02:c6:75:83:1a:3e\""));
386 let back: MacPinsState = serde_json::from_str(&json).unwrap();
387 assert_eq!(back, st);
388 }
389}