fix: use configured domain in CoreDNS Corefile generation

Two bugs caused DNS to fail when the domain name changes:
1. generate_corefile() hardcoded 'cell' as the zone name instead of
   using the configured domain — on startup it would silently reset any
   domain change back to 'cell'
2. apply_domain() regex replaced ALL non-dot zones (including local.cell)
   with the new domain → duplicate zone blocks → CoreDNS crash

Fix: add a domain parameter to generate_corefile/apply_all_dns_rules,
add _configured_domain() helper in app.py, and delegate Corefile updates
in apply_domain() to generate_corefile() so the logic is in one place.
Also parameterise SERVICE_HOSTS ACL entries via the domain argument.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 15:32:23 -04:00
parent e74d5e0504
commit 50671f71cb
4 changed files with 41 additions and 50 deletions
+8 -4
View File
@@ -188,11 +188,15 @@ cell_link_manager = CellLinkManager(
) )
# Apply firewall + DNS rules from stored peer settings (survives API restarts) # Apply firewall + DNS rules from stored peer settings (survives API restarts)
def _configured_domain() -> str:
return config_manager.configs.get('_identity', {}).get('domain', 'cell')
def _apply_startup_enforcement(): def _apply_startup_enforcement():
try: try:
peers = peer_registry.list_peers() peers = peer_registry.list_peers()
firewall_manager.apply_all_peer_rules(peers) firewall_manager.apply_all_peer_rules(peers)
firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH) firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH, _configured_domain())
logger.info(f"Applied enforcement rules for {len(peers)} peers on startup") logger.info(f"Applied enforcement rules for {len(peers)} peers on startup")
except Exception as e: except Exception as e:
logger.warning(f"Startup enforcement failed (non-fatal): {e}") logger.warning(f"Startup enforcement failed (non-fatal): {e}")
@@ -1387,7 +1391,7 @@ def apply_wireguard_enforcement():
try: try:
peers = peer_registry.list_peers() peers = peer_registry.list_peers()
firewall_manager.apply_all_peer_rules(peers) firewall_manager.apply_all_peer_rules(peers)
firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH) firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH, _configured_domain())
return jsonify({'ok': True, 'peers': len(peers)}) return jsonify({'ok': True, 'peers': len(peers)})
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@@ -1528,7 +1532,7 @@ def add_peer():
if success: if success:
# Apply server-side enforcement immediately # Apply server-side enforcement immediately
firewall_manager.apply_peer_rules(peer_info['ip'], peer_info) firewall_manager.apply_peer_rules(peer_info['ip'], peer_info)
firewall_manager.apply_all_dns_rules(peer_registry.list_peers(), COREFILE_PATH) firewall_manager.apply_all_dns_rules(peer_registry.list_peers(), COREFILE_PATH, _configured_domain())
return jsonify({"message": f"Peer {data['name']} added successfully", "ip": assigned_ip}), 201 return jsonify({"message": f"Peer {data['name']} added successfully", "ip": assigned_ip}), 201
else: else:
return jsonify({"error": f"Peer {data['name']} already exists"}), 400 return jsonify({"error": f"Peer {data['name']} already exists"}), 400
@@ -1564,7 +1568,7 @@ def update_peer(peer_name):
updated_peer = peer_registry.get_peer(peer_name) updated_peer = peer_registry.get_peer(peer_name)
if updated_peer: if updated_peer:
firewall_manager.apply_peer_rules(updated_peer['ip'], updated_peer) firewall_manager.apply_peer_rules(updated_peer['ip'], updated_peer)
firewall_manager.apply_all_dns_rules(peer_registry.list_peers(), COREFILE_PATH) firewall_manager.apply_all_dns_rules(peer_registry.list_peers(), COREFILE_PATH, _configured_domain())
result = {"message": f"Peer {peer_name} updated", "config_changed": config_changed} result = {"message": f"Peer {peer_name} updated", "config_changed": config_changed}
return jsonify(result) return jsonify(result)
else: else:
+21 -21
View File
@@ -212,30 +212,27 @@ def apply_all_peer_rules(peers: List[Dict[str, Any]]) -> None:
# DNS ACL (CoreDNS Corefile generation) # DNS ACL (CoreDNS Corefile generation)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Map service name → DNS hostname in .cell zone # Service subdomains that get per-peer ACL rules in the CoreDNS zone block
SERVICE_HOSTS = { _ACL_SERVICES = ('calendar', 'files', 'mail', 'webdav')
'calendar': 'calendar.cell.',
'files': 'files.cell.',
'mail': 'mail.cell.',
'webdav': 'webdav.cell.',
}
def _build_acl_block(blocked_peers_by_service: Dict[str, List[str]]) -> str: def _build_acl_block(blocked_peers_by_service: Dict[str, List[str]],
domain: str = 'cell') -> str:
""" """
Build CoreDNS ACL plugin stanzas. Build CoreDNS ACL plugin stanzas.
blocked_peers_by_service: { 'calendar': ['10.0.0.2', '10.0.0.3'], ... } blocked_peers_by_service: { 'calendar': ['10.0.0.2', '10.0.0.3'], ... }
Returns a string to embed in the `cell { }` zone block. Returns a string to embed in the primary zone block.
""" """
if not blocked_peers_by_service: if not blocked_peers_by_service:
return '' return ''
lines = [] lines = []
for service, peer_ips in blocked_peers_by_service.items(): for service in _ACL_SERVICES:
host = SERVICE_HOSTS.get(service) peer_ips = blocked_peers_by_service.get(service, [])
if not host or not peer_ips: if not peer_ips:
continue continue
host = f'{service}.{domain}.'
for ip in peer_ips: for ip in peer_ips:
lines.append(f' acl {host} {{') lines.append(f' acl {host} {{')
lines.append(f' block net {ip}/32') lines.append(f' block net {ip}/32')
@@ -245,10 +242,12 @@ def _build_acl_block(blocked_peers_by_service: Dict[str, List[str]]) -> str:
return '\n'.join(lines) return '\n'.join(lines)
def generate_corefile(peers: List[Dict[str, Any]], corefile_path: str = COREFILE_PATH) -> bool: def generate_corefile(peers: List[Dict[str, Any]], corefile_path: str = COREFILE_PATH,
domain: str = 'cell') -> bool:
""" """
Rewrite the CoreDNS Corefile with per-peer ACL rules and reload plugin. Rewrite the CoreDNS Corefile with per-peer ACL rules and reload plugin.
The file is written to corefile_path (API-side path mapped into CoreDNS container). The file is written to corefile_path (API-side path mapped into CoreDNS container).
domain: the configured cell domain (e.g. 'cell', 'dev') must match zone file names.
""" """
try: try:
# Collect which peers block which services # Collect which peers block which services
@@ -262,12 +261,12 @@ def generate_corefile(peers: List[Dict[str, Any]], corefile_path: str = COREFILE
if service not in allowed_services: if service not in allowed_services:
blocked[service].append(ip) blocked[service].append(ip)
acl_block = _build_acl_block(blocked) acl_block = _build_acl_block(blocked, domain)
cell_zone_block = 'cell {\n file /data/cell.zone\n log\n' primary_zone_block = f'{domain} {{\n file /data/{domain}.zone\n log\n'
if acl_block: if acl_block:
cell_zone_block += acl_block + '\n' primary_zone_block += acl_block + '\n'
cell_zone_block += '}\n' primary_zone_block += '}\n'
corefile = f""". {{ corefile = f""". {{
forward . 8.8.8.8 1.1.1.1 forward . 8.8.8.8 1.1.1.1
@@ -276,8 +275,8 @@ def generate_corefile(peers: List[Dict[str, Any]], corefile_path: str = COREFILE
health health
}} }}
{cell_zone_block} {primary_zone_block}
local.cell {{ local.{domain} {{
file /data/local.zone file /data/local.zone
log log
}} }}
@@ -307,9 +306,10 @@ def reload_coredns() -> bool:
return False return False
def apply_all_dns_rules(peers: List[Dict[str, Any]], corefile_path: str = COREFILE_PATH) -> bool: def apply_all_dns_rules(peers: List[Dict[str, Any]], corefile_path: str = COREFILE_PATH,
domain: str = 'cell') -> bool:
"""Regenerate Corefile and reload CoreDNS.""" """Regenerate Corefile and reload CoreDNS."""
ok = generate_corefile(peers, corefile_path) ok = generate_corefile(peers, corefile_path, domain)
if ok: if ok:
reload_coredns() reload_coredns()
return ok return ok
+9 -22
View File
@@ -401,30 +401,17 @@ class NetworkManager(BaseServiceManager):
except Exception as e: except Exception as e:
warnings.append(f"dnsmasq domain update failed: {e}") warnings.append(f"dnsmasq domain update failed: {e}")
# 2. Update Corefile: replace old primary zone block with new domain # 2. Regenerate Corefile using generate_corefile so it always stays consistent
try: try:
import firewall_manager as _fm
corefile = os.path.join(self.config_dir, 'dns', 'Corefile') corefile = os.path.join(self.config_dir, 'dns', 'Corefile')
if os.path.exists(corefile): peers_file = os.path.join(self.data_dir, 'peers.json')
with open(corefile) as f: try:
content = f.read() import json as _json
import re peers = _json.loads(open(peers_file).read()) if os.path.exists(peers_file) else []
# Replace first named zone block (not the catch-all .) with new domain except Exception:
# Matches: <word> { ... } blocks (zone names like "cell", "oldname") peers = []
def replace_zone(m): _fm.generate_corefile(peers, corefile, domain)
zone = m.group(1)
if zone == '.':
return m.group(0) # keep catch-all
# Replace zone name with new domain; update file path reference
body = m.group(2)
body = re.sub(r'file\s+/data/\S+\.zone',
f'file /data/{domain}.zone', body)
return f'{domain} {{{body}}}'
new_content = re.sub(
r'(\S+)\s*\{([^}]*)\}',
replace_zone, content, flags=re.DOTALL
)
with open(corefile, 'w') as f:
f.write(new_content)
except Exception as e: except Exception as e:
warnings.append(f"Corefile domain update failed: {e}") warnings.append(f"Corefile domain update failed: {e}")
+3 -3
View File
@@ -5,12 +5,12 @@
health health
} }
cell { dev {
file /data/cell.zone file /data/dev.zone
log log
} }
local.cell { local.dev {
file /data/local.zone file /data/local.zone
log log
} }