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:
+8
-4
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user