Skip to content

Commit

Permalink
grpc-js-xds: Add XdsChannelCredentials
Browse files Browse the repository at this point in the history
  • Loading branch information
murgatroid99 committed Dec 9, 2024
1 parent 8f08bbe commit b84940e
Show file tree
Hide file tree
Showing 21 changed files with 690 additions and 88 deletions.
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

0 comments on commit b84940e

Please sign in to comment.