Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

net: add autoSelectFamily global getter and setter #45777

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down
46 changes: 44 additions & 2 deletions doc/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 25 additions & 6 deletions lib/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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');
Expand Down Expand Up @@ -1370,6 +1385,8 @@ function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options,
}
}

self.autoSelectFamilyAttemptedAddresses = [];

const context = {
socket: self,
addresses,
Expand Down Expand Up @@ -2223,4 +2240,6 @@ module.exports = {
Server,
Socket,
Stream: Socket, // Legacy naming
getDefaultAutoSelectFamily,
setDefaultAutoSelectFamily,
};
4 changes: 4 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
108 changes: 108 additions & 0 deletions test/parallel/test-net-autoselectfamily-commandline-option.js
Original file line number Diff line number Diff line change
@@ -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');
}));
}));
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
});
Expand Down Expand Up @@ -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;
});
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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}`);
Expand Down
Loading