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:
+52
-674
@@ -1,21 +1,16 @@
|
||||
"""
|
||||
firewall.py - firewalld manager for Vacuum Wall SSL proxy firewall appliance.
|
||||
firewall.py - firewalld parsing helpers & declarative config for Vacuum Wall.
|
||||
|
||||
Wraps firewall-cmd CLI via sudo, manages zones, rules, masquerade/NAT,
|
||||
and port-forwarding. All mutations are --permanent followed by --reload.
|
||||
|
||||
A JSON snapshot of all rules is persisted at DATA_DIR/rules.json so the
|
||||
Flask UI can inspect or restore previous configurations.
|
||||
Pure logic only — no subprocess or sudo calls.
|
||||
All privileged commands are handled by daemon/handlers/firewall.py.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from lib.common import load_json, run, save_json
|
||||
from lib.common import load_json, save_json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,35 +28,8 @@ DEFAULT_CONFIG: dict[str, Any] = {"zones": {}}
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _reload() -> None:
|
||||
"""Reload firewalld so permanent changes take effect immediately."""
|
||||
try:
|
||||
run(["firewall-cmd", "--reload"], sudo=True)
|
||||
logger.info("firewalld reloaded")
|
||||
except RuntimeError as exc:
|
||||
logger.error("firewalld reload failed: %s", exc)
|
||||
raise
|
||||
|
||||
|
||||
def _gen_id() -> str:
|
||||
"""Generate a short unique identifier (8 hex characters)."""
|
||||
return uuid4().hex[:8]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read-only queries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_available_zones() -> list[str]:
|
||||
"""Return the list of all built-in (available) firewalld zone names."""
|
||||
output = run(["firewall-cmd", "--get-zones"], sudo=True)
|
||||
return output.split()
|
||||
|
||||
|
||||
def get_active_zones() -> dict[str, list[str]]:
|
||||
"""Return a dict mapping active zone names to their assigned interfaces."""
|
||||
output = run(["firewall-cmd", "--get-active-zones"], sudo=True)
|
||||
def _parse_active_zones(output: str) -> dict[str, list[str]]:
|
||||
"""Parse ``firewall-cmd --get-active-zones`` output."""
|
||||
zones: dict[str, list[str]] = {}
|
||||
current_zone: str | None = None
|
||||
for raw_line in output.splitlines():
|
||||
@@ -75,17 +43,30 @@ def get_active_zones() -> dict[str, list[str]]:
|
||||
else zones.get(list(zones.keys())[-1], [])
|
||||
)
|
||||
for piece in stripped.split():
|
||||
if piece.endswith(":"):
|
||||
continue
|
||||
if current_zone and piece not in current_ifaces:
|
||||
current_ifaces.append(piece)
|
||||
else:
|
||||
current_zone = stripped
|
||||
current_zone = stripped.removesuffix(" (default)")
|
||||
zones[current_zone] = []
|
||||
return zones
|
||||
|
||||
|
||||
def get_zone_info(zone: str) -> dict[str, Any]:
|
||||
"""Return detailed information for *zone*."""
|
||||
output = run(["firewall-cmd", f"--zone={zone}", "--list-all"], sudo=True)
|
||||
def _parse_interfaces(output: str) -> list[str]:
|
||||
"""Parse ``ip -o link show`` output."""
|
||||
ifaces: list[str] = []
|
||||
for line in output.splitlines():
|
||||
if line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
name = parts[1].rstrip(":")
|
||||
ifaces.append(name)
|
||||
return ifaces
|
||||
|
||||
|
||||
def _parse_zone_output(zone: str, output: str) -> dict[str, Any]:
|
||||
"""Parse ``firewall-cmd --zone=Z --list-all`` output."""
|
||||
info: dict[str, Any] = {"name": zone}
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
@@ -135,444 +116,6 @@ def get_zone_info(zone: str) -> dict[str, Any]:
|
||||
return info
|
||||
|
||||
|
||||
def get_services() -> list[str]:
|
||||
"""Return the list of available service names known to firewalld."""
|
||||
output = run(["firewall-cmd", "--get-services"], sudo=True)
|
||||
return output.split()
|
||||
|
||||
|
||||
def get_icmp_blocks() -> list[str]:
|
||||
"""Return the list of available ICMP block names."""
|
||||
output = run(["firewall-cmd", "--get-icmptypes"], sudo=True)
|
||||
return output.split()
|
||||
|
||||
|
||||
def get_interfaces() -> list[str]:
|
||||
"""Return the list of network interfaces visible via iproute2."""
|
||||
output = run(["ip", "-o", "link", "show"])
|
||||
ifaces: list[str] = []
|
||||
for line in output.splitlines():
|
||||
if line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
name = parts[1].rstrip(":")
|
||||
ifaces.append(name)
|
||||
return ifaces
|
||||
|
||||
|
||||
def get_rich_rules(zone: str) -> list[str]:
|
||||
"""Return the rich rules defined for *zone* as a list of raw strings."""
|
||||
output = run(["firewall-cmd", f"--zone={zone}", "--list-rich-rules"], sudo=True)
|
||||
output = output.strip()
|
||||
if not output:
|
||||
return []
|
||||
rules: list[str] = []
|
||||
current: list[str] = []
|
||||
for line in output.splitlines():
|
||||
raw = line.rstrip()
|
||||
if not raw.endswith(";"):
|
||||
current.append(raw)
|
||||
else:
|
||||
current.append(raw)
|
||||
rules.append(" ".join(current))
|
||||
current = []
|
||||
if current:
|
||||
rules.append(" ".join(current))
|
||||
return rules
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zone CRUD
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def create_zone(zone: str, target: str = "default") -> None:
|
||||
"""Create a new permanent zone in firewalld."""
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
f"--set-target={target}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
logger.info("Firewall zone '%s' created (target=%s)", zone, target)
|
||||
|
||||
|
||||
def delete_zone(zone: str) -> None:
|
||||
"""Delete an existing zone."""
|
||||
run(["firewall-cmd", f"--zone={zone}", "--delete", "--permanent"], sudo=True)
|
||||
_reload()
|
||||
logger.info("Firewall zone '%s' deleted", zone)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interface assignment
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def set_zone_interfaces(zone: str, interfaces: list[str]) -> None:
|
||||
"""Assign *interfaces* to *zone*, replacing any existing assignments."""
|
||||
try:
|
||||
current = get_zone_info(zone).get("interfaces", [])
|
||||
except Exception:
|
||||
current = []
|
||||
for iface in current:
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
"--remove-interface=" + iface,
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
for iface in interfaces:
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
"--add-interface=" + iface,
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
logger.info("Zone '%s' interfaces set to %s", zone, interfaces)
|
||||
|
||||
|
||||
def add_zone_interface(zone: str, iface: str) -> None:
|
||||
"""Add a single interface to *zone*."""
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
"--add-interface=" + iface,
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
logger.info("Interface '%s' added to zone '%s'", iface, zone)
|
||||
|
||||
|
||||
def remove_zone_interface(zone: str, iface: str) -> None:
|
||||
"""Remove a single interface from *zone*."""
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
"--remove-interface=" + iface,
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
logger.info("Interface '%s' removed from zone '%s'", iface, zone)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Service management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def set_zone_services(zone: str, services: list[str]) -> None:
|
||||
"""Set services for *zone*, replacing any previously allowed services."""
|
||||
current = get_zone_info(zone).get("services", [])
|
||||
for svc in current:
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
f"--remove-service={svc}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
for svc in services:
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
f"--add-service={svc}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
logger.info("Zone '%s' services set to %s", zone, services)
|
||||
|
||||
|
||||
def add_zone_service(zone: str, service: str) -> None:
|
||||
"""Add a single service to *zone*."""
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
f"--add-service={service}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
logger.info("Service '%s' added to zone '%s'", service, zone)
|
||||
|
||||
|
||||
def remove_zone_service(zone: str, service: str) -> None:
|
||||
"""Remove a single service from *zone*."""
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
f"--remove-service={service}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
logger.info("Service '%s' removed from zone '%s'", service, zone)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rich rules
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def add_rich_rule(zone: str, rule: str) -> dict[str, Any]:
|
||||
"""Add a rich rule to *zone* and persist to declarative config."""
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
"--add-rich-rule=" + rule,
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
_persist_rich_rule(zone, rule)
|
||||
rule_entry = _get_rich_rule_entry(zone, rule)
|
||||
logger.info("Rich rule added to zone '%s': %s", zone, rule[:80])
|
||||
return rule_entry
|
||||
|
||||
|
||||
def remove_rich_rule(zone: str, rule: str) -> None:
|
||||
"""Remove a rich rule from *zone*."""
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
"--remove-rich-rule=" + rule,
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
_unpersist_rich_rule(zone, rule)
|
||||
logger.info("Rich rule removed from zone '%s': %s", zone, rule[:80])
|
||||
|
||||
|
||||
def _persist_rich_rule(zone: str, rule: str) -> dict[str, Any]:
|
||||
"""Add a rich rule to the declarative config with a generated id."""
|
||||
cfg = get_config()
|
||||
cfg.setdefault("zones", {})
|
||||
cfg["zones"].setdefault(zone, {})
|
||||
cfg["zones"][zone].setdefault("rich_rules", [])
|
||||
existing_rules = cfg["zones"][zone]["rich_rules"]
|
||||
rule_id = _gen_id()
|
||||
entry = {"id": rule_id, "rule": rule}
|
||||
existing_rules.append(entry)
|
||||
save_config(cfg)
|
||||
return entry
|
||||
|
||||
|
||||
def _unpersist_rich_rule(zone: str, rule: str) -> None:
|
||||
"""Remove a rich rule from the declarative config by rule string."""
|
||||
cfg = get_config()
|
||||
zone_cfg = cfg.get("zones", {}).get(zone, {})
|
||||
rules = zone_cfg.get("rich_rules", [])
|
||||
zone_cfg["rich_rules"] = [r for r in rules if r.get("rule") != rule]
|
||||
save_config(cfg)
|
||||
|
||||
|
||||
def _get_rich_rule_entry(zone: str, rule: str) -> dict[str, Any]:
|
||||
"""Look up a rich rule entry in the declarative config."""
|
||||
cfg = get_config()
|
||||
for r in cfg.get("zones", {}).get(zone, {}).get("rich_rules", []):
|
||||
if r.get("rule") == rule:
|
||||
return r
|
||||
return {"rule": rule}
|
||||
|
||||
|
||||
def remove_rich_rule_by_id(zone: str, rule_id: str) -> None:
|
||||
"""Remove a rich rule from *zone* by its config id."""
|
||||
cfg = get_config()
|
||||
zone_cfg = cfg.get("zones", {}).get(zone, {})
|
||||
entry = None
|
||||
for r in zone_cfg.get("rich_rules", []):
|
||||
if r.get("id") == rule_id:
|
||||
entry = r
|
||||
break
|
||||
if entry is None:
|
||||
raise ValueError(f"Rich rule '{rule_id}' not found in zone '{zone}'")
|
||||
rule = entry["rule"]
|
||||
remove_rich_rule(zone, rule)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Masquerade (NAT)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def set_masquerade(zone: str, enable: bool) -> None:
|
||||
"""Enable or disable masquerade (source-NAT) on *zone*."""
|
||||
action = "--add-masquerade" if enable else "--remove-masquerade"
|
||||
run(["firewall-cmd", f"--zone={zone}", action, "--permanent"], sudo=True)
|
||||
_reload()
|
||||
logger.info("Masquerade %s on zone '%s'", "enabled" if enable else "disabled", zone)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Port forwarding
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def add_forward_port(
|
||||
zone: str,
|
||||
port: int,
|
||||
protocol: str,
|
||||
toaddr: str | None = None,
|
||||
toport: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Add a port forwarding rule to *zone* and persist to declarative config."""
|
||||
fwd = f"port={port}/proto={protocol}"
|
||||
if toaddr and toport:
|
||||
fwd += f"/toaddr={toaddr}/toport={toport}"
|
||||
elif toport:
|
||||
fwd += f"/toport={toport}"
|
||||
else:
|
||||
fwd += f"/toaddr={toaddr}" if toaddr else ""
|
||||
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
f"--add-forward-port={fwd}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
_persist_forward_port(zone, port, protocol, toaddr, toport)
|
||||
fp_entry = _get_forward_port_entry(zone, port, protocol)
|
||||
logger.info("Port forward added to zone '%s': %s", zone, fwd)
|
||||
return fp_entry
|
||||
|
||||
|
||||
def remove_forward_port(
|
||||
zone: str,
|
||||
port: int,
|
||||
protocol: str,
|
||||
toaddr: str | None = None,
|
||||
toport: int | None = None,
|
||||
) -> None:
|
||||
"""Remove a previously added port-forwarding rule from *zone*."""
|
||||
fwd = f"port={port}/proto={protocol}"
|
||||
if toaddr and toport:
|
||||
fwd += f"/toaddr={toaddr}/toport={toport}"
|
||||
elif toport:
|
||||
fwd += f"/toport={toport}"
|
||||
else:
|
||||
fwd += f"/toaddr={toaddr}" if toaddr else ""
|
||||
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone}",
|
||||
f"--remove-forward-port={fwd}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
)
|
||||
_reload()
|
||||
_unpersist_forward_port(zone, port, protocol)
|
||||
logger.info("Port forward removed from zone '%s': %s", zone, fwd)
|
||||
|
||||
|
||||
def _persist_forward_port(
|
||||
zone: str,
|
||||
port: int,
|
||||
protocol: str,
|
||||
toaddr: str | None = None,
|
||||
toport: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Add a forward port to the declarative config with a generated id."""
|
||||
cfg = get_config()
|
||||
cfg.setdefault("zones", {})
|
||||
cfg["zones"].setdefault(zone, {})
|
||||
cfg["zones"][zone].setdefault("forward_ports", [])
|
||||
fp_id = _gen_id()
|
||||
entry: dict[str, Any] = {
|
||||
"id": fp_id,
|
||||
"port": port,
|
||||
"proto": protocol,
|
||||
}
|
||||
if toaddr:
|
||||
entry["toaddr"] = toaddr
|
||||
if toport:
|
||||
entry["toport"] = toport
|
||||
cfg["zones"][zone]["forward_ports"].append(entry)
|
||||
save_config(cfg)
|
||||
return entry
|
||||
|
||||
|
||||
def _unpersist_forward_port(zone: str, port: int, protocol: str) -> None:
|
||||
"""Remove a forward port from the declarative config by port+proto."""
|
||||
cfg = get_config()
|
||||
fps = cfg.get("zones", {}).get(zone, {}).get("forward_ports", [])
|
||||
cfg.setdefault("zones", {}).setdefault(zone, {})
|
||||
cfg["zones"][zone]["forward_ports"] = [
|
||||
fp for fp in fps if not (fp.get("port") == port and fp.get("proto") == protocol)
|
||||
]
|
||||
save_config(cfg)
|
||||
|
||||
|
||||
def _get_forward_port_entry(zone: str, port: int, protocol: str) -> dict[str, Any]:
|
||||
"""Look up a forward port entry in the declarative config."""
|
||||
cfg = get_config()
|
||||
for fp in cfg.get("zones", {}).get(zone, {}).get("forward_ports", []):
|
||||
if fp.get("port") == port and fp.get("proto") == protocol:
|
||||
return fp
|
||||
entry: dict[str, Any] = {"port": port, "proto": protocol}
|
||||
return entry
|
||||
|
||||
|
||||
def remove_forward_port_by_id(zone: str, port: int, protocol: str) -> None:
|
||||
"""Remove a forward port from *zone* by port+proto (id used by API layer)."""
|
||||
cfg = get_config()
|
||||
zone_cfg = cfg.get("zones", {}).get(zone, {})
|
||||
entry = None
|
||||
for fp in zone_cfg.get("forward_ports", []):
|
||||
if fp.get("port") == port and fp.get("proto") == protocol:
|
||||
entry = fp
|
||||
break
|
||||
if entry is None:
|
||||
raise ValueError(f"Forward port {port}/{protocol} not found in zone '{zone}'")
|
||||
remove_forward_port(
|
||||
zone,
|
||||
port,
|
||||
protocol,
|
||||
toaddr=entry.get("toaddr"),
|
||||
toport=entry.get("toport"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers for parsing forward-port lines
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -608,36 +151,16 @@ def _parse_forward_ports(value: str) -> list[dict[str, Any]]:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_state() -> dict[str, Any]:
|
||||
"""Return the complete current state of firewalld as a Python dict."""
|
||||
zones: dict[str, dict[str, Any]] = {}
|
||||
for name in get_available_zones():
|
||||
try:
|
||||
zones[name] = get_zone_info(name)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return {
|
||||
"active_zones": get_active_zones(),
|
||||
"interfaces": get_interfaces(),
|
||||
"available_services": get_services(),
|
||||
"zones": zones,
|
||||
"rich_rules": {name: get_rich_rules(name) for name in zones},
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
"""Return the current UTC time as an ISO-8601 string."""
|
||||
return datetime.now(UTC).isoformat()
|
||||
|
||||
|
||||
def save_backup() -> str:
|
||||
"""Capture the full state and write it to RULES_FILE on disk."""
|
||||
state = get_state()
|
||||
def save_backup(state: dict[str, Any]) -> str:
|
||||
"""Write *state* to RULES_FILE on disk."""
|
||||
save_json(RULES_FILE, state)
|
||||
logger.info("Firewall state backup saved to %s", RULES_FILE)
|
||||
return RULES_FILE
|
||||
return str(RULES_FILE)
|
||||
|
||||
|
||||
def load_backup() -> dict[str, Any]:
|
||||
@@ -645,60 +168,6 @@ def load_backup() -> dict[str, Any]:
|
||||
return load_json(RULES_FILE)
|
||||
|
||||
|
||||
def restore_backup(state: dict[str, Any]) -> None:
|
||||
"""Apply the zone configuration described in *state*."""
|
||||
zones_cfg = state.get("zones", {})
|
||||
for zone_name, zinfo in zones_cfg.items():
|
||||
if zone_name not in get_available_zones():
|
||||
target = zinfo.get("target", "default")
|
||||
create_zone(zone_name, target)
|
||||
|
||||
services = zinfo.get("services", [])
|
||||
set_zone_services(zone_name, services)
|
||||
|
||||
interfaces = zinfo.get("interfaces", [])
|
||||
set_zone_interfaces(zone_name, interfaces)
|
||||
|
||||
if zinfo.get("masquerade"):
|
||||
set_masquerade(zone_name, True)
|
||||
|
||||
for fp in zinfo.get("forward-ports", []):
|
||||
if isinstance(fp, str):
|
||||
fp_str = fp
|
||||
else:
|
||||
parts = [f"port={fp['port']}", f"proto={fp['proto']}"]
|
||||
if "toaddr" in fp:
|
||||
parts.append(f"toaddr={fp['toaddr']}")
|
||||
if "toport" in fp:
|
||||
parts.append(f"toport={fp['toport']}")
|
||||
fp_str = "/".join(parts)
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone_name}",
|
||||
f"--add-forward-port={fp_str}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
for rule in zinfo.get("rich-rules", []):
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone_name}",
|
||||
f"--add-rich-rule={rule}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
_reload()
|
||||
logger.info("Firewall backup restored, %d zones processed", len(zones_cfg))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Declarative config management (config/firewall/config.json)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -745,12 +214,16 @@ def _live_target_to_config(target: str) -> str:
|
||||
return "DEFAULT"
|
||||
|
||||
|
||||
def config_pending() -> dict[str, Any]:
|
||||
"""Compare declarative config against live firewalld state, return diff."""
|
||||
cfg = get_config()
|
||||
live_state = get_state()
|
||||
def _compute_pending_changes(
|
||||
cfg: dict[str, Any],
|
||||
live_zones: dict[str, dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
"""Compare declarative config against live zone state, return diff.
|
||||
|
||||
Pure function — no subprocess calls. Caller is responsible for providing
|
||||
live state (typically from the daemon).
|
||||
"""
|
||||
cfg_zones = cfg.get("zones", {})
|
||||
live_zones = live_state.get("zones", {})
|
||||
|
||||
changes: list[dict[str, Any]] = []
|
||||
unknown_live: dict[str, Any] = {}
|
||||
@@ -851,93 +324,15 @@ def config_pending() -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def config_apply() -> dict[str, Any]:
|
||||
"""Apply the declarative config to live firewalld."""
|
||||
def config_pending(state: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Compare declarative config against firewalld live state, return diff.
|
||||
|
||||
*state* is required — the daemon always passes live state via
|
||||
`daemon.handlers.firewall.get_state()`.
|
||||
"""
|
||||
cfg = get_config()
|
||||
cfg_zones = cfg.get("zones", {})
|
||||
|
||||
save_backup()
|
||||
|
||||
available = get_available_zones()
|
||||
applied: list[str] = []
|
||||
for zone_name, zone_cfg in cfg_zones.items():
|
||||
need_create = zone_name not in available
|
||||
|
||||
if need_create:
|
||||
target = _normalize_target(zone_cfg.get("target", "DEFAULT"))
|
||||
create_zone(zone_name, target)
|
||||
else:
|
||||
desired_target = _normalize_target(zone_cfg.get("target", "DEFAULT"))
|
||||
if desired_target != "default":
|
||||
with suppress(RuntimeError):
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone_name}",
|
||||
f"--set-target={desired_target}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
set_zone_services(zone_name, zone_cfg.get("services", []))
|
||||
set_zone_interfaces(zone_name, zone_cfg.get("interfaces", []))
|
||||
|
||||
mq = zone_cfg.get("masquerade", False)
|
||||
if mq is not None:
|
||||
set_masquerade(zone_name, mq)
|
||||
|
||||
for rule_entry in zone_cfg.get("rich_rules", []):
|
||||
rule_str = (
|
||||
rule_entry.get("rule", "")
|
||||
if isinstance(rule_entry, dict)
|
||||
else str(rule_entry)
|
||||
)
|
||||
if rule_str:
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone_name}",
|
||||
f"--add-rich-rule={rule_str}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
for fp_entry in zone_cfg.get("forward_ports", []):
|
||||
if isinstance(fp_entry, str):
|
||||
fp_str = fp_entry
|
||||
else:
|
||||
parts = [f"port={fp_entry['port']}", f"proto={fp_entry['proto']}"]
|
||||
if "toaddr" in fp_entry:
|
||||
parts.append(f"toaddr={fp_entry['toaddr']}")
|
||||
if "toport" in fp_entry:
|
||||
parts.append(f"toport={fp_entry['toport']}")
|
||||
fp_str = "/".join(parts)
|
||||
run(
|
||||
[
|
||||
"firewall-cmd",
|
||||
f"--zone={zone_name}",
|
||||
f"--add-forward-port={fp_str}",
|
||||
"--permanent",
|
||||
],
|
||||
sudo=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
applied.append(zone_name)
|
||||
|
||||
_reload()
|
||||
backup_path = save_backup()
|
||||
|
||||
logger.info("Firewall config applied to %d zones", len(applied))
|
||||
|
||||
return {
|
||||
"applied_zones": applied,
|
||||
"backup": backup_path,
|
||||
}
|
||||
live_zones = state.get("zones", {})
|
||||
return _compute_pending_changes(cfg, live_zones)
|
||||
|
||||
|
||||
__all__ = [
|
||||
@@ -946,35 +341,18 @@ __all__ = [
|
||||
"DATA_DIR",
|
||||
"DEFAULT_CONFIG",
|
||||
"RULES_FILE",
|
||||
"_reload",
|
||||
"add_forward_port",
|
||||
"add_rich_rule",
|
||||
"add_zone_interface",
|
||||
"add_zone_service",
|
||||
"config_apply",
|
||||
"_compute_pending_changes",
|
||||
"_ensure_config_file",
|
||||
"_live_target_to_config",
|
||||
"_normalize_target",
|
||||
"_now_iso",
|
||||
"_parse_active_zones",
|
||||
"_parse_forward_ports",
|
||||
"_parse_interfaces",
|
||||
"_parse_zone_output",
|
||||
"config_pending",
|
||||
"create_zone",
|
||||
"delete_zone",
|
||||
"get_active_zones",
|
||||
"get_available_zones",
|
||||
"get_config",
|
||||
"get_icmp_blocks",
|
||||
"get_interfaces",
|
||||
"get_rich_rules",
|
||||
"get_services",
|
||||
"get_state",
|
||||
"get_zone_info",
|
||||
"load_backup",
|
||||
"remove_forward_port",
|
||||
"remove_forward_port_by_id",
|
||||
"remove_rich_rule",
|
||||
"remove_rich_rule_by_id",
|
||||
"remove_zone_interface",
|
||||
"remove_zone_service",
|
||||
"restore_backup",
|
||||
"save_backup",
|
||||
"save_config",
|
||||
"set_masquerade",
|
||||
"set_zone_interfaces",
|
||||
"set_zone_services",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user