diff --git a/lib/internal/dns/utils.js b/lib/internal/dns/utils.js index f895a46c1a037e..511d857b59191c 100644 --- a/lib/internal/dns/utils.js +++ b/lib/internal/dns/utils.js @@ -10,6 +10,7 @@ const { RegExpPrototypeExec, RegExpPrototypeSymbolReplace, ObjectCreate, + Symbol, } = primordials; const errors = require('internal/errors'); @@ -35,6 +36,12 @@ const { ERR_INVALID_IP_ADDRESS, } = errors.codes; +const { + addSerializeCallback, + addDeserializeCallback, + isBuildingSnapshot, +} = require('v8').startupSnapshot; + function validateTimeout(options) { const { timeout = -1 } = { ...options }; validateInt32(timeout, 'options.timeout', -1); @@ -47,12 +54,27 @@ function validateTries(options) { return tries; } +const kSerializeResolver = Symbol('dns:resolver:serialize'); +const kDeserializeResolver = Symbol('dns:resolver:deserialize'); +const kSnapshotStates = Symbol('dns:resolver:config'); +const kInitializeHandle = Symbol('dns:resolver:initializeHandle'); +const kSetServersInteral = Symbol('dns:resolver:setServers'); + // Resolver instances correspond 1:1 to c-ares channels. class ResolverBase { constructor(options = undefined) { const timeout = validateTimeout(options); const tries = validateTries(options); + // If we are building snapshot, save the states of the resolver along + // the way. + if (isBuildingSnapshot()) { + this[kSnapshotStates] = { timeout, tries }; + } + this[kInitializeHandle](timeout, tries); + } + + [kInitializeHandle](timeout, tries) { const { ChannelWrap } = lazyBinding(); this._handle = new ChannelWrap(timeout, tries); } @@ -77,9 +99,7 @@ class ResolverBase { // Cache the original servers because in the event of an error while // setting the servers, c-ares won't have any servers available for // resolution. - const orig = this._handle.getServers() || []; const newSet = []; - ArrayPrototypeForEach(servers, (serv, index) => { validateString(serv, `servers[${index}]`); let ipVersion = isIP(serv); @@ -118,6 +138,11 @@ class ResolverBase { throw new ERR_INVALID_IP_ADDRESS(serv); }); + this[kSetServersInteral](newSet, servers); + } + + [kSetServersInteral](newSet, servers) { + const orig = this._handle.getServers() || []; const errorNumber = this._handle.setServers(newSet); if (errorNumber !== 0) { @@ -127,8 +152,13 @@ class ResolverBase { const err = strerror(errorNumber); throw new ERR_DNS_SET_SERVERS_FAILED(err, servers); } + + if (isBuildingSnapshot()) { + this[kSnapshotStates].servers = newSet; + } } + setLocalAddress(ipv4, ipv6) { validateString(ipv4, 'ipv4'); @@ -137,6 +167,31 @@ class ResolverBase { } this._handle.setLocalAddress(ipv4, ipv6); + + if (isBuildingSnapshot()) { + this[kSnapshotStates].localAddress = { ipv4, ipv6 }; + } + } + + // TODO(joyeecheung): consider exposing this if custom DNS resolvers + // end up being useful for snapshot users. + [kSerializeResolver]() { + this._handle = null; // We'll restore it during deserialization. + addDeserializeCallback(function deserializeResolver(resolver) { + resolver[kDeserializeResolver](); + }, this); + } + + [kDeserializeResolver]() { + const { timeout, tries, localAddress, servers } = this[kSnapshotStates]; + this[kInitializeHandle](timeout, tries); + if (localAddress) { + const { ipv4, ipv6 } = localAddress; + this._handle.setLocalAddress(ipv4, ipv6); + } + if (servers) { + this[kSetServersInteral](servers, servers); + } } } @@ -151,6 +206,14 @@ function initializeDns() { // Allow the deserialized application to override order from CLI. dnsOrder = orderFromCLI; } + + if (!isBuildingSnapshot()) { + return; + } + + addSerializeCallback(() => { + defaultResolver?.[kSerializeResolver](); + }); } const resolverKeys = [ diff --git a/lib/internal/main/mksnapshot.js b/lib/internal/main/mksnapshot.js index b91ef29ef14da4..ca43a19bda65c2 100644 --- a/lib/internal/main/mksnapshot.js +++ b/lib/internal/main/mksnapshot.js @@ -49,7 +49,7 @@ const supportedModules = new SafeSet(new SafeArrayIterator([ 'crypto', // 'dgram', // 'diagnostics_channel', - // 'dns', + 'dns', // 'dns/promises', // 'domain', 'events', diff --git a/src/cares_wrap.cc b/src/cares_wrap.cc index d19705f94e7f6e..eb3ebceeca6622 100644 --- a/src/cares_wrap.cc +++ b/src/cares_wrap.cc @@ -19,18 +19,19 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +#include "cares_wrap.h" #include "async_wrap-inl.h" -#include "base_object-inl.h" #include "base64-inl.h" -#include "cares_wrap.h" +#include "base_object-inl.h" #include "env-inl.h" #include "memory_tracker-inl.h" #include "node.h" #include "node_errors.h" +#include "node_external_reference.h" #include "req_wrap-inl.h" #include "util-inl.h" -#include "v8.h" #include "uv.h" +#include "v8.h" #include #include @@ -1955,7 +1956,36 @@ void Initialize(Local target, SetConstructorFunction(context, target, "ChannelWrap", channel_wrap); } +void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(GetAddrInfo); + registry->Register(GetNameInfo); + registry->Register(CanonicalizeIP); + registry->Register(StrError); + registry->Register(ChannelWrap::New); + + registry->Register(Query); + registry->Register(Query); + registry->Register(Query); + registry->Register(Query); + registry->Register(Query); + registry->Register(Query); + registry->Register(Query); + registry->Register(Query); + registry->Register(Query); + registry->Register(Query); + registry->Register(Query); + registry->Register(Query); + registry->Register(Query); + + registry->Register(GetServers); + registry->Register(SetServers); + registry->Register(SetLocalAddress); + registry->Register(Cancel); +} + } // namespace cares_wrap } // namespace node NODE_MODULE_CONTEXT_AWARE_INTERNAL(cares_wrap, node::cares_wrap::Initialize) +NODE_MODULE_EXTERNAL_REFERENCE(cares_wrap, + node::cares_wrap::RegisterExternalReferences) diff --git a/src/cares_wrap.h b/src/cares_wrap.h index 60f99e65edf348..ea339b773991e4 100644 --- a/src/cares_wrap.h +++ b/src/cares_wrap.h @@ -9,8 +9,9 @@ #include "base_object.h" #include "env.h" #include "memory_tracker.h" -#include "util.h" #include "node.h" +#include "node_internals.h" +#include "util.h" #include "ares.h" #include "v8.h" diff --git a/src/node_external_reference.h b/src/node_external_reference.h index e190d5c7038884..95292d45f78c2e 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -62,6 +62,7 @@ class ExternalReferenceRegistry { V(blob) \ V(buffer) \ V(builtins) \ + V(cares_wrap) \ V(contextify) \ V(credentials) \ V(env_var) \ diff --git a/test/common/snapshot.js b/test/common/snapshot.js new file mode 100644 index 00000000000000..97367c05656c3c --- /dev/null +++ b/test/common/snapshot.js @@ -0,0 +1,66 @@ +'use strict'; + +const tmpdir = require('../common/tmpdir'); +const { spawnSync } = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const assert = require('assert'); + +function buildSnapshot(entry, env) { + const child = spawnSync(process.execPath, [ + '--snapshot-blob', + path.join(tmpdir.path, 'snapshot.blob'), + '--build-snapshot', + entry, + ], { + cwd: tmpdir.path, + env: { + ...process.env, + ...env, + }, + }); + + const stderr = child.stderr.toString(); + const stdout = child.stdout.toString(); + console.log('[stderr]'); + console.log(stderr); + console.log('[stdout]'); + console.log(stdout); + + assert.strictEqual(child.status, 0); + + const stats = fs.statSync(path.join(tmpdir.path, 'snapshot.blob')); + assert(stats.isFile()); + + return { child, stderr, stdout }; +} + +function runWithSnapshot(entry, env) { + const args = ['--snapshot-blob', path.join(tmpdir.path, 'snapshot.blob')]; + if (entry !== undefined) { + args.push(entry); + } + const child = spawnSync(process.execPath, args, { + cwd: tmpdir.path, + env: { + ...process.env, + ...env, + } + }); + + const stderr = child.stderr.toString(); + const stdout = child.stdout.toString(); + console.log('[stderr]'); + console.log(stderr); + console.log('[stdout]'); + console.log(stdout); + + assert.strictEqual(child.status, 0); + + return { child, stderr, stdout }; +} + +module.exports = { + buildSnapshot, + runWithSnapshot, +}; diff --git a/test/fixtures/snapshot/dns-lookup.js b/test/fixtures/snapshot/dns-lookup.js new file mode 100644 index 00000000000000..773b508f7cb214 --- /dev/null +++ b/test/fixtures/snapshot/dns-lookup.js @@ -0,0 +1,39 @@ +'use strict'; +const dns = require('dns'); +const assert = require('assert'); + +assert(process.env.NODE_TEST_HOST); + +const { + setDeserializeMainFunction, +} = require('v8').startupSnapshot; + +function onError(err) { + console.error('error:', err); +} + +function onLookup(address, family) { + console.log(`address: ${JSON.stringify(address)}`); + console.log(`family: ${JSON.stringify(family)}`); +} + +function query() { + const host = process.env.NODE_TEST_HOST; + if (process.env.NODE_TEST_PROMISE === 'true') { + dns.promises.lookup(host, { family: 4 }).then( + ({address, family}) => onLookup(address, family), + onError); + } else { + dns.lookup(host, { family: 4 }, (err, address, family) => { + if (err) { + onError(err); + } else { + onLookup(address, family); + } + }); + } +} + +query(); + +setDeserializeMainFunction(query); diff --git a/test/fixtures/snapshot/dns-resolve.js b/test/fixtures/snapshot/dns-resolve.js new file mode 100644 index 00000000000000..6a776f29704cf5 --- /dev/null +++ b/test/fixtures/snapshot/dns-resolve.js @@ -0,0 +1,59 @@ +'use strict'; +const dns = require('dns'); +const assert = require('assert'); + +assert(process.env.NODE_TEST_HOST); + +const { + setDeserializeMainFunction, +} = require('v8').startupSnapshot; + +function onError(err) { + console.error('error:', err); +} + +function onResolve(addresses) { + console.log(`addresses: ${JSON.stringify(addresses)}`); +} + +function onReverse(hostnames) { + console.log(`hostnames: ${JSON.stringify(hostnames)}`); +} + +function query() { + if (process.env.NODE_TEST_DNS) { + dns.setServers([process.env.NODE_TEST_DNS]) + } + + const host = process.env.NODE_TEST_HOST; + if (process.env.NODE_TEST_PROMISE === 'true') { + dns.promises.resolve4(host).then(onResolve, onError); + } else { + dns.resolve4(host, (err, addresses) => { + if (err) { + onError(err); + } else { + onResolve(addresses); + } + }); + } + + const ip = process.env.NODE_TEST_IP; + if (ip) { + if (process.env.NODE_TEST_PROMISE === 'true') { + dns.promises.reverse(ip).then(onReverse, onError); + } else { + dns.reverse(ip, (err, hostnames) => { + if (err) { + onError(err); + } else { + onReverse(hostnames); + } + }); + } + } +} + +query(); + +setDeserializeMainFunction(query); diff --git a/test/internet/test-snapshot-dns-lookup.js b/test/internet/test-snapshot-dns-lookup.js new file mode 100644 index 00000000000000..842e73e0568839 --- /dev/null +++ b/test/internet/test-snapshot-dns-lookup.js @@ -0,0 +1,34 @@ +'use strict'; + +// This tests support for the dns module in snapshot. + +require('../common'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { buildSnapshot, runWithSnapshot } = require('../common/snapshot'); +const { + addresses: { INET4_HOST }, +} = require('../common/internet'); + +const entry = fixtures.path('snapshot', 'dns-lookup.js'); +const env = { + NODE_TEST_HOST: INET4_HOST, + NODE_TEST_PROMISE: 'false', +}; + +tmpdir.refresh(); +function checkOutput(stderr, stdout) { + assert(stdout.match(stdout, /address: "\d+\.\d+\.\d+\.\d+"/)); + assert(stdout.match(stdout, /family: 4/)); + assert.strictEqual(stdout.trim().split('\n').length, 2); +} +{ + const { stderr, stdout } = buildSnapshot(entry, env); + checkOutput(stderr, stdout); +} + +{ + const { stderr, stdout } = runWithSnapshot(entry, env); + checkOutput(stderr, stdout); +} diff --git a/test/internet/test-snapshot-dns-resolve.js b/test/internet/test-snapshot-dns-resolve.js new file mode 100644 index 00000000000000..6efea9b4f741a0 --- /dev/null +++ b/test/internet/test-snapshot-dns-resolve.js @@ -0,0 +1,36 @@ +'use strict'; + +// This tests support for the dns module in snapshot. + +require('../common'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { buildSnapshot, runWithSnapshot } = require('../common/snapshot'); +const { + addresses: { DNS4_SERVER, INET4_IP, INET4_HOST }, +} = require('../common/internet'); + +const entry = fixtures.path('snapshot', 'dns-resolve.js'); +const env = { + NODE_TEST_IP: INET4_IP, + NODE_TEST_HOST: INET4_HOST, + NODE_TEST_DNS: DNS4_SERVER, + NODE_TEST_PROMISE: 'false', +}; + +tmpdir.refresh(); +function checkOutput(stderr, stdout) { + assert(stdout.includes('hostnames: [')); + assert(stdout.includes('addresses: [')); + assert.strictEqual(stdout.trim().split('\n').length, 2); +} +{ + const { stderr, stdout } = buildSnapshot(entry, env); + checkOutput(stderr, stdout); +} + +{ + const { stderr, stdout } = runWithSnapshot(entry, env); + checkOutput(stderr, stdout); +} diff --git a/test/parallel/test-snapshot-dns-lookup-localhost-promise.js b/test/parallel/test-snapshot-dns-lookup-localhost-promise.js new file mode 100644 index 00000000000000..d19488a716aa04 --- /dev/null +++ b/test/parallel/test-snapshot-dns-lookup-localhost-promise.js @@ -0,0 +1,35 @@ +'use strict'; + +// This tests support for the dns module in snapshot. + +require('../common'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { buildSnapshot, runWithSnapshot } = require('../common/snapshot'); + +const entry = fixtures.path('snapshot', 'dns-lookup.js'); +const env = { + NODE_TEST_HOST: 'localhost', + NODE_TEST_PROMISE: 'true', +}; + +tmpdir.refresh(); +function checkOutput(stderr, stdout) { + // We allow failures as it's not always possible to resolve localhost. + // Functional tests are done in test/internet instead. + if (!stderr.startsWith('error:')) { + assert(stdout.match(stdout, /address: "\d+\.\d+\.\d+\.\d+"/)); + assert(stdout.match(stdout, /family: 4/)); + assert.strictEqual(stdout.trim().split('\n').length, 2); + } +} +{ + const { stderr, stdout } = buildSnapshot(entry, env); + checkOutput(stderr, stdout); +} + +{ + const { stderr, stdout } = runWithSnapshot(entry, env); + checkOutput(stderr, stdout); +} diff --git a/test/parallel/test-snapshot-dns-lookup-localhost.js b/test/parallel/test-snapshot-dns-lookup-localhost.js new file mode 100644 index 00000000000000..af00480bcca89b --- /dev/null +++ b/test/parallel/test-snapshot-dns-lookup-localhost.js @@ -0,0 +1,35 @@ +'use strict'; + +// This tests support for the dns module in snapshot. + +require('../common'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { buildSnapshot, runWithSnapshot } = require('../common/snapshot'); + +const entry = fixtures.path('snapshot', 'dns-lookup.js'); +const env = { + NODE_TEST_HOST: 'localhost', + NODE_TEST_PROMISE: 'false', +}; + +tmpdir.refresh(); +function checkOutput(stderr, stdout) { + // We allow failures as it's not always possible to resolve localhost. + // Functional tests are done in test/internet instead. + if (!stderr.startsWith('error:')) { + assert(stdout.match(stdout, /address: "\d+\.\d+\.\d+\.\d+"/)); + assert(stdout.match(stdout, /family: 4/)); + assert.strictEqual(stdout.trim().split('\n').length, 2); + } +} +{ + const { stderr, stdout } = buildSnapshot(entry, env); + checkOutput(stderr, stdout); +} + +{ + const { stderr, stdout } = runWithSnapshot(entry, env); + checkOutput(stderr, stdout); +} diff --git a/test/parallel/test-snapshot-dns-resolve-localhost-promise.js b/test/parallel/test-snapshot-dns-resolve-localhost-promise.js new file mode 100644 index 00000000000000..cca86007f16f67 --- /dev/null +++ b/test/parallel/test-snapshot-dns-resolve-localhost-promise.js @@ -0,0 +1,34 @@ +'use strict'; + +// This tests support for the dns module in snapshot. + +require('../common'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { buildSnapshot, runWithSnapshot } = require('../common/snapshot'); + +const entry = fixtures.path('snapshot', 'dns-resolve.js'); +const env = { + NODE_TEST_HOST: 'localhost', + NODE_TEST_PROMISE: 'true', +}; + +tmpdir.refresh(); +function checkOutput(stderr, stdout) { + // We allow failures as it's not always possible to resolve localhost. + // Functional tests are done in test/internet instead. + if (!stderr.startsWith('error:')) { + assert(stdout.includes('addresses: [')); + assert.strictEqual(stdout.trim().split('\n').length, 1); + } +} +{ + const { stderr, stdout } = buildSnapshot(entry, env); + checkOutput(stderr, stdout); +} + +{ + const { stderr, stdout } = runWithSnapshot(entry, env); + checkOutput(stderr, stdout); +} diff --git a/test/parallel/test-snapshot-dns-resolve-localhost.js b/test/parallel/test-snapshot-dns-resolve-localhost.js new file mode 100644 index 00000000000000..af4576bcc3e48a --- /dev/null +++ b/test/parallel/test-snapshot-dns-resolve-localhost.js @@ -0,0 +1,34 @@ +'use strict'; + +// This tests support for the dns module in snapshot. + +require('../common'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const { buildSnapshot, runWithSnapshot } = require('../common/snapshot'); + +const entry = fixtures.path('snapshot', 'dns-resolve.js'); +const env = { + NODE_TEST_HOST: 'localhost', + NODE_TEST_PROMISE: 'false', +}; + +tmpdir.refresh(); +function checkOutput(stderr, stdout) { + // We allow failures as it's not always possible to resolve localhost. + // Functional tests are done in test/internet instead. + if (!stderr.startsWith('error:')) { + assert(stdout.includes('addresses: [')); + assert.strictEqual(stdout.trim().split('\n').length, 1); + } +} +{ + const { stderr, stdout } = buildSnapshot(entry, env); + checkOutput(stderr, stdout); +} + +{ + const { stderr, stdout } = runWithSnapshot(entry, env); + checkOutput(stderr, stdout); +}