blob: 4b0e4383fcdc9d21ddb9f8510ab843abdbfa053a [file] [log] [blame]
// 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 (
"crypto/md5"
"encoding/binary"
"fmt"
"sort"
"strconv"
"strings"
"unsafe"
)
import (
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"
tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
"github.com/hashicorp/go-multierror"
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"
istio_route "github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/core/v1alpha3/route"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/telemetry"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/util"
"github.com/apache/dubbo-go-pixiu/pkg/config"
"github.com/apache/dubbo-go-pixiu/pkg/config/gateway"
"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/istiomultierror"
"github.com/apache/dubbo-go-pixiu/pkg/util/sets"
)
type mutableListenerOpts struct {
mutable *MutableListener
opts *buildListenerOpts
}
func (configgen *ConfigGeneratorImpl) buildGatewayListeners(builder *ListenerBuilder) *ListenerBuilder {
if builder.node.MergedGateway == nil {
log.Debugf("buildGatewayListeners: no gateways for router %v", builder.node.ID)
return builder
}
mergedGateway := builder.node.MergedGateway
log.Debugf("buildGatewayListeners: gateways after merging: %v", mergedGateway)
actualWildcard, _ := getActualWildcardAndLocalHost(builder.node)
errs := istiomultierror.New()
// Mutable objects keyed by listener name so that we can build listeners at the end.
mutableopts := make(map[string]mutableListenerOpts)
proxyConfig := builder.node.Metadata.ProxyConfigOrDefault(builder.push.Mesh.DefaultConfig)
for _, port := range mergedGateway.ServerPorts {
// Skip ports we cannot bind to. Note that MergeGateways will already translate Service port to
// targetPort, which handles the common case of exposing ports like 80 and 443 but listening on
// higher numbered ports.
if builder.node.IsUnprivileged() && port.Number < 1024 {
log.Warnf("buildGatewayListeners: skipping privileged gateway port %d for node %s as it is an unprivileged pod",
port.Number, builder.node.ID)
continue
}
bind := actualWildcard
if len(port.Bind) > 0 {
bind = port.Bind
}
// NOTE: There is no gating here to check for the value of the QUIC feature flag. However,
// they are created in MergeGatways only when the flag is set. So when it is turned off, the
// MergedQUICTransportServers would be nil so that no listener would be created. It is written this way
// to make testing a little easier.
transportToServers := map[istionetworking.TransportProtocol]map[model.ServerPort]*model.MergedServers{
istionetworking.TransportProtocolTCP: mergedGateway.MergedServers,
istionetworking.TransportProtocolQUIC: mergedGateway.MergedQUICTransportServers,
}
for transport, gwServers := range transportToServers {
if gwServers == nil {
log.Debugf("buildGatewayListeners: no gateway-server for transport %s at port %d", transport.String(), port)
continue
}
// on a given port, we can either have plain text HTTP servers or
// HTTPS/TLS servers with SNI. We cannot have a mix of http and https server on same port.
// We can also have QUIC on a given port along with HTTPS/TLS on a given port. It does not
// cause port-conflict as they use different transport protocols
opts := &buildListenerOpts{
push: builder.push,
proxy: builder.node,
bind: bind,
port: &model.Port{Port: int(port.Number)},
bindToPort: true,
class: istionetworking.ListenerClassGateway,
transport: transport,
}
lname := getListenerName(bind, int(port.Number), transport)
p := protocol.Parse(port.Protocol)
serversForPort := gwServers[port]
if serversForPort == nil {
continue
}
var newFilterChains []istionetworking.FilterChain
switch transport {
case istionetworking.TransportProtocolTCP:
newFilterChains = configgen.buildGatewayTCPBasedFilterChains(builder, p, port, opts, serversForPort, proxyConfig, mergedGateway)
case istionetworking.TransportProtocolQUIC:
// Currently, we just assume that QUIC is HTTP/3 although that does not
// have to be the case (it is just the most common case now, in the future
// we will support more cases)
newFilterChains = configgen.buildGatewayHTTP3FilterChains(builder, serversForPort, mergedGateway, proxyConfig, opts)
}
var mutable *MutableListener
if mopts, exists := mutableopts[lname]; !exists {
mutable = &MutableListener{
MutableObjects: istionetworking.MutableObjects{
// Note: buildListener creates filter chains but does not populate the filters in the chain; that's what
// this is for.
FilterChains: newFilterChains,
},
}
mutableopts[lname] = mutableListenerOpts{mutable: mutable, opts: opts}
} else {
mopts.opts.filterChainOpts = append(mopts.opts.filterChainOpts, opts.filterChainOpts...)
mopts.mutable.MutableObjects.FilterChains = append(mopts.mutable.MutableObjects.FilterChains, newFilterChains...)
mutable = mopts.mutable
}
for cnum := range mutable.FilterChains {
if mutable.FilterChains[cnum].ListenerProtocol == istionetworking.ListenerProtocolTCP {
mutable.FilterChains[cnum].TCP = append(mutable.FilterChains[cnum].TCP, builder.authzCustomBuilder.BuildTCP()...)
mutable.FilterChains[cnum].TCP = append(mutable.FilterChains[cnum].TCP, builder.authzBuilder.BuildTCP()...)
}
}
}
}
listeners := make([]*listener.Listener, 0)
for _, ml := range mutableopts {
ml.mutable.Listener = buildListener(*ml.opts, core.TrafficDirection_OUTBOUND)
log.Debugf("buildGatewayListeners: marshaling listener %q with %d filter chains",
ml.mutable.Listener.GetName(), len(ml.mutable.Listener.GetFilterChains()))
// Filters are serialized one time into an opaque struct once we have the complete list.
if err := ml.mutable.build(builder, *ml.opts); err != nil {
errs = multierror.Append(errs, fmt.Errorf("gateway omitting listener %q due to: %v", ml.mutable.Listener.Name, err.Error()))
continue
}
listeners = append(listeners, ml.mutable.Listener)
}
// We'll try to return any listeners we successfully marshaled; if we have none, we'll emit the error we built up
err := errs.ErrorOrNil()
if err != nil {
// we have some listeners to return, but we also have some errors; log them
log.Info(err.Error())
}
if len(mutableopts) == 0 {
log.Warnf("gateway has zero listeners for node %v", builder.node.ID)
return builder
}
builder.gatewayListeners = listeners
return builder
}
func (configgen *ConfigGeneratorImpl) buildGatewayTCPBasedFilterChains(
builder *ListenerBuilder,
p protocol.Instance, port model.ServerPort,
opts *buildListenerOpts,
serversForPort *model.MergedServers,
proxyConfig *meshconfig.ProxyConfig,
mergedGateway *model.MergedGateway,
) []istionetworking.FilterChain {
newFilterChains := make([]istionetworking.FilterChain, 0)
if p.IsHTTP() {
// We have a list of HTTP servers on this port. Build a single listener for the server port.
// We only need to look at the first server in the list as the merge logic
// ensures that all servers are of same type.
port := &networking.Port{Number: port.Number, Protocol: port.Protocol}
opts.filterChainOpts = []*filterChainOpts{
configgen.createGatewayHTTPFilterChainOpts(builder.node, port, nil, serversForPort.RouteName,
proxyConfig, istionetworking.ListenerProtocolTCP),
}
newFilterChains = append(newFilterChains, istionetworking.FilterChain{
ListenerProtocol: istionetworking.ListenerProtocolHTTP,
})
} else {
// build http connection manager with TLS context, for HTTPS servers using simple/mutual TLS
// build listener with tcp proxy, with or without TLS context, for TCP servers
// or TLS servers using simple/mutual/passthrough TLS
// or HTTPS servers using passthrough TLS
// This process typically yields multiple filter chain matches (with SNI) [if TLS is used]
tcpFilterChainOpts := make([]*filterChainOpts, 0)
for _, server := range serversForPort.Servers {
if gateway.IsTLSServer(server) && gateway.IsHTTPServer(server) {
routeName := mergedGateway.TLSServerInfo[server].RouteName
// This is a HTTPS server, where we are doing TLS termination. Build a http connection manager with TLS context
tcpFilterChainOpts = append(tcpFilterChainOpts, configgen.createGatewayHTTPFilterChainOpts(builder.node, server.Port, server,
routeName, proxyConfig, istionetworking.TransportProtocolTCP))
newFilterChains = append(newFilterChains, istionetworking.FilterChain{
ListenerProtocol: istionetworking.ListenerProtocolHTTP,
})
} else {
// This is the case of TCP or PASSTHROUGH.
tcpChainOpts := configgen.createGatewayTCPFilterChainOpts(builder.node, builder.push,
server, mergedGateway.GatewayNameForServer[server])
tcpFilterChainOpts = append(tcpFilterChainOpts, tcpChainOpts...)
for i := 0; i < len(tcpChainOpts); i++ {
newFilterChains = append(newFilterChains, istionetworking.FilterChain{
ListenerProtocol: istionetworking.ListenerProtocolTCP,
})
}
}
}
opts.filterChainOpts = tcpFilterChainOpts
}
return newFilterChains
}
func (configgen *ConfigGeneratorImpl) buildGatewayHTTP3FilterChains(
builder *ListenerBuilder,
serversForPort *model.MergedServers,
mergedGateway *model.MergedGateway,
proxyConfig *meshconfig.ProxyConfig,
opts *buildListenerOpts,
) []istionetworking.FilterChain {
newFilterChains := make([]istionetworking.FilterChain, 0)
quicFilterChainOpts := make([]*filterChainOpts, 0)
for _, server := range serversForPort.Servers {
log.Debugf("buildGatewayListeners: creating QUIC filter chain for port %d(%s:%s)",
server.GetPort().GetNumber(), server.GetPort().GetName(), server.GetPort().GetProtocol())
// Here it is assumed that this HTTP/3 server is a mirror of an existing HTTPS
// server. So the same route name would be reused instead of creating new one.
routeName := mergedGateway.TLSServerInfo[server].RouteName
quicFilterChainOpts = append(quicFilterChainOpts, configgen.createGatewayHTTPFilterChainOpts(builder.node, server.Port, server,
routeName, proxyConfig, istionetworking.TransportProtocolQUIC))
newFilterChains = append(newFilterChains, istionetworking.FilterChain{
// Make sure that this is set to HTTP so that JWT and Authorization
// filters that are applied to HTTPS are also applied to this chain.
// Not doing so is a security hole as would allow bypassing auth.
ListenerProtocol: istionetworking.ListenerProtocolHTTP,
TransportProtocol: istionetworking.TransportProtocolQUIC,
})
}
opts.filterChainOpts = quicFilterChainOpts
return newFilterChains
}
func getListenerName(bind string, port int, transport istionetworking.TransportProtocol) string {
switch transport {
case istionetworking.TransportProtocolTCP:
return bind + "_" + strconv.Itoa(port)
case istionetworking.TransportProtocolQUIC:
return "udp_" + bind + "_" + strconv.Itoa(port)
}
return "unknown"
}
func buildNameToServiceMapForHTTPRoutes(node *model.Proxy, push *model.PushContext,
virtualService config.Config) map[host.Name]*model.Service {
vs := virtualService.Spec.(*networking.VirtualService)
nameToServiceMap := map[host.Name]*model.Service{}
addService := func(hostname host.Name) {
if _, exist := nameToServiceMap[hostname]; exist {
return
}
var service *model.Service
// First, we obtain the service which has the same namespace as virtualService
s, exist := push.ServiceIndex.HostnameAndNamespace[hostname][virtualService.Namespace]
if exist {
// We should check whether the selected service is visible to the proxy node.
if push.IsServiceVisible(s, node.ConfigNamespace) {
service = s
}
}
// If we find no service for the namespace of virtualService or the selected service is not visible to the proxy node,
// we should fallback to pick one service which is visible to the ConfigNamespace of node.
if service == nil {
service = push.ServiceForHostname(node, hostname)
}
nameToServiceMap[hostname] = service
}
for _, httpRoute := range vs.Http {
if httpRoute.GetMirror() != nil {
addService(host.Name(httpRoute.GetMirror().GetHost()))
}
for _, route := range httpRoute.GetRoute() {
if route.GetDestination() != nil {
addService(host.Name(route.GetDestination().GetHost()))
}
}
}
return nameToServiceMap
}
func (configgen *ConfigGeneratorImpl) buildGatewayHTTPRouteConfig(node *model.Proxy, push *model.PushContext,
routeName string) *route.RouteConfiguration {
if node.MergedGateway == nil {
log.Warnf("buildGatewayRoutes: no gateways for router %v", node.ID)
return &route.RouteConfiguration{
Name: routeName,
VirtualHosts: []*route.VirtualHost{},
ValidateClusters: proto.BoolFalse,
}
}
merged := node.MergedGateway
log.Debugf("buildGatewayRoutes: gateways after merging: %v", merged)
// make sure that there is some server listening on this port
if _, ok := merged.ServersByRouteName[routeName]; !ok {
log.Warnf("Gateway missing for route %s. This is normal if gateway was recently deleted.", routeName)
// This can happen when a gateway has recently been deleted. Envoy will still request route
// information due to the draining of listeners, so we should not return an error.
return nil
}
servers := merged.ServersByRouteName[routeName]
// When this is true, we add alt-svc header to the response to tell the client
// that HTTP/3 over QUIC is available on the same port for this host. This is
// very important for discovering HTTP/3 services
_, isH3DiscoveryNeeded := merged.HTTP3AdvertisingRoutes[routeName]
gatewayRoutes := make(map[string]map[string][]*route.Route)
gatewayVirtualServices := make(map[string][]config.Config)
vHostDedupMap := make(map[host.Name]*route.VirtualHost)
for _, server := range servers {
gatewayName := merged.GatewayNameForServer[server]
port := int(server.Port.Number)
var virtualServices []config.Config
var exists bool
if virtualServices, exists = gatewayVirtualServices[gatewayName]; !exists {
virtualServices = push.VirtualServicesForGateway(node.ConfigNamespace, gatewayName)
gatewayVirtualServices[gatewayName] = virtualServices
}
for _, virtualService := range virtualServices {
virtualServiceHosts := host.NewNames(virtualService.Spec.(*networking.VirtualService).Hosts)
serverHosts := host.NamesForNamespace(server.Hosts, virtualService.Namespace)
// We have two cases here:
// 1. virtualService hosts are 1.foo.com, 2.foo.com, 3.foo.com and server hosts are ns/*.foo.com
// 2. virtualService hosts are *.foo.com, and server hosts are ns/1.foo.com, ns/2.foo.com, ns/3.foo.com
intersectingHosts := serverHosts.Intersection(virtualServiceHosts)
if len(intersectingHosts) == 0 {
continue
}
// Make sure we can obtain services which are visible to this virtualService as much as possible.
nameToServiceMap := buildNameToServiceMapForHTTPRoutes(node, push, virtualService)
var routes []*route.Route
var exists bool
var err error
if _, exists = gatewayRoutes[gatewayName]; !exists {
gatewayRoutes[gatewayName] = make(map[string][]*route.Route)
}
vskey := virtualService.Name + "/" + virtualService.Namespace
if routes, exists = gatewayRoutes[gatewayName][vskey]; !exists {
hashByDestination := istio_route.GetConsistentHashForVirtualService(push, node, virtualService, nameToServiceMap)
routes, err = istio_route.BuildHTTPRoutesForVirtualService(node, virtualService, nameToServiceMap,
hashByDestination, port, map[string]bool{gatewayName: true}, isH3DiscoveryNeeded, push.Mesh)
if err != nil {
log.Debugf("%s omitting routes for virtual service %v/%v due to error: %v", node.ID, virtualService.Namespace, virtualService.Name, err)
continue
}
gatewayRoutes[gatewayName][vskey] = routes
}
for _, hostname := range intersectingHosts {
if vHost, exists := vHostDedupMap[hostname]; exists {
vHost.Routes = append(vHost.Routes, routes...)
if server.Tls != nil && server.Tls.HttpsRedirect {
vHost.RequireTls = route.VirtualHost_ALL
}
} else {
newVHost := &route.VirtualHost{
Name: util.DomainName(string(hostname), port),
Domains: buildGatewayVirtualHostDomains(string(hostname), port),
Routes: routes,
IncludeRequestAttemptCount: true,
}
if server.Tls != nil && server.Tls.HttpsRedirect {
newVHost.RequireTls = route.VirtualHost_ALL
}
vHostDedupMap[hostname] = newVHost
}
}
}
// check all hostname in vHostDedupMap and if is not exist with HttpsRedirect set to true
// create VirtualHost to redirect
for _, hostname := range server.Hosts {
if !server.GetTls().GetHttpsRedirect() {
continue
}
if vHost, exists := vHostDedupMap[host.Name(hostname)]; exists {
vHost.RequireTls = route.VirtualHost_ALL
continue
}
newVHost := &route.VirtualHost{
Name: util.DomainName(hostname, port),
Domains: buildGatewayVirtualHostDomains(hostname, port),
IncludeRequestAttemptCount: true,
RequireTls: route.VirtualHost_ALL,
}
vHostDedupMap[host.Name(hostname)] = newVHost
}
}
var virtualHosts []*route.VirtualHost
if len(vHostDedupMap) == 0 {
port := int(servers[0].Port.Number)
log.Warnf("constructed http route config for route %s on port %d with no vhosts; Setting up a default 404 vhost", routeName, port)
virtualHosts = []*route.VirtualHost{{
Name: util.DomainName("blackhole", port),
Domains: []string{"*"},
// Empty route list will cause Envoy to 404 NR any requests
Routes: []*route.Route{},
}}
} else {
virtualHosts = make([]*route.VirtualHost, 0, len(vHostDedupMap))
vHostDedupMap = collapseDuplicateRoutes(vHostDedupMap)
for _, v := range vHostDedupMap {
v.Routes = istio_route.CombineVHostRoutes(v.Routes)
virtualHosts = append(virtualHosts, v)
}
}
util.SortVirtualHosts(virtualHosts)
routeCfg := &route.RouteConfiguration{
// Retain the routeName as its used by EnvoyFilter patching logic
Name: routeName,
VirtualHosts: virtualHosts,
ValidateClusters: proto.BoolFalse,
}
return routeCfg
}
// hashRouteList returns a hash of a list of pointers
func hashRouteList(r []*route.Route) uint64 {
hash := md5.New()
for _, v := range r {
u := uintptr(unsafe.Pointer(v))
size := unsafe.Sizeof(u)
b := make([]byte, size)
switch size {
case 4:
binary.LittleEndian.PutUint32(b, uint32(u))
default:
binary.LittleEndian.PutUint64(b, uint64(u))
}
hash.Write(b)
}
var tmp [md5.Size]byte
sum := hash.Sum(tmp[:0])
return binary.LittleEndian.Uint64(sum)
}
// collapseDuplicateRoutes prevents cardinality explosion when we have multiple hostnames defined for the same set of routes
// with virtual service: {hosts: [a, b], routes: [r1, r2]}
// before: [{vhosts: [a], routes: [r1, r2]},{vhosts: [b], routes: [r1, r2]}]
// after: [{vhosts: [a,b], routes: [r1, r2]}]
// Note: At this point in the code, r1 and r2 are just pointers. However, once we send them over the wire
// they are fully expanded and expensive, so the optimization is important.
func collapseDuplicateRoutes(input map[host.Name]*route.VirtualHost) map[host.Name]*route.VirtualHost {
if !features.EnableRouteCollapse {
return input
}
dedupe := make(map[host.Name]*route.VirtualHost, len(input))
known := make(map[uint64]host.Name, len(input))
// In order to ensure stable XDS, we need to sort things. First vhost alphabetically will be the "primary"
var hostnameKeys host.Names = make([]host.Name, 0, len(input))
for k := range input {
hostnameKeys = append(hostnameKeys, k)
}
sort.Sort(hostnameKeys)
for _, h := range hostnameKeys {
vh := input[h]
hash := hashRouteList(vh.Routes)
eh, f := known[hash]
if f && vhostMergeable(vh, dedupe[eh]) {
// Merge domains, routes are identical. We check the hash *and* routesEqual so that we don't depend on not having
// collisions.
// routesEqual is fairly cheap, but not cheap enough to do n^2 checks, so both are needed
dedupe[eh].Domains = append(dedupe[eh].Domains, vh.Domains...)
} else {
known[hash] = h
dedupe[h] = vh
}
}
return dedupe
}
// vhostMergeable checks if two virtual hosts can be merged
// We explicitly do not check domains or name, as those are the keys for the merge
func vhostMergeable(a, b *route.VirtualHost) bool {
if a.IncludeRequestAttemptCount != b.IncludeRequestAttemptCount {
return false
}
if a.RequireTls != b.RequireTls {
return false
}
if !routesEqual(a.Routes, b.Routes) {
return false
}
return true
}
func routesEqual(a, b []*route.Route) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// builds a HTTP connection manager for servers of type HTTP or HTTPS (mode: simple/mutual)
func (configgen *ConfigGeneratorImpl) createGatewayHTTPFilterChainOpts(node *model.Proxy, port *networking.Port, server *networking.Server,
routeName string, proxyConfig *meshconfig.ProxyConfig, transportProtocol istionetworking.TransportProtocol) *filterChainOpts {
serverProto := protocol.Parse(port.Protocol)
if serverProto.IsHTTP() {
return &filterChainOpts{
// This works because we validate that only HTTPS servers can have same port but still different port names
// and that no two non-HTTPS servers can be on same port or share port names.
// Validation is done per gateway and also during merging
sniHosts: nil,
tlsContext: nil,
httpOpts: &httpListenerOpts{
rds: routeName,
useRemoteAddress: true,
connectionManager: buildGatewayConnectionManager(proxyConfig, node, false /* http3SupportEnabled */),
protocol: serverProto,
class: istionetworking.ListenerClassGateway,
},
}
}
// Build a filter chain for the HTTPS server
// We know that this is a HTTPS server because this function is called only for ports of type HTTP/HTTPS
// where HTTPS server's TLS mode is not passthrough and not nil
http3Enabled := transportProtocol == istionetworking.TransportProtocolQUIC
return &filterChainOpts{
// This works because we validate that only HTTPS servers can have same port but still different port names
// and that no two non-HTTPS servers can be on same port or share port names.
// Validation is done per gateway and also during merging
sniHosts: node.MergedGateway.TLSServerInfo[server].SNIHosts,
tlsContext: buildGatewayListenerTLSContext(server, node, transportProtocol),
httpOpts: &httpListenerOpts{
rds: routeName,
useRemoteAddress: true,
connectionManager: buildGatewayConnectionManager(proxyConfig, node, http3Enabled),
protocol: serverProto,
statPrefix: server.Name,
http3Only: http3Enabled,
class: istionetworking.ListenerClassGateway,
},
}
}
func buildGatewayConnectionManager(proxyConfig *meshconfig.ProxyConfig, node *model.Proxy, http3SupportEnabled bool) *hcm.HttpConnectionManager {
httpProtoOpts := &core.Http1ProtocolOptions{}
if features.HTTP10 || enableHTTP10(node.Metadata.HTTP10) {
httpProtoOpts.AcceptHttp_10 = true
}
xffNumTrustedHops := uint32(0)
forwardClientCertDetails := util.MeshConfigToEnvoyForwardClientCertDetails(meshconfig.Topology_SANITIZE_SET)
if proxyConfig != nil && proxyConfig.GatewayTopology != nil {
xffNumTrustedHops = proxyConfig.GatewayTopology.NumTrustedProxies
if proxyConfig.GatewayTopology.ForwardClientCertDetails != meshconfig.Topology_UNDEFINED {
forwardClientCertDetails = util.MeshConfigToEnvoyForwardClientCertDetails(proxyConfig.GatewayTopology.ForwardClientCertDetails)
}
}
var stripPortMode *hcm.HttpConnectionManager_StripAnyHostPort
if features.StripHostPort {
stripPortMode = &hcm.HttpConnectionManager_StripAnyHostPort{StripAnyHostPort: true}
}
httpConnManager := &hcm.HttpConnectionManager{
XffNumTrustedHops: xffNumTrustedHops,
// Forward client cert if connection is mTLS
ForwardClientCertDetails: forwardClientCertDetails,
SetCurrentClientCertDetails: &hcm.HttpConnectionManager_SetCurrentClientCertDetails{
Subject: proto.BoolTrue,
Cert: true,
Uri: true,
Dns: true,
},
ServerName: EnvoyServerName,
HttpProtocolOptions: httpProtoOpts,
StripPortMode: stripPortMode,
}
if http3SupportEnabled {
httpConnManager.Http3ProtocolOptions = &core.Http3ProtocolOptions{}
httpConnManager.CodecType = hcm.HttpConnectionManager_HTTP3
}
return httpConnManager
}
// sdsPath: is the path to the mesh-wide workload sds uds path, and it is assumed that if this path is unset, that sds is
// disabled mesh-wide
// metadata: map of miscellaneous configuration values sent from the Envoy instance back to Pilot, could include the field
//
// Below is a table of potential scenarios for the gateway configuration:
//
// TLS mode | Mesh-wide SDS | Ingress SDS | Resulting Configuration
// SIMPLE/MUTUAL | ENABLED | ENABLED | support SDS at ingress gateway to terminate SSL communication outside the mesh
// ISTIO_MUTUAL | ENABLED | DISABLED | support SDS at gateway to terminate workload mTLS, with internal workloads
//
// | for egress or with another trusted cluster for ingress)
//
// ISTIO_MUTUAL | DISABLED | DISABLED | use file-mounted secret paths to terminate workload mTLS from gateway
//
// Note that ISTIO_MUTUAL TLS mode and ingressSds should not be used simultaneously on the same ingress gateway.
func buildGatewayListenerTLSContext(
server *networking.Server, proxy *model.Proxy, transportProtocol istionetworking.TransportProtocol) *tls.DownstreamTlsContext {
// Server.TLS cannot be nil or passthrough. But as a safety guard, return nil
if server.Tls == nil || gateway.IsPassThroughServer(server) {
return nil // We don't need to setup TLS context for passthrough mode
}
server.Tls.CipherSuites = filteredGatewayCipherSuites(server)
return BuildListenerTLSContext(server.Tls, proxy, transportProtocol)
}
func convertTLSProtocol(in networking.ServerTLSSettings_TLSProtocol) tls.TlsParameters_TlsProtocol {
out := tls.TlsParameters_TlsProtocol(in) // There should be a one-to-one enum mapping
if out < tls.TlsParameters_TLS_AUTO || out > tls.TlsParameters_TLSv1_3 {
log.Warnf("was not able to map TLS protocol to Envoy TLS protocol")
return tls.TlsParameters_TLS_AUTO
}
return out
}
func (configgen *ConfigGeneratorImpl) createGatewayTCPFilterChainOpts(
node *model.Proxy, push *model.PushContext, server *networking.Server,
gatewayName string) []*filterChainOpts {
// We have a TCP/TLS server. This could be TLS termination (user specifies server.TLS with simple/mutual)
// or opaque TCP (server.TLS is nil). or it could be a TLS passthrough with SNI based routing.
// This is opaque TCP server. Find matching virtual services with TCP blocks and forward
if server.Tls == nil {
if filters := buildGatewayNetworkFiltersFromTCPRoutes(node,
push, server, gatewayName); len(filters) > 0 {
return []*filterChainOpts{
{
sniHosts: nil,
tlsContext: nil,
networkFilters: filters,
},
}
}
} else if !gateway.IsPassThroughServer(server) {
// TCP with TLS termination and forwarding. Setup TLS context to terminate, find matching services with TCP blocks
// and forward to backend
// Validation ensures that non-passthrough servers will have certs
if filters := buildGatewayNetworkFiltersFromTCPRoutes(node,
push, server, gatewayName); len(filters) > 0 {
return []*filterChainOpts{
{
sniHosts: node.MergedGateway.TLSServerInfo[server].SNIHosts,
tlsContext: buildGatewayListenerTLSContext(server, node, istionetworking.TransportProtocolTCP),
networkFilters: filters,
},
}
}
} else {
// Passthrough server.
return buildGatewayNetworkFiltersFromTLSRoutes(node, push, server, gatewayName)
}
return []*filterChainOpts{}
}
// buildGatewayNetworkFiltersFromTCPRoutes builds tcp proxy routes for all VirtualServices with TCP blocks.
// It first obtains all virtual services bound to the set of Gateways for this workload, filters them by this
// server's port and hostnames, and produces network filters for each destination from the filtered services.
func buildGatewayNetworkFiltersFromTCPRoutes(node *model.Proxy, push *model.PushContext, server *networking.Server, gateway string) []*listener.Filter {
port := &model.Port{
Name: server.Port.Name,
Port: int(server.Port.Number),
Protocol: protocol.Parse(server.Port.Protocol),
}
gatewayServerHosts := make(map[host.Name]bool, len(server.Hosts))
for _, hostname := range server.Hosts {
gatewayServerHosts[host.Name(hostname)] = true
}
virtualServices := push.VirtualServicesForGateway(node.ConfigNamespace, gateway)
if len(virtualServices) == 0 {
log.Warnf("no virtual service bound to gateway: %v", gateway)
}
for _, v := range virtualServices {
vsvc := v.Spec.(*networking.VirtualService)
// We have two cases here:
// 1. virtualService hosts are 1.foo.com, 2.foo.com, 3.foo.com and gateway's hosts are ns/*.foo.com
// 2. virtualService hosts are *.foo.com, and gateway's hosts are ns/1.foo.com, ns/2.foo.com, ns/3.foo.com
// Since this is TCP, neither matters. We are simply looking for matching virtual service for this gateway
matchingHosts := pickMatchingGatewayHosts(gatewayServerHosts, v)
if len(matchingHosts) == 0 {
// the VirtualService's hosts don't include hosts advertised by server
continue
}
// ensure we satisfy the rule's l4 match conditions, if any exist
// For the moment, there can be only one match that succeeds
// based on the match port/server port and the gateway name
for _, tcp := range vsvc.Tcp {
if l4MultiMatch(tcp.Match, server, gateway) {
return buildOutboundNetworkFilters(node, tcp.Route, push, port, v.Meta)
}
}
}
return nil
}
// buildGatewayNetworkFiltersFromTLSRoutes builds tcp proxy routes for all VirtualServices with TLS blocks.
// It first obtains all virtual services bound to the set of Gateways for this workload, filters them by this
// server's port and hostnames, and produces network filters for each destination from the filtered services
func buildGatewayNetworkFiltersFromTLSRoutes(node *model.Proxy, push *model.PushContext, server *networking.Server,
gatewayName string) []*filterChainOpts {
port := &model.Port{
Name: server.Port.Name,
Port: int(server.Port.Number),
Protocol: protocol.Parse(server.Port.Protocol),
}
gatewayServerHosts := make(map[host.Name]bool, len(server.Hosts))
for _, hostname := range server.Hosts {
gatewayServerHosts[host.Name(hostname)] = true
}
filterChains := make([]*filterChainOpts, 0)
if server.Tls.Mode == networking.ServerTLSSettings_AUTO_PASSTHROUGH {
if features.EnableLegacyAutoPassthrough {
// auto passthrough does not require virtual services. It sets up envoy.filters.network.sni_cluster filter
filterChains = append(filterChains, &filterChainOpts{
sniHosts: node.MergedGateway.TLSServerInfo[server].SNIHosts,
tlsContext: nil, // NO TLS context because this is passthrough
networkFilters: buildOutboundAutoPassthroughFilterStack(push, node, port),
})
} else {
filterChains = append(filterChains, builtAutoPassthroughFilterChains(push, node, node.MergedGateway.TLSServerInfo[server].SNIHosts)...)
}
} else {
tlsSniHosts := map[string]struct{}{} // sni host -> exists
virtualServices := push.VirtualServicesForGateway(node.ConfigNamespace, gatewayName)
for _, v := range virtualServices {
vsvc := v.Spec.(*networking.VirtualService)
// We have two cases here:
// 1. virtualService hosts are 1.foo.com, 2.foo.com, 3.foo.com and gateway's hosts are ns/*.foo.com
// 2. virtualService hosts are *.foo.com, and gateway's hosts are ns/1.foo.com, ns/2.foo.com, ns/3.foo.com
// The code below only handles 1.
// TODO: handle case 2
matchingHosts := pickMatchingGatewayHosts(gatewayServerHosts, v)
if len(matchingHosts) == 0 {
// the VirtualService's hosts don't include hosts advertised by server
continue
}
// For every matching TLS block, generate a filter chain with sni match
// TODO: Bug..if there is a single virtual service with *.foo.com, and multiple TLS block
// matches, one for 1.foo.com, another for 2.foo.com, this code will produce duplicate filter
// chain matches
for _, tls := range vsvc.Tls {
for i, match := range tls.Match {
if l4SingleMatch(convertTLSMatchToL4Match(match), server, gatewayName) {
// Envoy will reject config that has multiple filter chain matches with the same matching rules
// To avoid this, we need to make sure we don't have duplicated SNI hosts, which will become
// SNI filter chain matches
if duplicateSniHosts := model.CheckDuplicates(match.SniHosts, tlsSniHosts); len(duplicateSniHosts) != 0 {
log.Debugf(
"skipping VirtualService %s rule #%v on server port %d of gateway %s, duplicate SNI host names: %v",
v.Meta.Name, i, port.Port, gatewayName, duplicateSniHosts)
model.RecordRejectedConfig(gatewayName)
continue
}
// the sni hosts in the match will become part of a filter chain match
filterChains = append(filterChains, &filterChainOpts{
sniHosts: match.SniHosts,
tlsContext: nil, // NO TLS context because this is passthrough
networkFilters: buildOutboundNetworkFilters(node, tls.Route, push, port, v.Meta),
})
}
}
}
}
}
return filterChains
}
// builtAutoPassthroughFilterChains builds a set of filter chains for auto_passthrough gateway servers.
// These servers allow connecting to any SNI-DNAT upstream cluster that matches the server's hostname.
// To handle this, we generate a filter chain per upstream cluster
func builtAutoPassthroughFilterChains(push *model.PushContext, proxy *model.Proxy, hosts []string) []*filterChainOpts {
filterChains := make([]*filterChainOpts, 0)
for _, service := range proxy.SidecarScope.Services() {
if service.MeshExternal {
continue
}
for _, port := range service.Ports {
if port.Protocol == protocol.UDP {
continue
}
matchFound := false
for _, h := range hosts {
if service.Hostname.SubsetOf(host.Name(h)) {
matchFound = true
break
}
}
if !matchFound {
continue
}
clusterName := model.BuildDNSSrvSubsetKey(model.TrafficDirectionOutbound, "", service.Hostname, port.Port)
statPrefix := clusterName
if len(push.Mesh.OutboundClusterStatName) != 0 {
statPrefix = telemetry.BuildStatPrefix(push.Mesh.OutboundClusterStatName, string(service.Hostname), "", port, &service.Attributes)
}
destinationRule := CastDestinationRule(proxy.SidecarScope.DestinationRule(
model.TrafficDirectionOutbound, proxy, service.Hostname))
// First, we build the standard cluster. We match on the SNI matching the cluster name
// (per the spec of AUTO_PASSTHROUGH), as well as all possible Istio mTLS ALPNs. This,
// along with filtering out plaintext destinations in EDS, ensures that our requests will
// always hit an Istio mTLS filter chain on the inbound side. As a result, it should not
// be possible for anyone to access a cluster without mTLS. Note that we cannot actually
// check for mTLS here, as we are doing passthrough TLS.
filterChains = append(filterChains, &filterChainOpts{
sniHosts: []string{clusterName},
match: &listener.FilterChainMatch{ApplicationProtocols: allIstioMtlsALPNs},
tlsContext: nil, // NO TLS context because this is passthrough
networkFilters: buildOutboundNetworkFiltersWithSingleDestination(push, proxy, statPrefix, clusterName, "", port, destinationRule),
})
// Do the same, but for each subset
for _, subset := range destinationRule.GetSubsets() {
subsetClusterName := model.BuildDNSSrvSubsetKey(model.TrafficDirectionOutbound, subset.Name, service.Hostname, port.Port)
subsetStatPrefix := subsetClusterName
// If stat name is configured, build the stat prefix from configured pattern.
if len(push.Mesh.OutboundClusterStatName) != 0 {
subsetStatPrefix = telemetry.BuildStatPrefix(push.Mesh.OutboundClusterStatName, string(service.Hostname), subset.Name, port, &service.Attributes)
}
filterChains = append(filterChains, &filterChainOpts{
sniHosts: []string{subsetClusterName},
match: &listener.FilterChainMatch{ApplicationProtocols: allIstioMtlsALPNs},
tlsContext: nil, // NO TLS context because this is passthrough
networkFilters: buildOutboundNetworkFiltersWithSingleDestination(push, proxy, subsetStatPrefix, subsetClusterName, subset.Name, port, destinationRule),
})
}
}
}
return filterChains
}
// Select the virtualService's hosts that match the ones specified in the gateway server's hosts
// based on the wildcard hostname match and the namespace match
func pickMatchingGatewayHosts(gatewayServerHosts map[host.Name]bool, virtualService config.Config) map[string]host.Name {
matchingHosts := make(map[string]host.Name)
virtualServiceHosts := virtualService.Spec.(*networking.VirtualService).Hosts
for _, vsvcHost := range virtualServiceHosts {
for gatewayHost := range gatewayServerHosts {
gwHostnameForMatching := gatewayHost
if strings.Contains(string(gwHostnameForMatching), "/") {
// match the namespace first
// gateway merging code ensures that we only have ns/host
// and no ./* or */host
parts := strings.Split(string(gwHostnameForMatching), "/")
if parts[0] != virtualService.Namespace {
continue
}
// strip the namespace
gwHostnameForMatching = host.Name(parts[1])
}
if gwHostnameForMatching.Matches(host.Name(vsvcHost)) {
// assign the actual gateway host because calling code uses it as a key
// to locate TLS redirect servers
matchingHosts[vsvcHost] = gatewayHost
}
}
}
return matchingHosts
}
func convertTLSMatchToL4Match(tlsMatch *networking.TLSMatchAttributes) *networking.L4MatchAttributes {
return &networking.L4MatchAttributes{
DestinationSubnets: tlsMatch.DestinationSubnets,
Port: tlsMatch.Port,
SourceLabels: tlsMatch.SourceLabels,
Gateways: tlsMatch.Gateways,
SourceNamespace: tlsMatch.SourceNamespace,
}
}
func l4MultiMatch(predicates []*networking.L4MatchAttributes, server *networking.Server, gateway string) bool {
// NB from proto definitions: each set of predicates is OR'd together; inside of a predicate all conditions are AND'd.
// This means we can return as soon as we get any match of an entire predicate.
for _, match := range predicates {
if l4SingleMatch(match, server, gateway) {
return true
}
}
// If we had no predicates we match; otherwise we don't match since we'd have exited at the first match.
return len(predicates) == 0
}
func l4SingleMatch(match *networking.L4MatchAttributes, server *networking.Server, gateway string) bool {
// if there's no gateway predicate, gatewayMatch is true; otherwise we match against the gateways for this workload
return isPortMatch(match.Port, server) && isGatewayMatch(gateway, match.Gateways)
}
func isPortMatch(port uint32, server *networking.Server) bool {
// if there's no port predicate, portMatch is true; otherwise we evaluate the port predicate against the server's port
portMatch := port == 0
if port != 0 {
portMatch = server.Port.Number == port
}
return portMatch
}
func isGatewayMatch(gateway string, gatewayNames []string) bool {
// if there's no gateway predicate, gatewayMatch is true; otherwise we match against the gateways for this workload
if len(gatewayNames) == 0 {
return true
}
if len(gatewayNames) > 0 {
for _, gatewayName := range gatewayNames {
if gatewayName == gateway {
return true
}
}
}
return false
}
func buildGatewayVirtualHostDomains(hostname string, port int) []string {
domains := []string{hostname}
if features.StripHostPort || hostname == "*" {
return domains
}
// Per https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-virtualhost
// we can only have one wildcard. Ideally, we want to match any port, as the host
// header may have a different port (behind a LB, nodeport, etc). However, if we
// have a wildcard domain we cannot do that since we would need two wildcards.
// Therefore, we we will preserve the original port if there is a wildcard host.
// TODO(https://github.com/envoyproxy/envoy/issues/12647) support wildcard host with wildcard port.
if len(hostname) > 0 && hostname[0] == '*' {
domains = append(domains, util.DomainName(hostname, port))
} else {
domains = append(domains, util.IPv6Compliant(hostname)+":*")
}
return domains
}
// Invalid cipher suites lead Envoy to NACKing. This filters the list down to just the supported set.
func filteredGatewayCipherSuites(server *networking.Server) []string {
suites := server.Tls.CipherSuites
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 for server %s", s, server.String())
}
} else if log.DebugEnabled() {
log.Debugf("ignoring unsupported cipherSuite: %q for server %s", s, server.String())
}
}
return ret
}