istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/validation/envoyfilter/envoyfilter.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package envoyfilter
    16  
    17  import (
    18  	"fmt"
    19  	"regexp"
    20  	"strings"
    21  
    22  	udpaa "github.com/cncf/xds/go/udpa/annotations"
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/reflect/protoreflect"
    25  	"google.golang.org/protobuf/reflect/protoregistry"
    26  	"google.golang.org/protobuf/types/descriptorpb"
    27  	"google.golang.org/protobuf/types/known/anypb"
    28  
    29  	networking "istio.io/api/networking/v1alpha3"
    30  	"istio.io/istio/pkg/config"
    31  	"istio.io/istio/pkg/config/validation"
    32  	"istio.io/istio/pkg/config/xds"
    33  	"istio.io/istio/pkg/wellknown"
    34  )
    35  
    36  type (
    37  	Warning    = validation.Warning
    38  	Validation = validation.Validation
    39  )
    40  
    41  // ValidateEnvoyFilter checks envoy filter config supplied by user
    42  var ValidateEnvoyFilter = validation.RegisterValidateFunc("ValidateEnvoyFilter",
    43  	func(cfg config.Config) (Warning, error) {
    44  		errs := Validation{}
    45  		errs = validation.AppendWarningf(errs, "EnvoyFilter exposes internal implementation details that may change at any time. "+
    46  			"Prefer other APIs if possible, and exercise extreme caution, especially around upgrades.")
    47  		return validateEnvoyFilter(cfg, errs)
    48  	})
    49  
    50  func validateEnvoyFilter(cfg config.Config, errs Validation) (Warning, error) {
    51  	rule, ok := cfg.Spec.(*networking.EnvoyFilter)
    52  	if !ok {
    53  		return nil, fmt.Errorf("cannot cast to Envoy filter")
    54  	}
    55  	warning, err := validation.ValidateAlphaWorkloadSelector(rule.WorkloadSelector)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  
    60  	// If workloadSelector is defined and labels are not set, it is most likely
    61  	// an user error. Marking it as a warning to keep it backwards compatible.
    62  	if warning != nil {
    63  		errs = validation.AppendValidation(errs,
    64  			validation.WrapWarning(fmt.Errorf("Envoy filter: %s, will be applied to all services in namespace", warning))) // nolint: stylecheck
    65  	}
    66  
    67  	for _, cp := range rule.ConfigPatches {
    68  		if cp == nil {
    69  			errs = validation.AppendValidation(errs, fmt.Errorf("Envoy filter: null config patch")) // nolint: stylecheck
    70  			continue
    71  		}
    72  		if cp.ApplyTo == networking.EnvoyFilter_INVALID {
    73  			errs = validation.AppendValidation(errs, fmt.Errorf("Envoy filter: missing applyTo")) // nolint: stylecheck
    74  			continue
    75  		}
    76  		if cp.Patch == nil {
    77  			errs = validation.AppendValidation(errs, fmt.Errorf("Envoy filter: missing patch")) // nolint: stylecheck
    78  			continue
    79  		}
    80  		if cp.Patch.Operation == networking.EnvoyFilter_Patch_INVALID {
    81  			errs = validation.AppendValidation(errs, fmt.Errorf("Envoy filter: missing patch operation")) // nolint: stylecheck
    82  			continue
    83  		}
    84  		if cp.Patch.Operation != networking.EnvoyFilter_Patch_REMOVE && cp.Patch.Value == nil {
    85  			errs = validation.AppendValidation(errs, fmt.Errorf("Envoy filter: missing patch value for non-remove operation")) // nolint: stylecheck
    86  			continue
    87  		}
    88  
    89  		// ensure that the supplied regex for proxy version compiles
    90  		if cp.Match != nil && cp.Match.Proxy != nil && cp.Match.Proxy.ProxyVersion != "" {
    91  			if _, err := regexp.Compile(cp.Match.Proxy.ProxyVersion); err != nil {
    92  				errs = validation.AppendValidation(errs, fmt.Errorf("Envoy filter: invalid regex for proxy version, [%v]", err)) // nolint: stylecheck
    93  				continue
    94  			}
    95  		}
    96  		// ensure that applyTo, match and patch all line up
    97  		switch cp.ApplyTo {
    98  		case networking.EnvoyFilter_LISTENER,
    99  			networking.EnvoyFilter_FILTER_CHAIN,
   100  			networking.EnvoyFilter_NETWORK_FILTER,
   101  			networking.EnvoyFilter_HTTP_FILTER:
   102  			if cp.Match != nil && cp.Match.ObjectTypes != nil {
   103  				if cp.Match.GetListener() == nil {
   104  					errs = validation.AppendValidation(errs,
   105  						fmt.Errorf("Envoy filter: applyTo for listener class objects cannot have non listener match")) // nolint: stylecheck
   106  					continue
   107  				}
   108  				listenerMatch := cp.Match.GetListener()
   109  				if listenerMatch.FilterChain != nil {
   110  					if listenerMatch.FilterChain.Filter != nil {
   111  						if cp.ApplyTo == networking.EnvoyFilter_LISTENER || cp.ApplyTo == networking.EnvoyFilter_FILTER_CHAIN {
   112  							// This would be an error but is a warning for backwards compatibility
   113  							errs = validation.AppendValidation(errs, validation.WrapWarning(
   114  								fmt.Errorf("Envoy filter: filter match has no effect when used with %v", cp.ApplyTo))) // nolint: stylecheck
   115  						}
   116  						// filter names are required if network filter matches are being made
   117  						if listenerMatch.FilterChain.Filter.Name == "" {
   118  							errs = validation.AppendValidation(errs, fmt.Errorf("Envoy filter: filter match has no name to match on")) // nolint: stylecheck
   119  							continue
   120  						} else if listenerMatch.FilterChain.Filter.SubFilter != nil {
   121  							// sub filter match is supported only for applyTo HTTP_FILTER
   122  							if cp.ApplyTo != networking.EnvoyFilter_HTTP_FILTER {
   123  								errs = validation.AppendValidation(errs, fmt.Errorf("Envoy filter: subfilter match can be used with applyTo HTTP_FILTER only")) // nolint: stylecheck
   124  								continue
   125  							}
   126  							// sub filter match requires the network filter to match to envoy http connection manager
   127  							if listenerMatch.FilterChain.Filter.Name != wellknown.HTTPConnectionManager &&
   128  								listenerMatch.FilterChain.Filter.Name != "envoy.http_connection_manager" {
   129  								errs = validation.AppendValidation(errs, fmt.Errorf("Envoy filter: subfilter match requires filter match with %s", // nolint: stylecheck
   130  									wellknown.HTTPConnectionManager))
   131  								continue
   132  							}
   133  							if listenerMatch.FilterChain.Filter.SubFilter.Name == "" {
   134  								errs = validation.AppendValidation(errs, fmt.Errorf("Envoy filter: subfilter match has no name to match on")) // nolint: stylecheck
   135  								continue
   136  							}
   137  						}
   138  						errs = validation.AppendValidation(errs, validateListenerMatchName(listenerMatch.FilterChain.Filter.GetName()))
   139  						errs = validation.AppendValidation(errs, validateListenerMatchName(listenerMatch.FilterChain.Filter.GetSubFilter().GetName()))
   140  					}
   141  				}
   142  			}
   143  		case networking.EnvoyFilter_ROUTE_CONFIGURATION, networking.EnvoyFilter_VIRTUAL_HOST, networking.EnvoyFilter_HTTP_ROUTE:
   144  			if cp.Match != nil && cp.Match.ObjectTypes != nil {
   145  				if cp.Match.GetRouteConfiguration() == nil {
   146  					errs = validation.AppendValidation(errs,
   147  						fmt.Errorf("Envoy filter: applyTo for http route class objects cannot have non route configuration match")) // nolint: stylecheck
   148  				}
   149  			}
   150  
   151  		case networking.EnvoyFilter_CLUSTER:
   152  			if cp.Match != nil && cp.Match.ObjectTypes != nil {
   153  				if cp.Match.GetCluster() == nil {
   154  					errs = validation.AppendValidation(errs, fmt.Errorf("Envoy filter: applyTo for cluster class objects cannot have non cluster match")) // nolint: stylecheck
   155  				}
   156  			}
   157  		}
   158  		// ensure that the struct is valid
   159  		if _, err := xds.BuildXDSObjectFromStruct(cp.ApplyTo, cp.Patch.Value, false); err != nil {
   160  			if strings.Contains(err.Error(), "could not resolve Any message type") {
   161  				if strings.Contains(err.Error(), ".v2.") {
   162  					err = fmt.Errorf("referenced type unknown (hint: try using the v3 XDS API): %v", err)
   163  				} else {
   164  					err = fmt.Errorf("referenced type unknown: %v", err)
   165  				}
   166  			}
   167  			errs = validation.AppendValidation(errs, err)
   168  		} else {
   169  			// Run with strict validation, and emit warnings. This helps capture cases like unknown fields
   170  			// We do not want to reject in case the proto is valid but our libraries are outdated
   171  			obj, err := xds.BuildXDSObjectFromStruct(cp.ApplyTo, cp.Patch.Value, true)
   172  			if err != nil {
   173  				errs = validation.AppendValidation(errs, validation.WrapWarning(err))
   174  			}
   175  
   176  			// Append any deprecation notices
   177  			if obj != nil {
   178  				// Note: since we no longer import v2 protos, v2 references will fail during BuildXDSObjectFromStruct.
   179  				errs = validation.AppendValidation(errs, validateDeprecatedFilterTypes(obj))
   180  				errs = validation.AppendValidation(errs, validateMissingTypedConfigFilterTypes(obj))
   181  			}
   182  		}
   183  	}
   184  
   185  	return errs.Unwrap()
   186  }
   187  
   188  func validateListenerMatchName(name string) error {
   189  	if newName, f := xds.ReverseDeprecatedFilterNames[name]; f {
   190  		return validation.WrapWarning(fmt.Errorf("using deprecated filter name %q; use %q instead", name, newName))
   191  	}
   192  	return nil
   193  }
   194  
   195  func recurseDeprecatedTypes(message protoreflect.Message) ([]string, error) {
   196  	var topError error
   197  	var deprecatedTypes []string
   198  	if message == nil {
   199  		return nil, nil
   200  	}
   201  	message.Range(func(descriptor protoreflect.FieldDescriptor, value protoreflect.Value) bool {
   202  		m, isMessage := value.Interface().(protoreflect.Message)
   203  		if isMessage {
   204  			anyMessage, isAny := m.Interface().(*anypb.Any)
   205  			if isAny {
   206  				mt, err := protoregistry.GlobalTypes.FindMessageByURL(anyMessage.TypeUrl)
   207  				if err != nil {
   208  					topError = err
   209  					return false
   210  				}
   211  				var fileOpts proto.Message = mt.Descriptor().ParentFile().Options().(*descriptorpb.FileOptions)
   212  				if proto.HasExtension(fileOpts, udpaa.E_FileStatus) {
   213  					ext := proto.GetExtension(fileOpts, udpaa.E_FileStatus)
   214  					udpaext, ok := ext.(*udpaa.StatusAnnotation)
   215  					if !ok {
   216  						topError = fmt.Errorf("extension was of wrong type: %T", ext)
   217  						return false
   218  					}
   219  					if udpaext.PackageVersionStatus == udpaa.PackageVersionStatus_FROZEN {
   220  						deprecatedTypes = append(deprecatedTypes, anyMessage.TypeUrl)
   221  					}
   222  				}
   223  			}
   224  			newTypes, err := recurseDeprecatedTypes(m)
   225  			if err != nil {
   226  				topError = err
   227  				return false
   228  			}
   229  			deprecatedTypes = append(deprecatedTypes, newTypes...)
   230  		}
   231  		return true
   232  	})
   233  	return deprecatedTypes, topError
   234  }
   235  
   236  // recurseMissingTypedConfig checks that configured filters do not rely on `name` and elide `typed_config`.
   237  // This is temporarily enabled in Envoy by the envoy.reloadable_features.no_extension_lookup_by_name flag, but in the future will be removed.
   238  func recurseMissingTypedConfig(message protoreflect.Message) []string {
   239  	var deprecatedTypes []string
   240  	if message == nil {
   241  		return nil
   242  	}
   243  	// First, iterate over the fields to find the 'name' field to help with reporting errors.
   244  	var name string
   245  	for i := 0; i < message.Type().Descriptor().Fields().Len(); i++ {
   246  		field := message.Type().Descriptor().Fields().Get(i)
   247  		if field.JSONName() == "name" {
   248  			name = fmt.Sprintf("%v", message.Get(field).Interface())
   249  		}
   250  	}
   251  
   252  	hasTypedConfig := false
   253  	requiresTypedConfig := false
   254  	// Now go through fields again
   255  	for i := 0; i < message.Type().Descriptor().Fields().Len(); i++ {
   256  		field := message.Type().Descriptor().Fields().Get(i)
   257  		set := message.Has(field)
   258  		// If it has a typedConfig field, it must be set.
   259  		requiresTypedConfig = requiresTypedConfig || field.JSONName() == "typedConfig"
   260  		// Note: it is possible there is some API that has typedConfig but has a non-deprecated alternative,
   261  		// but I couldn't find any. Worst case, this is a warning, not an error, so a false positive is not so bad.
   262  		// The one exception is configDiscovery (used for ECDS)
   263  		if field.JSONName() == "typedConfig" && set {
   264  			hasTypedConfig = true
   265  		}
   266  		if field.JSONName() == "configDiscovery" && set {
   267  			hasTypedConfig = true
   268  		}
   269  		if set {
   270  			// If the field was set and is a message, recurse into it to check children
   271  			m, isMessage := message.Get(field).Interface().(protoreflect.Message)
   272  			if isMessage {
   273  				deprecatedTypes = append(deprecatedTypes, recurseMissingTypedConfig(m)...)
   274  			}
   275  		}
   276  	}
   277  	if requiresTypedConfig && !hasTypedConfig {
   278  		deprecatedTypes = append(deprecatedTypes, name)
   279  	}
   280  	return deprecatedTypes
   281  }
   282  
   283  func validateDeprecatedFilterTypes(obj proto.Message) error {
   284  	deprecated, err := recurseDeprecatedTypes(obj.ProtoReflect())
   285  	if err != nil {
   286  		return fmt.Errorf("failed to find deprecated types: %v", err)
   287  	}
   288  	if len(deprecated) > 0 {
   289  		return validation.WrapWarning(fmt.Errorf("using deprecated type_url(s); %v", strings.Join(deprecated, ", ")))
   290  	}
   291  	return nil
   292  }
   293  
   294  func validateMissingTypedConfigFilterTypes(obj proto.Message) error {
   295  	missing := recurseMissingTypedConfig(obj.ProtoReflect())
   296  	if len(missing) > 0 {
   297  		return validation.WrapWarning(fmt.Errorf("using deprecated types by name without typed_config; %v", strings.Join(missing, ", ")))
   298  	}
   299  	return nil
   300  }