fix(ext/node): add autoSelectFamily option to net.createConnection (#26661)

This commit is contained in:
Yoshiya Hinosawa 2024-11-12 19:54:47 +09:00 committed by GitHub
parent 90236d67c5
commit c3c2b37966
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 824 additions and 16 deletions

View File

@ -18,7 +18,7 @@
*/
import { primordials } from "ext:core/mod.js";
const { JSONStringify, SymbolFor } = primordials;
const { JSONStringify, SafeArrayIterator, SymbolFor } = primordials;
import { format, inspect } from "ext:deno_node/internal/util/inspect.mjs";
import { codes } from "ext:deno_node/internal/error_codes.ts";
import {
@ -1874,6 +1874,11 @@ export class ERR_SOCKET_CLOSED extends NodeError {
super("ERR_SOCKET_CLOSED", `Socket is closed`);
}
}
export class ERR_SOCKET_CONNECTION_TIMEOUT extends NodeError {
constructor() {
super("ERR_SOCKET_CONNECTION_TIMEOUT", `Socket connection timeout`);
}
}
export class ERR_SOCKET_DGRAM_IS_CONNECTED extends NodeError {
constructor() {
super("ERR_SOCKET_DGRAM_IS_CONNECTED", `Already connected`);
@ -2633,11 +2638,30 @@ export function aggregateTwoErrors(
}
return innerError || outerError;
}
export class NodeAggregateError extends AggregateError {
code: string;
constructor(errors, message) {
super(new SafeArrayIterator(errors), message);
this.code = errors[0]?.code;
}
get [kIsNodeError]() {
return true;
}
// deno-lint-ignore adjacent-overload-signatures
get ["constructor"]() {
return AggregateError;
}
}
codes.ERR_IPC_CHANNEL_CLOSED = ERR_IPC_CHANNEL_CLOSED;
codes.ERR_INVALID_ARG_TYPE = ERR_INVALID_ARG_TYPE;
codes.ERR_INVALID_ARG_VALUE = ERR_INVALID_ARG_VALUE;
codes.ERR_OUT_OF_RANGE = ERR_OUT_OF_RANGE;
codes.ERR_SOCKET_BAD_PORT = ERR_SOCKET_BAD_PORT;
codes.ERR_SOCKET_CONNECTION_TIMEOUT = ERR_SOCKET_CONNECTION_TIMEOUT;
codes.ERR_BUFFER_OUT_OF_BOUNDS = ERR_BUFFER_OUT_OF_BOUNDS;
codes.ERR_UNKNOWN_ENCODING = ERR_UNKNOWN_ENCODING;
codes.ERR_PARSE_ARGS_INVALID_OPTION_VALUE = ERR_PARSE_ARGS_INVALID_OPTION_VALUE;

View File

@ -95,4 +95,5 @@ export function makeSyncWrite(fd: number) {
};
}
export const kReinitializeHandle = Symbol("kReinitializeHandle");
export const normalizedArgsSymbol = Symbol("normalizedArgs");

View File

@ -530,10 +530,12 @@ export function mapSysErrnoToUvErrno(sysErrno: number): number {
export const UV_EAI_MEMORY = codeMap.get("EAI_MEMORY")!;
export const UV_EBADF = codeMap.get("EBADF")!;
export const UV_ECANCELED = codeMap.get("ECANCELED")!;
export const UV_EEXIST = codeMap.get("EEXIST");
export const UV_EINVAL = codeMap.get("EINVAL")!;
export const UV_ENOENT = codeMap.get("ENOENT");
export const UV_ENOTSOCK = codeMap.get("ENOTSOCK")!;
export const UV_ETIMEDOUT = codeMap.get("ETIMEDOUT")!;
export const UV_UNKNOWN = codeMap.get("UNKNOWN")!;
export function errname(errno: number): string {

View File

@ -31,6 +31,7 @@ import {
isIP,
isIPv4,
isIPv6,
kReinitializeHandle,
normalizedArgsSymbol,
} from "ext:deno_node/internal/net.ts";
import { Duplex } from "node:stream";
@ -50,9 +51,11 @@ import {
ERR_SERVER_ALREADY_LISTEN,
ERR_SERVER_NOT_RUNNING,
ERR_SOCKET_CLOSED,
ERR_SOCKET_CONNECTION_TIMEOUT,
errnoException,
exceptionWithHostPort,
genericNodeError,
NodeAggregateError,
uvExceptionWithHostPort,
} from "ext:deno_node/internal/errors.ts";
import type { ErrnoException } from "ext:deno_node/internal/errors.ts";
@ -80,6 +83,7 @@ import { Buffer } from "node:buffer";
import type { LookupOneOptions } from "ext:deno_node/internal/dns/utils.ts";
import {
validateAbortSignal,
validateBoolean,
validateFunction,
validateInt32,
validateNumber,
@ -100,13 +104,25 @@ import { ShutdownWrap } from "ext:deno_node/internal_binding/stream_wrap.ts";
import { assert } from "ext:deno_node/_util/asserts.ts";
import { isWindows } from "ext:deno_node/_util/os.ts";
import { ADDRCONFIG, lookup as dnsLookup } from "node:dns";
import { codeMap } from "ext:deno_node/internal_binding/uv.ts";
import {
codeMap,
UV_ECANCELED,
UV_ETIMEDOUT,
} from "ext:deno_node/internal_binding/uv.ts";
import { guessHandleType } from "ext:deno_node/internal_binding/util.ts";
import { debuglog } from "ext:deno_node/internal/util/debuglog.ts";
import type { DuplexOptions } from "ext:deno_node/_stream.d.ts";
import type { BufferEncoding } from "ext:deno_node/_global.d.ts";
import type { Abortable } from "ext:deno_node/_events.d.ts";
import { channel } from "node:diagnostics_channel";
import { primordials } from "ext:core/mod.js";
const {
ArrayPrototypeIncludes,
ArrayPrototypePush,
FunctionPrototypeBind,
MathMax,
} = primordials;
let debug = debuglog("net", (fn) => {
debug = fn;
@ -120,6 +136,9 @@ const kBytesWritten = Symbol("kBytesWritten");
const DEFAULT_IPV4_ADDR = "0.0.0.0";
const DEFAULT_IPV6_ADDR = "::";
let autoSelectFamilyDefault = true;
let autoSelectFamilyAttemptTimeoutDefault = 250;
type Handle = TCP | Pipe;
interface HandleOptions {
@ -214,6 +233,8 @@ interface TcpSocketConnectOptions extends ConnectOptions {
hints?: number;
family?: number;
lookup?: LookupFunction;
autoSelectFamily?: boolean | undefined;
autoSelectFamilyAttemptTimeout?: number | undefined;
}
interface IpcSocketConnectOptions extends ConnectOptions {
@ -316,12 +337,6 @@ export function _normalizeArgs(args: unknown[]): NormalizedArgs {
return arr;
}
function _isTCPConnectWrap(
req: TCPConnectWrap | PipeConnectWrap,
): req is TCPConnectWrap {
return "localAddress" in req && "localPort" in req;
}
function _afterConnect(
status: number,
// deno-lint-ignore no-explicit-any
@ -372,7 +387,7 @@ function _afterConnect(
socket.connecting = false;
let details;
if (_isTCPConnectWrap(req)) {
if (req.localAddress && req.localPort) {
details = req.localAddress + ":" + req.localPort;
}
@ -384,7 +399,7 @@ function _afterConnect(
details,
);
if (_isTCPConnectWrap(req)) {
if (details) {
ex.localAddress = req.localAddress;
ex.localPort = req.localPort;
}
@ -393,6 +408,107 @@ function _afterConnect(
}
}
function _createConnectionError(req, status) {
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;
}
return ex;
}
function _afterConnectMultiple(
context,
current,
status,
handle,
req,
readable,
writable,
) {
debug(
"connect/multiple: connection attempt to %s:%s completed with status %s",
req.address,
req.port,
status,
);
// Make sure another connection is not spawned
clearTimeout(context[kTimeout]);
// One of the connection has completed and correctly dispatched but after timeout, ignore this one
if (status === 0 && current !== context.current - 1) {
debug(
"connect/multiple: ignoring successful but timedout connection to %s:%s",
req.address,
req.port,
);
handle.close();
return;
}
const self = context.socket;
// Some error occurred, add to the list of exceptions
if (status !== 0) {
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);
}
return;
}
_afterConnect(status, self._handle, req, readable, writable);
}
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();
// Try the next address, unless we were aborted
if (context.socket.connecting) {
_internalConnectMultiple(context);
}
}
function _checkBindError(err: number, port: number, handle: TCP) {
// EADDRINUSE may not be reported until we call `listen()` or `connect()`.
// To complicate matters, a failed `bind()` followed by `listen()` or `connect()`
@ -495,6 +611,131 @@ function _internalConnect(
}
}
function _internalConnectMultiple(context, canceled?: boolean) {
clearTimeout(context[kTimeout]);
const self = context.socket;
// We were requested to abort. Stop all operations
if (self._aborted) {
return;
}
// All connections have been tried without success, destroy with error
if (canceled || context.current === context.addresses.length) {
if (context.errors.length === 0) {
self.destroy(new ERR_SOCKET_CONNECTION_TIMEOUT());
return;
}
self.destroy(new NodeAggregateError(context.errors));
return;
}
assert(self.connecting);
const current = context.current++;
if (current > 0) {
self[kReinitializeHandle](new TCP(TCPConstants.SOCKET));
}
const { localPort, port, flags } = context;
const { address, family: addressType } = context.addresses[current];
let localAddress;
let err;
if (localPort) {
if (addressType === 4) {
localAddress = DEFAULT_IPV4_ADDR;
err = self._handle.bind(localAddress, localPort);
} else { // addressType === 6
localAddress = DEFAULT_IPV6_ADDR;
err = self._handle.bind6(localAddress, localPort, flags);
}
debug(
"connect/multiple: binding to localAddress: %s and localPort: %d (addressType: %d)",
localAddress,
localPort,
addressType,
);
err = _checkBindError(err, localPort, self._handle);
if (err) {
ArrayPrototypePush(
context.errors,
exceptionWithHostPort(err, "bind", localAddress, localPort),
);
_internalConnectMultiple(context);
return;
}
}
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}`,
);
if (addressType === 4) {
err = self._handle.connect(req, address, port);
} else {
err = self._handle.connect6(req, address, port);
}
if (err) {
const sockname = self._getsockname();
let details;
if (sockname) {
details = sockname.address + ":" + sockname.port;
}
const ex = exceptionWithHostPort(err, "connect", address, port, details);
ArrayPrototypePush(context.errors, ex);
self.emit("connectionAttemptFailed", address, port, addressType, ex);
_internalConnectMultiple(context);
return;
}
if (current < context.addresses.length - 1) {
debug(
"connect/multiple: setting the attempt timeout to %d ms",
context.timeout,
);
// If the attempt has not returned an error, start the connection timer
context[kTimeout] = setTimeout(
_internalConnectMultipleTimeout,
context.timeout,
context,
req,
self._handle,
);
}
}
// Provide a better error message when we call end() as a result
// of the other side sending a FIN. The standard "write after end"
// is overly vague, and makes it seem like the user's code is to blame.
@ -597,7 +838,7 @@ function _lookupAndConnect(
) {
const { localAddress, localPort } = options;
const host = options.host || "localhost";
let { port } = options;
let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options;
if (localAddress && !isIP(localAddress)) {
throw new ERR_INVALID_IP_ADDRESS(localAddress);
@ -621,6 +862,22 @@ function _lookupAndConnect(
port |= 0;
if (autoSelectFamily != null) {
validateBoolean(autoSelectFamily, "options.autoSelectFamily");
} else {
autoSelectFamily = autoSelectFamilyDefault;
}
if (autoSelectFamilyAttemptTimeout !== undefined) {
validateInt32(autoSelectFamilyAttemptTimeout);
if (autoSelectFamilyAttemptTimeout < 10) {
autoSelectFamilyAttemptTimeout = 10;
}
} else {
autoSelectFamilyAttemptTimeout = autoSelectFamilyAttemptTimeoutDefault;
}
// If host is an IP, skip performing a lookup
const addressType = isIP(host);
if (addressType) {
@ -649,6 +906,7 @@ function _lookupAndConnect(
const dnsOpts = {
family: options.family,
hints: options.hints || 0,
all: false,
};
if (
@ -665,6 +923,31 @@ function _lookupAndConnect(
self._host = host;
const lookup = options.lookup || dnsLookup;
if (
dnsOpts.family !== 4 && dnsOpts.family !== 6 && !localAddress &&
autoSelectFamily
) {
debug("connect: autodetecting");
dnsOpts.all = true;
defaultTriggerAsyncIdScope(self[asyncIdSymbol], function () {
_lookupAndConnectMultiple(
self,
asyncIdSymbol,
lookup,
host,
options,
dnsOpts,
port,
localAddress,
localPort,
autoSelectFamilyAttemptTimeout,
);
});
return;
}
defaultTriggerAsyncIdScope(self[asyncIdSymbol], function () {
lookup(
host,
@ -719,6 +1002,143 @@ function _lookupAndConnect(
});
}
function _lookupAndConnectMultiple(
self: Socket,
asyncIdSymbol: number,
// deno-lint-ignore no-explicit-any
lookup: any,
host: string,
options: TcpSocketConnectOptions,
dnsopts,
port: number,
localAddress: string,
localPort: number,
timeout: number | undefined,
) {
defaultTriggerAsyncIdScope(self[asyncIdSymbol], 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) {
self.emit("lookup", err, undefined, undefined, host);
// 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.
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 validAddresses = [[], []];
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);
// It's possible we were destroyed while looking this up.
if (!self.connecting) {
return;
}
if (isIP(ip) && (addressType === 4 || addressType === 6)) {
destinations ||= addressType === 6 ? { 6: 0, 4: 1 } : { 4: 0, 6: 1 };
const destination = destinations[addressType];
// Only try an address once
if (!ArrayPrototypeIncludes(validIps[destination], ip)) {
ArrayPrototypePush(validAddresses[destination], address);
ArrayPrototypePush(validIps[destination], ip);
}
}
}
// When no AAAA or A records are available, fail on the first one
if (!validAddresses[0].length && !validAddresses[1].length) {
const { address: firstIp, family: firstAddressType } = addresses[0];
if (!isIP(firstIp)) {
err = new ERR_INVALID_IP_ADDRESS(firstIp);
nextTick(_connectErrorNT, self, err);
} else if (firstAddressType !== 4 && firstAddressType !== 6) {
err = new ERR_INVALID_ADDRESS_FAMILY(
firstAddressType,
options.host,
options.port,
);
nextTick(_connectErrorNT, self, err);
}
return;
}
// Sort addresses alternating families
const toAttempt = [];
for (
let i = 0,
l = MathMax(validAddresses[0].length, validAddresses[1].length);
i < l;
i++
) {
if (i in validAddresses[0]) {
ArrayPrototypePush(toAttempt, validAddresses[0][i]);
}
if (i in validAddresses[1]) {
ArrayPrototypePush(toAttempt, validAddresses[1][i]);
}
}
if (toAttempt.length === 1) {
debug(
"connect/multiple: only one address found, switching back to single connection",
);
const { address: ip, family: addressType } = toAttempt[0];
self._unrefTimer();
defaultTriggerAsyncIdScope(
self[asyncIdSymbol],
_internalConnect,
self,
ip,
port,
addressType,
localAddress,
localPort,
);
return;
}
self.autoSelectFamilyAttemptedAddresses = [];
debug("connect/multiple: will try the following addresses", toAttempt);
const context = {
socket: self,
addresses: toAttempt,
current: 0,
port,
localPort,
timeout,
[kTimeout]: null,
errors: [],
};
self._unrefTimer();
defaultTriggerAsyncIdScope(
self[asyncIdSymbol],
_internalConnectMultiple,
context,
);
});
});
}
function _afterShutdown(this: ShutdownWrap<TCP>) {
// deno-lint-ignore no-explicit-any
const self: any = this.handle[ownerSymbol];
@ -777,6 +1197,7 @@ export class Socket extends Duplex {
_host: string | null = null;
// deno-lint-ignore no-explicit-any
_parent: any = null;
autoSelectFamilyAttemptedAddresses: AddressInfo[] | undefined = undefined;
constructor(options: SocketOptions | number) {
if (typeof options === "number") {
@ -1546,6 +1967,16 @@ export class Socket extends Duplex {
set _handle(v: Handle | null) {
this[kHandle] = v;
}
// deno-lint-ignore no-explicit-any
[kReinitializeHandle](handle: any) {
this._handle?.close();
this._handle = handle;
this._handle[ownerSymbol] = this;
_initSocketHandle(this);
}
}
export const Stream = Socket;
@ -1593,6 +2024,33 @@ export function connect(...args: unknown[]) {
export const createConnection = connect;
/** https://docs.deno.com/api/node/net/#namespace_getdefaultautoselectfamily */
export function getDefaultAutoSelectFamily() {
return autoSelectFamilyDefault;
}
/** https://docs.deno.com/api/node/net/#namespace_setdefaultautoselectfamily */
export function setDefaultAutoSelectFamily(value: boolean) {
validateBoolean(value, "value");
autoSelectFamilyDefault = value;
}
/** https://docs.deno.com/api/node/net/#namespace_getdefaultautoselectfamilyattempttimeout */
export function getDefaultAutoSelectFamilyAttemptTimeout() {
return autoSelectFamilyAttemptTimeoutDefault;
}
/** https://docs.deno.com/api/node/net/#namespace_setdefaultautoselectfamilyattempttimeout */
export function setDefaultAutoSelectFamilyAttemptTimeout(value: number) {
validateInt32(value, "value", 1);
if (value < 10) {
value = 10;
}
autoSelectFamilyAttemptTimeoutDefault = value;
}
export interface ListenOptions extends Abortable {
fd?: number;
port?: number | undefined;
@ -2478,15 +2936,19 @@ export { BlockList, isIP, isIPv4, isIPv6, SocketAddress };
export default {
_createServerHandle,
_normalizeArgs,
isIP,
isIPv4,
isIPv6,
BlockList,
SocketAddress,
connect,
createConnection,
createServer,
getDefaultAutoSelectFamily,
getDefaultAutoSelectFamilyAttemptTimeout,
isIP,
isIPv4,
isIPv6,
Server,
setDefaultAutoSelectFamily,
setDefaultAutoSelectFamilyAttemptTimeout,
Socket,
SocketAddress,
Stream,
};

View File

@ -77,6 +77,7 @@
"test-fs-rmdir-recursive.js",
"test-fs-write-file.js",
"test-http-url.parse-https.request.js",
"test-net-autoselectfamily.js",
"test-net-better-error-messages-path.js",
"test-net-connect-buffer.js",
"test-net-connect-buffer2.js",
@ -404,6 +405,7 @@
"test-http-url.parse-only-support-http-https-protocol.js",
"test-icu-transcode.js",
"test-net-access-byteswritten.js",
"test-net-autoselectfamily.js",
"test-net-better-error-messages-listen-path.js",
"test-net-better-error-messages-path.js",
"test-net-better-error-messages-port-hostname.js",

View File

@ -1767,7 +1767,6 @@ NOTE: This file should not be manually edited. Please edit `tests/node_compat/co
- [parallel/test-net-autoselectfamily-commandline-option.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily-commandline-option.js)
- [parallel/test-net-autoselectfamily-default.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily-default.js)
- [parallel/test-net-autoselectfamily-ipv4first.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily-ipv4first.js)
- [parallel/test-net-autoselectfamily.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-autoselectfamily.js)
- [parallel/test-net-better-error-messages-listen.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-better-error-messages-listen.js)
- [parallel/test-net-binary.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-binary.js)
- [parallel/test-net-bind-twice.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-net-bind-twice.js)

View File

@ -473,6 +473,7 @@ const pwdCommand = isWindows ?
module.exports = {
allowGlobals,
defaultAutoSelectFamilyAttemptTimeout: 2500,
expectsError,
expectWarning,
getArrayBufferViews,

View File

@ -0,0 +1,312 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 18.12.1
// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
'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.
// 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');
// TODO(kt3k): We use common.mustCallAtLeast instead of common.mustCall
// because Deno sends multiple requests to the DNS server.
// This can be addressed if Deno.resolveDns supports ANY record type.
// See https://github.com/denoland/deno/issues/14492
socket.on('message', common.mustCallAtLeast((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();
}));
});
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, [`::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 only the last successful connection is established.
{
createDnsServer(
['2606:4700::6810:85e5', '2606:4700::6810:84e5', "::1"],
// TODO(kt3k): Comment out ipv4 addresses to make the test pass faster.
// Enable this when Deno.connect() call becomes cancellable.
// See https://github.com/denoland/deno/issues/26819
// ['104.20.22.46', '104.20.23.46', '127.0.0.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: 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,
});
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'));
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) => {
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();
}));
}));
}));
}

View File

@ -10,6 +10,11 @@ import * as net from "node:net";
import { assert, assertEquals } from "@std/assert";
import { curlRequest } from "../unit/test_util.ts";
// Increase the timeout for the auto select family to avoid flakiness
net.setDefaultAutoSelectFamilyAttemptTimeout(
net.getDefaultAutoSelectFamilyAttemptTimeout() * 30,
);
for (const url of ["http://localhost:4246", "https://localhost:4247"]) {
Deno.test(`[node/http2 client] ${url}`, {
ignore: Deno.build.os === "windows",