From 66500bb128affdb99b3d904dda8365b841cf7922 Mon Sep 17 00:00:00 2001
From: Dmitrii Iurco
Date: Thu, 28 May 2026 05:06:52 -0400
Subject: [PATCH] fix: use effective_domain for service links and clean up
stale DNS records
Dashboard, Email, Calendar, and Files pages were building service URLs
with the internal LAN zone name (e.g. 'cell') instead of the public
effective domain (e.g. 'pic2.pic.ngo'), and always using http:// even
in DDNS mode where HTTPS is available.
Changes:
- Dashboard/Email/Calendar/Files: read effective_domain + domain_mode
from ConfigContext; use effective_domain in non-LAN mode and https://
for all DDNS domain modes.
- Calendar: show port 443 instead of 80 in DDNS mode.
- network_manager.update_split_horizon_zone: when the primary internal
zone name is a parent of the effective DDNS domain (e.g. pic.ngo is a
parent of pic2.pic.ngo), remove stale bootstrap service records (api,
calendar, files, mail, webmail, webdav) that pollute the DNS display
and would shadow public DNS responses.
Co-Authored-By: Claude Sonnet 4.6
---
api/network_manager.py | 12 ++++++++++++
tests/test_network_manager.py | 37 +++++++++++++++++++++++++++++++++++
webui/src/pages/Calendar.jsx | 18 +++++++++--------
webui/src/pages/Dashboard.jsx | 12 +++++++-----
webui/src/pages/Email.jsx | 10 ++++++----
webui/src/pages/Files.jsx | 20 ++++++++++---------
6 files changed, 83 insertions(+), 26 deletions(-)
diff --git a/api/network_manager.py b/api/network_manager.py
index be9e2b4..8cc0904 100644
--- a/api/network_manager.py
+++ b/api/network_manager.py
@@ -182,6 +182,18 @@ class NetworkManager(BaseServiceManager):
if not ok:
logger.warning('update_split_horizon_zone: zone file write failed for %s', effective_domain)
+ # If the internal zone name happens to be a parent of the effective DDNS
+ # 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', 'calendar', 'files', 'mail', 'webmail', 'webdav'}
+ 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]
+ if len(cleaned) < len(existing):
+ self.update_dns_zone(primary_domain, cleaned)
+ logger.info('Removed stale service records from zone %s', primary_domain)
+
corefile = os.path.join(self.config_dir, 'dns', 'Corefile')
peers_data = peers or []
ok_cf = _fm.generate_corefile(
diff --git a/tests/test_network_manager.py b/tests/test_network_manager.py
index 5a66be3..804cffb 100644
--- a/tests/test_network_manager.py
+++ b/tests/test_network_manager.py
@@ -458,6 +458,43 @@ class TestUpdateSplitHorizonZone(unittest.TestCase):
calls = [str(c) for c in mock_run.call_args_list]
self.assertTrue(any('SIGUSR1' in c for c in calls))
+ @patch('subprocess.run')
+ def test_removes_stale_service_records_when_primary_is_parent(self, _mock):
+ """Stale LAN service names (api, calendar…) are removed from a parent zone."""
+ # Bootstrap a pic.ngo zone with service records (wrong internal zone name)
+ stale_records = [
+ {'name': 'pic2', 'type': 'A', 'value': '10.0.0.1'},
+ {'name': 'api', 'type': 'A', 'value': '10.0.0.1'},
+ {'name': 'calendar','type': 'A', 'value': '10.0.0.1'},
+ {'name': 'files', 'type': 'A', 'value': '10.0.0.1'},
+ ]
+ self.nm.update_dns_zone('pic.ngo', stale_records)
+
+ # update_split_horizon_zone should strip api/calendar/files from pic.ngo
+ self.nm.update_split_horizon_zone(
+ 'pic2.pic.ngo', '172.20.0.2', primary_domain='pic.ngo'
+ )
+ content = open(os.path.join(self.data_dir, 'dns', 'pic.ngo.zone')).read()
+ self.assertNotIn('calendar', content)
+ self.assertNotIn('\napi ', content)
+ self.assertNotIn('\nfiles ', content)
+ # Non-stale record (pic2 is the cell_name, not in _stale set) survives
+ # but api/calendar/files are gone
+ self.assertIn('172.20.0.2', open(
+ os.path.join(self.data_dir, 'dns', 'pic2.pic.ngo.zone')).read())
+
+ @patch('subprocess.run')
+ def test_no_stale_cleanup_when_primary_not_parent(self, _mock):
+ """When primary_domain is unrelated, no zone file is touched."""
+ stale_records = [{'name': 'calendar', 'type': 'A', 'value': '10.0.0.1'}]
+ self.nm.update_dns_zone('cell', stale_records)
+ self.nm.update_split_horizon_zone(
+ 'pic2.pic.ngo', '172.20.0.2', primary_domain='cell'
+ )
+ # cell zone is untouched
+ content = open(os.path.join(self.data_dir, 'dns', 'cell.zone')).read()
+ self.assertIn('calendar', content)
+
if __name__ == '__main__':
unittest.main()
\ No newline at end of file
diff --git a/webui/src/pages/Calendar.jsx b/webui/src/pages/Calendar.jsx
index fd1a63d..05e7299 100644
--- a/webui/src/pages/Calendar.jsx
+++ b/webui/src/pages/Calendar.jsx
@@ -31,8 +31,10 @@ function InfoRow({ label, value }) {
}
function Calendar() {
- const { domain = 'cell', service_ips = {}, service_configs = {} } = useConfig();
- const cellHost = `calendar.${domain}`;
+ const { domain = 'cell', effective_domain, domain_mode = 'lan', service_ips = {}, service_configs = {} } = useConfig();
+ const svcDomain = (domain_mode !== 'lan' && effective_domain) ? effective_domain : domain;
+ const proto = domain_mode === 'lan' ? 'http' : 'https';
+ const cellHost = `calendar.${svcDomain}`;
const calendarIp = service_ips.vip_calendar || '172.20.0.21';
const dnsIp = service_ips.dns || '172.20.0.3';
const calendarPort = service_configs.calendar?.port ?? 5232;
@@ -85,10 +87,10 @@ function Calendar() {
Use these settings in your calendar / contacts app (iOS, Android, Thunderbird, etc.)
-
-
-
-
+
+
+
+
@@ -118,7 +120,7 @@ function Calendar() {
Android (DAVx⁵ app)
- Install DAVx⁵ from Play Store / F-Droid
- - Login with URL: http://{cellHost}/
+ - Login with URL: {proto}://{cellHost}/
- Select calendars & address books to sync
@@ -126,7 +128,7 @@ function Calendar() {
Thunderbird
- Calendar → New Calendar → On the Network
- - Format: CalDAV, Location: http://{cellHost}/
+ - Format: CalDAV, Location: {proto}://{cellHost}/
diff --git a/webui/src/pages/Dashboard.jsx b/webui/src/pages/Dashboard.jsx
index f626106..9ef95cb 100644
--- a/webui/src/pages/Dashboard.jsx
+++ b/webui/src/pages/Dashboard.jsx
@@ -21,12 +21,14 @@ import { useConfig } from '../contexts/ConfigContext';
function Dashboard({ isOnline }) {
const navigate = useNavigate();
- const { domain = 'cell', cell_name = 'mycell' } = useConfig();
+ const { domain = 'cell', cell_name = 'mycell', effective_domain, domain_mode = 'lan' } = useConfig();
+ const svcDomain = (domain_mode !== 'lan' && effective_domain) ? effective_domain : domain;
+ const proto = domain_mode === 'lan' ? 'http' : 'https';
const SERVICES = [
- { name: 'Cell Home', url: `http://${cell_name}.${domain}`, desc: 'Main UI — no login needed' },
- { name: 'Calendar', url: `http://calendar.${domain}`, desc: 'Use your configured account credentials' },
- { name: 'Files', url: `http://files.${domain}`, desc: 'Use your configured account credentials' },
- { name: 'Webmail', url: `http://mail.${domain}`, desc: 'Use your configured account credentials' },
+ { name: 'Cell Home', url: domain_mode === 'lan' ? `http://${cell_name}.${domain}` : `https://${svcDomain}`, desc: 'Main UI — no login needed' },
+ { name: 'Calendar', url: `${proto}://calendar.${svcDomain}`, desc: 'Use your configured account credentials' },
+ { name: 'Files', url: `${proto}://files.${svcDomain}`, desc: 'Use your configured account credentials' },
+ { name: 'Webmail', url: `${proto}://mail.${svcDomain}`, desc: 'Use your configured account credentials' },
];
const [cellStatus, setCellStatus] = useState(null);
const [servicesStatus, setServicesStatus] = useState(null);
diff --git a/webui/src/pages/Email.jsx b/webui/src/pages/Email.jsx
index 97b978b..4fad071 100644
--- a/webui/src/pages/Email.jsx
+++ b/webui/src/pages/Email.jsx
@@ -31,8 +31,10 @@ function InfoRow({ label, value }) {
}
function Email() {
- const { domain = 'cell', service_ips = {}, service_configs = {} } = useConfig();
- const cellHost = `mail.${domain}`;
+ const { domain = 'cell', effective_domain, domain_mode = 'lan', service_ips = {}, service_configs = {} } = useConfig();
+ const svcDomain = (domain_mode !== 'lan' && effective_domain) ? effective_domain : domain;
+ const proto = domain_mode === 'lan' ? 'http' : 'https';
+ const cellHost = `mail.${svcDomain}`;
const emailCfg = service_configs.email || {};
const mailIp = service_ips.vip_mail || '172.20.0.23';
const dnsIp = service_ips.dns || '172.20.0.3';
@@ -113,8 +115,8 @@ function Email() {
Webmail
-
-
+
+
diff --git a/webui/src/pages/Files.jsx b/webui/src/pages/Files.jsx
index aaeea44..bb1999f 100644
--- a/webui/src/pages/Files.jsx
+++ b/webui/src/pages/Files.jsx
@@ -31,9 +31,11 @@ function InfoRow({ label, value }) {
}
function Files() {
- const { domain = 'cell', service_ips = {}, service_configs = {} } = useConfig();
- const filesHost = `files.${domain}`;
- const webdavHost = `webdav.${domain}`;
+ const { domain = 'cell', effective_domain, domain_mode = 'lan', service_ips = {}, service_configs = {} } = useConfig();
+ const svcDomain = (domain_mode !== 'lan' && effective_domain) ? effective_domain : domain;
+ const proto = domain_mode === 'lan' ? 'http' : 'https';
+ const filesHost = `files.${svcDomain}`;
+ const webdavHost = `webdav.${svcDomain}`;
const filesIp = service_ips.vip_files || '172.20.0.22';
const webdavIp = service_ips.vip_webdav || '172.20.0.24';
const filesCfg = service_configs.files || {};
@@ -85,7 +87,7 @@ function Files() {
Web file manager
-
+
@@ -101,7 +103,7 @@ function Files() {
WebDAV (mount as drive)
-
+
@@ -120,19 +122,19 @@ function Files() {
macOS (Finder)
-
Go → Connect to Server → http://{webdavHost}
+
Go → Connect to Server → {proto}://{webdavHost}
Windows
-
Map Network Drive → \\{webdavHost}\DavWWWRoot or use http://{webdavHost} in "Connect to a Web Site"
+
Map Network Drive → \\{webdavHost}\DavWWWRoot or use {proto}://{webdavHost} in "Connect to a Web Site"
iOS (Files app)
-
Files → ... → Connect to Server → http://{webdavHost}
+
Files → ... → Connect to Server → {proto}://{webdavHost}
Android
-
Use Solid Explorer or FX File Explorer → Add cloud → WebDAV → http://{webdavHost}
+
Use Solid Explorer or FX File Explorer → Add cloud → WebDAV → {proto}://{webdavHost}