Extract Bonjour hostname from mDNS and use it in arrival/departure alerts
_extract_mdns_hostname scans A records in mDNS payload before on_arrival fires, so the ARRIVED alert shows the device's actual name instead of the IP. _parse_mdns_for_names also now updates known_devices hostname on subsequent frames, so departure alerts get the real name even if the first frame had no A record.
This commit is contained in:
+73
-15
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user