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:
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
webui/api/firewall.py - Firewall (firewalld) management API blueprint.
|
||||
|
||||
Exposed at /api/firewall/* and delegates all mutations to lib.firewall.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from lib.firewall import (
|
||||
add_forward_port,
|
||||
add_rich_rule,
|
||||
create_zone,
|
||||
delete_zone,
|
||||
get_active_zones,
|
||||
get_available_zones,
|
||||
get_interfaces,
|
||||
get_rich_rules,
|
||||
get_services,
|
||||
get_zone_info,
|
||||
remove_forward_port,
|
||||
remove_rich_rule,
|
||||
set_masquerade,
|
||||
set_zone_interfaces,
|
||||
set_zone_services,
|
||||
)
|
||||
|
||||
bp = Blueprint("firewall", __name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Error helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _error(msg, code=400):
|
||||
return jsonify({"ok": False, "error": msg}), code
|
||||
|
||||
|
||||
def _ok(data=None):
|
||||
body = {"ok": True}
|
||||
if data is not None:
|
||||
body["data"] = data
|
||||
return jsonify(body)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zones
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@bp.route("/zones", methods=["GET"])
|
||||
def list_zones():
|
||||
try:
|
||||
active = get_active_zones()
|
||||
available = get_available_zones()
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"data": {
|
||||
"active": active,
|
||||
"available": available,
|
||||
},
|
||||
}
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
@bp.route("/zones/<name>", methods=["GET"])
|
||||
def zone_details(name):
|
||||
try:
|
||||
info = get_zone_info(name)
|
||||
return jsonify({"ok": True, "data": info})
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
@bp.route("/zones", methods=["POST"])
|
||||
def create_zone_bp():
|
||||
body = request.get_json(silent=True) or {}
|
||||
zone_name = body.get("name", "").strip()
|
||||
target = body.get("target", "default").strip() or "default"
|
||||
if not zone_name:
|
||||
return _error("Zone name is required", 400)
|
||||
try:
|
||||
if zone_name in get_available_zones():
|
||||
return _error(f"Zone '{zone_name}' already exists", 400)
|
||||
create_zone(zone_name, target)
|
||||
return _ok({"name": zone_name, "target": target})
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
@bp.route("/zones/<name>", methods=["DELETE"])
|
||||
def delete_zone_bp(name):
|
||||
try:
|
||||
available = get_available_zones()
|
||||
if name not in available:
|
||||
return _error(f"Zone '{name}' does not exist", 404)
|
||||
delete_zone(name)
|
||||
return _ok({"name": name})
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zone interfaces
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@bp.route("/zones/<name>/interfaces", methods=["POST"])
|
||||
def set_zone_interfaces_bp(name):
|
||||
body = request.get_json(silent=True) or {}
|
||||
interfaces = body.get("interfaces", [])
|
||||
if not isinstance(interfaces, list):
|
||||
return _error("'interfaces' must be a list", 400)
|
||||
try:
|
||||
set_zone_interfaces(name, interfaces)
|
||||
return _ok({"zone": name, "interfaces": interfaces})
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zone services
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@bp.route("/zones/<name>/services", methods=["POST"])
|
||||
def set_zone_services_bp(name):
|
||||
body = request.get_json(silent=True) or {}
|
||||
services = body.get("services", [])
|
||||
if not isinstance(services, list):
|
||||
return _error("'services' must be a list", 400)
|
||||
try:
|
||||
set_zone_services(name, services)
|
||||
return _ok({"zone": name, "services": services})
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Available services and interfaces
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@bp.route("/services", methods=["GET"])
|
||||
def list_services():
|
||||
try:
|
||||
return _ok(get_services())
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
@bp.route("/interfaces", methods=["GET"])
|
||||
def list_interfaces():
|
||||
try:
|
||||
return _ok(get_interfaces())
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rich rules
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@bp.route("/rich-rules", methods=["POST"])
|
||||
def add_rich_rule_bp():
|
||||
body = request.get_json(silent=True) or {}
|
||||
zone = body.get("zone", "").strip()
|
||||
rule = body.get("rule", "").strip()
|
||||
if not zone or not rule:
|
||||
return _error("Both 'zone' and 'rule' are required", 400)
|
||||
try:
|
||||
add_rich_rule(zone, rule)
|
||||
return _ok({"zone": zone, "rule": rule})
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
@bp.route("/rich-rules", methods=["DELETE"])
|
||||
def remove_rich_rule_bp():
|
||||
body = request.get_json(silent=True) or {}
|
||||
zone = body.get("zone", "").strip()
|
||||
rule = body.get("rule", "").strip()
|
||||
if not zone or not rule:
|
||||
return _error("Both 'zone' and 'rule' are required", 400)
|
||||
try:
|
||||
remove_rich_rule(zone, rule)
|
||||
return _ok({"zone": zone, "rule": rule})
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
@bp.route("/rich-rules/<zone>", methods=["GET"])
|
||||
def list_rich_rules(zone):
|
||||
try:
|
||||
rules = get_rich_rules(zone)
|
||||
return _ok(rules)
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Masquerade (NAT)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@bp.route("/masquerade", methods=["POST"])
|
||||
def set_masquerade_bp():
|
||||
body = request.get_json(silent=True) or {}
|
||||
zone = body.get("zone", "").strip()
|
||||
enable = body.get("enable")
|
||||
if not zone or enable is None:
|
||||
return _error("'zone' and 'enable' (bool) are required", 400)
|
||||
try:
|
||||
set_masquerade(zone, bool(enable))
|
||||
return _ok({"zone": zone, "masquerade": bool(enable)})
|
||||
except RuntimeError as exc:
|
||||
return _error(str(exc), 500)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Port forwarding
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@bp.route("/forward-port", methods=["POST"])
|
||||
def add_forward_port_bp():
|
||||
body = request.get_json(silent=True) or {}
|
||||
zone = body.get("zone", "").strip()
|
||||
port = body.get("port")
|
||||
proto = body.get("proto", "").strip()
|
||||
toaddr = body.get("toaddr")
|
||||
toport = body.get("toport")
|
||||
if not zone or port is None or not proto:
|
||||
return _error("'zone', 'port', and 'proto' are required", 400)
|
||||
try:
|
||||
add_forward_port(
|
||||
zone,
|
||||
int(port),
|
||||
proto,
|
||||
toaddr=str(toaddr) if toaddr else None,
|
||||
toport=int(toport) if toport else None,
|
||||
)
|
||||
return _ok({"zone": zone, "port": int(port), "proto": proto})
|
||||
except (ValueError, RuntimeError) as exc:
|
||||
code = 400 if isinstance(exc, ValueError) else 500
|
||||
return _error(str(exc), code)
|
||||
|
||||
|
||||
@bp.route("/forward-port", methods=["DELETE"])
|
||||
def remove_forward_port_bp():
|
||||
body = request.get_json(silent=True) or {}
|
||||
zone = body.get("zone", "").strip()
|
||||
port = body.get("port")
|
||||
proto = body.get("proto", "").strip()
|
||||
toaddr = body.get("toaddr")
|
||||
toport = body.get("toport")
|
||||
if not zone or port is None or not proto:
|
||||
return _error("'zone', 'port', and 'proto' are required", 400)
|
||||
try:
|
||||
remove_forward_port(
|
||||
zone,
|
||||
int(port),
|
||||
proto,
|
||||
toaddr=str(toaddr) if toaddr else None,
|
||||
toport=int(toport) if toport else None,
|
||||
)
|
||||
return _ok({"zone": zone, "port": int(port), "proto": proto})
|
||||
except (ValueError, RuntimeError) as exc:
|
||||
code = 400 if isinstance(exc, ValueError) else 500
|
||||
return _error(str(exc), code)
|
||||
Reference in New Issue
Block a user