From d1e89bddf5c6abd82638e7c3d8a0ca4e0d0a7fa0 Mon Sep 17 00:00:00 2001 From: Cobra Date: Fri, 10 Apr 2026 22:57:40 -0400 Subject: [PATCH] Add three dynamic infrastructure auto-suppression mechanisms to net_alerter - Option A: OUI-based auto-exclusion for network equipment (Ubiquiti, Cisco, Aruba, TP-Link, Netgear, MikroTik, Ruckus) - Option B: Churn-based suppression (cycles 3+ times in 30min auto-suppresses device) - Option C: Verify NET_ALERTER_INFRA_IPS env var is documented and functional Implementation: - Added NETWORK_EQUIPMENT_OUIS set with 45 common network equipment OUI prefixes - Added _check_churn() function to track departure events within 30min window - Integrated OUI check into on_arrival() and seed_infrastructure_ips() - Integrated churn check into on_departure() before timer start - Enhanced seed_infrastructure_ips() docstring to document all 5 seeding steps - Removed interactive BB_ALERTER_INFRA_IPS prompt from operator_setup.sh (post-deploy config only) --- net_alerter/net_alerter.py | 74 ++++++++++++++++++++++++++++++++++++-- operator_setup.sh | 6 ---- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/net_alerter/net_alerter.py b/net_alerter/net_alerter.py index 6979582..9090696 100644 --- a/net_alerter/net_alerter.py +++ b/net_alerter/net_alerter.py @@ -44,6 +44,38 @@ _interface_recovering = False _recovery_timer = None +# Network equipment OUI prefixes (first 3 octets, uppercase no colons) +# Auto-exclusion for known infrastructure devices +NETWORK_EQUIPMENT_OUIS = { + # Ubiquiti + "0418D6", "24A43C", "44D9E7", "68729C", "788A20", "802AA8", + "9C05D6", "DC9FDB", "E063DA", "F09FC2", "6C63F8", "B4FBE4", + "00156D", "002722", "04186F", + # Cisco + "000C29", "001A2F", "001BB1", "001E13", "001F26", "002155", + "0060B0", "00902B", "008024", "0090AB", "00E014", "001011", + # Aruba + "000B86", "00FC8B", "040E3C", "1C28AF", "40E3D6", "6C29D1", + "84D47E", "884774", "94B40F", "AC3744", + # TP-Link + "000AEB", "001777", "1C61B4", "5C63BF", "6046D9", "A84E3F", + "B4B024", "C46AB7", "EC086B", "F81A67", + # Netgear + "001B2F", "00146C", "002196", "00A0CC", "081102", "1C1E29", + "206A8A", "30469A", "4F2AFF", "6CB0CE", "8C3BAD", + # MikroTik + "4C5E0C", "B8069F", "C4AD34", "CC2DE0", "D4CA6D", "E48D8C", + # Ruckus + "000C67", "00249B", "04BD88", "14778B", "2C5D93", "34A84E", +} + +# Churn tracking — auto-suppresses devices that cycle 3+ times in 30min +_churn_tracker = {} # {mac: [timestamps of departure events]} +_churn_lock = threading.Lock() +CHURN_THRESHOLD = 3 # cycles in window before suppression +CHURN_WINDOW_SEC = 1800 # 30 minutes + + def _check_flap(mac: str) -> bool: """Return True if this departure is part of an interface flap (suppress it).""" global _interface_recovering, _recovery_timer @@ -68,7 +100,20 @@ def _clear_recovery(): logging.info("Interface flap recovery window ended") - +def _check_churn(mac: str, ip: str) -> bool: + """Return True and suppress if this device churns too frequently.""" + now = time.time() + with _churn_lock: + events = _churn_tracker.get(mac, []) + events = [t for t in events if now - t < CHURN_WINDOW_SEC] + events.append(now) + _churn_tracker[mac] = events + if len(events) >= CHURN_THRESHOLD: + with known_lock: + infrastructure_ips.add(ip) + logging.info(f"Churn suppression: {ip} (MAC:{mac}) cycled {len(events)}x in {CHURN_WINDOW_SEC//60}min — auto-added to infrastructure") + return True + return False # Top 200 common device OUI prefixes (MAC first 3 octets) OUI_DICT = { "88A29E": "Apple", @@ -448,6 +493,12 @@ def on_arrival(mac: str, ip: str, hostname: str = "") -> None: if ip in infrastructure_ips: logging.debug(f"Ignoring infrastructure IP {ip} (MAC:{mac})") return + # OUI-based auto-exclusion for network equipment + oui = mac.replace(":", "").upper()[:6] + if oui in NETWORK_EQUIPMENT_OUIS: + infrastructure_ips.add(ip) + logging.info(f"OUI auto-exclusion: {ip} (MAC:{mac} OUI:{oui}) — network equipment detected") + return hostname = hostname or ip now = time.time() @@ -540,6 +591,9 @@ def on_departure(mac: str) -> None: if _check_flap(mac): logging.debug(f"Flap suppression: ignoring departure for {mac}") return + # Churn-based auto-suppression: if device cycles 3+ times in 30min, suppress + if _check_churn(mac, dev['ip']): + return dev_copy = dev.copy() # Mark departing instead of popping — allows re-arrival to detect and cancel timer @@ -621,7 +675,16 @@ def get_local_ip(iface: str) -> str: def seed_infrastructure_ips(iface: str) -> None: """ Seed infrastructure IPs that should never trigger alerts. - Reads gateway from 'ip route show default', adds broadcast and self IP. + + Seeding steps: + 1. Self IP (this device's interface IP) + 2. Gateway IP (from 'ip route show default') + 3. Broadcast IP (x.x.x.255) + 4. Network equipment by OUI (Ubiquiti, Cisco, Aruba, TP-Link, Netgear, MikroTik, Ruckus) + 5. Operator-provided IPs via NET_ALERTER_INFRA_IPS env var (comma-separated) + + NET_ALERTER_INFRA_IPS is a post-deploy configuration for operators to add + infrastructure IPs known after deployment (access points, switches, etc). """ global infrastructure_ips infrastructure_ips.clear() @@ -661,6 +724,13 @@ def seed_infrastructure_ips(iface: str) -> None: except Exception: pass + # Auto-exclude known network equipment by OUI + with known_lock: + for mac, dev in list(known_devices.items()): + oui = mac.replace(":", "").upper()[:6] + if oui in NETWORK_EQUIPMENT_OUIS: + infrastructure_ips.add(dev['ip']) + logging.info(f"OUI auto-exclusion: {dev['ip']} (MAC:{mac} OUI:{oui}) — network equipment detected") logging.info(f"Infrastructure IPs: {infrastructure_ips}") # Allow operator to add additional IPs via env var (comma-separated) diff --git a/operator_setup.sh b/operator_setup.sh index 75adb26..05d1186 100755 --- a/operator_setup.sh +++ b/operator_setup.sh @@ -405,9 +405,6 @@ if [[ "$enable_alerter" =~ ^[yY]$ ]]; then BB_ALERTER_WEBHOOK="$ntfy_url" fi - # Infrastructure IPs to exclude from alerts (access points, switches, etc.) - prompt_default alerter_infra_ips "Infrastructure IPs to exclude (comma-separated, optional)" "" - BB_ALERTER_INFRA_IPS="$alerter_infra_ips" fi # BLE Presence Detection @@ -714,9 +711,6 @@ if [[ -n "$BB_ALERTER_MATRIX_HS" ]]; then echo "BB_ALERTER_MATRIX_ROOM=\"${BB_ALERTER_MATRIX_ROOM}\"" >> "${MOUNT_POINT}/root/bb-config.env" echo "BB_ALERTER_MATRIX_TOKEN=\"${BB_ALERTER_MATRIX_TOKEN}\"" >> "${MOUNT_POINT}/root/bb-config.env" fi -if [[ -n "$BB_ALERTER_INFRA_IPS" ]]; then - echo "BB_ALERTER_INFRA_IPS=\"${BB_ALERTER_INFRA_IPS}\"" >> "${MOUNT_POINT}/root/bb-config.env" -fi # Store Tailscale auth key if needed if [[ "$BB_VPN" == "tailscale" && -n "$TAILSCALE_KEY" ]]; then