Skip to content

Commit f867643

Browse files
authored
Merge pull request #2776 from gkampitakis/gkampitakis/patch_dns_lookup
fix: support authority overrides in the DNS resolver
2 parents da54e75 + 201595c commit f867643

File tree

6 files changed

+114
-24
lines changed

6 files changed

+114
-24
lines changed

doc/environment_variables.md

+5
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,8 @@ can be set.
6262
- INFO - log INFO and ERROR message
6363
- ERROR - log only errors (default)
6464
- NONE - won't log any
65+
66+
* GRPC_NODE_USE_ALTERNATIVE_RESOLVER
67+
Allows changing dns resolve behavior and parse DNS server authority as described in https://github.com/grpc/grpc/blob/master/doc/naming.md
68+
- true - use alternative resolver
69+
- false - use default resolver (default)

gulpfile.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const nativeTestOnly = gulp.parallel(healthCheck.test);
4141

4242
const nativeTest = gulp.series(build, nativeTestOnly);
4343

44-
const testOnly = gulp.parallel(jsCore.test, nativeTestOnly, protobuf.test, jsXds.test, reflection.test);
44+
const testOnly = gulp.series(jsCore.test, nativeTestOnly, protobuf.test, jsXds.test, reflection.test);
4545

4646
const test = gulp.series(build, testOnly, internalTest.test);
4747

packages/grpc-js/gulpfile.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,27 @@ const runTests = checkTask(() => {
8181
);
8282
});
8383

84-
const test = gulp.series(install, copyTestFixtures, runTests);
84+
const testWithAlternativeResolver = checkTask(() => {
85+
process.env.GRPC_NODE_USE_ALTERNATIVE_RESOLVER = 'true';
86+
return gulp.src(`${outDir}/test/test-resolver.js`).pipe(
87+
mocha({
88+
reporter: 'mocha-jenkins-reporter',
89+
require: ['ts-node/register'],
90+
})
91+
);
92+
});
93+
94+
const resetEnv = () => {
95+
process.env.GRPC_NODE_USE_ALTERNATIVE_RESOLVER = 'false';
96+
return Promise.resolve();
97+
};
98+
99+
const test = gulp.series(
100+
install,
101+
copyTestFixtures,
102+
runTests,
103+
testWithAlternativeResolver,
104+
resetEnv
105+
);
85106

86107
export { install, lint, clean, cleanAll, compile, test };

packages/grpc-js/src/environment.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2024 gRPC authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
export const GRPC_NODE_USE_ALTERNATIVE_RESOLVER =
19+
(process.env.GRPC_NODE_USE_ALTERNATIVE_RESOLVER ?? 'false') === 'true';

packages/grpc-js/src/resolver-dns.ts

+54-19
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ import {
2020
registerResolver,
2121
registerDefaultScheme,
2222
} from './resolver';
23-
import * as dns from 'dns';
24-
import * as util from 'util';
23+
import { promises as dns } from 'node:dns';
2524
import { extractAndSelectServiceConfig, ServiceConfig } from './service-config';
2625
import { Status } from './constants';
2726
import { StatusObject } from './call-interface';
@@ -33,6 +32,7 @@ import { GrpcUri, uriToString, splitHostPort } from './uri-parser';
3332
import { isIPv6, isIPv4 } from 'net';
3433
import { ChannelOptions } from './channel-options';
3534
import { BackoffOptions, BackoffTimeout } from './backoff-timeout';
35+
import { GRPC_NODE_USE_ALTERNATIVE_RESOLVER } from './environment';
3636

3737
const TRACER_NAME = 'dns_resolver';
3838

@@ -47,9 +47,6 @@ export const DEFAULT_PORT = 443;
4747

4848
const DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS = 30_000;
4949

50-
const resolveTxtPromise = util.promisify(dns.resolveTxt);
51-
const dnsLookupPromise = util.promisify(dns.lookup);
52-
5350
/**
5451
* Resolver implementation that handles DNS names and IP addresses.
5552
*/
@@ -63,7 +60,7 @@ class DnsResolver implements Resolver {
6360
* Failures are handled by the backoff timer.
6461
*/
6562
private readonly minTimeBetweenResolutionsMs: number;
66-
private pendingLookupPromise: Promise<dns.LookupAddress[]> | null = null;
63+
private pendingLookupPromise: Promise<TcpSubchannelAddress[]> | null = null;
6764
private pendingTxtPromise: Promise<string[][]> | null = null;
6865
private latestLookupResult: Endpoint[] | null = null;
6966
private latestServiceConfig: ServiceConfig | null = null;
@@ -76,12 +73,17 @@ class DnsResolver implements Resolver {
7673
private isNextResolutionTimerRunning = false;
7774
private isServiceConfigEnabled = true;
7875
private returnedIpResult = false;
76+
private alternativeResolver = new dns.Resolver();
77+
7978
constructor(
8079
private target: GrpcUri,
8180
private listener: ResolverListener,
8281
channelOptions: ChannelOptions
8382
) {
8483
trace('Resolver constructed for target ' + uriToString(target));
84+
if (target.authority) {
85+
this.alternativeResolver.setServers([target.authority]);
86+
}
8587
const hostPort = splitHostPort(target.path);
8688
if (hostPort === null) {
8789
this.ipResult = null;
@@ -185,11 +187,7 @@ class DnsResolver implements Resolver {
185187
* revert to an effectively blank one. */
186188
this.latestLookupResult = null;
187189
const hostname: string = this.dnsHostname;
188-
/* We lookup both address families here and then split them up later
189-
* because when looking up a single family, dns.lookup outputs an error
190-
* if the name exists but there are no records for that family, and that
191-
* error is indistinguishable from other kinds of errors */
192-
this.pendingLookupPromise = dnsLookupPromise(hostname, { all: true });
190+
this.pendingLookupPromise = this.lookup(hostname);
193191
this.pendingLookupPromise.then(
194192
addressList => {
195193
if (this.pendingLookupPromise === null) {
@@ -198,17 +196,12 @@ class DnsResolver implements Resolver {
198196
this.pendingLookupPromise = null;
199197
this.backoff.reset();
200198
this.backoff.stop();
201-
const subchannelAddresses: TcpSubchannelAddress[] = addressList.map(
202-
addr => ({ host: addr.address, port: +this.port! })
203-
);
204-
this.latestLookupResult = subchannelAddresses.map(address => ({
199+
this.latestLookupResult = addressList.map(address => ({
205200
addresses: [address],
206201
}));
207202
const allAddressesString: string =
208203
'[' +
209-
subchannelAddresses
210-
.map(addr => addr.host + ':' + addr.port)
211-
.join(',') +
204+
addressList.map(addr => addr.host + ':' + addr.port).join(',') +
212205
']';
213206
trace(
214207
'Resolved addresses for target ' +
@@ -253,7 +246,7 @@ class DnsResolver implements Resolver {
253246
/* We handle the TXT query promise differently than the others because
254247
* the name resolution attempt as a whole is a success even if the TXT
255248
* lookup fails */
256-
this.pendingTxtPromise = resolveTxtPromise(hostname);
249+
this.pendingTxtPromise = this.resolveTxt(hostname);
257250
this.pendingTxtPromise.then(
258251
txtRecord => {
259252
if (this.pendingTxtPromise === null) {
@@ -302,6 +295,48 @@ class DnsResolver implements Resolver {
302295
}
303296
}
304297

298+
private async lookup(hostname: string): Promise<TcpSubchannelAddress[]> {
299+
if (GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
300+
trace('Using alternative DNS resolver.');
301+
302+
const records = await Promise.allSettled([
303+
this.alternativeResolver.resolve4(hostname),
304+
this.alternativeResolver.resolve6(hostname),
305+
]);
306+
307+
if (records.every(result => result.status === 'rejected')) {
308+
throw new Error((records[0] as PromiseRejectedResult).reason);
309+
}
310+
311+
return records
312+
.reduce<string[]>((acc, result) => {
313+
return result.status === 'fulfilled'
314+
? [...acc, ...result.value]
315+
: acc;
316+
}, [])
317+
.map(addr => ({
318+
host: addr,
319+
port: +this.port!,
320+
}));
321+
}
322+
323+
/* We lookup both address families here and then split them up later
324+
* because when looking up a single family, dns.lookup outputs an error
325+
* if the name exists but there are no records for that family, and that
326+
* error is indistinguishable from other kinds of errors */
327+
const addressList = await dns.lookup(hostname, { all: true });
328+
return addressList.map(addr => ({ host: addr.address, port: +this.port! }));
329+
}
330+
331+
private async resolveTxt(hostname: string): Promise<string[][]> {
332+
if (GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
333+
trace('Using alternative DNS resolver.');
334+
return this.alternativeResolver.resolveTxt(hostname);
335+
}
336+
337+
return dns.resolveTxt(hostname);
338+
}
339+
305340
private startNextResolutionTimer() {
306341
clearTimeout(this.nextResolutionTimer);
307342
this.nextResolutionTimer = setTimeout(() => {

packages/grpc-js/test/test-resolver.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
subchannelAddressEqual,
3232
} from '../src/subchannel-address';
3333
import { parseUri, GrpcUri } from '../src/uri-parser';
34+
import { GRPC_NODE_USE_ALTERNATIVE_RESOLVER } from '../src/environment';
3435

3536
function hasMatchingAddress(
3637
endpointList: Endpoint[],
@@ -55,7 +56,10 @@ describe('Name Resolver', () => {
5556
describe('DNS Names', function () {
5657
// For some reason DNS queries sometimes take a long time on Windows
5758
this.timeout(4000);
58-
it('Should resolve localhost properly', done => {
59+
it('Should resolve localhost properly', function (done) {
60+
if (GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
61+
this.skip();
62+
}
5963
const target = resolverManager.mapUriDefaultScheme(
6064
parseUri('localhost:50051')!
6165
)!;
@@ -82,7 +86,10 @@ describe('Name Resolver', () => {
8286
const resolver = resolverManager.createResolver(target, listener, {});
8387
resolver.updateResolution();
8488
});
85-
it('Should default to port 443', done => {
89+
it('Should default to port 443', function (done) {
90+
if (GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
91+
this.skip();
92+
}
8693
const target = resolverManager.mapUriDefaultScheme(
8794
parseUri('localhost')!
8895
)!;
@@ -402,7 +409,10 @@ describe('Name Resolver', () => {
402409
const resolver2 = resolverManager.createResolver(target2, listener, {});
403410
resolver2.updateResolution();
404411
});
405-
it('should not keep repeating successful resolutions', done => {
412+
it('should not keep repeating successful resolutions', function (done) {
413+
if (GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
414+
this.skip();
415+
}
406416
const target = resolverManager.mapUriDefaultScheme(
407417
parseUri('localhost')!
408418
)!;

0 commit comments

Comments
 (0)