"""WireGuard state collector.""" from copy import deepcopy from typing import Any from lib import schema from lib.common import compute_pending, run_proc, strip_apply_meta from lib.state import _now_iso, register_collector, register_volatile from lib.wireguard import DEFAULT_CONFIG, get_config, parse_wg_show_output def _collect_wireguard() -> schema.WgState: """Collect WireGuard config, per-class status, and peers. Returns: Dict containing interface config, per-class runtime status, combined peers, and overall tunnel status. """ # Load config via lib.wireguard defaults (which include the built-in # full/internet access classes). try: cfg = get_config() except Exception: cfg = deepcopy(DEFAULT_CONFIG) pending_changes, pending_diff = compute_pending(cfg) # Safe config (strip private keys from interface and access classes) safe = strip_apply_meta(cfg) if "interface" in safe: safe["interface"] = dict(safe["interface"]) safe["interface"].pop("private_key", None) if "access_classes" in safe: safe["access_classes"] = {} for ck, cv in cfg.get("access_classes", {}).items(): if isinstance(cv, dict): entry = dict(cv) entry.pop("private_key", None) safe["access_classes"][ck] = entry # Peers list (safe) peers: list[dict[str, Any]] = [] for name, info in cfg.get("peers", {}).items(): entry = dict(info) entry["name"] = name entry.pop("private_key", None) peers.append(entry) # Runtime status — per-class interfaces status: dict[str, Any] = { "up": False, "interface": {}, "peers": [], "classes": {}, } classes = cfg.get("access_classes", {}) any_up = False for class_key in classes: class_cfg = classes.get(class_key) if not isinstance(class_cfg, dict): continue ifname = f"wg-{class_key}" try: res = run_proc(["wg", "show", ifname], sudo=True, check=False) if res.returncode != 0: status["classes"][class_key] = { "up": False, "interface": {}, "peers": [], } continue parsed = parse_wg_show_output(res.stdout.strip()) status["classes"][class_key] = { "up": parsed["up"], "interface": parsed.get("interface", {}), "peers": parsed.get("peers", []), } if parsed["up"]: any_up = True except Exception: status["classes"][class_key] = {"up": False, "interface": {}, "peers": []} # Also collect legacy single-interface status try: ifname = cfg["interface"].get("name", "wg0") res = run_proc(["wg", "show", ifname], sudo=True, check=False) if res.returncode == 0: parsed = parse_wg_show_output(res.stdout.strip()) status["up"] = True status["interface"] = parsed.get("interface", {}) status["peers"] = parsed.get("peers", []) any_up = True except Exception: pass if any_up: status["up"] = True status["pending_changes"] = pending_changes # Drop any private-key paths so the pending summary never exposes # key material. status["pending_diff"] = [d for d in pending_diff if "private_key" not in d["path"]] return { "config": safe, "status": status, "peers": peers, "timestamp": _now_iso(), } register_collector("wireguard", _collect_wireguard) register_volatile( "wireguard", frozenset( { "status.peers[].transfer_received", "status.peers[].transfer_sent", "status.peers[].latest_handshake", "status.classes[].peers[].transfer_received", "status.classes[].peers[].transfer_sent", "status.classes[].peers[].latest_handshake", } ), )