"""WireGuard tunnel management API blueprint. Exposed at /api/wireguard/* and delegates to vacuum-walld. """ import logging from flask import Blueprint, request from daemon.client import BadRequest, NotFound, delete, get, patch, post from webui.api.common import _error, _ok logger = logging.getLogger(__name__) bp = Blueprint("wireguard", __name__) @bp.route("/config", methods=["GET"]) def get_config_bp(): try: return _ok(get("/wireguard/config")) except RuntimeError as exc: logger.error("Failed to read WireGuard config: %s", exc) return _error(str(exc), 500) @bp.route("/config", methods=["POST"]) def post_config(): body = request.get_json(silent=True) or {} if not isinstance(body, dict): return _error("Request body must be a JSON object", 400) try: if "interface" in body: body = dict(body) body["interface"] = dict(body["interface"]) body["interface"].pop("private_key", None) post("/wireguard/config", body) return _ok(None) except BadRequest as exc: logger.info("WireGuard config save rejected: %s", exc) return _error(str(exc), 400) except RuntimeError as exc: logger.error("Failed to save WireGuard config: %s", exc) return _error(str(exc), 500) @bp.route("/config", methods=["PATCH"]) def patch_config(): body = request.get_json(silent=True) or {} if not isinstance(body, dict): return _error("Request body must be a JSON object", 400) try: if "interface" in body: body = dict(body) body["interface"] = dict(body["interface"]) body["interface"].pop("private_key", None) patch("/wireguard/config", body) logger.info("WireGuard config patched: %s", sorted(body.keys())) return _ok(None) except BadRequest as exc: logger.info("WireGuard config patch rejected: %s", exc) return _error(str(exc), 400) except RuntimeError as exc: logger.error("Failed to patch WireGuard config: %s", exc) return _error(str(exc), 500) @bp.route("/apply", methods=["POST"]) def apply_bp(): try: post("/wireguard/apply") logger.info("WireGuard tunnel applied via API") return _ok(None) except RuntimeError as exc: logger.error("Failed to apply WireGuard config: %s", exc) return _error(str(exc), 500) @bp.route("/up", methods=["POST"]) def up_bp(): try: post("/wireguard/apply") logger.info("WireGuard tunnel started via API") return _ok(None) except RuntimeError as exc: logger.error("Failed to start WireGuard tunnel: %s", exc) return _error(str(exc), 500) @bp.route("/down", methods=["POST"]) def down_bp(): try: post("/wireguard/down") logger.info("WireGuard tunnel brought down via API") return _ok(None) except RuntimeError as exc: logger.error("Failed to bring down WireGuard tunnel: %s", exc) return _error(str(exc), 500) @bp.route("/status", methods=["GET"]) def status_bp(): try: return _ok(get("/wireguard/status")) except RuntimeError as exc: logger.error("Failed to get WireGuard status: %s", exc) return _error(str(exc), 500) @bp.route("/initialize", methods=["POST"]) def initialize_bp(): try: post("/wireguard/initialize") logger.info("WireGuard initialized via API") return _ok(None) except RuntimeError as exc: logger.error("Failed to initialize WireGuard: %s", exc) return _error(str(exc), 500) @bp.route("/peers", methods=["POST"]) def add_peer_bp(): body = request.get_json(silent=True) or {} name = body.get("name", "").strip() if not name: return _error("'name' is required", 400) try: peer = post( "/wireguard/peers/add", { "name": name, "endpoint": body.get("endpoint"), "allowed_ips": body.get("allowed_ips", []), "persistent_keepalive": body.get("persistent_keepalive"), "preshared_key": body.get("preshared_key"), }, ) logger.info("WireGuard peer '%s' added via API", name) return _ok(peer) except BadRequest as exc: logger.info("Add peer '%s' rejected: %s", name, exc) return _error(str(exc), 400) except RuntimeError as exc: logger.error("Failed to add peer '%s': %s", name, exc) return _error(str(exc), 500) @bp.route("/peers/", methods=["DELETE"]) def remove_peer_bp(name): try: delete("/wireguard/peers/remove", {"name": name}) logger.info("WireGuard peer '%s' removed via API", name) return _ok({"name": name}) except NotFound as exc: logger.info("WireGuard peer '%s' not found: %s", name, exc) return _error(str(exc), 404) except RuntimeError as exc: logger.error("Failed to remove peer '%s': %s", name, exc) return _error(str(exc), 500) @bp.route("/peers", methods=["GET"]) def peers_bp(): try: return _ok(get("/wireguard/peers")) except RuntimeError as exc: logger.error("Failed to list WireGuard peers: %s", exc) return _error(str(exc), 500) @bp.route("/peer-status", methods=["GET"]) def peer_status_bp(): try: return _ok(get("/wireguard/peer-status")) except RuntimeError as exc: logger.error("Failed to get WireGuard peer status: %s", exc) return _error(str(exc), 500) @bp.route("/generate-client", methods=["POST"]) def generate_client_bp(): body = request.get_json(silent=True) or {} name = body.get("name", "").strip() if not name: return _error("Field 'name' is required", 400) server_endpoint = body.get("server_endpoint", "") if not server_endpoint: return _error("Field 'server_endpoint' is required", 400) try: result = post( "/wireguard/generate-client", { "name": name, "server_endpoint": server_endpoint, }, ) logger.info("Client config generated for peer '%s' via API", name) return _ok({"config": result.get("config", "")}) except NotFound as exc: logger.info("Peer '%s' not found for client config: %s", name, exc) return _error(str(exc), 404) except RuntimeError as exc: logger.error("Failed to generate client config for '%s': %s", name, exc) return _error(str(exc), 500)