Expand description
§ados-macpin
Stable MAC addresses for Linux network adapters that randomize theirs every boot: chipset quirk table, deterministic per-machine derivation and systemd-networkd .link files.
Some USB WiFi chipsets have no MAC burned into efuse, so their driver invents a new random address each time it loads. On a DHCP network that means a new lease and a new IP every boot, which makes the machine hard to find. This crate pins a stable, locally administered MAC derived from /etc/machine-id, written as a systemd-networkd .link drop-in that takes effect on the next boot without touching the live interface. Reach for it on any embedded Linux box with a randomizing adapter.
use std::path::PathBuf;
use ados_macpin::engine::{adapter_key, link_file_name, render_link_file, PinLayout, NETWORKD_DIR};
use ados_macpin::{derive_pinned_mac, is_quirk_randomizer, UsbId};
// An AIC8800-family USB WiFi adapter: a known randomizer.
let id = UsbId { vid: 0xa69c, pid: 0x8d81 };
assert!(is_quirk_randomizer(id).is_some());
// Same machine-id and USB port in, same MAC out, on every boot.
let mac = derive_pinned_mac("03851cd61fc642d781d3f93a00e624cd", "5-1.3").unwrap();
assert!(mac.is_locally_administered() && mac.is_unicast());
let layout = PinLayout {
state_path: PathBuf::from("/var/lib/myapp/mac-pins.state"),
networkd_dir: PathBuf::from(NETWORKD_DIR),
link_prefix: "10-myapp-mac-".into(),
owner_label: "myapp service".into(),
};
let key = adapter_key("a69c:8d81", "5-1.3");
let name = link_file_name(&layout, &key);
let body = render_link_file(&layout, "Path=platform-xhci-hcd.0-usb-0:1.3:1.0", &mac);
assert_eq!(name, format!("10-myapp-mac-{}.link", "a69c-8d81@5-1.3"));
assert!(body.contains(&format!("MACAddress={mac}")));
assert!(body.contains("NamePolicy=kernel"));§Highlights
- Deterministic: the MAC is
02:plus five bytes ofsha256(machine_id:salt), so it is stable per machine and distinct across machines and USB ports. - Refuses unsafe
.linkfiles: a[Match]block that does not name exactly one adapter (a glob, a negation, a bareDriver=) is rejected before anything is written. - Next-boot only: provisioning writes a file and reloads the udev rules database; it never runs
udevadm triggeron thenetsubsystem and never changes a live address unless you callapply_live. - Every path is yours: the state file, drop-in directory, file prefix and header label come from a
PinLayout. - A cross-boot learner flags unknown adapters whose MAC keeps changing, for an operator to confirm.
- Async entry points (
apply_live,remove_pin_link) runipandudevadmbounded undertokio::processwithkill_on_drop.
§How it works
Detection does not trust two signals that look authoritative but lie in practice: the kernel’s addr_assign_type can read 0 (permanent) while the adapter randomizes, and the random MAC can carry a real-looking vendor OUI with the locally administered bit clear. So randomization is identified by chipset VID:PID (MAC_QUIRKS) or by observing the MAC change across boots for a stable device identity. Chipsets in KNOWN_STABLE_EFUSE are never pinned.
An adapter is identified by its model plus the USB port it sits in (adapter_key, vvvv:pppp@<usb_path>), which survives kernel renames and enumeration-order changes. classify_adapter is the pure decision: an operator override wins, then the stable-efuse allowlist, then the quirk table (pin when enabled, a systemd-udev link mechanism exists and a machine-id is available), then the learner (a candidate after LEARN_THRESHOLD MAC changes).
The .link file matches on the adapter’s udev ID_PATH (Path=), sets NamePolicy=kernel so the interface keeps its name, and sets an explicit MACAddress=. That key is unconditional, which is why match_block_is_specific guards every write: a wildcard match would hand one address to every interface on the machine.
On Linux, reconcile runs the whole pass: enumerate /sys/class/net, fold the learner, classify, write or sweep .link files and persist a MacPinsState JSON document atomically. On other platforms the sysfs functions are stubs, so the pure logic builds and tests anywhere. The engine module also has helpers that project the state file to camelCase JSON for a status API.
§Part of ADOS
Used in production by the ADOS Drone Agent, the onboard software of the ADOS software-defined drone platform. All ADOS crates · Docs · Discord
§Other ADOS crates
Platform (versioned with the ADOS Drone Agent)
| Crate | What it does | Docs |
|---|---|---|
ados-protocol | Wire contracts of the ADOS Drone Agent: IPC framing, plugin RPC envelope, capability tokens and the vision frame bus. | docs.rs |
ados-sdk | Write Rust plugins for the ADOS Drone Agent: IPC client, plugin context, driver traits and lifecycle runner. | docs.rs |
ados-vision | Single-object visual tracker: Kalman motion model, appearance re-identification and YOLOv8 head decoding. | docs.rs |
ados-gpio | Drive status buzzers and LEDs on Linux GPIO lines, with bounded software-PWM beep patterns. | docs.rs |
ados-hal-probe | Find out what a board’s hardware can really do by probing it, plus bundled board profiles. | docs.rs |
Standalone
| Crate | What it does | Docs |
|---|---|---|
ados-sensorhub | Sensor hub wire protocol: COBS framing, CRC-16 and fixed-layout IMU, baro, mag and GNSS packets. no_std. | docs.rs |
ados-offload | Choose where perception runs (onboard, offloaded or hybrid) and hold the vehicle when results go stale. | docs.rs |
ados-lora | LoRa transmit governor with cited regional band plans, duty cycle, dwell time and airtime accounting. | docs.rs |
ados-crsf-codec | no_std CRSF / ExpressLRS codec for RC channels, telemetry and parameter frames. | docs.rs |
ados-positioning | Split mixed UBX, NMEA and RTCM3 GNSS streams, configure u-blox receivers and run an RTK base survey-in. | docs.rs |
ados-rtcm | RTCM3 framing, CRC-24Q and MAVLink GPS_RTCM_DATA fragmentation for RTK corrections. | docs.rs |
ados-clock | Map remote clocks onto the host clock: least-squares and envelope fits, TIMESYNC and a chrony sink. | docs.rs |
ados-swarm | Swarm control laws: separation, flocking, formations, CBBA task allocation and a point-mass simulator. | docs.rs |
ados-mavlink-tunnel | Send large messages over MAVLink TUNNEL frames with chunking and bounded reassembly. | docs.rs |
ados-display-driver | Small-display output on Linux: SSD1306/SH1106 OLED over I2C, pixel packers and a framebuffer writer. | docs.rs |
ados-hmi | Front-panel input: touchscreen calibration and gestures, button presses and rotary encoders. | docs.rs |
§ADOS repositories
| Repo | Runs on | What it does | Link |
|---|---|---|---|
| ADOS Mission Control | Browser, desktop (Electron) | Ground control station: FC setup, mission planning, flight, 3D simulation, fleet | ADOSMissionControl |
| ADOS Drone Agent | Companion computer on the drone, or a ground node | Onboard agent: MAVLink routing, HD video, long-range radio link, plugins, remote control | ADOSDroneAgent |
| ADOS Android GCS | Android phones and tablets | Native ground control app for agents on the LAN, direct flight controllers and the cloud | ADOSAndroidGCS |
| ADOS Extensions | Drone Agent and Mission Control | First-party signed plugins, each with an agent half and a GCS half | ADOSExtensions |
| ADOS MCP | Your machine | Model Context Protocol server that lets AI clients operate ADOS drones under scopes and a safety gate | ADOS-MCP |
| Documentation | docs.altnautica.com | Public docs for every ADOS product | Documentation |
§License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Modules§
- engine
- The pinning engine: sysfs enumeration, the per-adapter decision, and the
systemd-networkd.linkprovisioning. The pure parts (render_link_file,parse_match_block,match_block_is_specific,parse_udev_id_path,classify_adapter) are unit-tested without I/O; the sysfs /udevadm/ subprocess parts are Linux-gated.
Structs§
- Adapter
Verdict - Per-adapter record in the state file.
- Learner
Record - The cross-boot learner’s per-device memory. Keyed by the stable device identity so a churning MAC + churning kernel name still resolves to the same record.
- MacAddr
- A 48-bit MAC address. Serialized as the canonical lowercase colon-hex string.
- MacPins
State - The state-file document: per-adapter verdicts plus the learner memory. The JSON shape may be read by other programs, so extend it additively only.
- MacQuirk
- A chipset-quirk entry: a USB vendor id plus an optional product id. A
Noneproduct matches every product from that vendor (a vendor-family match). - UsbId
- A USB device identity.
Enums§
- Adapter
Source - How an adapter’s randomizer verdict was reached.
- Adapter
State - The verdict for one network adapter, as written to the state file.
Constants§
- KNOWN_
STABLE_ EFUSE - Chipsets known to carry a stable efuse MAC. These are NEVER pinned, even before the cross-boot learner has confirmed them, so a known-good radio’s hardware address is never overwritten.
- MAC_
PINS_ STATE_ VERSION - On-disk schema version of
MacPinsState. - MAC_
QUIRKS - Chipsets that have no efuse MAC and randomize their address each driver load.
Functions§
- derive_
pinned_ mac - Derive the deterministic pinned MAC for a board.
- is_
known_ stable_ efuse - True when
idis a chipset known to carry a stable efuse MAC (never pin it). - is_
quirk_ randomizer - Return the quirk label when
idis a known no-efuse randomizer, elseNone.