Skip to content

Commit

Permalink
net: add autoSelectFamily global getter and setter
Browse files Browse the repository at this point in the history
  • Loading branch information
ShogunPanda committed Dec 7, 2022
1 parent 3bef549 commit 58f423b
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 9 deletions.
44 changes: 42 additions & 2 deletions doc/api/net.md
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,10 @@ 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`.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/44731
description: Added the `autoSelectFamily` option.
Expand Down Expand Up @@ -909,12 +927,13 @@ 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)`][].
* `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 +1514,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 +1712,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
30 changes: 24 additions & 6 deletions lib/net.js
Expand Up @@ -124,6 +124,7 @@ let cluster;
let dns;
let BlockList;
let SocketAddress;
let autoSelectFamilyDefault = false;

const { clearTimeout, setTimeout } = require('timers');
const { kTimeout } = require('internal/timers');
Expand Down Expand Up @@ -226,6 +227,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 +1101,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 +1195,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 +1216,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);

if (autoSelectFamilyAttemptTimeout < 10) {
Expand All @@ -1233,7 +1247,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 +1384,8 @@ function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options,
}
}

self.autoSelectFamilyAttemptedAddresses = [];

const context = {
socket: self,
addresses,
Expand Down Expand Up @@ -2223,4 +2239,6 @@ module.exports = {
Server,
Socket,
Stream: Socket, // Legacy naming
getDefaultAutoSelectFamily,
setDefaultAutoSelectFamily,
};
File renamed without changes.
File renamed without changes.
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
140 changes: 140 additions & 0 deletions test/parallel/test-net-autoselectfamilydefault.js
@@ -0,0 +1,140 @@
'use strict';

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, setDefaultAutoSelectFamily } = require('net');

// Test that the default for happy eyeballs algorithm is properly respected.

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 by default if IPV6 is not reachable and the default is enabled
{
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(() => {
setDefaultAutoSelectFamily(true);

const connection = createConnection({
host: 'example.org',
port: ipv4Server.address().port,
lookup,
autoSelectFamilyAttemptTimeout,
});

let response = '';
connection.setEncoding('utf-8');

connection.on('data', (chunk) => {
response += chunk;
});

connection.on('end', common.mustCall(() => {
assert.strictEqual(response, 'response-ipv4');
ipv4Server.close();
dnsServer.close();
}));

connection.write('request');
}));
}));
}

// Test that IPV4 is not reached by default if IPV6 is not reachable and the default is disabled
{
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(() => {
setDefaultAutoSelectFamily(false);

const port = ipv4Server.address().port;

const connection = createConnection({
host: 'example.org',
port,
lookup,
});

connection.on('ready', common.mustNotCall());
connection.on('error', common.mustCall((error) => {
if (common.hasIPv6) {
assert.strictEqual(error.code, 'ECONNREFUSED');
assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`);
} else {
assert.strictEqual(error.code, 'EADDRNOTAVAIL');
assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
}

ipv4Server.close();
dnsServer.close();
}));
}));
}));
}
@@ -0,0 +1,9 @@
'use strict';

require('../common');
const assert = require('assert');
const net = require('net');

assert.throws(() => {
net.connect({ port: 8080, autoSelectFamily: 'INVALID' });
}, { code: 'ERR_INVALID_ARG_TYPE' });

0 comments on commit 58f423b

Please sign in to comment.