Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

grpc-js-xds: Add XdsChannelCredentials #2866

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/grpc-js-xds/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import * as typed_struct_lb from './lb-policy-registry/typed-struct';
import * as pick_first_lb from './lb-policy-registry/pick-first';

export { XdsServer } from './server';
export { XdsServerCredentials } from './xds-credentials';
export { XdsChannelCredentials, XdsServerCredentials } from './xds-credentials';

/**
* Register the "xds:" name scheme with the @grpc/grpc-js library.
Expand Down
161 changes: 154 additions & 7 deletions packages/grpc-js-xds/src/load-balancer-cds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ import { XdsConfig } from './xds-dependency-manager';
import { LocalityEndpoint, PriorityChildRaw } from './load-balancer-priority';
import { Locality__Output } from './generated/envoy/config/core/v3/Locality';
import { AGGREGATE_CLUSTER_BACKWARDS_COMPAT, EXPERIMENTAL_OUTLIER_DETECTION } from './environment';
import { XDS_CONFIG_KEY } from './resolver-xds';
import { XDS_CLIENT_KEY, XDS_CONFIG_KEY } from './resolver-xds';
import { ContainsValueMatcher, Matcher, PrefixValueMatcher, RejectValueMatcher, SafeRegexValueMatcher, SuffixValueMatcher, ValueMatcher } from './matcher';
import { StringMatcher__Output } from './generated/envoy/type/matcher/v3/StringMatcher';
import { isIPv6 } from 'net';
import { formatIPv6, parseIPv6 } from './cidr';

const TRACER_NAME = 'cds_balancer';

Expand Down Expand Up @@ -67,6 +71,125 @@ class CdsLoadBalancingConfig implements TypedLoadBalancingConfig {
}
}

type SupportedSanType = 'DNS' | 'URI' | 'email' | 'IP Address';

function isSupportedSanType(type: string): type is SupportedSanType {
return ['DNS', 'URI', 'email', 'IP Address'].includes(type);
}

class DnsExactValueMatcher implements ValueMatcher {
constructor(private targetValue: string, private ignoreCase: boolean) {
if (ignoreCase) {
this.targetValue = this.targetValue.toLowerCase();
}
}
apply(entry: string): boolean {
let [type, value] = entry.split(':');
if (!isSupportedSanType(type)) {
return false;
}
if (!value) {
return false;
}
if (this.ignoreCase) {
value = value.toLowerCase();
}
if (type === 'DNS' && value.startsWith('*.') && this.targetValue.includes('.', 1)) {
return value.substring(2) === this.targetValue.substring(this.targetValue.indexOf('.') + 1);
} else {
return value === this.targetValue;
}
}

toString() {
return 'DnsExact(' + this.targetValue + ', ignore_case=' + this.ignoreCase + ')';
}
}

function canonicalizeSanEntryValue(type: SupportedSanType, value: string): string {
if (type === 'IP Address' && isIPv6(value)) {
return formatIPv6(parseIPv6(value));
}
return value;
}

class SanEntryMatcher implements ValueMatcher {
private childMatcher: ValueMatcher;
constructor(matcherConfig: StringMatcher__Output) {
const ignoreCase = matcherConfig.ignore_case;
switch(matcherConfig.match_pattern) {
case 'exact':
throw new Error('Unexpected exact matcher in SAN entry matcher');
case 'prefix':
this.childMatcher = new PrefixValueMatcher(matcherConfig.prefix!, ignoreCase);
break;
case 'suffix':
this.childMatcher = new SuffixValueMatcher(matcherConfig.suffix!, ignoreCase);
break;
case 'safe_regex':
this.childMatcher = new SafeRegexValueMatcher(matcherConfig.safe_regex!.regex);
break;
case 'contains':
this.childMatcher = new ContainsValueMatcher(matcherConfig.contains!, ignoreCase);
break;
default:
this.childMatcher = new RejectValueMatcher();
}
}
apply(entry: string): boolean {
let [type, value] = entry.split(':');
if (!isSupportedSanType(type)) {
return false;
}
value = canonicalizeSanEntryValue(type, value);
if (!entry) {
return false;
}
return this.childMatcher.apply(value);
}
toString(): string {
return this.childMatcher.toString();
}

}

export class SanMatcher implements ValueMatcher {
private childMatchers: ValueMatcher[];
constructor(matcherConfigs: StringMatcher__Output[]) {
this.childMatchers = matcherConfigs.map(config => {
if (config.match_pattern === 'exact') {
return new DnsExactValueMatcher(config.exact!, config.ignore_case);
} else {
return new SanEntryMatcher(config);
}
});
}
apply(value: string): boolean {
if (this.childMatchers.length === 0) {
return true;
}
for (const entry of value.split(', ')) {
for (const matcher of this.childMatchers) {
const checkResult = matcher.apply(entry);
if (checkResult) {
return true;
}
}
}
return false;
}
toString(): string {
return 'SanMatcher(' + this.childMatchers.map(matcher => matcher.toString()).sort().join(', ') + ')';
}

equals(other: SanMatcher): boolean {
return this.toString() === other.toString();
}
}

export const CA_CERT_PROVIDER_KEY = 'grpc.internal.ca_cert_provider';
export const IDENTITY_CERT_PROVIDER_KEY = 'grpc.internal.identity_cert_provider';
export const SAN_MATCHER_KEY = 'grpc.internal.san_matcher';

const RECURSION_DEPTH_LIMIT = 15;

Expand Down Expand Up @@ -102,6 +225,8 @@ export class CdsLoadBalancer implements LoadBalancer {
private priorityNames: string[] = [];
private nextPriorityChildNumber = 0;

private latestSanMatcher: SanMatcher | null = null;

constructor(private readonly channelControlHelper: ChannelControlHelper) {
this.childBalancer = new ChildLoadBalancerHandler(channelControlHelper);
}
Expand Down Expand Up @@ -140,7 +265,7 @@ export class CdsLoadBalancer implements LoadBalancer {
leafClusters = getLeafClusters(xdsConfig, clusterName);
} catch (e) {
trace('xDS config parsing failed with error ' + (e as Error).message);
this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `xDS config parsing failed with error ${(e as Error).message}`, metadata: new Metadata()}));
this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `xDS config parsing failed with error ${(e as Error).message}`}));
return;
}
const priorityChildren: {[name: string]: PriorityChildRaw} = {};
Expand All @@ -165,7 +290,7 @@ export class CdsLoadBalancer implements LoadBalancer {
typedChildConfig = parseLoadBalancingConfig(childConfig);
} catch (e) {
trace('LB policy config parsing failed with error ' + (e as Error).message);
this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `LB policy config parsing failed with error ${(e as Error).message}`, metadata: new Metadata()}));
this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `LB policy config parsing failed with error ${(e as Error).message}`}));
return;
}
this.childBalancer.updateAddressList(endpointList, typedChildConfig, {...options, [ROOT_CLUSTER_KEY]: clusterName});
Expand Down Expand Up @@ -272,17 +397,39 @@ export class CdsLoadBalancer implements LoadBalancer {
} else {
childConfig = xdsClusterImplConfig;
}
trace(JSON.stringify(childConfig, undefined, 2));
let typedChildConfig: TypedLoadBalancingConfig;
try {
typedChildConfig = parseLoadBalancingConfig(childConfig);
} catch (e) {
trace('LB policy config parsing failed with error ' + (e as Error).message);
this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `LB policy config parsing failed with error ${(e as Error).message}`, metadata: new Metadata()}));
this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `LB policy config parsing failed with error ${(e as Error).message}`}));
return;
}
trace(JSON.stringify(typedChildConfig.toJsonObject(), undefined, 2));
this.childBalancer.updateAddressList(childEndpointList, typedChildConfig, options);
const childOptions: ChannelOptions = {...options};
if (clusterConfig.cluster.securityUpdate) {
const securityUpdate = clusterConfig.cluster.securityUpdate;
const xdsClient = options[XDS_CLIENT_KEY] as XdsClient;
const caCertProvider = xdsClient.getCertificateProvider(securityUpdate.caCertificateProviderInstance);
if (!caCertProvider) {
this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `Cluster ${clusterName} configured with CA certificate provider ${securityUpdate.caCertificateProviderInstance} not in bootstrap`}));
return;
}
if (securityUpdate.identityCertificateProviderInstance) {
const identityCertProvider = xdsClient.getCertificateProvider(securityUpdate.identityCertificateProviderInstance);
if (!identityCertProvider) {
this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `Cluster ${clusterName} configured with identity certificate provider ${securityUpdate.identityCertificateProviderInstance} not in bootstrap`}));
return;
}
childOptions[IDENTITY_CERT_PROVIDER_KEY] = identityCertProvider;
}
childOptions[CA_CERT_PROVIDER_KEY] = caCertProvider;
const sanMatcher = new SanMatcher(securityUpdate.subjectAltNameMatchers);
if (this.latestSanMatcher === null || !this.latestSanMatcher.equals(sanMatcher)) {
this.latestSanMatcher = sanMatcher;
}
childOptions[SAN_MATCHER_KEY] = this.latestSanMatcher;
}
this.childBalancer.updateAddressList(childEndpointList, typedChildConfig, childOptions);
}
}
exitIdle(): void {
Expand Down
13 changes: 11 additions & 2 deletions packages/grpc-js-xds/src/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { ClusterConfig__Output } from './generated/envoy/extensions/clusters/agg
import { HttpConnectionManager__Output } from './generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager';
import { EXPERIMENTAL_FEDERATION } from './environment';
import { DownstreamTlsContext__Output } from './generated/envoy/extensions/transport_sockets/tls/v3/DownstreamTlsContext';
import { UpstreamTlsContext__Output } from './generated/envoy/extensions/transport_sockets/tls/v3/UpstreamTlsContext';

export const EDS_TYPE_URL = 'type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment';
export const CDS_TYPE_URL = 'type.googleapis.com/envoy.config.cluster.v3.Cluster';
Expand All @@ -55,10 +56,16 @@ export const DOWNSTREAM_TLS_CONTEXT_TYPE_URL = 'type.googleapis.com/envoy.extens

export type DownstreamTlsContextTypeUrl = 'type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext';

export const UPSTREAM_TLS_CONTEXT_TYPE_URL = 'type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext';

export type UpstreamTlsContextTypeUrl = 'type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext';

export type ResourceTypeUrl = AdsTypeUrl | HttpConnectionManagerTypeUrl | ClusterConfigTypeUrl | DownstreamTlsContextTypeUrl | UpstreamTlsContextTypeUrl;

/**
* Map type URLs to their corresponding message types
*/
export type AdsOutputType<T extends AdsTypeUrl | HttpConnectionManagerTypeUrl | ClusterConfigTypeUrl | DownstreamTlsContextTypeUrl> = T extends EdsTypeUrl
export type AdsOutputType<T extends ResourceTypeUrl> = T extends EdsTypeUrl
? ClusterLoadAssignment__Output
: T extends CdsTypeUrl
? Cluster__Output
Expand All @@ -70,6 +77,8 @@ export type AdsOutputType<T extends AdsTypeUrl | HttpConnectionManagerTypeUrl |
? HttpConnectionManager__Output
: T extends ClusterConfigTypeUrl
? ClusterConfig__Output
: T extends UpstreamTlsContextTypeUrl
? UpstreamTlsContext__Output
: DownstreamTlsContext__Output;


Expand Down Expand Up @@ -100,7 +109,7 @@ const toObjectOptions = {
oneofs: true
}

export function decodeSingleResource<T extends AdsTypeUrl | HttpConnectionManagerTypeUrl | ClusterConfigTypeUrl | DownstreamTlsContextTypeUrl>(targetTypeUrl: T, message: Buffer): AdsOutputType<T> {
export function decodeSingleResource<T extends ResourceTypeUrl>(targetTypeUrl: T, message: Buffer): AdsOutputType<T> {
const name = targetTypeUrl.substring(targetTypeUrl.lastIndexOf('/') + 1);
const type = resourceRoot.lookup(name);
if (type) {
Expand Down
38 changes: 37 additions & 1 deletion packages/grpc-js-xds/src/xds-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,43 @@
*
*/

import { ServerCredentials } from "@grpc/grpc-js";
import { CallCredentials, ChannelCredentials, ChannelOptions, ServerCredentials, VerifyOptions, experimental } from "@grpc/grpc-js";
import { CA_CERT_PROVIDER_KEY, IDENTITY_CERT_PROVIDER_KEY, SAN_MATCHER_KEY, SanMatcher } from "./load-balancer-cds";
import GrpcUri = experimental.GrpcUri;
import SecureConnector = experimental.SecureConnector;
import createCertificateProviderChannelCredentials = experimental.createCertificateProviderChannelCredentials;

export class XdsChannelCredentials extends ChannelCredentials {
constructor(private fallbackCredentials: ChannelCredentials) {
super();
}
_isSecure(): boolean {
return true;
}
_equals(other: ChannelCredentials): boolean {
return other instanceof XdsChannelCredentials && this.fallbackCredentials === other.fallbackCredentials;
}
_createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions, callCredentials?: CallCredentials): SecureConnector {
if (options[CA_CERT_PROVIDER_KEY]) {
const verifyOptions: VerifyOptions = {};
if (options[SAN_MATCHER_KEY]) {
const matcher = options[SAN_MATCHER_KEY] as SanMatcher;
verifyOptions.checkServerIdentity = (hostname, cert) => {
if (cert.subjectaltname && matcher.apply(cert.subjectaltname)) {
return undefined;
} else {
return new Error('No matching subject alternative name found in certificate');
}
}
}
const certProviderCreds = createCertificateProviderChannelCredentials(options[CA_CERT_PROVIDER_KEY], options[IDENTITY_CERT_PROVIDER_KEY] ?? null, verifyOptions);
return certProviderCreds._createSecureConnector(channelTarget, options, callCredentials);
} else {
return this.fallbackCredentials._createSecureConnector(channelTarget, options, callCredentials);
}
}

}

export class XdsServerCredentials extends ServerCredentials {
constructor(private fallbackCredentials: ServerCredentials) {
Expand Down
Loading
Loading