Skip to content

Commit

Permalink
net: added connection attempt events
Browse files Browse the repository at this point in the history
  • Loading branch information
ShogunPanda committed Dec 5, 2023
1 parent 2eb1808 commit 7546326
Show file tree
Hide file tree
Showing 10 changed files with 468 additions and 514 deletions.
44 changes: 42 additions & 2 deletions doc/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `'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.

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
Expand Down Expand Up @@ -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.
Expand Down
31 changes: 26 additions & 5 deletions lib/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -1149,13 +1151,15 @@ 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);
req.address = address;
req.port = port;
req.localAddress = localAddress;
req.localPort = localPort;
req.addressType = addressType;

ArrayPrototypePush(self.autoSelectFamilyAttemptedAddresses, `${address}:${port}`);

Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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) {
Expand Down
21 changes: 21 additions & 0 deletions test/common/dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const assert = require('assert');
const os = require('os');
const { isIP } = require('net');

const types = {
A: 1,
Expand Down Expand Up @@ -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,
Expand All @@ -317,4 +337,5 @@ module.exports = {
errorLookupMock,
mockedErrorCode,
mockedSysCall,
createMockedLookup,
};
29 changes: 29 additions & 0 deletions test/internet/test-net-autoselectfamily-events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

const common = require('../common');
const { addresses: { INET4_IP } } = require('../common/internet');
const { createMockedLookup } = require('../common/dns');

const assert = require('assert');
const { createConnection } = require('net');

// 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, '127.0.0.1'),
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout: 10,
});

connection.on('connectionAttemptTimeout', common.mustCall((address, port, family) => {
assert.strictEqual(address, INET4_IP);
assert.strictEqual(port, 443);
assert.strictEqual(family, 4);
connection.destroy();
}));

connection.on('ready', common.mustNotCall());
}
9 changes: 9 additions & 0 deletions test/internet/test-net-autoselectfamily-timeout-close.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
116 changes: 31 additions & 85 deletions test/parallel/test-net-autoselectfamily-commandline-option.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}));
});

ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;

const connection = createConnection({
host: 'example.org',
port,
lookup,
});
const ipv4Server = createServer((socket) => {
socket.on('data', common.mustCall(() => {
socket.write('response-ipv4');
socket.end();
}));
});

connection.on('ready', common.mustNotCall());
connection.on('error', common.mustCall((error) => {
assert.strictEqual(connection.autoSelectFamilyAttemptedAddresses, undefined);
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
const port = ipv4Server.address().port;

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 connection = createConnection({
host: 'example.org',
port,
lookup: createMockedLookup('::1', '127.0.0.1'),
});

ipv4Server.close();
dnsServer.close();
}));
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();
}));
}));
}

0 comments on commit 7546326

Please sign in to comment.