Skip to content

Commit 8e6b8db

Browse files
ShogunPandaRafaelGSS
authored andcommitted
net: add autoSelectFamily global getter and setter
PR-URL: #45777 Reviewed-By: Matteo Collina <[email protected]>
1 parent 7ea72ee commit 8e6b8db

12 files changed

+355
-9
lines changed

doc/api/cli.md

+10
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,15 @@ added: v6.0.0
314314
Enable FIPS-compliant crypto at startup. (Requires Node.js to be built
315315
against FIPS-compatible OpenSSL.)
316316

317+
### `--enable-network-family-autoselection`
318+
319+
<!-- YAML
320+
added: REPLACEME
321+
-->
322+
323+
Enables the family autoselection algorithm unless connection options explicitly
324+
disables it.
325+
317326
### `--enable-source-maps`
318327

319328
<!-- YAML
@@ -1847,6 +1856,7 @@ Node.js options that are allowed are:
18471856
* `--disable-proto`
18481857
* `--dns-result-order`
18491858
* `--enable-fips`
1859+
* `--enable-network-family-autoselection`
18501860
* `--enable-source-maps`
18511861
* `--experimental-abortcontroller`
18521862
* `--experimental-import-meta-resolve`

doc/api/net.md

+44-2
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,20 @@ Returns the bound `address`, the address `family` name and `port` of the
780780
socket as reported by the operating system:
781781
`{ port: 12346, family: 'IPv4', address: '127.0.0.1' }`
782782

783+
### `socket.autoSelectFamilyAttemptedAddresses`
784+
785+
<!-- YAML
786+
added: REPLACEME
787+
-->
788+
789+
* {string\[]}
790+
791+
This property is only present if the family autoselection algorithm is enabled in
792+
[`socket.connect(options)`][] and it is an array of the addresses that have been attempted.
793+
794+
Each address is a string in the form of `$IP:$PORT`. If the connection was successful,
795+
then the last address is the one that the socket is currently connected to.
796+
783797
### `socket.bufferSize`
784798

785799
<!-- YAML
@@ -856,6 +870,11 @@ behavior.
856870
<!-- YAML
857871
added: v0.1.90
858872
changes:
873+
- version: REPLACEME
874+
pr-url: https://github.com/nodejs/node/pull/45777
875+
description: The default value for autoSelectFamily option can be changed
876+
at runtime using `setDefaultAutoSelectFamily` or via the
877+
command line option `--enable-network-family-autoselection`.
859878
- version: v19.3.0
860879
pr-url: https://github.com/nodejs/node/pull/44731
861880
description: Added the `autoSelectFamily` option.
@@ -909,12 +928,14 @@ For TCP connections, available `options` are:
909928
that loosely implements section 5 of [RFC 8305][].
910929
The `all` option passed to lookup is set to `true` and the sockets attempts to connect to all
911930
obtained IPv6 and IPv4 addresses, in sequence, until a connection is established.
912-
The first returned AAAA address is tried first, then the first returned A address and so on.
931+
The first returned AAAA address is tried first, then the first returned A address,
932+
then the second returned AAAA address and so on.
913933
Each connection attempt is given the amount of time specified by the `autoSelectFamilyAttemptTimeout`
914934
option before timing out and trying the next address.
915935
Ignored if the `family` option is not `0` or if `localAddress` is set.
916936
Connection errors are not emitted if at least one connection succeeds.
917-
**Default:** `false`.
937+
**Default:** initially `false`, but it can be changed at runtime using [`net.setDefaultAutoSelectFamily(value)`][]
938+
or via the command line option `--enable-network-family-autoselection`.
918939
* `autoSelectFamilyAttemptTimeout` {number}: The amount of time in milliseconds to wait
919940
for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option.
920941
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
14951516
[`socket.connect(port[, host][, connectListener])`][`socket.connect(port)`],
14961517
then returns the `net.Socket` that starts the connection.
14971518

1519+
## `net.setDefaultAutoSelectFamily(value)`
1520+
1521+
<!-- YAML
1522+
added: REPLACEME
1523+
-->
1524+
1525+
Sets the default value of the `autoSelectFamily` option of [`socket.connect(options)`][].
1526+
1527+
* `value` {boolean} The new default value. The initial default value is `false`.
1528+
1529+
## `net.getDefaultAutoSelectFamily()`
1530+
1531+
<!-- YAML
1532+
added: REPLACEME
1533+
-->
1534+
1535+
Gets the current default value of the `autoSelectFamily` option of [`socket.connect(options)`][].
1536+
1537+
* Returns: {boolean} The current default value of the `autoSelectFamily` option.
1538+
14981539
## `net.createServer([options][, connectionListener])`
14991540

15001541
<!-- YAML
@@ -1673,6 +1714,7 @@ net.isIPv6('fhqwhgads'); // returns false
16731714
[`net.createConnection(path)`]: #netcreateconnectionpath-connectlistener
16741715
[`net.createConnection(port, host)`]: #netcreateconnectionport-host-connectlistener
16751716
[`net.createServer()`]: #netcreateserveroptions-connectionlistener
1717+
[`net.setDefaultAutoSelectFamily(value)`]: #netsetdefaultautoselectfamilyvalue
16761718
[`new net.Socket(options)`]: #new-netsocketoptions
16771719
[`readable.setEncoding()`]: stream.md#readablesetencodingencoding
16781720
[`server.close()`]: #serverclosecallback

lib/net.js

+25-6
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,14 @@ const {
118118
validateString
119119
} = require('internal/validators');
120120
const kLastWriteQueueSize = Symbol('lastWriteQueueSize');
121+
const { getOptionValue } = require('internal/options');
121122

122123
// Lazy loaded to improve startup performance.
123124
let cluster;
124125
let dns;
125126
let BlockList;
126127
let SocketAddress;
128+
let autoSelectFamilyDefault = getOptionValue('--enable-network-family-autoselection');
127129

128130
const { clearTimeout, setTimeout } = require('timers');
129131
const { kTimeout } = require('internal/timers');
@@ -226,6 +228,14 @@ function connect(...args) {
226228
return socket.connect(normalized);
227229
}
228230

231+
function getDefaultAutoSelectFamily() {
232+
return autoSelectFamilyDefault;
233+
}
234+
235+
function setDefaultAutoSelectFamily(value) {
236+
validateBoolean(value, 'value');
237+
autoSelectFamilyDefault = value;
238+
}
229239

230240
// Returns an array [options, cb], where options is an object,
231241
// cb is either a function or null.
@@ -1092,6 +1102,8 @@ function internalConnectMultiple(context) {
10921102
req.localAddress = localAddress;
10931103
req.localPort = localPort;
10941104

1105+
ArrayPrototypePush(self.autoSelectFamilyAttemptedAddresses, `${address}:${port}`);
1106+
10951107
if (addressType === 4) {
10961108
err = handle.connect(req, address, port);
10971109
} else {
@@ -1184,9 +1196,9 @@ function socketToDnsFamily(family) {
11841196
}
11851197

11861198
function lookupAndConnect(self, options) {
1187-
const { localAddress, localPort, autoSelectFamily } = options;
1199+
const { localAddress, localPort } = options;
11881200
const host = options.host || 'localhost';
1189-
let { port, autoSelectFamilyAttemptTimeout } = options;
1201+
let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options;
11901202

11911203
if (localAddress && !isIP(localAddress)) {
11921204
throw new ERR_INVALID_IP_ADDRESS(localAddress);
@@ -1205,11 +1217,14 @@ function lookupAndConnect(self, options) {
12051217
}
12061218
port |= 0;
12071219

1208-
if (autoSelectFamily !== undefined) {
1209-
validateBoolean(autoSelectFamily);
1220+
1221+
if (autoSelectFamily != null) {
1222+
validateBoolean(autoSelectFamily, 'options.autoSelectFamily');
1223+
} else {
1224+
autoSelectFamily = autoSelectFamilyDefault;
12101225
}
12111226

1212-
if (autoSelectFamilyAttemptTimeout !== undefined) {
1227+
if (autoSelectFamilyAttemptTimeout != null) {
12131228
validateInt32(autoSelectFamilyAttemptTimeout, 'options.autoSelectFamilyAttemptTimeout', 1);
12141229

12151230
if (autoSelectFamilyAttemptTimeout < 10) {
@@ -1233,7 +1248,7 @@ function lookupAndConnect(self, options) {
12331248
return;
12341249
}
12351250

1236-
if (options.lookup !== undefined)
1251+
if (options.lookup != null)
12371252
validateFunction(options.lookup, 'options.lookup');
12381253

12391254
if (dns === undefined) dns = require('dns');
@@ -1370,6 +1385,8 @@ function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options,
13701385
}
13711386
}
13721387

1388+
self.autoSelectFamilyAttemptedAddresses = [];
1389+
13731390
const context = {
13741391
socket: self,
13751392
addresses,
@@ -2223,4 +2240,6 @@ module.exports = {
22232240
Server,
22242241
Socket,
22252242
Stream: Socket, // Legacy naming
2243+
getDefaultAutoSelectFamily,
2244+
setDefaultAutoSelectFamily,
22262245
};

src/node_options.cc

+4
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
346346
"returned)",
347347
&EnvironmentOptions::dns_result_order,
348348
kAllowedInEnvvar);
349+
AddOption("--enable-network-family-autoselection",
350+
"Enable network address family autodetection algorithm",
351+
&EnvironmentOptions::enable_network_family_autoselection,
352+
kAllowedInEnvvar);
349353
AddOption("--enable-source-maps",
350354
"Source Map V3 support for stack traces",
351355
&EnvironmentOptions::enable_source_maps,

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ class EnvironmentOptions : public Options {
127127
bool frozen_intrinsics = false;
128128
int64_t heap_snapshot_near_heap_limit = 0;
129129
std::string heap_snapshot_signal;
130+
bool enable_network_family_autoselection = false;
130131
uint64_t max_http_header_size = 16 * 1024;
131132
bool deprecation = true;
132133
bool force_async_hooks_checks = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use strict';
2+
3+
// Flags: --enable-network-family-autoselection
4+
5+
const common = require('../common');
6+
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
7+
8+
const assert = require('assert');
9+
const dgram = require('dgram');
10+
const { Resolver } = require('dns');
11+
const { createConnection, createServer } = require('net');
12+
13+
// Test that happy eyeballs algorithm can be enable from command line.
14+
15+
let autoSelectFamilyAttemptTimeout = common.platformTimeout(250);
16+
if (common.isWindows) {
17+
// Some of the windows machines in the CI need more time to establish connection
18+
autoSelectFamilyAttemptTimeout = common.platformTimeout(1500);
19+
}
20+
21+
function _lookup(resolver, hostname, options, cb) {
22+
resolver.resolve(hostname, 'ANY', (err, replies) => {
23+
assert.notStrictEqual(options.family, 4);
24+
25+
if (err) {
26+
return cb(err);
27+
}
28+
29+
const hosts = replies
30+
.map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 }))
31+
.sort((a, b) => b.family - a.family);
32+
33+
if (options.all === true) {
34+
return cb(null, hosts);
35+
}
36+
37+
return cb(null, hosts[0].address, hosts[0].family);
38+
});
39+
}
40+
41+
function createDnsServer(ipv6Addr, ipv4Addr, cb) {
42+
// Create a DNS server which replies with a AAAA and a A record for the same host
43+
const socket = dgram.createSocket('udp4');
44+
45+
socket.on('message', common.mustCall((msg, { address, port }) => {
46+
const parsed = parseDNSPacket(msg);
47+
const domain = parsed.questions[0].domain;
48+
assert.strictEqual(domain, 'example.org');
49+
50+
socket.send(writeDNSPacket({
51+
id: parsed.id,
52+
questions: parsed.questions,
53+
answers: [
54+
{ type: 'AAAA', address: ipv6Addr, ttl: 123, domain: 'example.org' },
55+
{ type: 'A', address: ipv4Addr, ttl: 123, domain: 'example.org' },
56+
]
57+
}), port, address);
58+
}));
59+
60+
socket.bind(0, () => {
61+
const resolver = new Resolver();
62+
resolver.setServers([`127.0.0.1:${socket.address().port}`]);
63+
64+
cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
65+
});
66+
}
67+
68+
// Test that IPV4 is reached if IPV6 is not reachable
69+
{
70+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
71+
const ipv4Server = createServer((socket) => {
72+
socket.on('data', common.mustCall(() => {
73+
socket.write('response-ipv4');
74+
socket.end();
75+
}));
76+
});
77+
78+
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
79+
const port = ipv4Server.address().port;
80+
81+
const connection = createConnection({
82+
host: 'example.org',
83+
port: port,
84+
lookup,
85+
autoSelectFamilyAttemptTimeout,
86+
});
87+
88+
let response = '';
89+
connection.setEncoding('utf-8');
90+
91+
connection.on('ready', common.mustCall(() => {
92+
assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`, `127.0.0.1:${port}`]);
93+
}));
94+
95+
connection.on('data', (chunk) => {
96+
response += chunk;
97+
});
98+
99+
connection.on('end', common.mustCall(() => {
100+
assert.strictEqual(response, 'response-ipv4');
101+
ipv4Server.close();
102+
dnsServer.close();
103+
}));
104+
105+
connection.write('request');
106+
}));
107+
}));
108+
}

test/parallel/test-net-happy-eyeballs.js test/parallel/test-net-autoselectfamily.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,11 @@ function createDnsServer(ipv6Addr, ipv4Addr, cb) {
7474
});
7575

7676
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
77+
const port = ipv4Server.address().port;
78+
7779
const connection = createConnection({
7880
host: 'example.org',
79-
port: ipv4Server.address().port,
81+
port: port,
8082
lookup,
8183
autoSelectFamily: true,
8284
autoSelectFamilyAttemptTimeout,
@@ -85,6 +87,10 @@ function createDnsServer(ipv6Addr, ipv4Addr, cb) {
8587
let response = '';
8688
connection.setEncoding('utf-8');
8789

90+
connection.on('ready', common.mustCall(() => {
91+
assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`, `127.0.0.1:${port}`]);
92+
}));
93+
8894
connection.on('data', (chunk) => {
8995
response += chunk;
9096
});
@@ -132,6 +138,10 @@ if (common.hasIPv6) {
132138
let response = '';
133139
connection.setEncoding('utf-8');
134140

141+
connection.on('ready', common.mustCall(() => {
142+
assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`]);
143+
}));
144+
135145
connection.on('data', (chunk) => {
136146
response += chunk;
137147
});
@@ -162,6 +172,7 @@ if (common.hasIPv6) {
162172

163173
connection.on('ready', common.mustNotCall());
164174
connection.on('error', common.mustCall((error) => {
175+
assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, ['::1:10', '127.0.0.1:10']);
165176
assert.strictEqual(error.constructor.name, 'AggregateError');
166177
assert.strictEqual(error.errors.length, 2);
167178

@@ -199,6 +210,8 @@ if (common.hasIPv6) {
199210

200211
connection.on('ready', common.mustNotCall());
201212
connection.on('error', common.mustCall((error) => {
213+
assert.strictEqual(connection.autoSelectFamilyAttemptedAddresses, undefined);
214+
202215
if (common.hasIPv6) {
203216
assert.strictEqual(error.code, 'ECONNREFUSED');
204217
assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`);

0 commit comments

Comments
 (0)