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 }