Fix#605: Remove MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID from module-level globals.
Credentials are now read at call time in send_alert() via _read_env_value() to minimize
credential lifetime in process memory.
Fix#606: Update deploy.sh to use Infisical creds CLI instead of .secrets file.
Fetches NET_ALERTER_PASSWORD from matrix folder in Infisical vault, then generates
access token at deploy time.
All 16 net_alerter tests pass when run individually or in groups.
Two P1 bugs fixed:
1. Timer cancel deadlock: timer.cancel() was called while holding locks, which would deadlock if the callback was running/paused waiting for those same locks. Fixed by extracting timers outside of all locks before calling cancel().
2. MAC validation: _normalize_mac() now validates format with regex and returns empty string for invalid MACs. Callers (load_personal_macs_from_env, is_personal_device) skip empty results and log warnings.
All 16 tests pass.
- P1-1: Fix lock ordering deadlock by establishing canonical order (known_lock → departure_lock → occupancy_lock). Refactor on_arrival() to acquire locks in correct order. Fix send_departure_alert() callback to hold occupancy_lock while updating state.
- P1-2: Add _normalize_mac() helper to strip colons/hyphens, apply to load_personal_macs_from_env() and is_personal_device() for consistent MAC comparison.
- P1-3: Remove occupancy_lock acquisition from _update_occupancy_state_unlocked() - callers must hold lock. Update all callers to acquire occupancy_lock before calling.
All individual tests pass. Fixes allow on_arrival() and timer callbacks to execute without deadlock.
- Add MOBILE_DEVICE_OUIS set with 55+ OUI prefixes for personal devices
- Add is_personal_device() function to detect personal devices by OUI or manual config
- Add load_personal_macs_from_env() to parse NET_ALERTER_PERSONAL_MACS
- Add location_occupancy state tracking (VACANT/OCCUPIED/UNKNOWN)
- Add update_occupancy_state() function with thread-safe alerts on transitions
- Add _update_occupancy_state_unlocked() helper to prevent deadlock
- Fix deadlock in on_arrival() by using unlocked version when holding known_lock
- Update test suite with 5 new occupancy tests
- Fix test assertions to exclude occupancy alerts for device arrival tests
Tests now call cleanup_flap_state() at the start of each test to properly
reset the interface flap detection state (_flap_window, _interface_recovering,
_recovery_timer) between tests, preventing state carryover failures.
- Fix 1: Interface flap detection suppresses departure storms when wlan0 briefly loses association and triggers simultaneous RTM_DELNEIGH events for all cached neighbors
- Tracks 3-second window of departure MACs
- Locks recovery state for 60 seconds if 3+ departures occur in rapid succession
- Prevents false positive alerts during interface recovery
- Fix 2: Additional infrastructure IPs via NET_ALERTER_INFRA_IPS env var
- Allows operator to suppress alerts for devices like access points that cycle regularly
- Comma-separated list of IP addresses to be seeded as infrastructure at startup
- Example: NET_ALERTER_INFRA_IPS=192.168.1.214,192.168.1.220
Addresses DevTrack #467 (net_alerter false positive alerts)
- Reject dash/colon MAC-format device names (e.g. 45-48-39-7F-73-C7)
from open mode — were slipping past is_generic_name()
- Add exponential backoff (1s, 2s) on Matrix 429/5xx, max 3 attempts —
startup burst was rate-limiting and dropping alerts
- Commit active scanner flag (scanning_mode=active) that was in working
tree but not in prior commit 8686eb5 despite the commit message
- Add 3 tests: MAC format filter, 429 retry success, 429 give-up
Fixes DevTrack #518
Passive mode fails on Debian bookworm bleak 0.20.2 without or_patterns filter.
Active mode is functionally equivalent for building-level presence detection
and actually yields more complete advertisement data including device names.
When on_departure popped the device from known_devices, rapid RTM_NEWNEIGH
events during ARP cache rebuild each triggered on_arrival as if the device
was brand new, flooding the Matrix room with 5-10+ duplicate ARRIVED alerts.
Fix: keep device in known_devices with departing=True flag. on_arrival
silently cancels pending departure timers and returns without alerting when
it sees this flag. Timer callback checks flag before sending the alert and
only pops the device when departure is confirmed after 15 minutes.
- Bug: device departs → timer starts, but last_departed_time not set yet
If device re-arrives before timer fires, timer never cancelled
Result: false DEPARTED alert sent after re-arrival debounce expires
- Fix: Cancel any pending departure timer immediately on re-arrival,
before checking last_departed_time
- Test: Added regression test that verifies timer is cancelled when
device re-arrives before 15-min debounce expires, preventing false alerts
- All 10 tests passing
- lookup_oui() now tries SQLite first (/opt/net_alerter/oui.db)
- Falls back to inline OUI_DICT if DB missing or query fails
- No network calls, local SQLite reads only
- Debug-level logging for failures (no output noise)
- Infrastructure IPs (gateway, self, broadcast) silently ignored in
on_arrival() and on_departure(); seeded at startup from 'ip route'
- 15-minute departure debounce: alert only fires if device is still
absent after 900s, eliminating ARP cache flush false positives
- 5-minute re-arrival suppression: ARRIVED alert skipped when device
returns within 5 min of a recorded departure (covers interface flap)
- DHCP renewal dedup: known device re-announcing within 30 min skips
ARRIVED alert
- Tests added for all four suppression paths (9 total, all passing)
- 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
- Removed get_local_subnets() and scan_subnet() — no longer needed
- Added read_arp_cache() to read kernel ARP table from /proc/net/arp
- Added fallback to 'ip neigh show' if /proc/net/arp unavailable
- Added lookup_oui() to query system /usr/share/hwdata/oui.txt for vendor names
- Removed subprocess nmap call and re import (no longer needed)
- run() now directly reads ARP cache instead of scanning subnets
- Zero active network traffic — purely passive monitoring
- Removed sudo requirement
Parse nmap vendor name from MAC Address line (filtering out 'Unknown').
Fall back to socket.gethostbyaddr for IPs nmap didn't resolve a hostname
for. Alert label is now: 'hostname (vendor)' | 'hostname' | 'vendor' |
'unknown', in that priority order.
urllib's default Python-urllib user agent is flagged by Cloudflare Bot
Fight Mode (error 1010) when m.mealeyfamily.com homeserver sits behind
Cloudflare. Add a browser-like User-Agent to Matrix PUT/POST requests so
Cloudflare passes the traffic through.