From 08f46332b08babd0673902402d2d9d27aadea5e8 Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Sun, 7 Jun 2026 03:14:34 -0400 Subject: [PATCH] fix: add built-in service subdomains to DNS zone on startup _build_dns_records() only hardcoded 'api' and 'webui', relying on the optional service registry for the rest. Built-in services (calendar, files, mail, webdav) were never registered, so they were absent from the zone file and tests querying webdav. via CoreDNS got NXDOMAIN. Add _BUILTIN_SERVICE_SUBDOMAINS constant and include those names in every zone build. Also update _stale and apply_cell_name exclusion sets so DDNS mode correctly removes them from the parent zone. Co-Authored-By: Claude Sonnet 4.6 --- api/network_manager.py | 11 +++++++++-- tests/test_network_manager.py | 9 ++++----- tests/test_peer_dashboard_services.py | 18 +++++------------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/api/network_manager.py b/api/network_manager.py index 69d49ff..8a6d7df 100644 --- a/api/network_manager.py +++ b/api/network_manager.py @@ -203,7 +203,7 @@ class NetworkManager(BaseServiceManager): # domain (e.g. primary_domain='pic.ngo', effective_domain='pic2.pic.ngo'), # bootstrap service records like 'api', 'calendar' etc. would pollute the # zone display and shadow the public domain. Remove them. - _stale = {'api', 'webui'} | set(self._get_service_subdomains()) + _stale = {'api', 'webui'} | set(self._BUILTIN_SERVICE_SUBDOMAINS) | set(self._get_service_subdomains()) if effective_domain.endswith('.' + primary_domain): existing = self._load_dns_records(primary_domain) cleaned = [r for r in existing if r.get('name', '') not in _stale] @@ -270,6 +270,11 @@ class NetworkManager(BaseServiceManager): logger.warning('_get_service_subdomains: registry error: %s', exc) return [] + # Built-in service subdomains that are always present on a PIC instance. + # These must stay in sync with firewall_manager.SERVICE_IPS keys and the + # Caddy routes for each built-in service. + _BUILTIN_SERVICE_SUBDOMAINS = ('calendar', 'files', 'mail', 'webdav') + def _build_dns_records(self, cell_name: str, ip_range: str) -> List[Dict]: """Build the standard set of DNS A records. @@ -284,6 +289,8 @@ class NetworkManager(BaseServiceManager): {'name': 'api', 'type': 'A', 'value': wg_ip}, {'name': 'webui', 'type': 'A', 'value': wg_ip}, ] + for sub in self._BUILTIN_SERVICE_SUBDOMAINS: + records.append({'name': sub, 'type': 'A', 'value': wg_ip}) for sub in self._get_service_subdomains(): records.append({'name': sub, 'type': 'A', 'value': wg_ip}) return records @@ -614,7 +621,7 @@ class NetworkManager(BaseServiceManager): if not new_name: return {'restarted': restarted, 'warnings': warnings} # Exclude service names, wildcard, and apex from cell-hostname detection. - _service_names = {'api', 'webui'} | set(self._get_service_subdomains()) + _service_names = {'api', 'webui'} | set(self._BUILTIN_SERVICE_SUBDOMAINS) | set(self._get_service_subdomains()) _reserved = _service_names | {'@', '*'} changed = False try: diff --git a/tests/test_network_manager.py b/tests/test_network_manager.py index 8bff7d8..0c3623e 100644 --- a/tests/test_network_manager.py +++ b/tests/test_network_manager.py @@ -349,12 +349,11 @@ class TestApplyIpRange(unittest.TestCase): self.nm.apply_ip_range('10.1.2.0/24', 'pictest', 'mycell') zone_file = os.path.join(self.nm.dns_zones_dir, 'mycell.zone') content = open(zone_file).read() - # Without a registry, only the infrastructure names are generated - for host in ('pictest', 'api', 'webui'): + # Infrastructure and built-in service names are always generated + for host in ('pictest', 'api', 'webui', 'calendar', 'files', 'mail', 'webdav'): self.assertIn(host, content) - # Service records are only generated when a registry is wired - for host in ('calendar', 'files', 'mail', 'webmail', 'webdav'): - self.assertNotIn(host, content) + # Non-built-in names are only generated when a registry is wired + self.assertNotIn('webmail', content) @patch('subprocess.run') def test_same_range_updates_zone_without_error(self, _mock): diff --git a/tests/test_peer_dashboard_services.py b/tests/test_peer_dashboard_services.py index 0946d62..c03fe08 100644 --- a/tests/test_peer_dashboard_services.py +++ b/tests/test_peer_dashboard_services.py @@ -501,23 +501,15 @@ class TestDNSZoneRecords: ) def test_service_records_absent_without_registry(self): - """Without a registry, service subdomain records are not generated. - - Phase 2: service DNS records only exist when a service is installed - and the registry reports it. The hardcoded fallback is gone. - """ + """Built-in services always get DNS records; optional services require a registry.""" records = self._records() names = {r['name'] for r in records} - assert 'calendar' not in names, \ - 'calendar DNS record must not appear without a registry' - assert 'files' not in names, \ - 'files DNS record must not appear without a registry' - assert 'mail' not in names, \ - 'mail DNS record must not appear without a registry' + # Built-in services are always present + for svc in ('calendar', 'files', 'mail', 'webdav'): + assert svc in names, f'{svc} DNS record must always be generated' + # Non-built-in names are only generated when a registry is wired assert 'webmail' not in names, \ 'webmail DNS record must not appear without a registry' - assert 'webdav' not in names, \ - 'webdav DNS record must not appear without a registry' def test_service_records_present_with_registry(self): """With a registry that provides calendar/mail/files, all resolve to WG IP."""