Files
node/lib/internal/dns/utils.js
T
Shelley Vohr e38eede0e5 lib: load fewer builtins when bootstrapping without a snapshot
Contexts that are not deserialized from the built-in snapshot -- worker
threads, and the main context of embedders that create their own
isolate or of `node --no-node-snapshot` -- compile (with the code cache
at best) every builtin the bootstrap touches, so each eagerly required
builtin is startup time (~0.15-0.4 ms apiece). A number of them are only
required eagerly so that they end up in the snapshot, or for features
the bootstrap path never uses.

Load lazily what those paths do not need:

- is_main_thread.js: preload util, url, the ESM loader (translators,
  resolver, module_job/map, source maps, node:module, vm modules, mime,
  data_url, the TypeScript stripper), internal/blob and
  internal/dns/utils only while building a snapshot; they load on first
  use otherwise.
- fs: internal/blob (+ internal/encoding and its tables) is only used
  by fs.openAsBlob().
- internal/url: internal/data_url (+ internal/mime) is only used by the
  Buffer-returning file URL helpers.
- internal/process/execution, the CommonJS loader, esm/translators and
  esm/load: the TypeScript stripper and data: URL helpers are only
  needed for TypeScript sources / data: URLs.
- pre_execution: internal/dns/utils (+ internal/net) is only needed up
  front to validate an explicit --dns-result-order or to register the
  resolver's snapshot serializer; the default order becomes the
  variable's initializer.
- internal/worker: event_loop_utilization and error_serdes are only
  needed once a sub-worker's ELU is read or it reports an error.
- worker_threads: `locks` is defined lazily, like util's lazy exports.

Main-thread startup with the snapshot is unchanged (the same modules
are preloaded into it; the bootstrap-modules test lists are adjusted).
A bare worker compiles 95 -> 83 builtins (cold start -5%); without the
snapshot an empty CommonJS entry point compiles 76 -> 59 builtins and an
empty ES module entry point 76 -> 69.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65329
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
2026-08-22 17:07:39 +00:00

364 lines
9.7 KiB
JavaScript

'use strict';
const {
ArrayPrototypeForEach,
ArrayPrototypeMap,
ArrayPrototypePush,
FunctionPrototypeBind,
NumberParseInt,
RegExpPrototypeExec,
RegExpPrototypeSymbolReplace,
Symbol,
} = primordials;
const {
codes: {
ERR_DNS_SET_SERVERS_FAILED,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_IP_ADDRESS,
},
} = require('internal/errors');
const { isIP } = require('internal/net');
const { getOptionValue } = require('internal/options');
const {
validateArray,
validateInt32,
validateOneOf,
validatePort,
validateString,
validateUint32,
} = require('internal/validators');
let binding;
function lazyBinding() {
binding ??= internalBinding('cares_wrap');
return binding;
}
const IANA_DNS_PORT = 53;
const IPv6RE = /^\[([^[\]]*)\]/;
const addrSplitRE = /(^.+?)(?::(\d+))?$/;
const {
namespace: {
addSerializeCallback,
addDeserializeCallback,
isBuildingSnapshot,
},
} = require('internal/v8/startup_snapshot');
function validateTimeout(options) {
let { timeout = -1 } = { ...options };
validateInt32(timeout, 'options.timeout', -1);
// Coerce -0 to +0.
timeout += 0;
return timeout;
}
function validateMaxTimeout(options) {
let { maxTimeout = 0 } = { ...options };
validateUint32(maxTimeout, 'options.maxTimeout');
// Coerce -0 to +0.
maxTimeout += 0;
return maxTimeout;
}
function validateTries(options) {
const { tries = 4 } = { ...options };
validateInt32(tries, 'options.tries', 1);
return tries;
}
const kSerializeResolver = Symbol('dns:resolver:serialize');
const kDeserializeResolver = Symbol('dns:resolver:deserialize');
const kSnapshotStates = Symbol('dns:resolver:config');
const kInitializeHandle = Symbol('dns:resolver:initializeHandle');
const kSetServersInternal = Symbol('dns:resolver:setServers');
// Resolver instances correspond 1:1 to c-ares channels.
class ResolverBase {
constructor(options = undefined) {
const timeout = validateTimeout(options);
const tries = validateTries(options);
const maxTimeout = validateMaxTimeout(options);
// If we are building snapshot, save the states of the resolver along
// the way.
if (isBuildingSnapshot()) {
this[kSnapshotStates] = { timeout, tries, maxTimeout };
}
this[kInitializeHandle](timeout, tries, maxTimeout);
}
[kInitializeHandle](timeout, tries, maxTimeout) {
const { ChannelWrap } = lazyBinding();
this._handle = new ChannelWrap(timeout, tries, maxTimeout);
}
cancel() {
this._handle.cancel();
}
getServers() {
return ArrayPrototypeMap(this._handle.getServers() || [], (val) => {
if (!val[1] || val[1] === IANA_DNS_PORT)
return val[0];
const host = isIP(val[0]) === 6 ? `[${val[0]}]` : val[0];
return `${host}:${val[1]}`;
});
}
setServers(servers) {
validateArray(servers, 'servers');
// Cache the original servers because in the event of an error while
// setting the servers, c-ares won't have any servers available for
// resolution.
const newSet = [];
ArrayPrototypeForEach(servers, (serv, index) => {
validateString(serv, `servers[${index}]`);
let ipVersion = isIP(serv);
if (ipVersion !== 0)
return ArrayPrototypePush(newSet, [ipVersion, serv, IANA_DNS_PORT]);
const match = RegExpPrototypeExec(IPv6RE, serv);
// Check for an IPv6 in brackets.
if (match) {
ipVersion = isIP(match[1]);
if (ipVersion !== 0) {
const port = NumberParseInt(
RegExpPrototypeSymbolReplace(addrSplitRE, serv, '$2')) || IANA_DNS_PORT;
validatePort(port);
return ArrayPrototypePush(newSet, [ipVersion, match[1], port]);
}
}
// addr::port
const addrSplitMatch = RegExpPrototypeExec(addrSplitRE, serv);
if (addrSplitMatch) {
const hostIP = addrSplitMatch[1];
const port = NumberParseInt(addrSplitMatch[2]) || IANA_DNS_PORT;
ipVersion = isIP(hostIP);
if (ipVersion !== 0) {
validatePort(port);
return ArrayPrototypePush(newSet, [ipVersion, hostIP, port]);
}
}
throw new ERR_INVALID_IP_ADDRESS(serv);
});
this[kSetServersInternal](newSet, servers);
}
[kSetServersInternal](newSet, servers) {
const orig = ArrayPrototypeMap(this._handle.getServers() || [], (val) => {
val.unshift(isIP(val[0]));
return val;
});
const errorNumber = this._handle.setServers(newSet);
if (errorNumber !== 0) {
// Reset the servers to the old servers, because ares probably unset them.
this._handle.setServers(orig);
const { strerror } = lazyBinding();
const err = strerror(errorNumber);
throw new ERR_DNS_SET_SERVERS_FAILED(err, servers);
}
if (isBuildingSnapshot()) {
this[kSnapshotStates].servers = newSet;
}
}
setLocalAddress(ipv4, ipv6) {
validateString(ipv4, 'ipv4');
if (ipv6 !== undefined) {
validateString(ipv6, 'ipv6');
}
this._handle.setLocalAddress(ipv4, ipv6);
if (isBuildingSnapshot()) {
this[kSnapshotStates].localAddress = { ipv4, ipv6 };
}
}
// TODO(joyeecheung): consider exposing this if custom DNS resolvers
// end up being useful for snapshot users.
[kSerializeResolver]() {
this._handle = null; // We'll restore it during deserialization.
addDeserializeCallback(function deserializeResolver(resolver) {
resolver[kDeserializeResolver]();
}, this);
}
[kDeserializeResolver]() {
const { timeout, tries, maxTimeout, localAddress, servers } = this[kSnapshotStates];
this[kInitializeHandle](timeout, tries, maxTimeout);
if (localAddress) {
const { ipv4, ipv6 } = localAddress;
this._handle.setLocalAddress(ipv4, ipv6);
}
if (servers) {
this[kSetServersInternal](servers, servers);
}
}
}
let defaultResolver;
// May already hold a value chosen by the snapshotted application; a
// --dns-result-order flag given at runtime overrides it in initializeDns().
let dnsOrder = 'verbatim';
const validDnsOrders = ['verbatim', 'ipv4first', 'ipv6first'];
const validFamilies = [0, 4, 6];
function initializeDns() {
const orderFromCLI = getOptionValue('--dns-result-order');
if (orderFromCLI) {
// Allow the deserialized application to override order from CLI.
validateOneOf(orderFromCLI, '--dns-result-order', validDnsOrders);
dnsOrder = orderFromCLI;
}
if (!isBuildingSnapshot()) {
return;
}
addSerializeCallback(() => {
defaultResolver?.[kSerializeResolver]();
});
}
const resolverKeys = [
'getServers',
'resolve',
'resolve4',
'resolve6',
'resolveAny',
'resolveCaa',
'resolveCname',
'resolveMx',
'resolveNaptr',
'resolveNs',
'resolvePtr',
'resolveSoa',
'resolveSrv',
'resolveTlsa',
'resolveTxt',
'reverse',
];
function getDefaultResolver() {
// We do this here instead of pre-execution so that the default resolver is
// only ever created when the user loads any dns module.
if (defaultResolver === undefined) {
defaultResolver = new ResolverBase();
}
return defaultResolver;
}
function setDefaultResolver(resolver) {
defaultResolver = resolver;
}
function bindDefaultResolver(target, source) {
const defaultResolver = getDefaultResolver();
ArrayPrototypeForEach(resolverKeys, (key) => {
target[key] = FunctionPrototypeBind(source[key], defaultResolver);
});
}
function validateHints(hints) {
const { AI_ADDRCONFIG, AI_ALL, AI_V4MAPPED } = lazyBinding();
if ((hints & ~(AI_ADDRCONFIG | AI_ALL | AI_V4MAPPED)) !== 0) {
throw new ERR_INVALID_ARG_VALUE('hints', hints);
}
}
function setDefaultResultOrder(value) {
validateOneOf(value, 'dnsOrder', validDnsOrders);
dnsOrder = value;
}
function getDefaultResultOrder() {
return dnsOrder;
}
function createResolverClass(resolver) {
const resolveMap = { __proto__: null };
class Resolver extends ResolverBase {}
Resolver.prototype.resolveAny = resolveMap.ANY = resolver('queryAny');
Resolver.prototype.resolve4 = resolveMap.A = resolver('queryA');
Resolver.prototype.resolve6 = resolveMap.AAAA = resolver('queryAaaa');
Resolver.prototype.resolveCaa = resolveMap.CAA = resolver('queryCaa');
Resolver.prototype.resolveCname = resolveMap.CNAME = resolver('queryCname');
Resolver.prototype.resolveMx = resolveMap.MX = resolver('queryMx');
Resolver.prototype.resolveNs = resolveMap.NS = resolver('queryNs');
Resolver.prototype.resolveTlsa = resolveMap.TLSA = resolver('queryTlsa');
Resolver.prototype.resolveTxt = resolveMap.TXT = resolver('queryTxt');
Resolver.prototype.resolveSrv = resolveMap.SRV = resolver('querySrv');
Resolver.prototype.resolvePtr = resolveMap.PTR = resolver('queryPtr');
Resolver.prototype.resolveNaptr = resolveMap.NAPTR = resolver('queryNaptr');
Resolver.prototype.resolveSoa = resolveMap.SOA = resolver('querySoa');
Resolver.prototype.reverse = resolver('getHostByAddr');
return {
resolveMap,
Resolver,
};
}
// ERROR CODES
const errorCodes = {
NODATA: 'ENODATA',
FORMERR: 'EFORMERR',
SERVFAIL: 'ESERVFAIL',
NOTFOUND: 'ENOTFOUND',
NOTIMP: 'ENOTIMP',
REFUSED: 'EREFUSED',
BADQUERY: 'EBADQUERY',
BADNAME: 'EBADNAME',
BADFAMILY: 'EBADFAMILY',
BADRESP: 'EBADRESP',
CONNREFUSED: 'ECONNREFUSED',
TIMEOUT: 'ETIMEOUT',
EOF: 'EOF',
FILE: 'EFILE',
NOMEM: 'ENOMEM',
DESTRUCTION: 'EDESTRUCTION',
BADSTR: 'EBADSTR',
BADFLAGS: 'EBADFLAGS',
NONAME: 'ENONAME',
BADHINTS: 'EBADHINTS',
NOTINITIALIZED: 'ENOTINITIALIZED',
LOADIPHLPAPI: 'ELOADIPHLPAPI',
ADDRGETNETWORKPARAMS: 'EADDRGETNETWORKPARAMS',
CANCELLED: 'ECANCELLED',
};
module.exports = {
bindDefaultResolver,
getDefaultResolver,
setDefaultResolver,
validateHints,
validateTimeout,
validateTries,
getDefaultResultOrder,
setDefaultResultOrder,
errorCodes,
createResolverClass,
initializeDns,
validDnsOrders,
validFamilies,
};