"""Network status module. Cross-platform connection info (via psutil), a WireGuard VPN status/toggle that only works where NetworkManager + nmcli exist (Linux — this mirrors the XFCE panel's network-popup.py, minus the GTK UI), and a small user-defined list of ping targets so "is my router/VPN endpoint reachable" isn't tied to any one hardcoded host. """ from __future__ import annotations import json import os import platform import re import socket import subprocess import uuid from typing import Any, Dict, List from synapse.nexus_config import RUNTIME_DIR _TARGETS_FILE = RUNTIME_DIR / "network_targets.json" _PING_LATENCY_RE = re.compile(r"time[=<]\s*([\d.]+)\s*ms", re.IGNORECASE) # --- connection info --------------------------------------------------------------- def hostname() -> str: return socket.gethostname() def primary_connection() -> Dict[str, Any]: import psutil stats = psutil.net_if_stats() addrs = psutil.net_if_addrs() for name, addr_list in addrs.items(): st = stats.get(name) if not st or not st.isup: continue lname = name.lower() if lname.startswith(("lo", "loopback")): continue for a in addr_list: if a.family == socket.AF_INET and not a.address.startswith("169.254"): iface_type = "wifi" if any(k in lname for k in ("wlan", "wi-fi", "wireless", "wl")) else "ethernet" return {"interface": name, "ip": a.address, "type": iface_type} return {"interface": None, "ip": None, "type": "offline"} # --- WireGuard / VPN (Linux + NetworkManager only) ---------------------------------- def _nmcli(*args: str) -> str: try: return subprocess.check_output( ["nmcli", "-t", "--escape", "no", *args], text=True, stderr=subprocess.DEVNULL, timeout=5, ).strip() except Exception: return "" def _nmcli_available() -> bool: if os.name != "posix": return False try: subprocess.check_output(["nmcli", "--version"], stderr=subprocess.DEVNULL, timeout=3) return True except Exception: return False def vpn_status() -> Dict[str, Any]: if not _nmcli_available(): return {"available": False} wgs = [p for p in (line.split(":") for line in _nmcli("-f", "NAME,TYPE,STATE", "connection", "show").splitlines()) if len(p) >= 3 and p[1] == "wireguard"] for name, _t, state in wgs: if state == "activated": return {"available": True, "configured": True, "name": name, "connected": True} if wgs: return {"available": True, "configured": True, "name": wgs[0][0], "connected": False} return {"available": True, "configured": False, "name": None, "connected": False} def vpn_toggle(enable: bool) -> Dict[str, Any]: status = vpn_status() if not status.get("configured"): raise RuntimeError("no WireGuard tunnel configured") action = "up" if enable else "down" subprocess.check_output( ["nmcli", "connection", action, status["name"]], stderr=subprocess.STDOUT, text=True, timeout=15, ) return vpn_status() # --- ping targets -------------------------------------------------------------------- def _load_targets() -> List[Dict[str, Any]]: try: return json.loads(_TARGETS_FILE.read_text()).get("targets", []) except Exception: return [] def _write_targets(targets: List[Dict[str, Any]]) -> None: _TARGETS_FILE.parent.mkdir(parents=True, exist_ok=True) _TARGETS_FILE.write_text(json.dumps({"targets": targets}, indent=2)) def list_targets() -> List[Dict[str, Any]]: return _load_targets() def add_target(label: str, host: str) -> Dict[str, Any]: targets = _load_targets() t = {"id": uuid.uuid4().hex[:12], "label": label, "host": host} targets.append(t) _write_targets(targets) return t def delete_target(target_id: str) -> None: _write_targets([t for t in _load_targets() if t["id"] != target_id]) def ping(host: str) -> Dict[str, Any]: is_windows = platform.system() == "Windows" cmd = ["ping", "-n", "1", "-w", "1000", host] if is_windows else ["ping", "-c", "1", "-W", "1", host] try: out = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT, timeout=3) m = _PING_LATENCY_RE.search(out) return {"ok": True, "latency_ms": float(m.group(1)) if m else None} except subprocess.CalledProcessError: return {"ok": False, "latency_ms": None} except Exception as e: return {"ok": False, "latency_ms": None, "error": str(e)} def ping_targets() -> List[Dict[str, Any]]: return [{**t, **ping(t["host"])} for t in _load_targets()]