mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
net: add autoSelectFamily global getter and setter
PR-URL: https://github.com/nodejs/node/pull/45777 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
@@ -314,6 +314,15 @@ added: v6.0.0
|
||||
Enable FIPS-compliant crypto at startup. (Requires Node.js to be built
|
||||
against FIPS-compatible OpenSSL.)
|
||||
|
||||
### `--enable-network-family-autoselection`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
Enables the family autoselection algorithm unless connection options explicitly
|
||||
disables it.
|
||||
|
||||
### `--enable-source-maps`
|
||||
|
||||
<!-- YAML
|
||||
@@ -1870,6 +1879,7 @@ Node.js options that are allowed are:
|
||||
* `--disable-proto`
|
||||
* `--dns-result-order`
|
||||
* `--enable-fips`
|
||||
* `--enable-network-family-autoselection`
|
||||
* `--enable-source-maps`
|
||||
* `--experimental-abortcontroller`
|
||||
* `--experimental-import-meta-resolve`
|
||||
|
||||
@@ -780,6 +780,20 @@ Returns the bound `address`, the address `family` name and `port` of the
|
||||
socket as reported by the operating system:
|
||||
`{ port: 12346, family: 'IPv4', address: '127.0.0.1' }`
|
||||
|
||||
### `socket.autoSelectFamilyAttemptedAddresses`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* {string\[]}
|
||||
|
||||
This property is only present if the family autoselection algorithm is enabled in
|
||||
[`socket.connect(options)`][] and it is an array of the addresses that have been attempted.
|
||||
|
||||
Each address is a string in the form of `$IP:$PORT`. If the connection was successful,
|
||||
then the last address is the one that the socket is currently connected to.
|
||||
|
||||
### `socket.bufferSize`
|
||||
|
||||
<!-- YAML
|
||||
@@ -856,6 +870,11 @@ behavior.
|
||||
<!-- YAML
|
||||
added: v0.1.90
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/45777
|
||||
description: The default value for autoSelectFamily option can be changed
|
||||
at runtime using `setDefaultAutoSelectFamily` or via the
|
||||
command line option `--enable-network-family-autoselection`.
|
||||
- version: v19.3.0
|
||||
pr-url: https://github.com/nodejs/node/pull/44731
|
||||
description: Added the `autoSelectFamily` option.
|
||||
@@ -909,12 +928,14 @@ For TCP connections, available `options` are:
|
||||
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 and so on.
|
||||
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 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.
|
||||
**Default:** `false`.
|
||||
**Default:** initially `false`, but it can be changed at runtime using [`net.setDefaultAutoSelectFamily(value)`][]
|
||||
or via the command line option `--enable-network-family-autoselection`.
|
||||
* `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.
|
||||
@@ -1495,6 +1516,26 @@ 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: REPLACEME
|
||||
-->
|
||||
|
||||
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: REPLACEME
|
||||
-->
|
||||
|
||||
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
|
||||
@@ -1673,6 +1714,7 @@ net.isIPv6('fhqwhgads'); // returns false
|
||||
[`net.createConnection(path)`]: #netcreateconnectionpath-connectlistener
|
||||
[`net.createConnection(port, host)`]: #netcreateconnectionport-host-connectlistener
|
||||
[`net.createServer()`]: #netcreateserveroptions-connectionlistener
|
||||
[`net.setDefaultAutoSelectFamily(value)`]: #netsetdefaultautoselectfamilyvalue
|
||||
[`new net.Socket(options)`]: #new-netsocketoptions
|
||||
[`readable.setEncoding()`]: stream.md#readablesetencodingencoding
|
||||
[`server.close()`]: #serverclosecallback
|
||||
|
||||
31
lib/net.js
31
lib/net.js
@@ -118,12 +118,14 @@ const {
|
||||
validateString
|
||||
} = require('internal/validators');
|
||||
const kLastWriteQueueSize = Symbol('lastWriteQueueSize');
|
||||
const { getOptionValue } = require('internal/options');
|
||||
|
||||
// Lazy loaded to improve startup performance.
|
||||
let cluster;
|
||||
let dns;
|
||||
let BlockList;
|
||||
let SocketAddress;
|
||||
let autoSelectFamilyDefault = getOptionValue('--enable-network-family-autoselection');
|
||||
|
||||
const { clearTimeout, setTimeout } = require('timers');
|
||||
const { kTimeout } = require('internal/timers');
|
||||
@@ -226,6 +228,14 @@ function connect(...args) {
|
||||
return socket.connect(normalized);
|
||||
}
|
||||
|
||||
function getDefaultAutoSelectFamily() {
|
||||
return autoSelectFamilyDefault;
|
||||
}
|
||||
|
||||
function setDefaultAutoSelectFamily(value) {
|
||||
validateBoolean(value, 'value');
|
||||
autoSelectFamilyDefault = value;
|
||||
}
|
||||
|
||||
// Returns an array [options, cb], where options is an object,
|
||||
// cb is either a function or null.
|
||||
@@ -1092,6 +1102,8 @@ function internalConnectMultiple(context) {
|
||||
req.localAddress = localAddress;
|
||||
req.localPort = localPort;
|
||||
|
||||
ArrayPrototypePush(self.autoSelectFamilyAttemptedAddresses, `${address}:${port}`);
|
||||
|
||||
if (addressType === 4) {
|
||||
err = handle.connect(req, address, port);
|
||||
} else {
|
||||
@@ -1184,9 +1196,9 @@ function socketToDnsFamily(family) {
|
||||
}
|
||||
|
||||
function lookupAndConnect(self, options) {
|
||||
const { localAddress, localPort, autoSelectFamily } = options;
|
||||
const { localAddress, localPort } = options;
|
||||
const host = options.host || 'localhost';
|
||||
let { port, autoSelectFamilyAttemptTimeout } = options;
|
||||
let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options;
|
||||
|
||||
if (localAddress && !isIP(localAddress)) {
|
||||
throw new ERR_INVALID_IP_ADDRESS(localAddress);
|
||||
@@ -1205,11 +1217,14 @@ function lookupAndConnect(self, options) {
|
||||
}
|
||||
port |= 0;
|
||||
|
||||
if (autoSelectFamily !== undefined) {
|
||||
validateBoolean(autoSelectFamily);
|
||||
|
||||
if (autoSelectFamily != null) {
|
||||
validateBoolean(autoSelectFamily, 'options.autoSelectFamily');
|
||||
} else {
|
||||
autoSelectFamily = autoSelectFamilyDefault;
|
||||
}
|
||||
|
||||
if (autoSelectFamilyAttemptTimeout !== undefined) {
|
||||
if (autoSelectFamilyAttemptTimeout != null) {
|
||||
validateInt32(autoSelectFamilyAttemptTimeout, 'options.autoSelectFamilyAttemptTimeout', 1);
|
||||
|
||||
if (autoSelectFamilyAttemptTimeout < 10) {
|
||||
@@ -1233,7 +1248,7 @@ function lookupAndConnect(self, options) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.lookup !== undefined)
|
||||
if (options.lookup != null)
|
||||
validateFunction(options.lookup, 'options.lookup');
|
||||
|
||||
if (dns === undefined) dns = require('dns');
|
||||
@@ -1370,6 +1385,8 @@ function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options,
|
||||
}
|
||||
}
|
||||
|
||||
self.autoSelectFamilyAttemptedAddresses = [];
|
||||
|
||||
const context = {
|
||||
socket: self,
|
||||
addresses,
|
||||
@@ -2223,4 +2240,6 @@ module.exports = {
|
||||
Server,
|
||||
Socket,
|
||||
Stream: Socket, // Legacy naming
|
||||
getDefaultAutoSelectFamily,
|
||||
setDefaultAutoSelectFamily,
|
||||
};
|
||||
|
||||
@@ -346,6 +346,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
|
||||
"returned)",
|
||||
&EnvironmentOptions::dns_result_order,
|
||||
kAllowedInEnvvar);
|
||||
AddOption("--enable-network-family-autoselection",
|
||||
"Enable network address family autodetection algorithm",
|
||||
&EnvironmentOptions::enable_network_family_autoselection,
|
||||
kAllowedInEnvvar);
|
||||
AddOption("--enable-source-maps",
|
||||
"Source Map V3 support for stack traces",
|
||||
&EnvironmentOptions::enable_source_maps,
|
||||
|
||||
@@ -127,6 +127,7 @@ class EnvironmentOptions : public Options {
|
||||
bool frozen_intrinsics = false;
|
||||
int64_t heap_snapshot_near_heap_limit = 0;
|
||||
std::string heap_snapshot_signal;
|
||||
bool enable_network_family_autoselection = false;
|
||||
uint64_t max_http_header_size = 16 * 1024;
|
||||
bool deprecation = true;
|
||||
bool force_async_hooks_checks = true;
|
||||
|
||||
108
test/parallel/test-net-autoselectfamily-commandline-option.js
Normal file
108
test/parallel/test-net-autoselectfamily-commandline-option.js
Normal file
@@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
// Flags: --enable-network-family-autoselection
|
||||
|
||||
const common = require('../common');
|
||||
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
|
||||
|
||||
const assert = require('assert');
|
||||
const dgram = require('dgram');
|
||||
const { Resolver } = require('dns');
|
||||
const { createConnection, createServer } = 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);
|
||||
}
|
||||
|
||||
function _lookup(resolver, hostname, options, cb) {
|
||||
resolver.resolve(hostname, 'ANY', (err, replies) => {
|
||||
assert.notStrictEqual(options.family, 4);
|
||||
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const hosts = replies
|
||||
.map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 }))
|
||||
.sort((a, b) => b.family - a.family);
|
||||
|
||||
if (options.all === true) {
|
||||
return cb(null, hosts);
|
||||
}
|
||||
|
||||
return cb(null, hosts[0].address, hosts[0].family);
|
||||
});
|
||||
}
|
||||
|
||||
function createDnsServer(ipv6Addr, ipv4Addr, cb) {
|
||||
// Create a DNS server which replies with a AAAA and a A record for the same host
|
||||
const socket = dgram.createSocket('udp4');
|
||||
|
||||
socket.on('message', common.mustCall((msg, { address, port }) => {
|
||||
const parsed = parseDNSPacket(msg);
|
||||
const domain = parsed.questions[0].domain;
|
||||
assert.strictEqual(domain, 'example.org');
|
||||
|
||||
socket.send(writeDNSPacket({
|
||||
id: parsed.id,
|
||||
questions: parsed.questions,
|
||||
answers: [
|
||||
{ type: 'AAAA', address: ipv6Addr, ttl: 123, domain: 'example.org' },
|
||||
{ type: 'A', address: ipv4Addr, ttl: 123, domain: 'example.org' },
|
||||
]
|
||||
}), port, address);
|
||||
}));
|
||||
|
||||
socket.bind(0, () => {
|
||||
const resolver = new Resolver();
|
||||
resolver.setServers([`127.0.0.1:${socket.address().port}`]);
|
||||
|
||||
cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
|
||||
});
|
||||
}
|
||||
|
||||
// Test that IPV4 is reached if IPV6 is not reachable
|
||||
{
|
||||
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
|
||||
const ipv4Server = createServer((socket) => {
|
||||
socket.on('data', common.mustCall(() => {
|
||||
socket.write('response-ipv4');
|
||||
socket.end();
|
||||
}));
|
||||
});
|
||||
|
||||
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
|
||||
const port = ipv4Server.address().port;
|
||||
|
||||
const connection = createConnection({
|
||||
host: 'example.org',
|
||||
port: port,
|
||||
lookup,
|
||||
autoSelectFamilyAttemptTimeout,
|
||||
});
|
||||
|
||||
let response = '';
|
||||
connection.setEncoding('utf-8');
|
||||
|
||||
connection.on('ready', common.mustCall(() => {
|
||||
assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`, `127.0.0.1:${port}`]);
|
||||
}));
|
||||
|
||||
connection.on('data', (chunk) => {
|
||||
response += chunk;
|
||||
});
|
||||
|
||||
connection.on('end', common.mustCall(() => {
|
||||
assert.strictEqual(response, 'response-ipv4');
|
||||
ipv4Server.close();
|
||||
dnsServer.close();
|
||||
}));
|
||||
|
||||
connection.write('request');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
@@ -74,9 +74,11 @@ function createDnsServer(ipv6Addr, ipv4Addr, cb) {
|
||||
});
|
||||
|
||||
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
|
||||
const port = ipv4Server.address().port;
|
||||
|
||||
const connection = createConnection({
|
||||
host: 'example.org',
|
||||
port: ipv4Server.address().port,
|
||||
port: port,
|
||||
lookup,
|
||||
autoSelectFamily: true,
|
||||
autoSelectFamilyAttemptTimeout,
|
||||
@@ -85,6 +87,10 @@ function createDnsServer(ipv6Addr, ipv4Addr, cb) {
|
||||
let response = '';
|
||||
connection.setEncoding('utf-8');
|
||||
|
||||
connection.on('ready', common.mustCall(() => {
|
||||
assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`, `127.0.0.1:${port}`]);
|
||||
}));
|
||||
|
||||
connection.on('data', (chunk) => {
|
||||
response += chunk;
|
||||
});
|
||||
@@ -132,6 +138,10 @@ if (common.hasIPv6) {
|
||||
let response = '';
|
||||
connection.setEncoding('utf-8');
|
||||
|
||||
connection.on('ready', common.mustCall(() => {
|
||||
assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`]);
|
||||
}));
|
||||
|
||||
connection.on('data', (chunk) => {
|
||||
response += chunk;
|
||||
});
|
||||
@@ -162,6 +172,7 @@ if (common.hasIPv6) {
|
||||
|
||||
connection.on('ready', common.mustNotCall());
|
||||
connection.on('error', common.mustCall((error) => {
|
||||
assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, ['::1:10', '127.0.0.1:10']);
|
||||
assert.strictEqual(error.constructor.name, 'AggregateError');
|
||||
assert.strictEqual(error.errors.length, 2);
|
||||
|
||||
@@ -199,6 +210,8 @@ if (common.hasIPv6) {
|
||||
|
||||
connection.on('ready', common.mustNotCall());
|
||||
connection.on('error', common.mustCall((error) => {
|
||||
assert.strictEqual(connection.autoSelectFamilyAttemptedAddresses, undefined);
|
||||
|
||||
if (common.hasIPv6) {
|
||||
assert.strictEqual(error.code, 'ECONNREFUSED');
|
||||
assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`);
|
||||
140
test/parallel/test-net-autoselectfamilydefault.js
Normal file
140
test/parallel/test-net-autoselectfamilydefault.js
Normal file
@@ -0,0 +1,140 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
|
||||
|
||||
const assert = require('assert');
|
||||
const dgram = require('dgram');
|
||||
const { Resolver } = require('dns');
|
||||
const { createConnection, createServer, setDefaultAutoSelectFamily } = require('net');
|
||||
|
||||
// Test that the default for happy eyeballs algorithm is properly respected.
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function _lookup(resolver, hostname, options, cb) {
|
||||
resolver.resolve(hostname, 'ANY', (err, replies) => {
|
||||
assert.notStrictEqual(options.family, 4);
|
||||
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const hosts = replies
|
||||
.map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 }))
|
||||
.sort((a, b) => b.family - a.family);
|
||||
|
||||
if (options.all === true) {
|
||||
return cb(null, hosts);
|
||||
}
|
||||
|
||||
return cb(null, hosts[0].address, hosts[0].family);
|
||||
});
|
||||
}
|
||||
|
||||
function createDnsServer(ipv6Addr, ipv4Addr, cb) {
|
||||
// Create a DNS server which replies with a AAAA and a A record for the same host
|
||||
const socket = dgram.createSocket('udp4');
|
||||
|
||||
socket.on('message', common.mustCall((msg, { address, port }) => {
|
||||
const parsed = parseDNSPacket(msg);
|
||||
const domain = parsed.questions[0].domain;
|
||||
assert.strictEqual(domain, 'example.org');
|
||||
|
||||
socket.send(writeDNSPacket({
|
||||
id: parsed.id,
|
||||
questions: parsed.questions,
|
||||
answers: [
|
||||
{ type: 'AAAA', address: ipv6Addr, ttl: 123, domain: 'example.org' },
|
||||
{ type: 'A', address: ipv4Addr, ttl: 123, domain: 'example.org' },
|
||||
]
|
||||
}), port, address);
|
||||
}));
|
||||
|
||||
socket.bind(0, () => {
|
||||
const resolver = new Resolver();
|
||||
resolver.setServers([`127.0.0.1:${socket.address().port}`]);
|
||||
|
||||
cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
|
||||
});
|
||||
}
|
||||
|
||||
// Test that IPV4 is reached by default if IPV6 is not reachable and the default is enabled
|
||||
{
|
||||
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
|
||||
const ipv4Server = createServer((socket) => {
|
||||
socket.on('data', common.mustCall(() => {
|
||||
socket.write('response-ipv4');
|
||||
socket.end();
|
||||
}));
|
||||
});
|
||||
|
||||
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
|
||||
setDefaultAutoSelectFamily(true);
|
||||
|
||||
const connection = createConnection({
|
||||
host: 'example.org',
|
||||
port: ipv4Server.address().port,
|
||||
lookup,
|
||||
autoSelectFamilyAttemptTimeout,
|
||||
});
|
||||
|
||||
let response = '';
|
||||
connection.setEncoding('utf-8');
|
||||
|
||||
connection.on('data', (chunk) => {
|
||||
response += chunk;
|
||||
});
|
||||
|
||||
connection.on('end', common.mustCall(() => {
|
||||
assert.strictEqual(response, 'response-ipv4');
|
||||
ipv4Server.close();
|
||||
dnsServer.close();
|
||||
}));
|
||||
|
||||
connection.write('request');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
// Test that IPV4 is not reached by default if IPV6 is not reachable and the default is disabled
|
||||
{
|
||||
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
|
||||
const ipv4Server = createServer((socket) => {
|
||||
socket.on('data', common.mustCall(() => {
|
||||
socket.write('response-ipv4');
|
||||
socket.end();
|
||||
}));
|
||||
});
|
||||
|
||||
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
|
||||
setDefaultAutoSelectFamily(false);
|
||||
|
||||
const port = ipv4Server.address().port;
|
||||
|
||||
const connection = createConnection({
|
||||
host: 'example.org',
|
||||
port,
|
||||
lookup,
|
||||
});
|
||||
|
||||
connection.on('ready', common.mustNotCall());
|
||||
connection.on('error', common.mustCall((error) => {
|
||||
if (common.hasIPv6) {
|
||||
assert.strictEqual(error.code, 'ECONNREFUSED');
|
||||
assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`);
|
||||
} else {
|
||||
assert.strictEqual(error.code, 'EADDRNOTAVAIL');
|
||||
assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
|
||||
}
|
||||
|
||||
ipv4Server.close();
|
||||
dnsServer.close();
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
const assert = require('assert');
|
||||
const net = require('net');
|
||||
|
||||
assert.throws(() => {
|
||||
net.connect({ port: 8080, autoSelectFamily: 'INVALID' });
|
||||
}, { code: 'ERR_INVALID_ARG_TYPE' });
|
||||
Reference in New Issue
Block a user