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)
def _configured_domain() -> str:
return config_manager.configs.get('_identity', {}).get('domain', 'cell')
def _apply_startup_enforcement():
try:
peers = peer_registry.list_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")
except Exception as e:
logger.warning(f"Startup enforcement failed (non-fatal): {e}")
@@ -1387,7 +1391,7 @@ def apply_wireguard_enforcement():
try:
peers = peer_registry.list_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)})
except Exception as e:
return jsonify({'error': str(e)}), 500
@@ -1528,7 +1532,7 @@ def add_peer():
if success:
# Apply server-side enforcement immediately
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
else:
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)
if 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}
return jsonify(result)
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)
# ---------------------------------------------------------------------------
# Map service name → DNS hostname in .cell zone
SERVICE_HOSTS = {
'calendar': 'calendar.cell.',
'files': 'files.cell.',
'mail': 'mail.cell.',
'webdav': 'webdav.cell.',
}
# Service subdomains that get per-peer ACL rules in the CoreDNS zone block
_ACL_SERVICES = ('calendar', 'files', 'mail', 'webdav')
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.
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:
return ''
lines = []
for service, peer_ips in blocked_peers_by_service.items():
host = SERVICE_HOSTS.get(service)
if not host or not peer_ips:
for service in _ACL_SERVICES:
peer_ips = blocked_peers_by_service.get(service, [])
if not peer_ips:
continue
host = f'{service}.{domain}.'
for ip in peer_ips:
lines.append(f' acl {host} {{')
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)
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.
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:
# 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:
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:
cell_zone_block += acl_block + '\n'
cell_zone_block += '}\n'
primary_zone_block += acl_block + '\n'
primary_zone_block += '}\n'
corefile = f""". {{
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
}}
{cell_zone_block}
local.cell {{
{primary_zone_block}
local.{domain} {{
file /data/local.zone
log
}}
@@ -307,9 +306,10 @@ def reload_coredns() -> bool:
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."""
ok = generate_corefile(peers, corefile_path)
ok = generate_corefile(peers, corefile_path, domain)
if ok:
reload_coredns()
return ok
+9 -22
View File
@@ -401,30 +401,17 @@ class NetworkManager(BaseServiceManager):
except Exception as 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:
import firewall_manager as _fm
corefile = os.path.join(self.config_dir, 'dns', 'Corefile')
if os.path.exists(corefile):
with open(corefile) as f:
content = f.read()
import re
# Replace first named zone block (not the catch-all .) with new domain
# Matches: <word> { ... } blocks (zone names like "cell", "oldname")
def replace_zone(m):
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)
peers_file = os.path.join(self.data_dir, 'peers.json')
try:
import json as _json
peers = _json.loads(open(peers_file).read()) if os.path.exists(peers_file) else []
except Exception:
peers = []
_fm.generate_corefile(peers, corefile, domain)
except Exception as e:
warnings.append(f"Corefile domain update failed: {e}")
+3 -3
View File
@@ -5,12 +5,12 @@
health
}
cell {
file /data/cell.zone
dev {
file /data/dev.zone
log
}
local.cell {
local.dev {
file /data/local.zone
log
}