| // 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 v1alpha3 |
| |
| import ( |
| "fmt" |
| "net" |
| "sort" |
| "strconv" |
| "strings" |
| "time" |
| ) |
| |
| import ( |
| accesslog "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" |
| core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" |
| listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" |
| route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" |
| hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" |
| envoyquicv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/quic/v3" |
| auth "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" |
| "github.com/envoyproxy/go-control-plane/pkg/wellknown" |
| "google.golang.org/protobuf/types/known/durationpb" |
| wrappers "google.golang.org/protobuf/types/known/wrapperspb" |
| extensions "istio.io/api/extensions/v1alpha1" |
| meshconfig "istio.io/api/mesh/v1alpha1" |
| networking "istio.io/api/networking/v1alpha3" |
| "istio.io/pkg/log" |
| "istio.io/pkg/monitoring" |
| ) |
| |
| 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/pilot/pkg/networking/core/v1alpha3/extension" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/util" |
| authnmodel "github.com/apache/dubbo-go-pixiu/pilot/pkg/security/model" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/serviceregistry/provider" |
| xdsfilters "github.com/apache/dubbo-go-pixiu/pilot/pkg/xds/filters" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/xds/requestidextension" |
| "github.com/apache/dubbo-go-pixiu/pkg/config" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/constants" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/host" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/protocol" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/security" |
| "github.com/apache/dubbo-go-pixiu/pkg/proto" |
| "github.com/apache/dubbo-go-pixiu/pkg/util/sets" |
| ) |
| |
| const ( |
| NoConflict = iota |
| // HTTPOverTCP represents incoming HTTP existing TCP |
| HTTPOverTCP |
| // TCPOverHTTP represents incoming TCP existing HTTP |
| TCPOverHTTP |
| // TCPOverTCP represents incoming TCP existing TCP |
| TCPOverTCP |
| // TCPOverAuto represents incoming TCP existing AUTO |
| TCPOverAuto |
| // AutoOverHTTP represents incoming AUTO existing HTTP |
| AutoOverHTTP |
| // AutoOverTCP represents incoming AUTO existing TCP |
| AutoOverTCP |
| ) |
| |
| const ( |
| // ProxyInboundListenPort is the port on which all inbound traffic to the pod/vm will be captured to |
| // TODO: allow configuration through mesh config |
| ProxyInboundListenPort = 15006 |
| ) |
| |
| // MutableListener represents a listener that is being built. |
| type MutableListener struct { |
| istionetworking.MutableObjects |
| } |
| |
| // A set of pre-allocated variables related to protocol sniffing logic for |
| // propagating the ALPN to upstreams |
| var ( |
| // These are sniffed by the HTTP Inspector in the outbound listener |
| // We need to forward these ALPNs to upstream so that the upstream can |
| // properly use an HTTP or TCP listener |
| plaintextHTTPALPNs = func() []string { |
| if features.HTTP10 { |
| // If HTTP 1.0 is enabled, we will match it |
| return []string{"http/1.0", "http/1.1", "h2c"} |
| } |
| // Otherwise, matching would just lead to immediate rejection. By not matching, we can let it pass |
| // through as raw TCP at least. |
| // NOTE: mtlsHTTPALPNs can always include 1.0, for simplicity, as it will only be sent if a client |
| return []string{"http/1.1", "h2c"} |
| }() |
| mtlsHTTPALPNs = []string{"istio-http/1.0", "istio-http/1.1", "istio-h2"} |
| |
| allIstioMtlsALPNs = []string{"istio", "istio-peer-exchange", "istio-http/1.0", "istio-http/1.1", "istio-h2"} |
| |
| mtlsTCPWithMxcALPNs = []string{"istio-peer-exchange", "istio"} |
| ) |
| |
| // BuildListeners produces a list of listeners and referenced clusters for all proxies |
| func (configgen *ConfigGeneratorImpl) BuildListeners(node *model.Proxy, |
| push *model.PushContext) []*listener.Listener { |
| builder := NewListenerBuilder(node, push) |
| |
| switch node.Type { |
| case model.SidecarProxy: |
| builder = configgen.buildSidecarListeners(builder) |
| case model.Router: |
| builder = configgen.buildGatewayListeners(builder) |
| } |
| |
| builder.patchListeners() |
| return builder.getListeners() |
| } |
| |
| func BuildListenerTLSContext(serverTLSSettings *networking.ServerTLSSettings, |
| proxy *model.Proxy, transportProtocol istionetworking.TransportProtocol) *auth.DownstreamTlsContext { |
| alpnByTransport := util.ALPNHttp |
| if transportProtocol == istionetworking.TransportProtocolQUIC { |
| alpnByTransport = util.ALPNHttp3OverQUIC |
| } |
| |
| ctx := &auth.DownstreamTlsContext{ |
| CommonTlsContext: &auth.CommonTlsContext{ |
| AlpnProtocols: alpnByTransport, |
| }, |
| } |
| |
| ctx.RequireClientCertificate = proto.BoolFalse |
| if serverTLSSettings.Mode == networking.ServerTLSSettings_MUTUAL || |
| serverTLSSettings.Mode == networking.ServerTLSSettings_ISTIO_MUTUAL { |
| ctx.RequireClientCertificate = proto.BoolTrue |
| } |
| |
| if features.EnableLegacyIstioMutualCredentialName { |
| // Legacy code path. Can be removed after a couple releases. |
| switch { |
| // If credential name is specified at gateway config, create SDS config for gateway to fetch key/cert from Istiod. |
| case serverTLSSettings.CredentialName != "": |
| authnmodel.ApplyCredentialSDSToServerCommonTLSContext(ctx.CommonTlsContext, serverTLSSettings) |
| case serverTLSSettings.Mode == networking.ServerTLSSettings_ISTIO_MUTUAL: |
| authnmodel.ApplyToCommonTLSContext(ctx.CommonTlsContext, proxy, serverTLSSettings.SubjectAltNames, []string{}, ctx.RequireClientCertificate.Value) |
| default: |
| certProxy := &model.Proxy{} |
| certProxy.IstioVersion = proxy.IstioVersion |
| // If certificate files are specified in gateway configuration, use file based SDS. |
| certProxy.Metadata = &model.NodeMetadata{ |
| TLSServerCertChain: serverTLSSettings.ServerCertificate, |
| TLSServerKey: serverTLSSettings.PrivateKey, |
| TLSServerRootCert: serverTLSSettings.CaCertificates, |
| } |
| |
| authnmodel.ApplyToCommonTLSContext(ctx.CommonTlsContext, certProxy, serverTLSSettings.SubjectAltNames, []string{}, ctx.RequireClientCertificate.Value) |
| } |
| } else { |
| switch { |
| case serverTLSSettings.Mode == networking.ServerTLSSettings_ISTIO_MUTUAL: |
| authnmodel.ApplyToCommonTLSContext(ctx.CommonTlsContext, proxy, serverTLSSettings.SubjectAltNames, []string{}, ctx.RequireClientCertificate.Value) |
| // If credential name is specified at gateway config, create SDS config for gateway to fetch key/cert from Istiod. |
| case serverTLSSettings.CredentialName != "": |
| authnmodel.ApplyCredentialSDSToServerCommonTLSContext(ctx.CommonTlsContext, serverTLSSettings) |
| default: |
| certProxy := &model.Proxy{} |
| certProxy.IstioVersion = proxy.IstioVersion |
| // If certificate files are specified in gateway configuration, use file based SDS. |
| certProxy.Metadata = &model.NodeMetadata{ |
| TLSServerCertChain: serverTLSSettings.ServerCertificate, |
| TLSServerKey: serverTLSSettings.PrivateKey, |
| TLSServerRootCert: serverTLSSettings.CaCertificates, |
| } |
| |
| authnmodel.ApplyToCommonTLSContext(ctx.CommonTlsContext, certProxy, serverTLSSettings.SubjectAltNames, []string{}, ctx.RequireClientCertificate.Value) |
| } |
| } |
| |
| // Set TLS parameters if they are non-default |
| if len(serverTLSSettings.CipherSuites) > 0 || |
| serverTLSSettings.MinProtocolVersion != networking.ServerTLSSettings_TLS_AUTO || |
| serverTLSSettings.MaxProtocolVersion != networking.ServerTLSSettings_TLS_AUTO { |
| ctx.CommonTlsContext.TlsParams = &auth.TlsParameters{ |
| TlsMinimumProtocolVersion: convertTLSProtocol(serverTLSSettings.MinProtocolVersion), |
| TlsMaximumProtocolVersion: convertTLSProtocol(serverTLSSettings.MaxProtocolVersion), |
| CipherSuites: serverTLSSettings.CipherSuites, |
| } |
| } |
| |
| return ctx |
| } |
| |
| // Invalid cipher suites lead Envoy to NACKing. This filters the list down to just the supported set. |
| func filteredSidecarCipherSuites(suites []string) []string { |
| ret := make([]string, 0, len(suites)) |
| validCiphers := sets.New() |
| for _, s := range suites { |
| if security.IsValidCipherSuite(s) { |
| if !validCiphers.Contains(s) { |
| ret = append(ret, s) |
| validCiphers = validCiphers.Insert(s) |
| } else if log.DebugEnabled() { |
| log.Debugf("ignoring duplicated cipherSuite: %q", s) |
| } |
| } else if log.DebugEnabled() { |
| log.Debugf("ignoring unsupported cipherSuite: %q", s) |
| } |
| } |
| return ret |
| } |
| |
| // buildSidecarListeners produces a list of listeners for sidecar proxies |
| func (configgen *ConfigGeneratorImpl) buildSidecarListeners(builder *ListenerBuilder) *ListenerBuilder { |
| if builder.push.Mesh.ProxyListenPort > 0 { |
| // Any build order change need a careful code review |
| builder.appendSidecarInboundListeners(). |
| appendSidecarOutboundListeners(). |
| buildHTTPProxyListener(). |
| buildVirtualOutboundListener() |
| } |
| return builder |
| } |
| |
| // if enableFlag is "1" indicates that AcceptHttp_10 is enabled. |
| func enableHTTP10(enableFlag string) bool { |
| return enableFlag == "1" |
| } |
| |
| type outboundListenerEntry struct { |
| services []*model.Service |
| servicePort *model.Port |
| bind string |
| listener *listener.Listener |
| locked bool |
| protocol protocol.Instance |
| } |
| |
| func protocolName(p protocol.Instance) string { |
| switch istionetworking.ModelProtocolToListenerProtocol(p, core.TrafficDirection_OUTBOUND) { |
| case istionetworking.ListenerProtocolHTTP: |
| return "HTTP" |
| case istionetworking.ListenerProtocolTCP: |
| return "TCP" |
| default: |
| return "UNKNOWN" |
| } |
| } |
| |
| type outboundListenerConflict struct { |
| metric monitoring.Metric |
| node *model.Proxy |
| listenerName string |
| currentProtocol protocol.Instance |
| currentServices []*model.Service |
| newHostname host.Name |
| newProtocol protocol.Instance |
| } |
| |
| func (c outboundListenerConflict) addMetric(metrics model.Metrics) { |
| currentHostnames := make([]string, len(c.currentServices)) |
| for i, s := range c.currentServices { |
| currentHostnames[i] = string(s.Hostname) |
| } |
| concatHostnames := strings.Join(currentHostnames, ",") |
| metrics.AddMetric(c.metric, |
| c.listenerName, |
| c.node.ID, |
| fmt.Sprintf("Listener=%s Accepted%s=%s Rejected%s=%s %sServices=%d", |
| c.listenerName, |
| protocolName(c.currentProtocol), |
| concatHostnames, |
| protocolName(c.newProtocol), |
| c.newHostname, |
| protocolName(c.currentProtocol), |
| len(c.currentServices))) |
| } |
| |
| // buildSidecarOutboundListeners generates http and tcp listeners for |
| // outbound connections from the proxy based on the sidecar scope associated with the proxy. |
| func (lb *ListenerBuilder) buildSidecarOutboundListeners(node *model.Proxy, |
| push *model.PushContext) []*listener.Listener { |
| noneMode := node.GetInterceptionMode() == model.InterceptionNone |
| |
| actualWildcard, actualLocalHostAddress := getActualWildcardAndLocalHost(node) |
| |
| var tcpListeners, httpListeners []*listener.Listener |
| // For conflict resolution |
| listenerMap := make(map[string]*outboundListenerEntry) |
| |
| // The sidecarConfig if provided could filter the list of |
| // services/virtual services that we need to process. It could also |
| // define one or more listeners with specific ports. Once we generate |
| // listeners for these user specified ports, we will auto generate |
| // configs for other ports if and only if the sidecarConfig has an |
| // egressListener on wildcard port. |
| // |
| // Validation will ensure that we have utmost one wildcard egress listener |
| // occurring in the end |
| |
| // Add listeners based on the config in the sidecar.EgressListeners if |
| // no Sidecar CRD is provided for this config namespace, |
| // push.SidecarScope will generate a default catch all egress listener. |
| for _, egressListener := range node.SidecarScope.EgressListeners { |
| |
| services := egressListener.Services() |
| virtualServices := egressListener.VirtualServices() |
| |
| // determine the bindToPort setting for listeners |
| bindToPort := false |
| if noneMode { |
| // do not care what the listener's capture mode setting is. The proxy does not use iptables |
| bindToPort = true |
| } else if egressListener.IstioListener != nil { |
| if egressListener.IstioListener.CaptureMode == networking.CaptureMode_NONE { |
| // proxy uses iptables redirect or tproxy. IF mode is not set |
| // for older proxies, it defaults to iptables redirect. If the |
| // listener's capture mode specifies NONE, then the proxy wants |
| // this listener alone to be on a physical port. If the |
| // listener's capture mode is default, then its same as |
| // iptables i.e. bindToPort is false. |
| bindToPort = true |
| } else if strings.HasPrefix(egressListener.IstioListener.Bind, model.UnixAddressPrefix) { |
| // If the bind is a Unix domain socket, set bindtoPort to true as it makes no |
| // sense to have ORIG_DST listener for unix domain socket listeners. |
| bindToPort = true |
| } |
| } |
| |
| if egressListener.IstioListener != nil && |
| egressListener.IstioListener.Port != nil { |
| // We have a non catch all listener on some user specified port |
| // The user specified port may or may not match a service port. |
| // If it does not match any service port and the service has only |
| // one port, then we pick a default service port. If service has |
| // multiple ports, we expect the user to provide a virtualService |
| // that will route to a proper Service. |
| |
| // Skip ports we cannot bind to |
| if !node.CanBindToPort(bindToPort, egressListener.IstioListener.Port.Number) { |
| log.Warnf("buildSidecarOutboundListeners: skipping privileged sidecar port %d for node %s as it is an unprivileged proxy", |
| egressListener.IstioListener.Port.Number, node.ID) |
| continue |
| } |
| |
| listenPort := &model.Port{ |
| Port: int(egressListener.IstioListener.Port.Number), |
| Protocol: protocol.Parse(egressListener.IstioListener.Port.Protocol), |
| Name: egressListener.IstioListener.Port.Name, |
| } |
| |
| // If capture mode is NONE i.e., bindToPort is true, and |
| // Bind IP + Port is specified, we will bind to the specified IP and Port. |
| // This specified IP is ideally expected to be a loopback IP. |
| // |
| // If capture mode is NONE i.e., bindToPort is true, and |
| // only Port is specified, we will bind to the default loopback IP |
| // 127.0.0.1 and the specified Port. |
| // |
| // If capture mode is NONE, i.e., bindToPort is true, and |
| // only Bind IP is specified, we will bind to the specified IP |
| // for each port as defined in the service registry. |
| // |
| // If captureMode is not NONE, i.e., bindToPort is false, then |
| // we will bind to user specified IP (if any) or to the VIPs of services in |
| // this egress listener. |
| bind := egressListener.IstioListener.Bind |
| if bind == "" { |
| if bindToPort { |
| bind = actualLocalHostAddress |
| } else { |
| bind = actualWildcard |
| } |
| } |
| |
| // Build ListenerOpts and PluginParams once and reuse across all Services to avoid unnecessary allocations. |
| listenerOpts := buildListenerOpts{ |
| push: push, |
| proxy: node, |
| bind: bind, |
| port: listenPort, |
| bindToPort: bindToPort, |
| } |
| |
| for _, service := range services { |
| listenerOpts.service = service |
| // Set service specific attributes here. |
| lb.buildSidecarOutboundListenerForPortOrUDS(listenerOpts, listenerMap, virtualServices, actualWildcard) |
| } |
| } else { |
| // This is a catch all egress listener with no port. This |
| // should be the last egress listener in the sidecar |
| // Scope. Construct a listener for each service and service |
| // port, if and only if this port was not specified in any of |
| // the preceding listeners from the sidecarScope. This allows |
| // users to specify a trimmed set of services for one or more |
| // listeners and then add a catch all egress listener for all |
| // other ports. Doing so allows people to restrict the set of |
| // services exposed on one or more listeners, and avoid hard |
| // port conflicts like tcp taking over http or http taking over |
| // tcp, or simply specify that of all the listeners that Istio |
| // generates, the user would like to have only specific sets of |
| // services exposed on a particular listener. |
| // |
| // To ensure that we do not add anything to listeners we have |
| // already generated, run through the outboundListenerEntry map and set |
| // the locked bit to true. |
| // buildSidecarOutboundListenerForPortOrUDS will not add/merge |
| // any HTTP/TCP listener if there is already a outboundListenerEntry |
| // with locked bit set to true |
| for _, e := range listenerMap { |
| e.locked = true |
| } |
| |
| bind := "" |
| if egressListener.IstioListener != nil && egressListener.IstioListener.Bind != "" { |
| bind = egressListener.IstioListener.Bind |
| } |
| if bindToPort && bind == "" { |
| bind = actualLocalHostAddress |
| } |
| |
| // Build ListenerOpts and PluginParams once and reuse across all Services to avoid unnecessary allocations. |
| listenerOpts := buildListenerOpts{ |
| push: push, |
| proxy: node, |
| bindToPort: bindToPort, |
| } |
| |
| for _, service := range services { |
| saddress := service.GetAddressForProxy(node) |
| for _, servicePort := range service.Ports { |
| // Skip ports we cannot bind to |
| if !node.CanBindToPort(bindToPort, uint32(servicePort.Port)) { |
| // here, we log at DEBUG level instead of WARN to avoid noise |
| // when the catch all egress listener hits ports 80 and 443 |
| log.Debugf("buildSidecarOutboundListeners: skipping privileged sidecar port %d for node %s as it is an unprivileged proxy", |
| servicePort.Port, node.ID) |
| continue |
| } |
| |
| // bind might have been modified by below code, so reset it for every Service. |
| listenerOpts.bind = bind |
| // port depends on servicePort. |
| listenerOpts.port = servicePort |
| listenerOpts.service = service |
| |
| // Support statefulsets/headless services with TCP ports, and empty service address field. |
| // Instead of generating a single 0.0.0.0:Port listener, generate a listener |
| // for each instance. HTTP services can happily reside on 0.0.0.0:PORT and use the |
| // wildcard route match to get to the appropriate IP through original dst clusters. |
| if features.EnableHeadlessService && bind == "" && service.Resolution == model.Passthrough && |
| saddress == constants.UnspecifiedIP && (servicePort.Protocol.IsTCP() || servicePort.Protocol.IsUnsupported()) { |
| instances := push.ServiceInstancesByPort(service, servicePort.Port, nil) |
| if service.Attributes.ServiceRegistry != provider.Kubernetes && len(instances) == 0 && service.Attributes.LabelSelectors == nil { |
| // A Kubernetes service with no endpoints means there are no endpoints at |
| // all, so don't bother sending, as traffic will never work. If we did |
| // send a wildcard listener, we may get into a situation where a scale |
| // down leads to a listener conflict. Similarly, if we have a |
| // labelSelector on the Service, then this may have endpoints not yet |
| // selected or scaled down, so we skip these as well. This leaves us with |
| // only a plain ServiceEntry with resolution NONE. In this case, we will |
| // fallback to a wildcard listener. |
| lb.buildSidecarOutboundListenerForPortOrUDS(listenerOpts, listenerMap, virtualServices, actualWildcard) |
| continue |
| } |
| for _, instance := range instances { |
| // Make sure each endpoint address is a valid address |
| // as service entries could have NONE resolution with label selectors for workload |
| // entries (which could technically have hostnames). |
| if net.ParseIP(instance.Endpoint.Address) == nil { |
| continue |
| } |
| // Skip build outbound listener to the node itself, |
| // as when app access itself by pod ip will not flow through this listener. |
| // Simultaneously, it will be duplicate with inbound listener. |
| if instance.Endpoint.Address == node.IPAddresses[0] { |
| continue |
| } |
| listenerOpts.bind = instance.Endpoint.Address |
| lb.buildSidecarOutboundListenerForPortOrUDS(listenerOpts, listenerMap, virtualServices, actualWildcard) |
| } |
| } else { |
| // Standard logic for headless and non headless services |
| lb.buildSidecarOutboundListenerForPortOrUDS(listenerOpts, listenerMap, virtualServices, actualWildcard) |
| } |
| } |
| } |
| } |
| } |
| |
| // Now validate all the listeners. Collate the tcp listeners first and then the HTTP listeners |
| // TODO: This is going to be bad for caching as the order of listeners in tcpListeners or httpListeners is not |
| // guaranteed. |
| for _, l := range listenerMap { |
| if l.servicePort.Protocol.IsTCP() { |
| tcpListeners = append(tcpListeners, l.listener) |
| } else { |
| httpListeners = append(httpListeners, l.listener) |
| } |
| } |
| tcpListeners = append(tcpListeners, httpListeners...) |
| // Build pass through filter chains now that all the non-passthrough filter chains are ready. |
| for _, l := range tcpListeners { |
| appendListenerFallthroughRouteForCompleteListener(l, node, push) |
| } |
| removeListenerFilterTimeout(tcpListeners) |
| return tcpListeners |
| } |
| |
| func (lb *ListenerBuilder) buildHTTPProxy(node *model.Proxy, |
| push *model.PushContext) *listener.Listener { |
| httpProxyPort := push.Mesh.ProxyHttpPort // global |
| if node.Metadata.HTTPProxyPort != "" { |
| port, err := strconv.Atoi(node.Metadata.HTTPProxyPort) |
| if err == nil { |
| httpProxyPort = int32(port) |
| } |
| } |
| if httpProxyPort == 0 { |
| return nil |
| } |
| |
| // enable HTTP PROXY port if necessary; this will add an RDS route for this port |
| _, listenAddress := getActualWildcardAndLocalHost(node) |
| |
| httpOpts := &core.Http1ProtocolOptions{ |
| AllowAbsoluteUrl: proto.BoolTrue, |
| } |
| if features.HTTP10 || enableHTTP10(node.Metadata.HTTP10) { |
| httpOpts.AcceptHttp_10 = true |
| } |
| |
| opts := buildListenerOpts{ |
| push: push, |
| proxy: node, |
| bind: listenAddress, |
| port: &model.Port{Port: int(httpProxyPort)}, |
| filterChainOpts: []*filterChainOpts{{ |
| httpOpts: &httpListenerOpts{ |
| rds: model.RDSHttpProxy, |
| useRemoteAddress: false, |
| connectionManager: &hcm.HttpConnectionManager{ |
| HttpProtocolOptions: httpOpts, |
| }, |
| protocol: protocol.HTTP_PROXY, |
| class: istionetworking.ListenerClassSidecarOutbound, |
| }, |
| }}, |
| bindToPort: true, |
| skipUserFilters: true, |
| } |
| l := buildListener(opts, core.TrafficDirection_OUTBOUND) |
| |
| // TODO: plugins for HTTP_PROXY mode, envoyfilter needs another listener match for SIDECAR_HTTP_PROXY |
| mutable := &MutableListener{ |
| MutableObjects: istionetworking.MutableObjects{ |
| Listener: l, |
| FilterChains: []istionetworking.FilterChain{{}}, |
| }, |
| } |
| if err := mutable.build(lb, opts); err != nil { |
| log.Warn("buildHTTPProxy filter chain error ", err.Error()) |
| return nil |
| } |
| return l |
| } |
| |
| func buildSidecarOutboundHTTPListenerOptsForPortOrUDS(listenerMapKey *string, |
| currentListenerEntry **outboundListenerEntry, listenerOpts *buildListenerOpts, |
| listenerMap map[string]*outboundListenerEntry, actualWildcard string) (bool, []*filterChainOpts) { |
| // first identify the bind if its not set. Then construct the key |
| // used to lookup the listener in the conflict map. |
| if len(listenerOpts.bind) == 0 { // no user specified bind. Use 0.0.0.0:Port |
| listenerOpts.bind = actualWildcard |
| } |
| *listenerMapKey = listenerOpts.bind + ":" + strconv.Itoa(listenerOpts.port.Port) |
| |
| var exists bool |
| sniffingEnabled := features.EnableProtocolSniffingForOutbound |
| |
| // Have we already generated a listener for this Port based on user |
| // specified listener ports? if so, we should not add any more HTTP |
| // services to the port. The user could have specified a sidecar |
| // resource with one or more explicit ports and then added a catch |
| // all listener, implying add all other ports as usual. When we are |
| // iterating through the services for a catchAll egress listener, |
| // the caller would have set the locked bit for each listener Entry |
| // in the map. |
| // |
| // Check if this HTTP listener conflicts with an existing TCP |
| // listener. We could have listener conflicts occur on unix domain |
| // sockets, or on IP binds. Specifically, its common to see |
| // conflicts on binds for wildcard address when a service has NONE |
| // resolution type, since we collapse all HTTP listeners into a |
| // single 0.0.0.0:port listener and use vhosts to distinguish |
| // individual http services in that port |
| if *currentListenerEntry, exists = listenerMap[*listenerMapKey]; exists { |
| // NOTE: This is not a conflict. This is simply filtering the |
| // services for a given listener explicitly. |
| // When the user declares their own ports in Sidecar.egress |
| // with some specific services on those ports, we should not |
| // generate any more listeners on that port as the user does |
| // not want those listeners. Protocol sniffing is not needed. |
| if (*currentListenerEntry).locked { |
| return false, nil |
| } |
| |
| if !sniffingEnabled { |
| if listenerOpts.service != nil { |
| if !(*currentListenerEntry).servicePort.Protocol.IsHTTP() { |
| outboundListenerConflict{ |
| metric: model.ProxyStatusConflictOutboundListenerTCPOverHTTP, |
| node: listenerOpts.proxy, |
| listenerName: *listenerMapKey, |
| currentServices: (*currentListenerEntry).services, |
| currentProtocol: (*currentListenerEntry).servicePort.Protocol, |
| newHostname: listenerOpts.service.Hostname, |
| newProtocol: listenerOpts.port.Protocol, |
| }.addMetric(listenerOpts.push) |
| } |
| |
| // Skip building listener for the same http port |
| (*currentListenerEntry).services = append((*currentListenerEntry).services, listenerOpts.service) |
| } |
| return false, nil |
| } |
| } |
| |
| listenerProtocol := istionetworking.ModelProtocolToListenerProtocol(listenerOpts.port.Protocol, core.TrafficDirection_OUTBOUND) |
| |
| // No conflicts. Add a http filter chain option to the listenerOpts |
| var rdsName string |
| if listenerOpts.port.Port == 0 { |
| rdsName = listenerOpts.bind // use the UDS as a rds name |
| } else { |
| if listenerProtocol == istionetworking.ListenerProtocolAuto && |
| sniffingEnabled && listenerOpts.bind != actualWildcard && listenerOpts.service != nil { |
| rdsName = string(listenerOpts.service.Hostname) + ":" + strconv.Itoa(listenerOpts.port.Port) |
| } else { |
| rdsName = strconv.Itoa(listenerOpts.port.Port) |
| } |
| } |
| httpOpts := &httpListenerOpts{ |
| // Set useRemoteAddress to true for side car outbound listeners so that it picks up the localhost address of the sender, |
| // which is an internal address, so that trusted headers are not sanitized. This helps to retain the timeout headers |
| // such as "x-envoy-upstream-rq-timeout-ms" set by the calling application. |
| useRemoteAddress: features.UseRemoteAddress, |
| rds: rdsName, |
| |
| protocol: listenerOpts.port.Protocol, |
| class: istionetworking.ListenerClassSidecarOutbound, |
| } |
| |
| if features.HTTP10 || enableHTTP10(listenerOpts.proxy.Metadata.HTTP10) { |
| httpOpts.connectionManager = &hcm.HttpConnectionManager{ |
| HttpProtocolOptions: &core.Http1ProtocolOptions{ |
| AcceptHttp_10: true, |
| }, |
| } |
| } |
| |
| return true, []*filterChainOpts{{ |
| httpOpts: httpOpts, |
| }} |
| } |
| |
| func buildSidecarOutboundTCPListenerOptsForPortOrUDS(listenerMapKey *string, |
| currentListenerEntry **outboundListenerEntry, listenerOpts *buildListenerOpts, listenerMap map[string]*outboundListenerEntry, |
| virtualServices []config.Config, actualWildcard string) (bool, []*filterChainOpts) { |
| // first identify the bind if its not set. Then construct the key |
| // used to lookup the listener in the conflict map. |
| |
| // Determine the listener address if bind is empty |
| // we listen on the service VIP if and only |
| // if the address is an IP address. If its a CIDR, we listen on |
| // 0.0.0.0, and setup a filter chain match for the CIDR range. |
| // As a small optimization, CIDRs with /32 prefix will be converted |
| // into listener address so that there is a dedicated listener for this |
| // ip:port. This will reduce the impact of a listener reload |
| var destinationCIDR string |
| if len(listenerOpts.bind) == 0 { |
| svcListenAddress := listenerOpts.service.GetAddressForProxy(listenerOpts.proxy) |
| // We should never get an empty address. |
| // This is a safety guard, in case some platform adapter isn't doing things |
| // properly |
| if len(svcListenAddress) > 0 { |
| if !strings.Contains(svcListenAddress, "/") { |
| listenerOpts.bind = svcListenAddress |
| } else { |
| // Address is a CIDR. Fall back to 0.0.0.0 and |
| // filter chain match |
| destinationCIDR = svcListenAddress |
| listenerOpts.bind = actualWildcard |
| } |
| } |
| } |
| |
| // could be a unix domain socket or an IP bind |
| *listenerMapKey = listenerKey(listenerOpts.bind, listenerOpts.port.Port) |
| |
| var exists bool |
| |
| // Have we already generated a listener for this Port based on user |
| // specified listener ports? if so, we should not add any more |
| // services to the port. The user could have specified a sidecar |
| // resource with one or more explicit ports and then added a catch |
| // all listener, implying add all other ports as usual. When we are |
| // iterating through the services for a catchAll egress listener, |
| // the caller would have set the locked bit for each listener Entry |
| // in the map. |
| // |
| // Check if this TCP listener conflicts with an existing HTTP listener |
| if *currentListenerEntry, exists = listenerMap[*listenerMapKey]; exists { |
| // NOTE: This is not a conflict. This is simply filtering the |
| // services for a given listener explicitly. |
| // When the user declares their own ports in Sidecar.egress |
| // with some specific services on those ports, we should not |
| // generate any more listeners on that port as the user does |
| // not want those listeners. Protocol sniffing is not needed. |
| if (*currentListenerEntry).locked { |
| return false, nil |
| } |
| |
| if !features.EnableProtocolSniffingForOutbound { |
| // Check for port collisions between TCP/TLS and HTTP (or unknown). If |
| // configured correctly, TCP/TLS ports may not collide. We'll |
| // need to do additional work to find out if there is a |
| // collision within TCP/TLS. |
| // If the service port was defined as unknown. It will conflict with all other |
| // protocols. |
| if !(*currentListenerEntry).servicePort.Protocol.IsTCP() { |
| // NOTE: While pluginParams.Service can be nil, |
| // this code cannot be reached if Service is nil because a pluginParams.Service can be nil only |
| // for user defined Egress listeners with ports. And these should occur in the API before |
| // the wildcard egress listener. the check for the "locked" bit will eliminate the collision. |
| // User is also not allowed to add duplicate ports in the egress listener |
| var newHostname host.Name |
| if listenerOpts.service != nil { |
| newHostname = listenerOpts.service.Hostname |
| } else { |
| // user defined outbound listener via sidecar API |
| newHostname = "sidecar-config-egress-http-listener" |
| } |
| |
| // We have a collision with another TCP port. This can happen |
| // for headless services, or non-k8s services that do not have |
| // a VIP, or when we have two binds on a unix domain socket or |
| // on same IP. Unfortunately we won't know if this is a real |
| // conflict or not until we process the VirtualServices, etc. |
| // The conflict resolution is done later in this code |
| outboundListenerConflict{ |
| metric: model.ProxyStatusConflictOutboundListenerHTTPOverTCP, |
| node: listenerOpts.proxy, |
| listenerName: *listenerMapKey, |
| currentServices: (*currentListenerEntry).services, |
| currentProtocol: (*currentListenerEntry).servicePort.Protocol, |
| newHostname: newHostname, |
| newProtocol: listenerOpts.port.Protocol, |
| }.addMetric(listenerOpts.push) |
| return false, nil |
| } |
| } |
| } |
| |
| meshGateway := map[string]bool{constants.IstioMeshGateway: true} |
| return true, buildSidecarOutboundTCPTLSFilterChainOpts(listenerOpts.proxy, |
| listenerOpts.push, virtualServices, |
| destinationCIDR, listenerOpts.service, |
| listenerOpts.bind, listenerOpts.port, meshGateway) |
| } |
| |
| // buildSidecarOutboundListenerForPortOrUDS builds a single listener and |
| // adds it to the listenerMap provided by the caller. Listeners are added |
| // if one doesn't already exist. HTTP listeners on same port are ignored |
| // (as vhosts are shipped through RDS). TCP listeners on same port are |
| // allowed only if they have different CIDR matches. |
| func (lb *ListenerBuilder) buildSidecarOutboundListenerForPortOrUDS(listenerOpts buildListenerOpts, |
| listenerMap map[string]*outboundListenerEntry, virtualServices []config.Config, actualWildcard string) { |
| var listenerMapKey string |
| var currentListenerEntry *outboundListenerEntry |
| var ret bool |
| var opts []*filterChainOpts |
| |
| listenerOpts.class = istionetworking.ListenerClassSidecarOutbound |
| |
| conflictType := NoConflict |
| |
| outboundSniffingEnabled := features.EnableProtocolSniffingForOutbound |
| listenerPortProtocol := listenerOpts.port.Protocol |
| listenerProtocol := istionetworking.ModelProtocolToListenerProtocol(listenerOpts.port.Protocol, core.TrafficDirection_OUTBOUND) |
| |
| // For HTTP_PROXY protocol defined by sidecars, just create the HTTP listener right away. |
| if listenerPortProtocol == protocol.HTTP_PROXY { |
| if ret, opts = buildSidecarOutboundHTTPListenerOptsForPortOrUDS(&listenerMapKey, ¤tListenerEntry, |
| &listenerOpts, listenerMap, actualWildcard); !ret { |
| return |
| } |
| listenerOpts.filterChainOpts = opts |
| } else { |
| switch listenerProtocol { |
| case istionetworking.ListenerProtocolHTTP: |
| if ret, opts = buildSidecarOutboundHTTPListenerOptsForPortOrUDS(&listenerMapKey, |
| ¤tListenerEntry, &listenerOpts, listenerMap, actualWildcard); !ret { |
| return |
| } |
| |
| // Check if conflict happens |
| if outboundSniffingEnabled && currentListenerEntry != nil { |
| // Build HTTP listener. If current listener entry is using HTTP or protocol sniffing, |
| // append the service. Otherwise (TCP), change current listener to use protocol sniffing. |
| if currentListenerEntry.protocol.IsHTTP() { |
| // conflictType is HTTPOverHTTP |
| // In these cases, we just add the services and exit early rather than recreate an identical listener |
| currentListenerEntry.services = append(currentListenerEntry.services, listenerOpts.service) |
| return |
| } else if currentListenerEntry.protocol.IsTCP() { |
| conflictType = HTTPOverTCP |
| } else { |
| // conflictType is HTTPOverAuto |
| // In these cases, we just add the services and exit early rather than recreate an identical listener |
| currentListenerEntry.services = append(currentListenerEntry.services, listenerOpts.service) |
| return |
| } |
| } |
| // Add application protocol filter chain match to the http filter chain. The application protocol will be set by http inspector |
| // Since application protocol filter chain match has been added to the http filter chain, a fall through filter chain will be |
| // appended to the listener later to allow arbitrary egress TCP traffic pass through when its port is conflicted with existing |
| // HTTP services, which can happen when a pod accesses a non registry service. |
| if outboundSniffingEnabled { |
| if listenerOpts.bind == actualWildcard { |
| for _, opt := range opts { |
| if opt.match == nil { |
| opt.match = &listener.FilterChainMatch{} |
| } |
| |
| // Support HTTP/1.0, HTTP/1.1 and HTTP/2 |
| opt.match.ApplicationProtocols = append(opt.match.ApplicationProtocols, plaintextHTTPALPNs...) |
| opt.match.TransportProtocol = xdsfilters.RawBufferTransportProtocol |
| } |
| |
| listenerOpts.needHTTPInspector = true |
| |
| // if we have a tcp fallthrough filter chain, this is no longer an HTTP listener - it |
| // is instead "unsupported" (auto detected), as we have a TCP and HTTP filter chain with |
| // inspection to route between them |
| listenerPortProtocol = protocol.Unsupported |
| } |
| } |
| listenerOpts.filterChainOpts = opts |
| |
| case istionetworking.ListenerProtocolTCP: |
| if ret, opts = buildSidecarOutboundTCPListenerOptsForPortOrUDS(&listenerMapKey, ¤tListenerEntry, |
| &listenerOpts, listenerMap, virtualServices, actualWildcard); !ret { |
| return |
| } |
| |
| // Check if conflict happens |
| if outboundSniffingEnabled && currentListenerEntry != nil { |
| // Build TCP listener. If current listener entry is using HTTP, add a new TCP filter chain |
| // If current listener is using protocol sniffing, merge the TCP filter chains. |
| if currentListenerEntry.protocol.IsHTTP() { |
| conflictType = TCPOverHTTP |
| } else if currentListenerEntry.protocol.IsTCP() { |
| conflictType = TCPOverTCP |
| } else { |
| conflictType = TCPOverAuto |
| } |
| } |
| |
| listenerOpts.filterChainOpts = opts |
| |
| case istionetworking.ListenerProtocolAuto: |
| // Add tcp filter chain, build TCP filter chain first. |
| if ret, opts = buildSidecarOutboundTCPListenerOptsForPortOrUDS(&listenerMapKey, ¤tListenerEntry, |
| &listenerOpts, listenerMap, virtualServices, actualWildcard); !ret { |
| return |
| } |
| listenerOpts.filterChainOpts = append(listenerOpts.filterChainOpts, opts...) |
| |
| // Add http filter chain and tcp filter chain to the listener opts |
| if ret, opts = buildSidecarOutboundHTTPListenerOptsForPortOrUDS(&listenerMapKey, ¤tListenerEntry, |
| &listenerOpts, listenerMap, actualWildcard); !ret { |
| return |
| } |
| |
| // Add application protocol filter chain match to the http filter chain. The application protocol will be set by http inspector |
| for _, opt := range opts { |
| if opt.match == nil { |
| opt.match = &listener.FilterChainMatch{} |
| } |
| |
| // Support HTTP/1.0, HTTP/1.1 and HTTP/2 |
| opt.match.ApplicationProtocols = append(opt.match.ApplicationProtocols, plaintextHTTPALPNs...) |
| opt.match.TransportProtocol = xdsfilters.RawBufferTransportProtocol |
| } |
| |
| listenerOpts.filterChainOpts = append(listenerOpts.filterChainOpts, opts...) |
| listenerOpts.needHTTPInspector = true |
| |
| if currentListenerEntry != nil { |
| if currentListenerEntry.protocol.IsHTTP() { |
| conflictType = AutoOverHTTP |
| } else if currentListenerEntry.protocol.IsTCP() { |
| conflictType = AutoOverTCP |
| } else { |
| // conflictType is AutoOverAuto |
| // In these cases, we just add the services and exit early rather than recreate an identical listener |
| currentListenerEntry.services = append(currentListenerEntry.services, listenerOpts.service) |
| return |
| } |
| } |
| |
| default: |
| // UDP or other protocols: no need to log, it's too noisy |
| return |
| } |
| } |
| |
| // Lets build the new listener with the filter chains. In the end, we will |
| // merge the filter chains with any existing listener on the same port/bind point |
| l := buildListener(listenerOpts, core.TrafficDirection_OUTBOUND) |
| |
| mutable := &MutableListener{ |
| MutableObjects: istionetworking.MutableObjects{ |
| Listener: l, |
| FilterChains: getPluginFilterChain(listenerOpts), |
| }, |
| } |
| |
| // Filters are serialized one time into an opaque struct once we have the complete list. |
| if err := mutable.build(lb, listenerOpts); err != nil { |
| log.Warn("buildSidecarOutboundListeners: ", err.Error()) |
| return |
| } |
| |
| // If there is a TCP listener on well known port, cannot add any http filter chain |
| // with the inspector as it will break for server-first protocols. Similarly, |
| // if there was a HTTP listener on well known port, cannot add a tcp listener |
| // with the inspector as inspector breaks all server-first protocols. |
| if currentListenerEntry != nil && |
| !isConflictWithWellKnownPort(listenerOpts.port.Protocol, currentListenerEntry.protocol, conflictType) { |
| log.Warnf("conflict happens on a well known port %d, incoming protocol %v, existing protocol %v, conflict type %v", |
| listenerOpts.port.Port, listenerOpts.port.Protocol, currentListenerEntry.protocol, conflictType) |
| return |
| } |
| |
| // There are 9 types conflicts |
| // Incoming Existing |
| // 1. HTTP -> HTTP |
| // 2. HTTP -> TCP |
| // 3. HTTP -> unknown |
| // 4. TCP -> HTTP |
| // 5. TCP -> TCP |
| // 6. TCP -> unknown |
| // 7. unknown -> HTTP |
| // 8. unknown -> TCP |
| // 9. unknown -> unknown |
| // Type 1 can be resolved by appending service to existing services |
| // Type 2 can be resolved by merging TCP filter chain with HTTP filter chain |
| // Type 3 can be resolved by appending service to existing services |
| // Type 4 can be resolved by merging HTTP filter chain with TCP filter chain |
| // Type 5 can be resolved by merging TCP filter chains |
| // Type 6 can be resolved by merging TCP filter chains |
| // Type 7 can be resolved by appending service to existing services |
| // Type 8 can be resolved by merging TCP filter chains |
| // Type 9 can be resolved by merging TCP and HTTP filter chains |
| |
| switch conflictType { |
| case NoConflict: |
| if currentListenerEntry != nil { |
| currentListenerEntry.listener.FilterChains = mergeTCPFilterChains(mutable.Listener.FilterChains, |
| listenerOpts, listenerMapKey, listenerMap) |
| } else { |
| listenerMap[listenerMapKey] = &outboundListenerEntry{ |
| services: []*model.Service{listenerOpts.service}, |
| servicePort: listenerOpts.port, |
| bind: listenerOpts.bind, |
| listener: mutable.Listener, |
| protocol: listenerPortProtocol, |
| } |
| } |
| case HTTPOverTCP: |
| // Merge HTTP filter chain to TCP filter chain |
| currentListenerEntry.listener.FilterChains = mergeFilterChains(mutable.Listener.FilterChains, currentListenerEntry.listener.FilterChains) |
| currentListenerEntry.protocol = protocol.Unsupported |
| currentListenerEntry.listener.ListenerFilters = appendListenerFilters(currentListenerEntry.listener.ListenerFilters) |
| currentListenerEntry.services = append(currentListenerEntry.services, listenerOpts.service) |
| |
| case TCPOverHTTP: |
| // Merge TCP filter chain to HTTP filter chain |
| currentListenerEntry.listener.FilterChains = mergeFilterChains(currentListenerEntry.listener.FilterChains, mutable.Listener.FilterChains) |
| currentListenerEntry.protocol = protocol.Unsupported |
| currentListenerEntry.listener.ListenerFilters = appendListenerFilters(currentListenerEntry.listener.ListenerFilters) |
| case TCPOverTCP: |
| // Merge two TCP filter chains. HTTP filter chain will not conflict with TCP filter chain because HTTP filter chain match for |
| // HTTP filter chain is different from TCP filter chain's. |
| currentListenerEntry.listener.FilterChains = mergeTCPFilterChains(mutable.Listener.FilterChains, listenerOpts, listenerMapKey, listenerMap) |
| case TCPOverAuto: |
| // Merge two TCP filter chains. HTTP filter chain will not conflict with TCP filter chain because HTTP filter chain match for |
| // HTTP filter chain is different from TCP filter chain's. |
| currentListenerEntry.listener.FilterChains = mergeTCPFilterChains(mutable.Listener.FilterChains, listenerOpts, listenerMapKey, listenerMap) |
| |
| case AutoOverHTTP: |
| listenerMap[listenerMapKey] = &outboundListenerEntry{ |
| services: append(currentListenerEntry.services, listenerOpts.service), |
| servicePort: listenerOpts.port, |
| bind: listenerOpts.bind, |
| listener: mutable.Listener, |
| protocol: protocol.Unsupported, |
| } |
| currentListenerEntry.listener.ListenerFilters = appendListenerFilters(currentListenerEntry.listener.ListenerFilters) |
| |
| case AutoOverTCP: |
| // Merge two TCP filter chains. HTTP filter chain will not conflict with TCP filter chain because HTTP filter chain match for |
| // HTTP filter chain is different from TCP filter chain's. |
| currentListenerEntry.listener.FilterChains = mergeTCPFilterChains(mutable.Listener.FilterChains, |
| listenerOpts, listenerMapKey, listenerMap) |
| currentListenerEntry.protocol = protocol.Unsupported |
| currentListenerEntry.listener.ListenerFilters = appendListenerFilters(currentListenerEntry.listener.ListenerFilters) |
| |
| default: |
| // Covered previously - in this case we return early to prevent creating listeners that we end up throwing away |
| // This should never happen |
| log.Errorf("Got unexpected conflict type %v. This should never happen", conflictType) |
| } |
| |
| if log.DebugEnabled() && len(mutable.Listener.FilterChains) > 1 || currentListenerEntry != nil { |
| var numChains int |
| if currentListenerEntry != nil { |
| numChains = len(currentListenerEntry.listener.FilterChains) |
| } else { |
| numChains = len(mutable.Listener.FilterChains) |
| } |
| log.Debugf("buildSidecarOutboundListeners: multiple filter chain listener %s with %d chains", mutable.Listener.Name, numChains) |
| } |
| } |
| |
| // httpListenerOpts are options for an HTTP listener |
| type httpListenerOpts struct { |
| routeConfig *route.RouteConfiguration |
| rds string |
| // If set, use this as a basis |
| connectionManager *hcm.HttpConnectionManager |
| // stat prefix for the http connection manager |
| // DO not set this field. Will be overridden by buildCompleteFilterChain |
| statPrefix string |
| protocol protocol.Instance |
| useRemoteAddress bool |
| |
| // http3Only indicates that the HTTP codec used |
| // is HTTP/3 over QUIC transport (uses UDP) |
| http3Only bool |
| |
| class istionetworking.ListenerClass |
| } |
| |
| // filterChainOpts describes a filter chain: a set of filters with the same TLS context |
| type filterChainOpts struct { |
| filterChainName string |
| sniHosts []string |
| destinationCIDRs []string |
| metadata *core.Metadata |
| tlsContext *auth.DownstreamTlsContext |
| httpOpts *httpListenerOpts |
| match *listener.FilterChainMatch |
| listenerFilters []*listener.ListenerFilter |
| networkFilters []*listener.Filter |
| filterChain istionetworking.FilterChain |
| } |
| |
| // buildListenerOpts are the options required to build a Listener |
| type buildListenerOpts struct { |
| // nolint: maligned |
| push *model.PushContext |
| proxy *model.Proxy |
| bind string |
| port *model.Port |
| filterChainOpts []*filterChainOpts |
| bindToPort bool |
| skipUserFilters bool |
| needHTTPInspector bool |
| class istionetworking.ListenerClass |
| service *model.Service |
| transport istionetworking.TransportProtocol |
| } |
| |
| func (lb *ListenerBuilder) buildHTTPConnectionManager(httpOpts *httpListenerOpts) *hcm.HttpConnectionManager { |
| if httpOpts.connectionManager == nil { |
| httpOpts.connectionManager = &hcm.HttpConnectionManager{} |
| } |
| |
| connectionManager := httpOpts.connectionManager |
| if httpOpts.http3Only { |
| connectionManager.CodecType = hcm.HttpConnectionManager_HTTP3 |
| connectionManager.Http3ProtocolOptions = &core.Http3ProtocolOptions{} |
| } else { |
| connectionManager.CodecType = hcm.HttpConnectionManager_AUTO |
| } |
| connectionManager.AccessLog = []*accesslog.AccessLog{} |
| connectionManager.StatPrefix = httpOpts.statPrefix |
| |
| // Setup normalization |
| connectionManager.PathWithEscapedSlashesAction = hcm.HttpConnectionManager_KEEP_UNCHANGED |
| switch lb.push.Mesh.GetPathNormalization().GetNormalization() { |
| case meshconfig.MeshConfig_ProxyPathNormalization_NONE: |
| connectionManager.NormalizePath = proto.BoolFalse |
| case meshconfig.MeshConfig_ProxyPathNormalization_BASE, meshconfig.MeshConfig_ProxyPathNormalization_DEFAULT: |
| connectionManager.NormalizePath = proto.BoolTrue |
| case meshconfig.MeshConfig_ProxyPathNormalization_MERGE_SLASHES: |
| connectionManager.NormalizePath = proto.BoolTrue |
| connectionManager.MergeSlashes = true |
| case meshconfig.MeshConfig_ProxyPathNormalization_DECODE_AND_MERGE_SLASHES: |
| connectionManager.NormalizePath = proto.BoolTrue |
| connectionManager.MergeSlashes = true |
| connectionManager.PathWithEscapedSlashesAction = hcm.HttpConnectionManager_UNESCAPE_AND_FORWARD |
| } |
| |
| if httpOpts.useRemoteAddress { |
| connectionManager.UseRemoteAddress = proto.BoolTrue |
| } else { |
| connectionManager.UseRemoteAddress = proto.BoolFalse |
| } |
| |
| // Allow websocket upgrades |
| websocketUpgrade := &hcm.HttpConnectionManager_UpgradeConfig{UpgradeType: "websocket"} |
| connectionManager.UpgradeConfigs = []*hcm.HttpConnectionManager_UpgradeConfig{websocketUpgrade} |
| |
| idleTimeout, err := time.ParseDuration(lb.node.Metadata.IdleTimeout) |
| if err == nil { |
| connectionManager.CommonHttpProtocolOptions = &core.HttpProtocolOptions{ |
| IdleTimeout: durationpb.New(idleTimeout), |
| } |
| } |
| |
| notimeout := durationpb.New(0 * time.Second) |
| connectionManager.StreamIdleTimeout = notimeout |
| |
| if httpOpts.rds != "" { |
| rds := &hcm.HttpConnectionManager_Rds{ |
| Rds: &hcm.Rds{ |
| ConfigSource: &core.ConfigSource{ |
| ConfigSourceSpecifier: &core.ConfigSource_Ads{ |
| Ads: &core.AggregatedConfigSource{}, |
| }, |
| InitialFetchTimeout: durationpb.New(0), |
| ResourceApiVersion: core.ApiVersion_V3, |
| }, |
| RouteConfigName: httpOpts.rds, |
| }, |
| } |
| connectionManager.RouteSpecifier = rds |
| } else { |
| connectionManager.RouteSpecifier = &hcm.HttpConnectionManager_RouteConfig{RouteConfig: httpOpts.routeConfig} |
| } |
| |
| accessLogBuilder.setHTTPAccessLog(lb.push, lb.node, connectionManager, httpOpts.class) |
| |
| routerFilterCtx, reqIDExtensionCtx := configureTracing(lb.push, lb.node, connectionManager, httpOpts.class) |
| |
| filters := []*hcm.HttpFilter{} |
| wasm := lb.push.WasmPlugins(lb.node) |
| // TODO: how to deal with ext-authz? It will be in the ordering twice |
| filters = append(filters, lb.authzCustomBuilder.BuildHTTP(httpOpts.class)...) |
| filters = extension.PopAppend(filters, wasm, extensions.PluginPhase_AUTHN) |
| filters = append(filters, lb.authnBuilder.BuildHTTP(httpOpts.class)...) |
| filters = extension.PopAppend(filters, wasm, extensions.PluginPhase_AUTHZ) |
| filters = append(filters, lb.authzBuilder.BuildHTTP(httpOpts.class)...) |
| |
| // TODO: these feel like the wrong place to insert, but this retains backwards compatibility with the original implementation |
| filters = extension.PopAppend(filters, wasm, extensions.PluginPhase_STATS) |
| filters = extension.PopAppend(filters, wasm, extensions.PluginPhase_UNSPECIFIED_PHASE) |
| |
| if features.MetadataExchange { |
| filters = append(filters, xdsfilters.HTTPMx) |
| } |
| |
| if httpOpts.protocol == protocol.GRPCWeb { |
| filters = append(filters, xdsfilters.GrpcWeb) |
| } |
| |
| if httpOpts.protocol.IsGRPC() { |
| filters = append(filters, xdsfilters.GrpcStats) |
| } |
| |
| // append ALPN HTTP filter in HTTP connection manager for outbound listener only. |
| if features.ALPNFilter { |
| if httpOpts.class != istionetworking.ListenerClassSidecarInbound { |
| filters = append(filters, xdsfilters.Alpn) |
| } |
| } |
| |
| // TypedPerFilterConfig in route needs these filters. |
| filters = append(filters, xdsfilters.Fault, xdsfilters.Cors) |
| filters = append(filters, lb.push.Telemetry.HTTPFilters(lb.node, httpOpts.class)...) |
| filters = append(filters, xdsfilters.BuildRouterFilter(routerFilterCtx)) |
| |
| connectionManager.HttpFilters = filters |
| connectionManager.RequestIdExtension = requestidextension.BuildUUIDRequestIDExtension(reqIDExtensionCtx) |
| |
| return connectionManager |
| } |
| |
| // buildListener builds and initializes a Listener proto based on the provided opts. It does not set any filters. |
| // Optionally for HTTP filters with TLS enabled, HTTP/3 can be supported by generating QUIC Mirror filters for the |
| // same port (it is fine as QUIC uses UDP) |
| func buildListener(opts buildListenerOpts, trafficDirection core.TrafficDirection) *listener.Listener { |
| filterChains := make([]*listener.FilterChain, 0, len(opts.filterChainOpts)) |
| listenerFiltersMap := make(map[string]bool) |
| var listenerFilters []*listener.ListenerFilter |
| |
| // add a TLS inspector if we need to detect ServerName or ALPN |
| // (this is not applicable for QUIC listeners) |
| needTLSInspector := false |
| if opts.transport == istionetworking.TransportProtocolTCP { |
| for _, chain := range opts.filterChainOpts { |
| needsALPN := chain.tlsContext != nil && chain.tlsContext.CommonTlsContext != nil && len(chain.tlsContext.CommonTlsContext.AlpnProtocols) > 0 |
| if len(chain.sniHosts) > 0 || needsALPN { |
| needTLSInspector = true |
| break |
| } |
| } |
| } |
| |
| if opts.proxy.GetInterceptionMode() == model.InterceptionTproxy && trafficDirection == core.TrafficDirection_INBOUND { |
| listenerFiltersMap[wellknown.OriginalSource] = true |
| listenerFilters = append(listenerFilters, xdsfilters.OriginalSrc) |
| } |
| |
| // We add a TLS inspector when http inspector is needed for outbound only. This |
| // is because if we ever set ALPN in the match without |
| // transport_protocol=raw_buffer, Envoy will automatically inject a tls |
| // inspector: https://github.com/envoyproxy/envoy/issues/13601. This leads to |
| // excessive logging and loss of control over the config For inbound this is not |
| // needed, since we are explicitly setting transport protocol in every single |
| // match. We can do this for outbound as well, at which point this could be |
| // removed, but have not yet |
| if opts.transport == istionetworking.TransportProtocolTCP && |
| (needTLSInspector || (opts.class == istionetworking.ListenerClassSidecarOutbound && opts.needHTTPInspector)) { |
| listenerFiltersMap[wellknown.TlsInspector] = true |
| listenerFilters = append(listenerFilters, xdsfilters.TLSInspector) |
| } |
| |
| // TODO: For now we assume that only HTTP/3 is used over QUIC. Revisit this in the future |
| if opts.needHTTPInspector && opts.transport == istionetworking.TransportProtocolTCP { |
| listenerFiltersMap[wellknown.HttpInspector] = true |
| listenerFilters = append(listenerFilters, xdsfilters.HTTPInspector) |
| } |
| |
| for _, chain := range opts.filterChainOpts { |
| for _, filter := range chain.listenerFilters { |
| if _, exist := listenerFiltersMap[filter.Name]; !exist { |
| listenerFiltersMap[filter.Name] = true |
| listenerFilters = append(listenerFilters, filter) |
| } |
| } |
| match := &listener.FilterChainMatch{} |
| needMatch := false |
| if chain.match != nil { |
| needMatch = true |
| match = chain.match |
| } |
| if len(chain.sniHosts) > 0 { |
| fullWildcardFound := false |
| for _, h := range chain.sniHosts { |
| if h == "*" { |
| fullWildcardFound = true |
| // If we have a host with *, it effectively means match anything, i.e. |
| // no SNI based matching for this host. |
| break |
| } |
| } |
| if !fullWildcardFound { |
| chain.sniHosts = append([]string{}, chain.sniHosts...) |
| sort.Stable(sort.StringSlice(chain.sniHosts)) |
| match.ServerNames = chain.sniHosts |
| } |
| } |
| if len(chain.destinationCIDRs) > 0 { |
| chain.destinationCIDRs = append([]string{}, chain.destinationCIDRs...) |
| sort.Stable(sort.StringSlice(chain.destinationCIDRs)) |
| for _, d := range chain.destinationCIDRs { |
| cidr := util.ConvertAddressToCidr(d) |
| if cidr != nil && cidr.AddressPrefix != constants.UnspecifiedIP { |
| match.PrefixRanges = append(match.PrefixRanges, cidr) |
| } |
| } |
| } |
| |
| if !needMatch && filterChainMatchEmpty(match) { |
| match = nil |
| } |
| var transportSocket *core.TransportSocket |
| switch opts.transport { |
| case istionetworking.TransportProtocolTCP: |
| transportSocket = buildDownstreamTLSTransportSocket(chain.tlsContext) |
| case istionetworking.TransportProtocolQUIC: |
| transportSocket = buildDownstreamQUICTransportSocket(chain.tlsContext) |
| } |
| filterChains = append(filterChains, &listener.FilterChain{ |
| FilterChainMatch: match, |
| TransportSocket: transportSocket, |
| }) |
| } |
| |
| var res *listener.Listener |
| switch opts.transport { |
| case istionetworking.TransportProtocolTCP: |
| var bindToPort *wrappers.BoolValue |
| var connectionBalance *listener.Listener_ConnectionBalanceConfig |
| if !opts.bindToPort { |
| bindToPort = proto.BoolFalse |
| } |
| |
| // only use to exact_balance for tcp outbound listeners; virtualOutbound listener should |
| // not have this set per Envoy docs for redirected listeners |
| if opts.proxy.Metadata.OutboundListenerExactBalance && trafficDirection == core.TrafficDirection_OUTBOUND { |
| connectionBalance = &listener.Listener_ConnectionBalanceConfig{ |
| BalanceType: &listener.Listener_ConnectionBalanceConfig_ExactBalance_{ |
| ExactBalance: &listener.Listener_ConnectionBalanceConfig_ExactBalance{}, |
| }, |
| } |
| } |
| |
| res = &listener.Listener{ |
| // TODO: need to sanitize the opts.bind if its a UDS socket, as it could have colons, that envoy doesn't like |
| Name: getListenerName(opts.bind, opts.port.Port, istionetworking.TransportProtocolTCP), |
| Address: util.BuildAddress(opts.bind, uint32(opts.port.Port)), |
| TrafficDirection: trafficDirection, |
| ListenerFilters: listenerFilters, |
| FilterChains: filterChains, |
| BindToPort: bindToPort, |
| ConnectionBalanceConfig: connectionBalance, |
| } |
| |
| if opts.proxy.Type != model.Router { |
| res.ListenerFiltersTimeout = opts.push.Mesh.ProtocolDetectionTimeout |
| if res.ListenerFiltersTimeout != nil { |
| res.ContinueOnListenerFiltersTimeout = true |
| } |
| } |
| case istionetworking.TransportProtocolQUIC: |
| // TODO: switch on TransportProtocolQUIC is in too many places now. Once this is a bit |
| // mature, refactor some of these to an interface so that they kick off the process |
| // of building listener, filter chains, serializing etc based on transport protocol |
| listenerName := getListenerName(opts.bind, opts.port.Port, istionetworking.TransportProtocolQUIC) |
| log.Debugf("buildListener: building UDP/QUIC listener %s", listenerName) |
| res = &listener.Listener{ |
| Name: listenerName, |
| Address: util.BuildNetworkAddress(opts.bind, uint32(opts.port.Port), istionetworking.TransportProtocolQUIC), |
| TrafficDirection: trafficDirection, |
| FilterChains: filterChains, |
| UdpListenerConfig: &listener.UdpListenerConfig{ |
| // TODO: Maybe we should add options in MeshConfig to |
| // configure QUIC options - it should look similar |
| // to the H2 protocol options. |
| QuicOptions: &listener.QuicProtocolOptions{}, |
| DownstreamSocketConfig: &core.UdpSocketConfig{}, |
| }, |
| EnableReusePort: proto.BoolTrue, |
| } |
| } |
| |
| accessLogBuilder.setListenerAccessLog(opts.push, opts.proxy, res, opts.class) |
| |
| return res |
| } |
| |
| func getMatchAllFilterChain(l *listener.Listener) (int, *listener.FilterChain) { |
| for i, fc := range l.FilterChains { |
| if isMatchAllFilterChain(fc) { |
| return i, fc |
| } |
| } |
| return 0, nil |
| } |
| |
| // Create pass through filter chain for the listener assuming all the other filter chains are ready. |
| // The match member of pass through filter chain depends on the existing non-passthrough filter chain. |
| // TODO(lambdai): Calculate the filter chain match to replace the wildcard and replace appendListenerFallthroughRoute. |
| func appendListenerFallthroughRouteForCompleteListener(l *listener.Listener, node *model.Proxy, push *model.PushContext) { |
| matchIndex, matchAll := getMatchAllFilterChain(l) |
| |
| fallthroughNetworkFilters := buildOutboundCatchAllNetworkFiltersOnly(push, node) |
| |
| outboundPassThroughFilterChain := &listener.FilterChain{ |
| FilterChainMatch: &listener.FilterChainMatch{}, |
| Name: util.PassthroughFilterChain, |
| Filters: fallthroughNetworkFilters, |
| } |
| |
| // Set a default filter chain. This allows us to avoid issues where |
| // traffic starts to match a filter chain but then doesn't match latter criteria, leading to |
| // dropped requests. See https://github.com/istio/istio/issues/26079 for details. |
| // If there are multiple filter chains and a match all chain, move it to DefaultFilterChain |
| // This ensures it will always be used as the fallback. |
| if matchAll != nil && len(l.FilterChains) > 1 { |
| copy(l.FilterChains[matchIndex:], l.FilterChains[matchIndex+1:]) // Shift l.FilterChains[i+1:] left one index. |
| l.FilterChains[len(l.FilterChains)-1] = nil // Erase last element (write zero value). |
| l.FilterChains = l.FilterChains[:len(l.FilterChains)-1] // Truncate slice. |
| l.DefaultFilterChain = matchAll |
| } else if matchAll == nil { |
| // Otherwise, if there is no match all already, set a passthrough match all |
| l.DefaultFilterChain = outboundPassThroughFilterChain |
| } |
| } |
| |
| // build adds the provided TCP and HTTP filters to the provided Listener and serializes them. |
| // TODO: given how tightly tied listener.FilterChains, opts.filterChainOpts, and mutable.FilterChains |
| // are to each other we should encapsulate them some way to ensure they remain consistent (mainly that |
| // in each an index refers to the same chain). |
| func (ml *MutableListener) build(builder *ListenerBuilder, opts buildListenerOpts) error { |
| if len(opts.filterChainOpts) == 0 { |
| return fmt.Errorf("must have more than 0 chains in listener %q", ml.Listener.Name) |
| } |
| httpConnectionManagers := make([]*hcm.HttpConnectionManager, len(ml.FilterChains)) |
| for i := range ml.FilterChains { |
| chain := ml.FilterChains[i] |
| opt := opts.filterChainOpts[i] |
| ml.Listener.FilterChains[i].Metadata = opt.metadata |
| ml.Listener.FilterChains[i].Name = opt.filterChainName |
| if opt.httpOpts == nil { |
| // we are building a network filter chain (no http connection manager) for this filter chain |
| // In HTTP, we need to have RBAC, etc. upfront so that they can enforce policies immediately |
| // For network filters such as mysql, mongo, etc., we need the filter codec upfront. Data from this |
| // codec is used by RBAC later. |
| // |
| // Currently, when transport is QUIC we assume HTTP3. So it should not come here. |
| // When other protocols are used over QUIC, we have to revisit this assumption. |
| |
| if len(opt.networkFilters) > 0 { |
| // this is the terminating filter |
| lastNetworkFilter := opt.networkFilters[len(opt.networkFilters)-1] |
| |
| for n := 0; n < len(opt.networkFilters)-1; n++ { |
| ml.Listener.FilterChains[i].Filters = append(ml.Listener.FilterChains[i].Filters, opt.networkFilters[n]) |
| } |
| ml.Listener.FilterChains[i].Filters = append(ml.Listener.FilterChains[i].Filters, chain.TCP...) |
| ml.Listener.FilterChains[i].Filters = append(ml.Listener.FilterChains[i].Filters, lastNetworkFilter) |
| } else { |
| ml.Listener.FilterChains[i].Filters = append(ml.Listener.FilterChains[i].Filters, chain.TCP...) |
| } |
| log.Debugf("attached %d network filters to listener %q filter chain %d", len(chain.TCP)+len(opt.networkFilters), ml.Listener.Name, i) |
| } else { |
| // Add the TCP filters first.. and then the HTTP connection manager. |
| // Skip adding this if transport is not TCP (could be QUIC) |
| if chain.TransportProtocol == istionetworking.TransportProtocolTCP { |
| ml.Listener.FilterChains[i].Filters = append(ml.Listener.FilterChains[i].Filters, chain.TCP...) |
| } |
| |
| // If statPrefix has been set before calling this method, respect that. |
| if len(opt.httpOpts.statPrefix) == 0 { |
| opt.httpOpts.statPrefix = strings.ToLower(ml.Listener.TrafficDirection.String()) + "_" + ml.Listener.Name |
| } |
| httpConnectionManagers[i] = builder.buildHTTPConnectionManager(opt.httpOpts) |
| filter := &listener.Filter{ |
| Name: wellknown.HTTPConnectionManager, |
| ConfigType: &listener.Filter_TypedConfig{TypedConfig: util.MessageToAny(httpConnectionManagers[i])}, |
| } |
| ml.Listener.FilterChains[i].Filters = append(ml.Listener.FilterChains[i].Filters, filter) |
| log.Debugf("attached HTTP filter with %d http_filter options to listener %q filter chain %d", |
| len(httpConnectionManagers[i].HttpFilters), ml.Listener.Name, i) |
| } |
| } |
| |
| return nil |
| } |
| |
| func mergeTCPFilterChains(incoming []*listener.FilterChain, listenerOpts buildListenerOpts, listenerMapKey string, |
| listenerMap map[string]*outboundListenerEntry) []*listener.FilterChain { |
| // TODO(rshriram) merge multiple identical filter chains with just a single destination CIDR based |
| // filter chain match, into a single filter chain and array of destinationcidr matches |
| |
| // The code below checks for TCP over TCP conflicts and merges listeners |
| |
| // Merge the newly built listener with the existing listener, if and only if the filter chains have distinct conditions. |
| // Extract the current filter chain matches, for every new filter chain match being added, check if there is a matching |
| // one in previous filter chains, if so, skip adding this filter chain with a warning. |
| |
| currentListenerEntry := listenerMap[listenerMapKey] |
| mergedFilterChains := make([]*listener.FilterChain, 0, len(currentListenerEntry.listener.FilterChains)+len(incoming)) |
| // Start with the current listener's filter chains. |
| mergedFilterChains = append(mergedFilterChains, currentListenerEntry.listener.FilterChains...) |
| |
| for _, incomingFilterChain := range incoming { |
| conflict := false |
| |
| for _, existingFilterChain := range mergedFilterChains { |
| conflict = isConflict(existingFilterChain, incomingFilterChain) |
| |
| if conflict { |
| // NOTE: While pluginParams.Service can be nil, |
| // this code cannot be reached if Service is nil because a pluginParams.Service can be nil only |
| // for user defined Egress listeners with ports. And these should occur in the API before |
| // the wildcard egress listener. the check for the "locked" bit will eliminate the collision. |
| // User is also not allowed to add duplicate ports in the egress listener |
| var newHostname host.Name |
| if listenerOpts.service != nil { |
| newHostname = listenerOpts.service.Hostname |
| } else { |
| // user defined outbound listener via sidecar API |
| newHostname = "sidecar-config-egress-tcp-listener" |
| } |
| |
| outboundListenerConflict{ |
| metric: model.ProxyStatusConflictOutboundListenerTCPOverTCP, |
| node: listenerOpts.proxy, |
| listenerName: listenerMapKey, |
| currentServices: currentListenerEntry.services, |
| currentProtocol: currentListenerEntry.servicePort.Protocol, |
| newHostname: newHostname, |
| newProtocol: listenerOpts.port.Protocol, |
| }.addMetric(listenerOpts.push) |
| break |
| } |
| |
| } |
| if !conflict { |
| // There is no conflict with any filter chain in the existing listener. |
| // So append the new filter chains to the existing listener's filter chains |
| mergedFilterChains = append(mergedFilterChains, incomingFilterChain) |
| if listenerOpts.service != nil { |
| lEntry := listenerMap[listenerMapKey] |
| lEntry.services = append(lEntry.services, listenerOpts.service) |
| } |
| } |
| } |
| return mergedFilterChains |
| } |
| |
| // isConflict determines whether the incoming filter chain has conflict with existing filter chain. |
| func isConflict(existing, incoming *listener.FilterChain) bool { |
| return filterChainMatchEqual(existing.FilterChainMatch, incoming.FilterChainMatch) |
| } |
| |
| func filterChainMatchEmpty(fcm *listener.FilterChainMatch) bool { |
| return fcm == nil || filterChainMatchEqual(fcm, emptyFilterChainMatch) |
| } |
| |
| // filterChainMatchEqual returns true if both filter chains are equal otherwise false. |
| func filterChainMatchEqual(first *listener.FilterChainMatch, second *listener.FilterChainMatch) bool { |
| if first == nil || second == nil { |
| return first == second |
| } |
| if first.TransportProtocol != second.TransportProtocol { |
| return false |
| } |
| if !util.StringSliceEqual(first.ApplicationProtocols, second.ApplicationProtocols) { |
| return false |
| } |
| if first.DestinationPort.GetValue() != second.DestinationPort.GetValue() { |
| return false |
| } |
| if !util.CidrRangeSliceEqual(first.PrefixRanges, second.PrefixRanges) { |
| return false |
| } |
| if !util.CidrRangeSliceEqual(first.SourcePrefixRanges, second.SourcePrefixRanges) { |
| return false |
| } |
| if !util.CidrRangeSliceEqual(first.DirectSourcePrefixRanges, second.DirectSourcePrefixRanges) { |
| return false |
| } |
| if first.AddressSuffix != second.AddressSuffix { |
| return false |
| } |
| if first.SuffixLen.GetValue() != second.SuffixLen.GetValue() { |
| return false |
| } |
| if first.SourceType != second.SourceType { |
| return false |
| } |
| if !util.UInt32SliceEqual(first.SourcePorts, second.SourcePorts) { |
| return false |
| } |
| if !util.StringSliceEqual(first.ServerNames, second.ServerNames) { |
| return false |
| } |
| return true |
| } |
| |
| func mergeFilterChains(httpFilterChain, tcpFilterChain []*listener.FilterChain) []*listener.FilterChain { |
| var newFilterChan []*listener.FilterChain |
| for _, fc := range httpFilterChain { |
| if fc.FilterChainMatch == nil { |
| fc.FilterChainMatch = &listener.FilterChainMatch{} |
| } |
| |
| var missingHTTPALPNs []string |
| for _, p := range plaintextHTTPALPNs { |
| if !contains(fc.FilterChainMatch.ApplicationProtocols, p) { |
| missingHTTPALPNs = append(missingHTTPALPNs, p) |
| } |
| } |
| |
| fc.FilterChainMatch.ApplicationProtocols = append(fc.FilterChainMatch.ApplicationProtocols, missingHTTPALPNs...) |
| newFilterChan = append(newFilterChan, fc) |
| } |
| return append(tcpFilterChain, newFilterChan...) |
| } |
| |
| // It's fine to use this naive implementation for searching in a very short list like ApplicationProtocols |
| func contains(s []string, e string) bool { |
| for _, a := range s { |
| if a == e { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func getPluginFilterChain(opts buildListenerOpts) []istionetworking.FilterChain { |
| filterChain := make([]istionetworking.FilterChain, len(opts.filterChainOpts)) |
| |
| for id := range filterChain { |
| if opts.filterChainOpts[id].httpOpts == nil { |
| filterChain[id].ListenerProtocol = istionetworking.ListenerProtocolTCP |
| } else { |
| filterChain[id].ListenerProtocol = istionetworking.ListenerProtocolHTTP |
| } |
| filterChain[id].TCP = opts.filterChainOpts[id].filterChain.TCP |
| filterChain[id].HTTP = opts.filterChainOpts[id].filterChain.HTTP |
| } |
| |
| return filterChain |
| } |
| |
| // isConflictWithWellKnownPort checks conflicts between incoming protocol and existing protocol. |
| // Mongo and MySQL are not allowed to co-exist with other protocols in one port. |
| func isConflictWithWellKnownPort(incoming, existing protocol.Instance, conflict int) bool { |
| if conflict == NoConflict { |
| return true |
| } |
| |
| if (incoming == protocol.Mongo || |
| incoming == protocol.MySQL || |
| existing == protocol.Mongo || |
| existing == protocol.MySQL) && incoming != existing { |
| return false |
| } |
| |
| return true |
| } |
| |
| func appendListenerFilters(filters []*listener.ListenerFilter) []*listener.ListenerFilter { |
| hasTLSInspector := false |
| hasHTTPInspector := false |
| |
| for _, f := range filters { |
| hasTLSInspector = hasTLSInspector || f.Name == wellknown.TlsInspector |
| hasHTTPInspector = hasHTTPInspector || f.Name == wellknown.HttpInspector |
| } |
| |
| if !hasTLSInspector { |
| filters = append(filters, xdsfilters.TLSInspector) |
| } |
| |
| if !hasHTTPInspector { |
| filters = append(filters, xdsfilters.HTTPInspector) |
| } |
| |
| return filters |
| } |
| |
| // nolint: interfacer |
| func buildDownstreamTLSTransportSocket(tlsContext *auth.DownstreamTlsContext) *core.TransportSocket { |
| if tlsContext == nil { |
| return nil |
| } |
| return &core.TransportSocket{ |
| Name: util.EnvoyTLSSocketName, |
| ConfigType: &core.TransportSocket_TypedConfig{TypedConfig: util.MessageToAny(tlsContext)}, |
| } |
| } |
| |
| func buildDownstreamQUICTransportSocket(tlsContext *auth.DownstreamTlsContext) *core.TransportSocket { |
| if tlsContext == nil { |
| return nil |
| } |
| return &core.TransportSocket{ |
| Name: util.EnvoyQUICSocketName, |
| ConfigType: &core.TransportSocket_TypedConfig{ |
| TypedConfig: util.MessageToAny(&envoyquicv3.QuicDownstreamTransport{ |
| DownstreamTlsContext: tlsContext, |
| }), |
| }, |
| } |
| } |
| |
| func isMatchAllFilterChain(fc *listener.FilterChain) bool { |
| // See if it is empty filter chain. |
| return filterChainMatchEmpty(fc.FilterChainMatch) |
| } |
| |
| func removeListenerFilterTimeout(listeners []*listener.Listener) { |
| for _, l := range listeners { |
| // Remove listener filter timeout for |
| // 1. outbound listeners AND |
| // 2. without HTTP inspector |
| hasHTTPInspector := false |
| for _, lf := range l.ListenerFilters { |
| if lf.Name == wellknown.HttpInspector { |
| hasHTTPInspector = true |
| break |
| } |
| } |
| |
| if !hasHTTPInspector && l.TrafficDirection == core.TrafficDirection_OUTBOUND { |
| l.ListenerFiltersTimeout = nil |
| l.ContinueOnListenerFiltersTimeout = false |
| } |
| } |
| } |
| |
| // listenerKey builds the key for a given bind and port |
| func listenerKey(bind string, port int) string { |
| return bind + ":" + strconv.Itoa(port) |
| } |