""" webui/api/wireguard.py - WireGuard tunnel management API blueprint. Exposed at /api/wireguard/* and delegates to lib.wireguard. """ import logging from flask import Blueprint, request from lib.common import deep_merge from lib.wireguard import ( add_peer, apply, down, generate_client_conf, get_config, get_peer_status, get_peers, initialize, remove_peer, save_config, status, ) from webui.api.common import _error, _ok logger = logging.getLogger(__name__) bp = Blueprint("wireguard", __name__) # --------------------------------------------------------------------------- # Config # --------------------------------------------------------------------------- @bp.route("/config", methods=["GET"]) def get_config_bp(): try: cfg = get_config() safe = dict(cfg) if "interface" in safe: safe["interface"] = dict(safe["interface"]) safe["interface"].pop("private_key", None) return _ok(safe) 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: # Preserve existing server private key through full replacement current = get_config() current_key = current.get("interface", {}).get("private_key", "") if "interface" in body: body["interface"] = dict(body["interface"]) body["interface"].pop("private_key", None) if current_key: body.setdefault("interface", {})["private_key"] = current_key save_config(body) return _ok(None) 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["interface"] = dict(body["interface"]) body["interface"].pop("private_key", None) current = get_config() merged = deep_merge(current, body) save_config(merged) logger.info("WireGuard config patched: %s", sorted(body.keys())) return _ok(None) except RuntimeError as exc: logger.error("Failed to patch WireGuard config: %s", exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Apply / down # --------------------------------------------------------------------------- @bp.route("/apply", methods=["POST"]) def apply_bp(): try: 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: 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: 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) # --------------------------------------------------------------------------- # Status # --------------------------------------------------------------------------- @bp.route("/status", methods=["GET"]) def status_bp(): try: return _ok(status()) except RuntimeError as exc: logger.error("Failed to get WireGuard status: %s", exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Initialize (first-time setup) # --------------------------------------------------------------------------- @bp.route("/initialize", methods=["POST"]) def initialize_bp(): try: 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) # --------------------------------------------------------------------------- # Peer management # --------------------------------------------------------------------------- @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 = add_peer( 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 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: cfg = get_config() if name not in cfg.get("peers", {}): return _error(f"Peer '{name}' not found", 404) remove_peer(name) logger.info("WireGuard peer '%s' removed via API", name) return _ok({"name": name}) 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_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_peer_status()) except RuntimeError as exc: logger.error("Failed to get WireGuard peer status: %s", exc) return _error(str(exc), 500) # --------------------------------------------------------------------------- # Client config generation # --------------------------------------------------------------------------- @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) try: cfg = get_config() if name not in cfg.get("peers", {}): return _error(f"Peer '{name}' not found", 404) server_endpoint = body.get("server_endpoint", "") server_pubkey = cfg["interface"].get("public_key", "") if not server_endpoint: return _error( "Field 'server_endpoint' is required (e.g., '203.0.113.1:51820')", 400 ) conf_text = generate_client_conf(name, server_endpoint, server_pubkey) logger.info("Client config generated for peer '%s' via API", name) return _ok({"config": conf_text}) except (KeyError, ValueError, RuntimeError) as exc: code = 404 if isinstance(exc, (KeyError, ValueError)) else 500 logger.error("Failed to generate client config for '%s': %s", name, exc) return _error(str(exc), code)