Deduce vendor from mDNS services for privacy MACs

Modern devices increasingly use randomized ("Private WiFi Address") MACs,
which are locally administered and have no real OUI, so the MAC-prefix
vendor lookup returns nothing. Such devices still advertise distinctive
mDNS service types, which we now use to deduce their vendor.

- Extend the mDNS parser to also capture PTR service types alongside
  A-record hostnames.
- Add a service-type -> vendor mapping (data/mdns-service-vendor.json) and
  a service_vendor_finder, using canonical vendor names so device-type
  resolution still chains.
- Add utils::network::is_locally_administered to detect randomized MACs.
- In the mDNS scanner, fall back to service-based deduction only when the
  OUI lookup is empty and the MAC is locally administered.
- Group the three finders under a new data module.
- Preserve a known vendor (and its derived device_type) when a later
  sighting cannot deduce one, and suppress the spurious "vendor changed"
  notification in that case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-05-29 13:56:39 -04:00
co-authored by Claude Opus 4.7
parent 7fb457ca4a
commit 98a23aa7fd
12 changed files with 308 additions and 32 deletions
+19 -5
View File
@@ -7,11 +7,11 @@ use log::{debug, error, info, warn};
use crate::db;
use crate::device_finders::mdns;
use crate::events;
use crate::mac_vendor_finder;
use crate::data::mac_vendor_finder;
use crate::mdns_scanner_status;
use crate::model::devices::Device;
use crate::settings::get_settings;
use crate::vendor_device_type_finder;
use crate::data::vendor_device_type_finder;
/// Passively listen for mDNS/Bonjour announcements and feed discovered devices into the same
/// pipeline used by the ARP scanner (devices table + events + notifications).
@@ -38,18 +38,27 @@ pub async fn listen() -> Result<(), Box<dyn std::error::Error>> {
IpAddr::V6(_) => continue, // the device model is IPv4-only
};
let hostname = match mdns::parse_announcement(&buf[..len]).into_iter().next() {
let announcement = mdns::parse_announcement(&buf[..len]);
let hostname = match announcement.hostnames.into_iter().next() {
Some(host) => host,
None => continue,
};
process_announcement(src_ip, hostname, interface.clone(), probe_timeout).await;
process_announcement(
src_ip,
hostname,
announcement.service_types,
interface.clone(),
probe_timeout,
)
.await;
}
}
async fn process_announcement(
src_ip: Ipv4Addr,
hostname: String,
service_types: Vec<String>,
interface: Option<String>,
probe_timeout: Duration,
) {
@@ -61,7 +70,12 @@ async fn process_announcement(
}
};
let vendor = mac_vendor_finder::find(mac.get(0..8).unwrap_or("").to_string());
let mut vendor = mac_vendor_finder::find(mac.get(0..8).unwrap_or("").to_string());
// Privacy MACs are locally administered and have no real OUI, so the lookup above fails.
// Fall back to the vendor-specific mDNS services the device advertises.
if vendor.is_empty() && crate::utils::network::is_locally_administered(&mac) {
vendor = crate::data::service_vendor_finder::find(&service_types);
}
let mut device = Device::new(
mac.clone(),
src_ip.to_string(),