net: add connection attempt events

PR-URL: https://github.com/nodejs/node/pull/51045
Fixes: https://github.com/nodejs/node/issues/48763
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
This commit is contained in:
Paolo Insogna 2023-12-21 11:43:15 +01:00 committed by GitHub
parent abbdc3efaa
commit 2cb94240f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 566 additions and 531 deletions

View File

@ -691,6 +691,47 @@ added: v0.1.90
Emitted when a socket connection is successfully established.
See [`net.createConnection()`][].
### Event: `'connectionAttempt'`
<!-- YAML
added: REPLACEME
-->
* `ip` {number} The IP which the socket is attempting to connect to.
* `port` {number} The port which the socket is attempting to connect to.
* `family` {number} The family of the IP. It can be `6` for IPv6 or `4` for IPv4.
Emitted when a new connection attempt is started. This may be emitted multiple times
if the family autoselection algorithm is enabled in [`socket.connect(options)`][].
### Event: `'connectionAttemptFailed'`
<!-- YAML
added: REPLACEME
-->
* `ip` {number} The IP which the socket attempted to connect to.
* `port` {number} The port which the socket attempted to connect to.
* `family` {number} The family of the IP. It can be `6` for IPv6 or `4` for IPv4.
\* `error` {Error} The error associated with the failure.
Emitted when a connection attempt failed. This may be emitted multiple times
if the family autoselection algorithm is enabled in [`socket.connect(options)`][].
### Event: `'connectionAttemptTimeout'`
<!-- YAML
added: REPLACEME
-->
* `ip` {number} The IP which the socket attempted to connect to.
* `port` {number} The port which the socket attempted to connect to.
* `family` {number} The family of the IP. It can be `6` for IPv6 or `4` for IPv4.
Emitted when a connection attempt timed out. This is only emitted (and may be
emitted multiple times) if the family autoselection algorithm is enabled
in [`socket.connect(options)`][].
### Event: `'data'`
<!-- YAML
@ -963,8 +1004,7 @@ For TCP connections, available `options` are:
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,
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.
Each connection attempt (but the last one) 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.
If all connections attempts fails, a single `AggregateError` with all failed attempts is emitted.

View File

@ -1058,6 +1058,7 @@ function internalConnect(
}
debug('connect: attempting to connect to %s:%d (addressType: %d)', address, port, addressType);
self.emit('connectionAttempt', address, port, addressType);
if (addressType === 6 || addressType === 4) {
const req = new TCPConnectWrap();
@ -1066,6 +1067,7 @@ function internalConnect(
req.port = port;
req.localAddress = localAddress;
req.localPort = localPort;
req.addressType = addressType;
if (addressType === 4)
err = self._handle.connect(req, address, port);
@ -1149,6 +1151,7 @@ function internalConnectMultiple(context, canceled) {
}
debug('connect/multiple: attempting to connect to %s:%d (addressType: %d)', address, port, addressType);
self.emit('connectionAttempt', address, port, addressType);
const req = new TCPConnectWrap();
req.oncomplete = FunctionPrototypeBind(afterConnectMultiple, undefined, context, current);
@ -1156,6 +1159,7 @@ function internalConnectMultiple(context, canceled) {
req.port = port;
req.localAddress = localAddress;
req.localPort = localPort;
req.addressType = addressType;
ArrayPrototypePush(self.autoSelectFamilyAttemptedAddresses, `${address}:${port}`);
@ -1173,7 +1177,10 @@ function internalConnectMultiple(context, canceled) {
details = sockname.address + ':' + sockname.port;
}
ArrayPrototypePush(context.errors, new ExceptionWithHostPort(err, 'connect', address, port, details));
const ex = new ExceptionWithHostPort(err, 'connect', address, port, details);
ArrayPrototypePush(context.errors, ex);
self.emit('connectionAttemptFailed', address, port, addressType, ex);
internalConnectMultiple(context);
return;
}
@ -1601,6 +1608,8 @@ function afterConnect(status, handle, req, readable, writable) {
ex.localAddress = req.localAddress;
ex.localPort = req.localPort;
}
self.emit('connectionAttemptFailed', req.address, req.port, req.addressType, ex);
self.destroy(ex);
}
}
@ -1661,10 +1670,16 @@ function afterConnectMultiple(context, current, status, handle, req, readable, w
// Some error occurred, add to the list of exceptions
if (status !== 0) {
ArrayPrototypePush(context.errors, createConnectionError(req, status));
const ex = createConnectionError(req, status);
ArrayPrototypePush(context.errors, ex);
self.emit('connectionAttemptFailed', req.address, req.port, req.addressType, ex);
// Try the next address, unless we were aborted
if (context.socket.connecting) {
internalConnectMultiple(context, status === UV_ECANCELED);
}
// Try the next address
internalConnectMultiple(context, status === UV_ECANCELED);
return;
}
@ -1681,10 +1696,16 @@ function afterConnectMultiple(context, current, status, handle, req, readable, w
function internalConnectMultipleTimeout(context, req, handle) {
debug('connect/multiple: connection to %s:%s timed out', req.address, req.port);
context.socket.emit('connectionAttemptTimeout', req.address, req.port, req.addressType);
req.oncomplete = undefined;
ArrayPrototypePush(context.errors, createConnectionError(req, UV_ETIMEDOUT));
handle.close();
internalConnectMultiple(context);
// Try the next address, unless we were aborted
if (context.socket.connecting) {
internalConnectMultiple(context);
}
}
function addServerAbortSignalOption(self, options) {

View File

@ -2,6 +2,7 @@
const assert = require('assert');
const os = require('os');
const { isIP } = require('net');
const types = {
A: 1,
@ -309,6 +310,25 @@ function errorLookupMock(code = mockedErrorCode, syscall = mockedSysCall) {
};
}
function createMockedLookup(...addresses) {
addresses = addresses.map((address) => ({ address: address, family: isIP(address) }));
// Create a DNS server which replies with a AAAA and a A record for the same host
return function lookup(hostname, options, cb) {
if (options.all === true) {
process.nextTick(() => {
cb(null, addresses);
});
return;
}
process.nextTick(() => {
cb(null, addresses[0].address, addresses[0].family);
});
};
}
module.exports = {
types,
classes,
@ -317,4 +337,5 @@ module.exports = {
errorLookupMock,
mockedErrorCode,
mockedSysCall,
createMockedLookup,
};

View File

@ -0,0 +1,128 @@
'use strict';
const common = require('../common');
const { addresses: { INET6_IP, INET4_IP } } = require('../common/internet');
const { createMockedLookup } = require('../common/dns');
const assert = require('assert');
const { createConnection } = require('net');
//
// When testing this is MacOS, remember that the last connection will have no timeout at Node.js
// level but only at operating system one.
//
// The default for MacOS is 75 seconds. It can be changed by doing:
//
// sudo sysctl net.inet.tcp.keepinit=VALUE_IN_MS
//
// Test that all failure events are emitted when trying a single IP (which means autoselectfamily is bypassed)
{
const pass = common.mustCallAtLeast(1);
const connection = createConnection({
host: 'example.org',
port: 10,
lookup: createMockedLookup(INET4_IP),
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout: 10,
});
connection.on('connectionAttempt', (address, port, family) => {
assert.strictEqual(address, INET4_IP);
assert.strictEqual(port, 10);
assert.strictEqual(family, 4);
pass();
});
connection.on('connectionAttemptFailed', (address, port, family, error) => {
assert.strictEqual(address, INET4_IP);
assert.strictEqual(port, 10);
assert.strictEqual(family, 4);
assert.ok(
error.code.match(/ECONNREFUSED|ENETUNREACH|EHOSTUNREACH|ETIMEDOUT/),
`Received unexpected error code ${error.code}`,
);
pass();
});
connection.on('ready', () => {
pass();
connection.destroy();
});
connection.on('error', () => {
pass();
connection.destroy();
});
setTimeout(() => {
pass();
process.exit(0);
}, 5000).unref();
}
// Test that all events are emitted when trying multiple IPs
{
const pass = common.mustCallAtLeast(1);
const connection = createConnection({
host: 'example.org',
port: 10,
lookup: createMockedLookup(INET6_IP, INET4_IP),
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout: 10,
});
const addresses = [
{ address: INET6_IP, port: 10, family: 6 },
{ address: INET6_IP, port: 10, family: 6 },
{ address: INET4_IP, port: 10, family: 4 },
{ address: INET4_IP, port: 10, family: 4 },
];
connection.on('connectionAttempt', (address, port, family) => {
const expected = addresses.shift();
assert.strictEqual(address, expected.address);
assert.strictEqual(port, expected.port);
assert.strictEqual(family, expected.family);
pass();
});
connection.on('connectionAttemptFailed', (address, port, family, error) => {
const expected = addresses.shift();
assert.strictEqual(address, expected.address);
assert.strictEqual(port, expected.port);
assert.strictEqual(family, expected.family);
assert.ok(
error.code.match(/ECONNREFUSED|ENETUNREACH|EHOSTUNREACH|ETIMEDOUT/),
`Received unexpected error code ${error.code}`,
);
pass();
});
connection.on('ready', () => {
pass();
connection.destroy();
});
connection.on('error', () => {
pass();
connection.destroy();
});
setTimeout(() => {
pass();
process.exit(0);
}, 5000).unref();
}

View File

@ -0,0 +1,54 @@
'use strict';
const common = require('../common');
const { addresses: { INET6_IP, INET4_IP } } = require('../common/internet');
const { createMockedLookup } = require('../common/dns');
const assert = require('assert');
const { createConnection } = require('net');
//
// When testing this is MacOS, remember that the last connection will have no timeout at Node.js
// level but only at operating system one.
//
// The default for MacOS is 75 seconds. It can be changed by doing:
//
// sudo sysctl net.inet.tcp.keepinit=VALUE_IN_MS
//
// Depending on the network, it might be impossible to obtain a timeout in 10ms,
// which is the minimum value allowed by network family autoselection.
// At the time of writing (Dec 2023), the network times out on local machine and in the Node CI,
// but it does not on GitHub actions runner.
// Therefore, after five seconds we just consider this test as passed.
// Test that if a connection attempt times out and the socket is destroyed before the
// next attempt starts then the process does not crash
{
const connection = createConnection({
host: 'example.org',
port: 443,
lookup: createMockedLookup(INET4_IP, INET6_IP),
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout: 10,
});
const pass = common.mustCall();
connection.on('connectionAttemptTimeout', (address, port, family) => {
assert.strictEqual(address, INET4_IP);
assert.strictEqual(port, 443);
assert.strictEqual(family, 4);
connection.destroy();
pass();
});
connection.on('ready', () => {
pass();
connection.destroy();
});
setTimeout(() => {
pass();
process.exit(0);
}, 5000).unref();
}

View File

@ -6,6 +6,15 @@ const { addresses } = require('../common/internet');
const assert = require('assert');
const { connect } = require('net');
//
// When testing this is MacOS, remember that the last connection will have no timeout at Node.js
// level but only at operating system one.
//
// The default for MacOS is 75 seconds. It can be changed by doing:
//
// sudo sysctl net.inet.tcp.keepinit=VALUE_IN_MS
//
// Test that when all errors are returned when no connections succeeded and that the close event is emitted
{
const connection = connect({

View File

@ -3,100 +3,46 @@
// Flags: --no-network-family-autoselection
const common = require('../common');
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
const { createMockedLookup } = require('../common/dns');
const assert = require('assert');
const dgram = require('dgram');
const { Resolver } = require('dns');
const { createConnection, createServer } = require('net');
// Test that happy eyeballs algorithm can be enable from command line.
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 NOT reached if IPV6 is not reachable and the option has been disabled via command line
{
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();
}));
const ipv4Server = createServer((socket) => {
socket.on('data', common.mustCall(() => {
socket.write('response-ipv4');
socket.end();
}));
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;
const connection = createConnection({
host: 'example.org',
port,
lookup: createMockedLookup('::1', '127.0.0.1'),
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;
connection.on('ready', common.mustNotCall());
connection.on('error', common.mustCall((error) => {
assert.strictEqual(connection.autoSelectFamilyAttemptedAddresses, undefined);
const connection = createConnection({
host: 'example.org',
port,
lookup,
});
if (common.hasIPv6) {
assert.strictEqual(error.code, 'ECONNREFUSED');
assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`);
} else if (error.code === 'EAFNOSUPPORT') {
assert.strictEqual(error.message, `connect EAFNOSUPPORT ::1:${port} - Local (undefined:undefined)`);
} else if (error.code === 'EUNATCH') {
assert.strictEqual(error.message, `connect EUNATCH ::1:${port} - Local (:::0)`);
} else {
assert.strictEqual(error.code, 'EADDRNOTAVAIL');
assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
}
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}`);
} else if (error.code === 'EAFNOSUPPORT') {
assert.strictEqual(error.message, `connect EAFNOSUPPORT ::1:${port} - Local (undefined:undefined)`);
} else if (error.code === 'EUNATCH') {
assert.strictEqual(error.message, `connect EUNATCH ::1:${port} - Local (:::0)`);
} else {
assert.strictEqual(error.code, 'EADDRNOTAVAIL');
assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
}
ipv4Server.close();
dnsServer.close();
}));
ipv4Server.close();
}));
}));
}

View File

@ -1,140 +1,83 @@
'use strict';
const common = require('../common');
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
const { createMockedLookup } = 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.
const autoSelectFamilyAttemptTimeout = common.defaultAutoSelectFamilyAttemptTimeout;
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();
}));
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: createMockedLookup('::1', '127.0.0.1'),
autoSelectFamilyAttemptTimeout,
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
setDefaultAutoSelectFamily(true);
let response = '';
connection.setEncoding('utf-8');
const connection = createConnection({
host: 'example.org',
port: ipv4Server.address().port,
lookup,
autoSelectFamilyAttemptTimeout,
});
connection.on('data', (chunk) => {
response += chunk;
});
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');
connection.on('end', common.mustCall(() => {
assert.strictEqual(response, 'response-ipv4');
ipv4Server.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();
}));
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: createMockedLookup('::1', '127.0.0.1'),
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
setDefaultAutoSelectFamily(false);
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 if (error.code === 'EAFNOSUPPORT') {
assert.strictEqual(error.message, `connect EAFNOSUPPORT ::1:${port} - Local (undefined:undefined)`);
} else if (error.code === 'EUNATCH') {
assert.strictEqual(error.message, `connect EUNATCH ::1:${port} - Local (:::0)`);
} else {
assert.strictEqual(error.code, 'EADDRNOTAVAIL');
assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
}
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 if (error.code === 'EAFNOSUPPORT') {
assert.strictEqual(error.message, `connect EAFNOSUPPORT ::1:${port} - Local (undefined:undefined)`);
} else if (error.code === 'EUNATCH') {
assert.strictEqual(error.message, `connect EUNATCH ::1:${port} - Local (:::0)`);
} else {
assert.strictEqual(error.code, 'EADDRNOTAVAIL');
assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
}
ipv4Server.close();
dnsServer.close();
}));
ipv4Server.close();
}));
}));
}

View File

@ -1,105 +1,52 @@
'use strict';
const common = require('../common');
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
const { createMockedLookup } = require('../common/dns');
const assert = require('assert');
const dgram = require('dgram');
const { Resolver } = require('dns');
const { createConnection, createServer } = require('net');
// Test that happy eyeballs algorithm is properly implemented when a A record is returned first.
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 }));
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: 'A', address: ipv4Addr, ttl: 123, domain: 'example.org' },
{ type: 'AAAA', address: ipv6Addr, 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 IPV6 is NOT reached if IPV4 is sorted first
if (common.hasIPv6) {
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();
const ipv4Server = createServer((socket) => {
socket.on('data', common.mustCall(() => {
socket.write('response-ipv4');
socket.end();
}));
});
const ipv6Server = createServer((socket) => {
socket.on('data', common.mustNotCall(() => {
socket.write('response-ipv6');
socket.end();
}));
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;
ipv6Server.listen(port, '::1', common.mustCall(() => {
const connection = createConnection({
host: 'example.org',
port,
lookup: createMockedLookup('127.0.0.1', '::1'),
autoSelectFamily: true,
});
let response = '';
connection.setEncoding('utf-8');
connection.on('data', (chunk) => {
response += chunk;
});
connection.on('end', common.mustCall(() => {
assert.strictEqual(response, 'response-ipv4');
ipv4Server.close();
ipv6Server.close();
}));
});
const ipv6Server = createServer((socket) => {
socket.on('data', common.mustNotCall(() => {
socket.write('response-ipv6');
socket.end();
}));
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;
ipv6Server.listen(port, '::1', common.mustCall(() => {
const connection = createConnection({
host: 'example.org',
port,
lookup,
autoSelectFamily: true,
});
let response = '';
connection.setEncoding('utf-8');
connection.on('data', (chunk) => {
response += chunk;
});
connection.on('end', common.mustCall(() => {
assert.strictEqual(response, 'response-ipv4');
ipv4Server.close();
ipv6Server.close();
dnsServer.close();
}));
connection.write('request');
}));
connection.write('request');
}));
}));
}

View File

@ -1,93 +1,136 @@
'use strict';
const common = require('../common');
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
const { createMockedLookup } = require('../common/dns');
const assert = require('assert');
const dgram = require('dgram');
const { Resolver } = require('dns');
const { createConnection, createServer } = require('net');
// Test that happy eyeballs algorithm is properly implemented.
// Purposely not using setDefaultAutoSelectFamilyAttemptTimeout here to test the
// parameter is correctly used in options.
//
// Some of the machines in the CI need more time to establish connection
const autoSelectFamilyAttemptTimeout = common.defaultAutoSelectFamilyAttemptTimeout;
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(ipv6Addrs, ipv4Addrs, cb) {
if (!Array.isArray(ipv6Addrs)) {
ipv6Addrs = [ipv6Addrs];
}
if (!Array.isArray(ipv4Addrs)) {
ipv4Addrs = [ipv4Addrs];
}
// 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: [
...ipv6Addrs.map((address) => ({ type: 'AAAA', address, ttl: 123, domain: 'example.org' })),
...ipv4Addrs.map((address) => ({ type: 'A', address, 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 if IPV6 is not reachable
{
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();
}));
const ipv4Server = createServer((socket) => {
socket.on('data', common.mustCall(() => {
socket.write('response-ipv4');
socket.end();
}));
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;
const connection = createConnection({
host: 'example.org',
port: port,
lookup: createMockedLookup('::1', '127.0.0.1'),
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout,
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;
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;
});
connection.on('end', common.mustCall(() => {
assert.strictEqual(response, 'response-ipv4');
ipv4Server.close();
}));
connection.write('request');
}));
}
// Test that only the last successful connection is established.
{
const ipv4Server = createServer((socket) => {
socket.on('data', common.mustCall(() => {
socket.write('response-ipv4');
socket.end();
}));
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;
const connection = createConnection({
host: 'example.org',
port: port,
lookup: createMockedLookup(
'2606:4700::6810:85e5', '2606:4700::6810:84e5', '::1',
'104.20.22.46', '104.20.23.46', '127.0.0.1',
),
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout,
});
let response = '';
connection.setEncoding('utf-8');
connection.on('ready', common.mustCall(() => {
assert.deepStrictEqual(
connection.autoSelectFamilyAttemptedAddresses,
[
`2606:4700::6810:85e5:${port}`,
`104.20.22.46:${port}`,
`2606:4700::6810:84e5:${port}`,
`104.20.23.46:${port}`,
`::1:${port}`,
`127.0.0.1:${port}`,
]
);
}));
connection.on('data', (chunk) => {
response += chunk;
});
connection.on('end', common.mustCall(() => {
assert.strictEqual(response, 'response-ipv4');
ipv4Server.close();
}));
connection.write('request');
}));
}
// Test that IPV4 is NOT reached if IPV6 is reachable
if (common.hasIPv6) {
const ipv4Server = createServer((socket) => {
socket.on('data', common.mustNotCall(() => {
socket.write('response-ipv4');
socket.end();
}));
});
const ipv6Server = createServer((socket) => {
socket.on('data', common.mustCall(() => {
socket.write('response-ipv6');
socket.end();
}));
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;
ipv6Server.listen(port, '::1', common.mustCall(() => {
const connection = createConnection({
host: 'example.org',
port: port,
lookup,
port,
lookup: createMockedLookup('::1', '127.0.0.1'),
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout,
});
@ -96,7 +139,7 @@ function createDnsServer(ipv6Addrs, ipv4Addrs, cb) {
connection.setEncoding('utf-8');
connection.on('ready', common.mustCall(() => {
assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`, `127.0.0.1:${port}`]);
assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`]);
}));
connection.on('data', (chunk) => {
@ -104,9 +147,9 @@ function createDnsServer(ipv6Addrs, ipv4Addrs, cb) {
});
connection.on('end', common.mustCall(() => {
assert.strictEqual(response, 'response-ipv4');
assert.strictEqual(response, 'response-ipv6');
ipv4Server.close();
dnsServer.close();
ipv6Server.close();
}));
connection.write('request');
@ -114,184 +157,67 @@ function createDnsServer(ipv6Addrs, ipv4Addrs, cb) {
}));
}
// Test that only the last successful connection is established.
{
createDnsServer(
['2606:4700::6810:85e5', '2606:4700::6810:84e5', '::1'],
['104.20.22.46', '104.20.23.46', '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(() => {
const port = ipv4Server.address().port;
const connection = createConnection({
host: 'example.org',
port: port,
lookup,
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout,
});
let response = '';
connection.setEncoding('utf-8');
connection.on('ready', common.mustCall(() => {
assert.deepStrictEqual(
connection.autoSelectFamilyAttemptedAddresses,
[
`2606:4700::6810:85e5:${port}`,
`104.20.22.46:${port}`,
`2606:4700::6810:84e5:${port}`,
`104.20.23.46:${port}`,
`::1:${port}`,
`127.0.0.1:${port}`,
]
);
}));
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 if IPV6 is reachable
if (common.hasIPv6) {
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
const ipv4Server = createServer((socket) => {
socket.on('data', common.mustNotCall(() => {
socket.write('response-ipv4');
socket.end();
}));
});
const ipv6Server = createServer((socket) => {
socket.on('data', common.mustCall(() => {
socket.write('response-ipv6');
socket.end();
}));
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;
ipv6Server.listen(port, '::1', common.mustCall(() => {
const connection = createConnection({
host: 'example.org',
port,
lookup,
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout,
});
let response = '';
connection.setEncoding('utf-8');
connection.on('ready', common.mustCall(() => {
assert.deepStrictEqual(connection.autoSelectFamilyAttemptedAddresses, [`::1:${port}`]);
}));
connection.on('data', (chunk) => {
response += chunk;
});
connection.on('end', common.mustCall(() => {
assert.strictEqual(response, 'response-ipv6');
ipv4Server.close();
ipv6Server.close();
dnsServer.close();
}));
connection.write('request');
}));
}));
}));
}
// Test that when all errors are returned when no connections succeeded
{
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
const connection = createConnection({
host: 'example.org',
port: 10,
lookup,
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout,
});
const connection = createConnection({
host: 'example.org',
port: 10,
lookup: createMockedLookup('::1', '127.0.0.1'),
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout,
});
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);
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);
const errors = error.errors.map((e) => e.message);
assert.ok(errors.includes('connect ECONNREFUSED 127.0.0.1:10'));
const errors = error.errors.map((e) => e.message);
assert.ok(errors.includes('connect ECONNREFUSED 127.0.0.1:10'));
if (common.hasIPv6) {
assert.ok(errors.includes('connect ECONNREFUSED ::1:10'));
}
dnsServer.close();
}));
if (common.hasIPv6) {
assert.ok(errors.includes('connect ECONNREFUSED ::1:10'));
}
}));
}
// Test that the option can be 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();
}));
const ipv4Server = createServer((socket) => {
socket.on('data', common.mustCall(() => {
socket.write('response-ipv4');
socket.end();
}));
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;
const connection = createConnection({
host: 'example.org',
port,
lookup: createMockedLookup('::1', '127.0.0.1'),
autoSelectFamily: false,
});
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;
connection.on('ready', common.mustNotCall());
connection.on('error', common.mustCall((error) => {
assert.strictEqual(connection.autoSelectFamilyAttemptedAddresses, undefined);
const connection = createConnection({
host: 'example.org',
port,
lookup,
autoSelectFamily: false,
});
if (common.hasIPv6) {
assert.strictEqual(error.code, 'ECONNREFUSED');
assert.strictEqual(error.message, `connect ECONNREFUSED ::1:${port}`);
} else if (error.code === 'EAFNOSUPPORT') {
assert.strictEqual(error.message, `connect EAFNOSUPPORT ::1:${port} - Local (undefined:undefined)`);
} else if (error.code === 'EUNATCH') {
assert.strictEqual(error.message, `connect EUNATCH ::1:${port} - Local (:::0)`);
} else {
assert.strictEqual(error.code, 'EADDRNOTAVAIL');
assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
}
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}`);
} else if (error.code === 'EAFNOSUPPORT') {
assert.strictEqual(error.message, `connect EAFNOSUPPORT ::1:${port} - Local (undefined:undefined)`);
} else if (error.code === 'EUNATCH') {
assert.strictEqual(error.message, `connect EUNATCH ::1:${port} - Local (:::0)`);
} else {
assert.strictEqual(error.code, 'EADDRNOTAVAIL');
assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
}
ipv4Server.close();
dnsServer.close();
}));
ipv4Server.close();
}));
}));
}