Skip to content

Commit 8e79310

Browse files
committed
net: add autoDetectFamily option
1 parent a2a32d8 commit 8e79310

File tree

8 files changed

+576
-26
lines changed

8 files changed

+576
-26
lines changed

deps/llhttp/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cmake_minimum_required(VERSION 3.5.1)
22
cmake_policy(SET CMP0069 NEW)
33

4-
project(llhttp VERSION )
4+
project(llhttp VERSION 6.0.9)
55
include(GNUInstallDirs)
66

77
set(CMAKE_C_STANDARD 99)

doc/api/net.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,10 @@ behavior.
856856
<!-- YAML
857857
added: v0.1.90
858858
changes:
859+
- version: REPLACEME
860+
pr-url: https://github.com/nodejs/node/pull/44731
861+
description: Added the `autoDetectFamily` option, which enables the Happy
862+
Eyeballs algorithm for dualstack connections.
859863
- version:
860864
- v17.7.0
861865
- v16.15.0
@@ -889,6 +893,7 @@ For TCP connections, available `options` are:
889893
* `port` {number} Required. Port the socket should connect to.
890894
* `host` {string} Host the socket should connect to. **Default:** `'localhost'`.
891895
* `localAddress` {string} Local address the socket should connect from.
896+
This is ignored if `autoDetectFamily` is set to `true`.
892897
* `localPort` {number} Local port the socket should connect from.
893898
* `family` {number}: Version of IP stack. Must be `4`, `6`, or `0`. The value
894899
`0` indicates that both IPv4 and IPv6 addresses are allowed. **Default:** `0`.
@@ -902,7 +907,13 @@ For TCP connections, available `options` are:
902907
**Default:** `false`.
903908
* `keepAliveInitialDelay` {number} If set to a positive number, it sets the initial delay before
904909
the first keepalive probe is sent on an idle socket.**Default:** `0`.
905-
910+
* `autoDetectFamily` {boolean}: Enables the Happy Eyeballs connection algorithm.
911+
The `all` option passed to lookup is set to `true` and the sockets attempts to
912+
connect to all returned AAAA and A records at the same time, keeping only
913+
the first successful connection and disconnecting all the other ones.
914+
Connection errors are not emitted if at least a connection succeeds.
915+
Ignored if the `family` option is not `0`.
916+
906917
For [IPC][] connections, available `options` are:
907918

908919
* `path` {string} Required. Path the client should connect to.

doc/contributing/maintaining-http.md

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -78,32 +78,32 @@ are maintained in the [llhttp](https://github.com/nodejs/llhttp)
7878
repository. Updates are pulled into Node.js under
7979
[deps/llhttp](https://github.com/nodejs/node/tree/HEAD/deps/llhttp).
8080

81-
In order to update Node.js with a new version of llhttp:
82-
83-
* check out the tagged release that you want to update to (a release
84-
should be created in the llhttp repo before updating Node.js).
85-
* run `npm install` in the directory that you checked out llhttp.
86-
* run `make release` in the directory that you checked out llhttp.
87-
* copy the contents of the `release` directory from the directory you
88-
checked llhttp out to
89-
[deps/llhttp](https://github.com/nodejs/node/tree/HEAD/deps/llhttp)
90-
91-
It should look like the following:
92-
93-
```console
94-
├── CMakeLists.txt
95-
├── common.gypi
96-
├── include
97-
│ └── llhttp.h
98-
├── LICENSE-MIT
99-
├── llhttp.gyp
100-
├── README.md
101-
└── src
102-
├── api.c
103-
├── http.c
104-
└── llhttp.c
81+
In order to update Node.js with a new version of llhttp you can use the
82+
`tools/update-llhttp.sh` script.
83+
84+
The contents of the `deps/llhttp` folder should look like the following:
85+
86+
```bash
87+
$ find deps/llhttp
88+
89+
deps/llhttp/
90+
deps/llhttp/CMakeLists.txt
91+
deps/llhttp/include
92+
deps/llhttp/include/llhttp.h
93+
deps/llhttp/llhttp.gyp
94+
deps/llhttp/README.md
95+
deps/llhttp/common.gypi
96+
deps/llhttp/libllhttp.pc.in
97+
deps/llhttp/LICENSE-MIT
98+
deps/llhttp/src
99+
deps/llhttp/src/api.c
100+
deps/llhttp/src/http.c
101+
deps/llhttp/src/llhttp.c
105102
```
106103

104+
After updating, make sure the version in `CMakeLists.txt` and `include/llhttp.h`
105+
are the same and that they match the one you are expecting.
106+
107107
The low-level implementation is made available in the Node.js API through
108108
JavaScript code in the [lib](https://github.com/nodejs/node/tree/HEAD/lib)
109109
directory and C++ code in the

lib/internal/errors.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ const aggregateTwoErrors = hideStackFrames((innerError, outerError) => {
168168
return innerError || outerError;
169169
});
170170

171+
const aggregateErrors = hideStackFrames((errors, message, code) => {
172+
const err = new AggregateError(errors, message);
173+
err.code = errors[0]?.code;
174+
return err;
175+
});
176+
171177
// Lazily loaded
172178
let util;
173179
let assert;
@@ -893,6 +899,7 @@ function determineSpecificType(value) {
893899
module.exports = {
894900
AbortError,
895901
aggregateTwoErrors,
902+
aggregateErrors,
896903
captureLargerStackTrace,
897904
codes,
898905
connResetException,

lib/net.js

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ const {
9696
ERR_SOCKET_CLOSED,
9797
ERR_MISSING_ARGS,
9898
},
99+
aggregateErrors,
99100
errnoException,
100101
exceptionWithHostPort,
101102
genericNodeError,
@@ -1042,6 +1043,76 @@ function internalConnect(
10421043
}
10431044

10441045

1046+
function internalConnectMultiple(
1047+
self, addresses, port, localPort, flags
1048+
) {
1049+
assert(self.connecting);
1050+
1051+
const context = {
1052+
errors: [],
1053+
connecting: 0,
1054+
completed: false
1055+
};
1056+
1057+
const oncomplete = afterConnectMultiple.bind(self, context);
1058+
1059+
for (const { address, family: addressType } of addresses) {
1060+
const handle = new TCP(TCPConstants.SOCKET);
1061+
1062+
let localAddress;
1063+
let err;
1064+
1065+
if (localPort) {
1066+
if (addressType === 4) {
1067+
localAddress = DEFAULT_IPV4_ADDR;
1068+
err = handle.bind(localAddress, localPort);
1069+
} else { // addressType === 6
1070+
localAddress = DEFAULT_IPV6_ADDR;
1071+
err = handle.bind6(localAddress, localPort, flags);
1072+
}
1073+
1074+
debug('connect/happy eyeballs: binding to localAddress: %s and localPort: %d (addressType: %d)',
1075+
localAddress, localPort, addressType);
1076+
1077+
err = checkBindError(err, localPort, handle);
1078+
if (err) {
1079+
context.errors.push(exceptionWithHostPort(err, 'bind', localAddress, localPort));
1080+
continue;
1081+
}
1082+
}
1083+
1084+
const req = new TCPConnectWrap();
1085+
req.oncomplete = oncomplete;
1086+
req.address = address;
1087+
req.port = port;
1088+
req.localAddress = localAddress;
1089+
req.localPort = localPort;
1090+
1091+
if (addressType === 4) {
1092+
err = handle.connect(req, address, port);
1093+
} else {
1094+
err = handle.connect6(req, address, port);
1095+
}
1096+
1097+
if (err) {
1098+
const sockname = self._getsockname();
1099+
let details;
1100+
1101+
if (sockname) {
1102+
details = sockname.address + ':' + sockname.port;
1103+
}
1104+
1105+
context.errors.push(exceptionWithHostPort(err, 'connect', address, port, details));
1106+
} else {
1107+
context.connecting++;
1108+
}
1109+
}
1110+
1111+
if (context.errors.length && context.connecting === 0) {
1112+
self.destroy(aggregateErrors(context.error));
1113+
}
1114+
}
1115+
10451116
Socket.prototype.connect = function(...args) {
10461117
let normalized;
10471118
// If passed an array, it's treated as an array of arguments that have
@@ -1166,6 +1237,64 @@ function lookupAndConnect(self, options) {
11661237
debug('connect: dns options', dnsopts);
11671238
self._host = host;
11681239
const lookup = options.lookup || dns.lookup;
1240+
1241+
if (dnsopts.family !== 4 && dnsopts.family !== 6 && options.autoDetectFamily) {
1242+
debug('connect: autodetecting family via happy eyeballs');
1243+
1244+
dnsopts.all = true;
1245+
1246+
defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
1247+
lookup(host, dnsopts, function emitLookup(err, addresses) {
1248+
const validAddresses = [];
1249+
1250+
// Gather all the addresses we can use for happy eyeballs
1251+
for (let i = 0, l = addresses.length; i < l; i++) {
1252+
const address = addresses[i];
1253+
const { address: ip, family: addressType } = address;
1254+
self.emit('lookup', err, ip, addressType, host);
1255+
1256+
if (isIP(ip) && (addressType === 4 || addressType === 6)) {
1257+
validAddresses.push(address);
1258+
}
1259+
}
1260+
1261+
// It's possible we were destroyed while looking this up.
1262+
// XXX it would be great if we could cancel the promise returned by
1263+
// the look up.
1264+
if (!self.connecting) {
1265+
return;
1266+
} else if (err) {
1267+
// net.createConnection() creates a net.Socket object and immediately
1268+
// calls net.Socket.connect() on it (that's us). There are no event
1269+
// listeners registered yet so defer the error event to the next tick.
1270+
process.nextTick(connectErrorNT, self, err);
1271+
return;
1272+
}
1273+
1274+
const { address: firstIp, family: firstAddressType } = addresses[0];
1275+
1276+
if (!isIP(firstIp)) {
1277+
err = new ERR_INVALID_IP_ADDRESS(firstIp);
1278+
process.nextTick(connectErrorNT, self, err);
1279+
} else if (firstAddressType !== 4 && firstAddressType !== 6) {
1280+
err = new ERR_INVALID_ADDRESS_FAMILY(firstAddressType,
1281+
options.host,
1282+
options.port);
1283+
process.nextTick(connectErrorNT, self, err);
1284+
} else {
1285+
self._unrefTimer();
1286+
defaultTriggerAsyncIdScope(
1287+
self[async_id_symbol],
1288+
internalConnectMultiple,
1289+
self, validAddresses, port, localPort
1290+
);
1291+
}
1292+
});
1293+
});
1294+
1295+
return;
1296+
}
1297+
11691298
defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
11701299
lookup(host, dnsopts, function emitLookup(err, ip, addressType) {
11711300
self.emit('lookup', err, ip, addressType, host);
@@ -1294,6 +1423,57 @@ function afterConnect(status, handle, req, readable, writable) {
12941423
}
12951424
}
12961425

1426+
function afterConnectMultiple(context, status, handle, req, readable, writable) {
1427+
context.connecting--;
1428+
1429+
// Some error occurred, add to the list of exceptions
1430+
if (status !== 0) {
1431+
let details;
1432+
if (req.localAddress && req.localPort) {
1433+
details = req.localAddress + ':' + req.localPort;
1434+
}
1435+
const ex = exceptionWithHostPort(status,
1436+
'connect',
1437+
req.address,
1438+
req.port,
1439+
details);
1440+
if (details) {
1441+
ex.localAddress = req.localAddress;
1442+
ex.localPort = req.localPort;
1443+
}
1444+
1445+
context.errors.push(ex);
1446+
1447+
if (context.connecting === 0) {
1448+
this.destroy(aggregateErrors(context.errors));
1449+
}
1450+
1451+
return;
1452+
}
1453+
1454+
// One of the connection has completed and correctly dispatched, ignore this one
1455+
if (context.completed) {
1456+
debug('connect/happy eyeballs: ignoring successful connection to %s:%s', req.address, req.port);
1457+
handle.close();
1458+
return;
1459+
}
1460+
1461+
// Mark the connection as successful
1462+
context.completed = true;
1463+
this._handle = handle;
1464+
initSocketHandle(this);
1465+
1466+
if (hasObserver('net')) {
1467+
startPerf(
1468+
this,
1469+
kPerfHooksNetConnectContext,
1470+
{ type: 'net', name: 'connect', detail: { host: req.address, port: req.port } }
1471+
);
1472+
}
1473+
1474+
afterConnect(status, handle, req, readable, writable);
1475+
}
1476+
12971477
function addAbortSignalOption(self, options) {
12981478
if (options?.signal === undefined) {
12991479
return;

0 commit comments

Comments
 (0)