pkg/config/validation/validation.go (3,134 lines of code) (raw):

// Copyright Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package validation import ( "encoding/json" "errors" "fmt" "net" "net/http" "net/url" "path" "regexp" "strconv" "strings" "time" ) import ( udpaa "github.com/cncf/xds/go/udpa/annotations" "github.com/envoyproxy/go-control-plane/pkg/wellknown" "github.com/hashicorp/go-multierror" "github.com/lestrrat-go/jwx/jwk" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/descriptorpb" any "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/durationpb" "istio.io/api/annotation" extensions "istio.io/api/extensions/v1alpha1" meshconfig "istio.io/api/mesh/v1alpha1" networking "istio.io/api/networking/v1alpha3" networkingv1beta1 "istio.io/api/networking/v1beta1" security_beta "istio.io/api/security/v1beta1" telemetry "istio.io/api/telemetry/v1alpha1" type_beta "istio.io/api/type/v1beta1" "istio.io/pkg/log" ) import ( "github.com/apache/dubbo-go-pixiu/pilot/pkg/features" "github.com/apache/dubbo-go-pixiu/pilot/pkg/util/constant" "github.com/apache/dubbo-go-pixiu/pkg/config" "github.com/apache/dubbo-go-pixiu/pkg/config/constants" "github.com/apache/dubbo-go-pixiu/pkg/config/gateway" "github.com/apache/dubbo-go-pixiu/pkg/config/host" "github.com/apache/dubbo-go-pixiu/pkg/config/labels" "github.com/apache/dubbo-go-pixiu/pkg/config/protocol" "github.com/apache/dubbo-go-pixiu/pkg/config/security" "github.com/apache/dubbo-go-pixiu/pkg/config/visibility" "github.com/apache/dubbo-go-pixiu/pkg/config/xds" "github.com/apache/dubbo-go-pixiu/pkg/kube/apimirror" "github.com/apache/dubbo-go-pixiu/pkg/util/protomarshal" "github.com/apache/dubbo-go-pixiu/pkg/util/sets" ) // Constants for duration fields const ( // nolint: revive connectTimeoutMax = time.Second * 30 // nolint: revive connectTimeoutMin = time.Millisecond drainTimeMax = time.Hour parentShutdownTimeMax = time.Hour // UnixAddressPrefix is the prefix used to indicate an address is for a Unix Domain socket. It is used in // ServiceEntry.Endpoint.Address message. UnixAddressPrefix = "unix://" matchExact = "exact:" matchPrefix = "prefix:" ) const ( regionIndex int = iota zoneIndex subZoneIndex ) var ( // envoy supported retry on header values supportedRetryOnPolicies = map[string]bool{ // 'x-envoy-retry-on' supported policies: // https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter.html#x-envoy-retry-on "5xx": true, "gateway-error": true, "reset": true, "connect-failure": true, "retriable-4xx": true, "refused-stream": true, "retriable-status-codes": true, "retriable-headers": true, "envoy-ratelimited": true, // 'x-envoy-retry-grpc-on' supported policies: // https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#x-envoy-retry-grpc-on "cancelled": true, "deadline-exceeded": true, "internal": true, "resource-exhausted": true, "unavailable": true, } // golang supported methods: https://golang.org/src/net/http/method.go supportedMethods = map[string]bool{ http.MethodGet: true, http.MethodHead: true, http.MethodPost: true, http.MethodPut: true, http.MethodPatch: true, http.MethodDelete: true, http.MethodConnect: true, http.MethodOptions: true, http.MethodTrace: true, } scope = log.RegisterScope("validation", "CRD validation debugging", 0) // EmptyValidate is a Validate that does nothing and returns no error. EmptyValidate = registerValidateFunc("EmptyValidate", func(config.Config) (Warning, error) { return nil, nil }) validateFuncs = make(map[string]ValidateFunc) ) type Warning error // Validation holds errors and warnings. They can be joined with additional errors by called appendValidation type Validation struct { Err error Warning Warning } type AnalysisAwareError struct { Type string Msg string Parameters []interface{} } // OverlappingMatchValidationForHTTPRoute holds necessary information from virtualservice // to do such overlapping match validation type OverlappingMatchValidationForHTTPRoute struct { RouteStr string MatchStr string Prefix string MatchPort uint32 MatchMethod string MatchAuthority string MatchHeaders map[string]string MatchQueryParams map[string]string MatchNonHeaders map[string]string } var _ error = Validation{} // WrapError turns an error into a Validation func WrapError(e error) Validation { return Validation{Err: e} } // WrapWarning turns an error into a Validation as a warning func WrapWarning(e error) Validation { return Validation{Warning: e} } // Warningf formats according to a format specifier and returns the string as a // value that satisfies error. Like Errorf, but for warnings. func Warningf(format string, a ...interface{}) Validation { return WrapWarning(fmt.Errorf(format, a...)) } func (v Validation) Unwrap() (Warning, error) { return v.Warning, v.Err } func (v Validation) Error() string { if v.Err == nil { return "" } return v.Err.Error() } // ValidateFunc defines a validation func for an API proto. type ValidateFunc func(config config.Config) (Warning, error) // IsValidateFunc indicates whether there is a validation function with the given name. func IsValidateFunc(name string) bool { return GetValidateFunc(name) != nil } // GetValidateFunc returns the validation function with the given name, or null if it does not exist. func GetValidateFunc(name string) ValidateFunc { return validateFuncs[name] } func registerValidateFunc(name string, f ValidateFunc) ValidateFunc { // Wrap the original validate function with an extra validate function for the annotation "istio.io/dry-run". validate := validateAnnotationDryRun(f) validateFuncs[name] = validate return validate } func validateAnnotationDryRun(f ValidateFunc) ValidateFunc { return func(config config.Config) (Warning, error) { _, isAuthz := config.Spec.(*security_beta.AuthorizationPolicy) // Only the AuthorizationPolicy supports the annotation "istio.io/dry-run". if err := checkDryRunAnnotation(config, isAuthz); err != nil { return nil, err } return f(config) } } func checkDryRunAnnotation(cfg config.Config, allowed bool) error { if val, found := cfg.Annotations[annotation.IoIstioDryRun.Name]; found { if !allowed { return fmt.Errorf("%s/%s has unsupported annotation %s, please remove the annotation", cfg.Namespace, cfg.Name, annotation.IoIstioDryRun.Name) } if spec, ok := cfg.Spec.(*security_beta.AuthorizationPolicy); ok { switch spec.Action { case security_beta.AuthorizationPolicy_ALLOW, security_beta.AuthorizationPolicy_DENY: if _, err := strconv.ParseBool(val); err != nil { return fmt.Errorf("%s/%s has annotation %s with invalid value (%s): %v", cfg.Namespace, cfg.Name, annotation.IoIstioDryRun.Name, val, err) } default: return fmt.Errorf("the annotation %s currently only supports action ALLOW/DENY, found action %v in %s/%s", annotation.IoIstioDryRun.Name, spec.Action, cfg.Namespace, cfg.Name) } } } return nil } // ValidatePort checks that the network port is in range func ValidatePort(port int) error { if 1 <= port && port <= 65535 { return nil } return fmt.Errorf("port number %d must be in the range 1..65535", port) } // ValidateFQDN checks a fully-qualified domain name func ValidateFQDN(fqdn string) error { if err := checkDNS1123Preconditions(fqdn); err != nil { return err } return validateDNS1123Labels(fqdn) } // ValidateWildcardDomain checks that a domain is a valid FQDN, but also allows wildcard prefixes. func ValidateWildcardDomain(domain string) error { if err := checkDNS1123Preconditions(domain); err != nil { return err } // We only allow wildcards in the first label; split off the first label (parts[0]) from the rest of the host (parts[1]) parts := strings.SplitN(domain, ".", 2) if !labels.IsWildcardDNS1123Label(parts[0]) { return fmt.Errorf("domain name %q invalid (label %q invalid)", domain, parts[0]) } else if len(parts) > 1 { return validateDNS1123Labels(parts[1]) } return nil } // encapsulates DNS 1123 checks common to both wildcarded hosts and FQDNs func checkDNS1123Preconditions(name string) error { if len(name) > 255 { return fmt.Errorf("domain name %q too long (max 255)", name) } if len(name) == 0 { return fmt.Errorf("empty domain name not allowed") } return nil } func validateDNS1123Labels(domain string) error { parts := strings.Split(domain, ".") topLevelDomain := parts[len(parts)-1] if _, err := strconv.Atoi(topLevelDomain); err == nil { return fmt.Errorf("domain name %q invalid (top level domain %q cannot be all-numeric)", domain, topLevelDomain) } for i, label := range parts { // Allow the last part to be empty, for unambiguous names like `istio.io.` if i == len(parts)-1 && label == "" { return nil } if !labels.IsDNS1123Label(label) { return fmt.Errorf("domain name %q invalid (label %q invalid)", domain, label) } } return nil } // validate the trust domain format func ValidateTrustDomain(domain string) error { if len(domain) == 0 { return fmt.Errorf("empty domain name not allowed") } parts := strings.Split(domain, ".") for i, label := range parts { // Allow the last part to be empty, for unambiguous names like `istio.io.` if i == len(parts)-1 && label == "" { return nil } if !labels.IsDNS1123Label(label) { return fmt.Errorf("trust domain name %q invalid", domain) } } return nil } // ValidateHTTPHeaderName validates a header name func ValidateHTTPHeaderName(name string) error { if name == "" { return fmt.Errorf("header name cannot be empty") } return nil } // ValidateHTTPHeaderWithAuthorityOperationName validates a header name when used to add/set in request. func ValidateHTTPHeaderWithAuthorityOperationName(name string) error { if name == "" { return fmt.Errorf("header name cannot be empty") } // Authority header is validated later if isInternalHeader(name) && !isAuthorityHeader(name) { return fmt.Errorf(`invalid header %q: header cannot have ":" prefix`, name) } return nil } // ValidateHTTPHeaderWithHostOperationName validates a header name when used to destination specific add/set in request. // TODO(https://github.com/envoyproxy/envoy/issues/16775) merge with ValidateHTTPHeaderWithAuthorityOperationName func ValidateHTTPHeaderWithHostOperationName(name string) error { if name == "" { return fmt.Errorf("header name cannot be empty") } // Authority header is validated later if isInternalHeader(name) && !strings.EqualFold(name, "host") { return fmt.Errorf(`invalid header %q: header cannot have ":" prefix`, name) } return nil } // ValidateHTTPHeaderOperationName validates a header name when used to remove from request or modify response. func ValidateHTTPHeaderOperationName(name string) error { if name == "" { return fmt.Errorf("header name cannot be empty") } if strings.EqualFold(name, "host") { return fmt.Errorf(`invalid header %q: cannot set Host header`, name) } if isInternalHeader(name) { return fmt.Errorf(`invalid header %q: header cannot have ":" prefix`, name) } return nil } // ValidateHTTPHeaderValue validates a header value for Envoy // Valid: "foo", "%HOSTNAME%", "100%%", "prefix %HOSTNAME% suffix" // Invalid: "abc%123" // We don't try to check that what is inside the %% is one of Envoy recognized values, we just prevent invalid config. // See: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers.html#custom-request-response-headers func ValidateHTTPHeaderValue(value string) error { if strings.Count(value, "%")%2 != 0 { return errors.New("single % not allowed. Escape by doubling to %% or encase Envoy variable name in pair of %") } return nil } // ValidatePercent checks that percent is in range func ValidatePercent(val int32) error { if val < 0 || val > 100 { return fmt.Errorf("percentage %v is not in range 0..100", val) } return nil } // validatePercentage checks if the specified fractional percentage is valid. func validatePercentage(percentage *networking.Percent) error { if percentage != nil { if percentage.Value < 0.0 || percentage.Value > 100.0 || (percentage.Value > 0.0 && percentage.Value < 0.0001) { return fmt.Errorf("percentage %v is neither 0.0, nor in range [0.0001, 100.0]", percentage.Value) } } return nil } // ValidateIPSubnet checks that a string is in "CIDR notation" or "Dot-decimal notation" func ValidateIPSubnet(subnet string) error { // We expect a string in "CIDR notation" or "Dot-decimal notation" // E.g., a.b.c.d/xx form or just a.b.c.d or 2001:1::1/64 if strings.Count(subnet, "/") == 1 { // We expect a string in "CIDR notation", i.e. a.b.c.d/xx or 2001:1::1/64 form ip, _, err := net.ParseCIDR(subnet) if err != nil { return fmt.Errorf("%v is not a valid CIDR block", subnet) } if ip.To4() == nil && ip.To16() == nil { return fmt.Errorf("%v is not a valid IPv4 or IPv6 address", subnet) } return nil } return ValidateIPAddress(subnet) } // ValidateIPAddress validates that a string in "CIDR notation" or "Dot-decimal notation" func ValidateIPAddress(addr string) error { ip := net.ParseIP(addr) if ip == nil { return fmt.Errorf("%v is not a valid IP", addr) } return nil } // ValidateUnixAddress validates that the string is a valid unix domain socket path. func ValidateUnixAddress(addr string) error { if len(addr) == 0 { return errors.New("unix address must not be empty") } // Allow unix abstract domain sockets whose names start with @ if strings.HasPrefix(addr, "@") { return nil } // Note that we use path, not path/filepath even though a domain socket path is a file path. We don't want the // Pilot output to depend on which OS Pilot is run on, so we always use Unix-style forward slashes. if !path.IsAbs(addr) || strings.HasSuffix(addr, "/") { return fmt.Errorf("%s is not an absolute path to a file", addr) } return nil } // ValidateGateway checks gateway specifications var ValidateGateway = registerValidateFunc("ValidateGateway", func(cfg config.Config) (Warning, error) { name := cfg.Name v := Validation{} // Gateway name must conform to the DNS label format (no dots) if !labels.IsDNS1123Label(name) { v = appendValidation(v, fmt.Errorf("invalid gateway name: %q", name)) } value, ok := cfg.Spec.(*networking.Gateway) if !ok { v = appendValidation(v, fmt.Errorf("cannot cast to gateway: %#v", cfg.Spec)) return v.Unwrap() } if len(value.Servers) == 0 { v = appendValidation(v, fmt.Errorf("gateway must have at least one server")) } else { for _, server := range value.Servers { v = appendValidation(v, validateServer(server)) } } // Ensure unique port names portNames := make(map[string]bool) for _, s := range value.Servers { if s == nil { v = appendValidation(v, fmt.Errorf("server may not be nil")) continue } if s.Port != nil { if portNames[s.Port.Name] { v = appendValidation(v, fmt.Errorf("port names in servers must be unique: duplicate name %s", s.Port.Name)) } portNames[s.Port.Name] = true if !protocol.Parse(s.Port.Protocol).IsHTTP() && s.GetTls().GetHttpsRedirect() { v = appendValidation(v, WrapWarning(fmt.Errorf("tls.httpsRedirect should only be used with http servers"))) } } } return v.Unwrap() }) func validateServer(server *networking.Server) (v Validation) { if server == nil { return WrapError(fmt.Errorf("cannot have nil server")) } if len(server.Hosts) == 0 { v = appendValidation(v, fmt.Errorf("server config must contain at least one host")) } else { for _, hostname := range server.Hosts { v = appendValidation(v, validateNamespaceSlashWildcardHostname(hostname, true)) } } portErr := validateServerPort(server.Port) if portErr != nil { v = appendValidation(v, portErr) } v = appendValidation(v, validateServerBind(server.Port, server.Bind)) v = appendValidation(v, validateTLSOptions(server.Tls)) // If port is HTTPS or TLS, make sure that server has TLS options if portErr == nil { p := protocol.Parse(server.Port.Protocol) if p.IsTLS() && server.Tls == nil { v = appendValidation(v, fmt.Errorf("server must have TLS settings for HTTPS/TLS protocols")) } else if !p.IsTLS() && server.Tls != nil { // only tls redirect is allowed if this is a HTTP server if p.IsHTTP() { if !gateway.IsPassThroughServer(server) || server.Tls.CaCertificates != "" || server.Tls.PrivateKey != "" || server.Tls.ServerCertificate != "" { v = appendValidation(v, fmt.Errorf("server cannot have TLS settings for plain text HTTP ports")) } } else { v = appendValidation(v, fmt.Errorf("server cannot have TLS settings for non HTTPS/TLS ports")) } } } return v } func validateServerPort(port *networking.Port) (errs error) { if port == nil { return appendErrors(errs, fmt.Errorf("port is required")) } if protocol.Parse(port.Protocol) == protocol.Unsupported { errs = appendErrors(errs, fmt.Errorf("invalid protocol %q, supported protocols are HTTP, HTTP2, GRPC, GRPC-WEB, MONGO, REDIS, MYSQL, TCP", port.Protocol)) } if port.Number > 0 { errs = appendErrors(errs, ValidatePort(int(port.Number))) } if port.Name == "" { errs = appendErrors(errs, fmt.Errorf("port name must be set: %v", port)) } return } func validateServerBind(port *networking.Port, bind string) (errs error) { if strings.HasPrefix(bind, UnixAddressPrefix) { errs = appendErrors(errs, ValidateUnixAddress(strings.TrimPrefix(bind, UnixAddressPrefix))) if port != nil && port.Number != 0 { errs = appendErrors(errs, fmt.Errorf("port number must be 0 for unix domain socket: %v", port)) } } else if len(bind) != 0 { errs = appendErrors(errs, ValidateIPAddress(bind)) } return } func validateTLSOptions(tls *networking.ServerTLSSettings) (v Validation) { if tls == nil { // no tls config at all is valid return } invalidCiphers := sets.New() validCiphers := sets.New() duplicateCiphers := sets.New() for _, cs := range tls.CipherSuites { if !security.IsValidCipherSuite(cs) { invalidCiphers.Insert(cs) } else { if !validCiphers.Contains(cs) { validCiphers.Insert(cs) } else { duplicateCiphers.Insert(cs) } } } if len(invalidCiphers) > 0 { v = appendWarningf(v, "ignoring invalid cipher suites: %v", invalidCiphers.SortedList()) } if len(duplicateCiphers) > 0 { v = appendWarningf(v, "ignoring duplicate cipher suites: %v", duplicateCiphers.SortedList()) } if tls.Mode == networking.ServerTLSSettings_ISTIO_MUTUAL { // ISTIO_MUTUAL TLS mode uses either SDS or default certificate mount paths // therefore, we should fail validation if other TLS fields are set if tls.ServerCertificate != "" { v = appendValidation(v, fmt.Errorf("ISTIO_MUTUAL TLS cannot have associated server certificate")) } if tls.PrivateKey != "" { v = appendValidation(v, fmt.Errorf("ISTIO_MUTUAL TLS cannot have associated private key")) } if tls.CaCertificates != "" { v = appendValidation(v, fmt.Errorf("ISTIO_MUTUAL TLS cannot have associated CA bundle")) } if tls.CredentialName != "" { if features.EnableLegacyIstioMutualCredentialName { // Legacy mode enabled, just warn v = appendWarningf(v, "ISTIO_MUTUAL TLS cannot have associated credentialName") } else { v = appendValidation(v, fmt.Errorf("ISTIO_MUTUAL TLS cannot have associated credentialName")) } } return } if tls.Mode == networking.ServerTLSSettings_PASSTHROUGH || tls.Mode == networking.ServerTLSSettings_AUTO_PASSTHROUGH { if tls.ServerCertificate != "" || tls.PrivateKey != "" || tls.CaCertificates != "" || tls.CredentialName != "" { // Warn for backwards compatibility v = appendWarningf(v, "%v mode does not use certificates, they will be ignored", tls.Mode) } } if (tls.Mode == networking.ServerTLSSettings_SIMPLE || tls.Mode == networking.ServerTLSSettings_MUTUAL) && tls.CredentialName != "" { // If tls mode is SIMPLE or MUTUAL, and CredentialName is specified, credentials are fetched // remotely. ServerCertificate and CaCertificates fields are not required. return } if tls.Mode == networking.ServerTLSSettings_SIMPLE { if tls.ServerCertificate == "" { v = appendValidation(v, fmt.Errorf("SIMPLE TLS requires a server certificate")) } if tls.PrivateKey == "" { v = appendValidation(v, fmt.Errorf("SIMPLE TLS requires a private key")) } } else if tls.Mode == networking.ServerTLSSettings_MUTUAL { if tls.ServerCertificate == "" { v = appendValidation(v, fmt.Errorf("MUTUAL TLS requires a server certificate")) } if tls.PrivateKey == "" { v = appendValidation(v, fmt.Errorf("MUTUAL TLS requires a private key")) } if tls.CaCertificates == "" { v = appendValidation(v, fmt.Errorf("MUTUAL TLS requires a client CA bundle")) } } return } // ValidateDestinationRule checks proxy policies var ValidateDestinationRule = registerValidateFunc("ValidateDestinationRule", func(cfg config.Config) (Warning, error) { rule, ok := cfg.Spec.(*networking.DestinationRule) if !ok { return nil, fmt.Errorf("cannot cast to destination rule") } v := Validation{} if features.EnableDestinationRuleInheritance { if rule.Host == "" { if rule.GetWorkloadSelector() != nil { v = appendValidation(v, fmt.Errorf("mesh/namespace destination rule cannot have workloadSelector configured")) } if len(rule.Subsets) != 0 { v = appendValidation(v, fmt.Errorf("mesh/namespace destination rule cannot have subsets")) } if len(rule.ExportTo) != 0 { v = appendValidation(v, fmt.Errorf("mesh/namespace destination rule cannot have exportTo configured")) } if rule.TrafficPolicy != nil && len(rule.TrafficPolicy.PortLevelSettings) != 0 { v = appendValidation(v, fmt.Errorf("mesh/namespace destination rule cannot have portLevelSettings configured")) } } else { v = appendValidation(v, ValidateWildcardDomain(rule.Host)) } } else { v = appendValidation(v, ValidateWildcardDomain(rule.Host)) } v = appendValidation(v, validateTrafficPolicy(rule.TrafficPolicy)) for _, subset := range rule.Subsets { if subset == nil { v = appendValidation(v, errors.New("subset may not be null")) continue } v = appendValidation(v, validateSubset(subset)) } v = appendValidation(v, validateExportTo(cfg.Namespace, rule.ExportTo, false, rule.GetWorkloadSelector() != nil)) v = appendValidation(v, validateWorkloadSelector(rule.GetWorkloadSelector())) return v.Unwrap() }) func validateExportTo(namespace string, exportTo []string, isServiceEntry bool, isDestinationRuleWithSelector bool) (errs error) { if len(exportTo) > 0 { // Make sure there are no duplicates exportToSet := sets.New() for _, e := range exportTo { key := e if visibility.Instance(e) == visibility.Private { // substitute this with the current namespace so that we // can check for duplicates like ., namespace key = namespace } if exportToSet.Contains(key) { if key != e { errs = appendErrors(errs, fmt.Errorf("duplicate entries in exportTo: . and current namespace %s", namespace)) } else { errs = appendErrors(errs, fmt.Errorf("duplicate entries in exportTo for entry %s", e)) } } else { // if this is a serviceEntry, allow ~ in exportTo as it can be used to create // a service that is not even visible within the local namespace to anyone other // than the proxies of that service. if isServiceEntry && visibility.Instance(e) == visibility.None { exportToSet.Insert(key) } else { if err := visibility.Instance(key).Validate(); err != nil { errs = appendErrors(errs, err) } else { exportToSet.Insert(key) } } } } // Make sure workloadSelector based destination rule does not use exportTo other than current namespace if isDestinationRuleWithSelector && !exportToSet.IsEmpty() { if exportToSet.Contains(namespace) { if len(exportToSet) > 1 { errs = appendErrors(errs, fmt.Errorf("destination rule with workload selector cannot have multiple entries in exportTo")) } } else { errs = appendErrors(errs, fmt.Errorf("destination rule with workload selector cannot have exportTo beyond current namespace")) } } // Make sure we have only one of . or * if exportToSet.Contains(string(visibility.Public)) { // make sure that there are no other entries in the exportTo // i.e. no point in saying ns1,ns2,*. Might as well say * if len(exportTo) > 1 { errs = appendErrors(errs, fmt.Errorf("cannot have both public (*) and non-public exportTo values for a resource")) } } // if this is a service entry, then we need to disallow * and ~ together. Or ~ and other namespaces if exportToSet.Contains(string(visibility.None)) { if len(exportTo) > 1 { errs = appendErrors(errs, fmt.Errorf("cannot export service entry to no one (~) and someone")) } } } return } func validateAlphaWorkloadSelector(selector *networking.WorkloadSelector) error { var errs error if selector != nil { for k, v := range selector.Labels { if k == "" { errs = appendErrors(errs, fmt.Errorf("empty key is not supported in selector: %q", fmt.Sprintf("%s=%s", k, v))) } if strings.Contains(k, "*") || strings.Contains(v, "*") { errs = appendErrors(errs, fmt.Errorf("wildcard is not supported in selector: %q", fmt.Sprintf("%s=%s", k, v))) } } } return errs } // ValidateEnvoyFilter checks envoy filter config supplied by user var ValidateEnvoyFilter = registerValidateFunc("ValidateEnvoyFilter", func(cfg config.Config) (Warning, error) { errs := Validation{} rule, ok := cfg.Spec.(*networking.EnvoyFilter) if !ok { return nil, fmt.Errorf("cannot cast to Envoy filter") } if err := validateAlphaWorkloadSelector(rule.WorkloadSelector); err != nil { return nil, err } for _, cp := range rule.ConfigPatches { if cp == nil { errs = appendValidation(errs, fmt.Errorf("Envoy filter: null config patch")) // nolint: stylecheck continue } if cp.ApplyTo == networking.EnvoyFilter_INVALID { errs = appendValidation(errs, fmt.Errorf("Envoy filter: missing applyTo")) // nolint: stylecheck continue } if cp.Patch == nil { errs = appendValidation(errs, fmt.Errorf("Envoy filter: missing patch")) // nolint: stylecheck continue } if cp.Patch.Operation == networking.EnvoyFilter_Patch_INVALID { errs = appendValidation(errs, fmt.Errorf("Envoy filter: missing patch operation")) // nolint: stylecheck continue } if cp.Patch.Operation != networking.EnvoyFilter_Patch_REMOVE && cp.Patch.Value == nil { errs = appendValidation(errs, fmt.Errorf("Envoy filter: missing patch value for non-remove operation")) // nolint: stylecheck continue } // ensure that the supplied regex for proxy version compiles if cp.Match != nil && cp.Match.Proxy != nil && cp.Match.Proxy.ProxyVersion != "" { if _, err := regexp.Compile(cp.Match.Proxy.ProxyVersion); err != nil { errs = appendValidation(errs, fmt.Errorf("Envoy filter: invalid regex for proxy version, [%v]", err)) // nolint: stylecheck continue } } // ensure that applyTo, match and patch all line up switch cp.ApplyTo { case networking.EnvoyFilter_LISTENER, networking.EnvoyFilter_FILTER_CHAIN, networking.EnvoyFilter_NETWORK_FILTER, networking.EnvoyFilter_HTTP_FILTER: if cp.Match != nil && cp.Match.ObjectTypes != nil { if cp.Match.GetListener() == nil { errs = appendValidation(errs, fmt.Errorf("Envoy filter: applyTo for listener class objects cannot have non listener match")) // nolint: stylecheck continue } listenerMatch := cp.Match.GetListener() if listenerMatch.FilterChain != nil { if listenerMatch.FilterChain.Filter != nil { if cp.ApplyTo == networking.EnvoyFilter_LISTENER || cp.ApplyTo == networking.EnvoyFilter_FILTER_CHAIN { // This would be an error but is a warning for backwards compatibility errs = appendValidation(errs, WrapWarning( fmt.Errorf("Envoy filter: filter match has no effect when used with %v", cp.ApplyTo))) // nolint: stylecheck } // filter names are required if network filter matches are being made if listenerMatch.FilterChain.Filter.Name == "" { errs = appendValidation(errs, fmt.Errorf("Envoy filter: filter match has no name to match on")) // nolint: stylecheck continue } else if listenerMatch.FilterChain.Filter.SubFilter != nil { // sub filter match is supported only for applyTo HTTP_FILTER if cp.ApplyTo != networking.EnvoyFilter_HTTP_FILTER { errs = appendValidation(errs, fmt.Errorf("Envoy filter: subfilter match can be used with applyTo HTTP_FILTER only")) // nolint: stylecheck continue } // sub filter match requires the network filter to match to envoy http connection manager if listenerMatch.FilterChain.Filter.Name != wellknown.HTTPConnectionManager && listenerMatch.FilterChain.Filter.Name != "envoy.http_connection_manager" { errs = appendValidation(errs, fmt.Errorf("Envoy filter: subfilter match requires filter match with %s", // nolint: stylecheck wellknown.HTTPConnectionManager)) continue } if listenerMatch.FilterChain.Filter.SubFilter.Name == "" { errs = appendValidation(errs, fmt.Errorf("Envoy filter: subfilter match has no name to match on")) // nolint: stylecheck continue } } errs = appendValidation(errs, validateListenerMatchName(listenerMatch.FilterChain.Filter.GetName())) errs = appendValidation(errs, validateListenerMatchName(listenerMatch.FilterChain.Filter.GetSubFilter().GetName())) } } } case networking.EnvoyFilter_ROUTE_CONFIGURATION, networking.EnvoyFilter_VIRTUAL_HOST, networking.EnvoyFilter_HTTP_ROUTE: if cp.Match != nil && cp.Match.ObjectTypes != nil { if cp.Match.GetRouteConfiguration() == nil { errs = appendValidation(errs, fmt.Errorf("Envoy filter: applyTo for http route class objects cannot have non route configuration match")) // nolint: stylecheck } } case networking.EnvoyFilter_CLUSTER: if cp.Match != nil && cp.Match.ObjectTypes != nil { if cp.Match.GetCluster() == nil { errs = appendValidation(errs, fmt.Errorf("Envoy filter: applyTo for cluster class objects cannot have non cluster match")) // nolint: stylecheck } } } // ensure that the struct is valid if _, err := xds.BuildXDSObjectFromStruct(cp.ApplyTo, cp.Patch.Value, false); err != nil { errs = appendValidation(errs, err) } else { // Run with strict validation, and emit warnings. This helps capture cases like unknown fields // We do not want to reject in case the proto is valid but our libraries are outdated obj, err := xds.BuildXDSObjectFromStruct(cp.ApplyTo, cp.Patch.Value, true) if err != nil { errs = appendValidation(errs, WrapWarning(err)) } // Append any deprecation notices if obj != nil { errs = appendValidation(errs, validateDeprecatedFilterTypes(obj)) errs = appendValidation(errs, validateMissingTypedConfigFilterTypes(obj)) } } } return errs.Unwrap() }) func validateListenerMatchName(name string) error { if newName, f := xds.ReverseDeprecatedFilterNames[name]; f { return WrapWarning(fmt.Errorf("using deprecated filter name %q; use %q instead", name, newName)) } return nil } func recurseDeprecatedTypes(message protoreflect.Message) ([]string, error) { var topError error var deprecatedTypes []string if message == nil { return nil, nil } message.Range(func(descriptor protoreflect.FieldDescriptor, value protoreflect.Value) bool { m, isMessage := value.Interface().(protoreflect.Message) if isMessage { anyMessage, isAny := m.Interface().(*any.Any) if isAny { mt, err := protoregistry.GlobalTypes.FindMessageByURL(anyMessage.TypeUrl) if err != nil { topError = err return false } var fileOpts proto.Message = mt.Descriptor().ParentFile().Options().(*descriptorpb.FileOptions) if proto.HasExtension(fileOpts, udpaa.E_FileStatus) { ext := proto.GetExtension(fileOpts, udpaa.E_FileStatus) udpaext, ok := ext.(*udpaa.StatusAnnotation) if !ok { topError = fmt.Errorf("extension was of wrong type: %T", ext) return false } if udpaext.PackageVersionStatus == udpaa.PackageVersionStatus_FROZEN { deprecatedTypes = append(deprecatedTypes, anyMessage.TypeUrl) } } } newTypes, err := recurseDeprecatedTypes(m) if err != nil { topError = err return false } deprecatedTypes = append(deprecatedTypes, newTypes...) } return true }) return deprecatedTypes, topError } // recurseMissingTypedConfig checks that configured filters do not rely on `name` and elide `typed_config`. // This is temporarily enabled in Envoy by the envoy.reloadable_features.no_extension_lookup_by_name flag, but in the future will be removed. func recurseMissingTypedConfig(message protoreflect.Message) []string { var deprecatedTypes []string if message == nil { return nil } // First, iterate over the fields to find the 'name' field to help with reporting errors. var name string for i := 0; i < message.Type().Descriptor().Fields().Len(); i++ { field := message.Type().Descriptor().Fields().Get(i) if field.JSONName() == "name" { name = fmt.Sprintf("%v", message.Get(field).Interface()) } } // Now go through fields again for i := 0; i < message.Type().Descriptor().Fields().Len(); i++ { field := message.Type().Descriptor().Fields().Get(i) set := message.Has(field) // If it has a typedConfig field, it must be set. // Note: it is possible there is some API that has typedConfig but has a non-deprecated alternative, // but I couldn't find any. Worst case, this is a warning, not an error, so a false positive is not so bad. if field.JSONName() == "typedConfig" && !set { deprecatedTypes = append(deprecatedTypes, name) } if set { // If the field was set and is a message, recurse into it to check children m, isMessage := message.Get(field).Interface().(protoreflect.Message) if isMessage { deprecatedTypes = append(deprecatedTypes, recurseMissingTypedConfig(m)...) } } } return deprecatedTypes } func validateDeprecatedFilterTypes(obj proto.Message) error { deprecated, err := recurseDeprecatedTypes(obj.ProtoReflect()) if err != nil { return fmt.Errorf("failed to find deprecated types: %v", err) } if len(deprecated) > 0 { return WrapWarning(fmt.Errorf("using deprecated type_url(s); %v", strings.Join(deprecated, ", "))) } return nil } func validateMissingTypedConfigFilterTypes(obj proto.Message) error { missing := recurseMissingTypedConfig(obj.ProtoReflect()) if len(missing) > 0 { return WrapWarning(fmt.Errorf("using deprecated types by name without typed_config; %v", strings.Join(missing, ", "))) } return nil } // validates that hostname in ns/<hostname> is a valid hostname according to // API specs func validateSidecarOrGatewayHostnamePart(hostname string, isGateway bool) (errs error) { // short name hosts are not allowed if hostname != "*" && !strings.Contains(hostname, ".") { errs = appendErrors(errs, fmt.Errorf("short names (non FQDN) are not allowed")) } if err := ValidateWildcardDomain(hostname); err != nil { if !isGateway { errs = appendErrors(errs, err) } // Gateway allows IP as the host string, as well ipAddr := net.ParseIP(hostname) if ipAddr == nil { errs = appendErrors(errs, err) } } return } func validateNamespaceSlashWildcardHostname(hostname string, isGateway bool) (errs error) { parts := strings.SplitN(hostname, "/", 2) if len(parts) != 2 { if isGateway { // Old style host in the gateway return validateSidecarOrGatewayHostnamePart(hostname, true) } errs = appendErrors(errs, fmt.Errorf("host must be of form namespace/dnsName")) return } if len(parts[0]) == 0 || len(parts[1]) == 0 { errs = appendErrors(errs, fmt.Errorf("config namespace and dnsName in host entry cannot be empty")) } if !isGateway { // namespace can be * or . or ~ or a valid DNS label in sidecars if parts[0] != "*" && parts[0] != "." && parts[0] != "~" { if !labels.IsDNS1123Label(parts[0]) { errs = appendErrors(errs, fmt.Errorf("invalid namespace value %q in sidecar", parts[0])) } } } else { // namespace can be * or . or a valid DNS label in gateways if parts[0] != "*" && parts[0] != "." { if !labels.IsDNS1123Label(parts[0]) { errs = appendErrors(errs, fmt.Errorf("invalid namespace value %q in gateway", parts[0])) } } } errs = appendErrors(errs, validateSidecarOrGatewayHostnamePart(parts[1], isGateway)) return } // ValidateSidecar checks sidecar config supplied by user var ValidateSidecar = registerValidateFunc("ValidateSidecar", func(cfg config.Config) (Warning, error) { errs := Validation{} rule, ok := cfg.Spec.(*networking.Sidecar) if !ok { return nil, fmt.Errorf("cannot cast to Sidecar") } if err := validateAlphaWorkloadSelector(rule.WorkloadSelector); err != nil { return nil, err } if len(rule.Egress) == 0 && len(rule.Ingress) == 0 && rule.OutboundTrafficPolicy == nil { return nil, fmt.Errorf("sidecar: empty configuration provided") } portMap := make(map[uint32]struct{}) for _, i := range rule.Ingress { if i == nil { errs = appendValidation(errs, fmt.Errorf("sidecar: ingress may not be null")) continue } if i.Port == nil { errs = appendValidation(errs, fmt.Errorf("sidecar: port is required for ingress listeners")) continue } bind := i.GetBind() errs = appendValidation(errs, validateSidecarIngressPortAndBind(i.Port, bind)) if _, found := portMap[i.Port.Number]; found { errs = appendValidation(errs, fmt.Errorf("sidecar: ports on IP bound listeners must be unique")) } portMap[i.Port.Number] = struct{}{} if len(i.DefaultEndpoint) != 0 { if strings.HasPrefix(i.DefaultEndpoint, UnixAddressPrefix) { errs = appendValidation(errs, ValidateUnixAddress(strings.TrimPrefix(i.DefaultEndpoint, UnixAddressPrefix))) } else { // format should be 127.0.0.1:port or :port parts := strings.Split(i.DefaultEndpoint, ":") if len(parts) < 2 { errs = appendValidation(errs, fmt.Errorf("sidecar: defaultEndpoint must be of form 127.0.0.1:<port>, 0.0.0.0:<port>, unix://filepath, or unset")) } else { if len(parts[0]) > 0 && parts[0] != "127.0.0.1" && parts[0] != "0.0.0.0" { errs = appendValidation(errs, fmt.Errorf("sidecar: defaultEndpoint must be of form 127.0.0.1:<port>, 0.0.0.0:<port>, unix://filepath, or unset")) } port, err := strconv.Atoi(parts[1]) if err != nil { errs = appendValidation(errs, fmt.Errorf("sidecar: defaultEndpoint port (%s) is not a number: %v", parts[1], err)) } else { errs = appendValidation(errs, ValidatePort(port)) } } } } if i.Tls != nil { if len(i.Tls.SubjectAltNames) > 0 { errs = appendValidation(errs, fmt.Errorf("sidecar: subjectAltNames is not supported in ingress tls")) } if i.Tls.HttpsRedirect { errs = appendValidation(errs, fmt.Errorf("sidecar: httpsRedirect is not supported")) } if i.Tls.CredentialName != "" { errs = appendValidation(errs, fmt.Errorf("sidecar: credentialName is not currently supported")) } if i.Tls.Mode == networking.ServerTLSSettings_ISTIO_MUTUAL || i.Tls.Mode == networking.ServerTLSSettings_AUTO_PASSTHROUGH { errs = appendValidation(errs, fmt.Errorf("configuration is invalid: cannot set mode to %s in sidecar ingress tls", i.Tls.Mode.String())) } protocol := protocol.Parse(i.Port.Protocol) if !protocol.IsTLS() { errs = appendValidation(errs, fmt.Errorf("server cannot have TLS settings for non HTTPS/TLS ports")) } errs = appendValidation(errs, validateTLSOptions(i.Tls)) } } portMap = make(map[uint32]struct{}) udsMap := make(map[string]struct{}) catchAllEgressListenerFound := false for index, egress := range rule.Egress { if egress == nil { errs = appendValidation(errs, errors.New("egress listener may not be null")) continue } // there can be only one catch all egress listener with empty port, and it should be the last listener. if egress.Port == nil { if !catchAllEgressListenerFound { if index == len(rule.Egress)-1 { catchAllEgressListenerFound = true } else { errs = appendValidation(errs, fmt.Errorf("sidecar: the egress listener with empty port should be the last listener in the list")) } } else { errs = appendValidation(errs, fmt.Errorf("sidecar: egress can have only one listener with empty port")) continue } } else { bind := egress.GetBind() captureMode := egress.GetCaptureMode() errs = appendValidation(errs, validateSidecarEgressPortBindAndCaptureMode(egress.Port, bind, captureMode)) if egress.Port.Number == 0 { if _, found := udsMap[bind]; found { errs = appendValidation(errs, fmt.Errorf("sidecar: unix domain socket values for listeners must be unique")) } udsMap[bind] = struct{}{} } else { if _, found := portMap[egress.Port.Number]; found { errs = appendValidation(errs, fmt.Errorf("sidecar: ports on IP bound listeners must be unique")) } portMap[egress.Port.Number] = struct{}{} } } // validate that the hosts field is a slash separated value // of form ns1/host, or */host, or */*, or ns1/*, or ns1/*.example.com if len(egress.Hosts) == 0 { errs = appendValidation(errs, fmt.Errorf("sidecar: egress listener must contain at least one host")) } else { nssSvcs := map[string]map[string]bool{} for _, hostname := range egress.Hosts { parts := strings.SplitN(hostname, "/", 2) if len(parts) == 2 { ns := parts[0] svc := parts[1] if ns == "." { ns = cfg.Namespace } if _, ok := nssSvcs[ns]; !ok { nssSvcs[ns] = map[string]bool{} } // test/a // test/a // test/* if svc != "*" { if _, ok := nssSvcs[ns][svc]; ok || nssSvcs[ns]["*"] { // already exists // TODO: prevent this invalid setting, maybe in 1.12+ errs = appendValidation(errs, WrapWarning(fmt.Errorf("duplicated egress host: %s", hostname))) } } else { if len(nssSvcs[ns]) != 0 { errs = appendValidation(errs, WrapWarning(fmt.Errorf("duplicated egress host: %s", hostname))) } } nssSvcs[ns][svc] = true } errs = appendValidation(errs, validateNamespaceSlashWildcardHostname(hostname, false)) } // */* // test/a if nssSvcs["*"]["*"] && len(nssSvcs) != 1 { errs = appendValidation(errs, WrapWarning(fmt.Errorf("`*/*` host select all resources, no other hosts can be added"))) } } } errs = appendValidation(errs, validateSidecarOutboundTrafficPolicy(rule.OutboundTrafficPolicy)) return errs.Unwrap() }) func validateSidecarOutboundTrafficPolicy(tp *networking.OutboundTrafficPolicy) (errs error) { if tp == nil { return } mode := tp.GetMode() if tp.EgressProxy != nil { if mode != networking.OutboundTrafficPolicy_ALLOW_ANY { errs = appendErrors(errs, fmt.Errorf("sidecar: egress_proxy must be set only with ALLOW_ANY outbound_traffic_policy mode")) return } errs = appendErrors(errs, ValidateFQDN(tp.EgressProxy.GetHost())) if tp.EgressProxy.Port == nil { errs = appendErrors(errs, fmt.Errorf("sidecar: egress_proxy port must be non-nil")) return } errs = appendErrors(errs, validateDestination(tp.EgressProxy)) } return } func validateSidecarEgressPortBindAndCaptureMode(port *networking.Port, bind string, captureMode networking.CaptureMode) (errs error) { // Port name is optional. Validate if exists. if len(port.Name) > 0 { errs = appendErrors(errs, ValidatePortName(port.Name)) } // Handle Unix domain sockets if port.Number == 0 { // require bind to be a unix domain socket errs = appendErrors(errs, ValidateProtocol(port.Protocol)) if !strings.HasPrefix(bind, UnixAddressPrefix) { errs = appendErrors(errs, fmt.Errorf("sidecar: ports with 0 value must have a unix domain socket bind address")) } else { errs = appendErrors(errs, ValidateUnixAddress(strings.TrimPrefix(bind, UnixAddressPrefix))) } if captureMode != networking.CaptureMode_DEFAULT && captureMode != networking.CaptureMode_NONE { errs = appendErrors(errs, fmt.Errorf("sidecar: captureMode must be DEFAULT/NONE for unix domain socket listeners")) } } else { errs = appendErrors(errs, ValidateProtocol(port.Protocol), ValidatePort(int(port.Number))) if len(bind) != 0 { errs = appendErrors(errs, ValidateIPAddress(bind)) } } return } func validateSidecarIngressPortAndBind(port *networking.Port, bind string) (errs error) { // Port name is optional. Validate if exists. if len(port.Name) > 0 { errs = appendErrors(errs, ValidatePortName(port.Name)) } errs = appendErrors(errs, ValidateProtocol(port.Protocol), ValidatePort(int(port.Number))) if len(bind) != 0 { errs = appendErrors(errs, ValidateIPAddress(bind)) } return } func validateTrafficPolicy(policy *networking.TrafficPolicy) Validation { if policy == nil { return Validation{} } if policy.OutlierDetection == nil && policy.ConnectionPool == nil && policy.LoadBalancer == nil && policy.Tls == nil && policy.PortLevelSettings == nil { return WrapError(fmt.Errorf("traffic policy must have at least one field")) } return appendValidation(validateOutlierDetection(policy.OutlierDetection), validateConnectionPool(policy.ConnectionPool), validateLoadBalancer(policy.LoadBalancer), validateTLS(policy.Tls), validatePortTrafficPolicies(policy.PortLevelSettings)) } func validateOutlierDetection(outlier *networking.OutlierDetection) (errs Validation) { if outlier == nil { return } if outlier.BaseEjectionTime != nil { errs = appendValidation(errs, ValidateDuration(outlier.BaseEjectionTime)) } // nolint: staticcheck if outlier.ConsecutiveErrors != 0 { warn := "outlier detection consecutive errors is deprecated, use consecutiveGatewayErrors or consecutive5xxErrors instead" scope.Warnf(warn) errs = appendValidation(errs, WrapWarning(errors.New(warn))) } if !outlier.SplitExternalLocalOriginErrors && outlier.ConsecutiveLocalOriginFailures.GetValue() > 0 { err := "outlier detection consecutive local origin failures is specified, but split external local origin errors is set to false" errs = appendValidation(errs, errors.New(err)) } if outlier.Interval != nil { errs = appendValidation(errs, ValidateDuration(outlier.Interval)) } errs = appendValidation(errs, ValidatePercent(outlier.MaxEjectionPercent), ValidatePercent(outlier.MinHealthPercent)) return } func validateConnectionPool(settings *networking.ConnectionPoolSettings) (errs error) { if settings == nil { return } if settings.Http == nil && settings.Tcp == nil { return fmt.Errorf("connection pool must have at least one field") } if httpSettings := settings.Http; httpSettings != nil { if httpSettings.Http1MaxPendingRequests < 0 { errs = appendErrors(errs, fmt.Errorf("http1 max pending requests must be non-negative")) } if httpSettings.Http2MaxRequests < 0 { errs = appendErrors(errs, fmt.Errorf("http2 max requests must be non-negative")) } if httpSettings.MaxRequestsPerConnection < 0 { errs = appendErrors(errs, fmt.Errorf("max requests per connection must be non-negative")) } if httpSettings.MaxRetries < 0 { errs = appendErrors(errs, fmt.Errorf("max retries must be non-negative")) } if httpSettings.IdleTimeout != nil { errs = appendErrors(errs, ValidateDuration(httpSettings.IdleTimeout)) } if httpSettings.H2UpgradePolicy == networking.ConnectionPoolSettings_HTTPSettings_UPGRADE && httpSettings.UseClientProtocol { errs = appendErrors(errs, fmt.Errorf("use client protocol must not be true when H2UpgradePolicy is UPGRADE")) } } if tcp := settings.Tcp; tcp != nil { if tcp.MaxConnections < 0 { errs = appendErrors(errs, fmt.Errorf("max connections must be non-negative")) } if tcp.ConnectTimeout != nil { errs = appendErrors(errs, ValidateDuration(tcp.ConnectTimeout)) } } return } func validateLoadBalancer(settings *networking.LoadBalancerSettings) (errs error) { if settings == nil { return } // simple load balancing is always valid consistentHash := settings.GetConsistentHash() if consistentHash != nil { httpCookie := consistentHash.GetHttpCookie() if httpCookie != nil { if httpCookie.Name == "" { errs = appendErrors(errs, fmt.Errorf("name required for HttpCookie")) } if httpCookie.Ttl == nil { errs = appendErrors(errs, fmt.Errorf("ttl required for HttpCookie")) } } } if err := validateLocalityLbSetting(settings.LocalityLbSetting); err != nil { errs = multierror.Append(errs, err) } return } func validateTLS(settings *networking.ClientTLSSettings) (errs error) { if settings == nil { return } if (settings.Mode == networking.ClientTLSSettings_SIMPLE || settings.Mode == networking.ClientTLSSettings_MUTUAL) && settings.CredentialName != "" { if settings.ClientCertificate != "" || settings.CaCertificates != "" || settings.PrivateKey != "" { errs = appendErrors(errs, fmt.Errorf("cannot specify client certificates or CA certificate If credentialName is set")) } // If tls mode is SIMPLE or MUTUAL, and CredentialName is specified, credentials are fetched // remotely. ServerCertificate and CaCertificates fields are not required. return } if settings.Mode == networking.ClientTLSSettings_MUTUAL { if settings.ClientCertificate == "" { errs = appendErrors(errs, fmt.Errorf("client certificate required for mutual tls")) } if settings.PrivateKey == "" { errs = appendErrors(errs, fmt.Errorf("private key required for mutual tls")) } } return } func validateSubset(subset *networking.Subset) error { return appendErrors(validateSubsetName(subset.Name), labels.Instance(subset.Labels).Validate(), validateTrafficPolicy(subset.TrafficPolicy)) } func validatePortTrafficPolicies(pls []*networking.TrafficPolicy_PortTrafficPolicy) (errs error) { for _, t := range pls { if t == nil { errs = appendErrors(errs, fmt.Errorf("traffic policy may not be null")) continue } if t.Port == nil { errs = appendErrors(errs, fmt.Errorf("portTrafficPolicy must have valid port")) } if t.OutlierDetection == nil && t.ConnectionPool == nil && t.LoadBalancer == nil && t.Tls == nil { errs = appendErrors(errs, fmt.Errorf("port traffic policy must have at least one field")) } else { errs = appendErrors(errs, validateOutlierDetection(t.OutlierDetection), validateConnectionPool(t.ConnectionPool), validateLoadBalancer(t.LoadBalancer), validateTLS(t.Tls)) } } return } // ValidateProxyAddress checks that a network address is well-formed func ValidateProxyAddress(hostAddr string) error { hostname, p, err := net.SplitHostPort(hostAddr) if err != nil { return fmt.Errorf("unable to split %q: %v", hostAddr, err) } port, err := strconv.Atoi(p) if err != nil { return fmt.Errorf("port (%s) is not a number: %v", p, err) } if err = ValidatePort(port); err != nil { return err } if err = ValidateFQDN(hostname); err != nil { ip := net.ParseIP(hostname) if ip == nil { return fmt.Errorf("%q is not a valid hostname or an IP address", hostname) } } return nil } // ValidateDuration checks that a proto duration is well-formed func ValidateDuration(pd *durationpb.Duration) error { dur := pd.AsDuration() if dur < time.Millisecond { return errors.New("duration must be greater than 1ms") } if dur%time.Millisecond != 0 { return errors.New("only durations to ms precision are supported") } return nil } // ValidateDurationRange verifies range is in specified duration func ValidateDurationRange(dur, min, max time.Duration) error { if dur > max || dur < min { return fmt.Errorf("time %v must be >%v and <%v", dur.String(), min.String(), max.String()) } return nil } // ValidateParentAndDrain checks that parent and drain durations are valid func ValidateParentAndDrain(drainTime, parentShutdown *durationpb.Duration) (errs error) { if err := ValidateDuration(drainTime); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid drain duration:")) } if err := ValidateDuration(parentShutdown); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid parent shutdown duration:")) } if errs != nil { return } drainDuration := drainTime.AsDuration() parentShutdownDuration := parentShutdown.AsDuration() if drainDuration%time.Second != 0 { errs = multierror.Append(errs, errors.New("drain time only supports durations to seconds precision")) } if parentShutdownDuration%time.Second != 0 { errs = multierror.Append(errs, errors.New("parent shutdown time only supports durations to seconds precision")) } if parentShutdownDuration <= drainDuration { errs = multierror.Append(errs, fmt.Errorf("parent shutdown time %v must be greater than drain time %v", parentShutdownDuration.String(), drainDuration.String())) } if drainDuration > drainTimeMax { errs = multierror.Append(errs, fmt.Errorf("drain time %v must be <%v", drainDuration.String(), drainTimeMax.String())) } if parentShutdownDuration > parentShutdownTimeMax { errs = multierror.Append(errs, fmt.Errorf("parent shutdown time %v must be <%v", parentShutdownDuration.String(), parentShutdownTimeMax.String())) } return } // ValidateLightstepCollector validates the configuration for sending envoy spans to LightStep func ValidateLightstepCollector(ls *meshconfig.Tracing_Lightstep) error { var errs error if ls.GetAddress() == "" { errs = multierror.Append(errs, errors.New("address is required")) } if err := ValidateProxyAddress(ls.GetAddress()); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid lightstep address:")) } if ls.GetAccessToken() == "" { errs = multierror.Append(errs, errors.New("access token is required")) } return errs } // validateCustomTags validates that tracing CustomTags map does not contain any nil items func validateCustomTags(tags map[string]*meshconfig.Tracing_CustomTag) error { for tagName, tagVal := range tags { if tagVal == nil { return fmt.Errorf("encountered nil value for custom tag: %s", tagName) } } return nil } // ValidateZipkinCollector validates the configuration for sending envoy spans to Zipkin func ValidateZipkinCollector(z *meshconfig.Tracing_Zipkin) error { return ValidateProxyAddress(strings.Replace(z.GetAddress(), "$(HOST_IP)", "127.0.0.1", 1)) } // ValidateDatadogCollector validates the configuration for sending envoy spans to Datadog func ValidateDatadogCollector(d *meshconfig.Tracing_Datadog) error { // If the address contains $(HOST_IP), replace it with a valid IP before validation. return ValidateProxyAddress(strings.Replace(d.GetAddress(), "$(HOST_IP)", "127.0.0.1", 1)) } // ValidateConnectTimeout validates the envoy connection timeout func ValidateConnectTimeout(timeout *durationpb.Duration) error { if err := ValidateDuration(timeout); err != nil { return err } err := ValidateDurationRange(timeout.AsDuration(), connectTimeoutMin, connectTimeoutMax) return err } // ValidateProtocolDetectionTimeout validates the envoy protocol detection timeout func ValidateProtocolDetectionTimeout(timeout *durationpb.Duration) error { dur := timeout.AsDuration() // 0s is a valid value if trying to disable protocol detection timeout if dur == time.Second*0 { return nil } if dur%time.Millisecond != 0 { return errors.New("only durations to ms precision are supported") } return nil } // ValidateMaxServerConnectionAge validate negative duration func ValidateMaxServerConnectionAge(in time.Duration) error { if err := IsNegativeDuration(in); err != nil { return fmt.Errorf("%v: --keepaliveMaxServerConnectionAge only accepts positive duration eg: 30m", err) } return nil } // IsNegativeDuration check if the duration is negative func IsNegativeDuration(in time.Duration) error { if in < 0 { return fmt.Errorf("invalid duration: %s", in.String()) } return nil } // ValidateMeshConfig checks that the mesh config is well-formed func ValidateMeshConfig(mesh *meshconfig.MeshConfig) (errs error) { if err := ValidatePort(int(mesh.ProxyListenPort)); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid proxy listen port:")) } if err := ValidateConnectTimeout(mesh.ConnectTimeout); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid connect timeout:")) } if err := ValidateProtocolDetectionTimeout(mesh.ProtocolDetectionTimeout); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid protocol detection timeout:")) } if mesh.DefaultConfig == nil { errs = multierror.Append(errs, errors.New("missing default config")) } else if err := ValidateMeshConfigProxyConfig(mesh.DefaultConfig); err != nil { errs = multierror.Append(errs, err) } if err := validateLocalityLbSetting(mesh.LocalityLbSetting); err != nil { errs = multierror.Append(errs, err) } if err := validateServiceSettings(mesh); err != nil { errs = multierror.Append(errs, err) } if err := validateTrustDomainConfig(mesh); err != nil { errs = multierror.Append(errs, err) } if err := validateExtensionProvider(mesh); err != nil { scope.Warnf("found invalid extension provider (can be ignored if the given extension provider is not used): %v", err) } return } func validateTrustDomainConfig(config *meshconfig.MeshConfig) (errs error) { if err := ValidateTrustDomain(config.TrustDomain); err != nil { errs = multierror.Append(errs, fmt.Errorf("trustDomain: %v", err)) } for i, tda := range config.TrustDomainAliases { if err := ValidateTrustDomain(tda); err != nil { errs = multierror.Append(errs, fmt.Errorf("trustDomainAliases[%d], domain `%s` : %v", i, tda, err)) } } return } func validateServiceSettings(config *meshconfig.MeshConfig) (errs error) { for sIndex, s := range config.ServiceSettings { for _, h := range s.Hosts { if err := ValidateWildcardDomain(h); err != nil { errs = multierror.Append(errs, fmt.Errorf("serviceSettings[%d], host `%s`: %v", sIndex, h, err)) } } } return } func validatePrivateKeyProvider(pkpConf *meshconfig.PrivateKeyProvider) error { var errs error if pkpConf.GetProvider() == nil { errs = multierror.Append(errs, errors.New("private key provider confguration is required")) } switch pkpConf.GetProvider().(type) { case *meshconfig.PrivateKeyProvider_Cryptomb: cryptomb := pkpConf.GetCryptomb() if cryptomb == nil { errs = multierror.Append(errs, errors.New("cryptomb confguration is required")) } else { pollDelay := cryptomb.GetPollDelay() if pollDelay == nil { errs = multierror.Append(errs, errors.New("pollDelay is required")) } else if pollDelay.GetSeconds() == 0 && pollDelay.GetNanos() == 0 { errs = multierror.Append(errs, errors.New("pollDelay must be non zero")) } } default: errs = multierror.Append(errs, errors.New("unknown private key provider")) } return errs } // ValidateMeshConfigProxyConfig checks that the mesh config is well-formed func ValidateMeshConfigProxyConfig(config *meshconfig.ProxyConfig) (errs error) { if config.ConfigPath == "" { errs = multierror.Append(errs, errors.New("config path must be set")) } if config.BinaryPath == "" { errs = multierror.Append(errs, errors.New("binary path must be set")) } clusterName := config.GetClusterName() switch naming := clusterName.(type) { case *meshconfig.ProxyConfig_ServiceCluster: if naming.ServiceCluster == "" { errs = multierror.Append(errs, errors.New("service cluster must be specified")) } case *meshconfig.ProxyConfig_TracingServiceName_: // intentionally left empty for now default: errs = multierror.Append(errs, errors.New("oneof service cluster or tracing service name must be specified")) } if err := ValidateParentAndDrain(config.DrainDuration, config.ParentShutdownDuration); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid parent and drain time combination")) } // discovery address is mandatory since mutual TLS relies on CDS. // strictly speaking, proxies can operate without RDS/CDS and with hot restarts // but that requires additional test validation if config.DiscoveryAddress == "" { errs = multierror.Append(errs, errors.New("discovery address must be set to the proxy discovery service")) } else if err := ValidateProxyAddress(config.DiscoveryAddress); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid discovery address:")) } if tracer := config.GetTracing().GetLightstep(); tracer != nil { if err := ValidateLightstepCollector(tracer); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid lightstep config:")) } } if tracer := config.GetTracing().GetZipkin(); tracer != nil { if err := ValidateZipkinCollector(tracer); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid zipkin config:")) } } if tracer := config.GetTracing().GetDatadog(); tracer != nil { if err := ValidateDatadogCollector(tracer); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid datadog config:")) } } if tracer := config.GetTracing().GetTlsSettings(); tracer != nil { if err := validateTLS(tracer); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid tracing TLS config:")) } } if tracerCustomTags := config.GetTracing().GetCustomTags(); tracerCustomTags != nil { if err := validateCustomTags(tracerCustomTags); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid tracing custom tags:")) } } if config.StatsdUdpAddress != "" { if err := ValidateProxyAddress(config.StatsdUdpAddress); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid statsd udp address %q:", config.StatsdUdpAddress))) } } // nolint: staticcheck if config.EnvoyMetricsServiceAddress != "" { if err := ValidateProxyAddress(config.EnvoyMetricsServiceAddress); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid envoy metrics service address %q:", config.EnvoyMetricsServiceAddress))) } else { scope.Warnf("EnvoyMetricsServiceAddress is deprecated, use EnvoyMetricsService instead.") // nolint: stylecheck } } if config.EnvoyMetricsService != nil && config.EnvoyMetricsService.Address != "" { if err := ValidateProxyAddress(config.EnvoyMetricsService.Address); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid envoy metrics service address %q:", config.EnvoyMetricsService.Address))) } } if config.EnvoyAccessLogService != nil && config.EnvoyAccessLogService.Address != "" { if err := ValidateProxyAddress(config.EnvoyAccessLogService.Address); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid envoy access log service address %q:", config.EnvoyAccessLogService.Address))) } } if err := ValidatePort(int(config.ProxyAdminPort)); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid proxy admin port:")) } if err := ValidateControlPlaneAuthPolicy(config.ControlPlaneAuthPolicy); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid authentication policy:")) } if err := ValidatePort(int(config.StatusPort)); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid status port:")) } if pkpConf := config.GetPrivateKeyProvider(); pkpConf != nil { if err := validatePrivateKeyProvider(pkpConf); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, "invalid private key provider confguration:")) } } return } func ValidateControlPlaneAuthPolicy(policy meshconfig.AuthenticationPolicy) error { if policy == meshconfig.AuthenticationPolicy_NONE || policy == meshconfig.AuthenticationPolicy_MUTUAL_TLS { return nil } return fmt.Errorf("unrecognized control plane auth policy %q", policy) } func validateWorkloadSelector(selector *type_beta.WorkloadSelector) error { var errs error if selector != nil { for k, v := range selector.MatchLabels { if k == "" { errs = appendErrors(errs, fmt.Errorf("empty key is not supported in selector: %q", fmt.Sprintf("%s=%s", k, v))) } if strings.Contains(k, "*") || strings.Contains(v, "*") { errs = appendErrors(errs, fmt.Errorf("wildcard is not supported in selector: %q", fmt.Sprintf("%s=%s", k, v))) } } } return errs } // ValidateAuthorizationPolicy checks that AuthorizationPolicy is well-formed. var ValidateAuthorizationPolicy = registerValidateFunc("ValidateAuthorizationPolicy", func(cfg config.Config) (Warning, error) { in, ok := cfg.Spec.(*security_beta.AuthorizationPolicy) if !ok { return nil, fmt.Errorf("cannot cast to AuthorizationPolicy") } var errs error if err := validateWorkloadSelector(in.Selector); err != nil { errs = appendErrors(errs, err) } if in.Action == security_beta.AuthorizationPolicy_CUSTOM { if in.Rules == nil { errs = appendErrors(errs, fmt.Errorf("CUSTOM action without `rules` is meaningless as it will never be triggered, "+ "add an empty rule `{}` if you want it be triggered for every request")) } else { if in.GetProvider() == nil || in.GetProvider().GetName() == "" { errs = appendErrors(errs, fmt.Errorf("`provider.name` must not be empty")) } } // TODO(yangminzhu): Add support for more matching rules. for _, rule := range in.GetRules() { check := func(invalid bool, name string) error { if invalid { return fmt.Errorf("%s is currently not supported with CUSTOM action", name) } return nil } for _, from := range rule.GetFrom() { if src := from.GetSource(); src != nil { errs = appendErrors(errs, check(len(src.Namespaces) != 0, "From.Namespaces")) errs = appendErrors(errs, check(len(src.NotNamespaces) != 0, "From.NotNamespaces")) errs = appendErrors(errs, check(len(src.Principals) != 0, "From.Principals")) errs = appendErrors(errs, check(len(src.NotPrincipals) != 0, "From.NotPrincipals")) errs = appendErrors(errs, check(len(src.RequestPrincipals) != 0, "From.RequestPrincipals")) errs = appendErrors(errs, check(len(src.NotRequestPrincipals) != 0, "From.NotRequestPrincipals")) } } for _, when := range rule.GetWhen() { errs = appendErrors(errs, check(when.Key == "source.namespace", when.Key)) errs = appendErrors(errs, check(when.Key == "source.principal", when.Key)) errs = appendErrors(errs, check(strings.HasPrefix(when.Key, "request.auth."), when.Key)) } } } if in.GetProvider() != nil && in.Action != security_beta.AuthorizationPolicy_CUSTOM { errs = appendErrors(errs, fmt.Errorf("`provider` must not be with non CUSTOM action, found %s", in.Action)) } if in.Action == security_beta.AuthorizationPolicy_DENY && in.Rules == nil { errs = appendErrors(errs, fmt.Errorf("DENY action without `rules` is meaningless as it will never be triggered, "+ "add an empty rule `{}` if you want it be triggered for every request")) } for i, rule := range in.GetRules() { if rule == nil { errs = appendErrors(errs, fmt.Errorf("`rule` must not be nil, found at rule %d", i)) continue } if rule.From != nil && len(rule.From) == 0 { errs = appendErrors(errs, fmt.Errorf("`from` must not be empty, found at rule %d", i)) } for _, from := range rule.From { if from == nil { errs = appendErrors(errs, fmt.Errorf("`from` must not be nil, found at rule %d", i)) continue } if from.Source == nil { errs = appendErrors(errs, fmt.Errorf("`from.source` must not be nil, found at rule %d", i)) } else { src := from.Source if len(src.Principals) == 0 && len(src.RequestPrincipals) == 0 && len(src.Namespaces) == 0 && len(src.IpBlocks) == 0 && len(src.RemoteIpBlocks) == 0 && len(src.NotPrincipals) == 0 && len(src.NotRequestPrincipals) == 0 && len(src.NotNamespaces) == 0 && len(src.NotIpBlocks) == 0 && len(src.NotRemoteIpBlocks) == 0 { errs = appendErrors(errs, fmt.Errorf("`from.source` must not be empty, found at rule %d", i)) } errs = appendErrors(errs, security.ValidateIPs(from.Source.GetIpBlocks())) errs = appendErrors(errs, security.ValidateIPs(from.Source.GetNotIpBlocks())) errs = appendErrors(errs, security.ValidateIPs(from.Source.GetRemoteIpBlocks())) errs = appendErrors(errs, security.ValidateIPs(from.Source.GetNotRemoteIpBlocks())) errs = appendErrors(errs, security.CheckEmptyValues("Principals", src.Principals)) errs = appendErrors(errs, security.CheckEmptyValues("RequestPrincipals", src.RequestPrincipals)) errs = appendErrors(errs, security.CheckEmptyValues("Namespaces", src.Namespaces)) errs = appendErrors(errs, security.CheckEmptyValues("IpBlocks", src.IpBlocks)) errs = appendErrors(errs, security.CheckEmptyValues("RemoteIpBlocks", src.RemoteIpBlocks)) errs = appendErrors(errs, security.CheckEmptyValues("NotPrincipals", src.NotPrincipals)) errs = appendErrors(errs, security.CheckEmptyValues("NotRequestPrincipals", src.NotRequestPrincipals)) errs = appendErrors(errs, security.CheckEmptyValues("NotNamespaces", src.NotNamespaces)) errs = appendErrors(errs, security.CheckEmptyValues("NotIpBlocks", src.NotIpBlocks)) errs = appendErrors(errs, security.CheckEmptyValues("NotRemoteIpBlocks", src.NotRemoteIpBlocks)) } } if rule.To != nil && len(rule.To) == 0 { errs = appendErrors(errs, fmt.Errorf("`to` must not be empty, found at rule %d", i)) } for _, to := range rule.To { if to == nil { errs = appendErrors(errs, fmt.Errorf("`to` must not be nil, found at rule %d", i)) continue } if to.Operation == nil { errs = appendErrors(errs, fmt.Errorf("`to.operation` must not be nil, found at rule %d", i)) } else { op := to.Operation if len(op.Ports) == 0 && len(op.Methods) == 0 && len(op.Paths) == 0 && len(op.Hosts) == 0 && len(op.NotPorts) == 0 && len(op.NotMethods) == 0 && len(op.NotPaths) == 0 && len(op.NotHosts) == 0 { errs = appendErrors(errs, fmt.Errorf("`to.operation` must not be empty, found at rule %d", i)) } errs = appendErrors(errs, security.ValidatePorts(to.Operation.GetPorts())) errs = appendErrors(errs, security.ValidatePorts(to.Operation.GetNotPorts())) errs = appendErrors(errs, security.CheckEmptyValues("Ports", op.Ports)) errs = appendErrors(errs, security.CheckEmptyValues("Methods", op.Methods)) errs = appendErrors(errs, security.CheckEmptyValues("Paths", op.Paths)) errs = appendErrors(errs, security.CheckEmptyValues("Hosts", op.Hosts)) errs = appendErrors(errs, security.CheckEmptyValues("NotPorts", op.NotPorts)) errs = appendErrors(errs, security.CheckEmptyValues("NotMethods", op.NotMethods)) errs = appendErrors(errs, security.CheckEmptyValues("NotPaths", op.NotPaths)) errs = appendErrors(errs, security.CheckEmptyValues("NotHosts", op.NotHosts)) } } for _, condition := range rule.GetWhen() { key := condition.GetKey() if key == "" { errs = appendErrors(errs, fmt.Errorf("`key` must not be empty")) } else { if len(condition.GetValues()) == 0 && len(condition.GetNotValues()) == 0 { errs = appendErrors(errs, fmt.Errorf("at least one of `values` or `notValues` must be set for key %s", key)) } else { if err := security.ValidateAttribute(key, condition.GetValues()); err != nil { errs = appendErrors(errs, fmt.Errorf("invalid `value` for `key` %s: %v", key, err)) } if err := security.ValidateAttribute(key, condition.GetNotValues()); err != nil { errs = appendErrors(errs, fmt.Errorf("invalid `notValue` for `key` %s: %v", key, err)) } } } } } return nil, multierror.Prefix(errs, fmt.Sprintf("invalid policy %s.%s:", cfg.Name, cfg.Namespace)) }) // ValidateRequestAuthentication checks that request authentication spec is well-formed. var ValidateRequestAuthentication = registerValidateFunc("ValidateRequestAuthentication", func(cfg config.Config) (Warning, error) { in, ok := cfg.Spec.(*security_beta.RequestAuthentication) if !ok { return nil, errors.New("cannot cast to RequestAuthentication") } var errs error errs = appendErrors(errs, validateWorkloadSelector(in.Selector)) for _, rule := range in.JwtRules { errs = appendErrors(errs, validateJwtRule(rule)) } return nil, errs }) func validateJwtRule(rule *security_beta.JWTRule) (errs error) { if rule == nil { return nil } if len(rule.Issuer) == 0 { errs = multierror.Append(errs, errors.New("issuer must be set")) } for _, audience := range rule.Audiences { if len(audience) == 0 { errs = multierror.Append(errs, errors.New("audience must be non-empty string")) } } if len(rule.JwksUri) != 0 { if _, err := security.ParseJwksURI(rule.JwksUri); err != nil { errs = multierror.Append(errs, err) } } if rule.Jwks != "" { _, err := jwk.Parse([]byte(rule.Jwks)) if err != nil { errs = multierror.Append(errs, fmt.Errorf("jwks parse error: %v", err)) } } for _, location := range rule.FromHeaders { if location == nil { errs = multierror.Append(errs, errors.New("location header name must be non-null")) continue } if len(location.Name) == 0 { errs = multierror.Append(errs, errors.New("location header name must be non-empty string")) } } for _, location := range rule.FromParams { if len(location) == 0 { errs = multierror.Append(errs, errors.New("location query must be non-empty string")) } } return } // ValidatePeerAuthentication checks that peer authentication spec is well-formed. var ValidatePeerAuthentication = registerValidateFunc("ValidatePeerAuthentication", func(cfg config.Config) (Warning, error) { in, ok := cfg.Spec.(*security_beta.PeerAuthentication) if !ok { return nil, errors.New("cannot cast to PeerAuthentication") } var errs error emptySelector := in.Selector == nil || len(in.Selector.MatchLabels) == 0 if emptySelector && len(in.PortLevelMtls) != 0 { errs = appendErrors(errs, fmt.Errorf("mesh/namespace peer authentication cannot have port level mTLS")) } if in.PortLevelMtls != nil && len(in.PortLevelMtls) == 0 { errs = appendErrors(errs, fmt.Errorf("port level mTLS, if defined, must have at least one element")) } for port := range in.PortLevelMtls { if port == 0 { errs = appendErrors(errs, fmt.Errorf("port cannot be 0")) } } errs = appendErrors(errs, validateWorkloadSelector(in.Selector)) return nil, errs }) // ValidateVirtualService checks that a v1alpha3 route rule is well-formed. var ValidateVirtualService = registerValidateFunc("ValidateVirtualService", func(cfg config.Config) (Warning, error) { virtualService, ok := cfg.Spec.(*networking.VirtualService) if !ok { return nil, errors.New("cannot cast to virtual service") } errs := Validation{} if len(virtualService.Hosts) == 0 { // This must be delegate - enforce delegate validations. if len(virtualService.Gateways) != 0 { // meaningless to specify gateways in delegate errs = appendValidation(errs, fmt.Errorf("delegate virtual service must have no gateways specified")) } if len(virtualService.Tls) != 0 { // meaningless to specify tls in delegate, we donot support tls delegate errs = appendValidation(errs, fmt.Errorf("delegate virtual service must have no tls route specified")) } if len(virtualService.Tcp) != 0 { // meaningless to specify tls in delegate, we donot support tcp delegate errs = appendValidation(errs, fmt.Errorf("delegate virtual service must have no tcp route specified")) } } appliesToMesh := false appliesToGateway := false if len(virtualService.Gateways) == 0 { appliesToMesh = true } else { errs = appendValidation(errs, validateGatewayNames(virtualService.Gateways)) for _, gatewayName := range virtualService.Gateways { if gatewayName == constants.IstioMeshGateway { appliesToMesh = true } else { appliesToGateway = true } } } if !appliesToGateway { validateJWTClaimRoute := func(headers map[string]*networking.StringMatch) { for key := range headers { if strings.HasPrefix(key, constant.HeaderJWTClaim) { msg := fmt.Sprintf("JWT claim based routing (key: %s) is only supported for gateway, found no gateways: %v", key, virtualService.Gateways) errs = appendValidation(errs, errors.New(msg)) } } } for _, http := range virtualService.GetHttp() { for _, m := range http.GetMatch() { validateJWTClaimRoute(m.GetHeaders()) validateJWTClaimRoute(m.GetWithoutHeaders()) } } } allHostsValid := true for _, virtualHost := range virtualService.Hosts { if err := ValidateWildcardDomain(virtualHost); err != nil { ipAddr := net.ParseIP(virtualHost) // Could also be an IP if ipAddr == nil { errs = appendValidation(errs, err) allHostsValid = false } } else if appliesToMesh && virtualHost == "*" { errs = appendValidation(errs, fmt.Errorf("wildcard host * is not allowed for virtual services bound to the mesh gateway")) allHostsValid = false } } // Check for duplicate hosts // Duplicates include literal duplicates as well as wildcard duplicates // E.g., *.foo.com, and *.com are duplicates in the same virtual service if allHostsValid { for i := 0; i < len(virtualService.Hosts); i++ { hostI := host.Name(virtualService.Hosts[i]) for j := i + 1; j < len(virtualService.Hosts); j++ { hostJ := host.Name(virtualService.Hosts[j]) if hostI.Matches(hostJ) { errs = appendValidation(errs, fmt.Errorf("duplicate hosts in virtual service: %s & %s", hostI, hostJ)) } } } } if len(virtualService.Http) == 0 && len(virtualService.Tcp) == 0 && len(virtualService.Tls) == 0 { errs = appendValidation(errs, errors.New("http, tcp or tls must be provided in virtual service")) } for _, httpRoute := range virtualService.Http { if httpRoute == nil { errs = appendValidation(errs, errors.New("http route may not be null")) continue } errs = appendValidation(errs, validateHTTPRoute(httpRoute, len(virtualService.Hosts) == 0)) } for _, tlsRoute := range virtualService.Tls { errs = appendValidation(errs, validateTLSRoute(tlsRoute, virtualService)) } for _, tcpRoute := range virtualService.Tcp { errs = appendValidation(errs, validateTCPRoute(tcpRoute)) } errs = appendValidation(errs, validateExportTo(cfg.Namespace, virtualService.ExportTo, false, false)) warnUnused := func(ruleno, reason string) { errs = appendValidation(errs, WrapWarning(&AnalysisAwareError{ Type: "VirtualServiceUnreachableRule", Msg: fmt.Sprintf("virtualService rule %v not used (%s)", ruleno, reason), Parameters: []interface{}{ruleno, reason}, })) } warnIneffective := func(ruleno, matchno, dupno string) { errs = appendValidation(errs, WrapWarning(&AnalysisAwareError{ Type: "VirtualServiceIneffectiveMatch", Msg: fmt.Sprintf("virtualService rule %v match %v is not used (duplicate/overlapping match in rule %v)", ruleno, matchno, dupno), Parameters: []interface{}{ruleno, matchno, dupno}, })) } analyzeUnreachableHTTPRules(virtualService.Http, warnUnused, warnIneffective) analyzeUnreachableTCPRules(virtualService.Tcp, warnUnused, warnIneffective) analyzeUnreachableTLSRules(virtualService.Tls, warnUnused, warnIneffective) return errs.Unwrap() }) func assignExactOrPrefix(exact, prefix string) string { if exact != "" { return matchExact + exact } if prefix != "" { return matchPrefix + prefix } return "" } // genMatchHTTPRoutes build the match rules into struct OverlappingMatchValidationForHTTPRoute // based on particular HTTPMatchRequest, according to comments on https://github.com/istio/istio/pull/32701 // only support Match's port, method, authority, headers, query params and nonheaders for now. func genMatchHTTPRoutes(route *networking.HTTPRoute, match *networking.HTTPMatchRequest, rulen, matchn int) (matchHTTPRoutes *OverlappingMatchValidationForHTTPRoute) { // skip current match if no match field for current route if match == nil { return nil } // skip current match if no URI field if match.Uri == nil { return nil } // store all httproute with prefix match uri tmpPrefix := match.Uri.GetPrefix() if tmpPrefix != "" { // set Method methodExact := match.Method.GetExact() methodPrefix := match.Method.GetPrefix() methodMatch := assignExactOrPrefix(methodExact, methodPrefix) // if no method information, it should be GET by default if methodMatch == "" { methodMatch = matchExact + "GET" } // set Authority authorityExact := match.Authority.GetExact() authorityPrefix := match.Authority.GetPrefix() authorityMatch := assignExactOrPrefix(authorityExact, authorityPrefix) // set Headers headerMap := make(map[string]string) for hkey, hvalue := range match.Headers { hvalueExact := hvalue.GetExact() hvaluePrefix := hvalue.GetPrefix() hvalueMatch := assignExactOrPrefix(hvalueExact, hvaluePrefix) headerMap[hkey] = hvalueMatch } // set QueryParams QPMap := make(map[string]string) for qpkey, qpvalue := range match.QueryParams { qpvalueExact := qpvalue.GetExact() qpvaluePrefix := qpvalue.GetPrefix() qpvalueMatch := assignExactOrPrefix(qpvalueExact, qpvaluePrefix) QPMap[qpkey] = qpvalueMatch } // set WithoutHeaders noHeaderMap := make(map[string]string) for nhkey, nhvalue := range match.WithoutHeaders { nhvalueExact := nhvalue.GetExact() nhvaluePrefix := nhvalue.GetPrefix() nhvalueMatch := assignExactOrPrefix(nhvalueExact, nhvaluePrefix) noHeaderMap[nhkey] = nhvalueMatch } matchHTTPRoutes = &OverlappingMatchValidationForHTTPRoute{ routeName(route, rulen), requestName(match, matchn), tmpPrefix, match.Port, methodMatch, authorityMatch, headerMap, QPMap, noHeaderMap, } return } return nil } // coveredValidation validate the overlapping match between two instance of OverlappingMatchValidationForHTTPRoute func coveredValidation(vA, vB *OverlappingMatchValidationForHTTPRoute) bool { // check the URI overlapping match, such as vB.Prefix is '/debugs' and vA.Prefix is '/debug' if strings.HasPrefix(vB.Prefix, vA.Prefix) { // check the port field if vB.MatchPort != vA.MatchPort { return false } // check the match method if vA.MatchMethod != vB.MatchMethod { if !strings.HasPrefix(vA.MatchMethod, vB.MatchMethod) { return false } } // check the match authority if vA.MatchAuthority != vB.MatchAuthority { if !strings.HasPrefix(vA.MatchAuthority, vB.MatchAuthority) { return false } } // check the match Headers vAHeaderLen := len(vA.MatchHeaders) vBHeaderLen := len(vB.MatchHeaders) if vAHeaderLen != vBHeaderLen { return false } for hdKey, hdValue := range vA.MatchHeaders { vBhdValue, ok := vB.MatchHeaders[hdKey] if !ok { return false } else if hdValue != vBhdValue { if !strings.HasPrefix(hdValue, vBhdValue) { return false } } } // check the match QueryParams vAQPLen := len(vA.MatchQueryParams) vBQPLen := len(vB.MatchQueryParams) if vAQPLen != vBQPLen { return false } for qpKey, qpValue := range vA.MatchQueryParams { vBqpValue, ok := vB.MatchQueryParams[qpKey] if !ok { return false } else if qpValue != vBqpValue { if !strings.HasPrefix(qpValue, vBqpValue) { return false } } } // check the match NonHeaders vANonHDLen := len(vA.MatchNonHeaders) vBNonHDLen := len(vB.MatchNonHeaders) if vANonHDLen != vBNonHDLen { return false } for nhKey, nhValue := range vA.MatchNonHeaders { vBnhValue, ok := vB.MatchNonHeaders[nhKey] if !ok { return false } else if nhValue != vBnhValue { if !strings.HasPrefix(nhValue, vBnhValue) { return false } } } } else { // no URI overlapping match return false } return true } func analyzeUnreachableHTTPRules(routes []*networking.HTTPRoute, reportUnreachable func(ruleno, reason string), reportIneffective func(ruleno, matchno, dupno string)) { matchesEncountered := make(map[string]int) emptyMatchEncountered := -1 var matchHTTPRoutes []*OverlappingMatchValidationForHTTPRoute for rulen, route := range routes { if route == nil { continue } if len(route.Match) == 0 { if emptyMatchEncountered >= 0 { reportUnreachable(routeName(route, rulen), "only the last rule can have no matches") } emptyMatchEncountered = rulen continue } duplicateMatches := 0 for matchn, match := range route.Match { dupn, ok := matchesEncountered[asJSON(match)] if ok { reportIneffective(routeName(route, rulen), requestName(match, matchn), routeName(routes[dupn], dupn)) duplicateMatches++ // no need to handle for totally duplicated match rules continue } matchesEncountered[asJSON(match)] = rulen // build the match rules into struct OverlappingMatchValidationForHTTPRoute based on current match matchHTTPRoute := genMatchHTTPRoutes(route, match, rulen, matchn) if matchHTTPRoute != nil { matchHTTPRoutes = append(matchHTTPRoutes, matchHTTPRoute) } } if duplicateMatches == len(route.Match) { reportUnreachable(routeName(route, rulen), "all matches used by prior rules") } } // at least 2 prefix matched routes for overlapping match validation if len(matchHTTPRoutes) > 1 { // check the overlapping match from the first prefix information for routeIndex, routePrefix := range matchHTTPRoutes { for rIndex := routeIndex + 1; rIndex < len(matchHTTPRoutes); rIndex++ { // exclude the duplicate-match cases which have been validated above if strings.Compare(matchHTTPRoutes[rIndex].Prefix, routePrefix.Prefix) == 0 { continue } // Validate former prefix match does not cover the latter one. if coveredValidation(routePrefix, matchHTTPRoutes[rIndex]) { prefixMatchA := matchHTTPRoutes[rIndex].MatchStr + " of prefix " + matchHTTPRoutes[rIndex].Prefix prefixMatchB := routePrefix.MatchStr + " of prefix " + routePrefix.Prefix + " on " + routePrefix.RouteStr reportIneffective(matchHTTPRoutes[rIndex].RouteStr, prefixMatchA, prefixMatchB) } } } } } // NOTE: This method identical to analyzeUnreachableHTTPRules. func analyzeUnreachableTCPRules(routes []*networking.TCPRoute, reportUnreachable func(ruleno, reason string), reportIneffective func(ruleno, matchno, dupno string)) { matchesEncountered := make(map[string]int) emptyMatchEncountered := -1 for rulen, route := range routes { if route == nil { continue } if len(route.Match) == 0 { if emptyMatchEncountered >= 0 { reportUnreachable(routeName(route, rulen), "only the last rule can have no matches") } emptyMatchEncountered = rulen continue } duplicateMatches := 0 for matchn, match := range route.Match { dupn, ok := matchesEncountered[asJSON(match)] if ok { reportIneffective(routeName(route, rulen), requestName(match, matchn), routeName(routes[dupn], dupn)) duplicateMatches++ } else { matchesEncountered[asJSON(match)] = rulen } } if duplicateMatches == len(route.Match) { reportUnreachable(routeName(route, rulen), "all matches used by prior rules") } } } // NOTE: This method identical to analyzeUnreachableHTTPRules. func analyzeUnreachableTLSRules(routes []*networking.TLSRoute, reportUnreachable func(ruleno, reason string), reportIneffective func(ruleno, matchno, dupno string)) { matchesEncountered := make(map[string]int) emptyMatchEncountered := -1 for rulen, route := range routes { if route == nil { continue } if len(route.Match) == 0 { if emptyMatchEncountered >= 0 { reportUnreachable(routeName(route, rulen), "only the last rule can have no matches") } emptyMatchEncountered = rulen continue } duplicateMatches := 0 for matchn, match := range route.Match { dupn, ok := matchesEncountered[asJSON(match)] if ok { reportIneffective(routeName(route, rulen), requestName(match, matchn), routeName(routes[dupn], dupn)) duplicateMatches++ } else { matchesEncountered[asJSON(match)] = rulen } } if duplicateMatches == len(route.Match) { reportUnreachable(routeName(route, rulen), "all matches used by prior rules") } } } // asJSON() creates a JSON serialization of a match, to use for match comparison. We don't use the JSON itself. func asJSON(data interface{}) string { // Remove the name, so we can create a serialization that only includes traffic routing config switch mr := data.(type) { case *networking.HTTPMatchRequest: if mr != nil && mr.Name != "" { cl := &networking.HTTPMatchRequest{} protomarshal.ShallowCopy(cl, mr) cl.Name = "" data = cl } } b, err := json.Marshal(data) if err != nil { return err.Error() } return string(b) } func routeName(route interface{}, routen int) string { switch r := route.(type) { case *networking.HTTPRoute: if r.Name != "" { return fmt.Sprintf("%q", r.Name) } // TCP and TLS routes have no names } return fmt.Sprintf("#%d", routen) } func requestName(match interface{}, matchn int) string { switch mr := match.(type) { case *networking.HTTPMatchRequest: if mr != nil && mr.Name != "" { return fmt.Sprintf("%q", mr.Name) } // TCP and TLS matches have no names } return fmt.Sprintf("#%d", matchn) } func validateTLSRoute(tls *networking.TLSRoute, context *networking.VirtualService) (errs Validation) { if tls == nil { return } if len(tls.Match) == 0 { errs = appendValidation(errs, errors.New("TLS route must have at least one match condition")) } for _, match := range tls.Match { errs = appendValidation(errs, validateTLSMatch(match, context)) } if len(tls.Route) == 0 { errs = appendValidation(errs, errors.New("TLS route is required")) } errs = appendValidation(errs, validateRouteDestinations(tls.Route)) return errs } func validateTLSMatch(match *networking.TLSMatchAttributes, context *networking.VirtualService) (errs Validation) { if match == nil { errs = appendValidation(errs, errors.New("TLS match may not be null")) return } if len(match.SniHosts) == 0 { errs = appendValidation(errs, fmt.Errorf("TLS match must have at least one SNI host")) } else { for _, sniHost := range match.SniHosts { errs = appendValidation(errs, validateSniHost(sniHost, context)) } } for _, destinationSubnet := range match.DestinationSubnets { errs = appendValidation(errs, ValidateIPSubnet(destinationSubnet)) } if match.Port != 0 { errs = appendValidation(errs, ValidatePort(int(match.Port))) } errs = appendValidation(errs, labels.Instance(match.SourceLabels).Validate()) errs = appendValidation(errs, validateGatewayNames(match.Gateways)) return } func validateSniHost(sniHost string, context *networking.VirtualService) (errs Validation) { if err := ValidateWildcardDomain(sniHost); err != nil { ipAddr := net.ParseIP(sniHost) // Could also be an IP if ipAddr != nil { errs = appendValidation(errs, WrapWarning(fmt.Errorf("using an IP address (%q) goes against SNI spec and most clients do not support this", ipAddr))) return } return appendValidation(errs, err) } sniHostname := host.Name(sniHost) for _, hostname := range context.Hosts { if sniHostname.SubsetOf(host.Name(hostname)) { return } } return appendValidation(errs, fmt.Errorf("SNI host %q is not a compatible subset of any of the virtual service hosts: [%s]", sniHost, strings.Join(context.Hosts, ", "))) } func validateTCPRoute(tcp *networking.TCPRoute) (errs error) { if tcp == nil { return nil } for _, match := range tcp.Match { errs = appendErrors(errs, validateTCPMatch(match)) } if len(tcp.Route) == 0 { errs = appendErrors(errs, errors.New("TCP route is required")) } errs = appendErrors(errs, validateRouteDestinations(tcp.Route)) return } func validateTCPMatch(match *networking.L4MatchAttributes) (errs error) { if match == nil { errs = multierror.Append(errs, errors.New("tcp match may not be nil")) return } for _, destinationSubnet := range match.DestinationSubnets { errs = appendErrors(errs, ValidateIPSubnet(destinationSubnet)) } if match.Port != 0 { errs = appendErrors(errs, ValidatePort(int(match.Port))) } errs = appendErrors(errs, labels.Instance(match.SourceLabels).Validate()) errs = appendErrors(errs, validateGatewayNames(match.Gateways)) return } func validateStringMatchRegexp(sm *networking.StringMatch, where string) error { switch sm.GetMatchType().(type) { case *networking.StringMatch_Regex: default: return nil } re := sm.GetRegex() if re == "" { return fmt.Errorf("%q: regex string match should not be empty", where) } // Envoy enforces a re2.max_program_size.error_level re2 program size is not the same as length, // but it is always *larger* than length. Because goland does not have a way to evaluate the // program size, we approximate by the length. To ensure that a program that is smaller than 1024 // length but larger than 1024 size does not enter the system, we program Envoy to allow very large // regexs to avoid NACKs. See // https://github.com/jpeach/snippets/blob/889fda84cc8713af09205438b33553eb69dd5355/re2sz.cc to // evaluate program size. if len(re) > 1024 { return fmt.Errorf("%q: regex is too large, max length allowed is 1024", where) } _, err := regexp.Compile(re) if err == nil { return nil } return fmt.Errorf("%q: %w; Istio uses RE2 style regex-based match (https://github.com/google/re2/wiki/Syntax)", where, err) } func validateGatewayNames(gatewayNames []string) (errs Validation) { for _, gatewayName := range gatewayNames { parts := strings.SplitN(gatewayName, "/", 2) if len(parts) != 2 { if strings.Contains(gatewayName, ".") { // Legacy FQDN style parts := strings.Split(gatewayName, ".") recommended := fmt.Sprintf("%s/%s", parts[1], parts[0]) errs = appendValidation(errs, WrapWarning(fmt.Errorf( "using legacy gatewayName format %q; prefer the <namespace>/<name> format: %q", gatewayName, recommended))) } errs = appendValidation(errs, ValidateFQDN(gatewayName)) return } if len(parts[0]) == 0 || len(parts[1]) == 0 { errs = appendValidation(errs, fmt.Errorf("config namespace and gateway name cannot be empty")) } // namespace and name must be DNS labels if !labels.IsDNS1123Label(parts[0]) { errs = appendValidation(errs, fmt.Errorf("invalid value for namespace: %q", parts[0])) } if !labels.IsDNS1123Label(parts[1]) { errs = appendValidation(errs, fmt.Errorf("invalid value for gateway name: %q", parts[1])) } } return } func validateHTTPRouteDestinations(weights []*networking.HTTPRouteDestination) (errs error) { var totalWeight int32 for _, weight := range weights { if weight == nil { errs = multierror.Append(errs, errors.New("weight may not be nil")) continue } if weight.Destination == nil { errs = multierror.Append(errs, errors.New("destination is required")) } // header manipulations for name, val := range weight.Headers.GetRequest().GetAdd() { errs = appendErrors(errs, ValidateHTTPHeaderWithHostOperationName(name)) errs = appendErrors(errs, ValidateHTTPHeaderValue(val)) } for name, val := range weight.Headers.GetRequest().GetSet() { errs = appendErrors(errs, ValidateHTTPHeaderWithHostOperationName(name)) errs = appendErrors(errs, ValidateHTTPHeaderValue(val)) } for _, name := range weight.Headers.GetRequest().GetRemove() { errs = appendErrors(errs, ValidateHTTPHeaderOperationName(name)) } for name, val := range weight.Headers.GetResponse().GetAdd() { errs = appendErrors(errs, ValidateHTTPHeaderOperationName(name)) errs = appendErrors(errs, ValidateHTTPHeaderValue(val)) } for name, val := range weight.Headers.GetResponse().GetSet() { errs = appendErrors(errs, ValidateHTTPHeaderOperationName(name)) errs = appendErrors(errs, ValidateHTTPHeaderValue(val)) } for _, name := range weight.Headers.GetResponse().GetRemove() { errs = appendErrors(errs, ValidateHTTPHeaderOperationName(name)) } errs = appendErrors(errs, validateDestination(weight.Destination)) errs = appendErrors(errs, ValidatePercent(weight.Weight)) totalWeight += weight.Weight } if len(weights) > 1 && totalWeight != 100 { errs = appendErrors(errs, fmt.Errorf("total destination weight %v != 100", totalWeight)) } return } func validateRouteDestinations(weights []*networking.RouteDestination) (errs error) { var totalWeight int32 for _, weight := range weights { if weight == nil { errs = multierror.Append(errs, errors.New("weight may not be nil")) continue } if weight.Destination == nil { errs = multierror.Append(errs, errors.New("destination is required")) } errs = appendErrors(errs, validateDestination(weight.Destination)) errs = appendErrors(errs, ValidatePercent(weight.Weight)) totalWeight += weight.Weight } if len(weights) > 1 && totalWeight != 100 { errs = appendErrors(errs, fmt.Errorf("total destination weight %v != 100", totalWeight)) } return } func validateCORSPolicy(policy *networking.CorsPolicy) (errs error) { if policy == nil { return } for _, origin := range policy.AllowOrigins { errs = appendErrors(errs, validateAllowOrigins(origin)) } for _, method := range policy.AllowMethods { errs = appendErrors(errs, validateHTTPMethod(method)) } for _, name := range policy.AllowHeaders { errs = appendErrors(errs, ValidateHTTPHeaderName(name)) } for _, name := range policy.ExposeHeaders { errs = appendErrors(errs, ValidateHTTPHeaderName(name)) } if policy.MaxAge != nil { errs = appendErrors(errs, ValidateDuration(policy.MaxAge)) if policy.MaxAge.Nanos > 0 { errs = multierror.Append(errs, errors.New("max_age duration is accurate only to seconds precision")) } } return } func validateAllowOrigins(origin *networking.StringMatch) error { var match string switch origin.MatchType.(type) { case *networking.StringMatch_Exact: match = origin.GetExact() case *networking.StringMatch_Prefix: match = origin.GetPrefix() case *networking.StringMatch_Regex: match = origin.GetRegex() } if match == "" { return fmt.Errorf("'%v' is not a valid match type for CORS allow origins", match) } return validateStringMatchRegexp(origin, "corsPolicy.allowOrigins") } func validateHTTPMethod(method string) error { if !supportedMethods[method] { return fmt.Errorf("%q is not a supported HTTP method", method) } return nil } func validateHTTPFaultInjection(fault *networking.HTTPFaultInjection) (errs error) { if fault == nil { return } if fault.Abort == nil && fault.Delay == nil { errs = multierror.Append(errs, errors.New("HTTP fault injection must have an abort and/or a delay")) } errs = appendErrors(errs, validateHTTPFaultInjectionAbort(fault.Abort)) errs = appendErrors(errs, validateHTTPFaultInjectionDelay(fault.Delay)) return } func validateHTTPFaultInjectionAbort(abort *networking.HTTPFaultInjection_Abort) (errs error) { if abort == nil { return } errs = appendErrors(errs, validatePercentage(abort.Percentage)) switch abort.ErrorType.(type) { case *networking.HTTPFaultInjection_Abort_GrpcStatus: // TODO: gRPC status validation errs = multierror.Append(errs, errors.New("gRPC abort fault injection not supported yet")) case *networking.HTTPFaultInjection_Abort_Http2Error: // TODO: HTTP2 error validation errs = multierror.Append(errs, errors.New("HTTP/2 abort fault injection not supported yet")) case *networking.HTTPFaultInjection_Abort_HttpStatus: errs = appendErrors(errs, validateHTTPStatus(abort.GetHttpStatus())) } return } func validateHTTPStatus(status int32) error { if status < 200 || status > 600 { return fmt.Errorf("HTTP status %d is not in range 200-599", status) } return nil } func validateHTTPFaultInjectionDelay(delay *networking.HTTPFaultInjection_Delay) (errs error) { if delay == nil { return } errs = appendErrors(errs, validatePercentage(delay.Percentage)) switch v := delay.HttpDelayType.(type) { case *networking.HTTPFaultInjection_Delay_FixedDelay: errs = appendErrors(errs, ValidateDuration(v.FixedDelay)) case *networking.HTTPFaultInjection_Delay_ExponentialDelay: errs = appendErrors(errs, ValidateDuration(v.ExponentialDelay)) errs = multierror.Append(errs, fmt.Errorf("exponentialDelay not supported yet")) } return } func validateDestination(destination *networking.Destination) (errs error) { if destination == nil { return } hostname := destination.Host if hostname == "*" { errs = appendErrors(errs, fmt.Errorf("invalid destination host %s", hostname)) } else { errs = appendErrors(errs, ValidateWildcardDomain(hostname)) } if destination.Subset != "" { errs = appendErrors(errs, validateSubsetName(destination.Subset)) } if destination.Port != nil { errs = appendErrors(errs, validatePortSelector(destination.Port)) } return } func validateSubsetName(name string) error { if len(name) == 0 { return fmt.Errorf("subset name cannot be empty") } if !labels.IsDNS1123Label(name) { return fmt.Errorf("subset name is invalid: %s", name) } return nil } func validatePortSelector(selector *networking.PortSelector) (errs error) { if selector == nil { return nil } // port must be a number number := int(selector.GetNumber()) errs = appendErrors(errs, ValidatePort(number)) return } func validateHTTPRetry(retries *networking.HTTPRetry) (errs error) { if retries == nil { return } if retries.Attempts < 0 { errs = multierror.Append(errs, errors.New("attempts cannot be negative")) } if retries.Attempts == 0 && (retries.PerTryTimeout != nil || retries.RetryOn != "" || retries.RetryRemoteLocalities != nil) { errs = appendErrors(errs, errors.New("http retry policy configured when attempts are set to 0 (disabled)")) } if retries.PerTryTimeout != nil { errs = appendErrors(errs, ValidateDuration(retries.PerTryTimeout)) } if retries.RetryOn != "" { retryOnPolicies := strings.Split(retries.RetryOn, ",") for _, policy := range retryOnPolicies { // Try converting it to an integer to see if it's a valid HTTP status code. i, _ := strconv.Atoi(policy) if http.StatusText(i) == "" && !supportedRetryOnPolicies[policy] { errs = appendErrors(errs, fmt.Errorf("%q is not a valid retryOn policy", policy)) } } } return } func validateHTTPRedirect(redirect *networking.HTTPRedirect) error { if redirect == nil { return nil } if redirect.Uri == "" && redirect.Authority == "" && redirect.RedirectPort == nil && redirect.Scheme == "" { return errors.New("redirect must specify URI, authority, scheme, or port") } if redirect.RedirectCode != 0 { if redirect.RedirectCode < 300 || redirect.RedirectCode > 399 { return fmt.Errorf("%d is not a valid redirect code, must be 3xx", redirect.RedirectCode) } } if redirect.Scheme != "" && redirect.Scheme != "http" && redirect.Scheme != "https" { return fmt.Errorf(`invalid redirect scheme, must be "http" or "https"`) } if redirect.GetPort() > 0 { if err := ValidatePort(int(redirect.GetPort())); err != nil { return err } } return nil } func validateHTTPRewrite(rewrite *networking.HTTPRewrite) error { if rewrite != nil && rewrite.Uri == "" && rewrite.Authority == "" { return errors.New("rewrite must specify URI, authority, or both") } return nil } // ValidateWorkloadEntry validates a workload entry. var ValidateWorkloadEntry = registerValidateFunc("ValidateWorkloadEntry", func(cfg config.Config) (Warning, error) { we, ok := cfg.Spec.(*networking.WorkloadEntry) if !ok { return nil, fmt.Errorf("cannot cast to workload entry") } return validateWorkloadEntry(we) }) func validateWorkloadEntry(we *networking.WorkloadEntry) (Warning, error) { errs := Validation{} if we.Address == "" { return nil, fmt.Errorf("address must be set") } // Since we don't know if its meant to be DNS or STATIC type without association with a ServiceEntry, // check based on content and try validations. addr := we.Address // First check if it is a Unix endpoint - this will be specified for STATIC. if strings.HasPrefix(we.Address, UnixAddressPrefix) { errs = appendValidation(errs, ValidateUnixAddress(strings.TrimPrefix(addr, UnixAddressPrefix))) if len(we.Ports) != 0 { errs = appendValidation(errs, fmt.Errorf("unix endpoint %s must not include ports", we.Address)) } } else { // This could be IP (in STATIC resolution) or DNS host name (for DNS). ipAddr := net.ParseIP(we.Address) if ipAddr == nil { if err := ValidateFQDN(we.Address); err != nil { // Otherwise could be an FQDN errs = appendValidation(errs, fmt.Errorf("endpoint address %q is not a valid FQDN or an IP address", we.Address)) } } } errs = appendValidation(errs, labels.Instance(we.Labels).Validate()) for name, port := range we.Ports { // TODO: Validate port is part of Service Port - which is tricky to validate with out service entry. errs = appendValidation(errs, ValidatePortName(name), ValidatePort(int(port))) } return errs.Unwrap() } // ValidateWorkloadGroup validates a workload group. var ValidateWorkloadGroup = registerValidateFunc("ValidateWorkloadGroup", func(cfg config.Config) (warnings Warning, errs error) { wg, ok := cfg.Spec.(*networking.WorkloadGroup) if !ok { return nil, fmt.Errorf("cannot cast to workload entry") } if wg.Template == nil { return nil, fmt.Errorf("template is required") } // Do not call validateWorkloadEntry. Some fields, such as address, are required in WorkloadEntry // but not in the template since they are auto populated if wg.Metadata != nil { if err := labels.Instance(wg.Metadata.Labels).Validate(); err != nil { return nil, fmt.Errorf("invalid labels: %v", err) } } return nil, validateReadinessProbe(wg.Probe) }) func validateReadinessProbe(probe *networking.ReadinessProbe) (errs error) { if probe == nil { return nil } if probe.PeriodSeconds < 0 { errs = appendErrors(errs, fmt.Errorf("periodSeconds must be non-negative")) } if probe.InitialDelaySeconds < 0 { errs = appendErrors(errs, fmt.Errorf("initialDelaySeconds must be non-negative")) } if probe.TimeoutSeconds < 0 { errs = appendErrors(errs, fmt.Errorf("timeoutSeconds must be non-negative")) } if probe.SuccessThreshold < 0 { errs = appendErrors(errs, fmt.Errorf("successThreshold must be non-negative")) } if probe.FailureThreshold < 0 { errs = appendErrors(errs, fmt.Errorf("failureThreshold must be non-negative")) } switch m := probe.HealthCheckMethod.(type) { case *networking.ReadinessProbe_HttpGet: h := m.HttpGet if h == nil { errs = appendErrors(errs, fmt.Errorf("httpGet may not be nil")) break } errs = appendErrors(errs, ValidatePort(int(h.Port))) if h.Scheme != "" && h.Scheme != string(apimirror.URISchemeHTTPS) && h.Scheme != string(apimirror.URISchemeHTTP) { errs = appendErrors(errs, fmt.Errorf(`httpGet.scheme must be one of "http", "https"`)) } for _, header := range h.HttpHeaders { if header == nil { errs = appendErrors(errs, fmt.Errorf("invalid nil header")) continue } errs = appendErrors(errs, ValidateHTTPHeaderName(header.Name)) } case *networking.ReadinessProbe_TcpSocket: h := m.TcpSocket if h == nil { errs = appendErrors(errs, fmt.Errorf("tcpSocket may not be nil")) break } errs = appendErrors(errs, ValidatePort(int(h.Port))) case *networking.ReadinessProbe_Exec: h := m.Exec if h == nil { errs = appendErrors(errs, fmt.Errorf("exec may not be nil")) break } if len(h.Command) == 0 { errs = appendErrors(errs, fmt.Errorf("exec.command is required")) } default: errs = appendErrors(errs, fmt.Errorf("unknown health check method %T", m)) } return errs } // ValidateServiceEntry validates a service entry. var ValidateServiceEntry = registerValidateFunc("ValidateServiceEntry", func(cfg config.Config) (Warning, error) { serviceEntry, ok := cfg.Spec.(*networking.ServiceEntry) if !ok { return nil, fmt.Errorf("cannot cast to service entry") } if err := validateAlphaWorkloadSelector(serviceEntry.WorkloadSelector); err != nil { return nil, err } errs := Validation{} if serviceEntry.WorkloadSelector != nil && serviceEntry.Endpoints != nil { errs = appendValidation(errs, fmt.Errorf("only one of WorkloadSelector or Endpoints is allowed in Service Entry")) } if len(serviceEntry.Hosts) == 0 { errs = appendValidation(errs, fmt.Errorf("service entry must have at least one host")) } for _, hostname := range serviceEntry.Hosts { // Full wildcard is not allowed in the service entry. if hostname == "*" { errs = appendValidation(errs, fmt.Errorf("invalid host %s", hostname)) } else { errs = appendValidation(errs, ValidateWildcardDomain(hostname)) } } cidrFound := false for _, address := range serviceEntry.Addresses { cidrFound = cidrFound || strings.Contains(address, "/") errs = appendValidation(errs, ValidateIPSubnet(address)) } if cidrFound { if serviceEntry.Resolution != networking.ServiceEntry_NONE && serviceEntry.Resolution != networking.ServiceEntry_STATIC { errs = appendValidation(errs, fmt.Errorf("CIDR addresses are allowed only for NONE/STATIC resolution types")) } } servicePortNumbers := make(map[uint32]bool) servicePorts := make(map[string]bool, len(serviceEntry.Ports)) for _, port := range serviceEntry.Ports { if port == nil { errs = appendValidation(errs, fmt.Errorf("service entry port may not be null")) continue } if servicePorts[port.Name] { errs = appendValidation(errs, fmt.Errorf("service entry port name %q already defined", port.Name)) } servicePorts[port.Name] = true if servicePortNumbers[port.Number] { errs = appendValidation(errs, fmt.Errorf("service entry port %d already defined", port.Number)) } servicePortNumbers[port.Number] = true if port.TargetPort != 0 { errs = appendValidation(errs, ValidatePort(int(port.TargetPort))) } errs = appendValidation(errs, ValidatePortName(port.Name), ValidateProtocol(port.Protocol), ValidatePort(int(port.Number))) } switch serviceEntry.Resolution { case networking.ServiceEntry_NONE: if len(serviceEntry.Endpoints) != 0 { errs = appendValidation(errs, fmt.Errorf("no endpoints should be provided for resolution type none")) } case networking.ServiceEntry_STATIC: unixEndpoint := false for _, endpoint := range serviceEntry.Endpoints { addr := endpoint.GetAddress() if strings.HasPrefix(addr, UnixAddressPrefix) { unixEndpoint = true errs = appendValidation(errs, ValidateUnixAddress(strings.TrimPrefix(addr, UnixAddressPrefix))) if len(endpoint.Ports) != 0 { errs = appendValidation(errs, fmt.Errorf("unix endpoint %s must not include ports", addr)) } } else { errs = appendValidation(errs, ValidateIPAddress(addr)) for name, port := range endpoint.Ports { if !servicePorts[name] { errs = appendValidation(errs, fmt.Errorf("endpoint port %v is not defined by the service entry", port)) } } } errs = appendValidation(errs, labels.Instance(endpoint.Labels).Validate()) } if unixEndpoint && len(serviceEntry.Ports) != 1 { errs = appendValidation(errs, errors.New("exactly 1 service port required for unix endpoints")) } case networking.ServiceEntry_DNS, networking.ServiceEntry_DNS_ROUND_ROBIN: if len(serviceEntry.Endpoints) == 0 { for _, hostname := range serviceEntry.Hosts { if err := ValidateFQDN(hostname); err != nil { errs = appendValidation(errs, fmt.Errorf("hosts must be FQDN if no endpoints are provided for resolution mode %s", serviceEntry.Resolution)) } } } for _, endpoint := range serviceEntry.Endpoints { ipAddr := net.ParseIP(endpoint.Address) // Typically it is an IP address if ipAddr == nil { if err := ValidateFQDN(endpoint.Address); err != nil { // Otherwise could be an FQDN errs = appendValidation(errs, fmt.Errorf("endpoint address %q is not a valid FQDN or an IP address", endpoint.Address)) } } errs = appendValidation(errs, labels.Instance(endpoint.Labels).Validate()) for name, port := range endpoint.Ports { if !servicePorts[name] { errs = appendValidation(errs, fmt.Errorf("endpoint port %v is not defined by the service entry", port)) } errs = appendValidation(errs, ValidatePortName(name), ValidatePort(int(port))) } } if len(serviceEntry.Addresses) > 0 { for _, port := range serviceEntry.Ports { p := protocol.Parse(port.Protocol) if p.IsTCP() { if len(serviceEntry.Hosts) > 1 { // TODO: prevent this invalid setting, maybe in 1.11+ errs = appendValidation(errs, WrapWarning(fmt.Errorf("service entry can not have more than one host specified "+ "simultaneously with address and tcp port"))) } break } } } default: errs = appendValidation(errs, fmt.Errorf("unsupported resolution type %s", networking.ServiceEntry_Resolution_name[int32(serviceEntry.Resolution)])) } // multiple hosts and TCP is invalid unless the resolution type is NONE. // depending on the protocol, we can differentiate between hosts when proxying: // - with HTTP, the authority header can be used // - with HTTPS/TLS with SNI, the ServerName can be used // however, for plain TCP there is no way to differentiate between the // hosts so we consider it invalid, unless the resolution type is NONE // (because the hosts are ignored). if serviceEntry.Resolution != networking.ServiceEntry_NONE && len(serviceEntry.Hosts) > 1 { for _, port := range serviceEntry.Ports { p := protocol.Parse(port.Protocol) if !p.IsHTTP() && !p.IsTLS() { errs = appendValidation(errs, fmt.Errorf("multiple hosts provided with non-HTTP, non-TLS ports")) break } } } errs = appendValidation(errs, validateExportTo(cfg.Namespace, serviceEntry.ExportTo, true, false)) return errs.Unwrap() }) // ValidatePortName validates a port name to DNS-1123 func ValidatePortName(name string) error { if !labels.IsDNS1123Label(name) { return fmt.Errorf("invalid port name: %s", name) } return nil } // ValidateProtocol validates a portocol name is known func ValidateProtocol(protocolStr string) error { // Empty string is used for protocol sniffing. if protocolStr != "" && protocol.Parse(protocolStr) == protocol.Unsupported { return fmt.Errorf("unsupported protocol: %s", protocolStr) } return nil } // wrapper around multierror.Append that enforces the invariant that if all input errors are nil, the output // error is nil (allowing validation without branching). func appendValidation(v Validation, vs ...error) Validation { appendError := func(err, err2 error) error { if err == nil { return err2 } else if err2 == nil { return err } return multierror.Append(err, err2) } for _, nv := range vs { switch t := nv.(type) { case Validation: v.Err = appendError(v.Err, t.Err) v.Warning = appendError(v.Warning, t.Warning) default: v.Err = appendError(v.Err, t) } } return v } // appendErrorf appends a formatted error string // nolint: unparam func appendErrorf(v Validation, format string, a ...interface{}) Validation { return appendValidation(v, fmt.Errorf(format, a...)) } // appendWarningf appends a formatted warning string // nolint: unparam func appendWarningf(v Validation, format string, a ...interface{}) Validation { return appendValidation(v, Warningf(format, a...)) } // wrapper around multierror.Append that enforces the invariant that if all input errors are nil, the output // error is nil (allowing validation without branching). func appendErrors(err error, errs ...error) error { appendError := func(err, err2 error) error { if err == nil { return err2 } else if err2 == nil { return err } return multierror.Append(err, err2) } for _, err2 := range errs { switch t := err2.(type) { case Validation: err = appendError(err, t.Err) default: err = appendError(err, err2) } } return err } // validateLocalityLbSetting checks the LocalityLbSetting of MeshConfig func validateLocalityLbSetting(lb *networking.LocalityLoadBalancerSetting) error { if lb == nil { return nil } if len(lb.GetDistribute()) > 0 && len(lb.GetFailover()) > 0 { return fmt.Errorf("can not simultaneously specify 'distribute' and 'failover'") } srcLocalities := make([]string, 0, len(lb.GetDistribute())) for _, locality := range lb.GetDistribute() { srcLocalities = append(srcLocalities, locality.From) var totalWeight uint32 destLocalities := make([]string, 0) for loc, weight := range locality.To { destLocalities = append(destLocalities, loc) if weight <= 0 || weight > 100 { return fmt.Errorf("locality weight must be in range [1, 100]") } totalWeight += weight } if totalWeight != 100 { return fmt.Errorf("total locality weight %v != 100", totalWeight) } if err := validateLocalities(destLocalities); err != nil { return err } } if err := validateLocalities(srcLocalities); err != nil { return err } for _, failover := range lb.GetFailover() { if failover.From == failover.To { return fmt.Errorf("locality lb failover settings must specify different regions") } if strings.Contains(failover.From, "/") || strings.Contains(failover.To, "/") { return fmt.Errorf("locality lb failover only specify region") } if strings.Contains(failover.To, "*") || strings.Contains(failover.From, "*") { return fmt.Errorf("locality lb failover region should not contain '*' wildcard") } } return nil } func validateLocalities(localities []string) error { regionZoneSubZoneMap := map[string]map[string]map[string]bool{} for _, locality := range localities { if n := strings.Count(locality, "*"); n > 0 { if n > 1 || !strings.HasSuffix(locality, "*") { return fmt.Errorf("locality %s wildcard '*' number can not exceed 1 and must be in the end", locality) } } if _, exist := regionZoneSubZoneMap["*"]; exist { return fmt.Errorf("locality %s overlap with previous specified ones", locality) } region, zone, subZone, localityIndex, err := getLocalityParam(locality) if err != nil { return fmt.Errorf("locality %s must not contain empty region/zone/subzone info", locality) } switch localityIndex { case regionIndex: if _, exist := regionZoneSubZoneMap[region]; exist { return fmt.Errorf("locality %s overlap with previous specified ones", locality) } regionZoneSubZoneMap[region] = map[string]map[string]bool{"*": {"*": true}} case zoneIndex: if _, exist := regionZoneSubZoneMap[region]; exist { if _, exist := regionZoneSubZoneMap[region]["*"]; exist { return fmt.Errorf("locality %s overlap with previous specified ones", locality) } if _, exist := regionZoneSubZoneMap[region][zone]; exist { return fmt.Errorf("locality %s overlap with previous specified ones", locality) } regionZoneSubZoneMap[region][zone] = map[string]bool{"*": true} } else { regionZoneSubZoneMap[region] = map[string]map[string]bool{zone: {"*": true}} } case subZoneIndex: if _, exist := regionZoneSubZoneMap[region]; exist { if _, exist := regionZoneSubZoneMap[region]["*"]; exist { return fmt.Errorf("locality %s overlap with previous specified ones", locality) } if _, exist := regionZoneSubZoneMap[region][zone]; exist { if regionZoneSubZoneMap[region][zone]["*"] { return fmt.Errorf("locality %s overlap with previous specified ones", locality) } if regionZoneSubZoneMap[region][zone][subZone] { return fmt.Errorf("locality %s overlap with previous specified ones", locality) } regionZoneSubZoneMap[region][zone][subZone] = true } else { regionZoneSubZoneMap[region][zone] = map[string]bool{subZone: true} } } else { regionZoneSubZoneMap[region] = map[string]map[string]bool{zone: {subZone: true}} } } } return nil } func getLocalityParam(locality string) (string, string, string, int, error) { var region, zone, subZone string items := strings.SplitN(locality, "/", 3) for i, item := range items { if item == "" { return "", "", "", -1, errors.New("item is nil") } switch i { case regionIndex: region = items[i] case zoneIndex: zone = items[i] case subZoneIndex: subZone = items[i] } } return region, zone, subZone, len(items) - 1, nil } // ValidateMeshNetworks validates meshnetworks. func ValidateMeshNetworks(meshnetworks *meshconfig.MeshNetworks) (errs error) { // TODO validate using the same gateway on multiple networks? for name, network := range meshnetworks.Networks { if err := validateNetwork(network); err != nil { errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid network %v:", name))) } } return } func validateNetwork(network *meshconfig.Network) (errs error) { for _, n := range network.Endpoints { switch e := n.Ne.(type) { case *meshconfig.Network_NetworkEndpoints_FromCidr: if err := ValidateIPSubnet(e.FromCidr); err != nil { errs = multierror.Append(errs, err) } case *meshconfig.Network_NetworkEndpoints_FromRegistry: if ok := labels.IsDNS1123Label(e.FromRegistry); !ok { errs = multierror.Append(errs, fmt.Errorf("invalid registry name: %v", e.FromRegistry)) } } } for _, n := range network.Gateways { switch g := n.Gw.(type) { case *meshconfig.Network_IstioNetworkGateway_RegistryServiceName: if err := ValidateFQDN(g.RegistryServiceName); err != nil { errs = multierror.Append(errs, err) } case *meshconfig.Network_IstioNetworkGateway_Address: if ipErr := ValidateIPAddress(g.Address); ipErr != nil { if !features.ResolveHostnameGateways { err := fmt.Errorf("%v (hostname is allowed if RESOLVE_HOSTNAME_GATEWAYS is enabled)", ipErr) errs = multierror.Append(errs, err) } else if fqdnErr := ValidateFQDN(g.Address); fqdnErr != nil { errs = multierror.Append(fmt.Errorf("%v is not a valid IP address or DNS name", g.Address)) } } } if err := ValidatePort(int(n.Port)); err != nil { errs = multierror.Append(errs, err) } } return } func (aae *AnalysisAwareError) Error() string { return aae.Msg } // ValidateProxyConfig validates a ProxyConfig CR (as opposed to the MeshConfig field). var ValidateProxyConfig = registerValidateFunc("ValidateProxyConfig", func(cfg config.Config) (Warning, error) { spec, ok := cfg.Spec.(*networkingv1beta1.ProxyConfig) if !ok { return nil, fmt.Errorf("cannot cast to proxyconfig") } errs := Validation{} errs = appendValidation(errs, validateWorkloadSelector(spec.Selector), validateConcurrency(spec.Concurrency.GetValue()), ) return errs.Unwrap() }) func validateConcurrency(concurrency int32) (v Validation) { if concurrency < 0 { v = appendErrorf(v, "concurrency must be greater than or equal to 0") } return } // ValidateTelemetry validates a Telemetry. var ValidateTelemetry = registerValidateFunc("ValidateTelemetry", func(cfg config.Config) (Warning, error) { spec, ok := cfg.Spec.(*telemetry.Telemetry) if !ok { return nil, fmt.Errorf("cannot cast to telemetry") } errs := Validation{} errs = appendValidation(errs, validateWorkloadSelector(spec.Selector), validateTelemetryMetrics(spec.Metrics), validateTelemetryTracing(spec.Tracing), validateTelemetryAccessLogging(spec.AccessLogging), ) return errs.Unwrap() }) func validateTelemetryAccessLogging(logging []*telemetry.AccessLogging) (v Validation) { if len(logging) > 1 { v = appendWarningf(v, "multiple accessLogging is not currently supported") } for idx, l := range logging { if l == nil { continue } if len(l.Providers) > 1 { v = appendValidation(v, Warningf("accessLogging[%d]: multiple providers is not currently supported", idx)) } if l.Filter != nil { v = appendValidation(v, validateTelemetryFilter(l.Filter)) } v = appendValidation(v, validateTelemetryProviders(l.Providers)) } return } func validateTelemetryTracing(tracing []*telemetry.Tracing) (v Validation) { if len(tracing) > 1 { v = appendWarningf(v, "multiple tracing is not currently supported") } for _, l := range tracing { if l == nil { continue } if len(l.Providers) > 1 { v = appendWarningf(v, "multiple providers is not currently supported") } v = appendValidation(v, validateTelemetryProviders(l.Providers)) if l.RandomSamplingPercentage.GetValue() < 0 || l.RandomSamplingPercentage.GetValue() > 100 { v = appendErrorf(v, "randomSamplingPercentage must be in range [0.0, 100.0]") } for name, tag := range l.CustomTags { if name == "" { v = appendErrorf(v, "tag name may not be empty") } if tag == nil { v = appendErrorf(v, "tag '%s' may not have a nil value", name) continue } switch t := tag.Type.(type) { case *telemetry.Tracing_CustomTag_Literal: if t.Literal.GetValue() == "" { v = appendErrorf(v, "literal tag value may not be empty") } case *telemetry.Tracing_CustomTag_Header: if t.Header.GetName() == "" { v = appendErrorf(v, "header tag name may not be empty") } case *telemetry.Tracing_CustomTag_Environment: if t.Environment.GetName() == "" { v = appendErrorf(v, "environment tag name may not be empty") } } } } return } func validateTelemetryMetrics(metrics []*telemetry.Metrics) (v Validation) { for _, l := range metrics { if l == nil { continue } v = appendValidation(v, validateTelemetryProviders(l.Providers)) for _, o := range l.Overrides { if o == nil { v = appendErrorf(v, "tagOverrides may not be null") continue } for tagName, to := range o.TagOverrides { if tagName == "" { v = appendWarningf(v, "tagOverrides.name may not be empty") } if to == nil { v = appendErrorf(v, "tagOverrides may not be null") continue } switch to.Operation { case telemetry.MetricsOverrides_TagOverride_UPSERT: if to.Value == "" { v = appendErrorf(v, "tagOverrides.value must be set set when operation is UPSERT") } case telemetry.MetricsOverrides_TagOverride_REMOVE: if to.Value != "" { v = appendErrorf(v, "tagOverrides.value may only be set when operation is UPSERT") } } } if o.Match != nil { switch mm := o.Match.MetricMatch.(type) { case *telemetry.MetricSelector_CustomMetric: if mm.CustomMetric == "" { v = appendErrorf(v, "customMetric may not be empty") } } } } } return } func validateTelemetryProviders(providers []*telemetry.ProviderRef) error { for _, p := range providers { if p == nil || p.Name == "" { return fmt.Errorf("providers.name may not be empty") } } return nil } // ValidateWasmPlugin validates a WasmPlugin. var ValidateWasmPlugin = registerValidateFunc("ValidateWasmPlugin", func(cfg config.Config) (Warning, error) { spec, ok := cfg.Spec.(*extensions.WasmPlugin) if !ok { return nil, fmt.Errorf("cannot cast to wasmplugin") } errs := Validation{} errs = appendValidation(errs, validateWorkloadSelector(spec.Selector), validateWasmPluginURL(spec.Url), validateWasmPluginSHA(spec), validateWasmPluginVMConfig(spec.VmConfig), ) return errs.Unwrap() }) func validateWasmPluginURL(pluginURL string) error { if pluginURL == "" { return fmt.Errorf("url field needs to be set") } validSchemes := map[string]bool{ "": true, "file": true, "http": true, "https": true, "oci": true, } u, err := url.Parse(pluginURL) if err != nil { return fmt.Errorf("failed to parse url: %s", err) } if _, found := validSchemes[u.Scheme]; !found { return fmt.Errorf("url contains unsupported scheme: %s", u.Scheme) } return nil } func validateWasmPluginSHA(plugin *extensions.WasmPlugin) error { if plugin.Sha256 == "" { return nil } if len(plugin.Sha256) != 64 { return fmt.Errorf("sha256 field must be 64 characters long") } for _, r := range plugin.Sha256 { if !('a' <= r && r <= 'f' || '0' <= r && r <= '9') { return fmt.Errorf("sha256 field must match [a-f0-9]{64} pattern") } } return nil } func validateWasmPluginVMConfig(vm *extensions.VmConfig) error { if vm == nil || len(vm.Env) == 0 { return nil } keys := sets.New() for _, env := range vm.Env { if env == nil { continue } if env.Name == "" { return fmt.Errorf("spec.vmConfig.env invalid") } if keys.Contains(env.Name) { return fmt.Errorf("duplicate env") } keys.Insert(env.Name) } return nil }