363 lines
16 KiB
JavaScript
363 lines
16 KiB
JavaScript
"use strict";
|
|
/*
|
|
* Copyright 2019 gRPC authors.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.DEFAULT_PORT = void 0;
|
|
exports.setup = setup;
|
|
const resolver_1 = require("./resolver");
|
|
const dns_1 = require("dns");
|
|
const service_config_1 = require("./service-config");
|
|
const constants_1 = require("./constants");
|
|
const call_interface_1 = require("./call-interface");
|
|
const metadata_1 = require("./metadata");
|
|
const logging = require("./logging");
|
|
const constants_2 = require("./constants");
|
|
const uri_parser_1 = require("./uri-parser");
|
|
const net_1 = require("net");
|
|
const backoff_timeout_1 = require("./backoff-timeout");
|
|
const environment_1 = require("./environment");
|
|
const TRACER_NAME = 'dns_resolver';
|
|
function trace(text) {
|
|
logging.trace(constants_2.LogVerbosity.DEBUG, TRACER_NAME, text);
|
|
}
|
|
/**
|
|
* The default TCP port to connect to if not explicitly specified in the target.
|
|
*/
|
|
exports.DEFAULT_PORT = 443;
|
|
const DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS = 30000;
|
|
/**
|
|
* Resolver implementation that handles DNS names and IP addresses.
|
|
*/
|
|
class DnsResolver {
|
|
constructor(target, listener, channelOptions) {
|
|
var _a, _b, _c;
|
|
this.target = target;
|
|
this.listener = listener;
|
|
this.pendingLookupPromise = null;
|
|
this.pendingTxtPromise = null;
|
|
this.latestLookupResult = null;
|
|
this.latestServiceConfigResult = null;
|
|
this.continueResolving = false;
|
|
this.isNextResolutionTimerRunning = false;
|
|
this.isServiceConfigEnabled = true;
|
|
this.returnedIpResult = false;
|
|
this.alternativeResolver = new dns_1.promises.Resolver();
|
|
trace('Resolver constructed for target ' + (0, uri_parser_1.uriToString)(target));
|
|
if (target.authority) {
|
|
this.alternativeResolver.setServers([target.authority]);
|
|
}
|
|
const hostPort = (0, uri_parser_1.splitHostPort)(target.path);
|
|
if (hostPort === null) {
|
|
this.ipResult = null;
|
|
this.dnsHostname = null;
|
|
this.port = null;
|
|
}
|
|
else {
|
|
if ((0, net_1.isIPv4)(hostPort.host) || (0, net_1.isIPv6)(hostPort.host)) {
|
|
this.ipResult = [
|
|
{
|
|
addresses: [
|
|
{
|
|
host: hostPort.host,
|
|
port: (_a = hostPort.port) !== null && _a !== void 0 ? _a : exports.DEFAULT_PORT,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
this.dnsHostname = null;
|
|
this.port = null;
|
|
}
|
|
else {
|
|
this.ipResult = null;
|
|
this.dnsHostname = hostPort.host;
|
|
this.port = (_b = hostPort.port) !== null && _b !== void 0 ? _b : exports.DEFAULT_PORT;
|
|
}
|
|
}
|
|
this.percentage = Math.random() * 100;
|
|
if (channelOptions['grpc.service_config_disable_resolution'] === 1) {
|
|
this.isServiceConfigEnabled = false;
|
|
}
|
|
this.defaultResolutionError = {
|
|
code: constants_1.Status.UNAVAILABLE,
|
|
details: `Name resolution failed for target ${(0, uri_parser_1.uriToString)(this.target)}`,
|
|
metadata: new metadata_1.Metadata(),
|
|
};
|
|
const backoffOptions = {
|
|
initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'],
|
|
maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'],
|
|
};
|
|
this.backoff = new backoff_timeout_1.BackoffTimeout(() => {
|
|
if (this.continueResolving) {
|
|
this.startResolutionWithBackoff();
|
|
}
|
|
}, backoffOptions);
|
|
this.backoff.unref();
|
|
this.minTimeBetweenResolutionsMs =
|
|
(_c = channelOptions['grpc.dns_min_time_between_resolutions_ms']) !== null && _c !== void 0 ? _c : DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS;
|
|
this.nextResolutionTimer = setTimeout(() => { }, 0);
|
|
clearTimeout(this.nextResolutionTimer);
|
|
}
|
|
/**
|
|
* If the target is an IP address, just provide that address as a result.
|
|
* Otherwise, initiate A, AAAA, and TXT lookups
|
|
*/
|
|
startResolution() {
|
|
if (this.ipResult !== null) {
|
|
if (!this.returnedIpResult) {
|
|
trace('Returning IP address for target ' + (0, uri_parser_1.uriToString)(this.target));
|
|
setImmediate(() => {
|
|
this.listener((0, call_interface_1.statusOrFromValue)(this.ipResult), {}, null, '');
|
|
});
|
|
this.returnedIpResult = true;
|
|
}
|
|
this.backoff.stop();
|
|
this.backoff.reset();
|
|
this.stopNextResolutionTimer();
|
|
return;
|
|
}
|
|
if (this.dnsHostname === null) {
|
|
trace('Failed to parse DNS address ' + (0, uri_parser_1.uriToString)(this.target));
|
|
setImmediate(() => {
|
|
this.listener((0, call_interface_1.statusOrFromError)({
|
|
code: constants_1.Status.UNAVAILABLE,
|
|
details: `Failed to parse DNS address ${(0, uri_parser_1.uriToString)(this.target)}`
|
|
}), {}, null, '');
|
|
});
|
|
this.stopNextResolutionTimer();
|
|
}
|
|
else {
|
|
if (this.pendingLookupPromise !== null) {
|
|
return;
|
|
}
|
|
trace('Looking up DNS hostname ' + this.dnsHostname);
|
|
/* We clear out latestLookupResult here to ensure that it contains the
|
|
* latest result since the last time we started resolving. That way, the
|
|
* TXT resolution handler can use it, but only if it finishes second. We
|
|
* don't clear out any previous service config results because it's
|
|
* better to use a service config that's slightly out of date than to
|
|
* revert to an effectively blank one. */
|
|
this.latestLookupResult = null;
|
|
const hostname = this.dnsHostname;
|
|
this.pendingLookupPromise = this.lookup(hostname);
|
|
this.pendingLookupPromise.then(addressList => {
|
|
if (this.pendingLookupPromise === null) {
|
|
return;
|
|
}
|
|
this.pendingLookupPromise = null;
|
|
this.latestLookupResult = (0, call_interface_1.statusOrFromValue)(addressList.map(address => ({
|
|
addresses: [address],
|
|
})));
|
|
const allAddressesString = '[' +
|
|
addressList.map(addr => addr.host + ':' + addr.port).join(',') +
|
|
']';
|
|
trace('Resolved addresses for target ' +
|
|
(0, uri_parser_1.uriToString)(this.target) +
|
|
': ' +
|
|
allAddressesString);
|
|
/* If the TXT lookup has not yet finished, both of the last two
|
|
* arguments will be null, which is the equivalent of getting an
|
|
* empty TXT response. When the TXT lookup does finish, its handler
|
|
* can update the service config by using the same address list */
|
|
const healthStatus = this.listener(this.latestLookupResult, {}, this.latestServiceConfigResult, '');
|
|
this.handleHealthStatus(healthStatus);
|
|
}, err => {
|
|
if (this.pendingLookupPromise === null) {
|
|
return;
|
|
}
|
|
trace('Resolution error for target ' +
|
|
(0, uri_parser_1.uriToString)(this.target) +
|
|
': ' +
|
|
err.message);
|
|
this.pendingLookupPromise = null;
|
|
this.stopNextResolutionTimer();
|
|
this.listener((0, call_interface_1.statusOrFromError)(this.defaultResolutionError), {}, this.latestServiceConfigResult, '');
|
|
});
|
|
/* If there already is a still-pending TXT resolution, we can just use
|
|
* that result when it comes in */
|
|
if (this.isServiceConfigEnabled && this.pendingTxtPromise === null) {
|
|
/* We handle the TXT query promise differently than the others because
|
|
* the name resolution attempt as a whole is a success even if the TXT
|
|
* lookup fails */
|
|
this.pendingTxtPromise = this.resolveTxt(hostname);
|
|
this.pendingTxtPromise.then(txtRecord => {
|
|
if (this.pendingTxtPromise === null) {
|
|
return;
|
|
}
|
|
this.pendingTxtPromise = null;
|
|
let serviceConfig;
|
|
try {
|
|
serviceConfig = (0, service_config_1.extractAndSelectServiceConfig)(txtRecord, this.percentage);
|
|
if (serviceConfig) {
|
|
this.latestServiceConfigResult = (0, call_interface_1.statusOrFromValue)(serviceConfig);
|
|
}
|
|
else {
|
|
this.latestServiceConfigResult = null;
|
|
}
|
|
}
|
|
catch (err) {
|
|
this.latestServiceConfigResult = (0, call_interface_1.statusOrFromError)({
|
|
code: constants_1.Status.UNAVAILABLE,
|
|
details: `Parsing service config failed with error ${err.message}`
|
|
});
|
|
}
|
|
if (this.latestLookupResult !== null) {
|
|
/* We rely here on the assumption that calling this function with
|
|
* identical parameters will be essentialy idempotent, and calling
|
|
* it with the same address list and a different service config
|
|
* should result in a fast and seamless switchover. */
|
|
this.listener(this.latestLookupResult, {}, this.latestServiceConfigResult, '');
|
|
}
|
|
}, err => {
|
|
/* If TXT lookup fails we should do nothing, which means that we
|
|
* continue to use the result of the most recent successful lookup,
|
|
* or the default null config object if there has never been a
|
|
* successful lookup. We do not set the latestServiceConfigError
|
|
* here because that is specifically used for response validation
|
|
* errors. We still need to handle this error so that it does not
|
|
* bubble up as an unhandled promise rejection. */
|
|
});
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* The ResolverListener returns a boolean indicating whether the LB policy
|
|
* accepted the resolution result. A false result on an otherwise successful
|
|
* resolution should be treated as a resolution failure.
|
|
* @param healthStatus
|
|
*/
|
|
handleHealthStatus(healthStatus) {
|
|
if (healthStatus) {
|
|
this.backoff.stop();
|
|
this.backoff.reset();
|
|
}
|
|
else {
|
|
this.continueResolving = true;
|
|
}
|
|
}
|
|
async lookup(hostname) {
|
|
if (environment_1.GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
|
|
trace('Using alternative DNS resolver.');
|
|
const records = await Promise.allSettled([
|
|
this.alternativeResolver.resolve4(hostname),
|
|
this.alternativeResolver.resolve6(hostname),
|
|
]);
|
|
if (records.every(result => result.status === 'rejected')) {
|
|
throw new Error(records[0].reason);
|
|
}
|
|
return records
|
|
.reduce((acc, result) => {
|
|
return result.status === 'fulfilled'
|
|
? [...acc, ...result.value]
|
|
: acc;
|
|
}, [])
|
|
.map(addr => ({
|
|
host: addr,
|
|
port: +this.port,
|
|
}));
|
|
}
|
|
/* We lookup both address families here and then split them up later
|
|
* because when looking up a single family, dns.lookup outputs an error
|
|
* if the name exists but there are no records for that family, and that
|
|
* error is indistinguishable from other kinds of errors */
|
|
const addressList = await dns_1.promises.lookup(hostname, { all: true });
|
|
return addressList.map(addr => ({ host: addr.address, port: +this.port }));
|
|
}
|
|
async resolveTxt(hostname) {
|
|
if (environment_1.GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
|
|
trace('Using alternative DNS resolver.');
|
|
return this.alternativeResolver.resolveTxt(hostname);
|
|
}
|
|
return dns_1.promises.resolveTxt(hostname);
|
|
}
|
|
startNextResolutionTimer() {
|
|
var _a, _b;
|
|
clearTimeout(this.nextResolutionTimer);
|
|
this.nextResolutionTimer = setTimeout(() => {
|
|
this.stopNextResolutionTimer();
|
|
if (this.continueResolving) {
|
|
this.startResolutionWithBackoff();
|
|
}
|
|
}, this.minTimeBetweenResolutionsMs);
|
|
(_b = (_a = this.nextResolutionTimer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
this.isNextResolutionTimerRunning = true;
|
|
}
|
|
stopNextResolutionTimer() {
|
|
clearTimeout(this.nextResolutionTimer);
|
|
this.isNextResolutionTimerRunning = false;
|
|
}
|
|
startResolutionWithBackoff() {
|
|
if (this.pendingLookupPromise === null) {
|
|
this.continueResolving = false;
|
|
this.backoff.runOnce();
|
|
this.startNextResolutionTimer();
|
|
this.startResolution();
|
|
}
|
|
}
|
|
updateResolution() {
|
|
/* If there is a pending lookup, just let it finish. Otherwise, if the
|
|
* nextResolutionTimer or backoff timer is running, set the
|
|
* continueResolving flag to resolve when whichever of those timers
|
|
* fires. Otherwise, start resolving immediately. */
|
|
if (this.pendingLookupPromise === null) {
|
|
if (this.isNextResolutionTimerRunning || this.backoff.isRunning()) {
|
|
if (this.isNextResolutionTimerRunning) {
|
|
trace('resolution update delayed by "min time between resolutions" rate limit');
|
|
}
|
|
else {
|
|
trace('resolution update delayed by backoff timer until ' +
|
|
this.backoff.getEndTime().toISOString());
|
|
}
|
|
this.continueResolving = true;
|
|
}
|
|
else {
|
|
this.startResolutionWithBackoff();
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Reset the resolver to the same state it had when it was created. In-flight
|
|
* DNS requests cannot be cancelled, but they are discarded and their results
|
|
* will be ignored.
|
|
*/
|
|
destroy() {
|
|
this.continueResolving = false;
|
|
this.backoff.reset();
|
|
this.backoff.stop();
|
|
this.stopNextResolutionTimer();
|
|
this.pendingLookupPromise = null;
|
|
this.pendingTxtPromise = null;
|
|
this.latestLookupResult = null;
|
|
this.latestServiceConfigResult = null;
|
|
this.returnedIpResult = false;
|
|
}
|
|
/**
|
|
* Get the default authority for the given target. For IP targets, that is
|
|
* the IP address. For DNS targets, it is the hostname.
|
|
* @param target
|
|
*/
|
|
static getDefaultAuthority(target) {
|
|
return target.path;
|
|
}
|
|
}
|
|
/**
|
|
* Set up the DNS resolver class by registering it as the handler for the
|
|
* "dns:" prefix and as the default resolver.
|
|
*/
|
|
function setup() {
|
|
(0, resolver_1.registerResolver)('dns', DnsResolver);
|
|
(0, resolver_1.registerDefaultScheme)('dns');
|
|
}
|
|
//# sourceMappingURL=resolver-dns.js.map
|