"""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 daemon.iface import ( DELETE_WIREGUARD_PEERS_REMOVE, GET_WIREGUARD_CONFIG, GET_WIREGUARD_PEER_STATUS, GET_WIREGUARD_PEERS, GET_WIREGUARD_STATUS, PATCH_WIREGUARD_CONFIG, POST_WIREGUARD_APPLY, POST_WIREGUARD_CONFIG, POST_WIREGUARD_DOWN, POST_WIREGUARD_GENERATE_CLIENT, POST_WIREGUARD_INITIALIZE, POST_WIREGUARD_PEERS_ADD, ) from webui.api.common import _error, _ok logger = logging.getLogger(__name__) bp = Blueprint("wireguard", __name__) @bp.route("/config", methods=["GET"]) def get_config_bp(): """Get the current WireGuard configuration. Endpoint: GET /api/wireguard/config Returns: JSON response with the WireGuard config on success, or an error response on failure. """ try: return _ok(get(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(): """Create or fully replace the WireGuard configuration. Endpoint: POST /api/wireguard/config Args: body: JSON body with the configuration. If an ``interface`` key is present, the private key will be stripped before forwarding. Returns: Success response on acceptance, 400 on validation failure, or 500 on server error. """ 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(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(): """Partially update the WireGuard configuration. Endpoint: PATCH /api/wireguard/config Args: body: JSON body with the fields to update. If an ``interface`` key is present, the private key will be stripped before forwarding. Returns: Success response on acceptance, 400 on validation failure, or 500 on server error. """ 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(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(): """Apply the current WireGuard configuration to the live tunnel. Endpoint: POST /api/wireguard/apply Returns: Success response on acceptance, or 500 on server error. """ try: post(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(): """Bring the WireGuard tunnel interface up. Endpoint: POST /api/wireguard/up Returns: Success response on acceptance, or 500 on server error. """ try: post(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(): """Bring the WireGuard tunnel interface down. Endpoint: POST /api/wireguard/down Returns: Success response on acceptance, or 500 on server error. """ try: post(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(): """Get the current WireGuard tunnel status. Endpoint: GET /api/wireguard/status Returns: JSON response with the tunnel status on success, or an error response on failure. """ try: return _ok(get(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(): """Initialize WireGuard for first-time use. Endpoint: POST /api/wireguard/initialize Returns: Success response on acceptance, or 500 on server error. """ try: post(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(): """Add a new peer to the WireGuard configuration. Endpoint: POST /api/wireguard/peers Args: name: Peer display name (required). endpoint: Optional peer endpoint address. allowed_ips: Optional list of allowed IP CIDRs. persistent_keepalive: Optional keepalive interval in seconds. preshared_key: Optional pre-shared key in hex. Returns: JSON response with the created peer on success, 400 on validation failure, or 500 on server error. """ body = request.get_json(silent=True) or {} name = body.get("name", "").strip() if not name: return _error("'name' is required", 400) try: peer = post( 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): """Remove a peer from the WireGuard configuration. Endpoint: DELETE /api/wireguard/peers/ Args: name: Peer name to remove (from URL path). Returns: Success response with peer name on removal, 404 if peer not found, or 500 on server error. """ try: delete(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(): """List all configured WireGuard peers. Endpoint: GET /api/wireguard/peers Returns: JSON response with the peers list on success, or an error response on failure. """ try: return _ok(get(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(): """Get real-time status information for all WireGuard peers. Endpoint: GET /api/wireguard/peer-status Returns: JSON response with peer status on success, or an error response on failure. """ try: return _ok(get(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(): """Generate a WireGuard client configuration file for a peer. Endpoint: POST /api/wireguard/generate-client Args: name: Peer name (required). server_endpoint: Server endpoint address for the client config (required). Returns: JSON response with the generated config string on success, 404 if peer not found, 400 on validation failure, or 500 on server error. """ 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( 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)