mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
1 parent
019e869ad3
commit
9746ebce07
10 files changed
+540
-115
No files matched your search
+27
-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][].
|
||||
|
||||
@@ -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
@@ -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()));
|
||||
}));
|
||||
Reference in new issue
Block a user