Skip to content

Commit 58f423b

Browse files
committed
net: add autoSelectFamily global getter and setter
1 parent 3bef549 commit 58f423b

8 files changed

+229
-9
lines changed

doc/api/net.md

Lines changed: 42 additions & 2 deletions
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,10 @@ 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`.
859877
- version: REPLACEME
860878
pr-url: https://github.com/nodejs/node/pull/44731
861879
description: Added the `autoSelectFamily` option.
@@ -909,12 +927,13 @@ For TCP connections, available `options` are:
909927
that loosely implements section 5 of [RFC 8305][].
910928
The `all` option passed to lookup is set to `true` and the sockets attempts to connect to all
911929
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.
930+
The first returned AAAA address is tried first, then the first returned A address,
931+
then the second returned AAAA address and so on.
913932
Each connection attempt is given the amount of time specified by the `autoSelectFamilyAttemptTimeout`
914933
option before timing out and trying the next address.
915934
Ignored if the `family` option is not `0` or if `localAddress` is set.
916935
Connection errors are not emitted if at least one connection succeeds.
917-
**Default:** `false`.
936+
**Default:** initially `false`, but it can be changed at runtime using [`net.setDefaultAutoSelectFamily(value)`][].
918937
* `autoSelectFamilyAttemptTimeout` {number}: The amount of time in milliseconds to wait
919938
for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option.
920939
If set to a positive integer less than `10`, then the value `10` will be used instead.
@@ -1495,6 +1514,26 @@ immediately initiates connection with
14951514
[`socket.connect(port[, host][, connectListener])`][`socket.connect(port)`],
14961515
then returns the `net.Socket` that starts the connection.
14971516

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

15001539
<!-- YAML
@@ -1673,6 +1712,7 @@ net.isIPv6('fhqwhgads'); // returns false
16731712
[`net.createConnection(path)`]: #netcreateconnectionpath-connectlistener
16741713
[`net.createConnection(port, host)`]: #netcreateconnectionport-host-connectlistener
16751714
[`net.createServer()`]: #netcreateserveroptions-connectionlistener
1715+
[`net.setDefaultAutoSelectFamily(value)`]: #netsetdefaultautoselectfamilyvalue
16761716
[`new net.Socket(options)`]: #new-netsocketoptions
16771717
[`readable.setEncoding()`]: stream.md#readablesetencodingencoding
16781718
[`server.close()`]: #serverclosecallback

lib/net.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ let cluster;
124124
let dns;
125125
let BlockList;
126126
let SocketAddress;
127+
let autoSelectFamilyDefault = false;
127128

128129
const { clearTimeout, setTimeout } = require('timers');
129130
const { kTimeout } = require('internal/timers');
@@ -226,6 +227,14 @@ function connect(...args) {
226227
return socket.connect(normalized);
227228
}
228229

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

230239
// Returns an array [options, cb], where options is an object,
231240
// cb is either a function or null.
@@ -1092,6 +1101,8 @@ function internalConnectMultiple(context) {
10921101
req.localAddress = localAddress;
10931102
req.localPort = localPort;
10941103

1104+
ArrayPrototypePush(self.autoSelectFamilyAttemptedAddresses, `${address}:${port}`);
1105+
10951106
if (addressType === 4) {
10961107
err = handle.connect(req, address, port);
10971108
} else {
@@ -1184,9 +1195,9 @@ function socketToDnsFamily(family) {
11841195
}
11851196

11861197
function lookupAndConnect(self, options) {
1187-
const { localAddress, localPort, autoSelectFamily } = options;
1198+
const { localAddress, localPort } = options;
11881199
const host = options.host || 'localhost';
1189-
let { port, autoSelectFamilyAttemptTimeout } = options;
1200+
let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options;
11901201

11911202
if (localAddress && !isIP(localAddress)) {
11921203
throw new ERR_INVALID_IP_ADDRESS(localAddress);
@@ -1205,11 +1216,14 @@ function lookupAndConnect(self, options) {
12051216
}
12061217
port |= 0;
12071218

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

1212-
if (autoSelectFamilyAttemptTimeout !== undefined) {
1226+
if (autoSelectFamilyAttemptTimeout != null) {
12131227
validateInt32(autoSelectFamilyAttemptTimeout);
12141228

12151229
if (autoSelectFamilyAttemptTimeout < 10) {
@@ -1233,7 +1247,7 @@ function lookupAndConnect(self, options) {
12331247
return;
12341248
}
12351249

1236-
if (options.lookup !== undefined)
1250+
if (options.lookup != null)
12371251
validateFunction(options.lookup, 'options.lookup');
12381252

12391253
if (dns === undefined) dns = require('dns');
@@ -1370,6 +1384,8 @@ function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options,
13701384
}
13711385
}
13721386

1387+
self.autoSelectFamilyAttemptedAddresses = [];
1388+
13731389
const context = {
13741390
socket: self,
13751391
addresses,
@@ -2223,4 +2239,6 @@ module.exports = {
22232239
Server,
22242240
Socket,
22252241
Stream: Socket, // Legacy naming
2242+
getDefaultAutoSelectFamily,
2243+
setDefaultAutoSelectFamily,
22262244
};

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}`);
Lines changed: 140 additions & 0 deletions
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+
}
Lines changed: 9 additions & 0 deletions
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)