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:
@@ -143,6 +143,30 @@ class TestCollectAll:
|
||||
assert vlan_iface["ips"], "VLAN interface should have collected IPs"
|
||||
assert "10.0.0.1/24" in vlan_iface["ips"]
|
||||
|
||||
@patch("lib.state.get_service_descriptions")
|
||||
@patch("lib.state.run")
|
||||
def test_collect_firewall_includes_service_descriptions(self, mock_run, mock_desc):
|
||||
from lib.state import _collect_firewall
|
||||
|
||||
def run_side(args, **kwargs):
|
||||
if "--get-active-zones" in args:
|
||||
return ""
|
||||
if "--get-default-zone" in args:
|
||||
return "public\n"
|
||||
if "--get-services" in args:
|
||||
return "ssh http"
|
||||
if "ip" in args[0]:
|
||||
return ""
|
||||
if "--list-all-zones" in args:
|
||||
return ""
|
||||
|
||||
mock_run.side_effect = run_side
|
||||
descs = {"ssh": "OpenSSH", "http": "WWW"}
|
||||
mock_desc.return_value = descs
|
||||
result = _collect_firewall()
|
||||
mock_desc.assert_called_once_with()
|
||||
assert result["service_descriptions"] == descs
|
||||
|
||||
@patch("lib.state.run_proc")
|
||||
def test_collect_dnsmasq_returns_dict(self, mock_proc):
|
||||
from unittest.mock import Mock
|
||||
|
||||
Reference in New Issue
Block a user