Exclude infrastructure MACs from personal device detection via NET_ALERTER_INFRA_MACS

Travel routers and other LAA-bit devices get misclassified as personal phones
because the LAA heuristic fires on any MAC with bit 1 set (b6:cc is 0xb6).
Add infrastructure_macs set checked before the LAA heuristic in is_personal_device().
NET_ALERTER_INFRA_MACS env var (comma-separated MACs) populates the set at startup.
This commit is contained in:
Cobra
2026-04-14 22:10:28 -04:00
parent 409752eb2a
commit 7154e2b295
+19
View File
@@ -34,6 +34,7 @@ ENV_PATH = Path(os.getenv("NET_ALERTER_ENV", "/opt/net_alerter/.env"))
known_devices = {} # {mac: {ip, hostname, vendor, first_seen, last_seen, infrastructure (opt)}}
known_lock = threading.Lock()
infrastructure_ips = set() # IPs that should never trigger alerts (gateway, broadcast, self)
infrastructure_macs = set() # MACs that should never be treated as personal devices
departure_timers = {} # {mac: threading.Timer} for 15-min departure debounce
departure_lock = threading.Lock() # Protects departure_timers and last_departed_time
last_departed_time = {} # {mac: timestamp} when device was actually alerted as departed
@@ -683,6 +684,19 @@ def load_personal_macs_from_env() -> None:
logging.info(f"Loaded {len(personal_devices)} personal device MAC(s) and {len(personal_names)} personal name(s)")
def load_infrastructure_macs_from_env() -> None:
"""Parse NET_ALERTER_INFRA_MACS env var into infrastructure_macs set."""
global infrastructure_macs
macs_str = _read_env_value("NET_ALERTER_INFRA_MACS") or os.getenv("NET_ALERTER_INFRA_MACS", "")
for mac in (m.strip() for m in macs_str.split(",") if m.strip()):
normalized = _normalize_mac(mac)
if normalized:
infrastructure_macs.add(normalized)
logging.info(f"Seeded infrastructure MAC {mac} — excluded from personal device detection")
else:
logging.warning(f"Skipping invalid MAC from NET_ALERTER_INFRA_MACS: {mac}")
def _read_env_value(key: str) -> str:
"""Read a single environment variable from .env file or system env."""
if ENV_PATH.exists():
@@ -750,6 +764,10 @@ def is_personal_device(mac: str) -> bool:
if oui in MOBILE_DEVICE_OUIS:
return True
# Infrastructure MACs are never personal, regardless of LAA bit
if mac_normalized in infrastructure_macs:
return False
# Locally-administered bit: modern phones randomize MACs; TVs/infra don't
try:
return bool(int(mac_normalized[:2], 16) & 0x02)
@@ -1825,6 +1843,7 @@ def main() -> None:
logging.info(f"Net alerter starting on interface {iface}")
seed_infrastructure_ips(iface)
load_infrastructure_macs_from_env()
seed_from_arp_cache()
# Start monitor threads