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:
@@ -65,6 +65,10 @@ def status_pending(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
|
||||
Returns:
|
||||
Dict with per-subsystem pending status and total change count.
|
||||
The firewall section also carries advisory `uncovered_interfaces`
|
||||
and `coverage_warnings` fields (network-config interfaces not in
|
||||
any live zone); they are never counted in `needs_apply`,
|
||||
`change_count`, or `total_changes`.
|
||||
"""
|
||||
fw = state_store.get("firewall") or {}
|
||||
pending_fw = fw.get("pending", {})
|
||||
@@ -78,10 +82,23 @@ def status_pending(_request: Any, _body: Any) -> dict[str, Any]:
|
||||
summary = fw_change_summary(zone, ctype, c)
|
||||
fw_changes.append({"summary": summary, "detail": ""})
|
||||
|
||||
uncovered = fw.get("uncovered_interfaces") or []
|
||||
coverage_warnings = (
|
||||
[
|
||||
"Interfaces not in any firewall zone: "
|
||||
f"{', '.join(uncovered)} — clients on those segments lose "
|
||||
"connectivity and DHCP"
|
||||
]
|
||||
if uncovered
|
||||
else []
|
||||
)
|
||||
|
||||
fw_result = {
|
||||
"needs_apply": fw_needs_apply,
|
||||
"change_count": len(fw_changes),
|
||||
"changes": fw_changes,
|
||||
"uncovered_interfaces": uncovered,
|
||||
"coverage_warnings": coverage_warnings,
|
||||
}
|
||||
|
||||
hash_subsystems = {
|
||||
|
||||
Reference in New Issue
Block a user