|
21 | 21 |
|
22 | 22 | 'use strict'; |
23 | 23 |
|
24 | | -const { |
| 24 | +const { |
25 | 25 | ArrayIsArray, |
26 | 26 | ArrayPrototypeIndexOf, |
27 | 27 | Boolean, |
@@ -96,6 +96,7 @@ const { |
96 | 96 | ERR_SOCKET_CLOSED, |
97 | 97 | ERR_MISSING_ARGS, |
98 | 98 | }, |
| 99 | + aggregateErrors, |
99 | 100 | errnoException, |
100 | 101 | exceptionWithHostPort, |
101 | 102 | genericNodeError, |
@@ -1042,6 +1043,76 @@ function internalConnect( |
1042 | 1043 | } |
1043 | 1044 |
|
1044 | 1045 |
|
| 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 | + |
1045 | 1116 | Socket.prototype.connect = function(...args) { |
1046 | 1117 | let normalized; |
1047 | 1118 | // If passed an array, it's treated as an array of arguments that have |
@@ -1166,6 +1237,64 @@ function lookupAndConnect(self, options) { |
1166 | 1237 | debug('connect: dns options', dnsopts); |
1167 | 1238 | self._host = host; |
1168 | 1239 | 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 | + |
1169 | 1298 | defaultTriggerAsyncIdScope(self[async_id_symbol], function() { |
1170 | 1299 | lookup(host, dnsopts, function emitLookup(err, ip, addressType) { |
1171 | 1300 | self.emit('lookup', err, ip, addressType, host); |
@@ -1294,6 +1423,57 @@ function afterConnect(status, handle, req, readable, writable) { |
1294 | 1423 | } |
1295 | 1424 | } |
1296 | 1425 |
|
| 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 | + |
1297 | 1477 | function addAbortSignalOption(self, options) { |
1298 | 1478 | if (options?.signal === undefined) { |
1299 | 1479 | return; |
|
0 commit comments