Skip to content

Commit 5eded95

Browse files
authored
Merge pull request #2909 from murgatroid99/grpc-js-xds_security_tests
grpc-js-xds: Implement and enable security interop tests
2 parents 0c093b0 + 6094ebe commit 5eded95

20 files changed

+658
-164
lines changed

packages/grpc-js-xds/interop/test-client.Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/
4242
COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/
4343

4444
ENV GRPC_VERBOSITY="DEBUG"
45-
ENV GRPC_TRACE=xds_client,xds_resolver,xds_cluster_manager,cds_balancer,xds_cluster_resolver,xds_cluster_impl,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds,outlier_detection,server,server_call,ring_hash
45+
ENV GRPC_TRACE=xds_client,xds_resolver,xds_cluster_manager,cds_balancer,xds_cluster_resolver,xds_cluster_impl,priority,weighted_target,round_robin,resolving_load_balancer,subchannel,keepalive,dns_resolver,fault_injection,http_filter,csds,outlier_detection,server,server_call,ring_hash,transport,certificate_provider,xds_channel_credentials
4646
ENV NODE_XDS_INTEROP_VERBOSITY=1
4747

4848
ENTRYPOINT [ "/nodejs/bin/node", "/node/src/grpc-node/packages/grpc-js-xds/build/interop/xds-interop-client" ]

packages/grpc-js-xds/interop/test-server.Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/
4646
COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/
4747

4848
ENV GRPC_VERBOSITY="DEBUG"
49-
ENV GRPC_TRACE=xds_client,server,xds_server
49+
ENV GRPC_TRACE=xds_client,server,xds_server,http_filter,certificate_provider
5050

5151
# tini serves as PID 1 and enables the server to properly respond to signals.
5252
COPY --from=build /tini /tini

packages/grpc-js-xds/interop/xds-interop-client.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,9 @@ function main() {
519519
* channels do not share any subchannels. It does not have any
520520
* inherent function. */
521521
console.log(`Interop client channel ${i} starting sending ${argv.qps} QPS to ${argv.server}`);
522-
sendConstantQps(new loadedProto.grpc.testing.TestService(argv.server, grpc.credentials.createInsecure(), {'unique': i}),
522+
const insecureCreds = grpc.credentials.createInsecure();
523+
const creds = new grpc_xds.XdsChannelCredentials(insecureCreds);
524+
sendConstantQps(new loadedProto.grpc.testing.TestService(argv.server, creds, {'unique': i}),
523525
argv.qps,
524526
argv.fail_on_failed_rpcs === 'true',
525527
callStatsTracker);

packages/grpc-js-xds/interop/xds-interop-server.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import { SimpleRequest__Output } from './generated/grpc/testing/SimpleRequest';
3030
import { SimpleResponse } from './generated/grpc/testing/SimpleResponse';
3131
import { ReflectionService } from '@grpc/reflection';
3232

33+
grpc_xds.register();
34+
3335
const packageDefinition = protoLoader.loadSync('grpc/testing/test.proto', {
3436
keepCase: true,
3537
defaults: true,
@@ -158,6 +160,10 @@ function adminServiceInterceptor(methodDescriptor: grpc.ServerMethodDefinition<a
158160
const responder: grpc.Responder = {
159161
start: next => {
160162
next(listener);
163+
},
164+
sendMessage: (message, next) => {
165+
console.log(`Responded to request to method ${methodDescriptor.path}: ${JSON.stringify(message)}`);
166+
next(message);
161167
}
162168
};
163169
return new grpc.ServerInterceptingCall(call, responder);
@@ -228,11 +234,10 @@ function getIPv6Addresses(): string[] {
228234

229235
async function main() {
230236
const argv = yargs
231-
.string(['port', 'maintenance_port', 'address_type'])
232-
.boolean(['secure_mode'])
237+
.string(['port', 'maintenance_port', 'address_type', 'secure_mode'])
233238
.demandOption(['port'])
234239
.default('address_type', 'IPV4_IPV6')
235-
.default('secure_mode', false)
240+
.default('secure_mode', 'false')
236241
.parse()
237242
console.log('Starting xDS interop server. Args: ', argv);
238243
const healthImpl = new HealthImplementation({'': 'NOT_SERVING'});
@@ -250,7 +255,8 @@ async function main() {
250255
services: ['grpc.testing.TestService']
251256
})
252257
const addressType = argv.address_type.toUpperCase();
253-
if (argv.secure_mode) {
258+
const secureMode = argv.secure_mode.toLowerCase() == 'true';
259+
if (secureMode) {
254260
if (addressType !== 'IPV4_IPV6') {
255261
throw new Error('Secure mode only supports IPV4_IPV6 address type');
256262
}
@@ -265,7 +271,7 @@ async function main() {
265271
const xdsCreds = new grpc_xds.XdsServerCredentials(grpc.ServerCredentials.createInsecure());
266272
await Promise.all([
267273
serverBindPromise(maintenanceServer, `[::]:${argv.maintenance_port}`, grpc.ServerCredentials.createInsecure()),
268-
serverBindPromise(server, `[::]:${argv.port}`, xdsCreds)
274+
serverBindPromise(server, `0.0.0.0:${argv.port}`, xdsCreds)
269275
]);
270276
} else {
271277
const server = new grpc.Server({interceptors: [unifiedInterceptor]});

packages/grpc-js-xds/src/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,16 @@ import * as pick_first_lb from './lb-policy-registry/pick-first';
3333
export { XdsServer } from './server';
3434
export { XdsChannelCredentials, XdsServerCredentials } from './xds-credentials';
3535

36+
let registered = false;
37+
3638
/**
3739
* Register the "xds:" name scheme with the @grpc/grpc-js library.
3840
*/
3941
export function register() {
42+
if (registered) {
43+
return;
44+
}
45+
registered = true;
4046
resolver_xds.setup();
4147
load_balancer_cds.setup();
4248
xds_cluster_impl.setup();

packages/grpc-js-xds/src/load-balancer-cds.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,13 @@ class DnsExactValueMatcher implements ValueMatcher {
8484
}
8585
}
8686
apply(entry: string): boolean {
87-
let [type, value] = entry.split(':');
88-
if (!isSupportedSanType(type)) {
87+
const colonIndex = entry.indexOf(':');
88+
if (colonIndex < 0) {
8989
return false;
9090
}
91-
if (!value) {
91+
const type = entry.substring(0, colonIndex);
92+
let value = entry.substring(colonIndex + 1);
93+
if (!isSupportedSanType(type)) {
9294
return false;
9395
}
9496
if (this.ignoreCase) {
@@ -137,14 +139,16 @@ class SanEntryMatcher implements ValueMatcher {
137139
}
138140
}
139141
apply(entry: string): boolean {
140-
let [type, value] = entry.split(':');
141-
if (!isSupportedSanType(type)) {
142+
const colonIndex = entry.indexOf(':');
143+
if (colonIndex < 0) {
142144
return false;
143145
}
144-
value = canonicalizeSanEntryValue(type, value);
145-
if (!entry) {
146+
const type = entry.substring(0, colonIndex);
147+
let value = entry.substring(colonIndex + 1);
148+
if (!isSupportedSanType(type)) {
146149
return false;
147150
}
151+
value = canonicalizeSanEntryValue(type, value);
148152
return this.childMatcher.apply(value);
149153
}
150154
toString(): string {
@@ -433,6 +437,7 @@ export class CdsLoadBalancer implements LoadBalancer {
433437
if (this.latestSanMatcher === null || !this.latestSanMatcher.equals(sanMatcher)) {
434438
this.latestSanMatcher = sanMatcher;
435439
}
440+
trace('Configured subject alternative name matcher: ' + sanMatcher);
436441
childOptions[SAN_MATCHER_KEY] = this.latestSanMatcher;
437442
}
438443
this.childBalancer.updateAddressList(childEndpointList, typedChildConfig, childOptions);

packages/grpc-js-xds/src/server.ts

+34-13
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ interface ConfigParameters {
8383
createConnectionInjector: (credentials: ServerCredentials) => ConnectionInjector;
8484
drainGraceTimeMs: number;
8585
listenerResourceNameTemplate: string;
86+
unregisterChannelzRef: () => void;
8687
}
8788

8889
class FilterChainEntry {
@@ -159,22 +160,25 @@ class FilterChainEntry {
159160
}
160161
if (credentials instanceof XdsServerCredentials) {
161162
if (filterChain.transport_socket) {
163+
trace('Using secure credentials');
162164
const downstreamTlsContext = decodeSingleResource(DOWNSTREAM_TLS_CONTEXT_TYPE_URL, filterChain.transport_socket.typed_config!.value);
163165
const commonTlsContext = downstreamTlsContext.common_tls_context!;
164166
const instanceCertificateProvider = configParameters.xdsClient.getCertificateProvider(commonTlsContext.tls_certificate_provider_instance!.instance_name);
165167
if (!instanceCertificateProvider) {
166168
throw new Error(`Invalid TLS context detected: unrecognized certificate instance name: ${commonTlsContext.tls_certificate_provider_instance!.instance_name}`);
167169
}
168-
let validationContext: CertificateValidationContext__Output | null;
169-
switch (commonTlsContext?.validation_context_type) {
170-
case 'validation_context':
171-
validationContext = commonTlsContext.validation_context!;
172-
break;
173-
case 'combined_validation_context':
174-
validationContext = commonTlsContext.combined_validation_context!.default_validation_context;
175-
break;
176-
default:
177-
throw new Error(`Invalid TLS context detected: invalid validation_context_type: ${commonTlsContext.validation_context_type}`);
170+
let validationContext: CertificateValidationContext__Output | null = null;
171+
if (commonTlsContext?.validation_context_type) {
172+
switch (commonTlsContext?.validation_context_type) {
173+
case 'validation_context':
174+
validationContext = commonTlsContext.validation_context!;
175+
break;
176+
case 'combined_validation_context':
177+
validationContext = commonTlsContext.combined_validation_context!.default_validation_context;
178+
break;
179+
default:
180+
throw new Error(`Invalid TLS context detected: invalid validation_context_type: ${commonTlsContext.validation_context_type}`);
181+
}
178182
}
179183
let caCertificateProvider: experimental.CertificateProvider | null = null;
180184
if (validationContext?.ca_certificate_provider_instance) {
@@ -185,6 +189,7 @@ class FilterChainEntry {
185189
}
186190
credentials = experimental.createCertificateProviderServerCredentials(instanceCertificateProvider, caCertificateProvider, downstreamTlsContext.require_client_certificate?.value ?? false);
187191
} else {
192+
trace('Using fallback credentials');
188193
credentials = credentials.getFallbackCredentials();
189194
}
190195
}
@@ -287,6 +292,7 @@ class ListenerConfig {
287292
handleConnection(socket: net.Socket) {
288293
const matchingFilter = selectMostSpecificallyMatchingFilter(this.filterChainEntries, socket) ?? this.defaultFilterChain;
289294
if (!matchingFilter) {
295+
trace('Rejecting connection from ' + socket.remoteAddress + ': No filter matched');
290296
socket.destroy();
291297
return;
292298
}
@@ -449,12 +455,25 @@ class BoundPortEntry {
449455
this.tcpServer.close();
450456
const resourceName = formatTemplateString(this.configParameters.listenerResourceNameTemplate, this.boundAddress);
451457
ListenerResourceType.cancelWatch(this.configParameters.xdsClient, resourceName, this.listenerWatcher);
458+
this.configParameters.unregisterChannelzRef();
452459
}
453460
}
454461

455462
function normalizeFilterChainMatch(filterChainMatch: FilterChainMatch__Output | null): NormalizedFilterChainMatch[] {
456463
if (!filterChainMatch) {
457-
return [];
464+
filterChainMatch = {
465+
address_suffix: '',
466+
application_protocols: [],
467+
destination_port: null,
468+
direct_source_prefix_ranges: [],
469+
prefix_ranges: [],
470+
server_names: [],
471+
source_ports: [],
472+
source_prefix_ranges: [],
473+
source_type: 'ANY',
474+
suffix_len: null,
475+
transport_protocol: 'raw_buffer'
476+
};
458477
}
459478
if (filterChainMatch.destination_port) {
460479
return [];
@@ -613,11 +632,13 @@ export class XdsServer extends Server {
613632
if (!hostPort || !isValidIpPort(hostPort)) {
614633
throw new Error(`Listening port string must have the format IP:port with non-zero port, got ${port}`);
615634
}
635+
const channelzRef = this.experimentalRegisterListenerToChannelz({host: hostPort.host, port: hostPort.port!});
616636
const configParameters: ConfigParameters = {
617-
createConnectionInjector: (credentials) => this.createConnectionInjector(credentials),
637+
createConnectionInjector: (credentials) => this.experimentalCreateConnectionInjectorWithChannelzRef(credentials, channelzRef),
618638
drainGraceTimeMs: this.drainGraceTimeMs,
619639
listenerResourceNameTemplate: this.listenerResourceNameTemplate,
620-
xdsClient: this.xdsClient
640+
xdsClient: this.xdsClient,
641+
unregisterChannelzRef: () => this.experimentalUnregisterListenerFromChannelz(channelzRef)
621642
};
622643
const portEntry = new BoundPortEntry(configParameters, port, creds);
623644
const servingStatusListener: ServingStatusListener = statusObject => {

packages/grpc-js-xds/src/xds-credentials.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,18 @@
1515
*
1616
*/
1717

18-
import { CallCredentials, ChannelCredentials, ChannelOptions, ServerCredentials, VerifyOptions, experimental } from "@grpc/grpc-js";
18+
import { CallCredentials, ChannelCredentials, ChannelOptions, ServerCredentials, VerifyOptions, experimental, logVerbosity } from "@grpc/grpc-js";
1919
import { CA_CERT_PROVIDER_KEY, IDENTITY_CERT_PROVIDER_KEY, SAN_MATCHER_KEY, SanMatcher } from "./load-balancer-cds";
2020
import GrpcUri = experimental.GrpcUri;
2121
import SecureConnector = experimental.SecureConnector;
2222
import createCertificateProviderChannelCredentials = experimental.createCertificateProviderChannelCredentials;
2323

24+
const TRACER_NAME = 'xds_channel_credentials';
25+
26+
function trace(text: string) {
27+
experimental.trace(logVerbosity.DEBUG, TRACER_NAME, text);
28+
}
29+
2430
export class XdsChannelCredentials extends ChannelCredentials {
2531
constructor(private fallbackCredentials: ChannelCredentials) {
2632
super();
@@ -33,20 +39,23 @@ export class XdsChannelCredentials extends ChannelCredentials {
3339
}
3440
_createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions, callCredentials?: CallCredentials): SecureConnector {
3541
if (options[CA_CERT_PROVIDER_KEY]) {
42+
trace('Using secure credentials');
3643
const verifyOptions: VerifyOptions = {};
3744
if (options[SAN_MATCHER_KEY]) {
3845
const matcher = options[SAN_MATCHER_KEY] as SanMatcher;
3946
verifyOptions.checkServerIdentity = (hostname, cert) => {
4047
if (cert.subjectaltname && matcher.apply(cert.subjectaltname)) {
4148
return undefined;
4249
} else {
50+
trace('Subject alternative name not matched: ' + cert.subjectaltname);
4351
return new Error('No matching subject alternative name found in certificate');
4452
}
4553
}
4654
}
4755
const certProviderCreds = createCertificateProviderChannelCredentials(options[CA_CERT_PROVIDER_KEY], options[IDENTITY_CERT_PROVIDER_KEY] ?? null, verifyOptions);
4856
return certProviderCreds._createSecureConnector(channelTarget, options, callCredentials);
4957
} else {
58+
trace('Using fallback credentials');
5059
return this.fallbackCredentials._createSecureConnector(channelTarget, options, callCredentials);
5160
}
5261
}
@@ -55,7 +64,7 @@ export class XdsChannelCredentials extends ChannelCredentials {
5564

5665
export class XdsServerCredentials extends ServerCredentials {
5766
constructor(private fallbackCredentials: ServerCredentials) {
58-
super();
67+
super({});
5968
}
6069

6170
getFallbackCredentials() {

packages/grpc-js-xds/src/xds-dependency-manager.ts

+19-9
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,11 @@ export class XdsDependencyManager {
360360
constructor(private xdsClient: XdsClient, private listenerResourceName: string, private dataPlaneAuthority: string, private watcher: XdsConfigWatcher) {
361361
this.ldsWatcher = new Watcher<Listener__Output>({
362362
onResourceChanged: (update: Listener__Output) => {
363+
if (!update.api_listener) {
364+
this.trace('Received Listener resource not usable on client');
365+
this.handleListenerDoesNotExist();
366+
return;
367+
}
363368
this.latestListener = update;
364369
const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, update.api_listener!.api_listener!.value);
365370
switch (httpConnectionManager.route_specifier) {
@@ -401,15 +406,7 @@ export class XdsDependencyManager {
401406
},
402407
onResourceDoesNotExist: () => {
403408
this.trace('Resolution error: LDS resource does not exist');
404-
if (this.latestRouteConfigName) {
405-
this.trace('RDS.cancelWatch(' + this.latestRouteConfigName + '): LDS resource does not exist');
406-
RouteConfigurationResourceType.cancelWatch(this.xdsClient, this.latestRouteConfigName, this.rdsWatcher);
407-
this.latestRouteConfigName = null;
408-
this.latestRouteConfiguration = null;
409-
this.clusterRoots = [];
410-
this.pruneOrphanClusters();
411-
}
412-
this.watcher.onResourceDoesNotExist(`Listener ${listenerResourceName}`);
409+
this.handleListenerDoesNotExist();
413410
}
414411
});
415412
this.rdsWatcher = new Watcher<RouteConfiguration__Output>({
@@ -435,6 +432,19 @@ export class XdsDependencyManager {
435432
trace('[' + this.listenerResourceName + '] ' + text);
436433
}
437434

435+
private handleListenerDoesNotExist() {
436+
if (this.latestRouteConfigName) {
437+
this.trace('RDS.cancelWatch(' + this.latestRouteConfigName + '): LDS resource does not exist');
438+
RouteConfigurationResourceType.cancelWatch(this.xdsClient, this.latestRouteConfigName, this.rdsWatcher);
439+
this.latestRouteConfigName = null;
440+
this.latestRouteConfiguration = null;
441+
this.clusterRoots = [];
442+
this.pruneOrphanClusters();
443+
}
444+
this.watcher.onResourceDoesNotExist(`Listener ${this.listenerResourceName}`);
445+
446+
}
447+
438448
private maybeSendUpdate() {
439449
if (!this.latestListener) {
440450
this.trace('Not sending update: no Listener update received');

0 commit comments

Comments
 (0)