blob: 5c8870406dec74783c59818efcda8ef3011f4ed7 [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 model
import (
"fmt"
"sort"
"strconv"
"strings"
)
import (
networking "istio.io/api/networking/v1alpha3"
"istio.io/pkg/monitoring"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/features"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model/credentials"
"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/protocol"
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/gvk"
"github.com/apache/dubbo-go-pixiu/pkg/util/sets"
)
// ServerPort defines port for the gateway server.
type ServerPort struct {
// A valid non-negative integer port number.
Number uint32
// The protocol exposed on the port.
Protocol string
// The bind server specified on this port.
Bind string
}
// MergedServers describes set of servers defined in all gateways per port.
type MergedServers struct {
Servers []*networking.Server
RouteName string // RouteName for http servers. For HTTPS, TLSServerInfo will hold the route name.
}
// TLSServerInfo contains additional information for TLS Servers.
type TLSServerInfo struct {
RouteName string
SNIHosts []string
}
// MergedGateway describes a set of gateways for a workload merged into a single logical gateway.
type MergedGateway struct {
// ServerPorts maintains a list of unique server ports, used for stable ordering.
ServerPorts []ServerPort
// MergedServers map from physical port to virtual servers
// using TCP protocols (like HTTP1.1, H2, mysql, redis etc)
MergedServers map[ServerPort]*MergedServers
// MergedQUICTransportServers map from physical port to servers listening
// on QUIC (like HTTP3). Currently the support is experimental and
// is limited to HTTP3 only
MergedQUICTransportServers map[ServerPort]*MergedServers
// HTTP3AdvertisingRoutes represents the set of HTTP routes which advertise HTTP/3.
// This mapping is used to generate alt-svc header that is needed for HTTP/3 server discovery.
HTTP3AdvertisingRoutes map[string]struct{}
// GatewayNameForServer maps from server to the owning gateway name.
// Used for select the set of virtual services that apply to a port.
GatewayNameForServer map[*networking.Server]string
// ServersByRouteName maps from port names to virtual hosts
// Used for RDS. No two port names share same port except for HTTPS
// The typical length of the value is always 1, except for HTTP (not HTTPS),
ServersByRouteName map[string][]*networking.Server
// TLSServerInfo maps from server to a corresponding TLS information like TLS Routename and SNIHosts.
TLSServerInfo map[*networking.Server]*TLSServerInfo
// ContainsAutoPassthroughGateways determines if there are any type AUTO_PASSTHROUGH Gateways, requiring additional
// clusters to be sent to the workload
ContainsAutoPassthroughGateways bool
// PortMap defines a mapping of targetPorts to the set of Service ports that reference them
PortMap GatewayPortMap
// VerifiedCertificateReferences contains a set of all credentialNames referenced by gateways *in the same namespace as the proxy*.
// These are considered "verified", since there is mutually agreement from the pod, Secret, and Gateway, as all
// reside in the same namespace and trust boundary.
// Note: Secrets that are not referenced by any Gateway, but are in the same namespace as the pod, are explicitly *not*
// included. This ensures we don't give permission to unexpected secrets, such as the citadel root key/cert.
VerifiedCertificateReferences sets.Set
}
var (
typeTag = monitoring.MustCreateLabel("type")
nameTag = monitoring.MustCreateLabel("name")
totalRejectedConfigs = monitoring.NewSum(
"pilot_total_rejected_configs",
"Total number of configs that Pilot had to reject or ignore.",
monitoring.WithLabels(typeTag, nameTag),
)
)
func init() {
monitoring.MustRegister(totalRejectedConfigs)
}
func RecordRejectedConfig(gatewayName string) {
totalRejectedConfigs.With(typeTag.Value("gateway"), nameTag.Value(gatewayName)).Increment()
}
// DisableGatewayPortTranslationLabel is a label on Service that declares that, for that particular
// service, we should not translate Gateway ports to target ports. For example, if I have a Service
// on port 80 with target port 8080, with the label. Gateways on port 80 would *not* match. Instead,
// only Gateways on port 8080 would be used. This prevents ambiguities when there are multiple
// Services on port 80 referring to different target ports. Long term, this will be replaced by
// Gateways directly referencing a Service, rather than label selectors. Warning: this label is
// intended solely for as a workaround for Knative's Istio integration, and not intended for any
// other usage. It can, and will, be removed immediately after the new direct reference is ready for
// use.
const DisableGatewayPortTranslationLabel = "experimental.istio.io/disable-gateway-port-translation"
// MergeGateways combines multiple gateways targeting the same workload into a single logical Gateway.
// Note that today any Servers in the combined gateways listening on the same port must have the same protocol.
// If servers with different protocols attempt to listen on the same port, one of the protocols will be chosen at random.
func MergeGateways(gateways []gatewayWithInstances, proxy *Proxy, ps *PushContext) *MergedGateway {
gatewayPorts := make(map[uint32]bool)
mergedServers := make(map[ServerPort]*MergedServers)
mergedQUICServers := make(map[ServerPort]*MergedServers)
serverPorts := make([]ServerPort, 0)
plainTextServers := make(map[uint32]ServerPort)
serversByRouteName := make(map[string][]*networking.Server)
tlsServerInfo := make(map[*networking.Server]*TLSServerInfo)
gatewayNameForServer := make(map[*networking.Server]string)
verifiedCertificateReferences := sets.New()
http3AdvertisingRoutes := sets.New()
tlsHostsByPort := map[uint32]sets.Set{} // port -> host set
autoPassthrough := false
log.Debugf("MergeGateways: merging %d gateways", len(gateways))
for _, gwAndInstance := range gateways {
gatewayConfig := gwAndInstance.gateway
gatewayName := gatewayConfig.Namespace + "/" + gatewayConfig.Name // Format: %s/%s
gatewayCfg := gatewayConfig.Spec.(*networking.Gateway)
log.Debugf("MergeGateways: merging gateway %q :\n%v", gatewayName, gatewayCfg)
snames := sets.Set{}
for _, s := range gatewayCfg.Servers {
if len(s.Name) > 0 {
if snames.Contains(s.Name) {
log.Warnf("Server name %s is not unique in gateway %s and may create possible issues like stat prefix collision ",
s.Name, gatewayName)
} else {
snames.Insert(s.Name)
}
}
if s.Port == nil {
// Should be rejected in validation, this is an extra check
log.Debugf("invalid server without port: %q", gatewayName)
RecordRejectedConfig(gatewayName)
continue
}
sanitizeServerHostNamespace(s, gatewayConfig.Namespace)
gatewayNameForServer[s] = gatewayName
log.Debugf("MergeGateways: gateway %q processing server %s :%v", gatewayName, s.Name, s.Hosts)
cn := s.GetTls().GetCredentialName()
if cn != "" && proxy.VerifiedIdentity != nil {
rn := credentials.ToResourceName(cn)
parse, _ := credentials.ParseResourceName(rn, proxy.VerifiedIdentity.Namespace, "", "")
if gatewayConfig.Namespace == proxy.VerifiedIdentity.Namespace && parse.Namespace == proxy.VerifiedIdentity.Namespace {
// Same namespace is always allowed
verifiedCertificateReferences.Insert(rn)
} else if ps.ReferenceAllowed(gvk.Secret, rn, proxy.VerifiedIdentity.Namespace) {
// Explicitly allowed by some policy
verifiedCertificateReferences.Insert(rn)
}
}
for _, resolvedPort := range resolvePorts(s.Port.Number, gwAndInstance.instances, gwAndInstance.legacyGatewaySelector) {
routeName := gatewayRDSRouteName(s, resolvedPort, gatewayConfig)
if s.Tls != nil {
// 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 hosts, which will become
// SNI filter chain matches.
if tlsHostsByPort[resolvedPort] == nil {
tlsHostsByPort[resolvedPort] = sets.New()
}
if duplicateHosts := CheckDuplicates(s.Hosts, tlsHostsByPort[resolvedPort]); len(duplicateHosts) != 0 {
log.Debugf("skipping server on gateway %s, duplicate host names: %v", gatewayName, duplicateHosts)
RecordRejectedConfig(gatewayName)
continue
}
tlsServerInfo[s] = &TLSServerInfo{SNIHosts: GetSNIHostsForServer(s), RouteName: routeName}
if s.Tls.Mode == networking.ServerTLSSettings_AUTO_PASSTHROUGH {
autoPassthrough = true
}
}
serverPort := ServerPort{resolvedPort, s.Port.Protocol, s.Bind}
serverProtocol := protocol.Parse(serverPort.Protocol)
if gatewayPorts[resolvedPort] {
// We have two servers on the same port. Should we merge?
// 1. Yes if both servers are plain text and HTTP
// 2. Yes if both servers are using TLS
// if using HTTPS ensure that port name is distinct so that we can setup separate RDS
// for each server (as each server ends up as a separate http connection manager due to filter chain match)
// 3. No for everything else.
if current, exists := plainTextServers[resolvedPort]; exists {
if !canMergeProtocols(serverProtocol, protocol.Parse(current.Protocol)) {
log.Infof("skipping server on gateway %s port %s.%d.%s: conflict with existing server %d.%s",
gatewayConfig.Name, s.Port.Name, resolvedPort, s.Port.Protocol, serverPort.Number, serverPort.Protocol)
RecordRejectedConfig(gatewayName)
continue
}
if routeName == "" {
log.Debugf("skipping server on gateway %s port %s.%d.%s: could not build RDS name from server",
gatewayConfig.Name, s.Port.Name, resolvedPort, s.Port.Protocol)
RecordRejectedConfig(gatewayName)
continue
}
if current.Bind != serverPort.Bind {
// Merge it to servers with the same port and bind.
if mergedServers[serverPort] == nil {
mergedServers[serverPort] = &MergedServers{Servers: []*networking.Server{}}
serverPorts = append(serverPorts, serverPort)
}
ms := mergedServers[serverPort]
ms.RouteName = routeName
ms.Servers = append(ms.Servers, s)
} else {
// Merge this to current known port with same bind.
ms := mergedServers[current]
ms.Servers = append(ms.Servers, s)
}
serversByRouteName[routeName] = append(serversByRouteName[routeName], s)
} else {
// We have duplicate port. Its not in plaintext servers. So, this has to be a TLS server.
// Check if this is also a HTTP server and if so, ensure uniqueness of port name.
if gateway.IsHTTPServer(s) {
if routeName == "" {
log.Debugf("skipping server on gateway %s port %s.%d.%s: could not build RDS name from server",
gatewayConfig.Name, s.Port.Name, resolvedPort, s.Port.Protocol)
RecordRejectedConfig(gatewayName)
continue
}
// Both servers are HTTPS servers. Make sure the port names are different so that RDS can pick out individual servers.
// We cannot have two servers with same port name because we need the port name to distinguish one HTTPS server from another.
// We cannot merge two HTTPS servers even if their TLS settings have same path to the keys, because we don't know if the contents
// of the keys are same. So we treat them as effectively different TLS settings.
// This check is largely redundant now since we create rds names for https using gateway name, namespace
// and validation ensures that all port names within a single gateway config are unique.
if _, exists := serversByRouteName[routeName]; exists {
log.Infof("skipping server on gateway %s port %s.%d.%s: non unique port name for HTTPS port",
gatewayConfig.Name, s.Port.Name, resolvedPort, s.Port.Protocol)
RecordRejectedConfig(gatewayName)
continue
}
serversByRouteName[routeName] = []*networking.Server{s}
}
// We have another TLS server on the same port. Can differentiate servers using SNI
if s.Tls == nil {
log.Warnf("TLS server without TLS options %s %s", gatewayName, s.String())
continue
}
if mergedServers[serverPort] == nil {
mergedServers[serverPort] = &MergedServers{Servers: []*networking.Server{s}}
serverPorts = append(serverPorts, serverPort)
} else {
mergedServers[serverPort].Servers = append(mergedServers[serverPort].Servers, s)
}
// We have TLS settings defined and we have already taken care of unique route names
// if it is HTTPS. So we can construct a QUIC server on the same port. It is okay as
// QUIC listens on UDP port, not TCP
if features.EnableQUICListeners && gateway.IsEligibleForHTTP3Upgrade(s) &&
udpSupportedPort(s.GetPort().GetNumber(), gwAndInstance.instances) {
log.Debugf("Server at port %d eligible for HTTP3 upgrade. Add UDP listener for QUIC", serverPort.Number)
if mergedQUICServers[serverPort] == nil {
mergedQUICServers[serverPort] = &MergedServers{Servers: []*networking.Server{}}
}
mergedQUICServers[serverPort].Servers = append(mergedQUICServers[serverPort].Servers, s)
http3AdvertisingRoutes[routeName] = struct{}{}
}
}
} else {
// This is a new gateway on this port. Create MergedServers for it.
gatewayPorts[resolvedPort] = true
if !gateway.IsTLSServer(s) {
plainTextServers[serverPort.Number] = serverPort
}
if gateway.IsHTTPServer(s) {
serversByRouteName[routeName] = []*networking.Server{s}
if features.EnableQUICListeners && gateway.IsEligibleForHTTP3Upgrade(s) &&
udpSupportedPort(s.GetPort().GetNumber(), gwAndInstance.instances) {
log.Debugf("Server at port %d eligible for HTTP3 upgrade. So QUIC listener will be added", serverPort.Number)
http3AdvertisingRoutes[routeName] = struct{}{}
if mergedQUICServers[serverPort] == nil {
// This should be treated like non-passthrough HTTPS case. There will be multiple filter
// chains, multiple routes per server port. So just like in TLS server case we do not
// track route name here. Instead, TLS server info is used (it is fine for now because
// this would be a mirror of an existing non-passthrough HTTPS server)
mergedQUICServers[serverPort] = &MergedServers{Servers: []*networking.Server{s}}
}
}
}
mergedServers[serverPort] = &MergedServers{Servers: []*networking.Server{s}, RouteName: routeName}
serverPorts = append(serverPorts, serverPort)
}
log.Debugf("MergeGateways: gateway %q merged server %v", gatewayName, s.Hosts)
}
}
}
return &MergedGateway{
MergedServers: mergedServers,
MergedQUICTransportServers: mergedQUICServers,
ServerPorts: serverPorts,
GatewayNameForServer: gatewayNameForServer,
TLSServerInfo: tlsServerInfo,
ServersByRouteName: serversByRouteName,
HTTP3AdvertisingRoutes: http3AdvertisingRoutes,
ContainsAutoPassthroughGateways: autoPassthrough,
PortMap: getTargetPortMap(serversByRouteName),
VerifiedCertificateReferences: verifiedCertificateReferences,
}
}
func udpSupportedPort(number uint32, instances []*ServiceInstance) bool {
for _, w := range instances {
if int(number) == w.ServicePort.Port && w.ServicePort.Protocol == protocol.UDP {
return true
}
}
return false
}
// resolvePorts takes a Gateway port, and resolves it to the port that will actually be listened on.
// When legacyGatewaySelector=false, then the gateway is directly referencing a Service. In this
// case, the translation is un-ambiguous - we just find the matching port and return the targetPort
// When legacyGatewaySelector=true things are a bit more complex, as we support referencing a Service
// port and translating to the targetPort in addition to just directly referencing a port. In this
// case, we just make a best effort guess by picking the first match.
func resolvePorts(number uint32, instances []*ServiceInstance, legacyGatewaySelector bool) []uint32 {
ports := map[uint32]struct{}{}
for _, w := range instances {
if _, disablePortTranslation := w.Service.Attributes.Labels[DisableGatewayPortTranslationLabel]; disablePortTranslation && legacyGatewaySelector {
// Skip this Service, they opted out of port translation
// This is only done for legacyGatewaySelector, as the new gateway selection mechanism *only* allows
// referencing the Service port, and references are un-ambiguous.
continue
}
if w.ServicePort.Port == int(number) && w.Endpoint != nil {
if legacyGatewaySelector {
// When we are using legacy gateway label selection, we only resolve to a single port
// This has pros and cons; we don't allow merging of routes when it would be desirable, but
// we also avoid accidentally merging routes when we didn't intend to. While neither option is great,
// picking the first one here preserves backwards compatibility.
return []uint32{w.Endpoint.EndpointPort}
}
ports[w.Endpoint.EndpointPort] = struct{}{}
}
}
ret := make([]uint32, 0, len(ports))
for p := range ports {
ret = append(ret, p)
}
if len(ret) == 0 && legacyGatewaySelector {
// When we are using legacy gateway label selection, we should bind to the port as-is if there is
// no matching ServiceInstance.
return []uint32{number}
}
// For cases where we are directly referencing a Service, we know that they port *must* be in the Service,
// so we have no fallback. If there was no match, the Gateway is a no-op.
return ret
}
func canMergeProtocols(current protocol.Instance, p protocol.Instance) bool {
return (current.IsHTTP() || current == p) && p.IsHTTP()
}
func GetSNIHostsForServer(server *networking.Server) []string {
if server.Tls == nil {
return nil
}
// sanitize the server hosts as it could contain hosts of form ns/host
sniHosts := make(map[string]bool)
for _, h := range server.Hosts {
if strings.Contains(h, "/") {
parts := strings.Split(h, "/")
h = parts[1]
}
// do not add hosts, that have already been added
if !sniHosts[h] {
sniHosts[h] = true
}
}
sniHostsSlice := make([]string, 0, len(sniHosts))
for host := range sniHosts {
sniHostsSlice = append(sniHostsSlice, host)
}
sort.Strings(sniHostsSlice)
return sniHostsSlice
}
// CheckDuplicates returns all of the hosts provided that are already known
// If there were no duplicates, all hosts are added to the known hosts.
func CheckDuplicates(hosts []string, knownHosts sets.Set) []string {
var duplicates []string
for _, h := range hosts {
if knownHosts.Contains(h) {
duplicates = append(duplicates, h)
}
}
// No duplicates found, so we can mark all of these hosts as known
if len(duplicates) == 0 {
for _, h := range hosts {
knownHosts.Insert(h)
}
}
return duplicates
}
// gatewayRDSRouteName generates the RDS route config name for gateway's servers.
// Unlike sidecars where the RDS route name is the listener port number, gateways have a different
// structure for RDS.
// HTTP servers have route name set to http.<portNumber>.
//
// Multiple HTTP servers can exist on the same port and the code will combine all of them into
// one single RDS payload for http.<portNumber>
//
// HTTPS servers with TLS termination (i.e. envoy decoding the content, and making outbound http calls to backends)
// will use route name https.<portNumber>.<portName>.<gatewayName>.<namespace>. HTTPS servers using SNI passthrough or
// non-HTTPS servers (e.g., TCP+TLS) with SNI passthrough will be setup as opaque TCP proxies without terminating
// the SSL connection. They would inspect the SNI header and forward to the appropriate upstream as opaque TCP.
//
// Within HTTPS servers terminating TLS, user could setup multiple servers in the gateway. each server could have
// one or more hosts but have different TLS certificates. In this case, we end up having separate filter chain
// for each server, with the filter chain match matching on the server specific TLS certs and SNI headers.
// We have two options here: either have all filter chains use the same RDS route name (e.g. "443") and expose
// all virtual hosts on that port to every filter chain uniformly or expose only the set of virtual hosts
// configured under the server for those certificates. We adopt the latter approach. In other words, each
// filter chain in the multi-filter-chain listener will have a distinct RDS route name
// (https.<portNumber>.<portName>.<gatewayName>.<namespace>) so that when a RDS request comes in, we serve the virtual
// hosts and associated routes for that server.
//
// Note that the common case is one where multiple servers are exposed under a single multi-SAN cert on a single port.
// In this case, we have a single https.<portNumber>.<portName>.<gatewayName>.<namespace> RDS for the HTTPS server.
// While we can use the same RDS route name for two servers (say HTTP and HTTPS) exposing the same set of hosts on
// different ports, the optimization (one RDS instead of two) could quickly become useless the moment the set of
// hosts on the two servers start differing -- necessitating the need for two different RDS routes.
func gatewayRDSRouteName(server *networking.Server, portNumber uint32, cfg config.Config) string {
p := protocol.Parse(server.Port.Protocol)
bind := ""
if server.Bind != "" {
bind = "." + server.Bind
}
if p.IsHTTP() {
return "http" + "." + strconv.Itoa(int(portNumber)) + bind // Format: http.%d.%s
}
if p == protocol.HTTPS && server.Tls != nil && !gateway.IsPassThroughServer(server) {
return "https" + "." + strconv.Itoa(int(server.Port.Number)) + "." +
server.Port.Name + "." + cfg.Name + "." + cfg.Namespace + bind // Format: https.%d.%s.%s.%s.%s
}
return ""
}
// ParseGatewayRDSRouteName is used by the EnvoyFilter patching logic to match
// a specific route configuration to patch.
func ParseGatewayRDSRouteName(name string) (portNumber int, portName, gatewayName string) {
parts := strings.Split(name, ".")
if strings.HasPrefix(name, "http.") {
// this is a http gateway. Parse port number and return empty string for rest
if len(parts) >= 2 {
portNumber, _ = strconv.Atoi(parts[1])
}
} else if strings.HasPrefix(name, "https.") {
if len(parts) >= 5 {
portNumber, _ = strconv.Atoi(parts[1])
portName = parts[2]
// gateway name should be ns/name
gatewayName = parts[4] + "/" + parts[3]
}
}
return
}
// convert ./host to currentNamespace/Host
// */host to just host
// */* to just *
func sanitizeServerHostNamespace(server *networking.Server, namespace string) {
for i, h := range server.Hosts {
if strings.Contains(h, "/") {
parts := strings.Split(h, "/")
if parts[0] == "." {
server.Hosts[i] = fmt.Sprintf("%s/%s", namespace, parts[1])
} else if parts[0] == "*" {
if parts[1] == "*" {
server.Hosts = []string{"*"}
return
}
server.Hosts[i] = parts[1]
}
}
}
}
type GatewayPortMap map[int]map[int]struct{}
func getTargetPortMap(serversByRouteName map[string][]*networking.Server) GatewayPortMap {
pm := GatewayPortMap{}
for r, s := range serversByRouteName {
portNumber, _, _ := ParseGatewayRDSRouteName(r)
if _, f := pm[portNumber]; !f {
pm[portNumber] = map[int]struct{}{}
}
for _, se := range s {
if se.Port == nil {
continue
}
pm[portNumber][int(se.Port.Number)] = struct{}{}
}
}
return pm
}