diff --git a/net_alerter/net_alerter.py b/net_alerter/net_alerter.py index e2cc4c5..dd9d23a 100644 --- a/net_alerter/net_alerter.py +++ b/net_alerter/net_alerter.py @@ -1456,9 +1456,46 @@ def _update_last_seen(mac: str) -> None: with last_seen_lock: last_seen[mac] = time.time() +def _extract_mdns_hostname(payload: bytes) -> str: + """Scan mDNS DNS payload for an A record with a .local name. Returns display name (sans .local) or ''.""" + try: + if len(payload) < 12: + return "" + qdcount = struct.unpack('!H', payload[4:6])[0] + ancount = struct.unpack('!H', payload[6:8])[0] + nscount = struct.unpack('!H', payload[8:10])[0] + arcount = struct.unpack('!H', payload[10:12])[0] + offset = 12 + for _ in range(min(qdcount, 50)): + offset = _skip_dns_name(payload, offset) + if offset is None or offset + 4 > len(payload): + return "" + offset += 4 + for _ in range(min(ancount + nscount + arcount, 200)): + if offset is None or offset + 10 > len(payload): + break + name_offset = offset + offset = _skip_dns_name(payload, offset) + if offset is None or offset + 10 > len(payload): + break + record_type = struct.unpack('!H', payload[offset:offset+2])[0] + rdlen = struct.unpack('!H', payload[offset+8:offset+10])[0] + offset += 10 + if record_type == 1: # A record — name is the device hostname + name = _extract_dns_name(payload, name_offset) + if name and name.endswith('.local'): + return name[:-6] + if offset + rdlen > len(payload): + break + offset += rdlen + except Exception: + pass + return "" + + def _parse_mdns_for_names(payload: bytes, mac: str) -> None: """Parse mDNS DNS message to extract PTR/SRV/A record names and check against personal_names. - + If a record name matches a name in personal_names, add the source MAC to personal_devices. """ try: @@ -1501,18 +1538,37 @@ def _parse_mdns_for_names(payload: bytes, mac: str) -> None: # Extract the name string from the record name = _extract_dns_name(payload, name_offset) - if name and personal_names: - with personal_lock: - # Check if the record name matches any personal device names - for pname in personal_names: - if pname.lower() in name.lower(): - # Found a match - add this MAC to personal_devices - mac_normalized = _normalize_mac(mac) - if mac_normalized: - personal_devices.add(mac_normalized) - logging.debug(f"mDNS auto-discovery: added {mac_normalized} for name match '{pname}' in '{name}'") - break - + if name: + # A record (type 1): record name IS the Bonjour hostname (e.g. Johns-iPhone.local) + # Update known_devices hostname if currently unset or just an IP address + if record_type == 1 and name.endswith('.local'): + display_name = name[:-6] # strip .local + mac_normalized = _normalize_mac(mac) + if mac_normalized and display_name: + with known_lock: + dev = known_devices.get(mac_normalized) + if dev: + current = dev.get('hostname', '') + # Only overwrite if hostname is an IP or unset + try: + socket.inet_aton(current) + is_ip = True + except Exception: + is_ip = not current or current == mac_normalized + if is_ip or not current: + dev['hostname'] = display_name + logging.debug(f"mDNS hostname update: {mac_normalized} → {display_name}") + + if personal_names: + with personal_lock: + for pname in personal_names: + if pname.lower() in name.lower(): + mac_normalized = _normalize_mac(mac) + if mac_normalized: + personal_devices.add(mac_normalized) + logging.debug(f"mDNS auto-discovery: added {mac_normalized} for name match '{pname}' in '{name}'") + break + # Skip RDATA if offset + rdlen > len(payload): break @@ -1637,10 +1693,12 @@ def parse_frame(data: bytes) -> None: with known_lock: already_known = src_mac in known_devices if not already_known: - on_arrival(src_mac, src_ip) + # Try to extract Bonjour hostname from this frame before alerting + mdns_hostname = _extract_mdns_hostname(data[42:]) if len(data) >= 42 else "" + on_arrival(src_mac, src_ip, mdns_hostname) # Ingest mDNS signal for multi-source device identity _ingest_signal(src_mac, 'mdns') - # Parse mDNS DNS message for device names + # Parse mDNS DNS message for device names (updates hostname on subsequent frames) if len(data) >= 42: _parse_mdns_for_names(data[42:], src_mac) return