Skip to content

Commit

Permalink
net: add autoDetectFamily option
Browse files Browse the repository at this point in the history
  • Loading branch information
ShogunPanda committed Sep 23, 2022
1 parent e213dea commit 617860b
Show file tree
Hide file tree
Showing 191 changed files with 1,105 additions and 23 deletions.
25 changes: 25 additions & 0 deletions doc/api/net.md
Expand Up @@ -620,6 +620,18 @@ is received. For example, it is passed to the listeners of a
[`'connection'`][] event emitted on a [`net.Server`][], so the user can use
it to interact with the client.

### Class property: `net.Socket.autoDetectFamily`

<!-- YAML
added: REPLACEME
-->

* {boolean} **Default:** `true`

The default value for the `autoDetectFamily` option for new
[`socket.connect(options)`][] calls. If set to true it enables the Happy
Eyeballs connection algorithm. This value may be modified.

### `new net.Socket([options])`

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

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

Expand Down Expand Up @@ -1643,6 +1667,7 @@ net.isIPv6('fhqwhgads'); // returns false
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
[`dns.lookup()` hints]: dns.md#supported-getaddrinfo-flags
[`net.Server`]: #class-netserver
[`net.Socket.autoDetectFamily`]: #class-property-netsocketautodetectfamily
[`net.Socket`]: #class-netsocket
[`net.connect()`]: #netconnect
[`net.connect(options)`]: #netconnectoptions-connectlistener
Expand Down
17 changes: 13 additions & 4 deletions lib/_tls_wrap.js
Expand Up @@ -598,11 +598,10 @@ TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() {
this[kDisableRenegotiation] = true;
};

TLSSocket.prototype._wrapHandle = function(wrap) {
let handle;

if (wrap)
TLSSocket.prototype._wrapHandle = function(wrap, handle) {
if (!handle && wrap) {
handle = wrap._handle;
}

const options = this._tlsOptions;
if (!handle) {
Expand Down Expand Up @@ -633,6 +632,16 @@ TLSSocket.prototype._wrapHandle = function(wrap) {
return res;
};

TLSSocket.prototype._wrapConnectedHandle = function(handle) {
this._handle = this._wrapHandle(null, handle);
this.ssl = this._handle;
this._init();

if (this._tlsOptions.enableTrace) {
this._handle.enableTrace();
}
};

// This eliminates a cyclic reference to TLSWrap
// Ref: https://github.com/nodejs/node/commit/f7620fb96d339f704932f9bb9a0dceb9952df2d4
function defineHandleReading(socket, handle) {
Expand Down
8 changes: 8 additions & 0 deletions lib/internal/errors.js
Expand Up @@ -168,6 +168,13 @@ const aggregateTwoErrors = hideStackFrames((innerError, outerError) => {
return innerError || outerError;
});

const aggregateErrors = hideStackFrames((errors, message, code) => {
// eslint-disable-next-line no-restricted-syntax
const err = new AggregateError(new SafeArrayIterator(errors), message);
err.code = errors[0]?.code;
return err;
});

// Lazily loaded
let util;
let assert;
Expand Down Expand Up @@ -893,6 +900,7 @@ function determineSpecificType(value) {
module.exports = {
AbortError,
aggregateTwoErrors,
aggregateErrors,
captureLargerStackTrace,
codes,
connResetException,
Expand Down
210 changes: 210 additions & 0 deletions lib/net.js
Expand Up @@ -24,7 +24,9 @@
const {
ArrayIsArray,
ArrayPrototypeIndexOf,
ArrayPrototypePush,
Boolean,
FunctionPrototypeBind,
Number,
NumberIsNaN,
NumberParseInt,
Expand Down Expand Up @@ -96,6 +98,7 @@ const {
ERR_SOCKET_CLOSED,
ERR_MISSING_ARGS,
},
aggregateErrors,
errnoException,
exceptionWithHostPort,
genericNodeError,
Expand Down Expand Up @@ -458,6 +461,8 @@ function Socket(options) {
ObjectSetPrototypeOf(Socket.prototype, stream.Duplex.prototype);
ObjectSetPrototypeOf(Socket, stream.Duplex);

Socket.autoDetectFamily = true;

// Refresh existing timeouts.
Socket.prototype._unrefTimer = function _unrefTimer() {
for (let s = this; s !== null; s = s._parent) {
Expand Down Expand Up @@ -1042,6 +1047,81 @@ function internalConnect(
}


function internalConnectMultiple(
self, addresses, port, localPort, flags
) {
assert(self.connecting);

const context = {
errors: [],
connecting: 0,
completed: false,
};

const oncomplete = FunctionPrototypeBind(afterConnectMultiple, self, context);

for (let i = 0, l = addresses.length; i < l; i++) {
if (!addresses[i]) {
continue;
}

const { address, family: addressType } = addresses[i];
const handle = new TCP(TCPConstants.SOCKET);

let localAddress;
let err;

if (localPort) {
if (addressType === 4) {
localAddress = DEFAULT_IPV4_ADDR;
err = handle.bind(localAddress, localPort);
} else { // addressType === 6
localAddress = DEFAULT_IPV6_ADDR;
err = handle.bind6(localAddress, localPort, flags);
}

debug('connect/happy eyeballs: binding to localAddress: %s and localPort: %d (addressType: %d)',
localAddress, localPort, addressType);

err = checkBindError(err, localPort, handle);
if (err) {
ArrayPrototypePush(context.errors, exceptionWithHostPort(err, 'bind', localAddress, localPort));
continue;
}
}

const req = new TCPConnectWrap();
req.oncomplete = oncomplete;
req.address = address;
req.port = port;
req.localAddress = localAddress;
req.localPort = localPort;

if (addressType === 4) {
err = handle.connect(req, address, port);
} else {
err = handle.connect6(req, address, port);
}

if (err) {
const sockname = self._getsockname();
let details;

if (sockname) {
details = sockname.address + ':' + sockname.port;
}

ArrayPrototypePush(context.errors, exceptionWithHostPort(err, 'connect', address, port, details));
} else {
context.connecting++;
}
}

if (context.errors.length && context.connecting === 0) {
self.destroy(aggregateErrors(context.error));
}
}

Socket.prototype.connect = function(...args) {
let normalized;
// If passed an array, it's treated as an array of arguments that have
Expand Down Expand Up @@ -1113,6 +1193,7 @@ function socketToDnsFamily(family) {
function lookupAndConnect(self, options) {
const { localAddress, localPort } = options;
const host = options.host || 'localhost';
const autoDetectFamily = options.autoDetectFamily ?? Socket.autoDetectFamily;
let { port } = options;

if (localAddress && !isIP(localAddress)) {
Expand Down Expand Up @@ -1166,6 +1247,79 @@ function lookupAndConnect(self, options) {
debug('connect: dns options', dnsopts);
self._host = host;
const lookup = options.lookup || dns.lookup;

if (dnsopts.family !== 4 && dnsopts.family !== 6 && autoDetectFamily) {
debug('connect: autodetecting family via happy eyeballs');

dnsopts.all = true;

defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
lookup(host, dnsopts, function emitLookup(err, addresses) {
// It's possible we were destroyed while looking this up.
// XXX it would be great if we could cancel the promise returned by
// the look up.
if (!self.connecting) {
return;
} else if (err) {
// net.createConnection() creates a net.Socket object and immediately
// calls net.Socket.connect() on it (that's us). There are no event
// listeners registered yet so defer the error event to the next tick.
process.nextTick(connectErrorNT, self, err);
return;
}

// This array will contain two elements at most, the first is a AAAA record, the second a A record
let ipv4Address;
let ipv6Address;

// Gather all the addresses we can use for happy eyeballs
for (let i = 0, l = addresses.length; i < l; i++) {
const address = addresses[i];
const { address: ip, family: addressType } = address;
self.emit('lookup', err, ip, addressType, host);

if (isIP(ip)) {
if (addressType === 6 && !ipv6Address) {
ipv6Address = address;
} else if (addressType === 4 && !ipv4Address) {
ipv4Address = address;
}
}

if (ipv6Address && ipv4Address) {
break;
}
}

// When no AAAA or A records are available, fail on the first one
if (!ipv6Address && !ipv4Address) {
const { address: firstIp, family: firstAddressType } = addresses[0];

if (!isIP(firstIp)) {
err = new ERR_INVALID_IP_ADDRESS(firstIp);
process.nextTick(connectErrorNT, self, err);
} else if (firstAddressType !== 4 && firstAddressType !== 6) {
err = new ERR_INVALID_ADDRESS_FAMILY(firstAddressType,
options.host,
options.port);
process.nextTick(connectErrorNT, self, err);
}

return;
}

self._unrefTimer();
defaultTriggerAsyncIdScope(
self[async_id_symbol],
internalConnectMultiple,
self, [ipv6Address, ipv4Address], port, localPort
);
});
});

return;
}

defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
lookup(host, dnsopts, function emitLookup(err, ip, addressType) {
self.emit('lookup', err, ip, addressType, host);
Expand Down Expand Up @@ -1294,6 +1448,62 @@ function afterConnect(status, handle, req, readable, writable) {
}
}

function afterConnectMultiple(context, status, handle, req, readable, writable) {
context.connecting--;

// Some error occurred, add to the list of exceptions
if (status !== 0) {
let details;
if (req.localAddress && req.localPort) {
details = req.localAddress + ':' + req.localPort;
}
const ex = exceptionWithHostPort(status,
'connect',
req.address,
req.port,
details);
if (details) {
ex.localAddress = req.localAddress;
ex.localPort = req.localPort;
}

ArrayPrototypePush(context.errors, ex);

if (context.connecting === 0) {
this.destroy(aggregateErrors(context.errors));
}

return;
}

// One of the connection has completed and correctly dispatched, ignore this one
if (context.completed) {
debug('connect/happy eyeballs: ignoring successful connection to %s:%s', req.address, req.port);
handle.close();
return;
}

// Mark the connection as successful
context.completed = true;
this._handle = handle;
initSocketHandle(this);

if (this.encrypted) {
this._wrapConnectedHandle(handle);
initSocketHandle(this); // This is called again to initialize the TLSWrap
}

if (hasObserver('net')) {
startPerf(
this,
kPerfHooksNetConnectContext,
{ type: 'net', name: 'connect', detail: { host: req.address, port: req.port } }
);
}

afterConnect(status, handle, req, readable, writable);
}

function addAbortSignalOption(self, options) {
if (options?.signal === undefined) {
return;
Expand Down
4 changes: 3 additions & 1 deletion test/async-hooks/test-async-local-storage-socket.js
@@ -1,13 +1,15 @@
'use strict';

require('../common');
const common = require('../common');

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

const assert = require('assert');
const net = require('net');
const { AsyncLocalStorage } = require('async_hooks');

common.setHappyEyeballsEnabled(false);

net
.createServer((socket) => {
socket.write('Hello, world!');
Expand Down
2 changes: 2 additions & 0 deletions test/async-hooks/test-graph.shutdown.js
Expand Up @@ -8,6 +8,8 @@ const initHooks = require('./init-hooks');
const verifyGraph = require('./verify-graph');
const net = require('net');

common.setHappyEyeballsEnabled(false);

const hooks = initHooks();
hooks.enable();

Expand Down

0 comments on commit 617860b

Please sign in to comment.