Skip to content

Commit 863bdb7

Browse files
ShogunPandaruyadorno
authored andcommittedAug 14, 2023
net: add autoSelectFamily global getter and setter
PR-URL: #45777 Backport-PR-URL: #49016 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent e7d2e8e commit 863bdb7

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
@@ -1861,6 +1870,7 @@ Node.js options that are allowed are:
18611870
* `--disable-proto`
18621871
* `--dns-result-order`
18631872
* `--enable-fips`
1873+
* `--enable-network-family-autoselection`
18641874
* `--enable-source-maps`
18651875
* `--experimental-abortcontroller`
18661876
* `--experimental-global-customevent`

‎doc/api/net.md

+44-2
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.
@@ -1499,6 +1520,26 @@ immediately initiates connection with
14991520
[`socket.connect(port[, host][, connectListener])`][`socket.connect(port)`],
15001521
then returns the `net.Socket` that starts the connection.
15011522

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

15041545
<!-- YAML
@@ -1683,6 +1724,7 @@ net.isIPv6('fhqwhgads'); // returns false
16831724
[`net.createConnection(path)`]: #netcreateconnectionpath-connectlistener
16841725
[`net.createConnection(port, host)`]: #netcreateconnectionport-host-connectlistener
16851726
[`net.createServer()`]: #netcreateserveroptions-connectionlistener
1727+
[`net.setDefaultAutoSelectFamily(value)`]: #netsetdefaultautoselectfamilyvalue
16861728
[`new net.Socket(options)`]: #new-netsocketoptions
16871729
[`readable.setEncoding()`]: stream.md#readablesetencodingencoding
16881730
[`server.close()`]: #serverclosecallback

‎lib/net.js

+25-6
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');
@@ -244,6 +246,14 @@ function connect(...args) {
244246
return socket.connect(normalized);
245247
}
246248

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

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

1129+
ArrayPrototypePush(self.autoSelectFamilyAttemptedAddresses, `${address}:${port}`);
1130+
11191131
if (addressType === 4) {
11201132
err = handle.connect(req, address, port);
11211133
} else {
@@ -1208,9 +1220,9 @@ function socketToDnsFamily(family) {
12081220
}
12091221

12101222
function lookupAndConnect(self, options) {
1211-
const { localAddress, localPort, autoSelectFamily } = options;
1223+
const { localAddress, localPort } = options;
12121224
const host = options.host || 'localhost';
1213-
let { port, autoSelectFamilyAttemptTimeout } = options;
1225+
let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options;
12141226

12151227
if (localAddress && !isIP(localAddress)) {
12161228
throw new ERR_INVALID_IP_ADDRESS(localAddress);
@@ -1229,11 +1241,14 @@ function lookupAndConnect(self, options) {
12291241
}
12301242
port |= 0;
12311243

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

1236-
if (autoSelectFamilyAttemptTimeout !== undefined) {
1251+
if (autoSelectFamilyAttemptTimeout != null) {
12371252
validateInt32(autoSelectFamilyAttemptTimeout, 'options.autoSelectFamilyAttemptTimeout', 1);
12381253

12391254
if (autoSelectFamilyAttemptTimeout < 10) {
@@ -1257,7 +1272,7 @@ function lookupAndConnect(self, options) {
12571272
return;
12581273
}
12591274

1260-
if (options.lookup !== undefined)
1275+
if (options.lookup != null)
12611276
validateFunction(options.lookup, 'options.lookup');
12621277

12631278
if (dns === undefined) dns = require('dns');
@@ -1394,6 +1409,8 @@ function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options,
13941409
}
13951410
}
13961411

1412+
self.autoSelectFamilyAttemptedAddresses = [];
1413+
13971414
const context = {
13981415
socket: self,
13991416
addresses,
@@ -2262,4 +2279,6 @@ module.exports = {
22622279
Server,
22632280
Socket,
22642281
Stream: Socket, // Legacy naming
2282+
getDefaultAutoSelectFamily,
2283+
setDefaultAutoSelectFamily,
22652284
};

‎src/node_options.cc

+4
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
359359
"returned)",
360360
&EnvironmentOptions::dns_result_order,
361361
kAllowedInEnvvar);
362+
AddOption("--enable-network-family-autoselection",
363+
"Enable network address family autodetection algorithm",
364+
&EnvironmentOptions::enable_network_family_autoselection,
365+
kAllowedInEnvvar);
362366
AddOption("--enable-source-maps",
363367
"Source Map V3 support for stack traces",
364368
&EnvironmentOptions::enable_source_maps,

‎src/node_options.h

+1
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;
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}`);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
5+
6+
const assert = require('assert');
7+
const dgram = require('dgram');
8+
const { Resolver } = require('dns');
9+
const { createConnection, createServer, setDefaultAutoSelectFamily } = require('net');
10+
11+
// Test that the default for happy eyeballs algorithm is properly respected.
12+
13+
let autoSelectFamilyAttemptTimeout = common.platformTimeout(250);
14+
if (common.isWindows) {
15+
// Some of the windows machines in the CI need more time to establish connection
16+
autoSelectFamilyAttemptTimeout = common.platformTimeout(1500);
17+
}
18+
19+
function _lookup(resolver, hostname, options, cb) {
20+
resolver.resolve(hostname, 'ANY', (err, replies) => {
21+
assert.notStrictEqual(options.family, 4);
22+
23+
if (err) {
24+
return cb(err);
25+
}
26+
27+
const hosts = replies
28+
.map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 }))
29+
.sort((a, b) => b.family - a.family);
30+
31+
if (options.all === true) {
32+
return cb(null, hosts);
33+
}
34+
35+
return cb(null, hosts[0].address, hosts[0].family);
36+
});
37+
}
38+
39+
function createDnsServer(ipv6Addr, ipv4Addr, cb) {
40+
// Create a DNS server which replies with a AAAA and a A record for the same host
41+
const socket = dgram.createSocket('udp4');
42+
43+
socket.on('message', common.mustCall((msg, { address, port }) => {
44+
const parsed = parseDNSPacket(msg);
45+
const domain = parsed.questions[0].domain;
46+
assert.strictEqual(domain, 'example.org');
47+
48+
socket.send(writeDNSPacket({
49+
id: parsed.id,
50+
questions: parsed.questions,
51+
answers: [
52+
{ type: 'AAAA', address: ipv6Addr, ttl: 123, domain: 'example.org' },
53+
{ type: 'A', address: ipv4Addr, ttl: 123, domain: 'example.org' },
54+
]
55+
}), port, address);
56+
}));
57+
58+
socket.bind(0, () => {
59+
const resolver = new Resolver();
60+
resolver.setServers([`127.0.0.1:${socket.address().port}`]);
61+
62+
cb({ dnsServer: socket, lookup: _lookup.bind(null, resolver) });
63+
});
64+
}
65+
66+
// Test that IPV4 is reached by default if IPV6 is not reachable and the default is enabled
67+
{
68+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
69+
const ipv4Server = createServer((socket) => {
70+
socket.on('data', common.mustCall(() => {
71+
socket.write('response-ipv4');
72+
socket.end();
73+
}));
74+
});
75+
76+
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
77+
setDefaultAutoSelectFamily(true);
78+
79+
const connection = createConnection({
80+
host: 'example.org',
81+
port: ipv4Server.address().port,
82+
lookup,
83+
autoSelectFamilyAttemptTimeout,
84+
});
85+
86+
let response = '';
87+
connection.setEncoding('utf-8');
88+
89+
connection.on('data', (chunk) => {
90+
response += chunk;
91+
});
92+
93+
connection.on('end', common.mustCall(() => {
94+
assert.strictEqual(response, 'response-ipv4');
95+
ipv4Server.close();
96+
dnsServer.close();
97+
}));
98+
99+
connection.write('request');
100+
}));
101+
}));
102+
}
103+
104+
// Test that IPV4 is not reached by default if IPV6 is not reachable and the default is disabled
105+
{
106+
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
107+
const ipv4Server = createServer((socket) => {
108+
socket.on('data', common.mustCall(() => {
109+
socket.write('response-ipv4');
110+
socket.end();
111+
}));
112+
});
113+
114+
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
115+
setDefaultAutoSelectFamily(false);
116+
117+
const port = ipv4Server.address().port;
118+
119+
const connection = createConnection({
120+
host: 'example.org',
121+
port,
122+
lookup,
123+
});
124+
125+
connection.on('ready', common.mustNotCall());
126+
connection.on('error', common.mustCall((error) => {
127+
if (common.hasIPv6) {
128+
assert.strictEqual(error.code, 'ECONNREFUSED');
129+
assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`);
130+
} else {
131+
assert.strictEqual(error.code, 'EADDRNOTAVAIL');
132+
assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
133+
}
134+
135+
ipv4Server.close();
136+
dnsServer.close();
137+
}));
138+
}));
139+
}));
140+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('assert');
5+
const net = require('net');
6+
7+
assert.throws(() => {
8+
net.connect({ port: 8080, autoSelectFamily: 'INVALID' });
9+
}, { code: 'ERR_INVALID_ARG_TYPE' });

0 commit comments

Comments
 (0)
Please sign in to comment.