Skip to content

Commit 617860b

Browse files
committed
net: add autoDetectFamily option
1 parent e213dea commit 617860b

File tree

191 files changed

+1105
-23
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

191 files changed

+1105
-23
lines changed

doc/api/net.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,18 @@ is received. For example, it is passed to the listeners of a
620620
[`'connection'`][] event emitted on a [`net.Server`][], so the user can use
621621
it to interact with the client.
622622

623+
### Class property: `net.Socket.autoDetectFamily`
624+
625+
<!-- YAML
626+
added: REPLACEME
627+
-->
628+
629+
* {boolean} **Default:** `true`
630+
631+
The default value for the `autoDetectFamily` option for new
632+
[`socket.connect(options)`][] calls. If set to true it enables the Happy
633+
Eyeballs connection algorithm. This value may be modified.
634+
623635
### `new net.Socket([options])`
624636

625637
<!-- YAML
@@ -856,6 +868,10 @@ behavior.
856868
<!-- YAML
857869
added: v0.1.90
858870
changes:
871+
- version: REPLACEME
872+
pr-url: https://github.com/nodejs/node/pull/44731
873+
description: Added the `autoDetectFamily` option, which enables the Happy
874+
Eyeballs algorithm for dualstack connections.
859875
- version:
860876
- v17.7.0
861877
- v16.15.0
@@ -889,6 +905,7 @@ For TCP connections, available `options` are:
889905
* `port` {number} Required. Port the socket should connect to.
890906
* `host` {string} Host the socket should connect to. **Default:** `'localhost'`.
891907
* `localAddress` {string} Local address the socket should connect from.
908+
This is ignored if `autoDetectFamily` is set to `true`.
892909
* `localPort` {number} Local port the socket should connect from.
893910
* `family` {number}: Version of IP stack. Must be `4`, `6`, or `0`. The value
894911
`0` indicates that both IPv4 and IPv6 addresses are allowed. **Default:** `0`.
@@ -902,6 +919,13 @@ For TCP connections, available `options` are:
902919
**Default:** `false`.
903920
* `keepAliveInitialDelay` {number} If set to a positive number, it sets the initial delay before
904921
the first keepalive probe is sent on an idle socket.**Default:** `0`.
922+
* `autoDetectFamily` {boolean}: Enables the Happy Eyeballs connection algorithm.
923+
The `all` option passed to lookup is set to `true` and the sockets attempts to
924+
connect to all returned AAAA and A records at the same time, keeping only
925+
the first successful connection and disconnecting all the other ones.
926+
Connection errors are not emitted if at least a connection succeeds.
927+
Ignored if the `family` option is not `0`.
928+
**Default:** The value of [`net.Socket.autoDetectFamily`][].
905929

906930
For [IPC][] connections, available `options` are:
907931

@@ -1643,6 +1667,7 @@ net.isIPv6('fhqwhgads'); // returns false
16431667
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
16441668
[`dns.lookup()` hints]: dns.md#supported-getaddrinfo-flags
16451669
[`net.Server`]: #class-netserver
1670+
[`net.Socket.autoDetectFamily`]: #class-property-netsocketautodetectfamily
16461671
[`net.Socket`]: #class-netsocket
16471672
[`net.connect()`]: #netconnect
16481673
[`net.connect(options)`]: #netconnectoptions-connectlistener

lib/_tls_wrap.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -598,11 +598,10 @@ TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() {
598598
this[kDisableRenegotiation] = true;
599599
};
600600

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

607606
const options = this._tlsOptions;
608607
if (!handle) {
@@ -633,6 +632,16 @@ TLSSocket.prototype._wrapHandle = function(wrap) {
633632
return res;
634633
};
635634

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

lib/internal/errors.js

Lines changed: 8 additions & 0 deletions
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/net.js

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
const {
2525
ArrayIsArray,
2626
ArrayPrototypeIndexOf,
27+
ArrayPrototypePush,
2728
Boolean,
29+
FunctionPrototypeBind,
2830
Number,
2931
NumberIsNaN,
3032
NumberParseInt,
@@ -96,6 +98,7 @@ const {
9698
ERR_SOCKET_CLOSED,
9799
ERR_MISSING_ARGS,
98100
},
101+
aggregateErrors,
99102
errnoException,
100103
exceptionWithHostPort,
101104
genericNodeError,
@@ -458,6 +461,8 @@ function Socket(options) {
458461
ObjectSetPrototypeOf(Socket.prototype, stream.Duplex.prototype);
459462
ObjectSetPrototypeOf(Socket, stream.Duplex);
460463

464+
Socket.autoDetectFamily = true;
465+
461466
// Refresh existing timeouts.
462467
Socket.prototype._unrefTimer = function _unrefTimer() {
463468
for (let s = this; s !== null; s = s._parent) {
@@ -1042,6 +1047,81 @@ function internalConnect(
10421047
}
10431048

10441049

1050+
function internalConnectMultiple(
1051+
self, addresses, port, localPort, flags
1052+
) {
1053+
assert(self.connecting);
1054+
1055+
const context = {
1056+
errors: [],
1057+
connecting: 0,
1058+
completed: false,
1059+
};
1060+
1061+
const oncomplete = FunctionPrototypeBind(afterConnectMultiple, self, context);
1062+
1063+
for (let i = 0, l = addresses.length; i < l; i++) {
1064+
if (!addresses[i]) {
1065+
continue;
1066+
}
1067+
1068+
const { address, family: addressType } = addresses[i];
1069+
const handle = new TCP(TCPConstants.SOCKET);
1070+
1071+
let localAddress;
1072+
let err;
1073+
1074+
if (localPort) {
1075+
if (addressType === 4) {
1076+
localAddress = DEFAULT_IPV4_ADDR;
1077+
err = handle.bind(localAddress, localPort);
1078+
} else { // addressType === 6
1079+
localAddress = DEFAULT_IPV6_ADDR;
1080+
err = handle.bind6(localAddress, localPort, flags);
1081+
}
1082+
1083+
debug('connect/happy eyeballs: binding to localAddress: %s and localPort: %d (addressType: %d)',
1084+
localAddress, localPort, addressType);
1085+
1086+
err = checkBindError(err, localPort, handle);
1087+
if (err) {
1088+
ArrayPrototypePush(context.errors, exceptionWithHostPort(err, 'bind', localAddress, localPort));
1089+
continue;
1090+
}
1091+
}
1092+
1093+
const req = new TCPConnectWrap();
1094+
req.oncomplete = oncomplete;
1095+
req.address = address;
1096+
req.port = port;
1097+
req.localAddress = localAddress;
1098+
req.localPort = localPort;
1099+
1100+
if (addressType === 4) {
1101+
err = handle.connect(req, address, port);
1102+
} else {
1103+
err = handle.connect6(req, address, port);
1104+
}
1105+
1106+
if (err) {
1107+
const sockname = self._getsockname();
1108+
let details;
1109+
1110+
if (sockname) {
1111+
details = sockname.address + ':' + sockname.port;
1112+
}
1113+
1114+
ArrayPrototypePush(context.errors, exceptionWithHostPort(err, 'connect', address, port, details));
1115+
} else {
1116+
context.connecting++;
1117+
}
1118+
}
1119+
1120+
if (context.errors.length && context.connecting === 0) {
1121+
self.destroy(aggregateErrors(context.error));
1122+
}
1123+
}
1124+
10451125
Socket.prototype.connect = function(...args) {
10461126
let normalized;
10471127
// If passed an array, it's treated as an array of arguments that have
@@ -1113,6 +1193,7 @@ function socketToDnsFamily(family) {
11131193
function lookupAndConnect(self, options) {
11141194
const { localAddress, localPort } = options;
11151195
const host = options.host || 'localhost';
1196+
const autoDetectFamily = options.autoDetectFamily ?? Socket.autoDetectFamily;
11161197
let { port } = options;
11171198

11181199
if (localAddress && !isIP(localAddress)) {
@@ -1166,6 +1247,79 @@ function lookupAndConnect(self, options) {
11661247
debug('connect: dns options', dnsopts);
11671248
self._host = host;
11681249
const lookup = options.lookup || dns.lookup;
1250+
1251+
if (dnsopts.family !== 4 && dnsopts.family !== 6 && autoDetectFamily) {
1252+
debug('connect: autodetecting family via happy eyeballs');
1253+
1254+
dnsopts.all = true;
1255+
1256+
defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
1257+
lookup(host, dnsopts, function emitLookup(err, addresses) {
1258+
// It's possible we were destroyed while looking this up.
1259+
// XXX it would be great if we could cancel the promise returned by
1260+
// the look up.
1261+
if (!self.connecting) {
1262+
return;
1263+
} else if (err) {
1264+
// net.createConnection() creates a net.Socket object and immediately
1265+
// calls net.Socket.connect() on it (that's us). There are no event
1266+
// listeners registered yet so defer the error event to the next tick.
1267+
process.nextTick(connectErrorNT, self, err);
1268+
return;
1269+
}
1270+
1271+
// This array will contain two elements at most, the first is a AAAA record, the second a A record
1272+
let ipv4Address;
1273+
let ipv6Address;
1274+
1275+
// Gather all the addresses we can use for happy eyeballs
1276+
for (let i = 0, l = addresses.length; i < l; i++) {
1277+
const address = addresses[i];
1278+
const { address: ip, family: addressType } = address;
1279+
self.emit('lookup', err, ip, addressType, host);
1280+
1281+
if (isIP(ip)) {
1282+
if (addressType === 6 && !ipv6Address) {
1283+
ipv6Address = address;
1284+
} else if (addressType === 4 && !ipv4Address) {
1285+
ipv4Address = address;
1286+
}
1287+
}
1288+
1289+
if (ipv6Address && ipv4Address) {
1290+
break;
1291+
}
1292+
}
1293+
1294+
// When no AAAA or A records are available, fail on the first one
1295+
if (!ipv6Address && !ipv4Address) {
1296+
const { address: firstIp, family: firstAddressType } = addresses[0];
1297+
1298+
if (!isIP(firstIp)) {
1299+
err = new ERR_INVALID_IP_ADDRESS(firstIp);
1300+
process.nextTick(connectErrorNT, self, err);
1301+
} else if (firstAddressType !== 4 && firstAddressType !== 6) {
1302+
err = new ERR_INVALID_ADDRESS_FAMILY(firstAddressType,
1303+
options.host,
1304+
options.port);
1305+
process.nextTick(connectErrorNT, self, err);
1306+
}
1307+
1308+
return;
1309+
}
1310+
1311+
self._unrefTimer();
1312+
defaultTriggerAsyncIdScope(
1313+
self[async_id_symbol],
1314+
internalConnectMultiple,
1315+
self, [ipv6Address, ipv4Address], port, localPort
1316+
);
1317+
});
1318+
});
1319+
1320+
return;
1321+
}
1322+
11691323
defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
11701324
lookup(host, dnsopts, function emitLookup(err, ip, addressType) {
11711325
self.emit('lookup', err, ip, addressType, host);
@@ -1294,6 +1448,62 @@ function afterConnect(status, handle, req, readable, writable) {
12941448
}
12951449
}
12961450

1451+
function afterConnectMultiple(context, status, handle, req, readable, writable) {
1452+
context.connecting--;
1453+
1454+
// Some error occurred, add to the list of exceptions
1455+
if (status !== 0) {
1456+
let details;
1457+
if (req.localAddress && req.localPort) {
1458+
details = req.localAddress + ':' + req.localPort;
1459+
}
1460+
const ex = exceptionWithHostPort(status,
1461+
'connect',
1462+
req.address,
1463+
req.port,
1464+
details);
1465+
if (details) {
1466+
ex.localAddress = req.localAddress;
1467+
ex.localPort = req.localPort;
1468+
}
1469+
1470+
ArrayPrototypePush(context.errors, ex);
1471+
1472+
if (context.connecting === 0) {
1473+
this.destroy(aggregateErrors(context.errors));
1474+
}
1475+
1476+
return;
1477+
}
1478+
1479+
// One of the connection has completed and correctly dispatched, ignore this one
1480+
if (context.completed) {
1481+
debug('connect/happy eyeballs: ignoring successful connection to %s:%s', req.address, req.port);
1482+
handle.close();
1483+
return;
1484+
}
1485+
1486+
// Mark the connection as successful
1487+
context.completed = true;
1488+
this._handle = handle;
1489+
initSocketHandle(this);
1490+
1491+
if (this.encrypted) {
1492+
this._wrapConnectedHandle(handle);
1493+
initSocketHandle(this); // This is called again to initialize the TLSWrap
1494+
}
1495+
1496+
if (hasObserver('net')) {
1497+
startPerf(
1498+
this,
1499+
kPerfHooksNetConnectContext,
1500+
{ type: 'net', name: 'connect', detail: { host: req.address, port: req.port } }
1501+
);
1502+
}
1503+
1504+
afterConnect(status, handle, req, readable, writable);
1505+
}
1506+
12971507
function addAbortSignalOption(self, options) {
12981508
if (options?.signal === undefined) {
12991509
return;

test/async-hooks/test-async-local-storage-socket.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
'use strict';
22

3-
require('../common');
3+
const common = require('../common');
44

55
// Regression tests for https://github.com/nodejs/node/issues/40693
66

77
const assert = require('assert');
88
const net = require('net');
99
const { AsyncLocalStorage } = require('async_hooks');
1010

11+
common.setHappyEyeballsEnabled(false);
12+
1113
net
1214
.createServer((socket) => {
1315
socket.write('Hello, world!');

test/async-hooks/test-graph.shutdown.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const initHooks = require('./init-hooks');
88
const verifyGraph = require('./verify-graph');
99
const net = require('net');
1010

11+
common.setHappyEyeballsEnabled(false);
12+
1113
const hooks = initHooks();
1214
hooks.enable();
1315

0 commit comments

Comments
 (0)