nodejs/node · #66229
net: race family autoselection attempts
doc/api/net.md27 + / 19 −
@@ -1143,9 +1143,11 @@ added: * `port` {number} The port which the socket attempted to connect to. * `family` {number} The family of the IP. It can be `6` for IPv6 or `4` for IPv4. -Emitted when a connection attempt timed out. This is only emitted (and may be-emitted multiple times) if the family autoselection algorithm is enabled-in [`socket.connect(options)`][].+Emitted when a connection attempt is still pending after the configured+`autoSelectFamilyAttemptTimeout`. If another attempt is about to start, the+pending attempt remains active and may still establish the connection, unless+`localPort` requires sequential attempts. This is only emitted if the family+autoselection algorithm is enabled in [`socket.connect(options)`][]. ### Event: `'data'` @@ -1396,22 +1398,28 @@ For TCP connections, available `options` are: * `autoSelectFamily` {boolean}: If set to `true`, it enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305][]. The- `all` option passed to lookup is set to `true` and the sockets attempts to- connect to all obtained IPv6 and IPv4 addresses, in sequence, until a- connection is established. The first returned AAAA address is tried first,- then the first returned A address, then the second returned AAAA address and- so on. Each connection attempt (but the last one) is given the amount of time- specified by the `autoSelectFamilyAttemptTimeout` option before timing out and- trying the next address. Ignored if the `family` option is not `0` or if- `localAddress` is set. Connection errors are not emitted if at least one- connection succeeds. If all connections attempts fails, a single- `AggregateError` with all failed attempts is emitted. **Default:**- [`net.getDefaultAutoSelectFamily()`][].-* `autoSelectFamilyAttemptTimeout` {number}: The amount of time in milliseconds- to wait for a connection attempt to finish before trying the next address when- using the `autoSelectFamily` option. If set to a positive integer less than- `10`, then the value `10` will be used instead. **Default:**- [`net.getDefaultAutoSelectFamilyAttemptTimeout()`][].+ `all` option passed to lookup is set to `true` and the socket attempts to+ connect to all obtained IPv6 and IPv4 addresses until a connection is+ established. The first valid address is tried first, followed by addresses+ from alternating families in their original order. After+ `autoSelectFamilyAttemptTimeout` milliseconds, or as soon as an attempt fails,+ the next attempt starts without canceling any pending attempts. The first+ successful TCP connection wins and the other attempts are canceled. If the+ last attempt fails while others are still pending, they are given one more+ `autoSelectFamilyAttemptTimeout` before the connection fails. When `localPort`+ is set, attempts are made sequentially because multiple connections cannot+ portably bind the same local port. The option is ignored if `family` is not+ `0` or if `localAddress` is set. Connection errors are not emitted if at least+ one connection succeeds. If all connection attempts fail, a single+ `AggregateError` with all failed attempts, in attempt order, is emitted.+ **Default:** [`net.getDefaultAutoSelectFamily()`][].+* `autoSelectFamilyAttemptTimeout` {number}: The delay in milliseconds before+ starting the next connection attempt while the previous one is pending when+ using the `autoSelectFamily` option. A failed attempt starts the next one+ immediately. A pending attempt is not canceled when this delay elapses, except+ when `localPort` requires sequential attempts. If set to a positive integer+ less than `10`, then the value `10` will be used instead.+ **Default:** [`net.getDefaultAutoSelectFamilyAttemptTimeout()`][]. * `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`. * `hints` {number} Optional [`dns.lookup()` hints][].lib/internal/tls/wrap.js1 + / 1 −
@@ -728,7 +728,7 @@ ObjectSetPrototypeOf(TLSSocket, net.Socket); exports.TLSSocket = TLSSocket; const proxiedMethods = [- 'ref', 'unref', 'open', 'bind', 'listen', 'connect', 'bind6',+ 'ref', 'unref', 'hasRef', 'open', 'bind', 'listen', 'connect', 'bind6', 'connect6', 'getsockname', 'getpeername', 'setNoDelay', 'setKeepAlive', 'setSimultaneousAccepts', 'setBlocking', lib/net.js188 + / 79 −
@@ -26,6 +26,8 @@ const { ArrayPrototypeIncludes, ArrayPrototypeIndexOf, ArrayPrototypePush,+ ArrayPrototypeSlice,+ ArrayPrototypeSplice, Boolean, FunctionPrototypeBind, FunctionPrototypeCall,@@ -66,7 +68,6 @@ const { UV_EBADF, UV_EINVAL, UV_ENOTCONN,- UV_ECANCELED, UV_ETIMEDOUT, } = internalBinding('uv'); const { convertIpv6StringToBuffer } = internalBinding('cares_wrap');@@ -172,6 +173,10 @@ const DEFAULT_IPV6_ADDR = '::'; const noop = () => {}; const kPerfHooksNetConnectContext = Symbol('kPerfHooksNetConnectContext');+const kAutoSelectFamilyContext = Symbol('kAutoSelectFamilyContext');+const kSocketHandleUnused = 0;+const kSocketHandlePending = 1;+const kSocketHandleFailed = 2; const dc = require('diagnostics_channel'); const netClientSocketChannel = dc.channel('net.client.socket');@@ -1165,6 +1170,12 @@ Socket.prototype._destroy = function(exception, cb) { this.connecting = false; + const context = this[kAutoSelectFamilyContext];+ if (context) {+ closeConnectionAttempts(context);+ this[kAutoSelectFamilyContext] = undefined;+ }+ // `_parent` may be null; we use a loose `!= null` check in case external // code sets it to undefined. for (let s = this; s != null; s = s._parent) {@@ -1477,107 +1488,205 @@ function internalConnect( } -function internalConnectMultiple(context, canceled) {+function closeConnectionAttempts(context) { clearTimeout(context[kTimeout]);+ context[kTimeout] = null;+ context.done = true;+ for (let i = 0; i < context.pending.length; i++) {+ const { handle, req, own } = context.pending[i];+ req.oncomplete = undefined;+ // The socket's own handle is closed by whoever replaces or destroys it.+ if (!own) handle.close();+ }+ context.pending.length = 0;+}++function removePendingAttempt(context, req) {+ const { pending } = context;+ for (let i = 0; i < pending.length; i++) {+ if (pending[i].req === req) {+ const attempt = pending[i];+ ArrayPrototypeSplice(pending, i, 1);+ return attempt;+ }+ }+}++function connectMultipleError(context) {+ // Errors are stored by attempt index to preserve the attempt order.+ const errors = [];+ for (let i = 0; i < context.errors.length; i++) {+ if (context.errors[i] !== undefined) ArrayPrototypePush(errors, context.errors[i]);+ }+ return errors.length === 0 ? new ERR_SOCKET_CONNECTION_TIMEOUT() : new NodeAggregateError(errors);+}++// Returns the handle for the next attempt. The first attempt uses the socket's+// own handle so that any state set on it before connecting is kept.+function acquireAttemptHandle(context) { const self = context.socket;+ if (context.socketHandleState === kSocketHandlePending) {+ const handle = new TCP(TCPConstants.SOCKET);+ if (self._handle.hasRef() === false) handle.unref();+ return handle;+ } - // We were requested to abort. Stop all operations- if (self._aborted) {- return;+ if (context.socketHandleState === kSocketHandleFailed) {+ const unrefed = self._handle.hasRef() === false;+ self[kReinitializeHandle](new TCP(TCPConstants.SOCKET));+ if (unrefed) self._handle.unref(); } - // All connections have been tried without success, destroy with error- if (canceled || context.current === context.addresses.length) {- if (context.errors.length === 0) {- self.destroy(new ERR_SOCKET_CONNECTION_TIMEOUT());- return;- }+ context.socketHandleState = kSocketHandlePending;+ return self._handle;+}++function releaseAttemptHandle(context, handle, own) {+ if (own) {+ context.socketHandleState = kSocketHandleFailed;+ } else {+ handle.close();+ }+}++function connectionAttemptFailed(context, index) {+ const self = context.socket;+ if (context.done || !self.connecting) return; - self.destroy(new NodeAggregateError(context.errors));+ // Start the next attempt right away, without waiting for the attempt timeout.+ if (context.current < context.addresses.length) {+ internalConnectMultiple(context); return; } - assert(self.connecting);+ if (context.pending.length === 0) {+ self.destroy(connectMultipleError(context));+ return;+ } - const current = context.current++;+ // The last attempt failed but earlier ones are still pending: give them one+ // more attempt timeout instead of waiting for the operating system.+ if (index === context.addresses.length - 1 && context[kTimeout] === null) {+ context[kTimeout] = setTimeout(internalConnectMultipleFinalTimeout, context.timeout, context);+ if (self._handle.hasRef() === false) context[kTimeout].unref();+ }+} - if (current > 0) {- self[kReinitializeHandle](new TCP(TCPConstants.SOCKET));+function internalConnectMultipleTimeout(context, attempt) {+ context[kTimeout] = null;+ const self = context.socket;+ if (context.done || !self.connecting) return;++ const { handle, req, index, own } = attempt;+ if (ArrayPrototypeIncludes(context.pending, attempt)) {+ debug('connect/multiple: connection to %s:%s is still pending', req.address, req.port);+ attempt.timedOut = true;+ if (context.localPort) {+ // Two attempts cannot portably bind to the same fixed source port.+ removePendingAttempt(context, req);+ req.oncomplete = undefined;+ releaseAttemptHandle(context, handle, own);+ context.errors[index] = createConnectionError(req, UV_ETIMEDOUT);+ }+ self.emit('connectionAttemptTimeout', req.address, req.port, req.addressType); } + if (!context.done && self.connecting) internalConnectMultiple(context);+}++function internalConnectMultipleFinalTimeout(context) {+ context[kTimeout] = null;+ const self = context.socket;+ if (context.done || !self.connecting) return;++ const pending = ArrayPrototypeSlice(context.pending);+ for (let i = 0; i < pending.length; i++) {+ const { req, index, timedOut } = pending[i];+ debug('connect/multiple: connection to %s:%s timed out', req.address, req.port);+ context.errors[index] = createConnectionError(req, UV_ETIMEDOUT);+ if (!timedOut) self.emit('connectionAttemptTimeout', req.address, req.port, req.addressType);+ }++ if (!context.done && self.connecting) self.destroy(connectMultipleError(context));+}++function internalConnectMultiple(context) {+ clearTimeout(context[kTimeout]);+ context[kTimeout] = null;+ const self = context.socket;+ if (context.done || !self.connecting || self._aborted) return;++ const current = context.current++; const { localPort, port, flags } = context; const { address, family: addressType } = context.addresses[current];+ const handle = acquireAttemptHandle(context);+ const own = handle === self._handle; let localAddress; let err; if (localPort) { if (addressType === 4) { localAddress = DEFAULT_IPV4_ADDR;- err = self._handle.bind(localAddress, localPort);+ err = handle.bind(localAddress, localPort); } else { // addressType === 6 localAddress = DEFAULT_IPV6_ADDR;- err = self._handle.bind6(localAddress, localPort, flags);+ err = handle.bind6(localAddress, localPort, flags); } debug('connect/multiple: binding to localAddress: %s and localPort: %d (addressType: %d)', localAddress, localPort, addressType); - err = checkBindError(err, localPort, self._handle);+ err = checkBindError(err, localPort, handle); if (err) {- ArrayPrototypePush(context.errors, new ExceptionWithHostPort(err, 'bind', localAddress, localPort));- internalConnectMultiple(context);+ releaseAttemptHandle(context, handle, own);+ context.errors[current] = new ExceptionWithHostPort(err, 'bind', localAddress, localPort);+ connectionAttemptFailed(context, current); return; } } if (self.blockList?.check(address, `ipv${addressType}`)) {+ releaseAttemptHandle(context, handle, own); const ex = new ERR_IP_BLOCKED(address);- ArrayPrototypePush(context.errors, ex);+ context.errors[current] = ex; self.emit('connectionAttemptFailed', address, port, addressType, ex);- internalConnectMultiple(context);+ connectionAttemptFailed(context, current); return; } debug('connect/multiple: attempting to connect to %s:%d (addressType: %d)', address, port, addressType); self.emit('connectionAttempt', address, port, addressType);+ if (!self.connecting || context.done) {+ if (!own) handle.close();+ return;+ } const req = new TCPConnectWrap();- req.oncomplete = FunctionPrototypeBind(afterConnectMultiple, undefined, context, current);+ req.oncomplete = FunctionPrototypeBind(afterConnectMultiple, undefined, context); req.address = address; req.port = port; req.localAddress = localAddress; req.localPort = localPort; req.addressType = addressType; ArrayPrototypePush(self.autoSelectFamilyAttemptedAddresses, `${address}:${port}`);-- if (addressType === 4) {- err = self._handle.connect(req, address, port);- } else {- err = self._handle.connect6(req, address, port);- }+ err = addressType === 4 ? handle.connect(req, address, port) : handle.connect6(req, address, port); if (err) {- const sockname = self._getsockname();- let details;-- if (sockname) {- details = sockname.address + ':' + sockname.port;- }-- const ex = new ExceptionWithHostPort(err, 'connect', address, port, details);- ArrayPrototypePush(context.errors, ex);-+ releaseAttemptHandle(context, handle, own);+ const ex = createConnectionError(req, err);+ context.errors[current] = ex; self.emit('connectionAttemptFailed', address, port, addressType, ex);- internalConnectMultiple(context);+ connectionAttemptFailed(context, current); return; } - if (current < context.addresses.length - 1) {+ const attempt = { handle, req, index: current, own, timedOut: false };+ ArrayPrototypePush(context.pending, attempt);+ if (context.current < context.addresses.length) { debug('connect/multiple: setting the attempt timeout to %d ms', context.timeout);-- // If the attempt has not returned an error, start the connection timer- context[kTimeout] = setTimeout(internalConnectMultipleTimeout, context.timeout, context, req, self._handle);+ context[kTimeout] = setTimeout(internalConnectMultipleTimeout, context.timeout, context, attempt);+ if (self._handle.hasRef() === false) context[kTimeout].unref(); } } @@ -1985,7 +2094,11 @@ function lookupAndConnectMultiple( timeout, [kTimeout]: null, errors: [],+ pending: [],+ socketHandleState: kSocketHandleUnused,+ done: false, };+ self[kAutoSelectFamilyContext] = context; self._unrefTimer(); defaultTriggerAsyncIdScope(self[async_id_symbol], internalConnectMultiple, context);@@ -2007,6 +2120,13 @@ Socket.prototype.ref = function() { if (typeof this._handle.ref === 'function') { this._handle.ref(); }+ const context = this[kAutoSelectFamilyContext];+ if (context) {+ context[kTimeout]?.ref();+ for (let i = 0; i < context.pending.length; i++) {+ context.pending[i].handle.ref();+ }+ } return this; };@@ -2021,6 +2141,13 @@ Socket.prototype.unref = function() { if (typeof this._handle.unref === 'function') { this._handle.unref(); }+ const context = this[kAutoSelectFamilyContext];+ if (context) {+ context[kTimeout]?.unref();+ for (let i = 0; i < context.pending.length; i++) {+ context.pending[i].handle.unref();+ }+ } return this; };@@ -2138,61 +2265,43 @@ function createConnectionError(req, status) { return ex; } -function afterConnectMultiple(context, current, status, handle, req, readable, writable) {+function afterConnectMultiple(context, status, handle, req, readable, writable) { debug('connect/multiple: connection attempt to %s:%s completed with status %s', req.address, req.port, status);+ const attempt = removePendingAttempt(context, req);+ if (attempt === undefined) return; - // Make sure another connection is not spawned- clearTimeout(context[kTimeout]);-- // One of the connection has completed and correctly dispatched but after timeout, ignore this one- if (status === 0 && current !== context.current - 1) {- debug('connect/multiple: ignoring successful but timedout connection to %s:%s', req.address, req.port);- handle.close();+ const self = context.socket;+ if (context.done || !self.connecting) {+ if (!attempt.own) attempt.handle.close(); return; } - const self = context.socket;-- // Some error occurred, add to the list of exceptions if (status !== 0) {+ releaseAttemptHandle(context, attempt.handle, attempt.own); const ex = createConnectionError(req, status);- ArrayPrototypePush(context.errors, ex);-+ context.errors[attempt.index] = ex; self.emit('connectionAttemptFailed', req.address, req.port, req.addressType, ex);-- // Try the next address, unless we were aborted- if (context.socket.connecting) {- internalConnectMultiple(context, status === UV_ECANCELED);- }-+ connectionAttemptFailed(context, attempt.index); return; } + closeConnectionAttempts(context);+ self[kAutoSelectFamilyContext] = undefined;+ if (!attempt.own) {+ const unrefed = self._handle.hasRef() === false;+ self[kReinitializeHandle](attempt.handle);+ if (unrefed) self._handle.unref();+ } if (hasObserver('net')) { startPerf( self, kPerfHooksNetConnectContext, { type: 'net', name: 'connect', detail: { host: req.address, port: req.port } }, ); }- afterConnect(status, self._handle, req, readable, writable); } -function internalConnectMultipleTimeout(context, req, handle) {- debug('connect/multiple: connection to %s:%s timed out', req.address, req.port);- context.socket.emit('connectionAttemptTimeout', req.address, req.port, req.addressType);-- req.oncomplete = undefined;- ArrayPrototypePush(context.errors, createConnectionError(req, UV_ETIMEDOUT));- handle.close();-- // Try the next address, unless we were aborted- if (context.socket.connecting) {- internalConnectMultiple(context);- }-}- function addServerAbortSignalOption(self, options) { if (options?.signal === undefined) { return;test/internet/test-net-autoselectfamily-events-failure.js8 + / 8 −
@@ -80,30 +80,30 @@ const { createConnection } = require('net'); const addresses = [ { address: INET6_IP, port: 10, family: 6 },- { address: INET6_IP, port: 10, family: 6 },- { address: INET4_IP, port: 10, family: 4 }, { address: INET4_IP, port: 10, family: 4 }, ];+ const attempted = []; + // Attempts run in parallel, so a failure is not necessarily reported+ // before the next attempt starts. connection.on('connectionAttempt', common.mustCallAtLeast((address, port, family) => { const expected = addresses.shift(); assert.strictEqual(address, expected.address); assert.strictEqual(port, expected.port); assert.strictEqual(family, expected.family);+ attempted.push(expected); pass(); }, 0)); connection.on('connectionAttemptFailed', common.mustCallAtLeast((address, port, family, error) => {- const expected = addresses.shift();-- assert.strictEqual(address, expected.address);- assert.strictEqual(port, expected.port);- assert.strictEqual(family, expected.family);+ assert.ok(attempted.some((expected) => {+ return expected.address === address && expected.port === port && expected.family === family;+ })); assert.ok(- error.code.match(/ECONNREFUSED|ENETUNREACH|EHOSTUNREACH|ETIMEDOUT/),+ error.code.match(/EACCES|ECONNREFUSED|ENETUNREACH|EHOSTUNREACH|ETIMEDOUT/), `Received unexpected error code ${error.code}`, ); test/parallel/test-net-autoselectfamily-local-port.jsadded43 + / 0 −
@@ -0,0 +1,43 @@+'use strict';++const common = require('../common');+if (!common.hasIPv6) common.skip('IPv6 loopback is unavailable');++const assert = require('node:assert');+const { createConnection, createServer } = require('node:net');+const { TCP } = process.binding('tcp_wrap');+// A pending IPv6 connect with a fixed source port must be closed before an+// IPv4 candidate binds to that port.+TCP.prototype.connect6 = function() {+ return 0;+};++const reserved = createServer();+reserved.listen(0, '127.0.0.1', common.mustCall(() => {+ const localPort = reserved.address().port;+ reserved.close(common.mustCall(() => {+ const server = createServer(common.mustCall((socket) => socket.end()));+ server.listen(0, '127.0.0.1', common.mustCall(() => {+ const port = server.address().port;+ const connection = createConnection({+ host: 'example.org',+ port,+ localPort,+ lookup: common.mustCall((host, options, callback) => {+ process.nextTick(callback, null, [+ { address: '::1', family: 6 },+ { address: '127.0.0.1', family: 4 },+ ]);+ }),+ autoSelectFamily: true,+ autoSelectFamilyAttemptTimeout: 10,+ });+ connection.on('connect', common.mustCall(() => {+ assert.strictEqual(connection.localPort, localPort);+ assert.strictEqual(connection.remoteAddress, '127.0.0.1');+ }));+ connection.on('error', common.mustNotCall());+ connection.on('close', common.mustCall(() => server.close()));+ }));+ }));+}));test/parallel/test-net-autoselectfamily-parallel-destroy.jsadded48 + / 0 −
@@ -0,0 +1,48 @@+'use strict';++const common = require('../common');+const assert = require('node:assert');+const { createConnection, createServer } = require('node:net');+const { TCP } = process.binding('tcp_wrap');++const connect = TCP.prototype.connect;+let completeFirst;+TCP.prototype.connect = function(req, address, port) {+ if (address === '127.0.0.1') {+ const oncomplete = req.oncomplete;+ req.oncomplete = (...args) => {+ completeFirst = () => oncomplete(...args);+ };+ }+ return Reflect.apply(connect, this, [req, address, port]);+};++const server = createServer((socket) => socket.end());+server.listen(0, '127.0.0.1', common.mustCall(() => {+ const port = server.address().port;+ const secondAddress = common.hasIPv6 ? '::1' : '127.0.0.2';+ const connection = createConnection({+ host: 'example.org',+ port,+ lookup: common.mustCall((host, options, callback) => {+ process.nextTick(callback, null, [+ { address: '127.0.0.1', family: 4 },+ { address: secondAddress, family: common.hasIPv6 ? 6 : 4 },+ ]);+ }),+ autoSelectFamily: true,+ autoSelectFamilyAttemptTimeout: 10,+ });++ connection.on('connectionAttempt', common.mustCallAtLeast((address) => {+ if (address === secondAddress) connection.destroy();+ }, 2));+ connection.on('connect', common.mustNotCall());+ connection.on('error', common.mustNotCall());+ connection.on('close', common.mustCall(() => {+ assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses,+ [`127.0.0.1:${port}`]);+ if (completeFirst) setImmediate(completeFirst);+ server.close();+ }));+}));test/parallel/test-net-autoselectfamily-parallel-failure-fallback.jsadded52 + / 0 −
@@ -0,0 +1,52 @@+// Flags: --expose-internals+'use strict';++const common = require('../common');+const assert = require('node:assert');+const { createConnection, createServer } = require('node:net');+const { internalBinding } = require('internal/test/binding');+const { TCP } = internalBinding('tcp_wrap');+const { UV_ECONNREFUSED } = internalBinding('uv');++// An attempt that fails explicitly starts the next attempt right away, without+// waiting for autoSelectFamilyAttemptTimeout.+const connect = TCP.prototype.connect;+TCP.prototype.connect = function(req, address, port) {+ if (address === '10.0.0.1') {+ setImmediate(() => req.oncomplete(UV_ECONNREFUSED, this, req, false, false));+ return 0;+ }+ return Reflect.apply(connect, this, [req, address, port]);+};++const server = createServer(common.mustCall((socket) => socket.end()));+server.listen(0, '127.0.0.1', common.mustCall(() => {+ const port = server.address().port;+ const connection = createConnection({+ host: 'example.org',+ port,+ lookup: common.mustCall((host, options, callback) => {+ process.nextTick(callback, null, [+ { address: '10.0.0.1', family: 4 },+ { address: '127.0.0.1', family: 4 },+ ]);+ }),+ autoSelectFamily: true,+ autoSelectFamilyAttemptTimeout: common.platformTimeout(60000),+ });++ connection.on('connectionAttemptTimeout', common.mustNotCall());+ connection.on('connectionAttemptFailed', common.mustCall((address, p, family, error) => {+ assert.strictEqual(address, '10.0.0.1');+ assert.strictEqual(error.code, 'ECONNREFUSED');+ process.nextTick(common.mustCall(() => {+ assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses,+ [`10.0.0.1:${port}`, `127.0.0.1:${port}`]);+ }));+ }));+ connection.on('connect', common.mustCall(() => {+ assert.strictEqual(connection.remoteAddress, '127.0.0.1');+ }));+ connection.on('error', common.mustNotCall());+ connection.on('close', common.mustCall(() => server.close()));+}));test/parallel/test-net-autoselectfamily-parallel-final-timeout.jsadded49 + / 0 −
@@ -0,0 +1,49 @@+// Flags: --expose-internals+'use strict';++const common = require('../common');+const assert = require('node:assert');+const { createConnection } = require('node:net');+const { internalBinding } = require('internal/test/binding');+const { TCP } = internalBinding('tcp_wrap');+const { UV_ECONNREFUSED } = internalBinding('uv');++// When the last attempt fails while an earlier one is still pending, the+// pending attempt gets one more attempt timeout instead of waiting for the+// operating system. Errors are reported in attempt order.+TCP.prototype.connect = function(req, address, port) {+ if (address === '10.0.0.2') {+ setImmediate(() => req.oncomplete(UV_ECONNREFUSED, this, req, false, false));+ }+ // Attempts to 10.0.0.1 never complete.+ return 0;+};++const connection = createConnection({+ host: 'example.org',+ port: 10,+ lookup: common.mustCall((host, options, callback) => {+ process.nextTick(callback, null, [+ { address: '10.0.0.1', family: 4 },+ { address: '10.0.0.2', family: 4 },+ ]);+ }),+ autoSelectFamily: true,+ autoSelectFamilyAttemptTimeout: 10,+});++connection.on('connectionAttemptTimeout', common.mustCall((address) => {+ assert.strictEqual(address, '10.0.0.1');+}));+connection.on('connectionAttemptFailed', common.mustCall((address) => {+ assert.strictEqual(address, '10.0.0.2');+}));+connection.on('connect', common.mustNotCall());+connection.on('error', common.mustCall((error) => {+ assert.strictEqual(error.constructor.name, 'AggregateError');+ assert.deepStrictEqual(error.errors.map((e) => [e.address, e.code]), [+ ['10.0.0.1', 'ETIMEDOUT'],+ ['10.0.0.2', 'ECONNREFUSED'],+ ]);+}));+connection.on('close', common.mustCall());test/parallel/test-net-autoselectfamily-parallel-winner.jsadded55 + / 0 −
@@ -0,0 +1,55 @@+'use strict';++const common = require('../common');+if (!common.hasIPv6) common.skip('IPv6 loopback is unavailable');++const assert = require('node:assert');+const { createConnection, createServer } = require('node:net');+const { TCP } = process.binding('tcp_wrap');++// Hold the IPv6 completion while the IPv4 connection wins. A late completion+// for the losing handle must not emit another 'connect' or close the winner.+const connect = TCP.prototype.connect6;+let completeFirst;+let winnerSelected = false;+TCP.prototype.connect6 = function(req, address, port) {+ const oncomplete = req.oncomplete;+ req.oncomplete = (...args) => {+ const complete = () => oncomplete(...args);+ if (winnerSelected) setImmediate(complete);+ else completeFirst = complete;+ };+ return Reflect.apply(connect, this, [req, address, port]);+};++const ipv6 = createServer((socket) => socket.end());+const ipv4 = createServer(common.mustCall((socket) => socket.end()));+ipv6.listen(0, '::1', common.mustCall(() => {+ const port = ipv6.address().port;+ ipv4.listen(port, '127.0.0.1', common.mustCall(() => {+ const connection = createConnection({+ host: 'example.org',+ port,+ lookup: common.mustCall((host, options, callback) => {+ process.nextTick(callback, null, [+ { address: '::1', family: 6 },+ { address: '127.0.0.1', family: 4 },+ ]);+ }),+ autoSelectFamily: true,+ autoSelectFamilyAttemptTimeout: 10,+ });+ connection.on('connect', common.mustCall(() => {+ winnerSelected = true;+ assert.strictEqual(connection.remoteAddress, '127.0.0.1');+ assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses,+ [`::1:${port}`, `127.0.0.1:${port}`]);+ if (completeFirst) setImmediate(completeFirst);+ }));+ connection.on('error', common.mustNotCall());+ connection.on('close', common.mustCall(() => {+ ipv4.close();+ ipv6.close();+ }));+ }));+}));test/parallel/test-net-autoselectfamily-parallel.jsadded61 + / 0 −
@@ -0,0 +1,61 @@+'use strict';++const common = require('../common');+const assert = require('node:assert');+const { createConnection, createServer } = require('node:net');+const { TCP } = process.binding('tcp_wrap');++// Delay delivery of a successful TCP completion until the other address has+// failed. The first connection must remain usable after fallback has started.+const connect = TCP.prototype.connect;+let firstCompletion;+let secondFailed = false;+TCP.prototype.connect = function(req, address, port) {+ if (address === '127.0.0.1') {+ const oncomplete = req.oncomplete;+ req.oncomplete = (...args) => {+ const complete = () => oncomplete(...args);+ if (secondFailed) {+ setImmediate(complete);+ } else {+ firstCompletion = complete;+ }+ };+ }+ return Reflect.apply(connect, this, [req, address, port]);+};++const server = createServer(common.mustCall((socket) => socket.end()));+server.listen(0, '127.0.0.1', common.mustCall(() => {+ const port = server.address().port;+ const secondAddress = common.hasIPv6 ? '::1' : '127.0.0.2';+ const connection = createConnection({+ host: 'example.org',+ port,+ lookup: common.mustCall((host, options, callback) => {+ assert.strictEqual(options.all, true);+ process.nextTick(callback, null, [+ { address: '127.0.0.1', family: 4 },+ { address: secondAddress, family: common.hasIPv6 ? 6 : 4 },+ ]);+ }),+ autoSelectFamily: true,+ autoSelectFamilyAttemptTimeout: 10,+ });++ connection.on('connectionAttemptTimeout', common.mustCall((address) => {+ assert.strictEqual(address, '127.0.0.1');+ }));+ connection.on('connectionAttemptFailed', common.mustCall((address) => {+ assert.strictEqual(address, secondAddress);+ secondFailed = true;+ if (firstCompletion) setImmediate(firstCompletion);+ }));+ connection.on('connect', common.mustCall(() => {+ assert.strictEqual(connection.remoteAddress, '127.0.0.1');+ assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses,+ [`127.0.0.1:${port}`, `${secondAddress}:${port}`]);+ }));+ connection.on('error', common.mustNotCall());+ connection.on('close', common.mustCall(() => server.close()));+}));