diff --git a/src/apptask.js b/src/apptask.js index 20ec18881..26d1a0045 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -242,14 +242,27 @@ async function waitForDnsPropagation(app) { return; } - const ip = await sysinfo.getServerIPv4(); - const [error] = await safe(dns.waitForDnsRecord(app.location, app.domain, 'A', ip, { times: 240 })); - if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: app.location, domain: app.domain }); + const ipv4 = await sysinfo.getServerIPv4(); + const ipv6Enabled = await settings.getIPv6Config(); + const ipv6 = ipv6Enabled ? await sysinfo.getServerIPv6() : null; + + let error; + [error] = await safe(dns.waitForDnsRecord(app.location, app.domain, 'A', ipv4, { times: 240 })); + if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS A Record is not synced yet: ${error.message}`, { ipv4, subdomain: app.location, domain: app.domain }); + if (ipv6Enabled) { + [error] = await safe(dns.waitForDnsRecord(app.location, app.domain, 'AAAA', ipv6, { times: 240 })); + if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS AAAA Record is not synced yet: ${error.message}`, { ipv6, subdomain: app.location, domain: app.domain }); + } // now wait for alternateDomains and aliasDomains, if any for (const domain of app.alternateDomains.concat(app.aliasDomains)) { - const [error] = await safe(dns.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { times: 240 })); - if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: domain.subdomain, domain: domain.domain }); + [error] = await safe(dns.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ipv4, { times: 240 })); + if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS A Record is not synced yet: ${error.message}`, { ipv4, subdomain: domain.subdomain, domain: domain.domain }); + if (ipv6Enabled) { + [error] = await safe(dns.waitForDnsRecord(domain.subdomain, domain.domain, 'AAAA', ipv6, { times: 240 })); + if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS AAAA Record is not synced yet: ${error.message}`, { ipv6, subdomain: domain.subdomain, domain: domain.domain }); + } + } } diff --git a/src/cloudron.js b/src/cloudron.js index b6fcc8408..e05e17b38 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -323,12 +323,16 @@ async function setupDnsAndCert(subdomain, domain, auditSource, progressCallback) const domainObject = await domains.get(domain); const dashboardFqdn = dns.fqdn(subdomain, domainObject); - const ip = await sysinfo.getServerIPv4(); + const ipv4 = await sysinfo.getServerIPv4(); + const ipv6Enabled = await settings.getIPv6Config(); + const ipv6 = ipv6Enabled ? await sysinfo.getServerIPv6() : null; progressCallback({ message: `Updating DNS of ${dashboardFqdn}` }); - await dns.upsertDnsRecords(subdomain, domain, 'A', [ ip ]); + await dns.upsertDnsRecords(subdomain, domain, 'A', [ ipv4 ]); + if (ipv6Enabled) await dns.upsertDnsRecords(subdomain, domain, 'AAAA', [ ipv6 ]); progressCallback({ message: `Waiting for DNS of ${dashboardFqdn}` }); - await dns.waitForDnsRecord(subdomain, domain, 'A', ip, { interval: 30000, times: 50000 }); + await dns.waitForDnsRecord(subdomain, domain, 'A', ipv4, { interval: 30000, times: 50000 }); + if (ipv6Enabled) await dns.waitForDnsRecord(subdomain, domain, 'AAAA', ipv6, { interval: 30000, times: 50000 }); progressCallback({ message: `Getting certificate of ${dashboardFqdn}` }); await reverseProxy.ensureCertificate(dns.fqdn(subdomain, domainObject), domain, auditSource); } diff --git a/src/dns.js b/src/dns.js index 285a9f763..75ae0db71 100644 --- a/src/dns.js +++ b/src/dns.js @@ -34,6 +34,7 @@ const apps = require('./apps.js'), debug = require('debug')('box:dns'), dns = require('dns'), domains = require('./domains.js'), + ipaddr = require('ipaddr.js'), mail = require('./mail.js'), promiseRetry = require('./promise-retry.js'), safe = require('safetydance'), @@ -135,14 +136,22 @@ async function checkDnsRecords(location, domain) { assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof domain, 'string'); - const values = await getDnsRecords(location, domain, 'A'); + const ipv4Records = await getDnsRecords(location, domain, 'A'); + const ipv4 = await sysinfo.getServerIPv4(); - const ip = await sysinfo.getServerIPv4(); + // if empty OR exactly one record with the ip, we don't need to overwrite + if (ipv4Records.length !== 0 && (ipv4Records.length !== 1 || ipv4Records[0] !== ipv4)) return { needsOverwrite: true }; - if (values.length === 0) return { needsOverwrite: false }; // does not exist - if (values[0] === ip) return { needsOverwrite: false }; // exists but in sync + const ipv6Enabled = await settings.getIPv6Config(); + if (ipv6Enabled) { + const ipv6Records = await getDnsRecords(location, domain, 'AAAA'); + const ipv6 = await sysinfo.getServerIPv6(); - return { needsOverwrite: true }; + // if empty OR exactly one record with the ip, we don't need to overwrite + if (ipv6Records.length !== 0 && (ipv6Records.length !== 1 || ipaddr.parse(ipv6Records[0]).toRFC5952String() !== ipv6)) return { needsOverwrite: true }; + } + + return { needsOverwrite: false }; // one record exists and in sync } // note: for TXT records the values must be quoted @@ -227,12 +236,15 @@ async function registerLocations(locations, options, progressCallback) { debug(`registerLocations: Will register ${JSON.stringify(locations)} with options ${JSON.stringify(options)}`); const ipv4 = await sysinfo.getServerIPv4(); + const ipv6Enabled = await settings.getIPv6Config(); + const ipv6 = ipv6Enabled ? await sysinfo.getServerIPv6() : null; for (const location of locations) { progressCallback({ message: `Registering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` }); await promiseRetry({ times: 200, interval: 5000, debug, retry: (error) => error.retryable }, async function () { await registerLocation(location, options, 'A', ipv4); + if (ipv6Enabled) await registerLocation(location, options, 'AAAA', ipv6); }); } } @@ -252,12 +264,15 @@ async function unregisterLocations(locations, progressCallback) { assert.strictEqual(typeof progressCallback, 'function'); const ipv4 = await sysinfo.getServerIPv4(); + const ipv6Enabled = await settings.getIPv6Config(); + const ipv6 = ipv6Enabled ? await sysinfo.getServerIPv6() : null; for (const location of locations) { progressCallback({ message: `Unregistering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` }); await promiseRetry({ times: 30, interval: 5000, debug, retry: (error) => error.retryable }, async function () { await unregisterLocation(location, 'A', ipv4); + if (ipv6Enabled) await unregisterLocation(location, 'AAAA', ipv6); }); } } diff --git a/src/dns/namecheap.js b/src/dns/namecheap.js index 21efb2fc4..52d3743b3 100644 --- a/src/dns/namecheap.js +++ b/src/dns/namecheap.js @@ -37,7 +37,7 @@ function injectPrivateFields(newConfig, currentConfig) { async function getQuery(domainConfig) { assert.strictEqual(typeof domainConfig, 'object'); - const ip = await sysinfo.getServerIPv4(); + const ip = await sysinfo.getServerIPv4(); // only supports ipv4 return { ApiUser: domainConfig.username, diff --git a/src/dns/wildcard.js b/src/dns/wildcard.js index a89e4ee39..b45151514 100644 --- a/src/dns/wildcard.js +++ b/src/dns/wildcard.js @@ -15,6 +15,7 @@ const assert = require('assert'), debug = require('debug')('box:dns/manual'), dns = require('../dns.js'), safe = require('safetydance'), + settings = require('../settings.js'), sysinfo = require('../sysinfo.js'), waitForDns = require('./waitfordns.js'); @@ -75,7 +76,6 @@ async function verifyDomainConfig(domainObject) { const zoneName = domainObject.zoneName; - // Very basic check if the nameservers can be fetched const [error, nameservers] = await safe(dns.promises.resolve(zoneName, 'NS', { timeout: 5000 })); if (error && error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }); if (error || !nameservers) throw new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }); @@ -83,14 +83,22 @@ async function verifyDomainConfig(domainObject) { const location = 'cloudrontestdns'; const fqdn = dns.fqdn(location, domainObject); - const [error2, result] = await safe(dns.promises.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 })); - if (error2 && error2.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve ${fqdn}`, { field: 'nameservers' }); - if (error2 || !result) throw new BoxError(BoxError.BAD_FIELD, error2 ? error2.message : `Unable to resolve ${fqdn}`, { field: 'nameservers' }); + const [ipv4Error, ipv4Result] = await safe(dns.promises.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 })); + if (ipv4Error && ipv4Error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv4 of ${fqdn}`, { field: 'nameservers' }); + if (ipv4Error || !ipv4Result) throw new BoxError(BoxError.BAD_FIELD, ipv4Error ? ipv4Error.message : `Unable to resolve IPv4 of ${fqdn}`, { field: 'nameservers' }); - const [error3, ip] = await safe(sysinfo.getServerIPv4()); - if (error3) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to detect IP of this server: ${error3.message}`); + const ipv4 = await sysinfo.getServerIPv4(); + if (ipv4Result.length !== 1 || ipv4 !== ipv4Result[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(ipv4Result)} instead of IPv4 ${ipv4}`); - if (result.length !== 1 || ip !== result[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(result)} instead of ${ip}`); + const ipv6Enabled = await settings.getIPv6Config(); + if (ipv6Enabled) { + const [ipv6Error, ipv6Result] = await safe(dns.promises.resolve(fqdn, 'AAAA', { server: '127.0.0.1', timeout: 5000 })); + if (ipv6Error && ipv6Error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv6 of ${fqdn}`, { field: 'nameservers' }); + if (ipv6Error || !ipv6Result) throw new BoxError(BoxError.BAD_FIELD, ipv6Error ? ipv6Error.message : `Unable to resolve IPv6 of ${fqdn}`, { field: 'nameservers' }); + + const ipv6 = await sysinfo.getServerIPv6(); // both should be RFC 5952 format + if (ipv6Result.length !== 1 || ipv6 !== ipv6Result[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(ipv6Result)} instead of IPv6 ${ipv6}`); + } return {}; } diff --git a/src/dyndns.js b/src/dyndns.js index 7df7e7ddd..3cf25f872 100644 --- a/src/dyndns.js +++ b/src/dyndns.js @@ -19,30 +19,40 @@ const apps = require('./apps.js'), async function sync(auditSource) { assert.strictEqual(typeof auditSource, 'object'); - const ip = await sysinfo.getServerIPv4(); + const ipv4 = await sysinfo.getServerIPv4(); + const ipv6Enabled = await settings.getIPv6Config(); + const ipv6 = ipv6Enabled ? await sysinfo.getServerIPv6() : null; - let info = safe.JSON.parse(safe.fs.readFileSync(paths.DYNDNS_INFO_FILE, 'utf8')) || { ip: null }; - if (info.ip === ip) { - debug(`refreshDNS: no change in IP ${ip}`); + const info = safe.JSON.parse(safe.fs.readFileSync(paths.DYNDNS_INFO_FILE, 'utf8')) || { ipv4: null, ipv6: null }; + if (info.ip) { // legacy cache file + info.ipv4 = info.ip; + delete info.ip; + } + const ipv4Changed = info.ip !== ipv4; + const ipv6Changed = ipv6Enabled && info.ipv6 !== ipv6; // both should be RFC 5952 format + + if (!ipv4Changed && !ipv6Changed) { + debug(`refreshDNS: no change in IP ipv4: ${ipv4} ipv6: ${ipv6}`); return; } - debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`); - - await dns.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'A', [ ip ]); - debug('refreshDNS: updated admin location'); + debug(`refreshDNS: updating IP from ${info.ip} to ipv4: ${ipv4} (changed: ${ipv4Changed}) ipv6: ${ipv6} (changed: ${ipv6Changed})`); + if (ipv4Changed) await dns.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'A', [ ipv4 ]); + if (ipv6Changed) await dns.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'AAAA', [ ipv6 ]); const result = await apps.list(); for (const app of result) { // do not change state of installing apps since apptask will error if dns record already exists if (app.installationState !== apps.ISTATE_INSTALLED) continue; - await dns.upsertDnsRecords(app.location, app.domain, 'A', [ ip ]); + if (ipv4Changed) await dns.upsertDnsRecords(app.location, app.domain, 'A', [ ipv4 ]); + if (ipv6Changed) await dns.upsertDnsRecords(app.location, app.domain, 'AAAA', [ ipv6 ]); } debug('refreshDNS: updated apps'); - await eventlog.add(eventlog.ACTION_DYNDNS_UPDATE, auditSource, { fromIp: info.ip, toIp: ip }); - info.ip = ip; + await eventlog.add(eventlog.ACTION_DYNDNS_UPDATE, auditSource, { fromIpv4: info.ipv4, fromIpv6: info.ipv6, toIpv4: ipv4, toIpv6: ipv6 }); + info.ipv4 = ipv4; + info.ipv6 = ipv6; safe.fs.writeFileSync(paths.DYNDNS_INFO_FILE, JSON.stringify(info), 'utf8'); }