mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 20:38:55 +00:00
fix(ext/node): add autoSelectFamily option to net.createConnection (#26661)
This commit is contained in:
parent
90236d67c5
commit
c3c2b37966
@ -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;
|
||||
|
@ -95,4 +95,5 @@ export function makeSyncWrite(fd: number) {
|
||||
};
|
||||
}
|
||||
|
||||
export const kReinitializeHandle = Symbol("kReinitializeHandle");
|
||||
export const normalizedArgsSymbol = Symbol("normalizedArgs");
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -473,6 +473,7 @@ const pwdCommand = isWindows ?
|
||||
|
||||
module.exports = {
|
||||
allowGlobals,
|
||||
defaultAutoSelectFamilyAttemptTimeout: 2500,
|
||||
expectsError,
|
||||
expectWarning,
|
||||
getArrayBufferViews,
|
||||
|
312
tests/node_compat/test/parallel/test-net-autoselectfamily.js
Normal file
312
tests/node_compat/test/parallel/test-net-autoselectfamily.js
Normal 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();
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
}
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user