net: race family autoselection attempts

Keep pending TCP connections alive when starting
 fallback attempts. The first successful connection wins; fixed
 local ports retain sequential attempts.

Assisted-by: pi coding agent
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/66229
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
Matteo Collina authored and GitHub committed 2026-10-05 21:26:47 +00:00
1 parent 019e869ad3
commit 9746ebce07
10 files changed
+540 -115

No files matched your search

+27 -19
View File
@@ -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][].
+1 -1
View File
@@ -730,7 +730,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',
+196 -87
View File
@@ -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,72 +1488,181 @@ function internalConnect(
}
function internalConnectMultiple(context, canceled) {
function closeConnectionAttempts(context) {
clearTimeout(context[kTimeout]);
const self = context.socket;
// We were requested to abort. Stop all operations
if (self._aborted) {
return;
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;
}
// 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;
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;
}
}
}
self.destroy(new NodeAggregateError(context.errors));
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;
}
if (context.socketHandleState === kSocketHandleFailed) {
const unrefed = self._handle.hasRef() === false;
self[kReinitializeHandle](new TCP(TCPConstants.SOCKET));
if (unrefed) self._handle.unref();
}
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;
// 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;
}
// 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();
}
}
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++;
if (current > 0) {
self[kReinitializeHandle](new TCP(TCPConstants.SOCKET));
}
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;
@@ -1550,34 +1670,23 @@ function internalConnectMultiple(context, canceled) {
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,36 +2265,33 @@ 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);
// 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();
return;
}
const attempt = removePendingAttempt(context, req);
if (attempt === undefined) return;
const self = context.socket;
// Some error occurred, add to the list of exceptions
if (status !== 0) {
const ex = createConnectionError(req, status);
ArrayPrototypePush(context.errors, 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);
}
if (context.done || !self.connecting) {
if (!attempt.own) attempt.handle.close();
return;
}
if (status !== 0) {
releaseAttemptHandle(context, attempt.handle, attempt.own);
const ex = createConnectionError(req, status);
context.errors[attempt.index] = ex;
self.emit('connectionAttemptFailed', req.address, req.port, req.addressType, ex);
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,
@@ -2175,24 +2299,9 @@ function afterConnectMultiple(context, current, status, handle, req, readable, w
{ 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;
@@ -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}`,
);
@@ -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()));
}));
}));
}));
@@ -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();
}));
}));
@@ -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()));
}));
@@ -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());
@@ -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();
}));
}));
}));
@@ -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()));
}));