Skip to content

Commit 9151439

Browse files
joyeecheungdanielleadams
authored andcommittedOct 5, 2022
dns: support dns module in the snapshot
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>
1 parent f8b2d7a commit 9151439

14 files changed

+474
-7
lines changed
 

‎lib/internal/dns/utils.js

+65-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
RegExpPrototypeExec,
1111
RegExpPrototypeSymbolReplace,
1212
ObjectCreate,
13+
Symbol,
1314
} = primordials;
1415

1516
const errors = require('internal/errors');
@@ -35,6 +36,12 @@ const {
3536
ERR_INVALID_IP_ADDRESS,
3637
} = errors.codes;
3738

39+
const {
40+
addSerializeCallback,
41+
addDeserializeCallback,
42+
isBuildingSnapshot,
43+
} = require('v8').startupSnapshot;
44+
3845
function validateTimeout(options) {
3946
const { timeout = -1 } = { ...options };
4047
validateInt32(timeout, 'options.timeout', -1);
@@ -47,12 +54,27 @@ function validateTries(options) {
4754
return tries;
4855
}
4956

57+
const kSerializeResolver = Symbol('dns:resolver:serialize');
58+
const kDeserializeResolver = Symbol('dns:resolver:deserialize');
59+
const kSnapshotStates = Symbol('dns:resolver:config');
60+
const kInitializeHandle = Symbol('dns:resolver:initializeHandle');
61+
const kSetServersInteral = Symbol('dns:resolver:setServers');
62+
5063
// Resolver instances correspond 1:1 to c-ares channels.
5164

5265
class ResolverBase {
5366
constructor(options = undefined) {
5467
const timeout = validateTimeout(options);
5568
const tries = validateTries(options);
69+
// If we are building snapshot, save the states of the resolver along
70+
// the way.
71+
if (isBuildingSnapshot()) {
72+
this[kSnapshotStates] = { timeout, tries };
73+
}
74+
this[kInitializeHandle](timeout, tries);
75+
}
76+
77+
[kInitializeHandle](timeout, tries) {
5678
const { ChannelWrap } = lazyBinding();
5779
this._handle = new ChannelWrap(timeout, tries);
5880
}
@@ -77,9 +99,7 @@ class ResolverBase {
7799
// Cache the original servers because in the event of an error while
78100
// setting the servers, c-ares won't have any servers available for
79101
// resolution.
80-
const orig = this._handle.getServers() || [];
81102
const newSet = [];
82-
83103
ArrayPrototypeForEach(servers, (serv, index) => {
84104
validateString(serv, `servers[${index}]`);
85105
let ipVersion = isIP(serv);
@@ -118,6 +138,11 @@ class ResolverBase {
118138
throw new ERR_INVALID_IP_ADDRESS(serv);
119139
});
120140

141+
this[kSetServersInteral](newSet, servers);
142+
}
143+
144+
[kSetServersInteral](newSet, servers) {
145+
const orig = this._handle.getServers() || [];
121146
const errorNumber = this._handle.setServers(newSet);
122147

123148
if (errorNumber !== 0) {
@@ -127,8 +152,13 @@ class ResolverBase {
127152
const err = strerror(errorNumber);
128153
throw new ERR_DNS_SET_SERVERS_FAILED(err, servers);
129154
}
155+
156+
if (isBuildingSnapshot()) {
157+
this[kSnapshotStates].servers = newSet;
158+
}
130159
}
131160

161+
132162
setLocalAddress(ipv4, ipv6) {
133163
validateString(ipv4, 'ipv4');
134164

@@ -137,6 +167,31 @@ class ResolverBase {
137167
}
138168

139169
this._handle.setLocalAddress(ipv4, ipv6);
170+
171+
if (isBuildingSnapshot()) {
172+
this[kSnapshotStates].localAddress = { ipv4, ipv6 };
173+
}
174+
}
175+
176+
// TODO(joyeecheung): consider exposing this if custom DNS resolvers
177+
// end up being useful for snapshot users.
178+
[kSerializeResolver]() {
179+
this._handle = null; // We'll restore it during deserialization.
180+
addDeserializeCallback(function deserializeResolver(resolver) {
181+
resolver[kDeserializeResolver]();
182+
}, this);
183+
}
184+
185+
[kDeserializeResolver]() {
186+
const { timeout, tries, localAddress, servers } = this[kSnapshotStates];
187+
this[kInitializeHandle](timeout, tries);
188+
if (localAddress) {
189+
const { ipv4, ipv6 } = localAddress;
190+
this._handle.setLocalAddress(ipv4, ipv6);
191+
}
192+
if (servers) {
193+
this[kSetServersInteral](servers, servers);
194+
}
140195
}
141196
}
142197

@@ -151,6 +206,14 @@ function initializeDns() {
151206
// Allow the deserialized application to override order from CLI.
152207
dnsOrder = orderFromCLI;
153208
}
209+
210+
if (!isBuildingSnapshot()) {
211+
return;
212+
}
213+
214+
addSerializeCallback(() => {
215+
defaultResolver?.[kSerializeResolver]();
216+
});
154217
}
155218

156219
const resolverKeys = [

‎lib/internal/main/mksnapshot.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const supportedModules = new SafeSet(new SafeArrayIterator([
4949
'crypto',
5050
// 'dgram',
5151
// 'diagnostics_channel',
52-
// 'dns',
52+
'dns',
5353
// 'dns/promises',
5454
// 'domain',
5555
'events',

‎src/cares_wrap.cc

+33-3
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,19 @@
1919
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
2020
// USE OR OTHER DEALINGS IN THE SOFTWARE.
2121

22+
#include "cares_wrap.h"
2223
#include "async_wrap-inl.h"
23-
#include "base_object-inl.h"
2424
#include "base64-inl.h"
25-
#include "cares_wrap.h"
25+
#include "base_object-inl.h"
2626
#include "env-inl.h"
2727
#include "memory_tracker-inl.h"
2828
#include "node.h"
2929
#include "node_errors.h"
30+
#include "node_external_reference.h"
3031
#include "req_wrap-inl.h"
3132
#include "util-inl.h"
32-
#include "v8.h"
3333
#include "uv.h"
34+
#include "v8.h"
3435

3536
#include <cerrno>
3637
#include <cstring>
@@ -1955,7 +1956,36 @@ void Initialize(Local<Object> target,
19551956
SetConstructorFunction(context, target, "ChannelWrap", channel_wrap);
19561957
}
19571958

1959+
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
1960+
registry->Register(GetAddrInfo);
1961+
registry->Register(GetNameInfo);
1962+
registry->Register(CanonicalizeIP);
1963+
registry->Register(StrError);
1964+
registry->Register(ChannelWrap::New);
1965+
1966+
registry->Register(Query<QueryAnyWrap>);
1967+
registry->Register(Query<QueryAWrap>);
1968+
registry->Register(Query<QueryAaaaWrap>);
1969+
registry->Register(Query<QueryCaaWrap>);
1970+
registry->Register(Query<QueryCnameWrap>);
1971+
registry->Register(Query<QueryMxWrap>);
1972+
registry->Register(Query<QueryNsWrap>);
1973+
registry->Register(Query<QueryTxtWrap>);
1974+
registry->Register(Query<QuerySrvWrap>);
1975+
registry->Register(Query<QueryPtrWrap>);
1976+
registry->Register(Query<QueryNaptrWrap>);
1977+
registry->Register(Query<QuerySoaWrap>);
1978+
registry->Register(Query<GetHostByAddrWrap>);
1979+
1980+
registry->Register(GetServers);
1981+
registry->Register(SetServers);
1982+
registry->Register(SetLocalAddress);
1983+
registry->Register(Cancel);
1984+
}
1985+
19581986
} // namespace cares_wrap
19591987
} // namespace node
19601988

19611989
NODE_MODULE_CONTEXT_AWARE_INTERNAL(cares_wrap, node::cares_wrap::Initialize)
1990+
NODE_MODULE_EXTERNAL_REFERENCE(cares_wrap,
1991+
node::cares_wrap::RegisterExternalReferences)

‎src/cares_wrap.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
#include "base_object.h"
1010
#include "env.h"
1111
#include "memory_tracker.h"
12-
#include "util.h"
1312
#include "node.h"
13+
#include "node_internals.h"
14+
#include "util.h"
1415

1516
#include "ares.h"
1617
#include "v8.h"

‎src/node_external_reference.h

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class ExternalReferenceRegistry {
6262
V(blob) \
6363
V(buffer) \
6464
V(builtins) \
65+
V(cares_wrap) \
6566
V(contextify) \
6667
V(credentials) \
6768
V(env_var) \

‎test/common/snapshot.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use strict';
2+
3+
const tmpdir = require('../common/tmpdir');
4+
const { spawnSync } = require('child_process');
5+
const path = require('path');
6+
const fs = require('fs');
7+
const assert = require('assert');
8+
9+
function buildSnapshot(entry, env) {
10+
const child = spawnSync(process.execPath, [
11+
'--snapshot-blob',
12+
path.join(tmpdir.path, 'snapshot.blob'),
13+
'--build-snapshot',
14+
entry,
15+
], {
16+
cwd: tmpdir.path,
17+
env: {
18+
...process.env,
19+
...env,
20+
},
21+
});
22+
23+
const stderr = child.stderr.toString();
24+
const stdout = child.stdout.toString();
25+
console.log('[stderr]');
26+
console.log(stderr);
27+
console.log('[stdout]');
28+
console.log(stdout);
29+
30+
assert.strictEqual(child.status, 0);
31+
32+
const stats = fs.statSync(path.join(tmpdir.path, 'snapshot.blob'));
33+
assert(stats.isFile());
34+
35+
return { child, stderr, stdout };
36+
}
37+
38+
function runWithSnapshot(entry, env) {
39+
const args = ['--snapshot-blob', path.join(tmpdir.path, 'snapshot.blob')];
40+
if (entry !== undefined) {
41+
args.push(entry);
42+
}
43+
const child = spawnSync(process.execPath, args, {
44+
cwd: tmpdir.path,
45+
env: {
46+
...process.env,
47+
...env,
48+
}
49+
});
50+
51+
const stderr = child.stderr.toString();
52+
const stdout = child.stdout.toString();
53+
console.log('[stderr]');
54+
console.log(stderr);
55+
console.log('[stdout]');
56+
console.log(stdout);
57+
58+
assert.strictEqual(child.status, 0);
59+
60+
return { child, stderr, stdout };
61+
}
62+
63+
module.exports = {
64+
buildSnapshot,
65+
runWithSnapshot,
66+
};

‎test/fixtures/snapshot/dns-lookup.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
const dns = require('dns');
3+
const assert = require('assert');
4+
5+
assert(process.env.NODE_TEST_HOST);
6+
7+
const {
8+
setDeserializeMainFunction,
9+
} = require('v8').startupSnapshot;
10+
11+
function onError(err) {
12+
console.error('error:', err);
13+
}
14+
15+
function onLookup(address, family) {
16+
console.log(`address: ${JSON.stringify(address)}`);
17+
console.log(`family: ${JSON.stringify(family)}`);
18+
}
19+
20+
function query() {
21+
const host = process.env.NODE_TEST_HOST;
22+
if (process.env.NODE_TEST_PROMISE === 'true') {
23+
dns.promises.lookup(host, { family: 4 }).then(
24+
({address, family}) => onLookup(address, family),
25+
onError);
26+
} else {
27+
dns.lookup(host, { family: 4 }, (err, address, family) => {
28+
if (err) {
29+
onError(err);
30+
} else {
31+
onLookup(address, family);
32+
}
33+
});
34+
}
35+
}
36+
37+
query();
38+
39+
setDeserializeMainFunction(query);

‎test/fixtures/snapshot/dns-resolve.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
const dns = require('dns');
3+
const assert = require('assert');
4+
5+
assert(process.env.NODE_TEST_HOST);
6+
7+
const {
8+
setDeserializeMainFunction,
9+
} = require('v8').startupSnapshot;
10+
11+
function onError(err) {
12+
console.error('error:', err);
13+
}
14+
15+
function onResolve(addresses) {
16+
console.log(`addresses: ${JSON.stringify(addresses)}`);
17+
}
18+
19+
function onReverse(hostnames) {
20+
console.log(`hostnames: ${JSON.stringify(hostnames)}`);
21+
}
22+
23+
function query() {
24+
if (process.env.NODE_TEST_DNS) {
25+
dns.setServers([process.env.NODE_TEST_DNS])
26+
}
27+
28+
const host = process.env.NODE_TEST_HOST;
29+
if (process.env.NODE_TEST_PROMISE === 'true') {
30+
dns.promises.resolve4(host).then(onResolve, onError);
31+
} else {
32+
dns.resolve4(host, (err, addresses) => {
33+
if (err) {
34+
onError(err);
35+
} else {
36+
onResolve(addresses);
37+
}
38+
});
39+
}
40+
41+
const ip = process.env.NODE_TEST_IP;
42+
if (ip) {
43+
if (process.env.NODE_TEST_PROMISE === 'true') {
44+
dns.promises.reverse(ip).then(onReverse, onError);
45+
} else {
46+
dns.reverse(ip, (err, hostnames) => {
47+
if (err) {
48+
onError(err);
49+
} else {
50+
onReverse(hostnames);
51+
}
52+
});
53+
}
54+
}
55+
}
56+
57+
query();
58+
59+
setDeserializeMainFunction(query);
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
// This tests support for the dns module in snapshot.
4+
5+
require('../common');
6+
const assert = require('assert');
7+
const tmpdir = require('../common/tmpdir');
8+
const fixtures = require('../common/fixtures');
9+
const { buildSnapshot, runWithSnapshot } = require('../common/snapshot');
10+
const {
11+
addresses: { INET4_HOST },
12+
} = require('../common/internet');
13+
14+
const entry = fixtures.path('snapshot', 'dns-lookup.js');
15+
const env = {
16+
NODE_TEST_HOST: INET4_HOST,
17+
NODE_TEST_PROMISE: 'false',
18+
};
19+
20+
tmpdir.refresh();
21+
function checkOutput(stderr, stdout) {
22+
assert(stdout.match(stdout, /address: "\d+\.\d+\.\d+\.\d+"/));
23+
assert(stdout.match(stdout, /family: 4/));
24+
assert.strictEqual(stdout.trim().split('\n').length, 2);
25+
}
26+
{
27+
const { stderr, stdout } = buildSnapshot(entry, env);
28+
checkOutput(stderr, stdout);
29+
}
30+
31+
{
32+
const { stderr, stdout } = runWithSnapshot(entry, env);
33+
checkOutput(stderr, stdout);
34+
}
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
3+
// This tests support for the dns module in snapshot.
4+
5+
require('../common');
6+
const assert = require('assert');
7+
const tmpdir = require('../common/tmpdir');
8+
const fixtures = require('../common/fixtures');
9+
const { buildSnapshot, runWithSnapshot } = require('../common/snapshot');
10+
const {
11+
addresses: { DNS4_SERVER, INET4_IP, INET4_HOST },
12+
} = require('../common/internet');
13+
14+
const entry = fixtures.path('snapshot', 'dns-resolve.js');
15+
const env = {
16+
NODE_TEST_IP: INET4_IP,
17+
NODE_TEST_HOST: INET4_HOST,
18+
NODE_TEST_DNS: DNS4_SERVER,
19+
NODE_TEST_PROMISE: 'false',
20+
};
21+
22+
tmpdir.refresh();
23+
function checkOutput(stderr, stdout) {
24+
assert(stdout.includes('hostnames: ['));
25+
assert(stdout.includes('addresses: ['));
26+
assert.strictEqual(stdout.trim().split('\n').length, 2);
27+
}
28+
{
29+
const { stderr, stdout } = buildSnapshot(entry, env);
30+
checkOutput(stderr, stdout);
31+
}
32+
33+
{
34+
const { stderr, stdout } = runWithSnapshot(entry, env);
35+
checkOutput(stderr, stdout);
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
// This tests support for the dns module in snapshot.
4+
5+
require('../common');
6+
const assert = require('assert');
7+
const tmpdir = require('../common/tmpdir');
8+
const fixtures = require('../common/fixtures');
9+
const { buildSnapshot, runWithSnapshot } = require('../common/snapshot');
10+
11+
const entry = fixtures.path('snapshot', 'dns-lookup.js');
12+
const env = {
13+
NODE_TEST_HOST: 'localhost',
14+
NODE_TEST_PROMISE: 'true',
15+
};
16+
17+
tmpdir.refresh();
18+
function checkOutput(stderr, stdout) {
19+
// We allow failures as it's not always possible to resolve localhost.
20+
// Functional tests are done in test/internet instead.
21+
if (!stderr.startsWith('error:')) {
22+
assert(stdout.match(stdout, /address: "\d+\.\d+\.\d+\.\d+"/));
23+
assert(stdout.match(stdout, /family: 4/));
24+
assert.strictEqual(stdout.trim().split('\n').length, 2);
25+
}
26+
}
27+
{
28+
const { stderr, stdout } = buildSnapshot(entry, env);
29+
checkOutput(stderr, stdout);
30+
}
31+
32+
{
33+
const { stderr, stdout } = runWithSnapshot(entry, env);
34+
checkOutput(stderr, stdout);
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
// This tests support for the dns module in snapshot.
4+
5+
require('../common');
6+
const assert = require('assert');
7+
const tmpdir = require('../common/tmpdir');
8+
const fixtures = require('../common/fixtures');
9+
const { buildSnapshot, runWithSnapshot } = require('../common/snapshot');
10+
11+
const entry = fixtures.path('snapshot', 'dns-lookup.js');
12+
const env = {
13+
NODE_TEST_HOST: 'localhost',
14+
NODE_TEST_PROMISE: 'false',
15+
};
16+
17+
tmpdir.refresh();
18+
function checkOutput(stderr, stdout) {
19+
// We allow failures as it's not always possible to resolve localhost.
20+
// Functional tests are done in test/internet instead.
21+
if (!stderr.startsWith('error:')) {
22+
assert(stdout.match(stdout, /address: "\d+\.\d+\.\d+\.\d+"/));
23+
assert(stdout.match(stdout, /family: 4/));
24+
assert.strictEqual(stdout.trim().split('\n').length, 2);
25+
}
26+
}
27+
{
28+
const { stderr, stdout } = buildSnapshot(entry, env);
29+
checkOutput(stderr, stdout);
30+
}
31+
32+
{
33+
const { stderr, stdout } = runWithSnapshot(entry, env);
34+
checkOutput(stderr, stdout);
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
// This tests support for the dns module in snapshot.
4+
5+
require('../common');
6+
const assert = require('assert');
7+
const tmpdir = require('../common/tmpdir');
8+
const fixtures = require('../common/fixtures');
9+
const { buildSnapshot, runWithSnapshot } = require('../common/snapshot');
10+
11+
const entry = fixtures.path('snapshot', 'dns-resolve.js');
12+
const env = {
13+
NODE_TEST_HOST: 'localhost',
14+
NODE_TEST_PROMISE: 'true',
15+
};
16+
17+
tmpdir.refresh();
18+
function checkOutput(stderr, stdout) {
19+
// We allow failures as it's not always possible to resolve localhost.
20+
// Functional tests are done in test/internet instead.
21+
if (!stderr.startsWith('error:')) {
22+
assert(stdout.includes('addresses: ['));
23+
assert.strictEqual(stdout.trim().split('\n').length, 1);
24+
}
25+
}
26+
{
27+
const { stderr, stdout } = buildSnapshot(entry, env);
28+
checkOutput(stderr, stdout);
29+
}
30+
31+
{
32+
const { stderr, stdout } = runWithSnapshot(entry, env);
33+
checkOutput(stderr, stdout);
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
// This tests support for the dns module in snapshot.
4+
5+
require('../common');
6+
const assert = require('assert');
7+
const tmpdir = require('../common/tmpdir');
8+
const fixtures = require('../common/fixtures');
9+
const { buildSnapshot, runWithSnapshot } = require('../common/snapshot');
10+
11+
const entry = fixtures.path('snapshot', 'dns-resolve.js');
12+
const env = {
13+
NODE_TEST_HOST: 'localhost',
14+
NODE_TEST_PROMISE: 'false',
15+
};
16+
17+
tmpdir.refresh();
18+
function checkOutput(stderr, stdout) {
19+
// We allow failures as it's not always possible to resolve localhost.
20+
// Functional tests are done in test/internet instead.
21+
if (!stderr.startsWith('error:')) {
22+
assert(stdout.includes('addresses: ['));
23+
assert.strictEqual(stdout.trim().split('\n').length, 1);
24+
}
25+
}
26+
{
27+
const { stderr, stdout } = buildSnapshot(entry, env);
28+
checkOutput(stderr, stdout);
29+
}
30+
31+
{
32+
const { stderr, stdout } = runWithSnapshot(entry, env);
33+
checkOutput(stderr, stdout);
34+
}

0 commit comments

Comments
 (0)
Please sign in to comment.