fix: use effective_domain for service links and clean up stale DNS records
Unit Tests / test (push) Successful in 11m32s
Unit Tests / test (push) Successful in 11m32s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -182,6 +182,18 @@ class NetworkManager(BaseServiceManager):
|
|||||||
if not ok:
|
if not ok:
|
||||||
logger.warning('update_split_horizon_zone: zone file write failed for %s', effective_domain)
|
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')
|
corefile = os.path.join(self.config_dir, 'dns', 'Corefile')
|
||||||
peers_data = peers or []
|
peers_data = peers or []
|
||||||
ok_cf = _fm.generate_corefile(
|
ok_cf = _fm.generate_corefile(
|
||||||
|
|||||||
@@ -458,6 +458,43 @@ class TestUpdateSplitHorizonZone(unittest.TestCase):
|
|||||||
calls = [str(c) for c in mock_run.call_args_list]
|
calls = [str(c) for c in mock_run.call_args_list]
|
||||||
self.assertTrue(any('SIGUSR1' in c for c in calls))
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -31,8 +31,10 @@ function InfoRow({ label, value }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Calendar() {
|
function Calendar() {
|
||||||
const { domain = 'cell', service_ips = {}, service_configs = {} } = useConfig();
|
const { domain = 'cell', effective_domain, domain_mode = 'lan', service_ips = {}, service_configs = {} } = useConfig();
|
||||||
const cellHost = `calendar.${domain}`;
|
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 calendarIp = service_ips.vip_calendar || '172.20.0.21';
|
||||||
const dnsIp = service_ips.dns || '172.20.0.3';
|
const dnsIp = service_ips.dns || '172.20.0.3';
|
||||||
const calendarPort = service_configs.calendar?.port ?? 5232;
|
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.)
|
Use these settings in your calendar / contacts app (iOS, Android, Thunderbird, etc.)
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
||||||
<InfoRow label="Server URL" value={`http://${cellHost}`} />
|
<InfoRow label="Server URL" value={`${proto}://${cellHost}`} />
|
||||||
<InfoRow label="CalDAV path" value={`http://${cellHost}/`} />
|
<InfoRow label="CalDAV path" value={`${proto}://${cellHost}/`} />
|
||||||
<InfoRow label="CardDAV path" value={`http://${cellHost}/`} />
|
<InfoRow label="CardDAV path" value={`${proto}://${cellHost}/`} />
|
||||||
<InfoRow label="Port" value="80" />
|
<InfoRow label="Port" value={domain_mode === 'lan' ? '80' : '443'} />
|
||||||
<InfoRow label="Direct IP" value={calendarIp} />
|
<InfoRow label="Direct IP" value={calendarIp} />
|
||||||
<InfoRow label="Direct port" value={String(calendarPort)} />
|
<InfoRow label="Direct port" value={String(calendarPort)} />
|
||||||
<InfoRow label="Protocol" value="HTTP (CalDAV/CardDAV)" />
|
<InfoRow label="Protocol" value="HTTP (CalDAV/CardDAV)" />
|
||||||
@@ -118,7 +120,7 @@ function Calendar() {
|
|||||||
<p className="font-medium text-gray-900 mb-1">Android (DAVx⁵ app)</p>
|
<p className="font-medium text-gray-900 mb-1">Android (DAVx⁵ app)</p>
|
||||||
<ol className="list-decimal ml-4 space-y-0.5 text-xs text-gray-600">
|
<ol className="list-decimal ml-4 space-y-0.5 text-xs text-gray-600">
|
||||||
<li>Install DAVx⁵ from Play Store / F-Droid</li>
|
<li>Install DAVx⁵ from Play Store / F-Droid</li>
|
||||||
<li>Login with URL: <span className="font-mono">http://{cellHost}/</span></li>
|
<li>Login with URL: <span className="font-mono">{proto}://{cellHost}/</span></li>
|
||||||
<li>Select calendars & address books to sync</li>
|
<li>Select calendars & address books to sync</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +128,7 @@ function Calendar() {
|
|||||||
<p className="font-medium text-gray-900 mb-1">Thunderbird</p>
|
<p className="font-medium text-gray-900 mb-1">Thunderbird</p>
|
||||||
<ol className="list-decimal ml-4 space-y-0.5 text-xs text-gray-600">
|
<ol className="list-decimal ml-4 space-y-0.5 text-xs text-gray-600">
|
||||||
<li>Calendar → New Calendar → On the Network</li>
|
<li>Calendar → New Calendar → On the Network</li>
|
||||||
<li>Format: CalDAV, Location: <span className="font-mono">http://{cellHost}/</span></li>
|
<li>Format: CalDAV, Location: <span className="font-mono">{proto}://{cellHost}/</span></li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ import { useConfig } from '../contexts/ConfigContext';
|
|||||||
|
|
||||||
function Dashboard({ isOnline }) {
|
function Dashboard({ isOnline }) {
|
||||||
const navigate = useNavigate();
|
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 = [
|
const SERVICES = [
|
||||||
{ name: 'Cell Home', url: `http://${cell_name}.${domain}`, desc: 'Main UI — no login needed' },
|
{ name: 'Cell Home', url: domain_mode === 'lan' ? `http://${cell_name}.${domain}` : `https://${svcDomain}`, desc: 'Main UI — no login needed' },
|
||||||
{ name: 'Calendar', url: `http://calendar.${domain}`, desc: 'Use your configured account credentials' },
|
{ name: 'Calendar', url: `${proto}://calendar.${svcDomain}`, desc: 'Use your configured account credentials' },
|
||||||
{ name: 'Files', url: `http://files.${domain}`, desc: 'Use your configured account credentials' },
|
{ name: 'Files', url: `${proto}://files.${svcDomain}`, desc: 'Use your configured account credentials' },
|
||||||
{ name: 'Webmail', url: `http://mail.${domain}`, desc: 'Use your configured account credentials' },
|
{ name: 'Webmail', url: `${proto}://mail.${svcDomain}`, desc: 'Use your configured account credentials' },
|
||||||
];
|
];
|
||||||
const [cellStatus, setCellStatus] = useState(null);
|
const [cellStatus, setCellStatus] = useState(null);
|
||||||
const [servicesStatus, setServicesStatus] = useState(null);
|
const [servicesStatus, setServicesStatus] = useState(null);
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ function InfoRow({ label, value }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Email() {
|
function Email() {
|
||||||
const { domain = 'cell', service_ips = {}, service_configs = {} } = useConfig();
|
const { domain = 'cell', effective_domain, domain_mode = 'lan', service_ips = {}, service_configs = {} } = useConfig();
|
||||||
const cellHost = `mail.${domain}`;
|
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 emailCfg = service_configs.email || {};
|
||||||
const mailIp = service_ips.vip_mail || '172.20.0.23';
|
const mailIp = service_ips.vip_mail || '172.20.0.23';
|
||||||
const dnsIp = service_ips.dns || '172.20.0.3';
|
const dnsIp = service_ips.dns || '172.20.0.3';
|
||||||
@@ -113,8 +115,8 @@ function Email() {
|
|||||||
<h3 className="text-lg font-medium text-gray-900">Webmail</h3>
|
<h3 className="text-lg font-medium text-gray-900">Webmail</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
||||||
<InfoRow label="URL" value={`http://mail.${domain}`} />
|
<InfoRow label="URL" value={`${proto}://mail.${svcDomain}`} />
|
||||||
<InfoRow label="Alt URL" value={`http://webmail.${domain}`} />
|
<InfoRow label="Alt URL" value={`${proto}://webmail.${svcDomain}`} />
|
||||||
<InfoRow label="Direct IP" value={`http://${mailIp}`} />
|
<InfoRow label="Direct IP" value={`http://${mailIp}`} />
|
||||||
<InfoRow label="Direct port" value={String(webmailPort)} />
|
<InfoRow label="Direct port" value={String(webmailPort)} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ function InfoRow({ label, value }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Files() {
|
function Files() {
|
||||||
const { domain = 'cell', service_ips = {}, service_configs = {} } = useConfig();
|
const { domain = 'cell', effective_domain, domain_mode = 'lan', service_ips = {}, service_configs = {} } = useConfig();
|
||||||
const filesHost = `files.${domain}`;
|
const svcDomain = (domain_mode !== 'lan' && effective_domain) ? effective_domain : domain;
|
||||||
const webdavHost = `webdav.${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 filesIp = service_ips.vip_files || '172.20.0.22';
|
||||||
const webdavIp = service_ips.vip_webdav || '172.20.0.24';
|
const webdavIp = service_ips.vip_webdav || '172.20.0.24';
|
||||||
const filesCfg = service_configs.files || {};
|
const filesCfg = service_configs.files || {};
|
||||||
@@ -85,7 +87,7 @@ function Files() {
|
|||||||
<h3 className="text-lg font-medium text-gray-900">Web file manager</h3>
|
<h3 className="text-lg font-medium text-gray-900">Web file manager</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
||||||
<InfoRow label="URL" value={`http://${filesHost}`} />
|
<InfoRow label="URL" value={`${proto}://${filesHost}`} />
|
||||||
<InfoRow label="Direct IP" value={`http://${filesIp}`} />
|
<InfoRow label="Direct IP" value={`http://${filesIp}`} />
|
||||||
<InfoRow label="Direct port" value={String(filegatorPort)} />
|
<InfoRow label="Direct port" value={String(filegatorPort)} />
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +103,7 @@ function Files() {
|
|||||||
<h3 className="text-lg font-medium text-gray-900">WebDAV (mount as drive)</h3>
|
<h3 className="text-lg font-medium text-gray-900">WebDAV (mount as drive)</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
<div className="bg-gray-50 rounded-lg px-4 py-2">
|
||||||
<InfoRow label="URL" value={`http://${webdavHost}`} />
|
<InfoRow label="URL" value={`${proto}://${webdavHost}`} />
|
||||||
<InfoRow label="Direct IP" value={`http://${webdavIp}`} />
|
<InfoRow label="Direct IP" value={`http://${webdavIp}`} />
|
||||||
<InfoRow label="Direct port" value={String(webdavPort)} />
|
<InfoRow label="Direct port" value={String(webdavPort)} />
|
||||||
<InfoRow label="Auth" value="Basic (user / password)" />
|
<InfoRow label="Auth" value="Basic (user / password)" />
|
||||||
@@ -120,19 +122,19 @@ function Files() {
|
|||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900 mb-1">macOS (Finder)</p>
|
<p className="font-medium text-gray-900 mb-1">macOS (Finder)</p>
|
||||||
<p className="text-xs text-gray-600">Go → Connect to Server → <span className="font-mono">http://{webdavHost}</span></p>
|
<p className="text-xs text-gray-600">Go → Connect to Server → <span className="font-mono">{proto}://{webdavHost}</span></p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900 mb-1">Windows</p>
|
<p className="font-medium text-gray-900 mb-1">Windows</p>
|
||||||
<p className="text-xs text-gray-600">Map Network Drive → <span className="font-mono">\\{webdavHost}\DavWWWRoot</span> or use <span className="font-mono">http://{webdavHost}</span> in "Connect to a Web Site"</p>
|
<p className="text-xs text-gray-600">Map Network Drive → <span className="font-mono">\\{webdavHost}\DavWWWRoot</span> or use <span className="font-mono">{proto}://{webdavHost}</span> in "Connect to a Web Site"</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900 mb-1">iOS (Files app)</p>
|
<p className="font-medium text-gray-900 mb-1">iOS (Files app)</p>
|
||||||
<p className="text-xs text-gray-600">Files → ... → Connect to Server → <span className="font-mono">http://{webdavHost}</span></p>
|
<p className="text-xs text-gray-600">Files → ... → Connect to Server → <span className="font-mono">{proto}://{webdavHost}</span></p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900 mb-1">Android</p>
|
<p className="font-medium text-gray-900 mb-1">Android</p>
|
||||||
<p className="text-xs text-gray-600">Use <strong>Solid Explorer</strong> or <strong>FX File Explorer</strong> → Add cloud → WebDAV → <span className="font-mono">http://{webdavHost}</span></p>
|
<p className="text-xs text-gray-600">Use <strong>Solid Explorer</strong> or <strong>FX File Explorer</strong> → Add cloud → WebDAV → <span className="font-mono">{proto}://{webdavHost}</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user