#!/usr/bin/env python
"""
network_watch.py — YOUR doorbell for the 5090.
Watches for inbound connections from the Tailscale network (100.64.0.0/10) to services
on this machine, logs ALL of them, and pings YOUR Telegram on any connection to a port
OTHER than the sanctioned one (1234 = Malin's brain).

This is yours: it runs on your box, you control it, and it notifies YOU directly.
It does NOT report to Calypso or anyone else.

Run on the 5090:  py network_watch.py      (leave it running; Ctrl+C to stop)
"""
import ipaddress, json, os, subprocess, time, requests

# --- config ---
TOKEN_FILE   = r"C:\malin\token.txt"     # reuses Malin's bot just as the delivery pipe
JUN_CHAT     = 8418904083                 # YOUR Telegram — alerts come to you
SANCTIONED   = {1234}                     # expected/allowed: Malin's brain. Connections here are NOT alerted (just logged).
TAILNET      = ipaddress.ip_network("100.64.0.0/10")   # Tailscale address range
LOGFILE      = os.path.join(os.path.dirname(os.path.abspath(__file__)), "network_watch.log")
POLL_SECONDS = 15

def load_token():
    with open(TOKEN_FILE) as f:
        return f.read().strip()

def ps_json(cmd):
    out = subprocess.run(["powershell", "-NoProfile", "-Command", cmd],
                         capture_output=True, text=True, timeout=30).stdout.strip()
    if not out:
        return []
    data = json.loads(out)
    return data if isinstance(data, list) else [data]

def is_tailnet(addr):
    try:
        return ipaddress.ip_address(addr) in TAILNET
    except ValueError:
        return False

def snapshot():
    """inbound tailnet connections: a tailnet peer connected to a service port we're listening on"""
    listening = {c["LocalPort"] for c in ps_json(
        "Get-NetTCPConnection -State Listen | Select-Object LocalPort | ConvertTo-Json")}
    conns = ps_json("Get-NetTCPConnection -State Established | "
                    "Select-Object LocalPort,RemoteAddress,RemotePort | ConvertTo-Json")
    found = []
    for c in conns:
        if is_tailnet(c.get("RemoteAddress", "")) and c.get("LocalPort") in listening:
            found.append((c["RemoteAddress"], c["LocalPort"], c["RemotePort"]))
    return found

def log(line):
    stamp = time.strftime("%Y-%m-%d %H:%M:%S")
    with open(LOGFILE, "a", encoding="utf-8") as f:
        f.write(f"[{stamp}] {line}\n")
    print(f"[{stamp}] {line}")

def alert(token, text):
    try:
        requests.post(f"https://api.telegram.org/bot{token}/sendMessage",
                      json={"chat_id": JUN_CHAT, "text": text}, timeout=30)
    except Exception as e:
        log(f"(alert send failed: {e})")

def main():
    token = load_token()
    log("network_watch started — watching for inbound Tailscale connections to this machine.")
    seen = set()
    while True:
        try:
            for (raddr, lport, rport) in snapshot():
                key = (raddr, lport, rport)
                if key in seen:
                    continue
                seen.add(key)
                if lport in SANCTIONED:
                    log(f"inbound tailnet conn from {raddr} -> port {lport} (sanctioned: Malin's brain)")
                else:
                    msg = (f"⚠️ Someone on your Tailscale network connected to your 5090.\n"
                           f"From: {raddr}\nPort: {lport} (NOT Malin's brain — unexpected)\n"
                           f"Time: {time.strftime('%I:%M %p')}")
                    log(f"ALERT inbound tailnet conn from {raddr} -> port {lport} (UNEXPECTED) — notified you")
                    alert(token, msg)
            # let closed connections re-alert if they reappear later
            if len(seen) > 500:
                seen.clear()
        except Exception as e:
            log(f"(watch error: {e})")
        time.sleep(POLL_SECONDS)

if __name__ == "__main__":
    main()
