mirror of https://github.com/grpc/grpc-node.git
Merge branch 'master' into grpc-js-xds_security_tests
This commit is contained in:
commit
e5fa6b7c05
|
@ -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
|
||||
|
|
|
@ -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}]`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}]`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}]`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}]`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/",
|
||||
|
|
Loading…
Reference in New Issue