pilot/pkg/networking/util/util.go (508 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 util import ( "fmt" "net" "sort" "strconv" "strings" ) import ( core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" http_conn "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" "github.com/envoyproxy/go-control-plane/pkg/wellknown" "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/wrapperspb" meshconfig "istio.io/api/mesh/v1alpha1" networking "istio.io/api/networking/v1alpha3" "istio.io/pkg/log" ) import ( "github.com/apache/dubbo-go-pixiu/pilot/pkg/features" "github.com/apache/dubbo-go-pixiu/pilot/pkg/model" istionetworking "github.com/apache/dubbo-go-pixiu/pilot/pkg/networking" "github.com/apache/dubbo-go-pixiu/pkg/cluster" "github.com/apache/dubbo-go-pixiu/pkg/config" "github.com/apache/dubbo-go-pixiu/pkg/config/labels" "github.com/apache/dubbo-go-pixiu/pkg/network" "github.com/apache/dubbo-go-pixiu/pkg/proto/merge" "github.com/apache/dubbo-go-pixiu/pkg/util/strcase" ) const ( // BlackHoleCluster to catch traffic from routes with unresolved clusters. Traffic arriving here goes nowhere. BlackHoleCluster = "BlackHoleCluster" // BlackHole is the name of the virtual host and route name used to block all traffic BlackHole = "block_all" // PassthroughCluster to forward traffic to the original destination requested. This cluster is used when // traffic does not match any listener in envoy. PassthroughCluster = "PassthroughCluster" // Passthrough is the name of the virtual host used to forward traffic to the // PassthroughCluster Passthrough = "allow_any" // PassthroughFilterChain to catch traffic that doesn't match other filter chains. PassthroughFilterChain = "PassthroughFilterChain" // Inbound pass through cluster need to the bind the loopback ip address for the security and loop avoidance. InboundPassthroughClusterIpv4 = "InboundPassthroughClusterIpv4" InboundPassthroughClusterIpv6 = "InboundPassthroughClusterIpv6" // SniClusterFilter is the name of the sni_cluster envoy filter SniClusterFilter = "envoy.filters.network.sni_cluster" // IstioMetadataKey is the key under which metadata is added to a route or cluster // regarding the virtual service or destination rule used for each IstioMetadataKey = "istio" // EnvoyTransportSocketMetadataKey is the key under which metadata is added to an endpoint // which determines the endpoint level transport socket configuration. EnvoyTransportSocketMetadataKey = "envoy.transport_socket_match" // EnvoyRawBufferSocketName matched with hardcoded built-in Envoy transport name which determines // endpoint level plantext transport socket configuration EnvoyRawBufferSocketName = wellknown.TransportSocketRawBuffer // EnvoyTLSSocketName matched with hardcoded built-in Envoy transport name which determines endpoint // level tls transport socket configuration EnvoyTLSSocketName = wellknown.TransportSocketTls // EnvoyQUICSocketName matched with hardcoded built-in Envoy transport name which determines endpoint // level QUIC transport socket configuration EnvoyQUICSocketName = wellknown.TransportSocketQuic // Well-known header names AltSvcHeader = "alt-svc" ) // ALPNH2Only advertises that Proxy is going to use HTTP/2 when talking to the cluster. var ALPNH2Only = []string{"h2"} // ALPNInMeshH2 advertises that Proxy is going to use HTTP/2 when talking to the in-mesh cluster. // The custom "istio" value indicates in-mesh traffic and it's going to be used for routing decisions. // Once Envoy supports client-side ALPN negotiation, this should be {"istio", "h2", "http/1.1"}. var ALPNInMeshH2 = []string{"istio", "h2"} // ALPNInMeshH2WithMxc advertises that Proxy is going to use HTTP/2 when talking to the in-mesh cluster. // The custom "istio" value indicates in-mesh traffic and it's going to be used for routing decisions. // The custom "istio-peer-exchange" value indicates, metadata exchange is enabled for TCP. var ALPNInMeshH2WithMxc = []string{"istio-peer-exchange", "istio", "h2"} // ALPNInMesh advertises that Proxy is going to talk to the in-mesh cluster. // The custom "istio" value indicates in-mesh traffic and it's going to be used for routing decisions. var ALPNInMesh = []string{"istio"} // ALPNInMeshWithMxc advertises that Proxy is going to talk to the in-mesh cluster and has metadata exchange enabled for // TCP. The custom "istio-peer-exchange" value indicates, metadata exchange is enabled for TCP. The custom "istio" value // indicates in-mesh traffic and it's going to be used for routing decisions. var ALPNInMeshWithMxc = []string{"istio-peer-exchange", "istio"} // ALPNHttp advertises that Proxy is going to talking either http2 or http 1.1. var ALPNHttp = []string{"h2", "http/1.1"} // ALPNHttp3OverQUIC advertises that Proxy is going to talk HTTP/3 over QUIC var ALPNHttp3OverQUIC = []string{"h3"} // ALPNDownstreamWithMxc advertises that Proxy is going to talk either tcp(for metadata exchange), http2 or http 1.1. var ALPNDownstreamWithMxc = []string{"istio-peer-exchange", "h2", "http/1.1"} // ALPNDownstream advertises that Proxy is going to talk http2 or http 1.1. var ALPNDownstream = []string{"h2", "http/1.1"} // RegexEngine is the default google RE2 regex engine. var RegexEngine = &matcher.RegexMatcher_GoogleRe2{GoogleRe2: &matcher.RegexMatcher_GoogleRE2{}} func getMaxCidrPrefix(addr string) uint32 { ip := net.ParseIP(addr) if ip.To4() == nil { // ipv6 address return 128 } // ipv4 address return 32 } func ListContains(haystack []string, needle string) bool { for _, n := range haystack { if needle == n { return true } } return false } // ConvertAddressToCidr converts from string to CIDR proto func ConvertAddressToCidr(addr string) *core.CidrRange { if len(addr) == 0 { return nil } cidr := &core.CidrRange{ AddressPrefix: addr, PrefixLen: &wrapperspb.UInt32Value{ Value: getMaxCidrPrefix(addr), }, } if strings.Contains(addr, "/") { parts := strings.Split(addr, "/") cidr.AddressPrefix = parts[0] prefix, _ := strconv.Atoi(parts[1]) cidr.PrefixLen.Value = uint32(prefix) } return cidr } // BuildAddress returns a SocketAddress with the given ip and port or uds. func BuildAddress(bind string, port uint32) *core.Address { address := BuildNetworkAddress(bind, port, istionetworking.TransportProtocolTCP) if address != nil { return address } return &core.Address{ Address: &core.Address_Pipe{ Pipe: &core.Pipe{ Path: strings.TrimPrefix(bind, model.UnixAddressPrefix), }, }, } } func BuildNetworkAddress(bind string, port uint32, transport istionetworking.TransportProtocol) *core.Address { if port == 0 { return nil } return &core.Address{ Address: &core.Address_SocketAddress{ SocketAddress: &core.SocketAddress{ Address: bind, Protocol: transport.ToEnvoySocketProtocol(), PortSpecifier: &core.SocketAddress_PortValue{ PortValue: port, }, }, }, } } // MessageToAnyWithError converts from proto message to proto Any func MessageToAnyWithError(msg proto.Message) (*anypb.Any, error) { b, err := proto.MarshalOptions{Deterministic: true}.Marshal(msg) if err != nil { return nil, err } return &anypb.Any{ // nolint: staticcheck TypeUrl: "type.googleapis.com/" + string(msg.ProtoReflect().Descriptor().FullName()), Value: b, }, nil } // MessageToAny converts from proto message to proto Any func MessageToAny(msg proto.Message) *anypb.Any { out, err := MessageToAnyWithError(msg) if err != nil { log.Error(fmt.Sprintf("error marshaling Any %s: %v", prototext.Format(msg), err)) return nil } return out } // SortVirtualHosts sorts a slice of virtual hosts by name. // // Envoy computes a hash of RDS to see if things have changed - hash is affected by order of elements in the filter. Therefore // we sort virtual hosts by name before handing them back so the ordering is stable across HTTP Route Configs. func SortVirtualHosts(hosts []*route.VirtualHost) { if len(hosts) < 2 { return } sort.SliceStable(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name }) } // IsIstioVersionGE114 checks whether the given Istio version is greater than or equals 1.14. func IsIstioVersionGE114(version *model.IstioVersion) bool { return version == nil || version.Compare(&model.IstioVersion{Major: 1, Minor: 14, Patch: -1}) >= 0 } func IsProtocolSniffingEnabledForPort(port *model.Port) bool { return features.EnableProtocolSniffingForOutbound && port.Protocol.IsUnsupported() } func IsProtocolSniffingEnabledForInboundPort(port *model.Port) bool { return features.EnableProtocolSniffingForInbound && port.Protocol.IsUnsupported() } func IsProtocolSniffingEnabledForOutboundPort(port *model.Port) bool { return features.EnableProtocolSniffingForOutbound && port.Protocol.IsUnsupported() } // ConvertLocality converts '/' separated locality string to Locality struct. func ConvertLocality(locality string) *core.Locality { if locality == "" { return &core.Locality{} } region, zone, subzone := model.SplitLocalityLabel(locality) return &core.Locality{ Region: region, Zone: zone, SubZone: subzone, } } // LocalityToString converts Locality struct to '/' separated locality string. func LocalityToString(l *core.Locality) string { if l == nil { return "" } resp := l.Region if l.Zone == "" { return resp } resp += "/" + l.Zone if l.SubZone == "" { return resp } resp += "/" + l.SubZone return resp } // IsLocalityEmpty checks if a locality is empty (checking region is good enough, based on how its initialized) func IsLocalityEmpty(locality *core.Locality) bool { if locality == nil || (len(locality.GetRegion()) == 0) { return true } return false } func LocalityMatch(proxyLocality *core.Locality, ruleLocality string) bool { ruleRegion, ruleZone, ruleSubzone := model.SplitLocalityLabel(ruleLocality) regionMatch := ruleRegion == "*" || proxyLocality.GetRegion() == ruleRegion zoneMatch := ruleZone == "*" || ruleZone == "" || proxyLocality.GetZone() == ruleZone subzoneMatch := ruleSubzone == "*" || ruleSubzone == "" || proxyLocality.GetSubZone() == ruleSubzone if regionMatch && zoneMatch && subzoneMatch { return true } return false } func LbPriority(proxyLocality, endpointsLocality *core.Locality) int { if proxyLocality.GetRegion() == endpointsLocality.GetRegion() { if proxyLocality.GetZone() == endpointsLocality.GetZone() { if proxyLocality.GetSubZone() == endpointsLocality.GetSubZone() { return 0 } return 1 } return 2 } return 3 } // return a shallow copy ClusterLoadAssignment func CloneClusterLoadAssignment(original *endpoint.ClusterLoadAssignment) *endpoint.ClusterLoadAssignment { if original == nil { return nil } out := &endpoint.ClusterLoadAssignment{} out.ClusterName = original.ClusterName out.Endpoints = cloneLocalityLbEndpoints(original.Endpoints) out.Policy = original.Policy return out } // return a shallow copy LocalityLbEndpoints func cloneLocalityLbEndpoints(endpoints []*endpoint.LocalityLbEndpoints) []*endpoint.LocalityLbEndpoints { out := make([]*endpoint.LocalityLbEndpoints, 0, len(endpoints)) for _, ep := range endpoints { clone := CloneLocalityLbEndpoint(ep) out = append(out, clone) } return out } // return a shallow copy of LocalityLbEndpoints func CloneLocalityLbEndpoint(ep *endpoint.LocalityLbEndpoints) *endpoint.LocalityLbEndpoints { clone := &endpoint.LocalityLbEndpoints{} clone.Locality = ep.Locality clone.LbEndpoints = ep.LbEndpoints clone.Proximity = ep.Proximity clone.Priority = ep.Priority if ep.LoadBalancingWeight != nil { clone.LoadBalancingWeight = &wrapperspb.UInt32Value{ Value: ep.GetLoadBalancingWeight().GetValue(), } } return clone } // BuildConfigInfoMetadata builds core.Metadata struct containing the // name.namespace of the config, the type, etc. func BuildConfigInfoMetadata(config config.Meta) *core.Metadata { return AddConfigInfoMetadata(nil, config) } // AddConfigInfoMetadata adds name.namespace of the config, the type, etc // to the given core.Metadata struct, if metadata is not initialized, build a new metadata. func AddConfigInfoMetadata(metadata *core.Metadata, config config.Meta) *core.Metadata { if metadata == nil { metadata = &core.Metadata{ FilterMetadata: map[string]*structpb.Struct{}, } } s := "/apis/" + config.GroupVersionKind.Group + "/" + config.GroupVersionKind.Version + "/namespaces/" + config.Namespace + "/" + strcase.CamelCaseToKebabCase(config.GroupVersionKind.Kind) + "/" + config.Name if _, ok := metadata.FilterMetadata[IstioMetadataKey]; !ok { metadata.FilterMetadata[IstioMetadataKey] = &structpb.Struct{ Fields: map[string]*structpb.Value{}, } } metadata.FilterMetadata[IstioMetadataKey].Fields["config"] = &structpb.Value{ Kind: &structpb.Value_StringValue{ StringValue: s, }, } return metadata } // AddSubsetToMetadata will insert the subset name supplied. This should be called after the initial // "istio" metadata has been created for the cluster. If the "istio" metadata field is not already // defined, the subset information will not be added (to prevent adding this information where not // needed). This is used for telemetry reporting. func AddSubsetToMetadata(md *core.Metadata, subset string) { if istioMeta, ok := md.FilterMetadata[IstioMetadataKey]; ok { istioMeta.Fields["subset"] = &structpb.Value{ Kind: &structpb.Value_StringValue{ StringValue: subset, }, } } } // IsHTTPFilterChain returns true if the filter chain contains a HTTP connection manager filter func IsHTTPFilterChain(filterChain *listener.FilterChain) bool { for _, f := range filterChain.Filters { if f.Name == wellknown.HTTPConnectionManager { return true } } return false } // MergeAnyWithAny merges a given any typed message into the given Any typed message by dynamically inferring the // type of Any func MergeAnyWithAny(dst *anypb.Any, src *anypb.Any) (*anypb.Any, error) { // Assuming that Pilot is compiled with this type [which should always be the case] var err error // get an object of type used by this message dstX, err := dst.UnmarshalNew() if err != nil { return nil, err } // get an object of type used by this message srcX, err := src.UnmarshalNew() if err != nil { return nil, err } // Merge the two typed protos merge.Merge(dstX, srcX) var retVal *anypb.Any // Convert the merged proto back to dst if retVal, err = anypb.New(dstX); err != nil { return nil, err } return retVal, nil } // BuildLbEndpointMetadata adds metadata values to a lb endpoint func BuildLbEndpointMetadata(networkID network.ID, tlsMode, workloadname, namespace string, clusterID cluster.ID, labels labels.Instance) *core.Metadata { if networkID == "" && (tlsMode == "" || tlsMode == model.DisabledTLSModeLabel) && (!features.EndpointTelemetryLabel || !features.EnableTelemetryLabel) { return nil } metadata := &core.Metadata{ FilterMetadata: map[string]*structpb.Struct{}, } if tlsMode != "" && tlsMode != model.DisabledTLSModeLabel { metadata.FilterMetadata[EnvoyTransportSocketMetadataKey] = &structpb.Struct{ Fields: map[string]*structpb.Value{ model.TLSModeLabelShortname: {Kind: &structpb.Value_StringValue{StringValue: tlsMode}}, }, } } // Add compressed telemetry metadata. Note this is a short term solution to make server workload metadata // available at client sidecar, so that telemetry filter could use for metric labels. This is useful for two cases: // server does not have sidecar injected, and request fails to reach server and thus metadata exchange does not happen. // Due to performance concern, telemetry metadata is compressed into a semicolon separted string: // workload-name;namespace;canonical-service-name;canonical-service-revision;cluster-id. if features.EndpointTelemetryLabel { var sb strings.Builder sb.WriteString(workloadname) sb.WriteString(";") sb.WriteString(namespace) sb.WriteString(";") if csn, ok := labels[model.IstioCanonicalServiceLabelName]; ok { sb.WriteString(csn) } sb.WriteString(";") if csr, ok := labels[model.IstioCanonicalServiceRevisionLabelName]; ok { sb.WriteString(csr) } sb.WriteString(";") sb.WriteString(clusterID.String()) addIstioEndpointLabel(metadata, "workload", &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: sb.String()}}) } return metadata } // MaybeApplyTLSModeLabel may or may not update the metadata for the Envoy transport socket matches for auto mTLS. func MaybeApplyTLSModeLabel(ep *endpoint.LbEndpoint, tlsMode string) (*endpoint.LbEndpoint, bool) { if ep == nil || ep.Metadata == nil { return nil, false } epTLSMode := "" if ep.Metadata.FilterMetadata != nil { if v, ok := ep.Metadata.FilterMetadata[EnvoyTransportSocketMetadataKey]; ok { epTLSMode = v.Fields[model.TLSModeLabelShortname].GetStringValue() } } // Normalize the tls label name before comparison. This ensure we won't falsely cloning // the endpoint when they are "" and model.DisabledTLSModeLabel. if epTLSMode == model.DisabledTLSModeLabel { epTLSMode = "" } if tlsMode == model.DisabledTLSModeLabel { tlsMode = "" } if epTLSMode == tlsMode { return nil, false } // We make a copy instead of modifying on existing endpoint pointer directly to avoid data race. // See https://github.com/istio/istio/issues/34227 for details. newEndpoint := proto.Clone(ep).(*endpoint.LbEndpoint) if tlsMode != "" && tlsMode != model.DisabledTLSModeLabel { newEndpoint.Metadata.FilterMetadata[EnvoyTransportSocketMetadataKey] = &structpb.Struct{ Fields: map[string]*structpb.Value{ model.TLSModeLabelShortname: {Kind: &structpb.Value_StringValue{StringValue: tlsMode}}, }, } } else { delete(newEndpoint.Metadata.FilterMetadata, EnvoyTransportSocketMetadataKey) } return newEndpoint, true } func addIstioEndpointLabel(metadata *core.Metadata, key string, val *structpb.Value) { if _, ok := metadata.FilterMetadata[IstioMetadataKey]; !ok { metadata.FilterMetadata[IstioMetadataKey] = &structpb.Struct{ Fields: map[string]*structpb.Value{}, } } metadata.FilterMetadata[IstioMetadataKey].Fields[key] = val } // IsAllowAnyOutbound checks if allow_any is enabled for outbound traffic func IsAllowAnyOutbound(node *model.Proxy) bool { return node.SidecarScope != nil && node.SidecarScope.OutboundTrafficPolicy != nil && node.SidecarScope.OutboundTrafficPolicy.Mode == networking.OutboundTrafficPolicy_ALLOW_ANY } func StringToExactMatch(in []string) []*matcher.StringMatcher { if len(in) == 0 { return nil } res := make([]*matcher.StringMatcher, 0, len(in)) for _, s := range in { res = append(res, &matcher.StringMatcher{ MatchPattern: &matcher.StringMatcher_Exact{Exact: s}, }) } return res } func StringToPrefixMatch(in []string) []*matcher.StringMatcher { if len(in) == 0 { return nil } res := make([]*matcher.StringMatcher, 0, len(in)) for _, s := range in { res = append(res, &matcher.StringMatcher{ MatchPattern: &matcher.StringMatcher_Prefix{Prefix: s}, }) } return res } func ConvertToEnvoyMatches(in []*networking.StringMatch) []*matcher.StringMatcher { res := make([]*matcher.StringMatcher, 0, len(in)) for _, im := range in { if em := ConvertToEnvoyMatch(im); em != nil { res = append(res, em) } } return res } func ConvertToEnvoyMatch(in *networking.StringMatch) *matcher.StringMatcher { switch m := in.MatchType.(type) { case *networking.StringMatch_Exact: return &matcher.StringMatcher{MatchPattern: &matcher.StringMatcher_Exact{Exact: m.Exact}} case *networking.StringMatch_Prefix: return &matcher.StringMatcher{MatchPattern: &matcher.StringMatcher_Prefix{Prefix: m.Prefix}} case *networking.StringMatch_Regex: return &matcher.StringMatcher{ MatchPattern: &matcher.StringMatcher_SafeRegex{ SafeRegex: &matcher.RegexMatcher{ EngineType: RegexEngine, Regex: m.Regex, }, }, } } return nil } func StringSliceEqual(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func UInt32SliceEqual(a, b []uint32) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func CidrRangeSliceEqual(a, b []*core.CidrRange) bool { if len(a) != len(b) { return false } for i := range a { netA, err := toIPNet(a[i]) if err != nil { return false } netB, err := toIPNet(b[i]) if err != nil { return false } if netA.IP.String() != netB.IP.String() { return false } } return true } func toIPNet(c *core.CidrRange) (*net.IPNet, error) { _, cA, err := net.ParseCIDR(c.AddressPrefix + "/" + strconv.Itoa(int(c.PrefixLen.GetValue()))) if err != nil { log.Errorf("failed to parse CidrRange %v as IPNet: %v", c, err) } return cA, err } // meshconfig ForwardClientCertDetails and the Envoy config enum are off by 1 // due to the UNDEFINED in the meshconfig ForwardClientCertDetails func MeshConfigToEnvoyForwardClientCertDetails(c meshconfig.Topology_ForwardClientCertDetails) http_conn.HttpConnectionManager_ForwardClientCertDetails { return http_conn.HttpConnectionManager_ForwardClientCertDetails(c - 1) } // ByteCount returns a human readable byte format // Inspired by https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/ func ByteCount(b int) string { const unit = 1000 if b < unit { return fmt.Sprintf("%dB", b) } div, exp := int64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp]) } // IPv6Compliant encloses ipv6 addresses in square brackets followed by port number in Host header/URIs func IPv6Compliant(host string) string { if strings.Contains(host, ":") { return "[" + host + "]" } return host } // DomainName builds the domain name for a given host and port func DomainName(host string, port int) string { return net.JoinHostPort(host, strconv.Itoa(port)) }