diff --git a/src/apptask.js b/src/apptask.js index 6395e9f48..20ec18881 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -242,7 +242,7 @@ async function waitForDnsPropagation(app) { return; } - const ip = await sysinfo.getServerIp(); + 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 }); diff --git a/src/cloudron.js b/src/cloudron.js index 51530ced0..b6fcc8408 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -323,7 +323,7 @@ async function setupDnsAndCert(subdomain, domain, auditSource, progressCallback) const domainObject = await domains.get(domain); const dashboardFqdn = dns.fqdn(subdomain, domainObject); - const ip = await sysinfo.getServerIp(); + const ip = await sysinfo.getServerIPv4(); progressCallback({ message: `Updating DNS of ${dashboardFqdn}` }); await dns.upsertDnsRecords(subdomain, domain, 'A', [ ip ]); diff --git a/src/dns.js b/src/dns.js index 3d4160bf0..8ac13e0a7 100644 --- a/src/dns.js +++ b/src/dns.js @@ -137,7 +137,7 @@ async function checkDnsRecords(location, domain) { const values = await getDnsRecords(location, domain, 'A'); - const ip = await sysinfo.getServerIp(); + const ip = await sysinfo.getServerIPv4(); if (values.length === 0) return { needsOverwrite: false }; // does not exist if (values[0] === ip) return { needsOverwrite: false }; // exists but in sync @@ -204,7 +204,7 @@ async function registerLocations(locations, options, progressCallback) { const overwriteDns = options.overwriteDns || false; - const ip = await sysinfo.getServerIp(); + const ip = await sysinfo.getServerIPv4(); for (const location of locations) { const error = await promiseRetry({ times: 200, interval: 5000, debug }, async function () { // returns error to abort the "retry" @@ -240,7 +240,7 @@ async function unregisterLocations(locations, progressCallback) { assert(Array.isArray(locations)); assert.strictEqual(typeof progressCallback, 'function'); - const ip = await sysinfo.getServerIp(); + const ip = await sysinfo.getServerIPv4(); for (const location of locations) { const error = await promiseRetry({ times: 30, interval: 5000, debug }, async function () { // returns error to abort the "retry" diff --git a/src/dns/namecheap.js b/src/dns/namecheap.js index 8109270de..3fcac221b 100644 --- a/src/dns/namecheap.js +++ b/src/dns/namecheap.js @@ -37,7 +37,7 @@ function injectPrivateFields(newConfig, currentConfig) { async function getQuery(dnsConfig) { assert.strictEqual(typeof dnsConfig, 'object'); - const ip = await sysinfo.getServerIp(); + const ip = await sysinfo.getServerIPv4(); return { ApiUser: dnsConfig.username, diff --git a/src/dns/wildcard.js b/src/dns/wildcard.js index 4a9d5163b..847fc3873 100644 --- a/src/dns/wildcard.js +++ b/src/dns/wildcard.js @@ -87,7 +87,7 @@ async function verifyDnsConfig(domainObject) { 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 [error3, ip] = await safe(sysinfo.getServerIp()); + const [error3, ip] = await safe(sysinfo.getServerIPv4()); if (error3) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to detect IP of this server: ${error3.message}`); if (result.length !== 1 || ip !== result[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(result)} instead of ${ip}`); diff --git a/src/dyndns.js b/src/dyndns.js index 56a09ba03..7df7e7ddd 100644 --- a/src/dyndns.js +++ b/src/dyndns.js @@ -19,7 +19,7 @@ const apps = require('./apps.js'), async function sync(auditSource) { assert.strictEqual(typeof auditSource, 'object'); - const ip = await sysinfo.getServerIp(); + const ip = await sysinfo.getServerIPv4(); let info = safe.JSON.parse(safe.fs.readFileSync(paths.DYNDNS_INFO_FILE, 'utf8')) || { ip: null }; if (info.ip === ip) { diff --git a/src/mail.js b/src/mail.js index a0aea8487..22a66171c 100644 --- a/src/mail.js +++ b/src/mail.js @@ -356,7 +356,7 @@ async function checkMx(domain, mailFqdn) { const [error2, mxIps] = await safe(dns.promises.resolve(mxRecords[0].exchange, 'A', DNS_OPTIONS)); if (error2 || mxIps.length !== 1) return mx; - const [error3, ip] = await safe(sysinfo.getServerIp()); + const [error3, ip] = await safe(sysinfo.getServerIPv4()); if (error3) return mx; mx.status = mxIps[0] === ip; @@ -414,7 +414,7 @@ async function checkPtr(mailFqdn) { errorMessage: '' }; - const [error, ip] = await safe(sysinfo.getServerIp()); + const [error, ip] = await safe(sysinfo.getServerIPv4()); if (error) { ptr.errorMessage = error.message; return ptr; @@ -499,7 +499,7 @@ const RBL_LIST = [ // this function currently only looks for black lists based on IP. TODO: also look up by domain async function checkRblStatus(domain) { - const ip = await sysinfo.getServerIp(); + const ip = await sysinfo.getServerIPv4(); const flippedIp = ip.split('.').reverse().join('.'); diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index c69cf1e91..178f4ce50 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -294,7 +294,7 @@ async function syncExternalLdap(req, res, next) { } async function getServerIp(req, res, next) { - const [error, ip] = await safe(sysinfo.getServerIp()); + const [error, ip] = await safe(sysinfo.getServerIPv4()); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, { ip })); diff --git a/src/sysinfo.js b/src/sysinfo.js index cc9fa77da..bd983148a 100644 --- a/src/sysinfo.js +++ b/src/sysinfo.js @@ -1,7 +1,8 @@ 'use strict'; exports = module.exports = { - getServerIp, + getServerIPv4, + getServerIPv6, testConfig, hasIPv6 @@ -21,10 +22,16 @@ function api(provider) { } } -async function getServerIp() { +async function getServerIPv4() { const config = await settings.getSysinfoConfig(); - return await api(config.provider).getServerIp(config); + return await api(config.provider).getServerIPv4(config); +} + +async function getServerIPv6() { + const config = await settings.getSysinfoConfig(); + + return await api(config.provider).getServerIPv4(config); } function hasIPv6() { diff --git a/src/sysinfo/fixed.js b/src/sysinfo/fixed.js index c5c74836c..7dedee0a2 100644 --- a/src/sysinfo/fixed.js +++ b/src/sysinfo/fixed.js @@ -1,25 +1,37 @@ 'use strict'; exports = module.exports = { - getServerIp, + getServerIPv4, + getServerIPv6, testConfig }; const assert = require('assert'), BoxError = require('../boxerror.js'), - validator = require('validator'); + net = require('net'); -async function getServerIp(config) { +async function getServerIPv4(config) { assert.strictEqual(typeof config, 'object'); return config.ip; } +async function getServerIPv6(config) { + assert.strictEqual(typeof config, 'object'); + + return config.ipv6; +} + async function testConfig(config) { assert.strictEqual(typeof config, 'object'); - if (typeof config.ip !== 'string') return new BoxError(BoxError.BAD_FIELD, 'ip must be a string'); - if (!validator.isIP(config.ip, 4)) return new BoxError(BoxError.BAD_FIELD, 'ip is not a valid ipv4'); + if (typeof config.ip !== 'string') return new BoxError(BoxError.BAD_FIELD, 'ipv4 must be a string'); + if (!net.isIPv4(config.ip)) return new BoxError(BoxError.BAD_FIELD, 'ip is not a valid ipv4'); + + if ('ipv6' in config) { + if (typeof config.ipv6 !== 'string') return new BoxError(BoxError.BAD_FIELD, 'ipv6 must be a string'); + if (!net.isIPv6(config.ipv6)) return new BoxError(BoxError.BAD_FIELD, 'ipv6 is not a valid ipv6'); + } return null; } diff --git a/src/sysinfo/generic.js b/src/sysinfo/generic.js index b0a9231bd..92281912a 100644 --- a/src/sysinfo/generic.js +++ b/src/sysinfo/generic.js @@ -1,7 +1,8 @@ 'use strict'; exports = module.exports = { - getServerIp, + getServerIPv4, + getServerIPv6, testConfig }; @@ -12,25 +13,51 @@ const assert = require('assert'), safe = require('safetydance'), superagent = require('superagent'); -async function getServerIp(config) { +async function getServerIPv4(config) { assert.strictEqual(typeof config, 'object'); if (process.env.BOX_ENV === 'test') return '127.0.0.1'; return await promiseRetry({ times: 10, interval: 5000, debug }, async () => { - debug('getServerIp: getting server IP'); + debug('getServerIPv4: getting server IP'); const [networkError, response] = await safe(superagent.get('https://ipv4.api.cloudron.io/api/v1/helper/public_ip') .timeout(30 * 1000) .ok(() => true)); if (networkError || response.status !== 200) { - debug('Error getting IP', networkError); + debug('getServerIPv4: Error getting IP', networkError); throw new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to detect IP. API server unreachable'); } if (!response.body && !response.body.ip) { - debug('Unexpected answer. No "ip" found in response body.', response.body); + debug('getServerIPv4: Unexpected answer. No "ip" found in response body.', response.body); + throw new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to detect IP. No IP found in response'); + } + + return response.body.ip; + }); +} + +async function getServerIPv6(config) { + assert.strictEqual(typeof config, 'object'); + + if (process.env.BOX_ENV === 'test') return '127.0.0.1'; + + return await promiseRetry({ times: 10, interval: 5000, debug }, async () => { + debug('getServerIPv6: getting server IP'); + + const [networkError, response] = await safe(superagent.get('https://ipv6.api.cloudron.io/api/v1/helper/public_ip') + .timeout(30 * 1000) + .ok(() => true)); + + if (networkError || response.status !== 200) { + debug('getServerIPv6: Error getting IP', networkError); + throw new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to detect IP. API server unreachable'); + } + + if (!response.body && !response.body.ip) { + debug('getServerIPv6: Unexpected answer. No "ip" found in response body.', response.body); throw new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to detect IP. No IP found in response'); } diff --git a/src/sysinfo/interface.js b/src/sysinfo/interface.js index f4d51cadb..1d02c52d9 100644 --- a/src/sysinfo/interface.js +++ b/src/sysinfo/interface.js @@ -7,17 +7,24 @@ // ------------------------------------------- exports = module.exports = { - getServerIp, + getServerIPv4, + getServerIPv6, testConfig }; -var assert = require('assert'), +const assert = require('assert'), BoxError = require('../boxerror.js'); -async function getServerIp(config) { +async function getServerIPv4(config) { assert.strictEqual(typeof config, 'object'); - throw new BoxError(BoxError.NOT_IMPLEMENTED, 'testConfig is not implemented'); + throw new BoxError(BoxError.NOT_IMPLEMENTED, 'getServerIPv4 is not implemented'); +} + +async function getServerIPv6(config) { + assert.strictEqual(typeof config, 'object'); + + throw new BoxError(BoxError.NOT_IMPLEMENTED, 'getServerIPv6 is not implemented'); } async function testConfig(config) { diff --git a/src/sysinfo/network-interface.js b/src/sysinfo/network-interface.js index 12372a71f..a5ea428ff 100644 --- a/src/sysinfo/network-interface.js +++ b/src/sysinfo/network-interface.js @@ -1,7 +1,8 @@ 'use strict'; exports = module.exports = { - getServerIp, + getServerIPv4, + getServerIPv6, testConfig }; @@ -11,7 +12,7 @@ const assert = require('assert'), os = require('os'), safe = require('safetydance'); -async function getServerIp(config) { +async function getServerIPv4(config) { assert.strictEqual(typeof config, 'object'); const ifaces = os.networkInterfaces(); @@ -25,12 +26,26 @@ async function getServerIp(config) { return addresses[0]; } +async function getServerIPv6(config) { + assert.strictEqual(typeof config, 'object'); + + const ifaces = os.networkInterfaces(); + const iface = ifaces[config.ifname]; // array of addresses + if (!iface) throw new BoxError(BoxError.NETWORK_ERROR, `No interface named ${config.ifname}`); + + const addresses = iface.filter(i => i.family === 'IPv6').map(i => i.address); + if (addresses.length === 0) throw new BoxError(BoxError.NETWORK_ERROR, `${config.ifname} does not have any IPv4 address`); + if (addresses.length > 1) debug(`${config.ifname} has multiple ipv6 - ${JSON.stringify(addresses)}. choosing the first one.`); + + return addresses[0]; +} + async function testConfig(config) { assert.strictEqual(typeof config, 'object'); assert.strictEqual(typeof callback, 'function'); if (typeof config.ifname !== 'string') return new BoxError(BoxError.BAD_FIELD, 'ifname is not a string'); - const [error] = await safe(getServerIp(config)); + const [error] = await safe(getServerIPv4(config)); return error || null; }