• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const common = require('../common');
4const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
5
6const assert = require('assert');
7const dgram = require('dgram');
8const { Resolver } = require('dns');
9const { createConnection, createServer } = require('net');
10
11// Test that happy eyeballs algorithm is properly implemented.
12
13// Purposely not using setDefaultAutoSelectFamilyAttemptTimeout here to test the
14// parameter is correctly used in options.
15//
16// Some of the windows machines in the CI need more time to establish connection
17const autoSelectFamilyAttemptTimeout = common.platformTimeout(common.isWindows ? 1500 : 250);
18
19function _lookup(resolver, hostname, options, cb) {
20  resolver.resolve(hostname, 'ANY', (err, replies) => {
21    assert.notStrictEqual(options.family, 4);
22
23    if (err) {
24      return cb(err);
25    }
26
27    const hosts = replies
28      .map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 }))
29      .sort((a, b) => b.family - a.family);
30
31    if (options.all === true) {
32      return cb(null, hosts);
33    }
34
35    return cb(null, hosts[0].address, hosts[0].family);
36  });
37}
38
39function createDnsServer(ipv6Addrs, ipv4Addrs, cb) {
40  if (!Array.isArray(ipv6Addrs)) {
41    ipv6Addrs = [ipv6Addrs];
42  }
43
44  if (!Array.isArray(ipv4Addrs)) {
45    ipv4Addrs = [ipv4Addrs];
46  }
47
48  // Create a DNS server which replies with a AAAA and a A record for the same host
49  const socket = dgram.createSocket('udp4');
50
51  socket.on('message', common.mustCall((msg, { address, port }) => {
52    const parsed = parseDNSPacket(msg);
53    const domain = parsed.questions[0].domain;
54    assert.strictEqual(domain, 'example.org');
55
56    socket.send(writeDNSPacket({
57      id: parsed.id,
58      questions: parsed.questions,
59      answers: [
60        ...ipv6Addrs.map((address) => ({ type: 'AAAA', address, ttl: 123, domain: 'example.org' })),
61        ...ipv4Addrs.map((address) => ({ type: 'A', address, ttl: 123, domain: 'example.org' })),
62      ]
63    }), port, address);
64  }));
65
66  socket.bind(0, () => {
67    const resolver = new Resolver();
68    resolver.setServers([`127.0.0.1:${socket.address().port}`]);
69
70    cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
71  });
72}
73
74// Test that IPV4 is reached if IPV6 is not reachable
75{
76  createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
77    const ipv4Server = createServer((socket) => {
78      socket.on('data', common.mustCall(() => {
79        socket.write('response-ipv4');
80        socket.end();
81      }));
82    });
83
84    ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
85      const port = ipv4Server.address().port;
86
87      const connection = createConnection({
88        host: 'example.org',
89        port: port,
90        lookup,
91        autoSelectFamily: true,
92        autoSelectFamilyAttemptTimeout,
93      });
94
95      let response = '';
96      connection.setEncoding('utf-8');
97
98      connection.on('ready', common.mustCall(() => {
99        assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`, `127.0.0.1:${port}`]);
100      }));
101
102      connection.on('data', (chunk) => {
103        response += chunk;
104      });
105
106      connection.on('end', common.mustCall(() => {
107        assert.strictEqual(response, 'response-ipv4');
108        ipv4Server.close();
109        dnsServer.close();
110      }));
111
112      connection.write('request');
113    }));
114  }));
115}
116
117// Test that only the last successful connection is established.
118{
119  createDnsServer(
120    ['2606:4700::6810:85e5', '2606:4700::6810:84e5', '::1'],
121    ['104.20.22.46', '104.20.23.46', '127.0.0.1'],
122    common.mustCall(function({ dnsServer, lookup }) {
123      const ipv4Server = createServer((socket) => {
124        socket.on('data', common.mustCall(() => {
125          socket.write('response-ipv4');
126          socket.end();
127        }));
128      });
129
130      ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
131        const port = ipv4Server.address().port;
132
133        const connection = createConnection({
134          host: 'example.org',
135          port: port,
136          lookup,
137          autoSelectFamily: true,
138          autoSelectFamilyAttemptTimeout,
139        });
140
141        let response = '';
142        connection.setEncoding('utf-8');
143
144        connection.on('ready', common.mustCall(() => {
145          assert.deepStrictEqual(
146            connection.autoSelectFamilyAttemptedAddresses,
147            [
148              `2606:4700::6810:85e5:${port}`,
149              `104.20.22.46:${port}`,
150              `2606:4700::6810:84e5:${port}`,
151              `104.20.23.46:${port}`,
152              `::1:${port}`,
153              `127.0.0.1:${port}`,
154            ]
155          );
156        }));
157
158        connection.on('data', (chunk) => {
159          response += chunk;
160        });
161
162        connection.on('end', common.mustCall(() => {
163          assert.strictEqual(response, 'response-ipv4');
164          ipv4Server.close();
165          dnsServer.close();
166        }));
167
168        connection.write('request');
169      }));
170    })
171  );
172}
173
174// Test that IPV4 is NOT reached if IPV6 is reachable
175if (common.hasIPv6) {
176  createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
177    const ipv4Server = createServer((socket) => {
178      socket.on('data', common.mustNotCall(() => {
179        socket.write('response-ipv4');
180        socket.end();
181      }));
182    });
183
184    const ipv6Server = createServer((socket) => {
185      socket.on('data', common.mustCall(() => {
186        socket.write('response-ipv6');
187        socket.end();
188      }));
189    });
190
191    ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
192      const port = ipv4Server.address().port;
193
194      ipv6Server.listen(port, '::1', common.mustCall(() => {
195        const connection = createConnection({
196          host: 'example.org',
197          port,
198          lookup,
199          autoSelectFamily: true,
200          autoSelectFamilyAttemptTimeout,
201        });
202
203        let response = '';
204        connection.setEncoding('utf-8');
205
206        connection.on('ready', common.mustCall(() => {
207          assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`]);
208        }));
209
210        connection.on('data', (chunk) => {
211          response += chunk;
212        });
213
214        connection.on('end', common.mustCall(() => {
215          assert.strictEqual(response, 'response-ipv6');
216          ipv4Server.close();
217          ipv6Server.close();
218          dnsServer.close();
219        }));
220
221        connection.write('request');
222      }));
223    }));
224  }));
225}
226
227// Test that when all errors are returned when no connections succeeded
228{
229  createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
230    const connection = createConnection({
231      host: 'example.org',
232      port: 10,
233      lookup,
234      autoSelectFamily: true,
235      autoSelectFamilyAttemptTimeout,
236    });
237
238    connection.on('ready', common.mustNotCall());
239    connection.on('error', common.mustCall((error) => {
240      assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, ['::1:10', '127.0.0.1:10']);
241      assert.strictEqual(error.constructor.name, 'AggregateError');
242      assert.strictEqual(error.errors.length, 2);
243
244      const errors = error.errors.map((e) => e.message);
245      assert.ok(errors.includes('connect ECONNREFUSED 127.0.0.1:10'));
246
247      if (common.hasIPv6) {
248        assert.ok(errors.includes('connect ECONNREFUSED ::1:10'));
249      }
250
251      dnsServer.close();
252    }));
253  }));
254}
255
256// Test that the option can be disabled
257{
258  createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
259    const ipv4Server = createServer((socket) => {
260      socket.on('data', common.mustCall(() => {
261        socket.write('response-ipv4');
262        socket.end();
263      }));
264    });
265
266    ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
267      const port = ipv4Server.address().port;
268
269      const connection = createConnection({
270        host: 'example.org',
271        port,
272        lookup,
273        autoSelectFamily: false,
274      });
275
276      connection.on('ready', common.mustNotCall());
277      connection.on('error', common.mustCall((error) => {
278        assert.strictEqual(connection.autoSelectFamilyAttemptedAddresses, undefined);
279
280        if (common.hasIPv6) {
281          assert.strictEqual(error.code, 'ECONNREFUSED');
282          assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`);
283        } else if (error.code === 'EAFNOSUPPORT') {
284          assert.strictEqual(error.message, `connect EAFNOSUPPORT ::1:${port} - Local (undefined:undefined)`);
285        } else if (common.isIBMi) {
286          // IBMi returns EUNATCH (ERRNO 42) when IPv6 is disabled
287          // keep this errno assertion until EUNATCH is recognized by libuv
288          assert.strictEqual(error.errno, -42);
289        } else {
290          assert.strictEqual(error.code, 'EADDRNOTAVAIL');
291          assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
292        }
293
294        ipv4Server.close();
295        dnsServer.close();
296      }));
297    }));
298  }));
299}
300