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 20, 2022
1 parent a2a32d8 commit 8e79310
Show file tree
Hide file tree
Showing 8 changed files with 576 additions and 26 deletions.
2 changes: 1 addition & 1 deletion deps/llhttp/CMakeLists.txt
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.5.1)
cmake_policy(SET CMP0069 NEW)

project(llhttp VERSION )
project(llhttp VERSION 6.0.9)
include(GNUInstallDirs)

set(CMAKE_C_STANDARD 99)
Expand Down
13 changes: 12 additions & 1 deletion doc/api/net.md
Expand Up @@ -856,6 +856,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 +893,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,7 +907,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`.

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

* `path` {string} Required. Path the client should connect to.
Expand Down
48 changes: 24 additions & 24 deletions doc/contributing/maintaining-http.md
Expand Up @@ -78,32 +78,32 @@ are maintained in the [llhttp](https://github.com/nodejs/llhttp)
repository. Updates are pulled into Node.js under
[deps/llhttp](https://github.com/nodejs/node/tree/HEAD/deps/llhttp).

In order to update Node.js with a new version of llhttp:

* check out the tagged release that you want to update to (a release
should be created in the llhttp repo before updating Node.js).
* run `npm install` in the directory that you checked out llhttp.
* run `make release` in the directory that you checked out llhttp.
* copy the contents of the `release` directory from the directory you
checked llhttp out to
[deps/llhttp](https://github.com/nodejs/node/tree/HEAD/deps/llhttp)

It should look like the following:

```console
├── CMakeLists.txt
├── common.gypi
├── include
│ └── llhttp.h
├── LICENSE-MIT
├── llhttp.gyp
├── README.md
└── src
├── api.c
├── http.c
└── llhttp.c
In order to update Node.js with a new version of llhttp you can use the
`tools/update-llhttp.sh` script.

The contents of the `deps/llhttp` folder should look like the following:

```bash
$ find deps/llhttp

deps/llhttp/
deps/llhttp/CMakeLists.txt
deps/llhttp/include
deps/llhttp/include/llhttp.h
deps/llhttp/llhttp.gyp
deps/llhttp/README.md
deps/llhttp/common.gypi
deps/llhttp/libllhttp.pc.in
deps/llhttp/LICENSE-MIT
deps/llhttp/src
deps/llhttp/src/api.c
deps/llhttp/src/http.c
deps/llhttp/src/llhttp.c
```

After updating, make sure the version in `CMakeLists.txt` and `include/llhttp.h`
are the same and that they match the one you are expecting.

The low-level implementation is made available in the Node.js API through
JavaScript code in the [lib](https://github.com/nodejs/node/tree/HEAD/lib)
directory and C++ code in the
Expand Down
7 changes: 7 additions & 0 deletions lib/internal/errors.js
Expand Up @@ -168,6 +168,12 @@ const aggregateTwoErrors = hideStackFrames((innerError, outerError) => {
return innerError || outerError;
});

const aggregateErrors = hideStackFrames((errors, message, code) => {
const err = new AggregateError(errors, message);
err.code = errors[0]?.code;
return err;
});

// Lazily loaded
let util;
let assert;
Expand Down Expand Up @@ -893,6 +899,7 @@ function determineSpecificType(value) {
module.exports = {
AbortError,
aggregateTwoErrors,
aggregateErrors,
captureLargerStackTrace,
codes,
connResetException,
Expand Down
180 changes: 180 additions & 0 deletions lib/net.js
Expand Up @@ -96,6 +96,7 @@ const {
ERR_SOCKET_CLOSED,
ERR_MISSING_ARGS,
},
aggregateErrors,
errnoException,
exceptionWithHostPort,
genericNodeError,
Expand Down Expand Up @@ -1042,6 +1043,76 @@ function internalConnect(
}


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

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

const oncomplete = afterConnectMultiple.bind(self, context);

for (const { address, family: addressType } of addresses) {
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) {
context.errors.push(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;
}

context.errors.push(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 @@ -1166,6 +1237,64 @@ 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 && options.autoDetectFamily) {
debug('connect: autodetecting family via happy eyeballs');

dnsopts.all = true;

defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
lookup(host, dnsopts, function emitLookup(err, addresses) {
const validAddresses = [];

// 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) && (addressType === 4 || addressType === 6)) {
validAddresses.push(address);
}
}

// 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;
}

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);
} else {
self._unrefTimer();
defaultTriggerAsyncIdScope(
self[async_id_symbol],
internalConnectMultiple,
self, validAddresses, 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 +1423,57 @@ 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;
}

context.errors.push(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 (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

0 comments on commit 8e79310

Please sign in to comment.