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:
2026-05-27 23:38:23 +00:00
parent 5ac69dfa7e
commit 200e078bc5
39 changed files with 4671 additions and 1810 deletions
+42 -56
View File
@@ -14,25 +14,8 @@ from pathlib import Path
from flask import Flask, render_template, request
from lib.acme import get_email, list_certs
from lib.dnsmasq import get_config as dnsmasq_config
from lib.dnsmasq import get_lease_table
from lib.dnsmasq import get_status as dnsmasq_status
from lib.firewall import (
config_pending,
get_active_zones,
get_interfaces,
get_services,
get_zone_info,
)
from lib.firewall import (
get_config as fw_config_get,
)
from daemon.client import get
from lib.logging import setup_logging
from lib.nginx import get_config as nginx_config
from lib.nginx import get_domains
from lib.wireguard import get_config as wg_config
from lib.wireguard import status as wg_status
from webui.api.certs import bp as certs_bp
from webui.api.dhcp import bp as dhcp_bp
from webui.api.firewall import bp as firewall_bp
@@ -117,7 +100,6 @@ def _log_request_finish(response):
@app.template_filter("timestamp")
def timestamp_filter(value):
"""Convert an ISO timestamp string to a human-readable date."""
if not value:
return ""
try:
@@ -129,7 +111,6 @@ def timestamp_filter(value):
@app.template_filter("bytes")
def bytes_filter(value):
"""Format a byte count to a human-readable string (KB / MB / GB)."""
try:
num = float(value)
except (ValueError, TypeError):
@@ -145,7 +126,6 @@ def bytes_filter(value):
@app.template_filter("duration")
def duration_filter(value):
"""Format a duration in seconds to a human-readable string."""
try:
total = int(float(value))
except (ValueError, TypeError):
@@ -168,7 +148,6 @@ def duration_filter(value):
@app.template_filter("json_pretty")
def json_pretty_filter(value):
"""Pretty-print a JSON-serialisable value for debug displays."""
import json
try:
@@ -205,14 +184,21 @@ def _get_service_status(dnsmasq_info, wg_info):
return services
def _fw_config_get():
"""Read firewall config via daemon."""
return get("/firewall/config")
@app.route("/")
def dashboard():
active_zones = _safely(get_active_zones, {})
interfaces = _safely(get_interfaces, [])
dnsmasq = _safely(dnsmasq_status, {})
domains = _safely(get_domains, [])
certs = _safely(list_certs, [])
wg = _safely(wg_status, {})
active_zones = _safely(
lambda: {k: v for k, v in get("/firewall/zones").get("active", {}).items()}, {}
)
interfaces = _safely(lambda: get("/firewall/interfaces"), [])
dnsmasq = _safely(lambda: get("/dnsmasq/status"), {})
domains = _safely(lambda: get("/nginx/domains"), [])
certs = _safely(lambda: get("/acme/list"), [])
wg = _safely(lambda: get("/wireguard/status"), {})
return render_template(
"dashboard.html",
@@ -223,36 +209,30 @@ def dashboard():
certs=certs,
wg_status=wg,
services=_get_service_status(dnsmasq, wg),
firewall_config=_safely(fw_config_get, {}),
firewall_pending=_safely(config_pending, {}),
firewall_config=_safely(_fw_config_get, {}),
firewall_pending=_safely(lambda: get("/firewall/config/pending"), {}),
)
@app.route("/interfaces")
def interfaces_page():
firewall_config = _safely(fw_config_get, {})
firewall_pending = _safely(config_pending, {})
return render_template(
"interfaces.html",
interfaces=_safely(get_interfaces, []),
active_zones=_safely(get_active_zones, {}),
firewall_config=firewall_config,
firewall_pending=firewall_pending,
interfaces=_safely(lambda: get("/firewall/interfaces"), []),
zones=_safely(lambda: get("/firewall/zones").get("available", []), []),
firewall_config=_safely(_fw_config_get, {}),
firewall_pending=_safely(lambda: get("/firewall/config/pending"), {}),
)
@app.route("/zones")
def zones_page():
firewall_config = _safely(fw_config_get, {})
firewall_pending = _safely(config_pending, {})
zones_data = {}
for name in _safely(get_active_zones, {}):
zones_data[name] = _safely(lambda n=name: get_zone_info(n), {})
firewall_config = _safely(_fw_config_get, {})
firewall_pending = _safely(lambda: get("/firewall/config/pending"), {})
return render_template(
"zones.html",
zones=zones_data,
interfaces=_safely(get_interfaces, []),
services=_safely(get_services, []),
zones=_safely(lambda: get("/firewall/zones/all"), []),
services=_safely(lambda: get("/firewall/services"), []),
firewall_config=firewall_config,
firewall_pending=firewall_pending,
)
@@ -260,8 +240,8 @@ def zones_page():
@app.route("/rules")
def rules_page():
zones = list(_safely(get_active_zones, {}).keys())
raw = _safely(fw_config_get, {})
zones = list(_safely(lambda: get("/firewall/zones").get("active", {}).keys(), []))
raw = _safely(_fw_config_get, {})
rules = {}
for zname, zcfg in raw.get("zones", {}).items():
rr = zcfg.get("rich_rules", [])
@@ -272,40 +252,46 @@ def rules_page():
@app.route("/nat")
def nat_page():
zones = {}
for name in _safely(get_active_zones, {}):
zones[name] = _safely(lambda n=name: get_zone_info(n), {})
return render_template("nat.html", zones=zones)
return render_template(
"nat.html", zones=_safely(lambda: get("/firewall/zones/all"), [])
)
@app.route("/dhcp")
def dhcp_page():
return render_template(
"dhcp.html",
config=_safely(dnsmasq_config, {}),
status=_safely(dnsmasq_status, {}),
leases=_safely(get_lease_table, []),
config=_safely(lambda: get("/dnsmasq/config"), {}),
status=_safely(lambda: get("/dnsmasq/status"), {}),
leases=_safely(lambda: get("/dnsmasq/leases"), []),
)
@app.route("/proxy")
def proxy_page():
return render_template(
"proxy.html", domains=_safely(get_domains, []), config=_safely(nginx_config, {})
"proxy.html",
domains=_safely(lambda: get("/nginx/domains"), []),
config=_safely(lambda: get("/nginx/config"), {}),
)
@app.route("/certs")
def certs_page():
email_data = _safely(lambda: get("/acme/email"), {"email": ""})
return render_template(
"certs.html", certs=_safely(list_certs, []), email=_safely(get_email, "")
"certs.html",
certs=_safely(lambda: get("/acme/list"), []),
email=email_data.get("email", ""),
)
@app.route("/wireguard")
def wireguard_page():
return render_template(
"wireguard.html", config=_safely(wg_config, {}), status=_safely(wg_status, {})
"wireguard.html",
config=_safely(lambda: get("/wireguard/config"), {}),
status=_safely(lambda: get("/wireguard/status"), {}),
)