"""WireGuard tunnel management API blueprint. Exposed at /api/wireguard/* and delegates to vacuum-walld. """ from typing import Any from flask import Blueprint from daemon.client import ( # noqa: F401 (resolved via module globals) delete, get, patch, post, ) from daemon.iface import ( DELETE_WIREGUARD_CLASSES, DELETE_WIREGUARD_CLASSES_DOWN, DELETE_WIREGUARD_PEERS_REMOVE, GET_WIREGUARD_CLASS_STATUS, GET_WIREGUARD_CLASSES, GET_WIREGUARD_CONFIG, GET_WIREGUARD_PEER_STATUS, GET_WIREGUARD_PEERS, GET_WIREGUARD_STATUS, PATCH_WIREGUARD_CLASSES, PATCH_WIREGUARD_CONFIG, POST_WIREGUARD_APPLY, POST_WIREGUARD_CLASS_INIT_KEYS, POST_WIREGUARD_CLASSES, POST_WIREGUARD_CLASSES_UP, POST_WIREGUARD_CONFIG, POST_WIREGUARD_DOWN, POST_WIREGUARD_GENERATE_CLIENT, POST_WIREGUARD_INITIALIZE, POST_WIREGUARD_PEERS_ADD, ) from webui.api.common import NO_BODY, daemon_route, require_dict_body, void_transform bp = Blueprint("wireguard", __name__) # --------------------------------------------------------------------------- # Body builders / prechecks / transforms # --------------------------------------------------------------------------- def _wg_config_body(request: Any, _va: Any) -> dict[str, Any]: body = request.get_json(silent=True) or {} if "interface" in body: body = dict(body) body["interface"] = dict(body["interface"]) body["interface"].pop("private_key", None) return body def _add_peer_body(request: Any, _va: Any) -> dict[str, Any]: body = request.get_json(silent=True) or {} name = (body.get("name") or "").strip() if not name: raise ValueError("'name' is required") return { "name": name, "endpoint": body.get("endpoint"), "allowed_ips": body.get("allowed_ips", []), "persistent_keepalive": body.get("persistent_keepalive"), "preshared_key": body.get("preshared_key"), "description": body.get("description"), "access_class": body.get("access_class"), } def _gen_client_body(request: Any, _va: Any) -> dict[str, Any]: body = request.get_json(silent=True) or {} name = (body.get("name") or "").strip() if not name: raise ValueError("Field 'name' is required") server_endpoint = body.get("server_endpoint", "") if not server_endpoint: raise ValueError("Field 'server_endpoint' is required") return {"name": name, "server_endpoint": server_endpoint} def _delete_class_body(request: Any, _va: Any) -> dict[str, Any]: body = request.get_json(silent=True) or {} key = (body.get("key") or "").strip() if not key: raise ValueError("'key' is required") return {"key": key} def _class_key_precheck(json: Any, va: dict[str, Any]) -> None: require_dict_body(json, va) if not ((json or {}).get("key") or "").strip(): raise ValueError("'key' is required") def _peer_name_echo(_data: Any, _va: Any, sent: Any) -> Any: return {"name": sent["name"]} def _config_echo(data: Any, _va: Any, _sent: Any) -> Any: return {"config": data.get("config", "")} # --------------------------------------------------------------------------- # Config # --------------------------------------------------------------------------- @daemon_route(GET_WIREGUARD_CONFIG, bp) def get_config_bp(): """GET /api/wireguard/config — Get the current WireGuard configuration.""" @daemon_route( POST_WIREGUARD_CONFIG, bp, precheck=require_dict_body, body=_wg_config_body, transform=void_transform, ) def post_config(): """POST /api/wireguard/config — Create or fully replace the configuration.""" @daemon_route( PATCH_WIREGUARD_CONFIG, bp, precheck=require_dict_body, body=_wg_config_body, transform=void_transform, ) def patch_config(): """PATCH /api/wireguard/config — Partially update the configuration.""" # --------------------------------------------------------------------------- # Tunnel control # --------------------------------------------------------------------------- @daemon_route(POST_WIREGUARD_APPLY, bp, body=NO_BODY, transform=void_transform) def apply_bp(): """POST /api/wireguard/apply — Apply the current configuration to the tunnel.""" @daemon_route( POST_WIREGUARD_APPLY, bp, rule="/up", body=NO_BODY, transform=void_transform ) def up_bp(): """POST /api/wireguard/up — Bring the WireGuard tunnel interface up.""" @daemon_route(POST_WIREGUARD_DOWN, bp, body=NO_BODY, transform=void_transform) def down_bp(): """POST /api/wireguard/down — Bring the WireGuard tunnel interface down.""" @daemon_route(GET_WIREGUARD_STATUS, bp) def status_bp(): """GET /api/wireguard/status — Get the current WireGuard tunnel status.""" @daemon_route(POST_WIREGUARD_INITIALIZE, bp, body=NO_BODY, transform=void_transform) def initialize_bp(): """POST /api/wireguard/initialize — Initialize WireGuard for first-time use.""" # --------------------------------------------------------------------------- # Peers # --------------------------------------------------------------------------- @daemon_route(POST_WIREGUARD_PEERS_ADD, bp, rule="/peers", body=_add_peer_body) def add_peer_bp(): """POST /api/wireguard/peers — Add a new peer to the configuration.""" @daemon_route( DELETE_WIREGUARD_PEERS_REMOVE, bp, rule="/peers/", transform=_peer_name_echo ) def remove_peer_bp(): """DELETE /api/wireguard/peers/ — Remove a peer from the configuration.""" @daemon_route(GET_WIREGUARD_PEERS, bp) def peers_bp(): """GET /api/wireguard/peers — List all configured WireGuard peers.""" @daemon_route(GET_WIREGUARD_PEER_STATUS, bp) def peer_status_bp(): """GET /api/wireguard/peer-status — Get real-time status for all peers.""" @daemon_route( POST_WIREGUARD_GENERATE_CLIENT, bp, body=_gen_client_body, transform=_config_echo ) def generate_client_bp(): """POST /api/wireguard/generate-client — Generate a client config for a peer.""" # --------------------------------------------------------------------------- # Access classes # --------------------------------------------------------------------------- @daemon_route(GET_WIREGUARD_CLASSES, bp) def list_classes_bp(): """GET /api/wireguard/classes — List all access classes.""" @daemon_route(POST_WIREGUARD_CLASSES, bp, precheck=_class_key_precheck) def create_class_bp(): """POST /api/wireguard/classes — Create a new access class.""" @daemon_route( PATCH_WIREGUARD_CLASSES, bp, rule="/classes", precheck=_class_key_precheck ) def update_class_bp(): """PATCH /api/wireguard/classes — Update an access class.""" @daemon_route( DELETE_WIREGUARD_CLASSES, bp, rule="/classes", precheck=require_dict_body, body=_delete_class_body, ) def delete_class_bp(): """DELETE /api/wireguard/classes — Delete an access class.""" @daemon_route( POST_WIREGUARD_CLASSES_UP, bp, rule="/classes//up", params={"class_key": "key"}, body={}, transform=void_transform, ) def class_up_bp(): """POST /api/wireguard/classes//up — Bring up a class's tunnel.""" @daemon_route( DELETE_WIREGUARD_CLASSES_DOWN, bp, rule="/classes//down", methods=["POST"], params={"class_key": "key"}, body={}, transform=void_transform, ) def class_down_bp(): """POST /api/wireguard/classes//down — Bring down a class's tunnel.""" @daemon_route( GET_WIREGUARD_CLASS_STATUS, bp, rule="/classes//status", params={"class_key": "key"}, ) def class_status_bp(): """GET /api/wireguard/classes//status — Get status for a class's tunnel.""" @daemon_route( POST_WIREGUARD_CLASS_INIT_KEYS, bp, rule="/classes/keys/", params={"class_key": "key"}, body={}, transform=void_transform, ) def class_init_keys_bp(): """POST /api/wireguard/classes/keys/ — Generate keys for a class."""