refactor: introduce two-user daemon architecture with socket-based communication
- Add daemon/ module with aiohttp server, sync client, and handler registry - Add daemon/handlers/ for privileged operations (acme, dnsmasq, firewall, logs, nginx, wireguard) - Add system/acme-deploy.py, vacuum-walld sudoers and systemd service - Update API routes to use daemon client instead of lib/ directly - Update lib/, tests/, and webui/ for new architecture - Update docs and deployment scripts
This commit is contained in:
+53
-96
@@ -1,47 +1,23 @@
|
||||
"""
|
||||
webui/api/wireguard.py - WireGuard tunnel management API blueprint.
|
||||
"""WireGuard tunnel management API blueprint.
|
||||
|
||||
Exposed at /api/wireguard/* and delegates to lib.wireguard.
|
||||
Exposed at /api/wireguard/* and delegates to vacuum-walld.
|
||||
"""
|
||||
|
||||
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 daemon.client import BadRequest, NotFound, delete, get, patch, post
|
||||
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)
|
||||
return _ok(get("/wireguard/config"))
|
||||
except RuntimeError as exc:
|
||||
logger.error("Failed to read WireGuard config: %s", exc)
|
||||
return _error(str(exc), 500)
|
||||
@@ -53,19 +29,15 @@ def post_config():
|
||||
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 = dict(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)
|
||||
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)
|
||||
@@ -78,27 +50,24 @@ def patch_config():
|
||||
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)
|
||||
current = get_config()
|
||||
merged = deep_merge(current, body)
|
||||
save_config(merged)
|
||||
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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Apply / down
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@bp.route("/apply", methods=["POST"])
|
||||
def apply_bp():
|
||||
try:
|
||||
apply()
|
||||
post("/wireguard/apply")
|
||||
logger.info("WireGuard tunnel applied via API")
|
||||
return _ok(None)
|
||||
except RuntimeError as exc:
|
||||
@@ -109,7 +78,7 @@ def apply_bp():
|
||||
@bp.route("/up", methods=["POST"])
|
||||
def up_bp():
|
||||
try:
|
||||
apply()
|
||||
post("/wireguard/apply")
|
||||
logger.info("WireGuard tunnel started via API")
|
||||
return _ok(None)
|
||||
except RuntimeError as exc:
|
||||
@@ -120,7 +89,7 @@ def up_bp():
|
||||
@bp.route("/down", methods=["POST"])
|
||||
def down_bp():
|
||||
try:
|
||||
down()
|
||||
post("/wireguard/down")
|
||||
logger.info("WireGuard tunnel brought down via API")
|
||||
return _ok(None)
|
||||
except RuntimeError as exc:
|
||||
@@ -128,29 +97,19 @@ def down_bp():
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Status
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@bp.route("/status", methods=["GET"])
|
||||
def status_bp():
|
||||
try:
|
||||
return _ok(status())
|
||||
return _ok(get("/wireguard/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()
|
||||
post("/wireguard/initialize")
|
||||
logger.info("WireGuard initialized via API")
|
||||
return _ok(None)
|
||||
except RuntimeError as exc:
|
||||
@@ -158,11 +117,6 @@ def initialize_bp():
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Peer management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@bp.route("/peers", methods=["POST"])
|
||||
def add_peer_bp():
|
||||
body = request.get_json(silent=True) or {}
|
||||
@@ -170,15 +124,21 @@ def add_peer_bp():
|
||||
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"),
|
||||
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)
|
||||
@@ -187,12 +147,12 @@ def add_peer_bp():
|
||||
@bp.route("/peers/<name>", 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)
|
||||
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)
|
||||
@@ -201,7 +161,7 @@ def remove_peer_bp(name):
|
||||
@bp.route("/peers", methods=["GET"])
|
||||
def peers_bp():
|
||||
try:
|
||||
return _ok(get_peers())
|
||||
return _ok(get("/wireguard/peers"))
|
||||
except RuntimeError as exc:
|
||||
logger.error("Failed to list WireGuard peers: %s", exc)
|
||||
return _error(str(exc), 500)
|
||||
@@ -210,37 +170,34 @@ def peers_bp():
|
||||
@bp.route("/peer-status", methods=["GET"])
|
||||
def peer_status_bp():
|
||||
try:
|
||||
return _ok(get_peer_status())
|
||||
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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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)
|
||||
server_endpoint = body.get("server_endpoint", "")
|
||||
if not server_endpoint:
|
||||
return _error("Field 'server_endpoint' 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)
|
||||
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": conf_text})
|
||||
except (KeyError, ValueError, RuntimeError) as exc:
|
||||
code = 404 if isinstance(exc, (KeyError, ValueError)) else 500
|
||||
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), code)
|
||||
return _error(str(exc), 500)
|
||||
|
||||
Reference in New Issue
Block a user