pomerium/internal/controlplane/xds_clusters.go
Caleb Doxsey a4c7381eba
config: support multiple destination addresses (#1789)
* config: support multiple destination addresses

* use constructor for string slice

* add docs

* add test for multiple destinations

* fix name
2021-01-20 15:18:24 -07:00

346 lines
11 KiB
Go

package controlplane
import (
"encoding/base64"
"net"
"net/url"
"strings"
"time"
envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_config_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
envoy_extensions_transport_sockets_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
"github.com/golang/protobuf/ptypes"
"github.com/martinlindhe/base36"
"google.golang.org/protobuf/types/known/structpb"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/urlutil"
"github.com/pomerium/pomerium/pkg/cryptutil"
)
// An Endpoint is a URL with its corresponding Transport Socket.
type Endpoint struct {
url *url.URL
transportSocket *envoy_config_core_v3.TransportSocket
}
// NewEndpoint creates a new Endpoint.
func NewEndpoint(u *url.URL, ts *envoy_config_core_v3.TransportSocket) Endpoint {
return Endpoint{url: u, transportSocket: ts}
}
// TransportSocketName return the name for this endpoint.
func (e Endpoint) TransportSocketName() string {
if e.transportSocket == nil {
return ""
}
h := cryptutil.HashProto(e.transportSocket)
return "ts-" + base36.EncodeBytes(h)
}
func (srv *Server) buildClusters(options *config.Options) []*envoy_config_cluster_v3.Cluster {
grpcURL := &url.URL{
Scheme: "http",
Host: srv.GRPCListener.Addr().String(),
}
httpURL := &url.URL{
Scheme: "http",
Host: srv.HTTPListener.Addr().String(),
}
authzURL := &url.URL{
Scheme: options.GetAuthorizeURL().Scheme,
Host: options.GetAuthorizeURL().Host,
}
clusters := []*envoy_config_cluster_v3.Cluster{
srv.buildInternalCluster(options, "pomerium-control-plane-grpc", grpcURL, true),
srv.buildInternalCluster(options, "pomerium-control-plane-http", httpURL, false),
}
clusters = append(clusters, srv.buildInternalCluster(options, authzURL.Host, authzURL, true))
if config.IsProxy(options.Services) {
for i := range options.Policies {
policy := options.Policies[i]
if len(policy.Destinations) > 0 {
clusters = append(clusters, srv.buildPolicyCluster(options, &policy))
}
}
}
return clusters
}
func (srv *Server) buildInternalCluster(options *config.Options, name string, dst *url.URL, forceHTTP2 bool) *envoy_config_cluster_v3.Cluster {
endpoints := []Endpoint{NewEndpoint(dst, srv.buildInternalTransportSocket(options, dst))}
dnsLookupFamily := config.GetEnvoyDNSLookupFamily(options.DNSLookupFamily)
return buildCluster(name, endpoints, forceHTTP2, dnsLookupFamily, nil)
}
func (srv *Server) buildPolicyCluster(options *config.Options, policy *config.Policy) *envoy_config_cluster_v3.Cluster {
name := getPolicyName(policy)
endpoints := srv.buildPolicyEndpoints(policy)
dnsLookupFamily := config.GetEnvoyDNSLookupFamily(options.DNSLookupFamily)
if policy.EnableGoogleCloudServerlessAuthentication {
dnsLookupFamily = envoy_config_cluster_v3.Cluster_V4_ONLY
}
outlierDetection := (*envoy_config_cluster_v3.OutlierDetection)(policy.OutlierDetection)
return buildCluster(name, endpoints, false, dnsLookupFamily, outlierDetection)
}
func (srv *Server) buildPolicyEndpoints(policy *config.Policy) []Endpoint {
var endpoints []Endpoint
for _, dst := range policy.Destinations {
endpoints = append(endpoints, NewEndpoint(dst, srv.buildPolicyTransportSocket(policy, dst)))
}
return endpoints
}
func (srv *Server) buildInternalTransportSocket(options *config.Options, endpoint *url.URL) *envoy_config_core_v3.TransportSocket {
if endpoint.Scheme != "https" {
return nil
}
sni := endpoint.Hostname()
if options.OverrideCertificateName != "" {
sni = options.OverrideCertificateName
}
validationContext := &envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext{
MatchSubjectAltNames: []*envoy_type_matcher_v3.StringMatcher{{
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
Exact: sni,
},
}},
}
if options.CAFile != "" {
validationContext.TrustedCa = srv.filemgr.FileDataSource(options.CAFile)
} else if options.CA != "" {
bs, err := base64.StdEncoding.DecodeString(options.CA)
if err != nil {
log.Error().Err(err).Msg("invalid custom CA certificate")
}
validationContext.TrustedCa = srv.filemgr.BytesDataSource("custom-ca.pem", bs)
} else {
rootCA, err := getRootCertificateAuthority()
if err != nil {
log.Error().Err(err).Msg("unable to enable certificate verification because no root CAs were found")
} else {
validationContext.TrustedCa = srv.filemgr.FileDataSource(rootCA)
}
}
tlsContext := &envoy_extensions_transport_sockets_tls_v3.UpstreamTlsContext{
CommonTlsContext: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext{
AlpnProtocols: []string{"h2", "http/1.1"},
ValidationContextType: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext_ValidationContext{
ValidationContext: validationContext,
},
},
Sni: sni,
}
tlsConfig := marshalAny(tlsContext)
return &envoy_config_core_v3.TransportSocket{
Name: "tls",
ConfigType: &envoy_config_core_v3.TransportSocket_TypedConfig{
TypedConfig: tlsConfig,
},
}
}
func (srv *Server) buildPolicyTransportSocket(policy *config.Policy, dst *url.URL) *envoy_config_core_v3.TransportSocket {
if dst == nil || dst.Scheme != "https" {
return nil
}
sni := dst.Hostname()
if policy.TLSServerName != "" {
sni = policy.TLSServerName
}
tlsContext := &envoy_extensions_transport_sockets_tls_v3.UpstreamTlsContext{
CommonTlsContext: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext{
TlsParams: &envoy_extensions_transport_sockets_tls_v3.TlsParameters{
EcdhCurves: []string{
"X25519",
"P-256",
"P-384",
"P-521",
},
},
AlpnProtocols: []string{"http/1.1"},
ValidationContextType: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext_ValidationContext{
ValidationContext: srv.buildPolicyValidationContext(policy, dst),
},
},
Sni: sni,
}
if policy.ClientCertificate != nil {
tlsContext.CommonTlsContext.TlsCertificates = append(tlsContext.CommonTlsContext.TlsCertificates,
srv.envoyTLSCertificateFromGoTLSCertificate(policy.ClientCertificate))
}
tlsConfig := marshalAny(tlsContext)
return &envoy_config_core_v3.TransportSocket{
Name: "tls",
ConfigType: &envoy_config_core_v3.TransportSocket_TypedConfig{
TypedConfig: tlsConfig,
},
}
}
func (srv *Server) buildPolicyValidationContext(policy *config.Policy, dst *url.URL) *envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext {
if dst == nil {
return nil
}
sni := dst.Hostname()
if policy.TLSServerName != "" {
sni = policy.TLSServerName
}
validationContext := &envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext{
MatchSubjectAltNames: []*envoy_type_matcher_v3.StringMatcher{{
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
Exact: sni,
},
}},
}
if policy.TLSCustomCAFile != "" {
validationContext.TrustedCa = srv.filemgr.FileDataSource(policy.TLSCustomCAFile)
} else if policy.TLSCustomCA != "" {
bs, err := base64.StdEncoding.DecodeString(policy.TLSCustomCA)
if err != nil {
log.Error().Err(err).Msg("invalid custom CA certificate")
}
validationContext.TrustedCa = srv.filemgr.BytesDataSource("custom-ca.pem", bs)
} else {
rootCA, err := getRootCertificateAuthority()
if err != nil {
log.Error().Err(err).Msg("unable to enable certificate verification because no root CAs were found")
} else {
validationContext.TrustedCa = srv.filemgr.FileDataSource(rootCA)
}
}
if policy.TLSSkipVerify {
validationContext.TrustChainVerification = envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext_ACCEPT_UNTRUSTED
}
return validationContext
}
func buildCluster(
name string,
endpoints []Endpoint,
forceHTTP2 bool,
dnsLookupFamily envoy_config_cluster_v3.Cluster_DnsLookupFamily,
outlierDetection *envoy_config_cluster_v3.OutlierDetection,
) *envoy_config_cluster_v3.Cluster {
if len(endpoints) == 0 {
return nil
}
lbEndpoints := buildLbEndpoints(endpoints)
cluster := &envoy_config_cluster_v3.Cluster{
Name: name,
ConnectTimeout: ptypes.DurationProto(time.Second * 10),
LoadAssignment: &envoy_config_endpoint_v3.ClusterLoadAssignment{
ClusterName: name,
Endpoints: []*envoy_config_endpoint_v3.LocalityLbEndpoints{{
LbEndpoints: lbEndpoints,
}},
},
RespectDnsTtl: true,
TransportSocketMatches: buildTransportSocketMatches(endpoints),
DnsLookupFamily: dnsLookupFamily,
OutlierDetection: outlierDetection,
}
if forceHTTP2 {
cluster.Http2ProtocolOptions = &envoy_config_core_v3.Http2ProtocolOptions{
AllowConnect: true,
}
}
// for IPs we use a static discovery type, otherwise we use DNS
isIP := false
for _, lbe := range lbEndpoints {
if net.ParseIP(urlutil.StripPort(lbe.GetEndpoint().GetAddress().GetSocketAddress().GetAddress())) != nil {
isIP = true
}
}
if isIP {
cluster.ClusterDiscoveryType = &envoy_config_cluster_v3.Cluster_Type{Type: envoy_config_cluster_v3.Cluster_STATIC}
} else {
cluster.ClusterDiscoveryType = &envoy_config_cluster_v3.Cluster_Type{Type: envoy_config_cluster_v3.Cluster_STRICT_DNS}
}
return cluster
}
func buildLbEndpoints(endpoints []Endpoint) []*envoy_config_endpoint_v3.LbEndpoint {
var lbes []*envoy_config_endpoint_v3.LbEndpoint
for _, e := range endpoints {
defaultPort := 80
if e.transportSocket != nil && e.transportSocket.Name == "tls" {
defaultPort = 443
}
u := e.url
if e.url.Hostname() == "localhost" {
u = new(url.URL)
*u = *e.url
u.Host = strings.Replace(e.url.Host, "localhost", "127.0.0.1", -1)
}
lbe := &envoy_config_endpoint_v3.LbEndpoint{
HostIdentifier: &envoy_config_endpoint_v3.LbEndpoint_Endpoint{
Endpoint: &envoy_config_endpoint_v3.Endpoint{
Address: buildAddress(u.Host, defaultPort),
},
},
}
if e.transportSocket != nil {
lbe.Metadata = &envoy_config_core_v3.Metadata{
FilterMetadata: map[string]*structpb.Struct{
"envoy.transport_socket_match": {
Fields: map[string]*structpb.Value{
e.TransportSocketName(): structpb.NewBoolValue(true),
},
},
},
}
}
lbes = append(lbes, lbe)
}
return lbes
}
func buildTransportSocketMatches(endpoints []Endpoint) []*envoy_config_cluster_v3.Cluster_TransportSocketMatch {
var tsms []*envoy_config_cluster_v3.Cluster_TransportSocketMatch
seen := map[string]struct{}{}
for _, e := range endpoints {
if e.transportSocket == nil {
continue
}
// only add unique transport sockets
nm := e.TransportSocketName()
if _, ok := seen[nm]; ok {
continue
}
seen[nm] = struct{}{}
tsms = append(tsms, &envoy_config_cluster_v3.Cluster_TransportSocketMatch{
Name: nm,
Match: &structpb.Struct{
Fields: map[string]*structpb.Value{
nm: structpb.NewBoolValue(true),
},
},
TransportSocket: e.transportSocket,
})
}
return tsms
}