Add redacted WireGuard status API #37

Merged
sascha merged 2 commits from feature/wireguard-status-api into main 2026-08-16 20:06:04 +02:00
Showing only changes of commit 7dfcb2b654 - Show all commits

61
app.py
View file

@ -1213,6 +1213,67 @@ def _ssh(host, cmd, timeout=600):
return 124, "", f"SSH command timed out after {timeout} seconds" return 124, "", f"SSH command timed out after {timeout} seconds"
def _wireguard_status_command() -> str:
script = '''import json, subprocess
def run(args):
proc = subprocess.run(args, text=True, capture_output=True, timeout=15)
if proc.returncode != 0:
raise RuntimeError((proc.stderr or proc.stdout).strip() or "command failed")
return proc.stdout.strip()
result = {"interface": "wg0", "addresses": [], "listen_port": None, "service_active": False, "service_enabled": False, "routes": [], "peers": []}
result["service_active"] = subprocess.run(["systemctl", "is-active", "--quiet", "wg-quick@wg0"]).returncode == 0
result["service_enabled"] = subprocess.run(["systemctl", "is-enabled", "--quiet", "wg-quick@wg0"]).returncode == 0
try:
addr_data = json.loads(run(["ip", "-j", "address", "show", "dev", "wg0"]))
for item in addr_data:
for address in item.get("addr_info", []):
result["addresses"].append(address["local"] + "/" + str(address["prefixlen"]))
result["routes"] = json.loads(run(["ip", "-j", "route", "show", "dev", "wg0"]))
rows = run(["wg", "show", "wg0", "dump"]).splitlines()
if rows:
interface = rows[0].split("\\t")
result["listen_port"] = int(interface[2])
for raw in rows[1:]:
fields = raw.split("\\t")
result["peers"].append({
"public_key": fields[0],
"endpoint": None if fields[2] == "(none)" else fields[2],
"allowed_ips": [] if fields[3] == "(none)" else fields[3].split(","),
"latest_handshake": int(fields[4]),
"rx_bytes": int(fields[5]),
"tx_bytes": int(fields[6]),
"persistent_keepalive": int(fields[7]),
})
except Exception as exc:
result["error"] = str(exc)[:300]
print(json.dumps(result))
'''
encoded = base64.b64encode(script.encode()).decode()
return f'python3 -c "import base64;exec(base64.b64decode(\'{encoded}\'))"'
@app.get("/network/wireguard/{host}")
async def network_wireguard_status(host: str, _=Depends(_verify)):
"""Return redacted WireGuard state without private or preshared keys."""
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,62}", host):
raise HTTPException(400, "Invalid host name")
inventory = await asyncio.to_thread(_find_inventory_host, host)
if not inventory:
raise HTTPException(404, f"Host {host} not found")
target = f'{inventory["user"]}@{inventory["ip"]}'
rc, out, err = await asyncio.to_thread(_ssh, target, _wireguard_status_command(), 30)
if rc != 0:
raise HTTPException(502, (err or out).strip()[-500:] or "WireGuard status failed")
try:
result = json.loads(out)
except json.JSONDecodeError as exc:
raise HTTPException(502, "WireGuard status returned invalid JSON") from exc
_audit(f"/network/wireguard/{host}", "GET", 200, "redacted live status")
return {"host": host, **result}
SYSCTL_AUDIT_KEYS = ( SYSCTL_AUDIT_KEYS = (
"net.core.default_qdisc", "net.core.default_qdisc",
"net.core.rmem_default", "net.core.rmem_default",