Initial commit: SSL proxy / firewall appliance

Flask WebUI behind nginx reverse proxy with zone-based firewall, DHCP,
WireGuard, and ACME certificate management.
This commit is contained in:
2026-05-07 22:24:24 +00:00
commit e2f56b8cc8
56 changed files with 10013 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
"""
server.py - Vacuum Wall management WebUI entry point.
Serves the Flask application on 127.0.0.1:9090. Nginx terminates SSL
and enforces basic authentication before proxying to this port.
"""
import logging
import os
from datetime import datetime
from flask import Flask, render_template
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 get_active_zones, get_interfaces, get_zone_info
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
from webui.api.proxy import bp as proxy_bp
from webui.api.wireguard import bp as wireguard_bp
# ---------------------------------------------------------------------------
# App factory
# ---------------------------------------------------------------------------
app = Flask(__name__)
app.config["SECRET_KEY"] = os.urandom(32).hex()
app.register_blueprint(firewall_bp, url_prefix="/api/firewall")
app.register_blueprint(dhcp_bp, url_prefix="/api/dhcp")
app.register_blueprint(proxy_bp, url_prefix="/api/proxy")
app.register_blueprint(certs_bp, url_prefix="/api/certs")
app.register_blueprint(wireguard_bp, url_prefix="/api/wireguard")
# ---------------------------------------------------------------------------
# Jinja2 custom filters
# ---------------------------------------------------------------------------
@app.template_filter("timestamp")
def timestamp_filter(value):
"""Convert an ISO timestamp string to a human-readable date."""
if not value:
return ""
try:
dt = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
return dt.strftime("%Y-%m-%d %H:%M:%S")
except (ValueError, TypeError):
return str(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):
return str(value)
if num < 0:
return "0 B"
for unit in ("B", "KB", "MB", "GB", "TB"):
if abs(num) < 1024:
return f"{num:.1f} {unit}"
num /= 1024
return f"{num:.1f} PB"
@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):
return str(value)
if total < 0:
return "0s"
parts = []
days, remainder = divmod(total, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
if days:
parts.append(f"{days}d")
if hours:
parts.append(f"{hours}h")
if minutes:
parts.append(f"{minutes}m")
parts.append(f"{seconds}s")
return " ".join(parts)
@app.template_filter("json_pretty")
def json_pretty_filter(value):
"""Pretty-print a JSON-serialisable value for debug displays."""
import json
try:
return json.dumps(value, indent=2, default=str)
except (TypeError, ValueError):
return str(value)
# ---------------------------------------------------------------------------
# Page routes
# ---------------------------------------------------------------------------
logger = logging.getLogger(__name__)
def _safely(fn, default=None):
"""Call *fn* and return *default* on any exception."""
try:
return fn()
except Exception as exc:
logger.warning("WebUI data load failed: %s", exc)
return default
@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, {})
return render_template(
"dashboard.html",
active_zones=active_zones,
interfaces=interfaces,
dnsmasq=dnsmasq,
domains=domains,
certs=certs,
wg_status=wg,
)
@app.route("/interfaces")
def interfaces_page():
return render_template(
"interfaces.html",
interfaces=_safely(get_interfaces, []),
active_zones=_safely(get_active_zones, {}),
)
@app.route("/zones")
def zones_page():
zones = {}
for name in _safely(get_active_zones, {}):
zones[name] = _safely(lambda n=name: get_zone_info(n), {})
return render_template(
"zones.html",
zones=zones,
interfaces=_safely(get_interfaces, []),
services=_safely(
lambda: __import__(
"lib.firewall", fromlist=["get_services"]
).get_services(),
[],
),
)
@app.route("/rules")
def rules_page():
zones = list(_safely(get_active_zones, {}).keys())
return render_template("rules.html", zones=zones)
@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)
@app.route("/dhcp")
def dhcp_page():
return render_template(
"dhcp.html",
config=_safely(dnsmasq_config, {}),
status=_safely(dnsmasq_status, {}),
leases=_safely(get_lease_table, []),
)
@app.route("/proxy")
def proxy_page():
return render_template(
"proxy.html", domains=_safely(get_domains, []), config=_safely(nginx_config, {})
)
@app.route("/certs")
def certs_page():
return render_template(
"certs.html", certs=_safely(list_certs, []), email=_safely(get_email, "")
)
@app.route("/wireguard")
def wireguard_page():
return render_template(
"wireguard.html", config=_safely(wg_config, {}), status=_safely(wg_status, {})
)
@app.route("/logs")
def logs_page():
return render_template("logs.html")
if __name__ == "__main__":
app.run(host="127.0.0.1", port=9090)