diff --git a/api/app.py b/api/app.py index 0f44975..18a0520 100644 --- a/api/app.py +++ b/api/app.py @@ -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: diff --git a/api/firewall_manager.py b/api/firewall_manager.py index 32e57d9..51d65e1 100644 --- a/api/firewall_manager.py +++ b/api/firewall_manager.py @@ -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 diff --git a/api/network_manager.py b/api/network_manager.py index 75038b0..6721ec6 100644 --- a/api/network_manager.py +++ b/api/network_manager.py @@ -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: { ... } 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}") diff --git a/config/dns/Corefile b/config/dns/Corefile index b7001b5..ad1f4c2 100644 --- a/config/dns/Corefile +++ b/config/dns/Corefile @@ -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 }