dhcp: auto-populate gateway from interface IP for DHCP ranges
Add get_interface_ip() helper to resolve an interface's IPv4 address via 'ip -o addr show'. Use it to back-propagate gateway into DHCP ranges so clients receive their default route. - set_dhcp_range() resolves gateway: explicit > existing range > iface IP - DnsToFirewallSync and FirewallToDhcpSync sync ensure gateways are set - Remove automatic masquerade toggle from DnsToFirewallSync - Fix dnsmasq lease file path to /var/lib/misc/dnsmasq.leases - Rename lease state field expires_at -> expires (ISO string) - Add 'ip -o addr show' to sudo whitelist
This commit is contained in:
+95
-22
@@ -10,6 +10,8 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from lib.common import get_interface_ip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -235,7 +237,13 @@ def _safe_name(obj: object) -> str:
|
||||
|
||||
|
||||
class DnsToFirewallSync:
|
||||
"""Sync subscriber: dnsmasq config_saved → update firewall config."""
|
||||
"""Sync subscriber: dnsmasq config_saved → update firewall config.
|
||||
|
||||
Adds/removes dhcp and dns services on firewall zones based on
|
||||
which interfaces have DHCP ranges. Back-propagates: ensures
|
||||
DHCP ranges carry the gateway (interface IP) so clients get
|
||||
their default route.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def on_dnsmasq_config_saved(cls, event: SyncEvent) -> SyncResult | None:
|
||||
@@ -243,7 +251,12 @@ class DnsToFirewallSync:
|
||||
return None
|
||||
|
||||
try:
|
||||
from lib.dnsmasq import get_config as _get_dnsmasq_cfg
|
||||
from lib.dnsmasq import (
|
||||
get_config as _get_dnsmasq_cfg,
|
||||
)
|
||||
from lib.dnsmasq import (
|
||||
save_config as _save_dnsmasq_cfg,
|
||||
)
|
||||
from lib.firewall import get_config as _get_fw_cfg
|
||||
from lib.firewall import save_config as _save_fw_cfg
|
||||
|
||||
@@ -280,9 +293,10 @@ class DnsToFirewallSync:
|
||||
continue
|
||||
zone_ifaces[zname] = set(zdata.get("interfaces", []))
|
||||
|
||||
changes: list[str] = []
|
||||
firewall_changes: list[str] = []
|
||||
dnsmasq_changes: list[str] = []
|
||||
|
||||
# For active zones: ensure dhcp/dns services and masquerade
|
||||
# For active zones: ensure dhcp/dns services
|
||||
for zname, _ifaces in active_zones.items():
|
||||
zdata = zones.get(zname, {})
|
||||
if not isinstance(zdata, dict):
|
||||
@@ -295,15 +309,14 @@ class DnsToFirewallSync:
|
||||
added.append(svc)
|
||||
zdata["services"] = services
|
||||
if added:
|
||||
changes.append(
|
||||
firewall_changes.append(
|
||||
f"Added {', '.join(added)} service(s) to zone '{zname}'"
|
||||
)
|
||||
|
||||
if not zdata.get("masquerade"):
|
||||
zdata["masquerade"] = True
|
||||
changes.append(f"Enabled masquerade on zone '{zname}'")
|
||||
# Back-propagate: ensure DHCP ranges have gateway set
|
||||
cls._ensure_gateways(dnsmasq_cfg, zname, _ifaces, dnsmasq_changes)
|
||||
|
||||
# For inactive zones: remove dhcp/dns services, disable masquerade
|
||||
# For inactive zones: remove dhcp/dns services
|
||||
for zname, zdata in zones.items():
|
||||
if not isinstance(zdata, dict):
|
||||
continue
|
||||
@@ -320,26 +333,52 @@ class DnsToFirewallSync:
|
||||
removed.append(svc)
|
||||
zdata["services"] = services
|
||||
if removed:
|
||||
changes.append(
|
||||
firewall_changes.append(
|
||||
f"Removed {', '.join(removed)} service(s) from zone '{zname}'"
|
||||
)
|
||||
|
||||
if zdata.get("masquerade"):
|
||||
zdata["masquerade"] = False
|
||||
changes.append(f"Disabled masquerade on zone '{zname}'")
|
||||
result = SyncResult(changes=firewall_changes + dnsmasq_changes)
|
||||
|
||||
if changes:
|
||||
# Save firewall config if changed
|
||||
if firewall_changes:
|
||||
fw_cfg["zones"] = zones
|
||||
_save_fw_cfg(fw_cfg)
|
||||
return SyncResult(
|
||||
affected_subsystems=["firewall"],
|
||||
changes=changes,
|
||||
)
|
||||
return SyncResult(changes=changes)
|
||||
result.affected_subsystems.append("firewall")
|
||||
|
||||
# Save dnsmasq config if gateways were set
|
||||
if dnsmasq_changes:
|
||||
_save_dnsmasq_cfg(dnsmasq_cfg)
|
||||
result.affected_subsystems.append("dnsmasq")
|
||||
|
||||
return result
|
||||
except Exception:
|
||||
logger.exception("DnsToFirewallSync failed")
|
||||
return SyncResult()
|
||||
|
||||
@staticmethod
|
||||
def _ensure_gateways(
|
||||
dnsmasq_cfg: dict[str, Any],
|
||||
zone_name: str,
|
||||
zone_ifaces: set[str],
|
||||
changes: list[str],
|
||||
) -> None:
|
||||
"""Ensure DHCP ranges for *zone_ifaces* carry the gateway (interface IP)."""
|
||||
from lib.common import get_interface_ip
|
||||
|
||||
ranges = dnsmasq_cfg.get("dhcp", {}).get("ranges", [])
|
||||
for r in ranges:
|
||||
iface = r.get("interface", "")
|
||||
if iface not in zone_ifaces:
|
||||
continue
|
||||
if r.get("gateway"):
|
||||
continue
|
||||
ip = get_interface_ip(iface)
|
||||
if ip:
|
||||
r["gateway"] = ip
|
||||
changes.append(
|
||||
f"Set DHCP gateway {ip} for range on '{iface}' (zone '{zone_name}')"
|
||||
)
|
||||
|
||||
|
||||
class WgToFirewallSync:
|
||||
"""Sync subscriber: wireguard config_saved → update firewall config."""
|
||||
@@ -510,8 +549,9 @@ class FirewallToDhcpSync:
|
||||
"""Sync subscriber: firewall config_saved → sync dnsmasq DHCP ranges.
|
||||
|
||||
Removes DHCP ranges whose interface no longer belongs to any firewall
|
||||
zone. Also logs warnings for zones with dhcp service but no range
|
||||
(cannot auto-create a range without knowing IP addresses).
|
||||
zone. When masquerade is enabled on a zone, ensures DHCP ranges on
|
||||
that zone's interfaces carry the gateway (interface IP).
|
||||
Logs warnings for zones with dhcp service but no range.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@@ -570,6 +610,39 @@ class FirewallToDhcpSync:
|
||||
)
|
||||
changes.append(f"Removed stale DHCP range on interface '{iface}'")
|
||||
|
||||
# When masquerade is enabled on a zone, ensure DHCP ranges have gateway
|
||||
changed = False
|
||||
for zname, zdata in zones.items():
|
||||
if not isinstance(zdata, dict):
|
||||
continue
|
||||
if not zdata.get("masquerade"):
|
||||
continue
|
||||
# Find DHCP ranges on this zone's interfaces
|
||||
z_ifaces = set(zdata.get("interfaces", []))
|
||||
ranges = dnsmasq_cfg.get("dhcp", {}).get("ranges", [])
|
||||
for r in ranges:
|
||||
iface = r.get("interface", "")
|
||||
if not iface or iface not in z_ifaces:
|
||||
continue
|
||||
if r.get("gateway"):
|
||||
continue
|
||||
ip = get_interface_ip(iface)
|
||||
if ip:
|
||||
r["gateway"] = ip
|
||||
changed = True
|
||||
logger.info(
|
||||
"Set DHCP gateway %s on '%s' (masquerade on zone '%s')",
|
||||
ip,
|
||||
iface,
|
||||
zname,
|
||||
)
|
||||
changes.append(
|
||||
f"Set DHCP gateway {ip} for range on '{iface}' (zone '{zname}')"
|
||||
)
|
||||
|
||||
if changed:
|
||||
_save_dnsmasq_cfg(dnsmasq_cfg)
|
||||
|
||||
# Zones with dhcp service but no matching range (warn only)
|
||||
for iface in sorted(dhcp_service_ifaces - range_ifaces):
|
||||
logger.info(
|
||||
@@ -579,7 +652,7 @@ class FirewallToDhcpSync:
|
||||
changes.append(f"Zone has dhcp service on '{iface}' but no DHCP range")
|
||||
|
||||
return SyncResult(
|
||||
affected_subsystems=["dnsmasq"] if stale_ifaces else [],
|
||||
affected_subsystems=["dnsmasq"] if changed or stale_ifaces else [],
|
||||
changes=changes,
|
||||
)
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user