import type { Express } from "express"; import { execElevated } from "./exec-elevated.js"; import { managerHandler } from "./route-helpers.js"; import { ManagerInputError } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; import { isValidWireGuardInterface, isValidWireGuardAction, } from "./validation.js"; export interface WireGuardPeer { publicKey: string; endpoint: string | null; allowedIPs: string[]; latestHandshake: number | null; rxBytes: number; txBytes: number; } export interface WireGuardInterface { name: string; publicKey: string | null; listenPort: number | null; up: boolean; peers: WireGuardPeer[]; } export interface WireGuardData { installed: boolean; interfaces: WireGuardInterface[]; } const PROBE_CMD = [ "command -v wg >/dev/null 2>&1 && echo wg_installed=1 || echo wg_installed=0", "ip link show type wireguard 2>/dev/null", "echo __DUMP__", "wg show all dump 2>/dev/null", ].join("; "); export function parseWireGuardData(output: string): WireGuardData { if (output.includes("wg_installed=0")) { return { installed: false, interfaces: [] }; } const dumpIdx = output.indexOf("__DUMP__"); const ipLinkPart = dumpIdx >= 0 ? output.slice(0, dumpIdx) : ""; const dumpPart = dumpIdx >= 0 ? output.slice(dumpIdx + "__DUMP__".length) : ""; // Collect up interface names from `ip link show type wireguard` const upSet = new Set(); for (const line of ipLinkPart.split("\n")) { const m = line.match(/^\d+:\s+([A-Za-z0-9_-]+):/); if (m) upSet.add(m[1]); } const ifaceMap = new Map(); for (const raw of dumpPart.split("\n")) { const line = raw.trim(); if (!line) continue; const cols = line.split("\t"); if (cols.length === 5) { // Interface row: iface private_key public_key listen_port fwmark const [name, , publicKey, listenPortStr] = cols; if (!name) continue; ifaceMap.set(name, { name, publicKey: publicKey && publicKey !== "(none)" ? publicKey : null, listenPort: listenPortStr && listenPortStr !== "(none)" ? Number(listenPortStr) : null, up: upSet.has(name), peers: [], }); } else if (cols.length === 9) { // Peer row: iface public_key preshared_key endpoint allowed_ips latest_handshake rx_bytes tx_bytes persistent_keepalive const [ name, publicKey, , endpoint, allowedIPsStr, handshakeStr, rxStr, txStr, ] = cols; if (!name) continue; if (!ifaceMap.has(name)) { ifaceMap.set(name, { name, publicKey: null, listenPort: null, up: upSet.has(name), peers: [], }); } const handshake = Number(handshakeStr); ifaceMap.get(name)!.peers.push({ publicKey: publicKey ?? "", endpoint: endpoint && endpoint !== "(none)" ? endpoint : null, allowedIPs: allowedIPsStr && allowedIPsStr !== "(none)" ? allowedIPsStr.split(",").map((s) => s.trim()) : [], latestHandshake: handshake > 0 ? handshake : null, rxBytes: Number(rxStr) || 0, txBytes: Number(txStr) || 0, }); } } return { installed: true, interfaces: Array.from(ifaceMap.values()) }; } export function registerWireGuardRoutes( app: Express, { validateHostId, runOnHost }: ManagerRoutesDeps, ): void { /** * @openapi * /host-metrics/managers/wireguard/{id}: * get: * summary: Get WireGuard interfaces and peers * tags: * - Host Metrics * parameters: * - in: path * name: id * required: true * schema: * type: integer * responses: * 200: * description: WireGuard installation status, interfaces, and peer details. */ app.get( "/host-metrics/managers/wireguard/:id", validateHostId, managerHandler( runOnHost, "connect", "wireguard_read", async (client, host) => { const result = await execElevated( client, PROBE_CMD, host.sudoPassword, { forceSudo: false, timeoutMs: 15000, }, ); return parseWireGuardData(result.stdout); }, ), ); /** * @openapi * /host-metrics/managers/wireguard/{id}/action: * post: * summary: Bring a WireGuard interface up or down * tags: * - Host Metrics * parameters: * - in: path * name: id * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * interface: * type: string * action: * type: string * enum: [up, down] * responses: * 200: * description: Action result. */ app.post( "/host-metrics/managers/wireguard/:id/action", validateHostId, managerHandler( runOnHost, "connect", "wireguard_action", async (client, host, req) => { const { interface: iface, action } = req.body as { interface: unknown; action: unknown; }; if (!isValidWireGuardInterface(iface)) { throw new ManagerInputError("Invalid WireGuard interface name"); } if (!isValidWireGuardAction(action)) { throw new ManagerInputError("Invalid action, must be 'up' or 'down'"); } const result = await execElevated( client, `wg-quick ${action} ${iface}`, host.sudoPassword, { forceSudo: true, timeoutMs: 30000 }, ); return { success: result.code === 0, output: (result.stdout + result.stderr).trim(), }; }, ), ); }