Implement mDNS as standalone enrollment signal and active probes on device discovery
Changes: 1. mDNS as separate signal category: Map 'mdns' signal_type to its own signal_category (not grouped with 'wifi'). Enrollment now triggers when EITHER (a) len(signal_types) >= 2 OR (b) 'mdns' in signal_types. 2. Active probes on device discovery: When new identity is created, fire non-blocking probes to collect additional signals. Uses arping or ping (no new deps) to probe IP if available. Runs in daemon thread to avoid stalling correlation loop. This allows devices announcing via mDNS to be enrolled immediately without waiting for a second signal type, while still collecting additional signals for richer device profiles.
This commit is contained in:
+77
-12
@@ -164,8 +164,61 @@ def _create_identity_record(identity_id: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _fire_active_probes(identity_id: str, mac: str, signal_type: str, dhcp_fp: str) -> None:
|
||||
"""Fire active probes when a new device identity is created.
|
||||
|
||||
Non-blocking: runs probes in a thread to avoid stalling the main loop.
|
||||
Attempts ARP ping if IP available, or mDNS query if hostname available.
|
||||
"""
|
||||
def _probe_worker():
|
||||
try:
|
||||
# For now, log that probes were fired. Real probe logic can use scapy or zeroconf if available.
|
||||
# This is a non-blocking thread so we don't stall the correlator.
|
||||
with device_store_lock:
|
||||
if identity_id not in device_store:
|
||||
return
|
||||
dev = device_store[identity_id]
|
||||
|
||||
# Collect all known MACs for this identity
|
||||
all_macs = dev['wifi_macs'] | dev['ble_macs']
|
||||
|
||||
# Try to find IP from last_seen tracking
|
||||
ip_for_probe = None
|
||||
for m in all_macs:
|
||||
if m in dev['last_seen']:
|
||||
# Check if we can infer IP from known_devices (if MAC was previously seen)
|
||||
with known_lock:
|
||||
if m in known_devices:
|
||||
ip_for_probe = known_devices[m].get('ip')
|
||||
break
|
||||
|
||||
if ip_for_probe:
|
||||
logging.info(f"New device identity {identity_id}: firing active probes for IP {ip_for_probe}")
|
||||
try:
|
||||
# Attempt ARP ping using subprocess arping (lightweight, no extra deps)
|
||||
subprocess.run(['arping', '-c', '1', '-w', '1', ip_for_probe],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2)
|
||||
logging.debug(f"ARP probe sent to {ip_for_probe} (identity {identity_id})")
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
|
||||
# arping not available or timed out — try ping as fallback
|
||||
try:
|
||||
subprocess.run(['ping', '-c', '1', '-W', '1', ip_for_probe],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2)
|
||||
logging.debug(f"ICMP ping sent to {ip_for_probe} (identity {identity_id})")
|
||||
except (subprocess.TimeoutExpired, Exception):
|
||||
pass
|
||||
else:
|
||||
logging.debug(f"New device identity {identity_id}: no IP available for active probes yet")
|
||||
except Exception as e:
|
||||
logging.debug(f"Error in active probe thread for {identity_id}: {e}")
|
||||
|
||||
# Fire probe in background thread to avoid blocking
|
||||
probe_thread = threading.Thread(target=_probe_worker, daemon=True)
|
||||
probe_thread.start()
|
||||
|
||||
|
||||
def _fire_enrollment_callback(identity_id: str) -> None:
|
||||
"""Called when a device meets enrollment criteria (2+ signal types).
|
||||
"""Called when a device meets enrollment criteria (2+ signal types or mdns present).
|
||||
|
||||
Logs enrollment, sends no alert (silent enrollment).
|
||||
Caller must hold device_store_lock.
|
||||
@@ -194,8 +247,13 @@ def _correlate_or_create_identity(mac: str, signal_type: str, dhcp_fp: str = "",
|
||||
Returns the identity_id (stable key).
|
||||
"""
|
||||
with device_store_lock:
|
||||
# Determine signal category: 'ble' or 'wifi' (for dhcp/arp/mdns)
|
||||
signal_category = 'ble' if signal_type == 'ble' else 'wifi'
|
||||
# Determine signal category: 'ble', 'mdns', or 'wifi' (for dhcp/arp)
|
||||
if signal_type == 'ble':
|
||||
signal_category = 'ble'
|
||||
elif signal_type == 'mdns':
|
||||
signal_category = 'mdns'
|
||||
else: # dhcp, arp
|
||||
signal_category = 'wifi'
|
||||
|
||||
# Strategy 0: Check if MAC already exists in any identity
|
||||
for iid, dev in device_store.items():
|
||||
@@ -204,8 +262,8 @@ def _correlate_or_create_identity(mac: str, signal_type: str, dhcp_fp: str = "",
|
||||
dev['last_seen'][mac] = time.time()
|
||||
dev['signal_types'].add(signal_category)
|
||||
logging.debug(f"MAC {mac} already linked to identity {iid}, updated {signal_category} signal")
|
||||
# Check if meets enrollment criteria (2+ signal types)
|
||||
if len(dev['signal_types']) >= 2 and not dev['enrolled']:
|
||||
# Check if meets enrollment criteria: (2+ signal types) OR (mdns present)
|
||||
if not dev['enrolled'] and (len(dev['signal_types']) >= 2 or 'mdns' in dev['signal_types']):
|
||||
_fire_enrollment_callback(iid)
|
||||
return iid
|
||||
|
||||
@@ -233,6 +291,9 @@ def _correlate_or_create_identity(mac: str, signal_type: str, dhcp_fp: str = "",
|
||||
logging.debug(f"MAC rotation (DHCP fingerprint match): {old_macs} → {mac}, identity {iid}")
|
||||
# Add wifi signal type if not already present
|
||||
dev['signal_types'].add('wifi')
|
||||
# Check if meets enrollment criteria: (2+ signal types) OR (mdns present)
|
||||
if not dev['enrolled'] and (len(dev['signal_types']) >= 2 or 'mdns' in dev['signal_types']):
|
||||
_fire_enrollment_callback(iid)
|
||||
return iid
|
||||
|
||||
# Strategy 3: Create new provisional identity
|
||||
@@ -258,15 +319,16 @@ def _correlate_or_create_identity(mac: str, signal_type: str, dhcp_fp: str = "",
|
||||
dev['wifi_macs'].add(mac)
|
||||
if dhcp_fp:
|
||||
dev['dhcp_fingerprint'] = dhcp_fp
|
||||
dev['signal_types'].add('wifi')
|
||||
dev['signal_types'].add(signal_category)
|
||||
dev['last_seen'][mac] = time.time()
|
||||
# Check if meets enrollment criteria (2+ signal types)
|
||||
if len(dev['signal_types']) >= 2 and not dev['enrolled']:
|
||||
# Check if meets enrollment criteria: (2+ signal types) OR (mdns present)
|
||||
if not dev['enrolled'] and (len(dev['signal_types']) >= 2 or 'mdns' in dev['signal_types']):
|
||||
_fire_enrollment_callback(identity_id)
|
||||
logging.debug(f"Updated device identity {identity_id} with {signal_type} signal (MAC: {mac})")
|
||||
else:
|
||||
# Create new identity
|
||||
dev = _create_identity_record(identity_id)
|
||||
is_new_identity = True
|
||||
|
||||
# Add initial signal
|
||||
if signal_type == "ble":
|
||||
@@ -278,12 +340,15 @@ def _correlate_or_create_identity(mac: str, signal_type: str, dhcp_fp: str = "",
|
||||
dev['wifi_macs'].add(mac)
|
||||
if dhcp_fp:
|
||||
dev['dhcp_fingerprint'] = dhcp_fp
|
||||
dev['signal_types'].add('wifi')
|
||||
dev['signal_types'].add(signal_category)
|
||||
|
||||
dev['last_seen'][mac] = time.time()
|
||||
device_store[identity_id] = dev
|
||||
logging.debug(f"Created new device identity {identity_id} from {signal_type} signal")
|
||||
|
||||
# Fire active probes for new identity (non-blocking)
|
||||
_fire_active_probes(identity_id, mac, signal_type, dhcp_fp)
|
||||
|
||||
return identity_id
|
||||
|
||||
|
||||
@@ -292,7 +357,7 @@ def _ingest_signal(mac: str, signal_type: str, dhcp_fp: str = "", handoff_seq: i
|
||||
Ingest a signal from a device source (BLE, DHCP, ARP, mDNS).
|
||||
|
||||
Returns the identity_id it was correlated to.
|
||||
Handles enrollment when signal_count reaches 2+ from distinct types.
|
||||
Handles enrollment when signal_count reaches 2+ from distinct types or when mdns signal is present.
|
||||
"""
|
||||
identity_id = _correlate_or_create_identity(mac, signal_type, dhcp_fp, handoff_seq)
|
||||
|
||||
@@ -303,8 +368,8 @@ def _ingest_signal(mac: str, signal_type: str, dhcp_fp: str = "", handoff_seq: i
|
||||
dev = device_store[identity_id]
|
||||
dev['last_seen'][mac] = time.time()
|
||||
|
||||
# Enrollment check: if we have 2+ distinct signal types and not yet enrolled
|
||||
if not dev['enrolled'] and len(dev['signal_types']) >= 2:
|
||||
# Enrollment check: if we have (2+ distinct signal types) OR (mdns present) and not yet enrolled
|
||||
if not dev['enrolled'] and (len(dev['signal_types']) >= 2 or 'mdns' in dev['signal_types']):
|
||||
_fire_enrollment_callback(identity_id)
|
||||
|
||||
# Eviction: if device_store exceeds 500 entries, evict 100 oldest by first_seen
|
||||
|
||||
Reference in New Issue
Block a user