Merge branch 'master' into grpc-js-xds_security_tests

This commit is contained in:
Michael Lumish 2025-02-12 09:58:19 -08:00
commit e5fa6b7c05
7 changed files with 545 additions and 387 deletions

View File

@ -40,6 +40,12 @@ npm install
cd $base/../grpc-js
npm install
cd $base/../grpc-health-check
npm install
cd $base/../grpc-reflection
npm install
# grpc-js-xds has a dev dependency on "../grpc-js", so it should pull that in automatically
cd $base
git submodule update --init --recursive

View File

@ -16,7 +16,7 @@
*/
import { CDS_TYPE_URL, CLUSTER_CONFIG_TYPE_URL, decodeSingleResource, UPSTREAM_TLS_CONTEXT_TYPE_URL } from "../resources";
import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type";
import { ValidationResult, XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type";
import { LoadBalancingConfig, experimental, logVerbosity } from "@grpc/grpc-js";
import { XdsServerConfig } from "../xds-bootstrap";
import { Duration__Output } from "../generated/google/protobuf/Duration";
@ -33,6 +33,8 @@ import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionC
import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig;
import { StringMatcher__Output } from "../generated/envoy/type/matcher/v3/StringMatcher";
import { CertificateValidationContext__Output } from "../generated/envoy/extensions/transport_sockets/tls/v3/CertificateValidationContext";
import { SocketAddress__Output } from "../generated/envoy/config/core/v3/SocketAddress";
import { TransportSocket__Output } from "../generated/envoy/config/core/v3/TransportSocket";
const TRACER_NAME = 'xds_client';
@ -137,20 +139,124 @@ export class ClusterResourceType extends XdsResourceType {
return percentage.value >=0 && percentage.value <= 100;
}
private validateResource(context: XdsDecodeContext, message: Cluster__Output): CdsUpdate | null {
let lbPolicyConfig: LoadBalancingConfig;
private validateTransportSocket(context: XdsDecodeContext, transportSocket: TransportSocket__Output): ValidationResult<SecurityUpdate> {
const errors: string[] = [];
if (!transportSocket.typed_config) {
errors.push('transport_socket.typed_config unset');
return {
valid: false,
errors
};
}
if (transportSocket.typed_config.type_url !== UPSTREAM_TLS_CONTEXT_TYPE_URL) {
errors.push(`Unexpected transport_socket.typed_config.type_url: ${transportSocket.typed_config.type_url}`);
return {
valid: false,
errors
};
}
const upstreamTlsContext = decodeSingleResource(UPSTREAM_TLS_CONTEXT_TYPE_URL, transportSocket.typed_config.value);
if (!upstreamTlsContext.common_tls_context) {
errors.push('UpstreamTlsContext.common_tls_context unset');
return {
valid: false,
errors
};
}
trace('Decoded UpstreamTlsContext: ' + JSON.stringify(upstreamTlsContext, undefined, 2));
const commonTlsContext = upstreamTlsContext.common_tls_context;
if (commonTlsContext.tls_certificate_provider_instance) {
if (!(commonTlsContext.tls_certificate_provider_instance.instance_name in context.bootstrap.certificateProviders)) {
errors.push(`Unmatched UpstreamTlsContext.tls_certificate_provider_instance.instance_name: ${commonTlsContext.tls_certificate_provider_instance.instance_name}`);
}
} else {
if (commonTlsContext.tls_certificates.length > 0 ) {
errors.push('UpstreamTlsContext.common_tls_contexttls_certificate_provider_instance unset but UpstreamTlsContext.common_tls_context.tls_certificates populated');
}
if (commonTlsContext.tls_certificate_sds_secret_configs.length > 0) {
errors.push('UpstreamTlsContext.common_tls_contexttls_certificate_provider_instance unset but UpstreamTlsContext.common_tls_context.tls_certificates_sds_secret_config populated');
}
}
if (commonTlsContext.tls_params) {
errors.push('UpstreamTlsContext.common_tls_context.tls_params set');
}
if (commonTlsContext.custom_handshaker) {
errors.push('UpstreamTlsContext.common_tls_context.custom_handshaker set');
}
let validationContext: CertificateValidationContext__Output | null = null;
switch (commonTlsContext.validation_context_type) {
case 'validation_context_sds_secret_config':
errors.push('Unexpected UpstreamTlsContext.common_tls_context.validation_context_sds_secret_config');
break;
case 'validation_context':
if (!commonTlsContext.validation_context) {
errors.push('Empty UpstreamTlsContext.common_tls_context.validation_context');
break;
}
validationContext = commonTlsContext.validation_context;
break;
case 'combined_validation_context':
if (!commonTlsContext.combined_validation_context?.default_validation_context) {
errors.push('Empty UpstreamTlsContext.common_tls_context.combined_validation_context.default_validation_context');
break;
}
validationContext = commonTlsContext.combined_validation_context.default_validation_context;
break;
default:
errors.push(`Unsupported UpstreamTlsContext.common_tls_context.validation_context_type: ${commonTlsContext.validation_context_type}`);
}
if (validationContext) {
if (validationContext.verify_certificate_spki.length > 0) {
errors.push('ValidationContext.verify_certificate_spki populated');
}
if (validationContext.verify_certificate_hash.length > 0) {
errors.push('ValidationContext.verify_certificate_hash populated');
}
if (validationContext.require_signed_certificate_timestamp) {
errors.push('ValidationContext.require_signed_certificate_timestamp set');
}
if (validationContext.crl) {
errors.push('ValidationContext.crl set');
}
if (validationContext.custom_validator_config) {
errors.push('ValidationContext.custom_validator_config set')
}
if (validationContext.ca_certificate_provider_instance) {
if (!(validationContext.ca_certificate_provider_instance.instance_name in context.bootstrap.certificateProviders)) {
errors.push(`Unmatched ValidationContext.ca_certificate_provider_instance.instance_name: ${validationContext.ca_certificate_provider_instance.instance_name}`);
}
if (errors.length === 0) {
return {
valid: true,
result: {
caCertificateProviderInstance: validationContext.ca_certificate_provider_instance.instance_name,
identityCertificateProviderInstance: commonTlsContext.tls_certificate_provider_instance?.instance_name,
subjectAltNameMatchers: validationContext.match_subject_alt_names
}
}
}
} else {
errors.push('ValidationContext.ca_certificate_provider_instance unset');
}
}
return {
valid: false,
errors
}
}
private validateResource(context: XdsDecodeContext, message: Cluster__Output): ValidationResult<CdsUpdate> {
/* lbPolicyConfig starts as an empty config to satisfy the type checker.
* In all cases, either it should be reassigned or an error should be set.
* Either way, this empty config should never actually be used. */
let lbPolicyConfig: LoadBalancingConfig = {};
const errors: string[] = [];
if (EXPERIMENTAL_CUSTOM_LB_CONFIG && message.load_balancing_policy) {
try {
lbPolicyConfig = convertToLoadBalancingConfig(message.load_balancing_policy);
} catch (e) {
trace('LB policy config parsing failed with error ' + e);
return null;
}
try {
parseLoadBalancingConfig(lbPolicyConfig);
} catch (e) {
trace('LB policy config parsing failed with error ' + e);
return null;
errors.push(`load_balancing_policy parsing failed with error ${(e as Error).message}`);
}
} else if (message.lb_policy === 'ROUND_ROBIN') {
lbPolicyConfig = {
@ -160,15 +266,15 @@ export class ClusterResourceType extends XdsResourceType {
};
} else if(EXPERIMENTAL_RING_HASH && message.lb_policy === 'RING_HASH') {
if (message.ring_hash_lb_config && message.ring_hash_lb_config.hash_function !== 'XX_HASH') {
return null;
errors.push(`unsupported ring_hash_lb_config.hash_function: ${message.ring_hash_lb_config.hash_function}`);
}
const minRingSize = message.ring_hash_lb_config?.minimum_ring_size ? Number(message.ring_hash_lb_config.minimum_ring_size.value) : 1024;
if (minRingSize > 8_388_608) {
return null;
errors.push(`ring_hash_lb_config.minimum_ring_size is too large: ${minRingSize}`);
}
const maxRingSize = message.ring_hash_lb_config?.maximum_ring_size ? Number(message.ring_hash_lb_config.maximum_ring_size.value) : 8_388_608;
if (maxRingSize > 8_388_608) {
return null;
errors.push(`ring_hash_lb_config.maximum_ring_size is too large: ${maxRingSize}`);
}
lbPolicyConfig = {
ring_hash: {
@ -177,133 +283,81 @@ export class ClusterResourceType extends XdsResourceType {
}
};
} else {
return null;
if (EXPERIMENTAL_CUSTOM_LB_CONFIG) {
errors.push(`load_balancing_policy unset and unsupported lb_policy: ${message.lb_policy}`);
} else {
errors.push(`unsupported lb_policy: ${message.lb_policy}`);
}
}
if (message.lrs_server) {
if (!message.lrs_server.self) {
return null;
errors.push(`lrs_server set but lrs_server.self unset`);
}
}
if (EXPERIMENTAL_OUTLIER_DETECTION) {
if (message.outlier_detection) {
if (!this.validateNonnegativeDuration(message.outlier_detection.interval)) {
return null;
errors.push('outlier_detection.interval out of range');
}
if (!this.validateNonnegativeDuration(message.outlier_detection.base_ejection_time)) {
return null;
errors.push('outlier_detection.base_ejection_time out of range');
}
if (!this.validateNonnegativeDuration(message.outlier_detection.max_ejection_time)) {
return null;
errors.push('outlier_detection.max_ejection_time out of range');
}
if (!this.validatePercentage(message.outlier_detection.max_ejection_percent)) {
return null;
errors.push('outlier_detection.max_ejection_percent out of range');
}
if (!this.validatePercentage(message.outlier_detection.enforcing_success_rate)) {
return null;
errors.push('outlier_detection.enforcing_success_rate out of range');
}
if (!this.validatePercentage(message.outlier_detection.failure_percentage_threshold)) {
return null;
errors.push('outlier_detection.failure_percentage_threshold out of range');
}
if (!this.validatePercentage(message.outlier_detection.enforcing_failure_percentage)) {
return null;
errors.push('outlier_detection.enforcing_failure_percentage out of range');
}
}
}
let securityUpdate: SecurityUpdate | undefined = undefined;
if (message.transport_socket) {
const transportSocket = message.transport_socket;
if (!transportSocket.typed_config) {
trace('transportSocket.typed_config missing');
return null;
}
if (transportSocket.typed_config.type_url !== UPSTREAM_TLS_CONTEXT_TYPE_URL) {
trace('Incorrect transportSocket.typed_config.type_url: ' + transportSocket.typed_config.type_url)
return null;
}
const upstreamTlsContext = decodeSingleResource(UPSTREAM_TLS_CONTEXT_TYPE_URL, transportSocket.typed_config.value);
if (!upstreamTlsContext.common_tls_context) {
trace('Could not decode UpstreamTlsContext');
return null;
}
trace('Decoded UpstreamTlsContext: ' + JSON.stringify(upstreamTlsContext, undefined, 2));
const commonTlsContext = upstreamTlsContext.common_tls_context;
let validationContext: CertificateValidationContext__Output;
switch (commonTlsContext.validation_context_type) {
case 'validation_context_sds_secret_config':
return null;
case 'validation_context':
if (!commonTlsContext.validation_context) {
return null;
}
validationContext = commonTlsContext.validation_context;
break;
case 'combined_validation_context':
if (!commonTlsContext.combined_validation_context?.default_validation_context) {
return null;
}
validationContext = commonTlsContext.combined_validation_context.default_validation_context;
break;
default:
return null;
}
if (!validationContext.ca_certificate_provider_instance) {
return null;
}
if (!(validationContext.ca_certificate_provider_instance.instance_name in context.bootstrap.certificateProviders)) {
return null;
}
if (validationContext.verify_certificate_spki.length > 0) {
return null;
}
if (validationContext.verify_certificate_hash.length > 0) {
return null;
}
if (validationContext.require_signed_certificate_timestamp) {
return null;
}
if (validationContext.crl) {
return null;
}
if (validationContext.custom_validator_config) {
return null;
}
if (commonTlsContext.tls_certificate_provider_instance) {
if (!(commonTlsContext.tls_certificate_provider_instance.instance_name in context.bootstrap.certificateProviders)) {
return null;
}
const validationResult = this.validateTransportSocket(context, message.transport_socket);
if (validationResult.valid) {
securityUpdate = validationResult.result;
} else {
if (commonTlsContext.tls_certificates.length > 0 || commonTlsContext.tls_certificate_sds_secret_configs.length > 0) {
return null;
}
}
if (commonTlsContext.tls_params) {
return null;
}
if (commonTlsContext.custom_handshaker) {
return null;
}
securityUpdate = {
caCertificateProviderInstance: validationContext.ca_certificate_provider_instance.instance_name,
identityCertificateProviderInstance: commonTlsContext.tls_certificate_provider_instance?.instance_name,
subjectAltNameMatchers: validationContext.match_subject_alt_names
errors.push(...validationResult.errors);
}
}
if (message.cluster_discovery_type === 'cluster_type') {
if (!(message.cluster_type?.typed_config && message.cluster_type.typed_config.type_url === CLUSTER_CONFIG_TYPE_URL)) {
return null;
}
const clusterConfig = decodeSingleResource(CLUSTER_CONFIG_TYPE_URL, message.cluster_type.typed_config.value);
if (clusterConfig.clusters.length === 0) {
return null;
if (message.cluster_type?.typed_config) {
if (message.cluster_type.typed_config.type_url === CLUSTER_CONFIG_TYPE_URL) {
const clusterConfig = decodeSingleResource(CLUSTER_CONFIG_TYPE_URL, message.cluster_type.typed_config.value);
if (clusterConfig.clusters.length === 0) {
errors.push(`cluster_type.typed_config.clusters.length == ${clusterConfig.clusters.length}`);
}
if (errors.length === 0) {
return {
valid: true,
result: {
type: 'AGGREGATE',
name: message.name,
aggregateChildren: clusterConfig.clusters,
outlierDetectionUpdate: convertOutlierDetectionUpdate(null),
lbPolicyConfig: [lbPolicyConfig],
securityUpdate: securityUpdate
}
};
}
} else {
errors.push(`Unexpected cluster_type.typed_config.type_url: ${message.cluster_type.typed_config.type_url}`);
}
} else {
errors.push('cluster_type.typed_config unset') ;
}
return {
type: 'AGGREGATE',
name: message.name,
aggregateChildren: clusterConfig.clusters,
outlierDetectionUpdate: convertOutlierDetectionUpdate(null),
lbPolicyConfig: [lbPolicyConfig],
securityUpdate: securityUpdate
};
valid: false,
errors
}
} else {
let maxConcurrentRequests: number | undefined = undefined;
for (const threshold of message.circuit_breakers?.thresholds ?? []) {
@ -312,57 +366,91 @@ export class ClusterResourceType extends XdsResourceType {
}
}
if (message.type === 'EDS') {
if (!message.eds_cluster_config?.eds_config?.ads && !message.eds_cluster_config?.eds_config?.self) {
return null;
if (message.eds_cluster_config) {
if (!message.eds_cluster_config.eds_config?.ads && !message.eds_cluster_config.eds_config?.self) {
errors.push('eds_cluster_config.eds_config.ads and eds_cluster_config.eds_config.self both unset');
}
if (message.name.startsWith('xdstp:') && message.eds_cluster_config.service_name === '') {
errors.push('name starts with "xdstp:" and eds_cluster_config.service_name is empty');
}
} else {
errors.push('type == EDS but eds_cluster_config is unset');
}
if (message.name.startsWith('xdstp:') && message.eds_cluster_config.service_name === '') {
return null;
}
return {
type: 'EDS',
name: message.name,
aggregateChildren: [],
maxConcurrentRequests: maxConcurrentRequests,
edsServiceName: message.eds_cluster_config.service_name === '' ? undefined : message.eds_cluster_config.service_name,
lrsLoadReportingServer: message.lrs_server ? context.server : undefined,
outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection),
lbPolicyConfig: [lbPolicyConfig],
securityUpdate: securityUpdate
if (errors.length === 0) {
return {
valid: true,
result: {
type: 'EDS',
name: message.name,
aggregateChildren: [],
maxConcurrentRequests: maxConcurrentRequests,
edsServiceName: message.eds_cluster_config!.service_name === '' ? undefined : message.eds_cluster_config!.service_name,
lrsLoadReportingServer: message.lrs_server ? context.server : undefined,
outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection),
lbPolicyConfig: [lbPolicyConfig],
securityUpdate: securityUpdate
}
}
} else {
return {
valid: false,
errors
};
}
} else if (message.type === 'LOGICAL_DNS') {
if (!message.load_assignment) {
return null;
let socketAddress: SocketAddress__Output | null | undefined = undefined;
if (message.load_assignment) {
if (message.load_assignment.endpoints.length === 1) {
if (message.load_assignment.endpoints[0].lb_endpoints.length === 1) {
socketAddress = message.load_assignment.endpoints[0].lb_endpoints[0].endpoint?.address?.socket_address;
if (socketAddress) {
if (socketAddress.address === '') {
errors.push('load_assignment.endpoints[0].lb_endpoints[0].endpoint.address.socket_address.address is empty');
}
if (socketAddress.port_specifier !== 'port_value') {
errors.push(`Unsupported load_assignment.endpoints[0].lb_endpoints[0].endpoint.address.socket_address.port_value: ${socketAddress.port_value}`);
}
} else {
errors.push('load_assignment.endpoints[0].lb_endpoints[0].endpoint.address.socket_address is not set');
}
} else {
errors.push(`load_assignment.endpoints[0].lb_endpoints.length == ${message.load_assignment.endpoints[0].lb_endpoints.length}`);
}
} else {
errors.push(`load_assignment.endpoints.length == ${message.load_assignment.endpoints.length}`);
}
} else {
errors.push(`load_assignment unset`);
}
if (message.load_assignment.endpoints.length !== 1) {
return null;
if (errors.length === 0) {
return {
valid: true,
result: {
type: 'LOGICAL_DNS',
name: message.name,
aggregateChildren: [],
maxConcurrentRequests: maxConcurrentRequests,
dnsHostname: `${socketAddress!.address}:${socketAddress!.port_value}`,
lrsLoadReportingServer: message.lrs_server ? context.server : undefined,
outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection),
lbPolicyConfig: [lbPolicyConfig],
securityUpdate: securityUpdate
}
};
} else {
return {
valid: false,
errors
}
}
if (message.load_assignment.endpoints[0].lb_endpoints.length !== 1) {
return null;
}
const socketAddress = message.load_assignment.endpoints[0].lb_endpoints[0].endpoint?.address?.socket_address;
if (!socketAddress) {
return null;
}
if (socketAddress.address === '') {
return null;
}
if (socketAddress.port_specifier !== 'port_value') {
return null;
}
return {
type: 'LOGICAL_DNS',
name: message.name,
aggregateChildren: [],
maxConcurrentRequests: maxConcurrentRequests,
dnsHostname: `${socketAddress.address}:${socketAddress.port_value}`,
lrsLoadReportingServer: message.lrs_server ? context.server : undefined,
outlierDetectionUpdate: convertOutlierDetectionUpdate(message.outlier_detection),
lbPolicyConfig: [lbPolicyConfig],
securityUpdate: securityUpdate
};
} else {
errors.push(`Unsupported type ${message.type}`);
}
}
return null;
return {
valid: false,
errors
};
}
decode(context:XdsDecodeContext, resource: Any__Output): XdsDecodeResult {
@ -373,16 +461,16 @@ export class ClusterResourceType extends XdsResourceType {
}
const message = decodeSingleResource(CDS_TYPE_URL, resource.value);
trace('Decoded raw resource of type ' + CDS_TYPE_URL + ': ' + JSON.stringify(message, undefined, 2));
const validatedMessage = this.validateResource(context, message);
if (validatedMessage) {
const validationResult = this.validateResource(context, message);
if (validationResult.valid) {
return {
name: validatedMessage.name,
value: validatedMessage
name: validationResult.result.name,
value: validationResult.result
};
} else {
return {
name: message.name,
error: 'Cluster message validation failed'
error: `Cluster message validation failed: [${validationResult.errors}]`
};
}
}

View File

@ -1,6 +1,6 @@
import { experimental, logVerbosity } from "@grpc/grpc-js";
import { ClusterLoadAssignment__Output } from "../generated/envoy/config/endpoint/v3/ClusterLoadAssignment";
import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type";
import { ValidationResult, XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type";
import { Locality__Output } from "../generated/envoy/config/core/v3/Locality";
import { SocketAddress__Output } from "../generated/envoy/config/core/v3/SocketAddress";
import { isIPv4, isIPv6 } from "net";
@ -40,78 +40,86 @@ export class EndpointResourceType extends XdsResourceType {
return 'envoy.config.endpoint.v3.ClusterLoadAssignment';
}
private validateAddress(socketAddress: SocketAddress__Output, seenAddresses: SocketAddress__Output[]): boolean {
/**
*
* @param socketAddress
* @param seenAddresses
* @returns A list of validation errors, if there are any. An empty list indicates success
*/
private validateAddress(socketAddress: SocketAddress__Output, seenAddresses: SocketAddress__Output[]): string[] {
const errors: string[] = [];
if (socketAddress.port_specifier !== 'port_value') {
trace('EDS validation: socket_address.port_specifier !== "port_value"');
return false;
errors.push(`Unsupported port_specifier: ${socketAddress.port_specifier}`);
}
if (!(isIPv4(socketAddress.address) || isIPv6(socketAddress.address))) {
trace('EDS validation: address not a valid IPv4 or IPv6 address: ' + socketAddress.address);
return false;
errors.push(`address is not a valid IPv4 or IPv6 address: ${socketAddress.address}`);
}
for (const address of seenAddresses) {
if (addressesEqual(socketAddress, address)) {
trace('EDS validation: duplicate address seen: ' + address);
return false;
errors.push(`address is a duplicate of another address in the same endpoint: ${socketAddress.address}`);
}
}
return true;
return errors;
}
private validateResource(message: ClusterLoadAssignment__Output): ClusterLoadAssignment__Output | null {
private validateResource(message: ClusterLoadAssignment__Output): ValidationResult<ClusterLoadAssignment__Output> {
const errors: string[] = [];
const seenLocalities: {locality: Locality__Output, priority: number}[] = [];
const seenAddresses: SocketAddress__Output[] = [];
const priorityTotalWeights: Map<number, number> = new Map();
for (const endpoint of message.endpoints) {
for (const [index, endpoint] of message.endpoints.entries()) {
const errorPrefix = `endpoints[${index}]`;
if (!endpoint.locality) {
trace('EDS validation: endpoint locality unset');
return null;
errors.push(`${errorPrefix}.locality unset`);
continue;
}
for (const {locality, priority} of seenLocalities) {
if (localitiesEqual(endpoint.locality, locality) && endpoint.priority === priority) {
trace('EDS validation: endpoint locality duplicated: ' + JSON.stringify(locality) + ', priority=' + priority);
return null;
errors.push(`${errorPrefix}.locality is a duplicate of another locality in the endpoint`);
}
}
seenLocalities.push({locality: endpoint.locality, priority: endpoint.priority});
for (const lb of endpoint.lb_endpoints) {
for (const [lbIndex, lb] of endpoint.lb_endpoints.entries()) {
const lbErrorPrefix = `${errorPrefix}.lb_endpoints[${lbIndex}].endpoint`;
const socketAddress = lb.endpoint?.address?.socket_address;
if (!socketAddress) {
trace('EDS validation: endpoint socket_address not set');
return null;
if (socketAddress) {
errors.push(...this.validateAddress(socketAddress, seenAddresses).map(error => `${lbErrorPrefix}: ${error}`));
seenAddresses.push(socketAddress);
} else {
errors.push(`${lbErrorPrefix}.socket_address not set`);
}
if (!this.validateAddress(socketAddress, seenAddresses)) {
return null;
}
seenAddresses.push(socketAddress);
if (EXPERIMENTAL_DUALSTACK_ENDPOINTS && lb.endpoint?.additional_addresses) {
for (const additionalAddress of lb.endpoint.additional_addresses) {
if (!additionalAddress.address?.socket_address) {
trace('EDS validation: endpoint additional_addresses socket_address not set');
return null;
for (const [addressIndex, additionalAddress] of lb.endpoint.additional_addresses.entries()) {
if (additionalAddress.address?.socket_address) {
errors.push(...this.validateAddress(additionalAddress.address.socket_address, seenAddresses).map(error => `${lbErrorPrefix}.additional_addresses[${addressIndex}].address.socket_address: ${error}`));
seenAddresses.push(additionalAddress.address.socket_address);
} else {
errors.push(`${lbErrorPrefix}.additional_addresses[${addressIndex}].address.socket_address unset`);
}
if (!this.validateAddress(additionalAddress.address.socket_address, seenAddresses)) {
return null;
}
seenAddresses.push(additionalAddress.address.socket_address);
}
}
}
priorityTotalWeights.set(endpoint.priority, (priorityTotalWeights.get(endpoint.priority) ?? 0) + (endpoint.load_balancing_weight?.value ?? 0));
}
for (const totalWeight of priorityTotalWeights.values()) {
for (const [priority, totalWeight] of priorityTotalWeights.entries()) {
if (totalWeight > UINT32_MAX) {
trace('EDS validation: total weight > UINT32_MAX')
return null;
errors.push(`priority ${priority} has total weight greater than UINT32_MAX: ${totalWeight}`);
}
}
for (const priority of priorityTotalWeights.keys()) {
if (priority > 0 && !priorityTotalWeights.has(priority - 1)) {
trace('EDS validation: priorities not contiguous');
return null;
errors.push(`Endpoints have priority ${priority} but not ${priority - 1}`);
}
}
return message;
if (errors.length === 0) {
return {
valid: true,
result: message
};
} else {
return {
valid: false,
errors
};
}
}
decode(context: XdsDecodeContext, resource: Any__Output): XdsDecodeResult {
@ -122,16 +130,16 @@ export class EndpointResourceType extends XdsResourceType {
}
const message = decodeSingleResource(EDS_TYPE_URL, resource.value);
trace('Decoded raw resource of type ' + EDS_TYPE_URL + ': ' + JSON.stringify(message, undefined, 2));
const validatedMessage = this.validateResource(message);
if (validatedMessage) {
const validationResult = this.validateResource(message);
if (validationResult.valid) {
return {
name: validatedMessage.cluster_name,
value: validatedMessage
name: validationResult.result.cluster_name,
value: validationResult.result
};
} else {
return {
name: message.cluster_name,
error: 'Endpoint message validation failed'
error: `ClusterLoadAssignment message validation failed: [${validationResult.errors}]`
};
}
}

View File

@ -20,7 +20,7 @@ import { EXPERIMENTAL_FAULT_INJECTION } from "../environment";
import { Listener__Output } from "../generated/envoy/config/listener/v3/Listener";
import { Any__Output } from "../generated/google/protobuf/Any";
import { DOWNSTREAM_TLS_CONTEXT_TYPE_URL, HTTP_CONNECTION_MANGER_TYPE_URL, LDS_TYPE_URL, decodeSingleResource } from "../resources";
import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type";
import { ValidationResult, XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type";
import { getTopLevelFilterUrl, validateTopLevelFilter } from "../http-filter";
import { RouteConfigurationResourceType } from "./route-config-resource-type";
import { Watcher, XdsClient } from "../xds-client";
@ -30,6 +30,7 @@ import { crossProduct } from "../cross-product";
import { FilterChain__Output } from "../generated/envoy/config/listener/v3/FilterChain";
import { HttpConnectionManager__Output } from "../generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager";
import { CertificateValidationContext__Output } from "../generated/envoy/extensions/transport_sockets/tls/v3/CertificateValidationContext";
import { TransportSocket__Output } from "../generated/envoy/config/core/v3/TransportSocket";
const TRACER_NAME = 'xds_client';
@ -84,31 +85,32 @@ function normalizeFilterChainMatch(filterChainMatch: FilterChainMatch__Output):
}));
}
function validateHttpConnectionManager(httpConnectionManager: HttpConnectionManager__Output): boolean {
/**
* @param httpConnectionManager
* @returns A list of validation errors, if there are any. An empty list indicates success
*/
function validateHttpConnectionManager(httpConnectionManager: HttpConnectionManager__Output): string[] {
const errors: string[] = [];
if (EXPERIMENTAL_FAULT_INJECTION) {
const filterNames = new Set<string>();
for (const [index, httpFilter] of httpConnectionManager.http_filters.entries()) {
if (filterNames.has(httpFilter.name)) {
trace('LDS response validation failed: duplicate HTTP filter name ' + httpFilter.name);
return false;
errors.push(`duplicate HTTP filter name: ${httpFilter.name}`);
}
filterNames.add(httpFilter.name);
if (!validateTopLevelFilter(httpFilter)) {
trace('LDS response validation failed: ' + httpFilter.name + ' filter validation failed');
return false;
errors.push(`${httpFilter.name} filter validation failed`);
}
/* Validate that the last filter, and only the last filter, is the
* router filter. */
const filterUrl = getTopLevelFilterUrl(httpFilter.typed_config!)
if (index < httpConnectionManager.http_filters.length - 1) {
if (filterUrl === ROUTER_FILTER_URL) {
trace('LDS response validation failed: router filter is before end of list');
return false;
errors.push('router filter is before the end of the list');
}
} else {
if (filterUrl !== ROUTER_FILTER_URL) {
trace('LDS response validation failed: final filter is ' + filterUrl);
return false;
errors.push(`final filter is ${filterUrl}`);
}
}
}
@ -116,121 +118,133 @@ function validateHttpConnectionManager(httpConnectionManager: HttpConnectionMana
switch (httpConnectionManager.route_specifier) {
case 'rds':
if (!httpConnectionManager.rds?.config_source?.ads && !httpConnectionManager.rds?.config_source?.self) {
return false;
errors.push('rds.config_source.ads and rds.config_source.self both unset');
}
break;
case 'route_config':
if (!RouteConfigurationResourceType.get().validateResource(httpConnectionManager.route_config!)) {
return false;
case 'route_config': {
const routeConfigValidationResult = RouteConfigurationResourceType.get().validateResource(httpConnectionManager.route_config!);
if (!routeConfigValidationResult.valid) {
errors.push(...routeConfigValidationResult.errors.map(error => `route_config: ${error}`));
}
break;
default: return false;
}
default:
errors.push(`unexpected route_specifier ${httpConnectionManager.route_specifier}`);
}
return true;
return errors;
}
function validateFilterChain(context: XdsDecodeContext, filterChain: FilterChain__Output): boolean {
if (filterChain.filters.length !== 1) {
return false;
/**
* @param context
* @param transportSocket A list of validation errors, if there are any. An empty list indicates success
*/
function validateTransportSocket(context: XdsDecodeContext, transportSocket: TransportSocket__Output): string[] {
const errors: string[] = []
if (transportSocket.name !== 'envoy.transport_sockets.tls') {
errors.push(`Unexpected transport_socket.name: ${transportSocket.name}`);
}
if (filterChain.filters[0].typed_config?.type_url !== HTTP_CONNECTION_MANGER_TYPE_URL) {
return false;
if (!transportSocket.typed_config) {
errors.push('transport_socket.typed_config missing');
return errors;
}
const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, filterChain.filters[0].typed_config.value);
if (!validateHttpConnectionManager(httpConnectionManager)) {
return false;
if (transportSocket.typed_config.type_url !== DOWNSTREAM_TLS_CONTEXT_TYPE_URL) {
errors.push(`Unexpected transport_socket.typed_config.type_url: ${transportSocket.typed_config.type_url}`);
return errors;
}
const downstreamTlsContext = decodeSingleResource(DOWNSTREAM_TLS_CONTEXT_TYPE_URL, transportSocket.typed_config.value);
if (downstreamTlsContext.require_sni?.value) {
errors.push(`DownstreamTlsContext.require_sni set`);
}
if (downstreamTlsContext.ocsp_staple_policy !== 'LENIENT_STAPLING') {
errors.push(`Unsupported DownstreamTlsContext.ocsp_staple_policy: ${downstreamTlsContext.ocsp_staple_policy}`);
}
if (!downstreamTlsContext.common_tls_context) {
errors.push('Missing DownstreamTlsContext.common_tls_context');
return errors;
}
const commonTlsContext = downstreamTlsContext.common_tls_context;
let validationContext: CertificateValidationContext__Output | null = null;
switch (commonTlsContext.validation_context_type) {
case 'validation_context_sds_secret_config':
errors.push('Unexpected DownstreamTlsContext.common_tls_context.validation_context_sds_secret_config');
break;
case 'validation_context':
if (!commonTlsContext.validation_context) {
errors.push('Empty DownstreamTlsContext.common_tls_context.validation_context');
break;
}
validationContext = commonTlsContext.validation_context;
break;
case 'combined_validation_context':
if (!commonTlsContext.combined_validation_context) {
errors.push('Empty DownstreamTlsContext.common_tls_context.combined_validation_context')
break;
}
validationContext = commonTlsContext.combined_validation_context.default_validation_context;
break;
default:
errors.push(`Unsupported DownstreamTlsContext.common_tls_context.validation_context_type: ${commonTlsContext.validation_context_type}`);
}
if (downstreamTlsContext.require_client_certificate && !validationContext) {
errors.push('DownstreamTlsContext.require_client_certificate set without any validationContext');
}
if (validationContext) {
if (validationContext.ca_certificate_provider_instance && !(validationContext.ca_certificate_provider_instance.instance_name in context.bootstrap.certificateProviders)) {
errors.push(`Unmatched CertificateValidationContext.ca_certificate_provider.instance_name: ${validationContext.ca_certificate_provider_instance.instance_name}`);
}
if (validationContext.verify_certificate_spki.length > 0) {
errors.push('CertificateValidationContext.verify_certificate_spki populated');
}
if (validationContext.verify_certificate_hash.length > 0) {
errors.push('CertificateValidationContext.verify_certificate_hash populated');
}
if (validationContext.require_signed_certificate_timestamp) {
errors.push('CertificateValidationContext.require_signed_certificate_timestamp set');
}
if (validationContext.crl) {
errors.push('CertificateValidationContext.crl set');
}
if (validationContext.custom_validator_config) {
errors.push('CertificateValidationContext.custom_validator_config set');
}
}
if (commonTlsContext.tls_certificate_provider_instance) {
if (!(commonTlsContext.tls_certificate_provider_instance.instance_name in context.bootstrap.certificateProviders)) {
errors.push(`Unmatched DownstreamTlsContext.tls_certificate_provider_instance.instance_name: ${commonTlsContext.tls_certificate_provider_instance.instance_name}`);
}
} else {
errors.push('DownstreamTlsContext.common_tls_context.tls_certificate_provider_instance');
}
if (commonTlsContext.tls_params) {
errors.push('DownstreamTlsContext.common_tls_context.tls_params set');
}
if (commonTlsContext.custom_handshaker) {
errors.push('DownstreamTlsContext.common_tls_context.custom_handshaker set');
}
return errors;
}
/**
* @param context
* @param filterChain
* @returns A list of validation errors, if there are any. An empty list indicates success
*/
function validateFilterChain(context: XdsDecodeContext, filterChain: FilterChain__Output): string[] {
const errors: string[] = [];
if (filterChain.filters.length === 1) {
if (filterChain.filters[0].typed_config?.type_url === HTTP_CONNECTION_MANGER_TYPE_URL) {
const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, filterChain.filters[0].typed_config.value);
errors.push(...validateHttpConnectionManager(httpConnectionManager).map(error => `filters[0].typed_config: ${error}`));
} else {
errors.push(`Unexpected value of filters[0].typed_config.type_url: ${filterChain.filters[0].typed_config?.type_url}`);
}
} else {
errors.push(`Incorrect filters length: ${filterChain.filters.length}`);
}
if (filterChain.transport_socket) {
const transportSocket = filterChain.transport_socket;
if (transportSocket.name !== 'envoy.transport_sockets.tls') {
trace('Wrong transportSocket.name');
return false;
}
if (!transportSocket.typed_config) {
trace('No typed_config');
return false;
}
if (transportSocket.typed_config?.type_url !== DOWNSTREAM_TLS_CONTEXT_TYPE_URL) {
trace(`Wrong typed_config type_url: ${transportSocket.typed_config?.type_url}`);
return false;
}
const downstreamTlsContext = decodeSingleResource(DOWNSTREAM_TLS_CONTEXT_TYPE_URL, transportSocket.typed_config.value);
if (!downstreamTlsContext.common_tls_context) {
trace('No common_tls_context');
return false;
}
const commonTlsContext = downstreamTlsContext.common_tls_context;
if (!commonTlsContext.tls_certificate_provider_instance) {
trace('No tls_certificate_provider_instance');
return false;
}
if (!(commonTlsContext.tls_certificate_provider_instance.instance_name in context.bootstrap.certificateProviders)) {
trace('Unmatched tls_certificate_provider_instance instance_name');
return false;
}
let validationContext: CertificateValidationContext__Output | null;
switch (commonTlsContext.validation_context_type) {
case 'validation_context_sds_secret_config':
trace('Unexpected validation_context_sds_secret_config')
return false;
case 'validation_context':
if (!commonTlsContext.validation_context) {
trace('Missing validation_context');
return false;
}
validationContext = commonTlsContext.validation_context;
break;
case 'combined_validation_context':
if (!commonTlsContext.combined_validation_context) {
trace('Missing combined_validation_context')
return false;
}
validationContext = commonTlsContext.combined_validation_context.default_validation_context;
break;
default:
return false;
}
if (validationContext?.ca_certificate_provider_instance && !(validationContext.ca_certificate_provider_instance.instance_name in context.bootstrap.certificateProviders)) {
trace('Unmatched validationContext instance_name');
return false;
}
if (downstreamTlsContext.require_client_certificate && !validationContext) {
trace('require_client_certificate set without validationContext');
return false;
}
if (validationContext && validationContext.verify_certificate_spki.length > 0) {
return false;
}
if (validationContext && validationContext.verify_certificate_hash.length > 0) {
return false;
}
if (validationContext?.require_signed_certificate_timestamp) {
return false;
}
if (validationContext?.crl) {
return false;
}
if (validationContext?.custom_validator_config) {
return false;
}
if (commonTlsContext.tls_params) {
trace('tls_params set');
return false;
}
if (commonTlsContext.custom_handshaker) {
trace('custom_handshaker set');
return false;
}
if (downstreamTlsContext.require_sni?.value) {
trace('require_sni set');
return false;
}
if (downstreamTlsContext.ocsp_staple_policy !== 'LENIENT_STAPLING') {
trace('Unexpected ocsp_staple_policy');
return false;
}
errors.push(...validateTransportSocket(context, filterChain.transport_socket));
}
return true;
return errors;
}
export class ListenerResourceType extends XdsResourceType {
@ -246,24 +260,22 @@ export class ListenerResourceType extends XdsResourceType {
return 'envoy.config.listener.v3.Listener';
}
private validateResource(context: XdsDecodeContext, message: Listener__Output): Listener__Output | null {
private validateResource(context: XdsDecodeContext, message: Listener__Output): ValidationResult<Listener__Output> {
const errors: string[] = [];
if (
!(
message.api_listener?.api_listener &&
message.api_listener.api_listener.type_url === HTTP_CONNECTION_MANGER_TYPE_URL
)
message.api_listener?.api_listener &&
message.api_listener.api_listener.type_url === HTTP_CONNECTION_MANGER_TYPE_URL
) {
return null;
}
const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, message.api_listener!.api_listener.value);
if (!validateHttpConnectionManager(httpConnectionManager)) {
return null;
const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, message.api_listener!.api_listener.value);
errors.push(...validateHttpConnectionManager(httpConnectionManager).map(error => `api_listener.api_listener: ${error}`));
} else {
errors.push(`api_listener.api_listener.type_url != ${HTTP_CONNECTION_MANGER_TYPE_URL}`);
}
if (message.listener_filters.length > 0) {
return null;
errors.push('listener_filters populated');
}
if (message.use_original_dst?.value === true) {
return null;
errors.push('use_original_dst.value == true');
}
const seenMatches: NormalizedFilterChainMatch[] = [];
for (const filterChain of message.filter_chains) {
@ -271,19 +283,27 @@ export class ListenerResourceType extends XdsResourceType {
const normalizedMatches = normalizeFilterChainMatch(filterChain.filter_chain_match);
for (const match of normalizedMatches) {
if (seenMatches.some(prevMatch => normalizedFilterChainMatchEquals(match, prevMatch))) {
return null;
errors.push(`duplicate filter_chain_match entry in filter chain ${filterChain.name}`);
}
seenMatches.push(match);
}
}
if (!validateFilterChain(context, filterChain)) {
return null;
}
errors.push(...validateFilterChain(context, filterChain).map(error => `filter_chains[${filterChain.name}]: ${error}`));
}
if (message.default_filter_chain && !validateFilterChain(context, message.default_filter_chain)) {
return null;
if (message.default_filter_chain) {
errors.push(...validateFilterChain(context, message.default_filter_chain).map(error => `default_filter_chain: ${error}`));
}
if (errors.length === 0) {
return {
valid: true,
result: message
};
} else {
return {
valid: false,
errors
};
}
return message;
}
decode(context: XdsDecodeContext, resource: Any__Output): XdsDecodeResult {
@ -294,16 +314,16 @@ export class ListenerResourceType extends XdsResourceType {
}
const message = decodeSingleResource(LDS_TYPE_URL, resource.value);
trace('Decoded raw resource of type ' + LDS_TYPE_URL + ': ' + JSON.stringify(message, (key, value) => (value && value.type === 'Buffer' && Array.isArray(value.data)) ? (value.data as Number[]).map(n => n.toString(16)).join('') : value, 2));
const validatedMessage = this.validateResource(context, message);
if (validatedMessage) {
const validationResult = this.validateResource(context, message);
if (validationResult.valid) {
return {
name: validatedMessage.name,
value: validatedMessage
name: validationResult.result.name,
value: validationResult.result
};
} else {
return {
name: message.name,
error: 'Listener message validation failed'
error: `Listener message validation failed: [${validationResult.errors}]`
};
}
}

View File

@ -24,7 +24,7 @@ import { Duration__Output } from "../generated/google/protobuf/Duration";
import { validateOverrideFilter } from "../http-filter";
import { RDS_TYPE_URL, decodeSingleResource } from "../resources";
import { Watcher, XdsClient } from "../xds-client";
import { XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type";
import { ValidationResult, XdsDecodeContext, XdsDecodeResult, XdsResourceType } from "./xds-resource-type";
const TRACER_NAME = 'xds_client';
function trace(text: string): void {
@ -66,100 +66,114 @@ export class RouteConfigurationResourceType extends XdsResourceType {
return 'envoy.config.route.v3.RouteConfiguration';
}
private validateRetryPolicy(policy: RetryPolicy__Output | null): boolean {
/**
* @param policy
* @returns A list of validation errors, if there are any. An empty list indicates success
*/
private validateRetryPolicy(policy: RetryPolicy__Output | null): string[] {
if (policy === null) {
return true;
return [];
}
const errors: string[] = [];
const numRetries = policy.num_retries?.value ?? 1
if (numRetries < 1) {
return false;
errors.push(`Invalid policy.num_retries.value: ${numRetries}`);
}
if (policy.retry_back_off) {
if (!policy.retry_back_off.base_interval) {
return false;
}
const baseInterval = durationToMs(policy.retry_back_off.base_interval)!;
const maxInterval = durationToMs(policy.retry_back_off.max_interval) ?? (10 * baseInterval);
if (!(maxInterval >= baseInterval) && (baseInterval > 0)) {
return false;
if (policy.retry_back_off.base_interval) {
const baseInterval = durationToMs(policy.retry_back_off.base_interval)!;
const maxInterval = durationToMs(policy.retry_back_off.max_interval) ?? (10 * baseInterval);
if (baseInterval <= 0) {
errors.push(`Invalid retry_back_off.base_interval: ${JSON.stringify(policy.retry_back_off.base_interval)}`);
}
if (maxInterval < baseInterval) {
errors.push(`retry_back_off.max_interval < retry_back_off.base_interval: ${JSON.stringify(policy.retry_back_off.max_interval)} vs ${JSON.stringify(policy.retry_back_off.base_interval)}`);
}
} else {
errors.push('retry_back_off.base_interval unset');
}
}
return true;
return errors;
}
public validateResource(message: RouteConfiguration__Output): RouteConfiguration__Output | null {
public validateResource(message: RouteConfiguration__Output): ValidationResult<RouteConfiguration__Output> {
const errors: string[] = [];
// https://github.com/grpc/proposal/blob/master/A28-xds-traffic-splitting-and-routing.md#response-validation
for (const virtualHost of message.virtual_hosts) {
const errorPrefix = `virtual_hosts[${virtualHost.name}]`;
for (const domainPattern of virtualHost.domains) {
const starIndex = domainPattern.indexOf('*');
const lastStarIndex = domainPattern.lastIndexOf('*');
// A domain pattern can have at most one wildcard *
if (starIndex !== lastStarIndex) {
return null;
errors.push(`${errorPrefix}: domains entry has multiple wildcards: ${domainPattern}`);
}
// A wildcard * can either be absent or at the beginning or end of the pattern
if (!(starIndex === -1 || starIndex === 0 || starIndex === domainPattern.length - 1)) {
return null;
errors.push(`${errorPrefix}: domains entry has wildcard in the middle: ${domainPattern}`);
}
}
if (EXPERIMENTAL_FAULT_INJECTION) {
for (const filterConfig of Object.values(virtualHost.typed_per_filter_config ?? {})) {
if (!validateOverrideFilter(filterConfig)) {
return null;
errors.push(`${errorPrefix}: typed_per_filter_config validation failed for type_url: ${filterConfig.type_url}`);
}
}
}
if (EXPERIMENTAL_RETRY) {
if (!this.validateRetryPolicy(virtualHost.retry_policy)) {
return null;
}
errors.push(...this.validateRetryPolicy(virtualHost.retry_policy).map(error => `${errorPrefix}.retry_policy: ${error}`));
}
for (const route of virtualHost.routes) {
const routeErrorPrefix = `${errorPrefix}.routes[${route.name}]`;
const match = route.match;
if (!match) {
return null;
}
if (SUPPORTED_PATH_SPECIFIERS.indexOf(match.path_specifier) < 0) {
return null;
}
for (const headers of match.headers) {
if (SUPPPORTED_HEADER_MATCH_SPECIFIERS.indexOf(headers.header_match_specifier) < 0) {
return null;
if (match) {
if (SUPPORTED_PATH_SPECIFIERS.indexOf(match.path_specifier) < 0) {
errors.push(`${routeErrorPrefix}.match: unsupported path_specifier: ${match.path_specifier}`);
}
for (const headers of match.headers) {
if (SUPPPORTED_HEADER_MATCH_SPECIFIERS.indexOf(headers.header_match_specifier) < 0) {
errors.push(`${routeErrorPrefix}.match.headers[${headers.name}]: unsupported header_match_specifier: ${headers.header_match_specifier}`);
}
}
} else {
errors.push(`${routeErrorPrefix}.match unset`);
}
switch (route.action) {
case 'route': {
if (route.action !== 'route') {
return null;
if ((route.route === undefined) || (route.route === null)) {
errors.push(`${routeErrorPrefix}.route unset`);
break;
}
if ((route.route === undefined) || (route.route === null) || SUPPORTED_CLUSTER_SPECIFIERS.indexOf(route.route.cluster_specifier) < 0) {
return null;
if (SUPPORTED_CLUSTER_SPECIFIERS.indexOf(route.route.cluster_specifier) < 0) {
errors.push(`${routeErrorPrefix}: unsupported route.cluster_specifier: ${route.route.cluster_specifier}`);
}
if (EXPERIMENTAL_FAULT_INJECTION) {
for (const [name, filterConfig] of Object.entries(route.typed_per_filter_config ?? {})) {
if (!validateOverrideFilter(filterConfig)) {
return null;
errors.push(`${routeErrorPrefix}.typed_per_filter_config[${name}] validation failed`);
}
}
}
if (EXPERIMENTAL_RETRY) {
if (!this.validateRetryPolicy(route.route.retry_policy)) {
return null;
}
errors.push(...this.validateRetryPolicy(route.route.retry_policy).map(error => `${routeErrorPrefix}.route.retry_policy: ${error}`));
}
if (route.route!.cluster_specifier === 'weighted_clusters') {
let weightSum = 0;
for (const clusterWeight of route.route.weighted_clusters!.clusters) {
weightSum += clusterWeight.weight?.value ?? 0;
}
if (weightSum === 0 || weightSum > UINT32_MAX) {
return null;
if (weightSum === 0) {
errors.push(`${routeErrorPrefix}.route.weighted_clusters sum of weights is 0`);
}
if (weightSum > UINT32_MAX) {
errors.push(`${routeErrorPrefix}.route.weighted_clusters sum of weights is greater than UINT32_MAX`);
}
if (EXPERIMENTAL_FAULT_INJECTION) {
for (const weightedCluster of route.route!.weighted_clusters!.clusters) {
for (const filterConfig of Object.values(weightedCluster.typed_per_filter_config ?? {})) {
for (const [name, filterConfig] of Object.entries(weightedCluster.typed_per_filter_config ?? {})) {
if (!validateOverrideFilter(filterConfig)) {
return null;
errors.push(`${routeErrorPrefix}.route.weighted_clusters.clusters[${weightedCluster.name}].typed_per_filter_config[${name}] validation failed`);
}
}
}
@ -170,11 +184,21 @@ export class RouteConfigurationResourceType extends XdsResourceType {
case 'non_forwarding_action':
continue;
default:
return null;
errors.push(`${routeErrorPrefix}: unsupported action: ${route.action}`);
}
}
}
return message;
if (errors.length === 0) {
return {
valid: true,
result: message
};
} else {
return {
valid: false,
errors
}
}
}
decode(context: XdsDecodeContext, resource: Any__Output): XdsDecodeResult {
@ -185,16 +209,16 @@ export class RouteConfigurationResourceType extends XdsResourceType {
}
const message = decodeSingleResource(RDS_TYPE_URL, resource.value);
trace('Decoded raw resource of type ' + RDS_TYPE_URL + ': ' + JSON.stringify(message, undefined, 2));
const validatedMessage = this.validateResource(message);
if (validatedMessage) {
const validationResult = this.validateResource(message);
if (validationResult.valid) {
return {
name: validatedMessage.name,
value: validatedMessage
name: validationResult.result.name,
value: validationResult.result
};
} else {
return {
name: message.name,
error: 'Route configuration message validation failed'
error: `RouteConfiguration message validation failed: [${validationResult.errors}]`
};
}
}

View File

@ -69,6 +69,18 @@ function deepEqual(value1: ValueType, value2: ValueType): boolean {
return false;
}
export interface ValidationSuccess<T> {
valid: true;
result: T;
}
export interface ValidationFailure {
valid: false;
errors: string[]
}
export type ValidationResult<T> = ValidationSuccess<T> | ValidationFailure;
export abstract class XdsResourceType {
/**
* The type URL as used in xdstp: names

View File

@ -1,6 +1,6 @@
{
"name": "grpc-tools",
"version": "1.12.4",
"version": "1.13.0",
"author": "Google Inc.",
"description": "Tools for developing with gRPC on Node.js",
"homepage": "https://grpc.io/",