net: rework autoSelectFamily implementation

PR-URL: https://github.com/nodejs/node/pull/46587
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Paolo Insogna
2023-02-23 01:47:13 -08:00
committed by GitHub
parent 2636b55f0d
commit d12d8cd578
16 changed files with 250 additions and 128 deletions

View File

@@ -941,7 +941,7 @@ For TCP connections, available `options` are:
* `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:** `250`.
**Default:** initially `250`, but it can be changed at runtime using [`net.setDefaultAutoSelectFamilyAttemptTimeout(value)`][]
For [IPC][] connections, available `options` are:
@@ -1528,26 +1528,6 @@ immediately initiates connection with
[`socket.connect(port[, host][, connectListener])`][`socket.connect(port)`],
then returns the `net.Socket` that starts the connection.
## `net.setDefaultAutoSelectFamily(value)`
<!-- YAML
added: v19.4.0
-->
Sets the default value of the `autoSelectFamily` option of [`socket.connect(options)`][].
* `value` {boolean} The new default value. The initial default value is `false`.
## `net.getDefaultAutoSelectFamily()`
<!-- YAML
added: v19.4.0
-->
Gets the current default value of the `autoSelectFamily` option of [`socket.connect(options)`][].
* Returns: {boolean} The current default value of the `autoSelectFamily` option.
## `net.createServer([options][, connectionListener])`
<!-- YAML
@@ -1642,6 +1622,47 @@ Use `nc` to connect to a Unix domain socket server:
$ nc -U /tmp/echo.sock
```
## `net.getDefaultAutoSelectFamily()`
<!-- YAML
added: v19.4.0
-->
Gets the current default value of the `autoSelectFamily` option of [`socket.connect(options)`][].
* Returns: {boolean} The current default value of the `autoSelectFamily` option.
## `net.setDefaultAutoSelectFamily(value)`
<!-- YAML
added: v19.4.0
-->
Sets the default value of the `autoSelectFamily` option of [`socket.connect(options)`][].
* `value` {boolean} The new default value. The initial default value is `false`.
## `net.getDefaultAutoSelectFamilyAttemptTimeout()`
<!-- YAML
added: REPLACEME
-->
Gets the current default value of the `autoSelectFamilyAttemptTimeout` option of [`socket.connect(options)`][].
* Returns: {number} The current default value of the `autoSelectFamilyAttemptTimeout` option.
## `net.setDefaultAutoSelectFamilyAttemptTimeout(value)`
<!-- YAML
added: REPLACEME
-->
Sets the default value of the `autoSelectFamilyAttemptTimeout` option of [`socket.connect(options)`][].
* `value` {number} The new default value, which must be a positive number. If the number is less than `10`,
the value `10` is used insted The initial default value is `250`.
## `net.isIP(input)`
<!-- YAML
@@ -1727,6 +1748,7 @@ net.isIPv6('fhqwhgads'); // returns false
[`net.createConnection(port, host)`]: #netcreateconnectionport-host-connectlistener
[`net.createServer()`]: #netcreateserveroptions-connectionlistener
[`net.setDefaultAutoSelectFamily(value)`]: #netsetdefaultautoselectfamilyvalue
[`net.setDefaultAutoSelectFamilyAttemptTimeout(value)`]: #netsetdefaultautoselectfamilyattempttimeoutvalue
[`new net.Socket(options)`]: #new-netsocketoptions
[`readable.setEncoding()`]: stream.md#readablesetencodingencoding
[`server.close()`]: #serverclosecallback

View File

@@ -54,7 +54,7 @@ const EE = require('events');
const net = require('net');
const tls = require('tls');
const common = require('_tls_common');
const { kWrapConnectedHandle } = require('internal/net');
const { kReinitializeHandle } = require('internal/net');
const JSStreamSocket = require('internal/js_stream_socket');
const { Buffer } = require('buffer');
let debug = require('internal/util/debuglog').debuglog('tls', (fn) => {
@@ -633,14 +633,27 @@ TLSSocket.prototype._wrapHandle = function(wrap, handle) {
return res;
};
TLSSocket.prototype[kWrapConnectedHandle] = function(handle) {
this._handle = this._wrapHandle(null, handle);
TLSSocket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
const originalServername = this._handle.getServername();
const originalSession = this._handle.getSession();
this.handle = this._wrapHandle(null, handle);
this.ssl = this._handle;
net.Socket.prototype[kReinitializeHandle].call(this, this.handle);
this._init();
if (this._tlsOptions.enableTrace) {
this._handle.enableTrace();
}
if (originalSession) {
this.setSession(originalSession);
}
if (originalServername) {
this.setServername(originalServername);
}
};
// This eliminates a cyclic reference to TLSWrap
@@ -679,6 +692,30 @@ TLSSocket.prototype._destroySSL = function _destroySSL() {
this[kIsVerified] = false;
};
function keylogNewListener(event) {
if (event !== 'keylog')
return;
// Guard against enableKeylogCallback after destroy
if (!this._handle) return;
this._handle.enableKeylogCallback();
// Remove this listener since it's no longer needed.
this.removeListener('newListener', keylogNewListener);
}
function newListener(event) {
if (event !== 'session')
return;
// Guard against enableSessionCallbacks after destroy
if (!this._handle) return;
this._handle.enableSessionCallbacks();
// Remove this listener since it's no longer needed.
this.removeListener('newListener', newListener);
}
// Constructor guts, arbitrarily factored out.
let warnOnTlsKeylog = true;
let warnOnTlsKeylogError = true;
@@ -704,18 +741,9 @@ TLSSocket.prototype._init = function(socket, wrap) {
// Only call .onkeylog if there is a keylog listener.
ssl.onkeylog = onkeylog;
this.on('newListener', keylogNewListener);
function keylogNewListener(event) {
if (event !== 'keylog')
return;
// Guard against enableKeylogCallback after destroy
if (!this._handle) return;
this._handle.enableKeylogCallback();
// Remove this listener since it's no longer needed.
this.removeListener('newListener', keylogNewListener);
if (this.listenerCount('newListener', keylogNewListener) === 0) {
this.on('newListener', keylogNewListener);
}
if (options.isServer) {
@@ -750,18 +778,8 @@ TLSSocket.prototype._init = function(socket, wrap) {
ssl.onnewsession = onnewsessionclient;
// Only call .onnewsession if there is a session listener.
this.on('newListener', newListener);
function newListener(event) {
if (event !== 'session')
return;
// Guard against enableSessionCallbacks after destroy
if (!this._handle) return;
this._handle.enableSessionCallbacks();
// Remove this listener since it's no longer needed.
this.removeListener('newListener', newListener);
if (this.listenerCount('newListener', newListener) === 0) {
this.on('newListener', newListener);
}
}

View File

@@ -67,7 +67,7 @@ function makeSyncWrite(fd) {
}
module.exports = {
kWrapConnectedHandle: Symbol('wrapConnectedHandle'),
kReinitializeHandle: Symbol('reinitializeHandle'),
isIP,
isIPv4,
isIPv6,

View File

@@ -42,7 +42,7 @@ let debug = require('internal/util/debuglog').debuglog('net', (fn) => {
debug = fn;
});
const {
kWrapConnectedHandle,
kReinitializeHandle,
isIP,
isIPv4,
isIPv6,
@@ -53,7 +53,8 @@ const assert = require('internal/assert');
const {
UV_EADDRINUSE,
UV_EINVAL,
UV_ENOTCONN
UV_ENOTCONN,
UV_ECANCELED
} = internalBinding('uv');
const { Buffer } = require('buffer');
@@ -127,6 +128,7 @@ let dns;
let BlockList;
let SocketAddress;
let autoSelectFamilyDefault = getOptionValue('--enable-network-family-autoselection');
let autoSelectFamilyAttemptTimeoutDefault = 250;
const { clearTimeout, setTimeout } = require('timers');
const { kTimeout } = require('internal/timers');
@@ -238,6 +240,20 @@ function setDefaultAutoSelectFamily(value) {
autoSelectFamilyDefault = value;
}
function getDefaultAutoSelectFamilyAttemptTimeout() {
return autoSelectFamilyAttemptTimeoutDefault;
}
function setDefaultAutoSelectFamilyAttemptTimeout(value) {
validateInt32(value, 'value', 1);
if (value < 1) {
value = 10;
}
autoSelectFamilyAttemptTimeoutDefault = value;
}
// Returns an array [options, cb], where options is an object,
// cb is either a function or null.
// Used to normalize arguments of Socket.prototype.connect() and
@@ -678,7 +694,11 @@ function tryReadStart(socket) {
// Just call handle.readStart until we have enough in the buffer
Socket.prototype._read = function(n) {
debug('_read');
debug(
'_read - n', n,
'isConnecting?', !!this.connecting,
'hasHandle?', !!this._handle,
);
if (this.connecting || !this._handle) {
debug('_read wait for connection');
@@ -1017,7 +1037,7 @@ function internalConnect(
localAddress = localAddress || DEFAULT_IPV6_ADDR;
err = self._handle.bind6(localAddress, localPort, flags);
}
debug('binding to localAddress: %s and localPort: %d (addressType: %d)',
debug('connect: binding to localAddress: %s and localPort: %d (addressType: %d)',
localAddress, localPort, addressType);
err = checkBindError(err, localPort, self._handle);
@@ -1028,6 +1048,8 @@ function internalConnect(
}
}
debug('connect: attempting to connect to %s:%d (addressType: %d)', address, port, addressType);
if (addressType === 6 || addressType === 4) {
const req = new TCPConnectWrap();
req.oncomplete = afterConnect;
@@ -1064,20 +1086,21 @@ function internalConnect(
}
function internalConnectMultiple(context) {
function internalConnectMultiple(context, canceled) {
clearTimeout(context[kTimeout]);
const self = context.socket;
assert(self.connecting);
// All connections have been tried without success, destroy with error
if (context.current === context.addresses.length) {
if (canceled || context.current === context.addresses.length) {
self.destroy(aggregateErrors(context.errors));
return;
}
assert(self.connecting);
const handle = context.current === 0 ? self._handle : new TCP(TCPConstants.SOCKET);
const { localPort, port, flags } = context;
const { address, family: addressType } = context.addresses[context.current++];
const handle = new TCP(TCPConstants.SOCKET);
let localAddress;
let err;
@@ -1101,6 +1124,8 @@ function internalConnectMultiple(context) {
}
}
debug('connect/multiple: attempting to connect to %s:%d (addressType: %d)', address, port, addressType);
const req = new TCPConnectWrap();
req.oncomplete = FunctionPrototypeBind(afterConnectMultiple, undefined, context);
req.address = address;
@@ -1190,6 +1215,15 @@ Socket.prototype.connect = function(...args) {
return this;
};
Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
this._handle?.close();
this._handle = handle;
this._handle[owner_symbol] = this;
initSocketHandle(this);
};
function socketToDnsFamily(family) {
switch (family) {
case 'IPv4':
@@ -1237,7 +1271,7 @@ function lookupAndConnect(self, options) {
autoSelectFamilyAttemptTimeout = 10;
}
} else {
autoSelectFamilyAttemptTimeout = 250;
autoSelectFamilyAttemptTimeout = autoSelectFamilyAttemptTimeoutDefault;
}
// If host is an IP, skip performing a lookup
@@ -1279,17 +1313,19 @@ function lookupAndConnect(self, options) {
debug('connect: autodetecting');
dnsopts.all = true;
lookupAndConnectMultiple(
self,
async_id_symbol,
lookup,
host,
options,
dnsopts,
port,
localPort,
autoSelectFamilyAttemptTimeout,
);
defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
lookupAndConnectMultiple(
self,
async_id_symbol,
lookup,
host,
options,
dnsopts,
port,
localPort,
autoSelectFamilyAttemptTimeout,
);
});
return;
}
@@ -1337,6 +1373,8 @@ function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options,
if (!self.connecting) {
return;
} else if (err) {
self.emit('lookup', err, undefined, undefined, host);
// net.createConnection() creates a net.Socket object and immediately
// calls net.Socket.connect() on it (that's us). There are no event
// listeners registered yet so defer the error event to the next tick.
@@ -1529,7 +1567,7 @@ function afterConnectMultiple(context, status, handle, req, readable, writable)
ArrayPrototypePush(context.errors, ex);
// Try the next address
internalConnectMultiple(context);
internalConnectMultiple(context, status === UV_ECANCELED);
return;
}
@@ -1540,13 +1578,9 @@ function afterConnectMultiple(context, status, handle, req, readable, writable)
return;
}
// Perform initialization sequence on the handle, then move on with the regular callback
self._handle = handle;
initSocketHandle(self);
if (self[kWrapConnectedHandle]) {
self[kWrapConnectedHandle](handle);
initSocketHandle(self); // This is called again to initialize the TLSWrap
if (context.current > 1 && self[kReinitializeHandle]) {
self[kReinitializeHandle](handle);
handle = self._handle;
}
if (hasObserver('net')) {
@@ -2248,4 +2282,6 @@ module.exports = {
Stream: Socket, // Legacy naming
getDefaultAutoSelectFamily,
setDefaultAutoSelectFamily,
getDefaultAutoSelectFamilyAttemptTimeout,
setDefaultAutoSelectFamilyAttemptTimeout,
};

View File

@@ -7,14 +7,12 @@ const assert = require('assert');
const dgram = require('dgram');
const { Resolver } = require('dns');
const { request, createServer } = require('http');
const { setDefaultAutoSelectFamilyAttemptTimeout } = require('net');
// Test that happy eyeballs algorithm is properly implemented when using HTTP.
let autoSelectFamilyAttemptTimeout = common.platformTimeout(250);
if (common.isWindows) {
// Some of the windows machines in the CI need more time to establish connection
autoSelectFamilyAttemptTimeout = common.platformTimeout(1500);
}
// Some of the windows machines in the CI need more time to establish connection
setDefaultAutoSelectFamilyAttemptTimeout(common.platformTimeout(common.isWindows ? 1500 : 250));
function _lookup(resolver, hostname, options, cb) {
resolver.resolve(hostname, 'ANY', (err, replies) => {
@@ -77,7 +75,6 @@ function createDnsServer(ipv6Addr, ipv4Addr, cb) {
{
lookup,
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout
},
(res) => {
assert.strictEqual(res.statusCode, 200);
@@ -122,7 +119,6 @@ if (common.hasIPv6) {
{
lookup,
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout,
},
(res) => {
assert.strictEqual(res.statusCode, 200);

View File

@@ -11,6 +11,14 @@ const v8 = require('v8');
// after it is destroyed, either because they are detached from it or have been
// destroyed themselves.
// We use an higher autoSelectFamilyAttemptTimeout in this test as the v8.getHeapSnapshot().resume()
// will slow the connection flow and we don't want the second connection attempt to start.
let autoSelectFamilyAttemptTimeout = common.platformTimeout(1000);
if (common.isWindows) {
// Some of the windows machines in the CI need more time to establish connection
autoSelectFamilyAttemptTimeout = common.platformTimeout(10000);
}
for (const variant of ['ping', 'settings']) {
const server = http2.createServer();
server.on('session', common.mustCall((session) => {
@@ -30,7 +38,7 @@ for (const variant of ['ping', 'settings']) {
}));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`,
const client = http2.connect(`http://localhost:${server.address().port}`, { autoSelectFamilyAttemptTimeout },
common.mustCall());
client.on('error', (err) => {
// We destroy the session so it's possible to get ECONNRESET here.

View File

@@ -13,6 +13,7 @@ const assert = require('assert');
const dgram = require('dgram');
const { Resolver } = require('dns');
const { request, createServer } = require('https');
const { setDefaultAutoSelectFamilyAttemptTimeout } = require('net');
if (!common.hasCrypto)
common.skip('missing crypto');
@@ -24,11 +25,8 @@ const options = {
// Test that happy eyeballs algorithm is properly implemented when using HTTP.
let autoSelectFamilyAttemptTimeout = common.platformTimeout(250);
if (common.isWindows) {
// Some of the windows machines in the CI need more time to establish connection
autoSelectFamilyAttemptTimeout = common.platformTimeout(1500);
}
// Some of the windows machines in the CI need more time to establish connection
setDefaultAutoSelectFamilyAttemptTimeout(common.platformTimeout(common.isWindows ? 1500 : 250));
function _lookup(resolver, hostname, options, cb) {
resolver.resolve(hostname, 'ANY', (err, replies) => {
@@ -92,7 +90,6 @@ function createDnsServer(ipv6Addr, ipv4Addr, cb) {
lookup,
rejectUnauthorized: false,
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout
},
(res) => {
assert.strictEqual(res.statusCode, 200);
@@ -138,7 +135,6 @@ if (common.hasIPv6) {
lookup,
rejectUnauthorized: false,
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout,
},
(res) => {
assert.strictEqual(res.statusCode, 200);

View File

@@ -8,15 +8,12 @@ const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
const assert = require('assert');
const dgram = require('dgram');
const { Resolver } = require('dns');
const { createConnection, createServer } = require('net');
const { createConnection, createServer, setDefaultAutoSelectFamilyAttemptTimeout } = require('net');
// Test that happy eyeballs algorithm can be enable from command line.
let autoSelectFamilyAttemptTimeout = common.platformTimeout(250);
if (common.isWindows) {
// Some of the windows machines in the CI need more time to establish connection
autoSelectFamilyAttemptTimeout = common.platformTimeout(1500);
}
// Some of the windows machines in the CI need more time to establish connection
setDefaultAutoSelectFamilyAttemptTimeout(common.platformTimeout(common.isWindows ? 1500 : 250));
function _lookup(resolver, hostname, options, cb) {
resolver.resolve(hostname, 'ANY', (err, replies) => {
@@ -82,7 +79,6 @@ function createDnsServer(ipv6Addr, ipv4Addr, cb) {
host: 'example.org',
port: port,
lookup,
autoSelectFamilyAttemptTimeout,
});
let response = '';

View File

@@ -6,15 +6,12 @@ const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
const assert = require('assert');
const dgram = require('dgram');
const { Resolver } = require('dns');
const { createConnection, createServer } = require('net');
const { createConnection, createServer, setDefaultAutoSelectFamilyAttemptTimeout } = require('net');
// Test that happy eyeballs algorithm is properly implemented when a A record is returned first.
let autoSelectFamilyAttemptTimeout = common.platformTimeout(250);
if (common.isWindows) {
// Some of the windows machines in the CI need more time to establish connection
autoSelectFamilyAttemptTimeout = common.platformTimeout(1500);
}
// Some of the windows machines in the CI need more time to establish connection
setDefaultAutoSelectFamilyAttemptTimeout(common.platformTimeout(common.isWindows ? 1500 : 250));
function _lookup(resolver, hostname, options, cb) {
resolver.resolve(hostname, 'ANY', (err, replies) => {
@@ -88,7 +85,6 @@ if (common.hasIPv6) {
port,
lookup,
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout
});
let response = '';

View File

@@ -10,11 +10,11 @@ const { createConnection, createServer } = require('net');
// Test that happy eyeballs algorithm is properly implemented.
let autoSelectFamilyAttemptTimeout = common.platformTimeout(250);
if (common.isWindows) {
// Some of the windows machines in the CI need more time to establish connection
autoSelectFamilyAttemptTimeout = common.platformTimeout(1500);
}
// Purposely not using setDefaultAutoSelectFamilyAttemptTimeout here to test the
// parameter is correctly used in options.
//
// Some of the windows machines in the CI need more time to establish connection
const autoSelectFamilyAttemptTimeout = common.platformTimeout(common.isWindows ? 1500 : 250);
function _lookup(resolver, hostname, options, cb) {
resolver.resolve(hostname, 'ANY', (err, replies) => {

View File

@@ -26,13 +26,22 @@ function check(addressType, cb) {
function lookup(host, dnsopts, cb) {
dnsopts.family = addressType;
if (addressType === 4) {
process.nextTick(function() {
cb(null, common.localhostIPv4, 4);
if (dnsopts.all) {
cb(null, [{ address: common.localhostIPv4, family: 4 }]);
} else {
cb(null, common.localhostIPv4, 4);
}
});
} else {
process.nextTick(function() {
cb(null, '::1', 6);
if (dnsopts.all) {
cb(null, [{ address: '::1', family: 6 }]);
} else {
cb(null, '::1', 6);
}
});
}
}
@@ -48,7 +57,11 @@ check(4, function() {
host: 'localhost',
port: 80,
lookup(host, dnsopts, cb) {
cb(null, undefined, 4);
if (dnsopts.all) {
cb(null, [{ address: undefined, family: 4 }]);
} else {
cb(null, undefined, 4);
}
}
}).on('error', common.expectsError({ code: 'ERR_INVALID_IP_ADDRESS' }));
}

View File

@@ -31,10 +31,10 @@ const server = net.createServer(function(client) {
server.listen(0, common.mustCall(function() {
net.connect(this.address().port, 'localhost')
.on('lookup', common.mustCall(function(err, ip, type, host) {
.on('lookup', common.mustCallAtLeast(function(err, ip, type, host) {
assert.strictEqual(err, null);
assert.match(ip, /^(127\.0\.0\.1|::1)$/);
assert.match(type.toString(), /^(4|6)$/);
assert.strictEqual(host, 'localhost');
}));
}, 1));
}));

View File

@@ -36,7 +36,11 @@ function connectDoesNotThrow(input) {
{
// Verify that an error is emitted when an invalid address family is returned.
const s = connectDoesNotThrow((host, options, cb) => {
cb(null, '127.0.0.1', 100);
if (options.all) {
cb(null, [{ address: '127.0.0.1', family: 100 }]);
} else {
cb(null, '127.0.0.1', 100);
}
});
s.on('error', common.expectsError({

View File

@@ -20,17 +20,11 @@ server.on('close', common.mustCall());
assert.strictEqual(server, server.listen(0, () => {
net.createConnection(server.address().port)
.on('error', common.mustCall(
common.expectsError({
code: 'ECONNRESET',
name: 'Error'
}))
);
.on('error', common.mustCall((error) => {
assert.strictEqual(error.code, 'ECONNRESET');
}));
net.createConnection(server.address().port)
.on('error', common.mustCall(
common.expectsError({
code: 'ECONNRESET',
name: 'Error'
}))
);
.on('error', common.mustCall((error) => {
assert.strictEqual(error.code, 'ECONNRESET');
}));
}));

View File

@@ -1,5 +1,7 @@
'use strict';
require('../common');
const assert = require('assert');
const net = require('net');
@@ -11,4 +13,8 @@ for (const autoSelectFamilyAttemptTimeout of [-10, 0]) {
autoSelectFamilyAttemptTimeout,
});
}, { code: 'ERR_OUT_OF_RANGE' });
assert.throws(() => {
net.setDefaultAutoSelectFamilyAttemptTimeout(autoSelectFamilyAttemptTimeout);
}, { code: 'ERR_OUT_OF_RANGE' });
}

View File

@@ -0,0 +1,37 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
const { setDefaultAutoSelectFamilyAttemptTimeout } = require('net');
const { connect } = require('tls');
// Some of the windows machines in the CI need more time to establish connection
setDefaultAutoSelectFamilyAttemptTimeout(common.platformTimeout(common.isWindows ? 1500 : 250));
// Test that TLS connecting works without autoSelectFamily
{
const socket = connect({
host: 'google.com',
port: 443,
servername: 'google.com',
autoSelectFamily: false,
});
socket.on('secureConnect', common.mustCall(() => socket.end()));
}
// Test that TLS connecting works with autoSelectFamily
{
const socket = connect({
host: 'google.com',
port: 443,
servername: 'google.com',
autoSelectFamily: true,
});
socket.on('secureConnect', common.mustCall(() => socket.end()));
}