Add deauth/disassoc detection — firmware capture, coordinator ingestion, Alerts tab with burst detection
This commit is contained in:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user