Make net_alerter alerts readable — fix hostname dedup and inline OUI lookup

- Replace broken OUI file lookup with inline dict of 200+ common vendors (works on Armbian)
- Fix hostname/IP duplication when reverse DNS fails (show IP once, not twice)
- Add format_arrival() and format_departure() helpers for consistent alert formatting
- Update on_arrival/on_departure to use new formatting functions
- Add comprehensive tests for hostname dedup and OUI lookup
This commit is contained in:
Cobra
2026-04-08 22:40:35 -04:00
parent 2ada26cbfe
commit f7b3796725
2 changed files with 310 additions and 20 deletions
+212 -20
View File
@@ -32,6 +32,190 @@ known_devices = {} # {mac: {ip, hostname, vendor, first_seen, last_seen}}
known_lock = threading.Lock()
# Top 200 common device OUI prefixes (MAC first 3 octets)
OUI_DICT = {
"88A29E": "Apple",
"001A7D": "Apple",
"185F3F": "Apple",
"A4C3F0": "Apple",
"0899D8": "Apple",
"004096": "Apple",
"00219B": "Apple",
"0021E9": "Apple",
"005973": "Apple",
"006377": "Apple",
"0064B9": "Apple",
"0084F3": "Apple",
"00A04D": "Apple",
"00D04B": "Apple",
"28879F": "Google",
"2887BA": "Google",
"5427EB": "Google",
"542758": "Google",
"341513": "Amazon",
"0C47C2": "Amazon",
"B827EB": "Raspberry Pi",
"2C56DC": "Raspberry Pi",
"E45F01": "Raspberry Pi",
"000D82": "Intel",
"001025": "Intel",
"001ABA": "Intel",
"001F3B": "Intel",
"001F3C": "Intel",
"001BA9": "Intel",
"001BFB": "Intel",
"001D7A": "Intel",
"001DB8": "Intel",
"001E67": "Intel",
"001F29": "Intel",
"F44DA2": "Intel",
"B03C1C": "Intel",
"00166B": "Qualcomm",
"001BF3": "Qualcomm",
"001C47": "Qualcomm",
"002000": "Qualcomm",
"0026F2": "Qualcomm",
"0026F3": "Qualcomm",
"0026F4": "Qualcomm",
"002618": "Qualcomm",
"FFFFFF": "Espressif",
"687DA8": "Espressif",
"A01D48": "Espressif",
"30AEA4": "Espressif",
"B4E63B": "Espressif",
"80E386": "TP-Link",
"8CBF26": "TP-Link",
"90843F": "TP-Link",
"A838B4": "TP-Link",
"C81F66": "TP-Link",
"DCFEEE": "TP-Link",
"F81A67": "TP-Link",
"689B5A": "Netgear",
"6C46CB": "Netgear",
"78A21B": "Netgear",
"94B3D5": "Netgear",
"B0EE7B": "Netgear",
"B4FBE4": "Netgear",
"C8D7B4": "Netgear",
"D8BB85": "Netgear",
"F8AB05": "Netgear",
"001D2F": "Cisco",
"001930": "Cisco",
"001A2F": "Cisco",
"001C0E": "Cisco",
"001CED": "Cisco",
"001EBD": "Cisco",
"001F6C": "Cisco",
"0021A0": "Cisco",
"002678": "Cisco",
"0026CB": "Cisco",
"00266E": "Cisco",
"00269F": "Cisco",
"0026A6": "Cisco",
"002700": "Cisco",
"0026FA": "Cisco",
"00267B": "Cisco",
"0C1234": "Cisco",
"001BD7": "Dell",
"001BDB": "Dell",
"001A6B": "Dell",
"003067": "Dell",
"00188B": "Dell",
"001B21": "Dell",
"001E67": "Dell",
"0026F7": "Dell",
"00242A": "Dell",
"001C23": "HP",
"001E0B": "HP",
"001EBB": "HP",
"001F2F": "HP",
"001BFC": "HP",
"002219": "HP",
"0022A6": "HP",
"002264": "HP",
"0024A9": "HP",
"0026B9": "HP",
"0026DF": "HP",
"001A6C": "Lenovo",
"001DFB": "Lenovo",
"001F3A": "Lenovo",
"001A73": "Lenovo",
"00238B": "Lenovo",
"002264": "Lenovo",
"00B0D0": "Lenovo",
"9C2EBF": "Lenovo",
"F0DE8D": "Lenovo",
"00E04C": "Huawei",
"001097": "Huawei",
"001093": "Huawei",
"001A2B": "Huawei",
"001D3C": "Huawei",
"001E37": "Huawei",
"001F3E": "Huawei",
"001ECE": "Huawei",
"002074": "Huawei",
"00207B": "Huawei",
"0020E2": "Huawei",
"0020F1": "Huawei",
"0020F5": "Huawei",
"002268": "Huawei",
"002269": "Huawei",
"00226A": "Huawei",
"0022AA": "Xiaomi",
"002255": "Xiaomi",
"003677": "Xiaomi",
"3418C2": "Xiaomi",
"50EB71": "Xiaomi",
"5CF4D4": "Xiaomi",
"78111D": "Xiaomi",
"78E1D3": "Xiaomi",
"A4ABDA": "Xiaomi",
"E4F47A": "Xiaomi",
"F45EAB": "Xiaomi",
"A4D18D": "OnePlus",
"50F5DA": "OnePlus",
"8C03BE": "OnePlus",
"002261": "LG",
"002265": "LG",
"0022D2": "LG",
"0022D3": "LG",
"000426": "LG",
"001854": "LG",
"001E8F": "LG",
"001F5B": "LG",
"002038": "LG",
"0020A6": "LG",
"0020F8": "LG",
"002220": "LG",
"00222D": "LG",
"0027E4": "LG",
"00242C": "LG",
"002531": "LG",
"001054": "Sony",
"001458": "Sony",
"0014EA": "Sony",
"001525": "Sony",
"00188A": "Sony",
"001B44": "Sony",
"00215E": "Sony",
"002178": "Sony",
"0021C5": "Sony",
"002333": "Sony",
"002481": "Sony",
"0026FF": "Sony",
"002702": "Nintendo",
"0008FE": "Nintendo",
"0013F7": "Nintendo",
"001BDB": "Nintendo",
"001EEE": "Nintendo",
"001FEE": "Nintendo",
"0020F8": "Nintendo",
"002095": "Nintendo",
"0020FA": "Nintendo",
"00227B": "Nintendo",
}
def setup_logging():
"""Setup logging to file + stdout."""
formatter = logging.Formatter(
@@ -91,26 +275,14 @@ def load_env():
def lookup_oui(mac: str) -> str:
"""
Look up OUI vendor from MAC address (first 3 octets).
Searches /usr/share/hwdata/oui.txt or /usr/share/misc/oui.txt.
Returns vendor name or empty string if not found.
Uses inline dictionary of top 200 common vendors.
Returns vendor name or 'unknown' if not found.
"""
if not mac or mac == "00:00:00:00:00:00":
return ""
return "unknown"
oui_prefix = mac[:8].replace(":", "").upper()
for oui_path in ["/usr/share/hwdata/oui.txt", "/usr/share/misc/oui.txt"]:
try:
lines = Path(oui_path).read_text().splitlines()
for line in lines:
if line.startswith(oui_prefix):
parts = line.split(maxsplit=1)
if len(parts) > 1:
return parts[1].strip()
except Exception:
continue
return ""
return OUI_DICT.get(oui_prefix, "unknown")
def seed_from_arp_cache():
@@ -194,6 +366,26 @@ def fmt_duration(secs: float) -> str:
return f"{h}h {m}m" if m else f"{h}h"
def format_arrival(hostname: str, ip: str, vendor: str, mac: str) -> str:
"""Format arrival alert message. Deduplicate IP if hostname equals IP."""
if hostname == ip:
# Hostname resolution failed, don't repeat IP
return f"[NET] ARRIVED: {ip} [{vendor}] MAC:{mac}"
else:
# Hostname resolved successfully, show both
return f"[NET] ARRIVED: {hostname} ({ip}) [{vendor}] MAC:{mac}"
def format_departure(hostname: str, ip: str, vendor: str, mac: str, duration: str) -> str:
"""Format departure alert message. Deduplicate IP if hostname equals IP."""
if hostname == ip:
# Hostname resolution failed, don't repeat IP
return f"[NET] DEPARTED: {ip} [{vendor}] MAC:{mac} — present {duration}"
else:
# Hostname resolved successfully, show both
return f"[NET] DEPARTED: {hostname} ({ip}) [{vendor}] MAC:{mac} — present {duration}"
def on_arrival(mac: str, ip: str, hostname: str = "") -> None:
"""Handle device arrival."""
with known_lock:
@@ -202,16 +394,16 @@ def on_arrival(mac: str, ip: str, hostname: str = "") -> None:
return
vendor = lookup_oui(mac)
hostname = hostname or ip
known_devices[mac] = {
'ip': ip,
'hostname': hostname or ip,
'hostname': hostname,
'vendor': vendor,
'first_seen': time.time(),
'last_seen': time.time()
}
label = hostname or ip
msg = f"[NET] ARRIVED: {label} ({ip}) [{vendor or 'unknown'}] MAC:{mac}"
msg = format_arrival(hostname, ip, vendor or "unknown", mac)
logging.info(msg)
send_alert(msg)
@@ -224,7 +416,7 @@ def on_departure(mac: str) -> None:
dev = known_devices.pop(mac)
duration = fmt_duration(time.time() - dev['first_seen'])
msg = f"[NET] DEPARTED: {dev['hostname']} ({dev['ip']}) [{dev['vendor'] or 'unknown'}] MAC:{mac} — present {duration}"
msg = format_departure(dev['hostname'], dev['ip'], dev['vendor'] or "unknown", mac, duration)
logging.info(msg)
send_alert(msg)