firewall: interface-coverage apply guard, target drift, non-destructive DHCP sync
Post-DHCP-incident hardening per HARDEN.md.
- apply guard: refuse (ConflictError, `force` overrides) when a
network-managed interface would end up in no zone; absent
`interfaces` key = hands-off, explicit `[]` = unassign-all
- surface `uncovered_interfaces` in firewall state (lo/wg* filtered)
+ advisory in /api/status/pending; zones.js banner + interfaces-picker
last-zone confirm
- target drift (Option A): absent or default-normalizing target is
unmanaged: not diffed, never re-set by apply; create_zone runs
--new-zone first and sets non-default targets only; importer omits
the target key for default zones
- FirewallToDhcpSync keeps stale DHCP ranges and flags them instead of
deleting; `dnsmasq` affected only on a real gateway mutation
- real pre-apply recovery snapshot in data/firewall/rules.json
({timestamp, default_zone, zones, config}); drop the empty post-apply
skeleton
- daemon shutdown: bounded grace for in-flight tasks + suppressed
teardown exception noise on SIGTERM
- also carries the firewall service-descriptions feature
(get_service_descriptions + service_descriptions state field + UI)
- tests + docs across firewall/status/state/sync/schema; ruff clean,
867 passing
This commit is contained in:
@@ -26,10 +26,12 @@ from lib.common import (
|
||||
from lib.firewall import (
|
||||
_parse_active_zones,
|
||||
_parse_all_zones_output,
|
||||
get_service_descriptions,
|
||||
)
|
||||
from lib.firewall import (
|
||||
config_pending as _config_pending,
|
||||
)
|
||||
from lib.network import get_config as _network_get_config
|
||||
from lib.network import parse_networkctl_status
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -538,11 +540,29 @@ def _collect_firewall() -> schema.FirewallState:
|
||||
with contextlib.suppress(Exception):
|
||||
pending = _config_pending(full_state)
|
||||
|
||||
net_cfg: dict[str, Any] = {}
|
||||
with contextlib.suppress(Exception):
|
||||
net_cfg = _network_get_config()
|
||||
covered: set[str] = set()
|
||||
for zone_ifaces in active.values():
|
||||
covered.update(zone_ifaces)
|
||||
for zone in zones.values():
|
||||
covered.update(zone.get("interfaces", []))
|
||||
uncovered_interfaces = [
|
||||
name
|
||||
for name in net_cfg.get("interfaces", {})
|
||||
if name != "lo" and not name.startswith("wg") and name not in covered
|
||||
]
|
||||
|
||||
return {
|
||||
"active_zones": active,
|
||||
"default_zone": default_zone,
|
||||
"interfaces": ifaces,
|
||||
"available_services": services,
|
||||
# Parsed from the firewalld service XML definitions; cached per
|
||||
# process so the 30s poll does not re-read the files.
|
||||
"service_descriptions": get_service_descriptions(),
|
||||
"uncovered_interfaces": uncovered_interfaces,
|
||||
"zones": zones,
|
||||
"rich_rules": {n: z.get("rich-rules", []) for n, z in zones.items()},
|
||||
"config": config_data,
|
||||
|
||||
Reference in New Issue
Block a user