Skip to content

Commit 97ff063

Browse files
ShogunPandajuanarbol
authored andcommitted
net: add autoSelectFamily global getter and setter
PR-URL: #45777 Reviewed-By: Matteo Collina <[email protected]>
1 parent b749323 commit 97ff063

12 files changed

+355
-9
lines changed

doc/api/cli.md

Lines changed: 10 additions & 0 deletions
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
@@ -1853,6 +1862,7 @@ Node.js options that are allowed are:
18531862
* `--disable-proto`
18541863
* `--dns-result-order`
18551864
* `--enable-fips`
1865+
* `--enable-network-family-autoselection`
18561866
* `--enable-source-maps`
18571867
* `--experimental-abortcontroller`
18581868
* `--experimental-global-customevent`

doc/api/net.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,20 @@ Returns the bound `address`, the address `family` name and `port` of the
778778
socket as reported by the operating system:
779779
`{ port: 12346, family: 'IPv4', address: '127.0.0.1' }`
780780

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

783797
<!-- YAML
@@ -854,6 +868,11 @@ behavior.
854868
<!-- YAML
855869
added: v0.1.90
856870
changes:
871+
- version: REPLACEME
872+
pr-url: https://github.com/nodejs/node/pull/45777
873+
description: The default value for autoSelectFamily option can be changed
874+
at runtime using `setDefaultAutoSelectFamily` or via the
875+
command line option `--enable-network-family-autoselection`.
857876
- version: v18.13.0
858877
pr-url: https://github.com/nodejs/node/pull/44731
859878
description: Added the `autoSelectFamily` option.
@@ -905,12 +924,14 @@ For TCP connections, available `options` are:
905924
that loosely implements section 5 of [RFC 8305][].
906925
The `all` option passed to lookup is set to `true` and the sockets attempts to connect to all
907926
obtained IPv6 and IPv4 addresses, in sequence, until a connection is established.
908-
The first returned AAAA address is tried first, then the first returned A address and so on.
927+
The first returned AAAA address is tried first, then the first returned A address,
928+
then the second returned AAAA address and so on.
909929
Each connection attempt is given the amount of time specified by the `autoSelectFamilyAttemptTimeout`
910930
option before timing out and trying the next address.
911931
Ignored if the `family` option is not `0` or if `localAddress` is set.
912932
Connection errors are not emitted if at least one connection succeeds.
913-
**Default:** `false`.
933+
**Default:** initially `false`, but it can be changed at runtime using [`net.setDefaultAutoSelectFamily(value)`][]
934+
or via the command line option `--enable-network-family-autoselection`.
914935
* `autoSelectFamilyAttemptTimeout` {number}: The amount of time in milliseconds to wait
915936
for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option.
916937
If set to a positive integer less than `10`, then the value `10` will be used instead.
@@ -1497,6 +1518,26 @@ immediately initiates connection with
14971518
[`socket.connect(port[, host][, connectListener])`][`socket.connect(port)`],
14981519
then returns the `net.Socket` that starts the connection.
14991520

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

15021543
<!-- YAML
@@ -1675,6 +1716,7 @@ net.isIPv6('fhqwhgads'); // returns false
16751716
[`net.createConnection(path)`]: #netcreateconnectionpath-connectlistener
16761717
[`net.createConnection(port, host)`]: #netcreateconnectionport-host-connectlistener
16771718
[`net.createServer()`]: #netcreateserveroptions-connectionlistener
1719+
[`net.setDefaultAutoSelectFamily(value)`]: #netsetdefaultautoselectfamilyvalue
16781720
[`new net.Socket(options)`]: #new-netsocketoptions
16791721
[`readable.setEncoding()`]: stream.md#readablesetencodingencoding
16801722
[`server.close()`]: #serverclosecallback

lib/net.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,14 @@ const {
124124
DTRACE_NET_SERVER_CONNECTION,
125125
DTRACE_NET_STREAM_END,
126126
} = require('internal/dtrace');
127+
const { getOptionValue } = require('internal/options');
127128

128129
// Lazy loaded to improve startup performance.
129130
let cluster;
130131
let dns;
131132
let BlockList;
132133
let SocketAddress;
134+
let autoSelectFamilyDefault = getOptionValue('--enable-network-family-autoselection');
133135

134136
const { clearTimeout, setTimeout } = require('timers');
135137
const { kTimeout } = require('internal/timers');
@@ -243,6 +245,14 @@ function connect(...args) {
243245
return socket.connect(normalized);
244246
}
245247

248+
function getDefaultAutoSelectFamily() {
249+
return autoSelectFamilyDefault;
250+
}
251+
252+
function setDefaultAutoSelectFamily(value) {
253+
validateBoolean(value, 'value');
254+
autoSelectFamilyDefault = value;
255+
}
246256

247257
// Returns an array [options, cb], where options is an object,
248258
// cb is either a function or null.
@@ -1115,6 +1125,8 @@ function internalConnectMultiple(context) {
11151125
req.localAddress = localAddress;
11161126
req.localPort = localPort;
11171127

1128+
ArrayPrototypePush(self.autoSelectFamilyAttemptedAddresses, `${address}:${port}`);
1129+
11181130
if (addressType === 4) {
11191131
err = handle.connect(req, address, port);
11201132
} else {
@@ -1207,9 +1219,9 @@ function socketToDnsFamily(family) {
12071219
}
12081220

12091221
function lookupAndConnect(self, options) {
1210-
const { localAddress, localPort, autoSelectFamily } = options;
1222+
const { localAddress, localPort } = options;
12111223
const host = options.host || 'localhost';
1212-
let { port, autoSelectFamilyAttemptTimeout } = options;
1224+
let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options;
12131225

12141226
if (localAddress && !isIP(localAddress)) {
12151227
throw new ERR_INVALID_IP_ADDRESS(localAddress);
@@ -1228,11 +1240,14 @@ function lookupAndConnect(self, options) {
12281240
}
12291241
port |= 0;
12301242

1231-
if (autoSelectFamily !== undefined) {
1232-
validateBoolean(autoSelectFamily);
1243+
1244+
if (autoSelectFamily != null) {
1245+
validateBoolean(autoSelectFamily, 'options.autoSelectFamily');
1246+
} else {
1247+
autoSelectFamily = autoSelectFamilyDefault;
12331248
}
12341249

1235-
if (autoSelectFamilyAttemptTimeout !== undefined) {
1250+
if (autoSelectFamilyAttemptTimeout != null) {
12361251
validateInt32(autoSelectFamilyAttemptTimeout, 'options.autoSelectFamilyAttemptTimeout', 1);
12371252

12381253
if (autoSelectFamilyAttemptTimeout < 10) {
@@ -1256,7 +1271,7 @@ function lookupAndConnect(self, options) {
12561271
return;
12571272
}
12581273

1259-
if (options.lookup !== undefined)
1274+
if (options.lookup != null)
12601275
validateFunction(options.lookup, 'options.lookup');
12611276

12621277
if (dns === undefined) dns = require('dns');
@@ -1393,6 +1408,8 @@ function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options,
13931408
}
13941409
}
13951410

1411+
self.autoSelectFamilyAttemptedAddresses = [];
1412+
13961413
const context = {
13971414
socket: self,
13981415
addresses,
@@ -2249,4 +2266,6 @@ module.exports = {
22492266
Server,
22502267
Socket,
22512268
Stream: Socket, // Legacy naming
2269+
getDefaultAutoSelectFamily,
2270+
setDefaultAutoSelectFamily,
22522271
};

src/node_options.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
366366
"returned)",
367367
&EnvironmentOptions::dns_result_order,
368368
kAllowedInEnvvar);
369+
AddOption("--enable-network-family-autoselection",
370+
"Enable network address family autodetection algorithm",
371+
&EnvironmentOptions::enable_network_family_autoselection,
372+
kAllowedInEnvvar);
369373
AddOption("--enable-source-maps",
370374
"Source Map V3 support for stack traces",
371375
&EnvironmentOptions::enable_source_maps,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class EnvironmentOptions : public Options {
128128
bool frozen_intrinsics = false;
129129
int64_t heap_snapshot_near_heap_limit = 0;
130130
std::string heap_snapshot_signal;
131+
bool enable_network_family_autoselection = false;
131132
uint64_t max_http_header_size = 16 * 1024;
132133
bool deprecation = true;
133134
bool force_async_hooks_checks = true;
Lines changed: 108 additions & 0 deletions
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 renamed to test/parallel/test-net-autoselectfamily.js

Lines changed: 14 additions & 1 deletion
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)