Skip to content

Commit 048795d

Browse files
ShogunPandadanielleadams
authored andcommittedJan 3, 2023
net: add autoSelectFamily and autoSelectFamilyAttemptTimeout options
PR-URL: #44731 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 8203c02 commit 048795d

9 files changed

+932
-7
lines changed
 

‎doc/api/net.md

+18
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,9 @@ behavior.
854854
<!-- YAML
855855
added: v0.1.90
856856
changes:
857+
- version: REPLACEME
858+
pr-url: https://github.com/nodejs/node/pull/44731
859+
description: Added the `autoSelectFamily` option.
857860
- version: v17.7.0
858861
pr-url: https://github.com/nodejs/node/pull/41310
859862
description: The `noDelay`, `keepAlive`, and `keepAliveInitialDelay`
@@ -898,6 +901,20 @@ For TCP connections, available `options` are:
898901
**Default:** `false`.
899902
* `keepAliveInitialDelay` {number} If set to a positive number, it sets the initial delay before
900903
the first keepalive probe is sent on an idle socket.**Default:** `0`.
904+
* `autoSelectFamily` {boolean}: If set to `true`, it enables a family autodetection algorithm
905+
that loosely implements section 5 of [RFC 8305][].
906+
The `all` option passed to lookup is set to `true` and the sockets attempts to connect to all
907+
obtained IPv6 and IPv4 addresses, in sequence, until a connection is established.
908+
The first returned AAAA address is tried first, then the first returned A address and so on.
909+
Each connection attempt is given the amount of time specified by the `autoSelectFamilyAttemptTimeout`
910+
option before timing out and trying the next address.
911+
Ignored if the `family` option is not `0` or if `localAddress` is set.
912+
Connection errors are not emitted if at least one connection succeeds.
913+
**Default:** `false`.
914+
* `autoSelectFamilyAttemptTimeout` {number}: The amount of time in milliseconds to wait
915+
for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option.
916+
If set to a positive integer less than `10`, then the value `10` will be used instead.
917+
**Default:** `250`.
901918

902919
For [IPC][] connections, available `options` are:
903920

@@ -1622,6 +1639,7 @@ net.isIPv6('fhqwhgads'); // returns false
16221639

16231640
[IPC]: #ipc-support
16241641
[Identifying paths for IPC connections]: #identifying-paths-for-ipc-connections
1642+
[RFC 8305]: https://www.rfc-editor.org/rfc/rfc8305.txt
16251643
[Readable Stream]: stream.md#class-streamreadable
16261644
[`'close'`]: #event-close
16271645
[`'connect'`]: #event-connect

‎lib/_tls_wrap.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const EE = require('events');
5454
const net = require('net');
5555
const tls = require('tls');
5656
const common = require('_tls_common');
57+
const { kWrapConnectedHandle } = require('internal/net');
5758
const JSStreamSocket = require('internal/js_stream_socket');
5859
const { Buffer } = require('buffer');
5960
let debug = require('internal/util/debuglog').debuglog('tls', (fn) => {
@@ -598,11 +599,10 @@ TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() {
598599
this[kDisableRenegotiation] = true;
599600
};
600601

601-
TLSSocket.prototype._wrapHandle = function(wrap) {
602-
let handle;
603-
604-
if (wrap)
602+
TLSSocket.prototype._wrapHandle = function(wrap, handle) {
603+
if (!handle && wrap) {
605604
handle = wrap._handle;
605+
}
606606

607607
const options = this._tlsOptions;
608608
if (!handle) {
@@ -633,6 +633,16 @@ TLSSocket.prototype._wrapHandle = function(wrap) {
633633
return res;
634634
};
635635

636+
TLSSocket.prototype[kWrapConnectedHandle] = function(handle) {
637+
this._handle = this._wrapHandle(null, handle);
638+
this.ssl = this._handle;
639+
this._init();
640+
641+
if (this._tlsOptions.enableTrace) {
642+
this._handle.enableTrace();
643+
}
644+
};
645+
636646
// This eliminates a cyclic reference to TLSWrap
637647
// Ref: https://github.com/nodejs/node/commit/f7620fb96d339f704932f9bb9a0dceb9952df2d4
638648
function defineHandleReading(socket, handle) {

‎lib/internal/errors.js

+8
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ const aggregateTwoErrors = hideStackFrames((innerError, outerError) => {
168168
return innerError || outerError;
169169
});
170170

171+
const aggregateErrors = hideStackFrames((errors, message, code) => {
172+
// eslint-disable-next-line no-restricted-syntax
173+
const err = new AggregateError(new SafeArrayIterator(errors), message);
174+
err.code = errors[0]?.code;
175+
return err;
176+
});
177+
171178
// Lazily loaded
172179
let util;
173180
let assert;
@@ -893,6 +900,7 @@ function determineSpecificType(value) {
893900
module.exports = {
894901
AbortError,
895902
aggregateTwoErrors,
903+
aggregateErrors,
896904
captureLargerStackTrace,
897905
codes,
898906
connResetException,

‎lib/internal/net.js

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ function makeSyncWrite(fd) {
6767
}
6868

6969
module.exports = {
70+
kWrapConnectedHandle: Symbol('wrapConnectedHandle'),
7071
isIP,
7172
isIPv4,
7273
isIPv6,

‎lib/net.js

+252-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
const {
2525
ArrayIsArray,
2626
ArrayPrototypeIndexOf,
27+
ArrayPrototypePush,
2728
Boolean,
29+
FunctionPrototypeBind,
30+
MathMax,
2831
Number,
2932
NumberIsNaN,
3033
NumberParseInt,
@@ -40,6 +43,7 @@ let debug = require('internal/util/debuglog').debuglog('net', (fn) => {
4043
debug = fn;
4144
});
4245
const {
46+
kWrapConnectedHandle,
4347
isIP,
4448
isIPv4,
4549
isIPv6,
@@ -96,6 +100,7 @@ const {
96100
ERR_SOCKET_CLOSED,
97101
ERR_MISSING_ARGS,
98102
},
103+
aggregateErrors,
99104
errnoException,
100105
exceptionWithHostPort,
101106
genericNodeError,
@@ -105,6 +110,7 @@ const { isUint8Array } = require('internal/util/types');
105110
const { queueMicrotask } = require('internal/process/task_queues');
106111
const {
107112
validateAbortSignal,
113+
validateBoolean,
108114
validateFunction,
109115
validateInt32,
110116
validateNumber,
@@ -123,8 +129,9 @@ let dns;
123129
let BlockList;
124130
let SocketAddress;
125131

126-
const { clearTimeout } = require('timers');
132+
const { clearTimeout, setTimeout } = require('timers');
127133
const { kTimeout } = require('internal/timers');
134+
const kTimeoutTriggered = Symbol('kTimeoutTriggered');
128135

129136
const DEFAULT_IPV4_ADDR = '0.0.0.0';
130137
const DEFAULT_IPV6_ADDR = '::';
@@ -1057,6 +1064,73 @@ function internalConnect(
10571064
}
10581065

10591066

1067+
function internalConnectMultiple(context) {
1068+
clearTimeout(context[kTimeout]);
1069+
const self = context.socket;
1070+
assert(self.connecting);
1071+
1072+
// All connections have been tried without success, destroy with error
1073+
if (context.current === context.addresses.length) {
1074+
self.destroy(aggregateErrors(context.errors));
1075+
return;
1076+
}
1077+
1078+
const { localPort, port, flags } = context;
1079+
const { address, family: addressType } = context.addresses[context.current++];
1080+
const handle = new TCP(TCPConstants.SOCKET);
1081+
let localAddress;
1082+
let err;
1083+
1084+
if (localPort) {
1085+
if (addressType === 4) {
1086+
localAddress = DEFAULT_IPV4_ADDR;
1087+
err = handle.bind(localAddress, localPort);
1088+
} else { // addressType === 6
1089+
localAddress = DEFAULT_IPV6_ADDR;
1090+
err = handle.bind6(localAddress, localPort, flags);
1091+
}
1092+
1093+
debug('connect/multiple: binding to localAddress: %s and localPort: %d (addressType: %d)',
1094+
localAddress, localPort, addressType);
1095+
1096+
err = checkBindError(err, localPort, handle);
1097+
if (err) {
1098+
ArrayPrototypePush(context.errors, exceptionWithHostPort(err, 'bind', localAddress, localPort));
1099+
internalConnectMultiple(context);
1100+
return;
1101+
}
1102+
}
1103+
1104+
const req = new TCPConnectWrap();
1105+
req.oncomplete = FunctionPrototypeBind(afterConnectMultiple, undefined, context);
1106+
req.address = address;
1107+
req.port = port;
1108+
req.localAddress = localAddress;
1109+
req.localPort = localPort;
1110+
1111+
if (addressType === 4) {
1112+
err = handle.connect(req, address, port);
1113+
} else {
1114+
err = handle.connect6(req, address, port);
1115+
}
1116+
1117+
if (err) {
1118+
const sockname = self._getsockname();
1119+
let details;
1120+
1121+
if (sockname) {
1122+
details = sockname.address + ':' + sockname.port;
1123+
}
1124+
1125+
ArrayPrototypePush(context.errors, exceptionWithHostPort(err, 'connect', address, port, details));
1126+
internalConnectMultiple(context);
1127+
return;
1128+
}
1129+
1130+
// If the attempt has not returned an error, start the connection timer
1131+
context[kTimeout] = setTimeout(internalConnectMultipleTimeout, context.timeout, context, req);
1132+
}
1133+
10601134
Socket.prototype.connect = function(...args) {
10611135
let normalized;
10621136
// If passed an array, it's treated as an array of arguments that have
@@ -1126,9 +1200,9 @@ function socketToDnsFamily(family) {
11261200
}
11271201

11281202
function lookupAndConnect(self, options) {
1129-
const { localAddress, localPort } = options;
1203+
const { localAddress, localPort, autoSelectFamily } = options;
11301204
const host = options.host || 'localhost';
1131-
let { port } = options;
1205+
let { port, autoSelectFamilyAttemptTimeout } = options;
11321206

11331207
if (localAddress && !isIP(localAddress)) {
11341208
throw new ERR_INVALID_IP_ADDRESS(localAddress);
@@ -1147,6 +1221,20 @@ function lookupAndConnect(self, options) {
11471221
}
11481222
port |= 0;
11491223

1224+
if (autoSelectFamily !== undefined) {
1225+
validateBoolean(autoSelectFamily);
1226+
}
1227+
1228+
if (autoSelectFamilyAttemptTimeout !== undefined) {
1229+
validateInt32(autoSelectFamilyAttemptTimeout);
1230+
1231+
if (autoSelectFamilyAttemptTimeout < 10) {
1232+
autoSelectFamilyAttemptTimeout = 10;
1233+
}
1234+
} else {
1235+
autoSelectFamilyAttemptTimeout = 250;
1236+
}
1237+
11501238
// If host is an IP, skip performing a lookup
11511239
const addressType = isIP(host);
11521240
if (addressType) {
@@ -1181,6 +1269,26 @@ function lookupAndConnect(self, options) {
11811269
debug('connect: dns options', dnsopts);
11821270
self._host = host;
11831271
const lookup = options.lookup || dns.lookup;
1272+
1273+
if (dnsopts.family !== 4 && dnsopts.family !== 6 && !localAddress && autoSelectFamily) {
1274+
debug('connect: autodetecting');
1275+
1276+
dnsopts.all = true;
1277+
lookupAndConnectMultiple(
1278+
self,
1279+
async_id_symbol,
1280+
lookup,
1281+
host,
1282+
options,
1283+
dnsopts,
1284+
port,
1285+
localPort,
1286+
autoSelectFamilyAttemptTimeout
1287+
);
1288+
1289+
return;
1290+
}
1291+
11841292
defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
11851293
lookup(host, dnsopts, function emitLookup(err, ip, addressType) {
11861294
self.emit('lookup', err, ip, addressType, host);
@@ -1215,6 +1323,86 @@ function lookupAndConnect(self, options) {
12151323
});
12161324
}
12171325

1326+
function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options, dnsopts, port, localPort, timeout) {
1327+
defaultTriggerAsyncIdScope(self[async_id_symbol], function emitLookup() {
1328+
lookup(host, dnsopts, function emitLookup(err, addresses) {
1329+
// It's possible we were destroyed while looking this up.
1330+
// XXX it would be great if we could cancel the promise returned by
1331+
// the look up.
1332+
if (!self.connecting) {
1333+
return;
1334+
} else if (err) {
1335+
// net.createConnection() creates a net.Socket object and immediately
1336+
// calls net.Socket.connect() on it (that's us). There are no event
1337+
// listeners registered yet so defer the error event to the next tick.
1338+
process.nextTick(connectErrorNT, self, err);
1339+
return;
1340+
}
1341+
1342+
// Filter addresses by only keeping the one which are either IPv4 or IPV6.
1343+
// The first valid address determines which group has preference on the
1344+
// alternate family sorting which happens later.
1345+
const validIps = [[], []];
1346+
let destinations;
1347+
for (let i = 0, l = addresses.length; i < l; i++) {
1348+
const address = addresses[i];
1349+
const { address: ip, family: addressType } = address;
1350+
self.emit('lookup', err, ip, addressType, host);
1351+
1352+
if (isIP(ip) && (addressType === 4 || addressType === 6)) {
1353+
if (!destinations) {
1354+
destinations = addressType === 6 ? { 6: 0, 4: 1 } : { 4: 0, 6: 1 };
1355+
}
1356+
1357+
ArrayPrototypePush(validIps[destinations[addressType]], address);
1358+
}
1359+
}
1360+
1361+
// When no AAAA or A records are available, fail on the first one
1362+
if (!validIps[0].length && !validIps[1].length) {
1363+
const { address: firstIp, family: firstAddressType } = addresses[0];
1364+
1365+
if (!isIP(firstIp)) {
1366+
err = new ERR_INVALID_IP_ADDRESS(firstIp);
1367+
process.nextTick(connectErrorNT, self, err);
1368+
} else if (firstAddressType !== 4 && firstAddressType !== 6) {
1369+
err = new ERR_INVALID_ADDRESS_FAMILY(firstAddressType,
1370+
options.host,
1371+
options.port);
1372+
process.nextTick(connectErrorNT, self, err);
1373+
}
1374+
1375+
return;
1376+
}
1377+
1378+
// Sort addresses alternating families
1379+
const toAttempt = [];
1380+
for (let i = 0, l = MathMax(validIps[0].length, validIps[1].length); i < l; i++) {
1381+
if (i in validIps[0]) {
1382+
ArrayPrototypePush(toAttempt, validIps[0][i]);
1383+
}
1384+
if (i in validIps[1]) {
1385+
ArrayPrototypePush(toAttempt, validIps[1][i]);
1386+
}
1387+
}
1388+
1389+
const context = {
1390+
socket: self,
1391+
addresses,
1392+
current: 0,
1393+
port,
1394+
localPort,
1395+
timeout,
1396+
[kTimeout]: null,
1397+
[kTimeoutTriggered]: false,
1398+
errors: [],
1399+
};
1400+
1401+
self._unrefTimer();
1402+
defaultTriggerAsyncIdScope(self[async_id_symbol], internalConnectMultiple, context);
1403+
});
1404+
});
1405+
}
12181406

12191407
function connectErrorNT(self, err) {
12201408
self.destroy(err);
@@ -1309,6 +1497,67 @@ function afterConnect(status, handle, req, readable, writable) {
13091497
}
13101498
}
13111499

1500+
function afterConnectMultiple(context, status, handle, req, readable, writable) {
1501+
const self = context.socket;
1502+
1503+
// Make sure another connection is not spawned
1504+
clearTimeout(context[kTimeout]);
1505+
1506+
// Some error occurred, add to the list of exceptions
1507+
if (status !== 0) {
1508+
let details;
1509+
if (req.localAddress && req.localPort) {
1510+
details = req.localAddress + ':' + req.localPort;
1511+
}
1512+
const ex = exceptionWithHostPort(status,
1513+
'connect',
1514+
req.address,
1515+
req.port,
1516+
details);
1517+
if (details) {
1518+
ex.localAddress = req.localAddress;
1519+
ex.localPort = req.localPort;
1520+
}
1521+
1522+
ArrayPrototypePush(context.errors, ex);
1523+
1524+
// Try the next address
1525+
internalConnectMultiple(context);
1526+
return;
1527+
}
1528+
1529+
// One of the connection has completed and correctly dispatched but after timeout, ignore this one
1530+
if (context[kTimeoutTriggered]) {
1531+
debug('connect/multiple: ignoring successful but timedout connection to %s:%s', req.address, req.port);
1532+
handle.close();
1533+
return;
1534+
}
1535+
1536+
// Perform initialization sequence on the handle, then move on with the regular callback
1537+
self._handle = handle;
1538+
initSocketHandle(self);
1539+
1540+
if (self[kWrapConnectedHandle]) {
1541+
self[kWrapConnectedHandle](handle);
1542+
initSocketHandle(self); // This is called again to initialize the TLSWrap
1543+
}
1544+
1545+
if (hasObserver('net')) {
1546+
startPerf(
1547+
self,
1548+
kPerfHooksNetConnectContext,
1549+
{ type: 'net', name: 'connect', detail: { host: req.address, port: req.port } }
1550+
);
1551+
}
1552+
1553+
afterConnect(status, handle, req, readable, writable);
1554+
}
1555+
1556+
function internalConnectMultipleTimeout(context, req) {
1557+
context[kTimeoutTriggered] = true;
1558+
internalConnectMultiple(context);
1559+
}
1560+
13121561
function addAbortSignalOption(self, options) {
13131562
if (options?.signal === undefined) {
13141563
return;
+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
5+
6+
const assert = require('assert');
7+
const dgram = require('dgram');
8+
const { Resolver } = require('dns');
9+
const { request, createServer } = require('http');
10+
11+
// Test that happy eyeballs algorithm is properly implemented when using HTTP.
12+
13+
let autoSelectFamilyAttemptTimeout = common.platformTimeout(250);
14+
if (common.isWindows) {
15+
// Some of the windows machines in the CI need more time to establish connection
16+
autoSelectFamilyAttemptTimeout = common.platformTimeout(1500);
17+
}
18+
19+
function _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+
39+
function createDnsServer(ipv6Addr, ipv4Addr, cb) {
40+
// Create a DNS server which replies with a AAAA and a A record for the same host
41+
const socket = dgram.createSocket('udp4');
42+
43+
socket.on('message', common.mustCall((msg, { address, port }) => {
44+
const parsed = parseDNSPacket(msg);
45+
const domain = parsed.questions[0].domain;
46+
assert.strictEqual(domain, 'example.org');
47+
48+
socket.send(writeDNSPacket({
49+
id: parsed.id,
50+
questions: parsed.questions,
51+
answers: [
52+
{ type: 'AAAA', address: ipv6Addr, ttl: 123, domain: 'example.org' },
53+
{ type: 'A', address: ipv4Addr, ttl: 123, domain: 'example.org' },
54+
]
55+
}), port, address);
56+
}));
57+
58+
socket.bind(0, () => {
59+
const resolver = new Resolver();
60+
resolver.setServers([`127.0.0.1:${socket.address().port}`]);
61+
62+
cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
63+
});
64+
}
65+
66+
// Test that IPV4 is reached if IPV6 is not reachable
67+
{
68+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
69+
const ipv4Server = createServer(common.mustCall((_, res) => {
70+
res.writeHead(200, { Connection: 'close' });
71+
res.end('response-ipv4');
72+
}));
73+
74+
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
75+
request(
76+
`http://example.org:${ipv4Server.address().port}/`,
77+
{
78+
lookup,
79+
autoSelectFamily: true,
80+
autoSelectFamilyAttemptTimeout
81+
},
82+
(res) => {
83+
assert.strictEqual(res.statusCode, 200);
84+
res.setEncoding('utf-8');
85+
86+
let response = '';
87+
88+
res.on('data', (chunk) => {
89+
response += chunk;
90+
});
91+
92+
res.on('end', common.mustCall(() => {
93+
assert.strictEqual(response, 'response-ipv4');
94+
ipv4Server.close();
95+
dnsServer.close();
96+
}));
97+
}
98+
).end();
99+
}));
100+
}));
101+
}
102+
103+
// Test that IPV4 is NOT reached if IPV6 is reachable
104+
if (common.hasIPv6) {
105+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
106+
const ipv4Server = createServer(common.mustNotCall((_, res) => {
107+
res.writeHead(200, { Connection: 'close' });
108+
res.end('response-ipv4');
109+
}));
110+
111+
const ipv6Server = createServer(common.mustCall((_, res) => {
112+
res.writeHead(200, { Connection: 'close' });
113+
res.end('response-ipv6');
114+
}));
115+
116+
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
117+
const port = ipv4Server.address().port;
118+
119+
ipv6Server.listen(port, '::1', common.mustCall(() => {
120+
request(
121+
`http://example.org:${ipv4Server.address().port}/`,
122+
{
123+
lookup,
124+
autoSelectFamily: true,
125+
autoSelectFamilyAttemptTimeout,
126+
},
127+
(res) => {
128+
assert.strictEqual(res.statusCode, 200);
129+
res.setEncoding('utf-8');
130+
131+
let response = '';
132+
133+
res.on('data', (chunk) => {
134+
response += chunk;
135+
});
136+
137+
res.on('end', common.mustCall(() => {
138+
assert.strictEqual(response, 'response-ipv6');
139+
ipv4Server.close();
140+
ipv6Server.close();
141+
dnsServer.close();
142+
}));
143+
}
144+
).end();
145+
}));
146+
}));
147+
}));
148+
}
+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto) {
6+
common.skip('missing crypto');
7+
}
8+
9+
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
10+
const fixtures = require('../common/fixtures');
11+
12+
const assert = require('assert');
13+
const dgram = require('dgram');
14+
const { Resolver } = require('dns');
15+
const { request, createServer } = require('https');
16+
17+
if (!common.hasCrypto)
18+
common.skip('missing crypto');
19+
20+
const options = {
21+
key: fixtures.readKey('agent1-key.pem'),
22+
cert: fixtures.readKey('agent1-cert.pem')
23+
};
24+
25+
// Test that happy eyeballs algorithm is properly implemented when using HTTP.
26+
27+
let autoSelectFamilyAttemptTimeout = common.platformTimeout(250);
28+
if (common.isWindows) {
29+
// Some of the windows machines in the CI need more time to establish connection
30+
autoSelectFamilyAttemptTimeout = common.platformTimeout(1500);
31+
}
32+
33+
function _lookup(resolver, hostname, options, cb) {
34+
resolver.resolve(hostname, 'ANY', (err, replies) => {
35+
assert.notStrictEqual(options.family, 4);
36+
37+
if (err) {
38+
return cb(err);
39+
}
40+
41+
const hosts = replies
42+
.map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 }))
43+
.sort((a, b) => b.family - a.family);
44+
45+
if (options.all === true) {
46+
return cb(null, hosts);
47+
}
48+
49+
return cb(null, hosts[0].address, hosts[0].family);
50+
});
51+
}
52+
53+
function createDnsServer(ipv6Addr, ipv4Addr, cb) {
54+
// Create a DNS server which replies with a AAAA and a A record for the same host
55+
const socket = dgram.createSocket('udp4');
56+
57+
socket.on('message', common.mustCall((msg, { address, port }) => {
58+
const parsed = parseDNSPacket(msg);
59+
const domain = parsed.questions[0].domain;
60+
assert.strictEqual(domain, 'example.org');
61+
62+
socket.send(writeDNSPacket({
63+
id: parsed.id,
64+
questions: parsed.questions,
65+
answers: [
66+
{ type: 'AAAA', address: ipv6Addr, ttl: 123, domain: 'example.org' },
67+
{ type: 'A', address: ipv4Addr, ttl: 123, domain: 'example.org' },
68+
]
69+
}), port, address);
70+
}));
71+
72+
socket.bind(0, () => {
73+
const resolver = new Resolver();
74+
resolver.setServers([`127.0.0.1:${socket.address().port}`]);
75+
76+
cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
77+
});
78+
}
79+
80+
// Test that IPV4 is reached if IPV6 is not reachable
81+
{
82+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
83+
const ipv4Server = createServer(options, common.mustCall((_, res) => {
84+
res.writeHead(200, { Connection: 'close' });
85+
res.end('response-ipv4');
86+
}));
87+
88+
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
89+
request(
90+
`https://example.org:${ipv4Server.address().port}/`,
91+
{
92+
lookup,
93+
rejectUnauthorized: false,
94+
autoSelectFamily: true,
95+
autoSelectFamilyAttemptTimeout
96+
},
97+
(res) => {
98+
assert.strictEqual(res.statusCode, 200);
99+
res.setEncoding('utf-8');
100+
101+
let response = '';
102+
103+
res.on('data', (chunk) => {
104+
response += chunk;
105+
});
106+
107+
res.on('end', common.mustCall(() => {
108+
assert.strictEqual(response, 'response-ipv4');
109+
ipv4Server.close();
110+
dnsServer.close();
111+
}));
112+
}
113+
).end();
114+
}));
115+
}));
116+
}
117+
118+
// Test that IPV4 is NOT reached if IPV6 is reachable
119+
if (common.hasIPv6) {
120+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
121+
const ipv4Server = createServer(options, common.mustNotCall((_, res) => {
122+
res.writeHead(200, { Connection: 'close' });
123+
res.end('response-ipv4');
124+
}));
125+
126+
const ipv6Server = createServer(options, common.mustCall((_, res) => {
127+
res.writeHead(200, { Connection: 'close' });
128+
res.end('response-ipv6');
129+
}));
130+
131+
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
132+
const port = ipv4Server.address().port;
133+
134+
ipv6Server.listen(port, '::1', common.mustCall(() => {
135+
request(
136+
`https://example.org:${ipv4Server.address().port}/`,
137+
{
138+
lookup,
139+
rejectUnauthorized: false,
140+
autoSelectFamily: true,
141+
autoSelectFamilyAttemptTimeout,
142+
},
143+
(res) => {
144+
assert.strictEqual(res.statusCode, 200);
145+
res.setEncoding('utf-8');
146+
147+
let response = '';
148+
149+
res.on('data', (chunk) => {
150+
response += chunk;
151+
});
152+
153+
res.on('end', common.mustCall(() => {
154+
assert.strictEqual(response, 'response-ipv6');
155+
ipv4Server.close();
156+
ipv6Server.close();
157+
dnsServer.close();
158+
}));
159+
}
160+
).end();
161+
}));
162+
}));
163+
}));
164+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
5+
6+
const assert = require('assert');
7+
const dgram = require('dgram');
8+
const { Resolver } = require('dns');
9+
const { createConnection, createServer } = require('net');
10+
11+
// Test that happy eyeballs algorithm is properly implemented when a A record is returned first.
12+
13+
let autoSelectFamilyAttemptTimeout = common.platformTimeout(250);
14+
if (common.isWindows) {
15+
// Some of the windows machines in the CI need more time to establish connection
16+
autoSelectFamilyAttemptTimeout = common.platformTimeout(1500);
17+
}
18+
19+
function _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+
30+
if (options.all === true) {
31+
return cb(null, hosts);
32+
}
33+
34+
return cb(null, hosts[0].address, hosts[0].family);
35+
});
36+
}
37+
38+
function createDnsServer(ipv6Addr, ipv4Addr, cb) {
39+
// Create a DNS server which replies with a AAAA and a A record for the same host
40+
const socket = dgram.createSocket('udp4');
41+
42+
socket.on('message', common.mustCall((msg, { address, port }) => {
43+
const parsed = parseDNSPacket(msg);
44+
const domain = parsed.questions[0].domain;
45+
assert.strictEqual(domain, 'example.org');
46+
47+
socket.send(writeDNSPacket({
48+
id: parsed.id,
49+
questions: parsed.questions,
50+
answers: [
51+
{ type: 'A', address: ipv4Addr, ttl: 123, domain: 'example.org' },
52+
{ type: 'AAAA', address: ipv6Addr, ttl: 123, domain: 'example.org' },
53+
]
54+
}), port, address);
55+
}));
56+
57+
socket.bind(0, () => {
58+
const resolver = new Resolver();
59+
resolver.setServers([`127.0.0.1:${socket.address().port}`]);
60+
61+
cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
62+
});
63+
}
64+
65+
// Test that IPV6 is NOT reached if IPV4 is sorted first
66+
if (common.hasIPv6) {
67+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
68+
const ipv4Server = createServer((socket) => {
69+
socket.on('data', common.mustCall(() => {
70+
socket.write('response-ipv4');
71+
socket.end();
72+
}));
73+
});
74+
75+
const ipv6Server = createServer((socket) => {
76+
socket.on('data', common.mustNotCall(() => {
77+
socket.write('response-ipv6');
78+
socket.end();
79+
}));
80+
});
81+
82+
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
83+
const port = ipv4Server.address().port;
84+
85+
ipv6Server.listen(port, '::1', common.mustCall(() => {
86+
const connection = createConnection({
87+
host: 'example.org',
88+
port,
89+
lookup,
90+
autoSelectFamily: true,
91+
autoSelectFamilyAttemptTimeout
92+
});
93+
94+
let response = '';
95+
connection.setEncoding('utf-8');
96+
97+
connection.on('data', (chunk) => {
98+
response += chunk;
99+
});
100+
101+
connection.on('end', common.mustCall(() => {
102+
assert.strictEqual(response, 'response-ipv4');
103+
ipv4Server.close();
104+
ipv6Server.close();
105+
dnsServer.close();
106+
}));
107+
108+
connection.write('request');
109+
}));
110+
}));
111+
}));
112+
}
+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
5+
6+
const assert = require('assert');
7+
const dgram = require('dgram');
8+
const { Resolver } = require('dns');
9+
const { createConnection, createServer } = require('net');
10+
11+
// Test that happy eyeballs algorithm is properly implemented.
12+
13+
let autoSelectFamilyAttemptTimeout = common.platformTimeout(250);
14+
if (common.isWindows) {
15+
// Some of the windows machines in the CI need more time to establish connection
16+
autoSelectFamilyAttemptTimeout = common.platformTimeout(1500);
17+
}
18+
19+
function _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+
39+
function createDnsServer(ipv6Addr, ipv4Addr, cb) {
40+
// Create a DNS server which replies with a AAAA and a A record for the same host
41+
const socket = dgram.createSocket('udp4');
42+
43+
socket.on('message', common.mustCall((msg, { address, port }) => {
44+
const parsed = parseDNSPacket(msg);
45+
const domain = parsed.questions[0].domain;
46+
assert.strictEqual(domain, 'example.org');
47+
48+
socket.send(writeDNSPacket({
49+
id: parsed.id,
50+
questions: parsed.questions,
51+
answers: [
52+
{ type: 'AAAA', address: ipv6Addr, ttl: 123, domain: 'example.org' },
53+
{ type: 'A', address: ipv4Addr, ttl: 123, domain: 'example.org' },
54+
]
55+
}), port, address);
56+
}));
57+
58+
socket.bind(0, () => {
59+
const resolver = new Resolver();
60+
resolver.setServers([`127.0.0.1:${socket.address().port}`]);
61+
62+
cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
63+
});
64+
}
65+
66+
// Test that IPV4 is reached if IPV6 is not reachable
67+
{
68+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
69+
const ipv4Server = createServer((socket) => {
70+
socket.on('data', common.mustCall(() => {
71+
socket.write('response-ipv4');
72+
socket.end();
73+
}));
74+
});
75+
76+
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
77+
const connection = createConnection({
78+
host: 'example.org',
79+
port: ipv4Server.address().port,
80+
lookup,
81+
autoSelectFamily: true,
82+
autoSelectFamilyAttemptTimeout,
83+
});
84+
85+
let response = '';
86+
connection.setEncoding('utf-8');
87+
88+
connection.on('data', (chunk) => {
89+
response += chunk;
90+
});
91+
92+
connection.on('end', common.mustCall(() => {
93+
assert.strictEqual(response, 'response-ipv4');
94+
ipv4Server.close();
95+
dnsServer.close();
96+
}));
97+
98+
connection.write('request');
99+
}));
100+
}));
101+
}
102+
103+
// Test that IPV4 is NOT reached if IPV6 is reachable
104+
if (common.hasIPv6) {
105+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
106+
const ipv4Server = createServer((socket) => {
107+
socket.on('data', common.mustNotCall(() => {
108+
socket.write('response-ipv4');
109+
socket.end();
110+
}));
111+
});
112+
113+
const ipv6Server = createServer((socket) => {
114+
socket.on('data', common.mustCall(() => {
115+
socket.write('response-ipv6');
116+
socket.end();
117+
}));
118+
});
119+
120+
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
121+
const port = ipv4Server.address().port;
122+
123+
ipv6Server.listen(port, '::1', common.mustCall(() => {
124+
const connection = createConnection({
125+
host: 'example.org',
126+
port,
127+
lookup,
128+
autoSelectFamily: true,
129+
autoSelectFamilyAttemptTimeout,
130+
});
131+
132+
let response = '';
133+
connection.setEncoding('utf-8');
134+
135+
connection.on('data', (chunk) => {
136+
response += chunk;
137+
});
138+
139+
connection.on('end', common.mustCall(() => {
140+
assert.strictEqual(response, 'response-ipv6');
141+
ipv4Server.close();
142+
ipv6Server.close();
143+
dnsServer.close();
144+
}));
145+
146+
connection.write('request');
147+
}));
148+
}));
149+
}));
150+
}
151+
152+
// Test that when all errors are returned when no connections succeeded
153+
{
154+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
155+
const connection = createConnection({
156+
host: 'example.org',
157+
port: 10,
158+
lookup,
159+
autoSelectFamily: true,
160+
autoSelectFamilyAttemptTimeout,
161+
});
162+
163+
connection.on('ready', common.mustNotCall());
164+
connection.on('error', common.mustCall((error) => {
165+
assert.strictEqual(error.constructor.name, 'AggregateError');
166+
assert.strictEqual(error.errors.length, 2);
167+
168+
const errors = error.errors.map((e) => e.message);
169+
assert.ok(errors.includes('connect ECONNREFUSED 127.0.0.1:10'));
170+
171+
if (common.hasIPv6) {
172+
assert.ok(errors.includes('connect ECONNREFUSED ::1:10'));
173+
}
174+
175+
dnsServer.close();
176+
}));
177+
}));
178+
}
179+
180+
// Test that the option can be disabled
181+
{
182+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
183+
const ipv4Server = createServer((socket) => {
184+
socket.on('data', common.mustCall(() => {
185+
socket.write('response-ipv4');
186+
socket.end();
187+
}));
188+
});
189+
190+
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
191+
const port = ipv4Server.address().port;
192+
193+
const connection = createConnection({
194+
host: 'example.org',
195+
port,
196+
lookup,
197+
autoSelectFamily: false,
198+
});
199+
200+
connection.on('ready', common.mustNotCall());
201+
connection.on('error', common.mustCall((error) => {
202+
if (common.hasIPv6) {
203+
assert.strictEqual(error.code, 'ECONNREFUSED');
204+
assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`);
205+
} else {
206+
assert.strictEqual(error.code, 'EADDRNOTAVAIL');
207+
assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
208+
}
209+
210+
ipv4Server.close();
211+
dnsServer.close();
212+
}));
213+
}));
214+
}));
215+
}

0 commit comments

Comments
 (0)
Please sign in to comment.