mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
net: add autoSelectFamily and autoSelectFamilyAttemptTimeout options
PR-URL: https://github.com/nodejs/node/pull/44731 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
@@ -856,6 +856,9 @@ behavior.
|
||||
<!-- YAML
|
||||
added: v0.1.90
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/44731
|
||||
description: Added the `autoSelectFamily` option.
|
||||
- version:
|
||||
- v17.7.0
|
||||
- v16.15.0
|
||||
@@ -902,6 +905,20 @@ For TCP connections, available `options` are:
|
||||
**Default:** `false`.
|
||||
* `keepAliveInitialDelay` {number} If set to a positive number, it sets the initial delay before
|
||||
the first keepalive probe is sent on an idle socket.**Default:** `0`.
|
||||
* `autoSelectFamily` {boolean}: If set to `true`, it enables a family autodetection algorithm
|
||||
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.
|
||||
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`.
|
||||
* `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.
|
||||
**Default:** `250`.
|
||||
|
||||
For [IPC][] connections, available `options` are:
|
||||
|
||||
@@ -1630,6 +1647,7 @@ net.isIPv6('fhqwhgads'); // returns false
|
||||
|
||||
[IPC]: #ipc-support
|
||||
[Identifying paths for IPC connections]: #identifying-paths-for-ipc-connections
|
||||
[RFC 8305]: https://www.rfc-editor.org/rfc/rfc8305.txt
|
||||
[Readable Stream]: stream.md#class-streamreadable
|
||||
[`'close'`]: #event-close
|
||||
[`'connect'`]: #event-connect
|
||||
|
||||
@@ -54,6 +54,7 @@ const EE = require('events');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const common = require('_tls_common');
|
||||
const { kWrapConnectedHandle } = require('internal/net');
|
||||
const JSStreamSocket = require('internal/js_stream_socket');
|
||||
const { Buffer } = require('buffer');
|
||||
let debug = require('internal/util/debuglog').debuglog('tls', (fn) => {
|
||||
@@ -598,11 +599,10 @@ TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() {
|
||||
this[kDisableRenegotiation] = true;
|
||||
};
|
||||
|
||||
TLSSocket.prototype._wrapHandle = function(wrap) {
|
||||
let handle;
|
||||
|
||||
if (wrap)
|
||||
TLSSocket.prototype._wrapHandle = function(wrap, handle) {
|
||||
if (!handle && wrap) {
|
||||
handle = wrap._handle;
|
||||
}
|
||||
|
||||
const options = this._tlsOptions;
|
||||
if (!handle) {
|
||||
@@ -633,6 +633,16 @@ TLSSocket.prototype._wrapHandle = function(wrap) {
|
||||
return res;
|
||||
};
|
||||
|
||||
TLSSocket.prototype[kWrapConnectedHandle] = function(handle) {
|
||||
this._handle = this._wrapHandle(null, handle);
|
||||
this.ssl = this._handle;
|
||||
this._init();
|
||||
|
||||
if (this._tlsOptions.enableTrace) {
|
||||
this._handle.enableTrace();
|
||||
}
|
||||
};
|
||||
|
||||
// This eliminates a cyclic reference to TLSWrap
|
||||
// Ref: https://github.com/nodejs/node/commit/f7620fb96d339f704932f9bb9a0dceb9952df2d4
|
||||
function defineHandleReading(socket, handle) {
|
||||
|
||||
@@ -168,6 +168,13 @@ const aggregateTwoErrors = hideStackFrames((innerError, outerError) => {
|
||||
return innerError || outerError;
|
||||
});
|
||||
|
||||
const aggregateErrors = hideStackFrames((errors, message, code) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const err = new AggregateError(new SafeArrayIterator(errors), message);
|
||||
err.code = errors[0]?.code;
|
||||
return err;
|
||||
});
|
||||
|
||||
// Lazily loaded
|
||||
let util;
|
||||
let assert;
|
||||
@@ -891,6 +898,7 @@ function determineSpecificType(value) {
|
||||
module.exports = {
|
||||
AbortError,
|
||||
aggregateTwoErrors,
|
||||
aggregateErrors,
|
||||
captureLargerStackTrace,
|
||||
codes,
|
||||
connResetException,
|
||||
|
||||
@@ -67,6 +67,7 @@ function makeSyncWrite(fd) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
kWrapConnectedHandle: Symbol('wrapConnectedHandle'),
|
||||
isIP,
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
|
||||
255
lib/net.js
255
lib/net.js
@@ -24,7 +24,10 @@
|
||||
const {
|
||||
ArrayIsArray,
|
||||
ArrayPrototypeIndexOf,
|
||||
ArrayPrototypePush,
|
||||
Boolean,
|
||||
FunctionPrototypeBind,
|
||||
MathMax,
|
||||
Number,
|
||||
NumberIsNaN,
|
||||
NumberParseInt,
|
||||
@@ -40,6 +43,7 @@ let debug = require('internal/util/debuglog').debuglog('net', (fn) => {
|
||||
debug = fn;
|
||||
});
|
||||
const {
|
||||
kWrapConnectedHandle,
|
||||
isIP,
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
@@ -96,6 +100,7 @@ const {
|
||||
ERR_SOCKET_CLOSED,
|
||||
ERR_MISSING_ARGS,
|
||||
},
|
||||
aggregateErrors,
|
||||
errnoException,
|
||||
exceptionWithHostPort,
|
||||
genericNodeError,
|
||||
@@ -105,6 +110,7 @@ const { isUint8Array } = require('internal/util/types');
|
||||
const { queueMicrotask } = require('internal/process/task_queues');
|
||||
const {
|
||||
validateAbortSignal,
|
||||
validateBoolean,
|
||||
validateFunction,
|
||||
validateInt32,
|
||||
validateNumber,
|
||||
@@ -119,8 +125,9 @@ let dns;
|
||||
let BlockList;
|
||||
let SocketAddress;
|
||||
|
||||
const { clearTimeout } = require('timers');
|
||||
const { clearTimeout, setTimeout } = require('timers');
|
||||
const { kTimeout } = require('internal/timers');
|
||||
const kTimeoutTriggered = Symbol('kTimeoutTriggered');
|
||||
|
||||
const DEFAULT_IPV4_ADDR = '0.0.0.0';
|
||||
const DEFAULT_IPV6_ADDR = '::';
|
||||
@@ -1041,6 +1048,73 @@ function internalConnect(
|
||||
}
|
||||
|
||||
|
||||
function internalConnectMultiple(context) {
|
||||
clearTimeout(context[kTimeout]);
|
||||
const self = context.socket;
|
||||
assert(self.connecting);
|
||||
|
||||
// All connections have been tried without success, destroy with error
|
||||
if (context.current === context.addresses.length) {
|
||||
self.destroy(aggregateErrors(context.errors));
|
||||
return;
|
||||
}
|
||||
|
||||
const { localPort, port, flags } = context;
|
||||
const { address, family: addressType } = context.addresses[context.current++];
|
||||
const handle = new TCP(TCPConstants.SOCKET);
|
||||
let localAddress;
|
||||
let err;
|
||||
|
||||
if (localPort) {
|
||||
if (addressType === 4) {
|
||||
localAddress = DEFAULT_IPV4_ADDR;
|
||||
err = handle.bind(localAddress, localPort);
|
||||
} else { // addressType === 6
|
||||
localAddress = DEFAULT_IPV6_ADDR;
|
||||
err = handle.bind6(localAddress, localPort, flags);
|
||||
}
|
||||
|
||||
debug('connect/multiple: binding to localAddress: %s and localPort: %d (addressType: %d)',
|
||||
localAddress, localPort, addressType);
|
||||
|
||||
err = checkBindError(err, localPort, handle);
|
||||
if (err) {
|
||||
ArrayPrototypePush(context.errors, exceptionWithHostPort(err, 'bind', localAddress, localPort));
|
||||
internalConnectMultiple(context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const req = new TCPConnectWrap();
|
||||
req.oncomplete = FunctionPrototypeBind(afterConnectMultiple, undefined, context);
|
||||
req.address = address;
|
||||
req.port = port;
|
||||
req.localAddress = localAddress;
|
||||
req.localPort = localPort;
|
||||
|
||||
if (addressType === 4) {
|
||||
err = handle.connect(req, address, port);
|
||||
} else {
|
||||
err = handle.connect6(req, address, port);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
const sockname = self._getsockname();
|
||||
let details;
|
||||
|
||||
if (sockname) {
|
||||
details = sockname.address + ':' + sockname.port;
|
||||
}
|
||||
|
||||
ArrayPrototypePush(context.errors, exceptionWithHostPort(err, 'connect', address, port, details));
|
||||
internalConnectMultiple(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the attempt has not returned an error, start the connection timer
|
||||
context[kTimeout] = setTimeout(internalConnectMultipleTimeout, context.timeout, context, req);
|
||||
}
|
||||
|
||||
Socket.prototype.connect = function(...args) {
|
||||
let normalized;
|
||||
// If passed an array, it's treated as an array of arguments that have
|
||||
@@ -1110,9 +1184,9 @@ function socketToDnsFamily(family) {
|
||||
}
|
||||
|
||||
function lookupAndConnect(self, options) {
|
||||
const { localAddress, localPort } = options;
|
||||
const { localAddress, localPort, autoSelectFamily } = options;
|
||||
const host = options.host || 'localhost';
|
||||
let { port } = options;
|
||||
let { port, autoSelectFamilyAttemptTimeout } = options;
|
||||
|
||||
if (localAddress && !isIP(localAddress)) {
|
||||
throw new ERR_INVALID_IP_ADDRESS(localAddress);
|
||||
@@ -1131,6 +1205,20 @@ function lookupAndConnect(self, options) {
|
||||
}
|
||||
port |= 0;
|
||||
|
||||
if (autoSelectFamily !== undefined) {
|
||||
validateBoolean(autoSelectFamily);
|
||||
}
|
||||
|
||||
if (autoSelectFamilyAttemptTimeout !== undefined) {
|
||||
validateInt32(autoSelectFamilyAttemptTimeout);
|
||||
|
||||
if (autoSelectFamilyAttemptTimeout < 10) {
|
||||
autoSelectFamilyAttemptTimeout = 10;
|
||||
}
|
||||
} else {
|
||||
autoSelectFamilyAttemptTimeout = 250;
|
||||
}
|
||||
|
||||
// If host is an IP, skip performing a lookup
|
||||
const addressType = isIP(host);
|
||||
if (addressType) {
|
||||
@@ -1165,6 +1253,26 @@ function lookupAndConnect(self, options) {
|
||||
debug('connect: dns options', dnsopts);
|
||||
self._host = host;
|
||||
const lookup = options.lookup || dns.lookup;
|
||||
|
||||
if (dnsopts.family !== 4 && dnsopts.family !== 6 && !localAddress && autoSelectFamily) {
|
||||
debug('connect: autodetecting');
|
||||
|
||||
dnsopts.all = true;
|
||||
lookupAndConnectMultiple(
|
||||
self,
|
||||
async_id_symbol,
|
||||
lookup,
|
||||
host,
|
||||
options,
|
||||
dnsopts,
|
||||
port,
|
||||
localPort,
|
||||
autoSelectFamilyAttemptTimeout
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
|
||||
lookup(host, dnsopts, function emitLookup(err, ip, addressType) {
|
||||
self.emit('lookup', err, ip, addressType, host);
|
||||
@@ -1199,6 +1307,86 @@ function lookupAndConnect(self, options) {
|
||||
});
|
||||
}
|
||||
|
||||
function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options, dnsopts, port, localPort, timeout) {
|
||||
defaultTriggerAsyncIdScope(self[async_id_symbol], function emitLookup() {
|
||||
lookup(host, dnsopts, function emitLookup(err, addresses) {
|
||||
// It's possible we were destroyed while looking this up.
|
||||
// XXX it would be great if we could cancel the promise returned by
|
||||
// the look up.
|
||||
if (!self.connecting) {
|
||||
return;
|
||||
} else if (err) {
|
||||
// net.createConnection() creates a net.Socket object and immediately
|
||||
// calls net.Socket.connect() on it (that's us). There are no event
|
||||
// listeners registered yet so defer the error event to the next tick.
|
||||
process.nextTick(connectErrorNT, self, err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter addresses by only keeping the one which are either IPv4 or IPV6.
|
||||
// The first valid address determines which group has preference on the
|
||||
// alternate family sorting which happens later.
|
||||
const validIps = [[], []];
|
||||
let destinations;
|
||||
for (let i = 0, l = addresses.length; i < l; i++) {
|
||||
const address = addresses[i];
|
||||
const { address: ip, family: addressType } = address;
|
||||
self.emit('lookup', err, ip, addressType, host);
|
||||
|
||||
if (isIP(ip) && (addressType === 4 || addressType === 6)) {
|
||||
if (!destinations) {
|
||||
destinations = addressType === 6 ? { 6: 0, 4: 1 } : { 4: 0, 6: 1 };
|
||||
}
|
||||
|
||||
ArrayPrototypePush(validIps[destinations[addressType]], address);
|
||||
}
|
||||
}
|
||||
|
||||
// When no AAAA or A records are available, fail on the first one
|
||||
if (!validIps[0].length && !validIps[1].length) {
|
||||
const { address: firstIp, family: firstAddressType } = addresses[0];
|
||||
|
||||
if (!isIP(firstIp)) {
|
||||
err = new ERR_INVALID_IP_ADDRESS(firstIp);
|
||||
process.nextTick(connectErrorNT, self, err);
|
||||
} else if (firstAddressType !== 4 && firstAddressType !== 6) {
|
||||
err = new ERR_INVALID_ADDRESS_FAMILY(firstAddressType,
|
||||
options.host,
|
||||
options.port);
|
||||
process.nextTick(connectErrorNT, self, err);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort addresses alternating families
|
||||
const toAttempt = [];
|
||||
for (let i = 0, l = MathMax(validIps[0].length, validIps[1].length); i < l; i++) {
|
||||
if (i in validIps[0]) {
|
||||
ArrayPrototypePush(toAttempt, validIps[0][i]);
|
||||
}
|
||||
if (i in validIps[1]) {
|
||||
ArrayPrototypePush(toAttempt, validIps[1][i]);
|
||||
}
|
||||
}
|
||||
|
||||
const context = {
|
||||
socket: self,
|
||||
addresses,
|
||||
current: 0,
|
||||
port,
|
||||
localPort,
|
||||
timeout,
|
||||
[kTimeout]: null,
|
||||
[kTimeoutTriggered]: false,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
self._unrefTimer();
|
||||
defaultTriggerAsyncIdScope(self[async_id_symbol], internalConnectMultiple, context);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function connectErrorNT(self, err) {
|
||||
self.destroy(err);
|
||||
@@ -1293,6 +1481,67 @@ function afterConnect(status, handle, req, readable, writable) {
|
||||
}
|
||||
}
|
||||
|
||||
function afterConnectMultiple(context, status, handle, req, readable, writable) {
|
||||
const self = context.socket;
|
||||
|
||||
// Make sure another connection is not spawned
|
||||
clearTimeout(context[kTimeout]);
|
||||
|
||||
// Some error occurred, add to the list of exceptions
|
||||
if (status !== 0) {
|
||||
let details;
|
||||
if (req.localAddress && req.localPort) {
|
||||
details = req.localAddress + ':' + req.localPort;
|
||||
}
|
||||
const ex = exceptionWithHostPort(status,
|
||||
'connect',
|
||||
req.address,
|
||||
req.port,
|
||||
details);
|
||||
if (details) {
|
||||
ex.localAddress = req.localAddress;
|
||||
ex.localPort = req.localPort;
|
||||
}
|
||||
|
||||
ArrayPrototypePush(context.errors, ex);
|
||||
|
||||
// Try the next address
|
||||
internalConnectMultiple(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// One of the connection has completed and correctly dispatched but after timeout, ignore this one
|
||||
if (context[kTimeoutTriggered]) {
|
||||
debug('connect/multiple: ignoring successful but timedout connection to %s:%s', req.address, req.port);
|
||||
handle.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform initialization sequence on the handle, then move on with the regular callback
|
||||
self._handle = handle;
|
||||
initSocketHandle(self);
|
||||
|
||||
if (self[kWrapConnectedHandle]) {
|
||||
self[kWrapConnectedHandle](handle);
|
||||
initSocketHandle(self); // This is called again to initialize the TLSWrap
|
||||
}
|
||||
|
||||
if (hasObserver('net')) {
|
||||
startPerf(
|
||||
self,
|
||||
kPerfHooksNetConnectContext,
|
||||
{ type: 'net', name: 'connect', detail: { host: req.address, port: req.port } }
|
||||
);
|
||||
}
|
||||
|
||||
afterConnect(status, handle, req, readable, writable);
|
||||
}
|
||||
|
||||
function internalConnectMultipleTimeout(context, req) {
|
||||
context[kTimeoutTriggered] = true;
|
||||
internalConnectMultiple(context);
|
||||
}
|
||||
|
||||
function addAbortSignalOption(self, options) {
|
||||
if (options?.signal === undefined) {
|
||||
return;
|
||||
|
||||
148
test/parallel/test-http-happy-eyeballs.js
Normal file
148
test/parallel/test-http-happy-eyeballs.js
Normal file
@@ -0,0 +1,148 @@
|
||||
'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 { request, createServer } = require('http');
|
||||
|
||||
// Test that happy eyeballs algorithm is properly implemented when using HTTP.
|
||||
|
||||
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 if IPV6 is not reachable
|
||||
{
|
||||
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
|
||||
const ipv4Server = createServer(common.mustCall((_, res) => {
|
||||
res.writeHead(200, { Connection: 'close' });
|
||||
res.end('response-ipv4');
|
||||
}));
|
||||
|
||||
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
|
||||
request(
|
||||
`http://example.org:${ipv4Server.address().port}/`,
|
||||
{
|
||||
lookup,
|
||||
autoSelectFamily: true,
|
||||
autoSelectFamilyAttemptTimeout
|
||||
},
|
||||
(res) => {
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
res.setEncoding('utf-8');
|
||||
|
||||
let response = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
response += chunk;
|
||||
});
|
||||
|
||||
res.on('end', common.mustCall(() => {
|
||||
assert.strictEqual(response, 'response-ipv4');
|
||||
ipv4Server.close();
|
||||
dnsServer.close();
|
||||
}));
|
||||
}
|
||||
).end();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
// 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(common.mustNotCall((_, res) => {
|
||||
res.writeHead(200, { Connection: 'close' });
|
||||
res.end('response-ipv4');
|
||||
}));
|
||||
|
||||
const ipv6Server = createServer(common.mustCall((_, res) => {
|
||||
res.writeHead(200, { Connection: 'close' });
|
||||
res.end('response-ipv6');
|
||||
}));
|
||||
|
||||
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
|
||||
const port = ipv4Server.address().port;
|
||||
|
||||
ipv6Server.listen(port, '::1', common.mustCall(() => {
|
||||
request(
|
||||
`http://example.org:${ipv4Server.address().port}/`,
|
||||
{
|
||||
lookup,
|
||||
autoSelectFamily: true,
|
||||
autoSelectFamilyAttemptTimeout,
|
||||
},
|
||||
(res) => {
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
res.setEncoding('utf-8');
|
||||
|
||||
let response = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
response += chunk;
|
||||
});
|
||||
|
||||
res.on('end', common.mustCall(() => {
|
||||
assert.strictEqual(response, 'response-ipv6');
|
||||
ipv4Server.close();
|
||||
ipv6Server.close();
|
||||
dnsServer.close();
|
||||
}));
|
||||
}
|
||||
).end();
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
}
|
||||
164
test/parallel/test-https-happy-eyeballs.js
Normal file
164
test/parallel/test-https-happy-eyeballs.js
Normal file
@@ -0,0 +1,164 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
|
||||
if (!common.hasCrypto) {
|
||||
common.skip('missing crypto');
|
||||
}
|
||||
|
||||
const { parseDNSPacket, writeDNSPacket } = require('../common/dns');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
const assert = require('assert');
|
||||
const dgram = require('dgram');
|
||||
const { Resolver } = require('dns');
|
||||
const { request, createServer } = require('https');
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
const options = {
|
||||
key: fixtures.readKey('agent1-key.pem'),
|
||||
cert: fixtures.readKey('agent1-cert.pem')
|
||||
};
|
||||
|
||||
// Test that happy eyeballs algorithm is properly implemented when using HTTP.
|
||||
|
||||
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 if IPV6 is not reachable
|
||||
{
|
||||
createDnsServer('::1', '127.0.0.1', common.mustCall(function({ dnsServer, lookup }) {
|
||||
const ipv4Server = createServer(options, common.mustCall((_, res) => {
|
||||
res.writeHead(200, { Connection: 'close' });
|
||||
res.end('response-ipv4');
|
||||
}));
|
||||
|
||||
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
|
||||
request(
|
||||
`https://example.org:${ipv4Server.address().port}/`,
|
||||
{
|
||||
lookup,
|
||||
rejectUnauthorized: false,
|
||||
autoSelectFamily: true,
|
||||
autoSelectFamilyAttemptTimeout
|
||||
},
|
||||
(res) => {
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
res.setEncoding('utf-8');
|
||||
|
||||
let response = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
response += chunk;
|
||||
});
|
||||
|
||||
res.on('end', common.mustCall(() => {
|
||||
assert.strictEqual(response, 'response-ipv4');
|
||||
ipv4Server.close();
|
||||
dnsServer.close();
|
||||
}));
|
||||
}
|
||||
).end();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
// 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(options, common.mustNotCall((_, res) => {
|
||||
res.writeHead(200, { Connection: 'close' });
|
||||
res.end('response-ipv4');
|
||||
}));
|
||||
|
||||
const ipv6Server = createServer(options, common.mustCall((_, res) => {
|
||||
res.writeHead(200, { Connection: 'close' });
|
||||
res.end('response-ipv6');
|
||||
}));
|
||||
|
||||
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
|
||||
const port = ipv4Server.address().port;
|
||||
|
||||
ipv6Server.listen(port, '::1', common.mustCall(() => {
|
||||
request(
|
||||
`https://example.org:${ipv4Server.address().port}/`,
|
||||
{
|
||||
lookup,
|
||||
rejectUnauthorized: false,
|
||||
autoSelectFamily: true,
|
||||
autoSelectFamilyAttemptTimeout,
|
||||
},
|
||||
(res) => {
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
res.setEncoding('utf-8');
|
||||
|
||||
let response = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
response += chunk;
|
||||
});
|
||||
|
||||
res.on('end', common.mustCall(() => {
|
||||
assert.strictEqual(response, 'response-ipv6');
|
||||
ipv4Server.close();
|
||||
ipv6Server.close();
|
||||
dnsServer.close();
|
||||
}));
|
||||
}
|
||||
).end();
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
}
|
||||
112
test/parallel/test-net-happy-eyeballs-ipv4first.js
Normal file
112
test/parallel/test-net-happy-eyeballs-ipv4first.js
Normal file
@@ -0,0 +1,112 @@
|
||||
'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 } = require('net');
|
||||
|
||||
// Test that happy eyeballs algorithm is properly implemented when a A record is returned first.
|
||||
|
||||
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 }));
|
||||
|
||||
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 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,
|
||||
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();
|
||||
ipv6Server.close();
|
||||
dnsServer.close();
|
||||
}));
|
||||
|
||||
connection.write('request');
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
}
|
||||
215
test/parallel/test-net-happy-eyeballs.js
Normal file
215
test/parallel/test-net-happy-eyeballs.js
Normal file
@@ -0,0 +1,215 @@
|
||||
'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 } = require('net');
|
||||
|
||||
// Test that happy eyeballs algorithm is properly implemented.
|
||||
|
||||
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 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();
|
||||
}));
|
||||
});
|
||||
|
||||
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
|
||||
const connection = createConnection({
|
||||
host: 'example.org',
|
||||
port: ipv4Server.address().port,
|
||||
lookup,
|
||||
autoSelectFamily: true,
|
||||
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 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('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,
|
||||
});
|
||||
|
||||
connection.on('ready', common.mustNotCall());
|
||||
connection.on('error', common.mustCall((error) => {
|
||||
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'));
|
||||
|
||||
if (common.hasIPv6) {
|
||||
assert.ok(errors.includes('connect ECONNREFUSED ::1:10'));
|
||||
}
|
||||
|
||||
dnsServer.close();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
// 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();
|
||||
}));
|
||||
});
|
||||
|
||||
ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => {
|
||||
const port = ipv4Server.address().port;
|
||||
|
||||
const connection = createConnection({
|
||||
host: 'example.org',
|
||||
port,
|
||||
lookup,
|
||||
autoSelectFamily: 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 {
|
||||
assert.strictEqual(error.code, 'EADDRNOTAVAIL');
|
||||
assert.strictEqual(error.message, `connect EADDRNOTAVAIL ::1:${port} - Local (:::0)`);
|
||||
}
|
||||
|
||||
ipv4Server.close();
|
||||
dnsServer.close();
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user