From cf380bc9ebc9ea5be1fcbe83b57082b3ba95bd39 Mon Sep 17 00:00:00 2001 From: Cobra Date: Tue, 14 Apr 2026 23:06:05 -0400 Subject: [PATCH] Prevent labeled devices from being silently seeded; detect opcode=2 ARP for labeled MACs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seed_from_arp_cache was adding labeled personal devices to known_devices at startup, causing all subsequent arrivals to be ignored (already_known=True). Now skips them so on_arrival fires normally when they rejoin. ARP opcode=2 (replies) are now treated as genuine signals for explicitly labeled devices. iPhones with privacy MACs frequently emit only opcode=2 after initial association — the AP proxy forgery concern doesn't apply to devices the operator has explicitly named. --- net_alerter/net_alerter.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/net_alerter/net_alerter.py b/net_alerter/net_alerter.py index a1222c2..c3613e1 100644 --- a/net_alerter/net_alerter.py +++ b/net_alerter/net_alerter.py @@ -827,6 +827,17 @@ def seed_from_arp_cache(): if mac == "00:00:00:00:00:00" or flags == "0x0": continue + # Labeled personal devices must NOT be silently seeded — they need + # a real on_arrival call so ARRIVED fires when they rejoin. + # Just update last_seen as a baseline so the watchdog has a reference. + with personal_lock: + is_personal = mac in device_labels or mac in personal_devices + if is_personal: + with last_seen_lock: + if mac not in last_seen: + last_seen[mac] = time.time() + continue + vendor = lookup_oui(mac) with known_lock: if mac not in known_devices: @@ -1579,10 +1590,14 @@ def parse_frame(data: bytes) -> None: # Branch A: ARP frame (ethertype 0x0806) if eth_type == 0x0806: - # Only ARP requests (opcode 1) are genuine device-originated signals. - # ARP replies (opcode 2) may be AP ARP proxy forgeries: the AP answers - # on behalf of disconnected clients with the client MAC as Ethernet source. - if len(data) >= 22 and struct.unpack('!H', data[20:22])[0] == 1: + opcode = struct.unpack('!H', data[20:22])[0] if len(data) >= 22 else 0 + # Opcode 1 (request) is a genuine device signal. + # Opcode 2 (reply) may be an AP ARP proxy forgery — EXCEPT for explicitly + # labeled personal devices where we trust the frame regardless, because + # iPhones with privacy MACs often only emit opcode=2 after initial association. + with personal_lock: + is_labeled = src_mac in device_labels + if opcode == 1 or (opcode == 2 and is_labeled): _update_last_seen(src_mac) # Personal devices first seen via ARP get no DHCP/Netlink event. # Fire on_arrival so they get ARRIVED alert and occupancy counts.