istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/validation/agent/validation.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 agent 16 17 import ( 18 "errors" 19 "fmt" 20 "net" 21 "net/netip" 22 "strconv" 23 "strings" 24 "time" 25 26 "github.com/hashicorp/go-multierror" 27 "google.golang.org/protobuf/types/known/durationpb" 28 29 meshconfig "istio.io/api/mesh/v1alpha1" 30 networking "istio.io/api/networking/v1alpha3" 31 "istio.io/istio/pilot/pkg/features" 32 "istio.io/istio/pilot/pkg/serviceregistry/util/label" 33 "istio.io/istio/pkg/config/labels" 34 "istio.io/istio/pkg/config/security" 35 "istio.io/istio/pkg/log" 36 netutil "istio.io/istio/pkg/util/net" 37 "istio.io/istio/pkg/util/sets" 38 ) 39 40 // Constants for duration fields 41 const ( 42 // Set some high upper bound to avoid weird configurations 43 // nolint: revive 44 connectTimeoutMax = time.Hour 45 // nolint: revive 46 connectTimeoutMin = time.Millisecond 47 ) 48 49 var scope = log.RegisterScope("validation", "CRD validation debugging") 50 51 type Warning error 52 53 // Validation holds errors and warnings. They can be joined with additional errors by called AppendValidation 54 type Validation struct { 55 Err error 56 Warning Warning 57 } 58 59 func (v Validation) Unwrap() (Warning, error) { 60 return v.Warning, v.Err 61 } 62 63 func (v Validation) Error() string { 64 if v.Err == nil { 65 return "" 66 } 67 return v.Err.Error() 68 } 69 70 // WrapWarning turns an error into a Validation as a warning 71 func WrapWarning(e error) Validation { 72 return Validation{Warning: e} 73 } 74 75 // wrapper around multierror.Append that enforces the invariant that if all input errors are nil, the output 76 // error is nil (allowing validation without branching). 77 func AppendValidation(v Validation, vs ...error) Validation { 78 appendError := func(err, err2 error) error { 79 if err == nil { 80 return err2 81 } else if err2 == nil { 82 return err 83 } 84 return multierror.Append(err, err2) 85 } 86 87 for _, nv := range vs { 88 switch t := nv.(type) { 89 case Validation: 90 v.Err = appendError(v.Err, t.Err) 91 v.Warning = appendError(v.Warning, t.Warning) 92 default: 93 v.Err = appendError(v.Err, t) 94 } 95 } 96 return v 97 } 98 99 // AppendWarningf appends a formatted warning string 100 // nolint: unparam 101 func AppendWarningf(v Validation, format string, a ...any) Validation { 102 return AppendValidation(v, Warningf(format, a...)) 103 } 104 105 // Warningf formats according to a format specifier and returns the string as a 106 // value that satisfies error. Like Errorf, but for warnings. 107 func Warningf(format string, a ...any) Validation { 108 return WrapWarning(fmt.Errorf(format, a...)) 109 } 110 111 // wrapper around multierror.Append that enforces the invariant that if all input errors are nil, the output 112 // error is nil (allowing validation without branching). 113 func AppendErrors(err error, errs ...error) error { 114 appendError := func(err, err2 error) error { 115 if err == nil { 116 return err2 117 } else if err2 == nil { 118 return err 119 } 120 return multierror.Append(err, err2) 121 } 122 123 for _, err2 := range errs { 124 switch t := err2.(type) { 125 case Validation: 126 err = appendError(err, t.Err) 127 default: 128 err = appendError(err, err2) 129 } 130 } 131 return err 132 } 133 134 // ValidateDuration checks that a proto duration is well-formed 135 func ValidateDuration(pd *durationpb.Duration) error { 136 dur := pd.AsDuration() 137 if dur < time.Millisecond { 138 return errors.New("duration must be greater than 1ms") 139 } 140 if dur%time.Millisecond != 0 { 141 return errors.New("only durations to ms precision are supported") 142 } 143 return nil 144 } 145 146 // ValidateDurationRange verifies range is in specified duration 147 func ValidateDurationRange(dur, min, max time.Duration) error { 148 if dur > max || dur < min { 149 return fmt.Errorf("time %v must be >%v and <%v", dur.String(), min.String(), max.String()) 150 } 151 152 return nil 153 } 154 155 // ValidateDrainDuration checks that parent and drain durations are valid 156 func ValidateDrainDuration(drainTime *durationpb.Duration) (errs error) { 157 if err := ValidateDuration(drainTime); err != nil { 158 errs = multierror.Append(errs, multierror.Prefix(err, "invalid drain duration:")) 159 } 160 if errs != nil { 161 return 162 } 163 164 drainDuration := drainTime.AsDuration() 165 166 if drainDuration%time.Second != 0 { 167 errs = multierror.Append(errs, 168 errors.New("drain time only supports durations to seconds precision")) 169 } 170 return 171 } 172 173 // ValidatePort checks that the network port is in range 174 func ValidatePort(port int) error { 175 if 1 <= port && port <= 65535 { 176 return nil 177 } 178 return fmt.Errorf("port number %d must be in the range 1..65535", port) 179 } 180 181 // encapsulates DNS 1123 checks common to both wildcarded hosts and FQDNs 182 func CheckDNS1123Preconditions(name string) error { 183 if len(name) > 255 { 184 return fmt.Errorf("domain name %q too long (max 255)", name) 185 } 186 if len(name) == 0 { 187 return fmt.Errorf("empty domain name not allowed") 188 } 189 return nil 190 } 191 192 func ValidateDNS1123Labels(domain string) error { 193 parts := strings.Split(domain, ".") 194 topLevelDomain := parts[len(parts)-1] 195 if _, err := strconv.Atoi(topLevelDomain); err == nil { 196 return fmt.Errorf("domain name %q invalid (top level domain %q cannot be all-numeric)", domain, topLevelDomain) 197 } 198 for i, label := range parts { 199 // Allow the last part to be empty, for unambiguous names like `istio.io.` 200 if i == len(parts)-1 && label == "" { 201 return nil 202 } 203 if !labels.IsDNS1123Label(label) { 204 return fmt.Errorf("domain name %q invalid (label %q invalid)", domain, label) 205 } 206 } 207 return nil 208 } 209 210 // validateCustomTags validates that tracing CustomTags map does not contain any nil items 211 func validateCustomTags(tags map[string]*meshconfig.Tracing_CustomTag) error { 212 for tagName, tagVal := range tags { 213 if tagVal == nil { 214 return fmt.Errorf("encountered nil value for custom tag: %s", tagName) 215 } 216 } 217 return nil 218 } 219 220 // ValidateFQDN checks a fully-qualified domain name 221 func ValidateFQDN(fqdn string) error { 222 if err := CheckDNS1123Preconditions(fqdn); err != nil { 223 return err 224 } 225 return ValidateDNS1123Labels(fqdn) 226 } 227 228 // ValidateProxyAddress checks that a network address is well-formed 229 func ValidateProxyAddress(hostAddr string) error { 230 hostname, p, err := net.SplitHostPort(hostAddr) 231 if err != nil { 232 return fmt.Errorf("unable to split %q: %v", hostAddr, err) 233 } 234 port, err := strconv.Atoi(p) 235 if err != nil { 236 return fmt.Errorf("port (%s) is not a number: %v", p, err) 237 } 238 if err = ValidatePort(port); err != nil { 239 return err 240 } 241 if err = ValidateFQDN(hostname); err != nil { 242 if !netutil.IsValidIPAddress(hostname) { 243 return fmt.Errorf("%q is not a valid hostname or an IP address", hostname) 244 } 245 } 246 247 return nil 248 } 249 250 // ValidateLightstepCollector validates the configuration for sending envoy spans to LightStep 251 func ValidateLightstepCollector(ls *meshconfig.Tracing_Lightstep) error { 252 var errs error 253 if ls.GetAddress() == "" { 254 errs = multierror.Append(errs, errors.New("address is required")) 255 } 256 if err := ValidateProxyAddress(ls.GetAddress()); err != nil { 257 errs = multierror.Append(errs, multierror.Prefix(err, "invalid lightstep address:")) 258 } 259 if ls.GetAccessToken() == "" { 260 errs = multierror.Append(errs, errors.New("access token is required")) 261 } 262 return errs 263 } 264 265 // ValidateZipkinCollector validates the configuration for sending envoy spans to Zipkin 266 func ValidateZipkinCollector(z *meshconfig.Tracing_Zipkin) error { 267 return ValidateProxyAddress(strings.Replace(z.GetAddress(), "$(HOST_IP)", "127.0.0.1", 1)) 268 } 269 270 // ValidateDatadogCollector validates the configuration for sending envoy spans to Datadog 271 func ValidateDatadogCollector(d *meshconfig.Tracing_Datadog) error { 272 // If the address contains $(HOST_IP), replace it with a valid IP before validation. 273 return ValidateProxyAddress(strings.Replace(d.GetAddress(), "$(HOST_IP)", "127.0.0.1", 1)) 274 } 275 276 func ValidateTLS(settings *networking.ClientTLSSettings) (errs error) { 277 if settings == nil { 278 return 279 } 280 281 if settings.GetInsecureSkipVerify().GetValue() { 282 if settings.Mode == networking.ClientTLSSettings_SIMPLE { 283 // In tls simple mode, we can specify ca cert by CaCertificates or CredentialName. 284 if settings.CaCertificates != "" || settings.CredentialName != "" || settings.SubjectAltNames != nil { 285 errs = AppendErrors(errs, fmt.Errorf("cannot specify CaCertificates or CredentialName or SubjectAltNames when InsecureSkipVerify is set true")) 286 } 287 } 288 289 if settings.Mode == networking.ClientTLSSettings_MUTUAL { 290 // In tls mutual mode, we can specify both client cert and ca cert by CredentialName. 291 // However, here we can not distinguish whether user specify ca cert by CredentialName or not. 292 if settings.CaCertificates != "" || settings.SubjectAltNames != nil { 293 errs = AppendErrors(errs, fmt.Errorf("cannot specify CaCertificates or SubjectAltNames when InsecureSkipVerify is set true")) 294 } 295 } 296 } 297 298 if (settings.Mode == networking.ClientTLSSettings_SIMPLE || settings.Mode == networking.ClientTLSSettings_MUTUAL) && 299 settings.CredentialName != "" { 300 if settings.ClientCertificate != "" || settings.CaCertificates != "" || settings.PrivateKey != "" || settings.CaCrl != "" { 301 errs = AppendErrors(errs, 302 fmt.Errorf("cannot specify client certificates or CA certificate or CA CRL If credentialName is set")) 303 } 304 305 // If tls mode is SIMPLE or MUTUAL, and CredentialName is specified, credentials are fetched 306 // remotely. ServerCertificate and CaCertificates fields are not required. 307 return 308 } 309 310 if settings.Mode == networking.ClientTLSSettings_MUTUAL { 311 if settings.ClientCertificate == "" { 312 errs = AppendErrors(errs, fmt.Errorf("client certificate required for mutual tls")) 313 } 314 if settings.PrivateKey == "" { 315 errs = AppendErrors(errs, fmt.Errorf("private key required for mutual tls")) 316 } 317 } 318 319 return 320 } 321 322 // ValidateMeshConfigProxyConfig checks that the mesh config is well-formed 323 func ValidateMeshConfigProxyConfig(config *meshconfig.ProxyConfig) (errs error) { 324 if config.ConfigPath == "" { 325 errs = multierror.Append(errs, errors.New("config path must be set")) 326 } 327 328 if config.BinaryPath == "" { 329 errs = multierror.Append(errs, errors.New("binary path must be set")) 330 } 331 332 clusterName := config.GetClusterName() 333 switch naming := clusterName.(type) { 334 case *meshconfig.ProxyConfig_ServiceCluster: 335 if naming.ServiceCluster == "" { 336 errs = multierror.Append(errs, errors.New("service cluster must be specified")) 337 } 338 case *meshconfig.ProxyConfig_TracingServiceName_: // intentionally left empty for now 339 default: 340 errs = multierror.Append(errs, errors.New("oneof service cluster or tracing service name must be specified")) 341 } 342 343 if err := ValidateDrainDuration(config.DrainDuration); err != nil { 344 errs = multierror.Append(errs, err) 345 } 346 347 // discovery address is mandatory since mutual TLS relies on CDS. 348 // strictly speaking, proxies can operate without RDS/CDS and with hot restarts 349 // but that requires additional test validation 350 if config.DiscoveryAddress == "" { 351 errs = multierror.Append(errs, errors.New("discovery address must be set to the proxy discovery service")) 352 } else if err := ValidateProxyAddress(config.DiscoveryAddress); err != nil { 353 errs = multierror.Append(errs, multierror.Prefix(err, "invalid discovery address:")) 354 } 355 356 if tracer := config.GetTracing().GetLightstep(); tracer != nil { 357 if err := ValidateLightstepCollector(tracer); err != nil { 358 errs = multierror.Append(errs, multierror.Prefix(err, "invalid lightstep config:")) 359 } 360 } 361 362 if tracer := config.GetTracing().GetZipkin(); tracer != nil { 363 if err := ValidateZipkinCollector(tracer); err != nil { 364 errs = multierror.Append(errs, multierror.Prefix(err, "invalid zipkin config:")) 365 } 366 } 367 368 if tracer := config.GetTracing().GetDatadog(); tracer != nil { 369 if err := ValidateDatadogCollector(tracer); err != nil { 370 errs = multierror.Append(errs, multierror.Prefix(err, "invalid datadog config:")) 371 } 372 } 373 374 if tracer := config.GetTracing().GetTlsSettings(); tracer != nil { 375 if err := ValidateTLS(tracer); err != nil { 376 errs = multierror.Append(errs, multierror.Prefix(err, "invalid tracing TLS config:")) 377 } 378 } 379 380 if tracerCustomTags := config.GetTracing().GetCustomTags(); tracerCustomTags != nil { 381 if err := validateCustomTags(tracerCustomTags); err != nil { 382 errs = multierror.Append(errs, multierror.Prefix(err, "invalid tracing custom tags:")) 383 } 384 } 385 386 if config.StatsdUdpAddress != "" { 387 if err := ValidateProxyAddress(config.StatsdUdpAddress); err != nil { 388 errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid statsd udp address %q:", config.StatsdUdpAddress))) 389 } 390 } 391 392 // nolint: staticcheck 393 if config.EnvoyMetricsServiceAddress != "" { 394 if err := ValidateProxyAddress(config.EnvoyMetricsServiceAddress); err != nil { 395 errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid envoy metrics service address %q:", config.EnvoyMetricsServiceAddress))) 396 } else { 397 scope.Warnf("EnvoyMetricsServiceAddress is deprecated, use EnvoyMetricsService instead.") // nolint: stylecheck 398 } 399 } 400 401 if config.EnvoyMetricsService != nil && config.EnvoyMetricsService.Address != "" { 402 if err := ValidateProxyAddress(config.EnvoyMetricsService.Address); err != nil { 403 errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid envoy metrics service address %q:", config.EnvoyMetricsService.Address))) 404 } 405 } 406 407 if config.EnvoyAccessLogService != nil && config.EnvoyAccessLogService.Address != "" { 408 if err := ValidateProxyAddress(config.EnvoyAccessLogService.Address); err != nil { 409 errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid envoy access log service address %q:", config.EnvoyAccessLogService.Address))) 410 } 411 } 412 413 if err := ValidatePort(int(config.ProxyAdminPort)); err != nil { 414 errs = multierror.Append(errs, multierror.Prefix(err, "invalid proxy admin port:")) 415 } 416 417 if err := ValidateControlPlaneAuthPolicy(config.ControlPlaneAuthPolicy); err != nil { 418 errs = multierror.Append(errs, multierror.Prefix(err, "invalid authentication policy:")) 419 } 420 421 if err := ValidatePort(int(config.StatusPort)); err != nil { 422 errs = multierror.Append(errs, multierror.Prefix(err, "invalid status port:")) 423 } 424 425 if pkpConf := config.GetPrivateKeyProvider(); pkpConf != nil { 426 if err := validatePrivateKeyProvider(pkpConf); err != nil { 427 errs = multierror.Append(errs, multierror.Prefix(err, "invalid private key provider configuration:")) 428 } 429 } 430 431 return 432 } 433 434 func ValidateControlPlaneAuthPolicy(policy meshconfig.AuthenticationPolicy) error { 435 if policy == meshconfig.AuthenticationPolicy_NONE || policy == meshconfig.AuthenticationPolicy_MUTUAL_TLS { 436 return nil 437 } 438 return fmt.Errorf("unrecognized control plane auth policy %q", policy) 439 } 440 441 func validatePrivateKeyProvider(pkpConf *meshconfig.PrivateKeyProvider) error { 442 var errs error 443 if pkpConf.GetProvider() == nil { 444 errs = multierror.Append(errs, errors.New("private key provider configuration is required")) 445 } 446 447 switch pkpConf.GetProvider().(type) { 448 case *meshconfig.PrivateKeyProvider_Cryptomb: 449 cryptomb := pkpConf.GetCryptomb() 450 if cryptomb == nil { 451 errs = multierror.Append(errs, errors.New("cryptomb configuration is required")) 452 } else { 453 pollDelay := cryptomb.GetPollDelay() 454 if pollDelay == nil { 455 errs = multierror.Append(errs, errors.New("pollDelay is required")) 456 } else if pollDelay.GetSeconds() == 0 && pollDelay.GetNanos() == 0 { 457 errs = multierror.Append(errs, errors.New("pollDelay must be non zero")) 458 } 459 } 460 case *meshconfig.PrivateKeyProvider_Qat: 461 qatConf := pkpConf.GetQat() 462 if qatConf == nil { 463 errs = multierror.Append(errs, errors.New("qat configuration is required")) 464 } else { 465 pollDelay := qatConf.GetPollDelay() 466 if pollDelay == nil { 467 errs = multierror.Append(errs, errors.New("pollDelay is required")) 468 } else if pollDelay.GetSeconds() == 0 && pollDelay.GetNanos() == 0 { 469 errs = multierror.Append(errs, errors.New("pollDelay must be non zero")) 470 } 471 } 472 default: 473 errs = multierror.Append(errs, errors.New("unknown private key provider")) 474 } 475 476 return errs 477 } 478 479 // ValidateConnectTimeout validates the envoy connection timeout 480 func ValidateConnectTimeout(timeout *durationpb.Duration) error { 481 if err := ValidateDuration(timeout); err != nil { 482 return err 483 } 484 485 err := ValidateDurationRange(timeout.AsDuration(), connectTimeoutMin, connectTimeoutMax) 486 return err 487 } 488 489 // ValidateProtocolDetectionTimeout validates the envoy protocol detection timeout 490 func ValidateProtocolDetectionTimeout(timeout *durationpb.Duration) error { 491 dur := timeout.AsDuration() 492 // 0s is a valid value if trying to disable protocol detection timeout 493 if dur == time.Second*0 { 494 return nil 495 } 496 if dur%time.Millisecond != 0 { 497 return errors.New("only durations to ms precision are supported") 498 } 499 500 return nil 501 } 502 503 // ValidateLocalityLbSetting checks the LocalityLbSetting of MeshConfig 504 func ValidateLocalityLbSetting(lb *networking.LocalityLoadBalancerSetting, outlier *networking.OutlierDetection) (errs Validation) { 505 if lb == nil { 506 return 507 } 508 509 if len(lb.GetDistribute()) > 0 && len(lb.GetFailover()) > 0 { 510 errs = AppendValidation(errs, fmt.Errorf("can not simultaneously specify 'distribute' and 'failover'")) 511 return 512 } 513 514 if len(lb.GetFailover()) > 0 && len(lb.GetFailoverPriority()) > 0 { 515 for _, priorityLabel := range lb.GetFailoverPriority() { 516 switch priorityLabel { 517 case label.LabelTopologyRegion, label.LabelTopologyZone, label.LabelTopologySubzone: 518 errs = AppendValidation(errs, fmt.Errorf("can not simultaneously set 'failover' and topology label '%s' in 'failover_priority'", priorityLabel)) 519 return 520 } 521 } 522 } 523 524 srcLocalities := make([]string, 0, len(lb.GetDistribute())) 525 for _, locality := range lb.GetDistribute() { 526 srcLocalities = append(srcLocalities, locality.From) 527 var totalWeight uint32 528 destLocalities := make([]string, 0) 529 for loc, weight := range locality.To { 530 destLocalities = append(destLocalities, loc) 531 if weight <= 0 || weight > 100 { 532 errs = AppendValidation(errs, fmt.Errorf("locality weight must be in range [1, 100]")) 533 return 534 } 535 totalWeight += weight 536 } 537 if totalWeight != 100 { 538 errs = AppendValidation(errs, fmt.Errorf("total locality weight %v != 100", totalWeight)) 539 return 540 } 541 errs = AppendValidation(errs, validateLocalities(destLocalities)) 542 } 543 544 errs = AppendValidation(errs, validateLocalities(srcLocalities)) 545 546 if (len(lb.GetFailover()) != 0 || len(lb.GetFailoverPriority()) != 0) && outlier == nil { 547 errs = AppendValidation(errs, WrapWarning(fmt.Errorf("outlier detection policy must be provided for failover"))) 548 } 549 550 for _, failover := range lb.GetFailover() { 551 if failover.From == failover.To { 552 errs = AppendValidation(errs, fmt.Errorf("locality lb failover settings must specify different regions")) 553 } 554 if strings.Contains(failover.From, "/") || strings.Contains(failover.To, "/") { 555 errs = AppendValidation(errs, fmt.Errorf("locality lb failover only specify region")) 556 } 557 if strings.Contains(failover.To, "*") || strings.Contains(failover.From, "*") { 558 errs = AppendValidation(errs, fmt.Errorf("locality lb failover region should not contain '*' wildcard")) 559 } 560 } 561 562 return 563 } 564 565 const ( 566 regionIndex int = iota 567 zoneIndex 568 subZoneIndex 569 ) 570 571 func validateLocalities(localities []string) error { 572 regionZoneSubZoneMap := map[string]map[string]map[string]bool{} 573 for _, locality := range localities { 574 if n := strings.Count(locality, "*"); n > 0 { 575 if n > 1 || !strings.HasSuffix(locality, "*") { 576 return fmt.Errorf("locality %s wildcard '*' number can not exceed 1 and must be in the end", locality) 577 } 578 } 579 if _, exist := regionZoneSubZoneMap["*"]; exist { 580 return fmt.Errorf("locality %s overlap with previous specified ones", locality) 581 } 582 583 region, zone, subZone, localityIndex, err := getLocalityParam(locality) 584 if err != nil { 585 return fmt.Errorf("locality %s must not contain empty region/zone/subzone info", locality) 586 } 587 588 switch localityIndex { 589 case regionIndex: 590 if _, exist := regionZoneSubZoneMap[region]; exist { 591 return fmt.Errorf("locality %s overlap with previous specified ones", locality) 592 } 593 regionZoneSubZoneMap[region] = map[string]map[string]bool{"*": {"*": true}} 594 case zoneIndex: 595 if _, exist := regionZoneSubZoneMap[region]; exist { 596 if _, exist := regionZoneSubZoneMap[region]["*"]; exist { 597 return fmt.Errorf("locality %s overlap with previous specified ones", locality) 598 } 599 if _, exist := regionZoneSubZoneMap[region][zone]; exist { 600 return fmt.Errorf("locality %s overlap with previous specified ones", locality) 601 } 602 regionZoneSubZoneMap[region][zone] = map[string]bool{"*": true} 603 } else { 604 regionZoneSubZoneMap[region] = map[string]map[string]bool{zone: {"*": true}} 605 } 606 case subZoneIndex: 607 if _, exist := regionZoneSubZoneMap[region]; exist { 608 if _, exist := regionZoneSubZoneMap[region]["*"]; exist { 609 return fmt.Errorf("locality %s overlap with previous specified ones", locality) 610 } 611 if _, exist := regionZoneSubZoneMap[region][zone]; exist { 612 if regionZoneSubZoneMap[region][zone]["*"] { 613 return fmt.Errorf("locality %s overlap with previous specified ones", locality) 614 } 615 if regionZoneSubZoneMap[region][zone][subZone] { 616 return fmt.Errorf("locality %s overlap with previous specified ones", locality) 617 } 618 regionZoneSubZoneMap[region][zone][subZone] = true 619 } else { 620 regionZoneSubZoneMap[region][zone] = map[string]bool{subZone: true} 621 } 622 } else { 623 regionZoneSubZoneMap[region] = map[string]map[string]bool{zone: {subZone: true}} 624 } 625 } 626 } 627 628 return nil 629 } 630 631 func getLocalityParam(locality string) (string, string, string, int, error) { 632 var region, zone, subZone string 633 items := strings.SplitN(locality, "/", 3) 634 for i, item := range items { 635 if item == "" { 636 return "", "", "", -1, errors.New("item is nil") 637 } 638 switch i { 639 case regionIndex: 640 region = items[i] 641 case zoneIndex: 642 zone = items[i] 643 case subZoneIndex: 644 subZone = items[i] 645 } 646 } 647 return region, zone, subZone, len(items) - 1, nil 648 } 649 650 func validateServiceSettings(config *meshconfig.MeshConfig) (errs error) { 651 for sIndex, s := range config.ServiceSettings { 652 for _, h := range s.Hosts { 653 if err := ValidateWildcardDomain(h); err != nil { 654 errs = multierror.Append(errs, fmt.Errorf("serviceSettings[%d], host `%s`: %v", sIndex, h, err)) 655 } 656 } 657 } 658 return 659 } 660 661 // ValidateWildcardDomain checks that a domain is a valid FQDN, but also allows wildcard prefixes. 662 func ValidateWildcardDomain(domain string) error { 663 if err := CheckDNS1123Preconditions(domain); err != nil { 664 return err 665 } 666 // We only allow wildcards in the first label; split off the first label (parts[0]) from the rest of the host (parts[1]) 667 parts := strings.SplitN(domain, ".", 2) 668 if !labels.IsWildcardDNS1123Label(parts[0]) { 669 return fmt.Errorf("domain name %q invalid (label %q invalid)", domain, parts[0]) 670 } else if len(parts) > 1 { 671 return ValidateDNS1123Labels(parts[1]) 672 } 673 return nil 674 } 675 676 // validate the trust domain format 677 func ValidateTrustDomain(domain string) error { 678 if len(domain) == 0 { 679 return fmt.Errorf("empty domain name not allowed") 680 } 681 parts := strings.Split(domain, ".") 682 for i, label := range parts { 683 // Allow the last part to be empty, for unambiguous names like `istio.io.` 684 if i == len(parts)-1 && label == "" { 685 return nil 686 } 687 if !labels.IsDNS1123Label(label) { 688 return fmt.Errorf("trust domain name %q invalid", domain) 689 } 690 } 691 return nil 692 } 693 694 func validateTrustDomainConfig(config *meshconfig.MeshConfig) (errs error) { 695 if err := ValidateTrustDomain(config.TrustDomain); err != nil { 696 errs = multierror.Append(errs, fmt.Errorf("trustDomain: %v", err)) 697 } 698 for i, tda := range config.TrustDomainAliases { 699 if err := ValidateTrustDomain(tda); err != nil { 700 errs = multierror.Append(errs, fmt.Errorf("trustDomainAliases[%d], domain `%s` : %v", i, tda, err)) 701 } 702 } 703 return 704 } 705 706 func ValidateMeshTLSConfig(mesh *meshconfig.MeshConfig) (errs error) { 707 if meshMTLS := mesh.MeshMTLS; meshMTLS != nil { 708 if meshMTLS.EcdhCurves != nil { 709 errs = multierror.Append(errs, errors.New("mesh TLS does not support ECDH curves configuration")) 710 } 711 } 712 return errs 713 } 714 715 func ValidateMeshTLSDefaults(mesh *meshconfig.MeshConfig) (v Validation) { 716 unrecognizedECDHCurves := sets.New[string]() 717 validECDHCurves := sets.New[string]() 718 duplicateECDHCurves := sets.New[string]() 719 if tlsDefaults := mesh.TlsDefaults; tlsDefaults != nil { 720 for _, cs := range tlsDefaults.EcdhCurves { 721 if !security.IsValidECDHCurve(cs) { 722 unrecognizedECDHCurves.Insert(cs) 723 } else if validECDHCurves.InsertContains(cs) { 724 duplicateECDHCurves.Insert(cs) 725 } 726 } 727 } 728 729 if len(unrecognizedECDHCurves) > 0 { 730 v = AppendWarningf(v, "detected unrecognized ECDH curves: %v", sets.SortedList(unrecognizedECDHCurves)) 731 } 732 733 if len(duplicateECDHCurves) > 0 { 734 v = AppendWarningf(v, "detected duplicate ECDH curves: %v", sets.SortedList(duplicateECDHCurves)) 735 } 736 return 737 } 738 739 // ValidateMeshConfig checks that the mesh config is well-formed 740 func ValidateMeshConfig(mesh *meshconfig.MeshConfig) (Warning, error) { 741 v := Validation{} 742 if err := ValidatePort(int(mesh.ProxyListenPort)); err != nil { 743 v = AppendValidation(v, multierror.Prefix(err, "invalid proxy listen port:")) 744 } 745 746 if err := ValidateConnectTimeout(mesh.ConnectTimeout); err != nil { 747 v = AppendValidation(v, multierror.Prefix(err, "invalid connect timeout:")) 748 } 749 750 if err := ValidateProtocolDetectionTimeout(mesh.ProtocolDetectionTimeout); err != nil { 751 v = AppendValidation(v, multierror.Prefix(err, "invalid protocol detection timeout:")) 752 } 753 754 if mesh.DefaultConfig == nil { 755 v = AppendValidation(v, errors.New("missing default config")) 756 } else { 757 v = AppendValidation(v, ValidateMeshConfigProxyConfig(mesh.DefaultConfig)) 758 } 759 760 v = AppendValidation(v, ValidateLocalityLbSetting(mesh.LocalityLbSetting, &networking.OutlierDetection{})) 761 v = AppendValidation(v, validateServiceSettings(mesh)) 762 v = AppendValidation(v, validateTrustDomainConfig(mesh)) 763 764 if err := validateExtensionProvider(mesh); err != nil { 765 scope.Warnf("found invalid extension provider (can be ignored if the given extension provider is not used): %v", err) 766 } 767 768 v = AppendValidation(v, ValidateMeshTLSConfig(mesh)) 769 770 v = AppendValidation(v, ValidateMeshTLSDefaults(mesh)) 771 772 return v.Unwrap() 773 } 774 775 // ValidateIPAddress validates that a string in "CIDR notation" or "Dot-decimal notation" 776 func ValidateIPAddress(addr string) error { 777 if _, err := netip.ParseAddr(addr); err != nil { 778 return fmt.Errorf("%v is not a valid IP", addr) 779 } 780 781 return nil 782 } 783 784 func ValidatePartialWildCard(host string) error { 785 if strings.Contains(host, "*") && len(host) != 1 && !strings.HasPrefix(host, "*.") { 786 return fmt.Errorf("partial wildcard %q not allowed", host) 787 } 788 return nil 789 } 790 791 // validates that hostname in ns/<hostname> is a valid hostname according to 792 // API specs 793 func validateSidecarOrGatewayHostnamePart(hostname string, isGateway bool) (errs error) { 794 // short name hosts are not allowed 795 if hostname != "*" && !strings.Contains(hostname, ".") { 796 errs = AppendErrors(errs, fmt.Errorf("short names (non FQDN) are not allowed")) 797 } 798 799 if err := ValidateWildcardDomain(hostname); err != nil { 800 if !isGateway { 801 errs = AppendErrors(errs, err) 802 } 803 804 // Gateway allows IP as the host string, as well 805 if !netutil.IsValidIPAddress(hostname) { 806 errs = AppendErrors(errs, err) 807 } 808 } 809 // partial wildcard is not allowed 810 // More details please refer to: 811 // Gateway: https://istio.io/latest/docs/reference/config/networking/gateway/ 812 // SideCar: https://istio.io/latest/docs/reference/config/networking/sidecar/#IstioEgressListener 813 errs = AppendErrors(errs, ValidatePartialWildCard(hostname)) 814 return 815 } 816 817 func ValidateNamespaceSlashWildcardHostname(hostname string, isGateway bool, gatewaySemantics bool) (errs error) { 818 parts := strings.SplitN(hostname, "/", 2) 819 if len(parts) != 2 { 820 if isGateway { 821 // Old style host in the gateway 822 return validateSidecarOrGatewayHostnamePart(hostname, true) 823 } 824 errs = AppendErrors(errs, fmt.Errorf("host must be of form namespace/dnsName")) 825 return 826 } 827 828 if len(parts[0]) == 0 || len(parts[1]) == 0 { 829 errs = AppendErrors(errs, fmt.Errorf("config namespace and dnsName in host entry cannot be empty")) 830 } 831 832 if !isGateway { 833 // namespace can be * or . or ~ or a valid DNS label in sidecars 834 if parts[0] != "*" && parts[0] != "." && parts[0] != "~" { 835 if !labels.IsDNS1123Label(parts[0]) { 836 errs = AppendErrors(errs, fmt.Errorf("invalid namespace value %q in sidecar", parts[0])) 837 } 838 } 839 } else { 840 // namespace can be * or . or a valid DNS label in gateways 841 // namespace can be ~ in gateways converted from Gateway API when no routes match 842 if parts[0] != "*" && parts[0] != "." && (parts[0] != "~" || !gatewaySemantics) { 843 if !labels.IsDNS1123Label(parts[0]) { 844 errs = AppendErrors(errs, fmt.Errorf("invalid namespace value %q in gateway", parts[0])) 845 } 846 } 847 } 848 errs = AppendErrors(errs, validateSidecarOrGatewayHostnamePart(parts[1], isGateway)) 849 return 850 } 851 852 // ValidateIPSubnet checks that a string is in "CIDR notation" or "Dot-decimal notation" 853 func ValidateIPSubnet(subnet string) error { 854 // We expect a string in "CIDR notation" or "Dot-decimal notation" 855 // E.g., a.b.c.d/xx form or just a.b.c.d or 2001:1::1/64 856 if strings.Count(subnet, "/") == 1 { 857 // We expect a string in "CIDR notation", i.e. a.b.c.d/xx or 2001:1::1/64 form 858 if _, err := netip.ParsePrefix(subnet); err != nil { 859 return fmt.Errorf("%v is not a valid CIDR block", subnet) 860 } 861 862 return nil 863 } 864 return ValidateIPAddress(subnet) 865 } 866 867 func validateNetwork(network *meshconfig.Network) (errs error) { 868 for _, n := range network.Endpoints { 869 switch e := n.Ne.(type) { 870 case *meshconfig.Network_NetworkEndpoints_FromCidr: 871 if err := ValidateIPSubnet(e.FromCidr); err != nil { 872 errs = multierror.Append(errs, err) 873 } 874 case *meshconfig.Network_NetworkEndpoints_FromRegistry: 875 if ok := labels.IsDNS1123Label(e.FromRegistry); !ok { 876 errs = multierror.Append(errs, fmt.Errorf("invalid registry name: %v", e.FromRegistry)) 877 } 878 } 879 } 880 for _, n := range network.Gateways { 881 switch g := n.Gw.(type) { 882 case *meshconfig.Network_IstioNetworkGateway_RegistryServiceName: 883 if err := ValidateFQDN(g.RegistryServiceName); err != nil { 884 errs = multierror.Append(errs, err) 885 } 886 case *meshconfig.Network_IstioNetworkGateway_Address: 887 if ipErr := ValidateIPAddress(g.Address); ipErr != nil { 888 if !features.ResolveHostnameGateways { 889 err := fmt.Errorf("%v (hostname is allowed if RESOLVE_HOSTNAME_GATEWAYS is enabled)", ipErr) 890 errs = multierror.Append(errs, err) 891 } else if fqdnErr := ValidateFQDN(g.Address); fqdnErr != nil { 892 errs = multierror.Append(fmt.Errorf("%v is not a valid IP address or DNS name", g.Address)) 893 } 894 } 895 } 896 if err := ValidatePort(int(n.Port)); err != nil { 897 errs = multierror.Append(errs, err) 898 } 899 } 900 return 901 } 902 903 // ValidateMeshNetworks validates meshnetworks. 904 func ValidateMeshNetworks(meshnetworks *meshconfig.MeshNetworks) (errs error) { 905 // TODO validate using the same gateway on multiple networks? 906 for name, network := range meshnetworks.Networks { 907 if err := validateNetwork(network); err != nil { 908 errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid network %v:", name))) 909 } 910 } 911 return 912 }