Add deauth/disassoc detection — firmware capture, coordinator ingestion, Alerts tab with burst detection

This commit is contained in:
bot
2026-04-03 22:24:25 +03:00
parent cbef4143ee
commit fbf5b5c738
7 changed files with 426 additions and 23 deletions
+64 -1
View File
@@ -57,6 +57,20 @@ def init_db(path: str) -> sqlite3.Connection:
wifi_rssi INTEGER
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS deauth_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
received_at TEXT NOT NULL,
node_id TEXT,
node_ts INTEGER,
subtype TEXT,
src TEXT,
dst TEXT,
bssid TEXT,
reason INTEGER,
rssi INTEGER
)
""")
conn.commit()
return conn
@@ -149,6 +163,42 @@ def validate_heartbeat(ev: dict) -> str | None:
return f"wifi_rssi out of range: {ev['wifi_rssi']}"
return None
def store_deauth(conn: sqlite3.Connection, ev: dict, received_at: str):
conn.execute("""
INSERT INTO deauth_events
(received_at, node_id, node_ts, subtype, src, dst, bssid, reason, rssi)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
received_at,
ev.get("node_id"),
ev.get("ts"),
ev.get("type"),
ev.get("src"),
ev.get("dst"),
ev.get("bssid"),
ev.get("reason"),
ev.get("rssi"),
))
conn.commit()
def validate_deauth(ev: dict) -> str | None:
for field in ("node_id", "src", "dst", "bssid", "reason", "rssi"):
if ev.get(field) is None:
return f"missing field '{field}'"
if not _check_mac(ev["src"]):
return f"bad src format: {ev['src']}"
if not _check_mac(ev["dst"]):
return f"bad dst format: {ev['dst']}"
if not _check_mac(ev["bssid"]):
return f"bad bssid format: {ev['bssid']}"
if not isinstance(ev["reason"], int) or not (0 <= ev["reason"] <= 65535):
return f"invalid reason code: {ev['reason']}"
if not _check_rssi(ev["rssi"]):
return f"rssi out of range: {ev['rssi']}"
return None
def validate_probe(ev: dict) -> str | None:
"""Returns an error string if invalid, else None."""
for field in ("node_id", "src_mac", "rssi"):
@@ -182,7 +232,20 @@ def handle_packet(data: bytes, addr: tuple, conn: sqlite3.Connection):
received_at = datetime.datetime.utcnow().isoformat(timespec="seconds")
pkt_type = ev.get("type", "beacon")
if pkt_type == "heartbeat":
if pkt_type in ("deauth", "disassoc"):
err = validate_deauth(ev)
if err:
print(f"[DROP] {addr[0]} {pkt_type}{err}")
return
store_deauth(conn, ev, received_at)
node = ev.get("node_id", "?")
src = ev.get("src", "?")
dst = ev.get("dst", "?")
bssid = ev.get("bssid", "?")
reason = ev.get("reason", 0)
rssi = ev.get("rssi", 0)
print(f"\033[91m[{received_at}] {node} {pkt_type.upper():<8} {src}{dst} bssid={bssid} reason={reason} {rssi:>4}dBm{RESET}")
elif pkt_type == "heartbeat":
err = validate_heartbeat(ev)
if err:
print(f"[DROP] {addr[0]} heartbeat — {err}")