'use strict'; exports = module.exports = waitForDns; const assert = require('node:assert'), BoxError = require('../boxerror.js'), debug = require('debug')('box:dns/waitfordns'), dig = require('../dig.js'), dns = require('node:dns'), promiseRetry = require('../promise-retry.js'), safe = require('safetydance'), _ = require('../underscore.js'); async function resolveIp(hostname, type, options) { assert.strictEqual(typeof hostname, 'string'); assert(type === 'A' || type === 'AAAA'); assert.strictEqual(typeof options, 'object'); // try A record at authoritative server debug(`resolveIp: Checking ${type} for ${hostname} at ${options.server}`); const [error, results] = await safe(dig.resolve(hostname, type, options)); if (!error && results.length !== 0) return results; // try CNAME record at authoritative server debug(`resolveIp: No ${type}. Checking CNAME for ${hostname} at ${options.server}`); const cnameResults = await dig.resolve(hostname, 'CNAME', options); if (cnameResults.length === 0) return cnameResults; // recurse lookup the CNAME record debug(`resolveIp: found CNAME for ${hostname}. resolving ${cnameResults[0]}`); return await dig.resolve(cnameResults[0], type, _.omit(options, ['server'])); } async function isChangeSynced(hostname, type, value, nameserver) { assert.strictEqual(typeof hostname, 'string'); assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof value, 'string'); assert.strictEqual(typeof nameserver, 'string'); // ns records cannot have cname const [error4, nsIPv4s] = await safe(dig.resolve(nameserver, 'A', { timeout: 5000 })); const [error6, nsIPv6s] = await safe(dig.resolve(nameserver, 'AAAA', { timeout: 5000 })); if (error4 && error6) { debug(`isChangeSynced: cannot resolve NS ${nameserver}`); // NS doesn't resolve at all; it's fine return true; } const nsIPs = [].concat(nsIPv4s || []).concat(nsIPv6s || []); const status = []; for (let i = 0; i < nsIPs.length; i++) { const nsIp = nsIPs[i]; const resolveOptions = { server: nsIp, timeout: 5000, tries: 1 }; const resolver = type === 'A' || type === 'AAAA' ? resolveIp(hostname, type, resolveOptions) : dig.resolve(hostname, 'TXT', resolveOptions); const [error, answer] = await safe(resolver); // CONNREFUSED - when there is no ipv4/ipv6 connectivity. REFUSED - server won't answer maybe by policy if (error && (error.code === dns.TIMEOUT || error.code === dns.REFUSED || error.code === dns.CONNREFUSED)) { debug(`isChangeSynced: NS ${nameserver} (${nsIp}) not resolving ${hostname} (${type}): ${error}. Ignoring`); status[i] = true; // should be ok if dns server is down continue; } if (error) { debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${hostname} (${type}): ${error}`); status[i] = false; continue; } let match; if (type === 'A' || type === 'AAAA') { match = answer.length === 1 && answer[0] === value; } else if (type === 'TXT') { // answer is a 2d array of strings match = answer.some(function (a) { return value === a.join(''); }); } debug(`isChangeSynced: ${hostname} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`); status[i] = match; } return status.every(s => s === true); } // check if IP change has propagated to every nameserver async function waitForDns(hostname, zoneName, type, value, options) { assert.strictEqual(typeof hostname, 'string'); assert.strictEqual(typeof zoneName, 'string'); assert(type === 'A' || type === 'AAAA' || type === 'TXT'); assert.strictEqual(typeof value, 'string'); assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 } debug(`waitForDns: waiting for ${hostname} to be ${value} in zone ${zoneName}`); await promiseRetry(Object.assign({ debug }, options), async function () { const nameservers = await dig.resolve(zoneName, 'NS', { timeout: 5000 }); if (!nameservers) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to get nameservers'); debug(`waitForDns: nameservers are ${JSON.stringify(nameservers)}`); for (const nameserver of nameservers) { const synced = await isChangeSynced(hostname, type, value, nameserver); debug(`waitForDns: ${hostname} at ns ${nameserver}: ${synced ? 'done' : 'not done'} `); if (!synced) throw new BoxError(BoxError.EXTERNAL_ERROR, 'ETRYAGAIN'); } }); debug(`waitForDns: ${hostname} has propagated`); }