Add net_alerter: new device join alerts via Matrix to condo Pi

This commit is contained in:
n0mad1k
2026-03-24 20:29:24 -04:00
parent 06160b974a
commit d7cdd489ce
2 changed files with 235 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Deploy net_alerter to condo Pi (10.0.0.0)
# Usage: ./deploy.sh [access_token]
set -euo pipefail
PI_USER="pi"
PI_HOST="10.0.0.0"
PI_KEY="$HOME/.ssh/condo"
REMOTE_DIR="/opt/net_alerter"
SCRIPT="$(dirname "$0")/net_alerter.py"
# Matrix access token — get fresh one if not passed
if [[ -n "${1:-}" ]]; then
TOKEN="$1"
else
echo "[*] Fetching Matrix access token..."
TOKEN=$(curl -s -X POST https://m.example.org/_matrix/client/v3/login \
-H "Content-Type: application/json" \
-d '{"type":"m.login.password","identifier":{"type":"m.id.user","user":"spectre"},"password":"spectre-cfe6b5f9dd10dd31a204e4a7"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
fi
echo "[*] Deploying to $PI_HOST..."
ssh -i "$PI_KEY" -o StrictHostKeyChecking=no "$PI_USER@$PI_HOST" "
sudo mkdir -p $REMOTE_DIR
sudo chown \$USER: $REMOTE_DIR
"
scp -i "$PI_KEY" -o StrictHostKeyChecking=no "$SCRIPT" "$PI_USER@$PI_HOST:$REMOTE_DIR/net_alerter.py"
ssh -i "$PI_KEY" -o StrictHostKeyChecking=no "$PI_USER@$PI_HOST" "
# Install nmap if missing
if ! command -v nmap &>/dev/null; then
sudo apt-get install -y nmap
fi
# Write env config
cat > $REMOTE_DIR/.env <<EOF
MATRIX_HOMESERVER=https://m.example.org
MATRIX_ACCESS_TOKEN=$TOKEN
MATRIX_ROOM_ID=!REDACTED:example.org
NET_ALERTER_DB=$REMOTE_DIR/devices.db
EOF
chmod 600 $REMOTE_DIR/.env
# Write wrapper that sources env
cat > $REMOTE_DIR/run.sh <<'WRAPPER'
#!/usr/bin/env bash
set -a; source /opt/net_alerter/.env; set +a
exec python3 /opt/net_alerter/net_alerter.py
WRAPPER
chmod +x $REMOTE_DIR/run.sh
# Install cron (every 5 minutes)
CRON='*/5 * * * * /opt/net_alerter/run.sh >> /opt/net_alerter/net_alerter.log 2>&1'
(crontab -l 2>/dev/null | grep -v net_alerter; echo \"\$CRON\") | crontab -
echo '[ok] Deployed. Running first scan...'
bash $REMOTE_DIR/run.sh && echo '[ok] First scan complete'
"
echo "[done] net_alerter deployed and cron set (every 5 min)"
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""
net_alerter.py — Alert on new devices joining the local network.
Runs via cron, sends Matrix messages when unknown MACs appear.
"""
import json
import os
import re
import socket
import sqlite3
import subprocess
import sys
import urllib.request
from datetime import datetime
from pathlib import Path
# --- Config (overridable via env) ---
MATRIX_HOMESERVER = os.getenv("MATRIX_HOMESERVER", "https://m.example.org")
MATRIX_ACCESS_TOKEN = os.getenv("MATRIX_ACCESS_TOKEN", "")
MATRIX_ROOM_ID = os.getenv("MATRIX_ROOM_ID", "!REDACTED:example.org")
DB_PATH = Path(os.getenv("NET_ALERTER_DB", "/opt/net_alerter/devices.db"))
def get_db() -> sqlite3.Connection:
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS devices (
mac TEXT PRIMARY KEY,
ip TEXT,
name TEXT,
first_seen TEXT,
last_seen TEXT
)
""")
conn.commit()
return conn
def get_local_subnets() -> list[str]:
"""Return CIDR subnets for non-loopback, non-tailscale, non-wireguard interfaces."""
try:
out = subprocess.check_output(["ip", "-o", "-4", "addr", "show"], text=True)
except Exception:
return []
subnets = []
for line in out.splitlines():
# Fields: index iface inet ip/prefix ...
parts = line.split()
if len(parts) < 4:
continue
iface = parts[1]
cidr = parts[3]
# Skip loopback, tailscale, wireguard, docker
if any(iface.startswith(p) for p in ("lo", "tailscale", "wg", "tun", "docker", "br-")):
continue
subnets.append(cidr)
return subnets
def scan_subnet(cidr: str) -> list[dict]:
"""Run nmap ping scan, return list of {ip, mac, name}."""
try:
out = subprocess.check_output(
["nmap", "-sn", "-PR", "--host-timeout", "5s", cidr],
text=True, stderr=subprocess.DEVNULL
)
except Exception:
return []
devices = []
current = {}
for line in out.splitlines():
if line.startswith("Nmap scan report for"):
if current.get("mac"):
devices.append(current)
# "Nmap scan report for hostname (ip)" or "... for ip"
m = re.search(r"for (.+?)(?:\s+\((.+?)\))?$", line)
if m:
if m.group(2):
current = {"name": m.group(1), "ip": m.group(2), "mac": ""}
else:
current = {"name": "", "ip": m.group(1), "mac": ""}
elif "MAC Address:" in line:
m = re.search(r"MAC Address: ([0-9A-F:]{17})", line)
if m:
current["mac"] = m.group(1).lower()
if current.get("mac"):
devices.append(current)
return devices
def send_matrix_alert(msg: str) -> None:
if not MATRIX_ACCESS_TOKEN:
print(f"[warn] No MATRIX_ACCESS_TOKEN set, skipping alert: {msg}", file=sys.stderr)
return
url = (
f"{MATRIX_HOMESERVER}/_matrix/client/v3/rooms/"
f"{urllib.parse.quote(MATRIX_ROOM_ID, safe='')}/send/m.room.message"
)
payload = json.dumps({"msgtype": "m.text", "body": msg}).encode()
req = urllib.request.Request(
url,
data=payload,
headers={
"Authorization": f"Bearer {MATRIX_ACCESS_TOKEN}",
"Content-Type": "application/json",
},
method="POST",
)
try:
urllib.request.urlopen(req, timeout=10)
except Exception as e:
print(f"[error] Matrix send failed: {e}", file=sys.stderr)
# urllib.parse needed for quote
import urllib.parse # noqa: E402
def run() -> None:
conn = get_db()
now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
hostname = socket.gethostname()
subnets = get_local_subnets()
if not subnets:
print("[warn] No local subnets found", file=sys.stderr)
return
new_devices = []
for cidr in subnets:
devices = scan_subnet(cidr)
for d in devices:
mac = d["mac"]
row = conn.execute("SELECT mac FROM devices WHERE mac=?", (mac,)).fetchone()
if row is None:
conn.execute(
"INSERT INTO devices (mac, ip, name, first_seen, last_seen) VALUES (?,?,?,?,?)",
(mac, d["ip"], d["name"], now, now),
)
new_devices.append(d)
else:
conn.execute(
"UPDATE devices SET ip=?, name=?, last_seen=? WHERE mac=?",
(d["ip"], d["name"], now, mac),
)
conn.commit()
conn.close()
for d in new_devices:
label = d["name"] if d["name"] else "unknown"
msg = (
f"[{hostname}] NEW DEVICE\n"
f" IP: {d['ip']}\n"
f" MAC: {d['mac']}\n"
f" Name: {label}\n"
f" Time: {now}"
)
print(msg)
send_matrix_alert(msg)
if __name__ == "__main__":
run()