istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/endpoints/endpoint_builder.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 endpoints 16 17 import ( 18 "math" 19 "net" 20 "sort" 21 "strconv" 22 "strings" 23 24 corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 25 endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" 26 "google.golang.org/protobuf/types/known/structpb" 27 "google.golang.org/protobuf/types/known/wrapperspb" 28 29 "istio.io/api/networking/v1alpha3" 30 "istio.io/istio/pilot/pkg/features" 31 "istio.io/istio/pilot/pkg/model" 32 "istio.io/istio/pilot/pkg/networking/core/loadbalancer" 33 "istio.io/istio/pilot/pkg/networking/util" 34 "istio.io/istio/pkg/cluster" 35 "istio.io/istio/pkg/config/constants" 36 "istio.io/istio/pkg/config/host" 37 "istio.io/istio/pkg/config/labels" 38 "istio.io/istio/pkg/config/schema/kind" 39 istiolog "istio.io/istio/pkg/log" 40 "istio.io/istio/pkg/network" 41 "istio.io/istio/pkg/slices" 42 "istio.io/istio/pkg/util/hash" 43 netutil "istio.io/istio/pkg/util/net" 44 ) 45 46 var ( 47 Separator = []byte{'~'} 48 Slash = []byte{'/'} 49 50 // same as the above "xds" package 51 log = istiolog.RegisterScope("ads", "ads debugging") 52 ) 53 54 // ConnectOriginate is the name for the resources associated with the origination of HTTP CONNECT. 55 // Duplicated from v1alpha3/waypoint.go to avoid import cycle 56 const connectOriginate = "connect_originate" 57 58 type EndpointBuilder struct { 59 // These fields define the primary key for an endpoint, and can be used as a cache key 60 clusterName string 61 network network.ID 62 proxyView model.ProxyView 63 clusterID cluster.ID 64 locality *corev3.Locality 65 destinationRule *model.ConsolidatedDestRule 66 service *model.Service 67 clusterLocal bool 68 nodeType model.NodeType 69 failoverPriorityLabels []byte 70 71 // These fields are provided for convenience only 72 subsetName string 73 subsetLabels labels.Instance 74 hostname host.Name 75 port int 76 push *model.PushContext 77 proxy *model.Proxy 78 dir model.TrafficDirection 79 80 mtlsChecker *mtlsChecker 81 } 82 83 func NewEndpointBuilder(clusterName string, proxy *model.Proxy, push *model.PushContext) EndpointBuilder { 84 dir, subsetName, hostname, port := model.ParseSubsetKey(clusterName) 85 86 svc := push.ServiceForHostname(proxy, hostname) 87 var dr *model.ConsolidatedDestRule 88 if svc != nil { 89 dr = proxy.SidecarScope.DestinationRule(model.TrafficDirectionOutbound, proxy, svc.Hostname) 90 } 91 92 return *NewCDSEndpointBuilder( 93 proxy, push, clusterName, 94 dir, subsetName, hostname, port, 95 svc, dr, 96 ) 97 } 98 99 // NewCDSEndpointBuilder allows setting some fields directly when we already 100 // have the Service and DestinationRule. 101 func NewCDSEndpointBuilder( 102 proxy *model.Proxy, push *model.PushContext, clusterName string, 103 dir model.TrafficDirection, subsetName string, hostname host.Name, port int, 104 service *model.Service, dr *model.ConsolidatedDestRule, 105 ) *EndpointBuilder { 106 b := EndpointBuilder{ 107 clusterName: clusterName, 108 network: proxy.Metadata.Network, 109 proxyView: proxy.GetView(), 110 clusterID: proxy.Metadata.ClusterID, 111 locality: proxy.Locality, 112 destinationRule: dr, 113 service: service, 114 clusterLocal: push.IsClusterLocal(service), 115 nodeType: proxy.Type, 116 117 subsetName: subsetName, 118 hostname: hostname, 119 port: port, 120 push: push, 121 proxy: proxy, 122 dir: dir, 123 } 124 b.populateSubsetInfo() 125 b.populateFailoverPriorityLabels() 126 return &b 127 } 128 129 func (b *EndpointBuilder) servicePort(port int) *model.Port { 130 if !b.ServiceFound() { 131 log.Debugf("can not find the service %s for cluster %s", b.hostname, b.clusterName) 132 return nil 133 } 134 svcPort, f := b.service.Ports.GetByPort(port) 135 if !f { 136 log.Debugf("can not find the service port %d for cluster %s", b.port, b.clusterName) 137 return nil 138 } 139 return svcPort 140 } 141 142 func (b *EndpointBuilder) WithSubset(subset string) *EndpointBuilder { 143 if b == nil { 144 return nil 145 } 146 subsetBuilder := *b 147 subsetBuilder.subsetName = subset 148 subsetBuilder.populateSubsetInfo() 149 return &subsetBuilder 150 } 151 152 func (b *EndpointBuilder) populateSubsetInfo() { 153 if b.dir == model.TrafficDirectionInboundVIP { 154 b.subsetName = strings.TrimPrefix(b.subsetName, "http/") 155 b.subsetName = strings.TrimPrefix(b.subsetName, "tcp/") 156 } 157 b.mtlsChecker = newMtlsChecker(b.push, b.port, b.destinationRule.GetRule(), b.subsetName) 158 b.subsetLabels = getSubSetLabels(b.DestinationRule(), b.subsetName) 159 } 160 161 func (b *EndpointBuilder) populateFailoverPriorityLabels() { 162 enableFailover, lb := getOutlierDetectionAndLoadBalancerSettings(b.DestinationRule(), b.port, b.subsetName) 163 if enableFailover { 164 lbSetting := loadbalancer.GetLocalityLbSetting(b.push.Mesh.GetLocalityLbSetting(), lb.GetLocalityLbSetting()) 165 if lbSetting != nil && lbSetting.Distribute == nil && 166 len(lbSetting.FailoverPriority) > 0 && (lbSetting.Enabled == nil || lbSetting.Enabled.Value) { 167 b.failoverPriorityLabels = util.GetFailoverPriorityLabels(b.proxy.Labels, lbSetting.FailoverPriority) 168 } 169 } 170 } 171 172 func (b *EndpointBuilder) DestinationRule() *v1alpha3.DestinationRule { 173 if dr := b.destinationRule.GetRule(); dr != nil { 174 dr, _ := dr.Spec.(*v1alpha3.DestinationRule) 175 return dr 176 } 177 return nil 178 } 179 180 func (b *EndpointBuilder) Type() string { 181 return model.EDSType 182 } 183 184 func (b *EndpointBuilder) ServiceFound() bool { 185 return b.service != nil 186 } 187 188 func (b *EndpointBuilder) IsDNSCluster() bool { 189 return b.service != nil && (b.service.Resolution == model.DNSLB || b.service.Resolution == model.DNSRoundRobinLB) 190 } 191 192 // Key provides the eds cache key and should include any information that could change the way endpoints are generated. 193 func (b *EndpointBuilder) Key() any { 194 // nolint: gosec 195 // Not security sensitive code 196 h := hash.New() 197 b.WriteHash(h) 198 return h.Sum64() 199 } 200 201 func (b *EndpointBuilder) WriteHash(h hash.Hash) { 202 if b == nil { 203 return 204 } 205 h.WriteString(b.clusterName) 206 h.Write(Separator) 207 h.WriteString(string(b.network)) 208 h.Write(Separator) 209 h.WriteString(string(b.clusterID)) 210 h.Write(Separator) 211 h.WriteString(string(b.nodeType)) 212 h.Write(Separator) 213 h.WriteString(strconv.FormatBool(b.clusterLocal)) 214 h.Write(Separator) 215 if b.proxy != nil { 216 h.WriteString(strconv.FormatBool(b.proxy.IsProxylessGrpc())) 217 h.Write(Separator) 218 h.WriteString(strconv.FormatBool(bool(b.proxy.Metadata.DisableHBONESend))) 219 h.Write(Separator) 220 } 221 h.WriteString(util.LocalityToString(b.locality)) 222 h.Write(Separator) 223 if len(b.failoverPriorityLabels) > 0 { 224 h.Write(b.failoverPriorityLabels) 225 h.Write(Separator) 226 } 227 if b.service.Attributes.NodeLocal { 228 h.WriteString(b.proxy.GetNodeName()) 229 h.Write(Separator) 230 } 231 232 if b.push != nil && b.push.AuthnPolicies != nil { 233 h.WriteString(b.push.AuthnPolicies.GetVersion()) 234 } 235 h.Write(Separator) 236 237 for _, dr := range b.destinationRule.GetFrom() { 238 h.WriteString(dr.Name) 239 h.Write(Slash) 240 h.WriteString(dr.Namespace) 241 } 242 h.Write(Separator) 243 244 if b.service != nil { 245 h.WriteString(string(b.service.Hostname)) 246 h.Write(Slash) 247 h.WriteString(b.service.Attributes.Namespace) 248 } 249 h.Write(Separator) 250 251 if b.proxyView != nil { 252 h.WriteString(b.proxyView.String()) 253 } 254 h.Write(Separator) 255 } 256 257 func (b *EndpointBuilder) Cacheable() bool { 258 // If service is not defined, we cannot do any caching as we will not have a way to 259 // invalidate the results. 260 // Service being nil means the EDS will be empty anyways, so not much lost here. 261 return b.service != nil 262 } 263 264 func (b *EndpointBuilder) DependentConfigs() []model.ConfigHash { 265 drs := b.destinationRule.GetFrom() 266 configs := make([]model.ConfigHash, 0, len(drs)+1) 267 if b.destinationRule != nil { 268 for _, dr := range drs { 269 configs = append(configs, model.ConfigKey{ 270 Kind: kind.DestinationRule, 271 Name: dr.Name, Namespace: dr.Namespace, 272 }.HashCode()) 273 } 274 } 275 if b.service != nil { 276 configs = append(configs, model.ConfigKey{ 277 Kind: kind.ServiceEntry, 278 Name: string(b.service.Hostname), Namespace: b.service.Attributes.Namespace, 279 }.HashCode()) 280 } 281 282 // For now, this matches clusterCache's DependentConfigs. If adding anything here, we may need to add them there. 283 284 return configs 285 } 286 287 type LocalityEndpoints struct { 288 istioEndpoints []*model.IstioEndpoint 289 // The protobuf message which contains LbEndpoint slice. 290 llbEndpoints endpoint.LocalityLbEndpoints 291 } 292 293 func (e *LocalityEndpoints) append(ep *model.IstioEndpoint, le *endpoint.LbEndpoint) { 294 e.istioEndpoints = append(e.istioEndpoints, ep) 295 e.llbEndpoints.LbEndpoints = append(e.llbEndpoints.LbEndpoints, le) 296 } 297 298 func (e *LocalityEndpoints) refreshWeight() { 299 var weight *wrapperspb.UInt32Value 300 if len(e.llbEndpoints.LbEndpoints) == 0 { 301 weight = nil 302 } else { 303 weight = &wrapperspb.UInt32Value{} 304 for _, lbEp := range e.llbEndpoints.LbEndpoints { 305 weight.Value += lbEp.GetLoadBalancingWeight().Value 306 } 307 } 308 e.llbEndpoints.LoadBalancingWeight = weight 309 } 310 311 func (e *LocalityEndpoints) AssertInvarianceInTest() { 312 if len(e.llbEndpoints.LbEndpoints) != len(e.istioEndpoints) { 313 panic(" len(e.llbEndpoints.LbEndpoints) != len(e.tunnelMetadata)") 314 } 315 } 316 317 // FromServiceEndpoints builds LocalityLbEndpoints from the PushContext's snapshotted ServiceIndex. 318 // Used for CDS (ClusterLoadAssignment constructed elsewhere). 319 func (b *EndpointBuilder) FromServiceEndpoints() []*endpoint.LocalityLbEndpoints { 320 if b == nil { 321 return nil 322 } 323 svcEps := b.push.ServiceEndpointsByPort(b.service, b.port, b.subsetLabels) 324 // don't use the pre-computed endpoints for CDS to preserve previous behavior 325 return ExtractEnvoyEndpoints(b.generate(svcEps)) 326 } 327 328 // BuildClusterLoadAssignment converts the shards for this EndpointBuilder's Service 329 // into a ClusterLoadAssignment. Used for EDS. 330 func (b *EndpointBuilder) BuildClusterLoadAssignment(endpointIndex *model.EndpointIndex) *endpoint.ClusterLoadAssignment { 331 svcPort := b.servicePort(b.port) 332 if svcPort == nil { 333 return buildEmptyClusterLoadAssignment(b.clusterName) 334 } 335 svcEps := b.snapshotShards(endpointIndex) 336 svcEps = slices.FilterInPlace(svcEps, func(ep *model.IstioEndpoint) bool { 337 // filter out endpoints that don't match the service port 338 if svcPort.Name != ep.ServicePortName { 339 return false 340 } 341 // filter out endpoint that has invalid ip address, mostly domain name. Because this is generated from ServiceEntry. 342 // There are other two cases that should not be filtered out: 343 // 1. ep.Address can be empty since https://github.com/istio/istio/pull/45150, in this case we will replace it with gateway ip. 344 // 2. ep.Address can be uds when EndpointPort = 0 345 if ep.Address != "" && ep.EndpointPort != 0 && !netutil.IsValidIPAddress(ep.Address) { 346 return false 347 } 348 // filter out endpoints that don't match the subset 349 if !b.subsetLabels.SubsetOf(ep.Labels) { 350 return false 351 } 352 return true 353 }) 354 355 localityLbEndpoints := b.generate(svcEps) 356 if len(localityLbEndpoints) == 0 { 357 return buildEmptyClusterLoadAssignment(b.clusterName) 358 } 359 360 l := b.createClusterLoadAssignment(localityLbEndpoints) 361 362 // If locality aware routing is enabled, prioritize endpoints or set their lb weight. 363 // Failover should only be enabled when there is an outlier detection, otherwise Envoy 364 // will never detect the hosts are unhealthy and redirect traffic. 365 enableFailover, lb := getOutlierDetectionAndLoadBalancerSettings(b.DestinationRule(), b.port, b.subsetName) 366 lbSetting := loadbalancer.GetLocalityLbSetting(b.push.Mesh.GetLocalityLbSetting(), lb.GetLocalityLbSetting()) 367 if lbSetting != nil { 368 // Make a shallow copy of the cla as we are mutating the endpoints with priorities/weights relative to the calling proxy 369 l = util.CloneClusterLoadAssignment(l) 370 wrappedLocalityLbEndpoints := make([]*loadbalancer.WrappedLocalityLbEndpoints, len(localityLbEndpoints)) 371 for i := range localityLbEndpoints { 372 wrappedLocalityLbEndpoints[i] = &loadbalancer.WrappedLocalityLbEndpoints{ 373 IstioEndpoints: localityLbEndpoints[i].istioEndpoints, 374 LocalityLbEndpoints: l.Endpoints[i], 375 } 376 } 377 loadbalancer.ApplyLocalityLoadBalancer(l, wrappedLocalityLbEndpoints, b.locality, b.proxy.Labels, lbSetting, enableFailover) 378 } 379 return l 380 } 381 382 // generate endpoints with applies weights, multi-network mapping and other filtering 383 func (b *EndpointBuilder) generate(eps []*model.IstioEndpoint) []*LocalityEndpoints { 384 // shouldn't happen here 385 if !b.ServiceFound() { 386 return nil 387 } 388 389 eps = slices.Filter(eps, func(ep *model.IstioEndpoint) bool { 390 return b.filterIstioEndpoint(ep) 391 }) 392 393 localityEpMap := make(map[string]*LocalityEndpoints) 394 for _, ep := range eps { 395 mtlsEnabled := b.mtlsChecker.checkMtlsEnabled(ep, b.proxy.IsWaypointProxy()) 396 eep := buildEnvoyLbEndpoint(b, ep, mtlsEnabled) 397 if eep == nil { 398 continue 399 } 400 locLbEps, found := localityEpMap[ep.Locality.Label] 401 if !found { 402 locLbEps = &LocalityEndpoints{ 403 llbEndpoints: endpoint.LocalityLbEndpoints{ 404 Locality: util.ConvertLocality(ep.Locality.Label), 405 LbEndpoints: make([]*endpoint.LbEndpoint, 0, len(eps)), 406 }, 407 } 408 localityEpMap[ep.Locality.Label] = locLbEps 409 } 410 locLbEps.append(ep, eep) 411 } 412 413 locEps := make([]*LocalityEndpoints, 0, len(localityEpMap)) 414 locs := make([]string, 0, len(localityEpMap)) 415 for k := range localityEpMap { 416 locs = append(locs, k) 417 } 418 if len(locs) >= 2 { 419 sort.Strings(locs) 420 } 421 for _, locality := range locs { 422 locLbEps := localityEpMap[locality] 423 var weight uint32 424 var overflowStatus bool 425 for _, ep := range locLbEps.llbEndpoints.LbEndpoints { 426 weight, overflowStatus = addUint32(weight, ep.LoadBalancingWeight.GetValue()) 427 } 428 locLbEps.llbEndpoints.LoadBalancingWeight = &wrapperspb.UInt32Value{ 429 Value: weight, 430 } 431 if overflowStatus { 432 log.Warnf("Sum of localityLbEndpoints weight is overflow: service:%s, port: %d, locality:%s", 433 b.service.Hostname, b.port, locality) 434 } 435 locEps = append(locEps, locLbEps) 436 } 437 438 if len(locEps) == 0 { 439 b.push.AddMetric(model.ProxyStatusClusterNoInstances, b.clusterName, "", "") 440 } 441 442 // Apply the Split Horizon EDS filter, if applicable. 443 locEps = b.EndpointsByNetworkFilter(locEps) 444 445 if model.IsDNSSrvSubsetKey(b.clusterName) { 446 // For the SNI-DNAT clusters, we are using AUTO_PASSTHROUGH gateway. AUTO_PASSTHROUGH is intended 447 // to passthrough mTLS requests. However, at the gateway we do not actually have any way to tell if the 448 // request is a valid mTLS request or not, since its passthrough TLS. 449 // To ensure we allow traffic only to mTLS endpoints, we filter out non-mTLS endpoints for these cluster types. 450 locEps = b.EndpointsWithMTLSFilter(locEps) 451 } 452 453 return locEps 454 } 455 456 // addUint32AvoidOverflow returns sum of two uint32 and status. If sum overflows, 457 // and returns MaxUint32 and status. 458 func addUint32(left, right uint32) (uint32, bool) { 459 if math.MaxUint32-right < left { 460 return math.MaxUint32, true 461 } 462 return left + right, false 463 } 464 465 func (b *EndpointBuilder) filterIstioEndpoint(ep *model.IstioEndpoint) bool { 466 // for ServiceInternalTrafficPolicy 467 if b.service.Attributes.NodeLocal && ep.NodeName != b.proxy.GetNodeName() { 468 return false 469 } 470 // Only send endpoints from the networks in the network view requested by the proxy. 471 // The default network view assigned to the Proxy is nil, in that case match any network. 472 if !b.proxyView.IsVisible(ep) { 473 // Endpoint's network doesn't match the set of networks that the proxy wants to see. 474 return false 475 } 476 // If the downstream service is configured as cluster-local, only include endpoints that 477 // reside in the same cluster. 478 if b.clusterLocal && (b.clusterID != ep.Locality.ClusterID) { 479 return false 480 } 481 // TODO(nmittler): Consider merging discoverability policy with cluster-local 482 if !ep.IsDiscoverableFromProxy(b.proxy) { 483 return false 484 } 485 // If we don't know the address we must eventually use a gateway address 486 if ep.Address == "" && (!b.gateways().IsMultiNetworkEnabled() || b.proxy.InNetwork(ep.Network)) { 487 return false 488 } 489 // Filter out unhealthy endpoints 490 if !features.SendUnhealthyEndpoints.Load() && ep.HealthStatus == model.UnHealthy { 491 return false 492 } 493 // Draining endpoints are only sent to 'persistent session' clusters. 494 draining := ep.HealthStatus == model.Draining || 495 features.DrainingLabel != "" && ep.Labels[features.DrainingLabel] != "" 496 if draining { 497 persistentSession := b.service.Attributes.Labels[features.PersistentSessionLabel] != "" 498 if !persistentSession { 499 return false 500 } 501 } 502 return true 503 } 504 505 // snapshotShards into a local slice to avoid lock contention 506 func (b *EndpointBuilder) snapshotShards(endpointIndex *model.EndpointIndex) []*model.IstioEndpoint { 507 shards := b.findShards(endpointIndex) 508 if shards == nil { 509 return nil 510 } 511 512 // Determine whether or not the target service is considered local to the cluster 513 // and should, therefore, not be accessed from outside the cluster. 514 isClusterLocal := b.clusterLocal 515 var eps []*model.IstioEndpoint 516 shards.RLock() 517 defer shards.RUnlock() 518 // Extract shard keys so we can iterate in order. This ensures a stable EDS output. 519 keys := shards.Keys() 520 // The shards are updated independently, now need to filter and merge for this cluster 521 for _, shardKey := range keys { 522 if shardKey.Cluster != b.clusterID { 523 // If the downstream service is configured as cluster-local, only include endpoints that 524 // reside in the same cluster. 525 if isClusterLocal || b.service.Attributes.NodeLocal { 526 continue 527 } 528 } 529 eps = append(eps, shards.Shards[shardKey]...) 530 } 531 return eps 532 } 533 534 // findShards returns the endpoints for a cluster 535 func (b *EndpointBuilder) findShards(endpointIndex *model.EndpointIndex) *model.EndpointShards { 536 if b.service == nil { 537 log.Debugf("can not find the service for cluster %s", b.clusterName) 538 return nil 539 } 540 541 // Service resolution type might have changed and Cluster may be still in the EDS cluster list of "Connection.Clusters". 542 // This can happen if a ServiceEntry's resolution is changed from STATIC to DNS which changes the Envoy cluster type from 543 // EDS to STRICT_DNS or LOGICAL_DNS. When pushEds is called before Envoy sends the updated cluster list via Endpoint request which in turn 544 // will update "Connection.Clusters", we might accidentally send EDS updates for STRICT_DNS cluster. This check guards 545 // against such behavior and returns nil. When the updated cluster warms up in Envoy, it would update with new endpoints 546 // automatically. 547 // Gateways use EDS for Passthrough cluster. So we should allow Passthrough here. 548 if b.IsDNSCluster() { 549 log.Infof("cluster %s in eds cluster, but its resolution now is updated to %v, skipping it.", b.clusterName, b.service.Resolution) 550 return nil 551 } 552 553 epShards, f := endpointIndex.ShardsForService(string(b.hostname), b.service.Attributes.Namespace) 554 if !f { 555 // Shouldn't happen here 556 log.Debugf("can not find the endpointShards for cluster %s", b.clusterName) 557 return nil 558 } 559 return epShards 560 } 561 562 // Create the CLusterLoadAssignment. At this moment the options must have been applied to the locality lb endpoints. 563 func (b *EndpointBuilder) createClusterLoadAssignment(llbOpts []*LocalityEndpoints) *endpoint.ClusterLoadAssignment { 564 llbEndpoints := make([]*endpoint.LocalityLbEndpoints, 0, len(llbOpts)) 565 for _, l := range llbOpts { 566 llbEndpoints = append(llbEndpoints, &l.llbEndpoints) 567 } 568 return &endpoint.ClusterLoadAssignment{ 569 ClusterName: b.clusterName, 570 Endpoints: llbEndpoints, 571 } 572 } 573 574 // cluster with no endpoints 575 func buildEmptyClusterLoadAssignment(clusterName string) *endpoint.ClusterLoadAssignment { 576 return &endpoint.ClusterLoadAssignment{ 577 ClusterName: clusterName, 578 } 579 } 580 581 func (b *EndpointBuilder) gateways() *model.NetworkGateways { 582 if b.IsDNSCluster() { 583 return b.push.NetworkManager().Unresolved 584 } 585 return b.push.NetworkManager().NetworkGateways 586 } 587 588 func ExtractEnvoyEndpoints(locEps []*LocalityEndpoints) []*endpoint.LocalityLbEndpoints { 589 var locLbEps []*endpoint.LocalityLbEndpoints 590 for _, eps := range locEps { 591 locLbEps = append(locLbEps, &eps.llbEndpoints) 592 } 593 return locLbEps 594 } 595 596 // buildEnvoyLbEndpoint packs the endpoint based on istio info. 597 func buildEnvoyLbEndpoint(b *EndpointBuilder, e *model.IstioEndpoint, mtlsEnabled bool) *endpoint.LbEndpoint { 598 addr := util.BuildAddress(e.Address, e.EndpointPort) 599 healthStatus := e.HealthStatus 600 if features.DrainingLabel != "" && e.Labels[features.DrainingLabel] != "" { 601 healthStatus = model.Draining 602 } 603 604 ep := &endpoint.LbEndpoint{ 605 HealthStatus: corev3.HealthStatus(healthStatus), 606 LoadBalancingWeight: &wrapperspb.UInt32Value{ 607 Value: e.GetLoadBalancingWeight(), 608 }, 609 HostIdentifier: &endpoint.LbEndpoint_Endpoint{ 610 Endpoint: &endpoint.Endpoint{ 611 Address: addr, 612 }, 613 }, 614 Metadata: &corev3.Metadata{}, 615 } 616 617 // Istio telemetry depends on the metadata value being set for endpoints in the mesh. 618 // Istio endpoint level tls transport socket configuration depends on this logic 619 // Do not remove 620 var meta *model.EndpointMetadata 621 if features.CanonicalServiceForMeshExternalServiceEntry && b.service.MeshExternal { 622 svcLabels := b.service.Attributes.Labels 623 if _, ok := svcLabels[model.IstioCanonicalServiceLabelName]; ok { 624 meta = e.MetadataClone() 625 if meta.Labels == nil { 626 meta.Labels = make(map[string]string) 627 } 628 meta.Labels[model.IstioCanonicalServiceLabelName] = svcLabels[model.IstioCanonicalServiceLabelName] 629 meta.Labels[model.IstioCanonicalServiceRevisionLabelName] = svcLabels[model.IstioCanonicalServiceRevisionLabelName] 630 } else { 631 meta = e.Metadata() 632 } 633 meta.Namespace = b.service.Attributes.Namespace 634 } else { 635 meta = e.Metadata() 636 } 637 638 // detect if mTLS is possible for this endpoint, used later during ep filtering 639 // this must be done while converting IstioEndpoints because we still have workload labels 640 if !mtlsEnabled { 641 meta.TLSMode = "" 642 } 643 util.AppendLbEndpointMetadata(meta, ep.Metadata) 644 645 tunnel := supportTunnel(b, e) 646 if mtlsEnabled && !features.PreferHBONESend { 647 tunnel = false 648 } 649 if b.proxy.Metadata.DisableHBONESend { 650 tunnel = false 651 } 652 if tunnel { 653 address, port := e.Address, int(e.EndpointPort) 654 // We intentionally do not take into account waypoints here. 655 // 1. Workload waypoints: sidecar/ingress do not support sending traffic directly to workloads, only to services, 656 // so these are not applicable. 657 // 2. Service waypoints: in ztunnel, we would defer handling service traffic if the service has a waypoint, and instead 658 // send to the waypoint. However, with sidecars this is problematic. We don't know which service is the intended destination 659 // until *after* we apply policies. If we then sent to a service waypoint, we apply service policies twice. 660 // This can be problematic: double mirroring, fault injection, request manipulation, .... 661 // Instead, we consider this to workload traffic. This gives the same behavior as if we were an application doing internal load balancing 662 // with ztunnel. 663 // Note: there is a pretty valid case for wanting to send to the service from ingress. This gives a two tier delegation. 664 // However, it's not safe to do that by default; perhaps a future API could opt into this. 665 // Support connecting to server side waypoint proxy, if the destination has one. This is for sidecars and ingress. 666 // Setup tunnel metadata so requests will go through the tunnel 667 ep.HostIdentifier = &endpoint.LbEndpoint_Endpoint{Endpoint: &endpoint.Endpoint{ 668 Address: util.BuildInternalAddressWithIdentifier(connectOriginate, net.JoinHostPort(address, strconv.Itoa(port))), 669 }} 670 ep.Metadata.FilterMetadata[util.OriginalDstMetadataKey] = util.BuildTunnelMetadataStruct(address, port) 671 if b.dir != model.TrafficDirectionInboundVIP { 672 // Add TLS metadata matcher to indicate we can use HBONE for this endpoint. 673 // We skip this for service waypoint, which doesn't need to dynamically match mTLS vs HBONE. 674 ep.Metadata.FilterMetadata[util.EnvoyTransportSocketMetadataKey] = &structpb.Struct{ 675 Fields: map[string]*structpb.Value{ 676 model.TunnelLabelShortName: {Kind: &structpb.Value_StringValue{StringValue: model.TunnelHTTP}}, 677 }, 678 } 679 } 680 } 681 682 return ep 683 } 684 685 func supportTunnel(b *EndpointBuilder, e *model.IstioEndpoint) bool { 686 if b.proxy.IsProxylessGrpc() { 687 // Proxyless client cannot handle tunneling, even if the server can 688 return false 689 } 690 691 // Other side is a waypoint proxy. 692 if al := e.Labels[constants.ManagedGatewayLabel]; al == constants.ManagedGatewayMeshControllerLabel { 693 return true 694 } 695 696 // Otherwise has ambient enabled. Note: this is a synthetic label, not existing in the real Pod. 697 if b.push.SupportsTunnel(e.Network, e.Address) { 698 return true 699 } 700 // Otherwise supports tunnel 701 // Currently we only support HTTP tunnel, so just check for that. If we support more, we will 702 // need to pick the right one based on our support overlap. 703 if e.SupportsTunnel(model.TunnelHTTP) { 704 return true 705 } 706 707 return false 708 } 709 710 func getOutlierDetectionAndLoadBalancerSettings( 711 destinationRule *v1alpha3.DestinationRule, 712 portNumber int, 713 subsetName string, 714 ) (bool, *v1alpha3.LoadBalancerSettings) { 715 if destinationRule == nil { 716 return false, nil 717 } 718 outlierDetectionEnabled := false 719 var lbSettings *v1alpha3.LoadBalancerSettings 720 721 port := &model.Port{Port: portNumber} 722 policy := getSubsetTrafficPolicy(destinationRule, port, subsetName) 723 if policy != nil { 724 lbSettings = policy.LoadBalancer 725 if policy.OutlierDetection != nil { 726 outlierDetectionEnabled = true 727 } 728 } 729 730 return outlierDetectionEnabled, lbSettings 731 } 732 733 func getSubsetTrafficPolicy(destinationRule *v1alpha3.DestinationRule, port *model.Port, subsetName string) *v1alpha3.TrafficPolicy { 734 var subSetTrafficPolicy *v1alpha3.TrafficPolicy 735 for _, subset := range destinationRule.Subsets { 736 if subset.Name == subsetName { 737 subSetTrafficPolicy = subset.TrafficPolicy 738 break 739 } 740 } 741 return util.MergeSubsetTrafficPolicy(destinationRule.TrafficPolicy, subSetTrafficPolicy, port) 742 } 743 744 // getSubSetLabels returns the labels associated with a subset of a given service. 745 func getSubSetLabels(dr *v1alpha3.DestinationRule, subsetName string) labels.Instance { 746 // empty subset 747 if subsetName == "" { 748 return nil 749 } 750 751 if dr == nil { 752 return nil 753 } 754 755 for _, subset := range dr.Subsets { 756 if subset.Name == subsetName { 757 if len(subset.Labels) == 0 { 758 return nil 759 } 760 return subset.Labels 761 } 762 } 763 764 return nil 765 }