Skip to content

Commit

Permalink
dns: support dns module in the snapshot
Browse files Browse the repository at this point in the history
For the initial iteration, only the default resolver can be
serialized/deserialized. If `dns.setServers()` has been
called, we'll preserve the configured DNS servers in the snapshot.
We can consider exposing the serialization method if it becomes
necessary for user-land snapshots.

PR-URL: #44633
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
  • Loading branch information
joyeecheung authored and danielleadams committed Oct 5, 2022
1 parent f8b2d7a commit 9151439
Show file tree
Hide file tree
Showing 14 changed files with 474 additions and 7 deletions.
67 changes: 65 additions & 2 deletions lib/internal/dns/utils.js
Expand Up @@ -10,6 +10,7 @@ const {
RegExpPrototypeExec,
RegExpPrototypeSymbolReplace,
ObjectCreate,
Symbol,
} = primordials;

const errors = require('internal/errors');
Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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');

Expand All @@ -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);
}
}
}

Expand All @@ -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 = [
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/main/mksnapshot.js
Expand Up @@ -49,7 +49,7 @@ const supportedModules = new SafeSet(new SafeArrayIterator([
'crypto',
// 'dgram',
// 'diagnostics_channel',
// 'dns',
'dns',
// 'dns/promises',
// 'domain',
'events',
Expand Down
36 changes: 33 additions & 3 deletions src/cares_wrap.cc
Expand Up @@ -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 <cerrno>
#include <cstring>
Expand Down Expand Up @@ -1955,7 +1956,36 @@ void Initialize(Local<Object> 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<QueryAnyWrap>);
registry->Register(Query<QueryAWrap>);
registry->Register(Query<QueryAaaaWrap>);
registry->Register(Query<QueryCaaWrap>);
registry->Register(Query<QueryCnameWrap>);
registry->Register(Query<QueryMxWrap>);
registry->Register(Query<QueryNsWrap>);
registry->Register(Query<QueryTxtWrap>);
registry->Register(Query<QuerySrvWrap>);
registry->Register(Query<QueryPtrWrap>);
registry->Register(Query<QueryNaptrWrap>);
registry->Register(Query<QuerySoaWrap>);
registry->Register(Query<GetHostByAddrWrap>);

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)
3 changes: 2 additions & 1 deletion src/cares_wrap.h
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/node_external_reference.h
Expand Up @@ -62,6 +62,7 @@ class ExternalReferenceRegistry {
V(blob) \
V(buffer) \
V(builtins) \
V(cares_wrap) \
V(contextify) \
V(credentials) \
V(env_var) \
Expand Down
66 changes: 66 additions & 0 deletions 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,
};
39 changes: 39 additions & 0 deletions 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);
59 changes: 59 additions & 0 deletions 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);

0 comments on commit 9151439

Please sign in to comment.