|
24 | 24 | const {
|
25 | 25 | ArrayIsArray,
|
26 | 26 | ArrayPrototypeIndexOf,
|
| 27 | + ArrayPrototypePush, |
27 | 28 | Boolean,
|
| 29 | + FunctionPrototypeBind, |
28 | 30 | Number,
|
29 | 31 | NumberIsNaN,
|
30 | 32 | NumberParseInt,
|
@@ -96,6 +98,7 @@ const {
|
96 | 98 | ERR_SOCKET_CLOSED,
|
97 | 99 | ERR_MISSING_ARGS,
|
98 | 100 | },
|
| 101 | + aggregateErrors, |
99 | 102 | errnoException,
|
100 | 103 | exceptionWithHostPort,
|
101 | 104 | genericNodeError,
|
@@ -458,6 +461,8 @@ function Socket(options) {
|
458 | 461 | ObjectSetPrototypeOf(Socket.prototype, stream.Duplex.prototype);
|
459 | 462 | ObjectSetPrototypeOf(Socket, stream.Duplex);
|
460 | 463 |
|
| 464 | +Socket.autoDetectFamily = true; |
| 465 | + |
461 | 466 | // Refresh existing timeouts.
|
462 | 467 | Socket.prototype._unrefTimer = function _unrefTimer() {
|
463 | 468 | for (let s = this; s !== null; s = s._parent) {
|
@@ -1042,6 +1047,81 @@ function internalConnect(
|
1042 | 1047 | }
|
1043 | 1048 |
|
1044 | 1049 |
|
| 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 | + |
1045 | 1125 | Socket.prototype.connect = function(...args) {
|
1046 | 1126 | let normalized;
|
1047 | 1127 | // If passed an array, it's treated as an array of arguments that have
|
@@ -1113,6 +1193,7 @@ function socketToDnsFamily(family) {
|
1113 | 1193 | function lookupAndConnect(self, options) {
|
1114 | 1194 | const { localAddress, localPort } = options;
|
1115 | 1195 | const host = options.host || 'localhost';
|
| 1196 | + const autoDetectFamily = options.autoDetectFamily ?? Socket.autoDetectFamily; |
1116 | 1197 | let { port } = options;
|
1117 | 1198 |
|
1118 | 1199 | if (localAddress && !isIP(localAddress)) {
|
@@ -1166,6 +1247,79 @@ function lookupAndConnect(self, options) {
|
1166 | 1247 | debug('connect: dns options', dnsopts);
|
1167 | 1248 | self._host = host;
|
1168 | 1249 | 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 | + |
1169 | 1323 | defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
|
1170 | 1324 | lookup(host, dnsopts, function emitLookup(err, ip, addressType) {
|
1171 | 1325 | self.emit('lookup', err, ip, addressType, host);
|
@@ -1294,6 +1448,62 @@ function afterConnect(status, handle, req, readable, writable) {
|
1294 | 1448 | }
|
1295 | 1449 | }
|
1296 | 1450 |
|
| 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 | + |
1297 | 1507 | function addAbortSignalOption(self, options) {
|
1298 | 1508 | if (options?.signal === undefined) {
|
1299 | 1509 | return;
|
|
0 commit comments