mirror of https://github.com/hashicorp/consul
570 lines
17 KiB
Go
570 lines
17 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package extensioncommon
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
|
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
|
envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
|
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
|
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
|
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
|
envoy_tcp_proxy_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3"
|
|
envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
|
envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
|
|
"github.com/hashicorp/consul/api"
|
|
"github.com/hashicorp/consul/envoyextensions/xdscommon"
|
|
)
|
|
|
|
// MakeUpstreamTLSTransportSocket generates an Envoy transport socket for the given TLS context.
|
|
func MakeUpstreamTLSTransportSocket(tlsContext *envoy_tls_v3.UpstreamTlsContext) (*envoy_core_v3.TransportSocket, error) {
|
|
if tlsContext == nil {
|
|
return nil, nil
|
|
}
|
|
return MakeTransportSocket("tls", tlsContext)
|
|
}
|
|
|
|
// MakeTransportSocket generates an Envoy transport socket from the given proto message.
|
|
func MakeTransportSocket(name string, config proto.Message) (*envoy_core_v3.TransportSocket, error) {
|
|
any, err := anypb.New(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &envoy_core_v3.TransportSocket{
|
|
Name: name,
|
|
ConfigType: &envoy_core_v3.TransportSocket_TypedConfig{
|
|
TypedConfig: any,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// MakeEnvoyHTTPFilter generates an Envoy HTTP filter from the given proto message.
|
|
func MakeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFilter, error) {
|
|
any, err := anypb.New(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &envoy_http_v3.HttpFilter{
|
|
Name: name,
|
|
ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{TypedConfig: any},
|
|
}, nil
|
|
}
|
|
|
|
// MakeFilter generates an Envoy listener filter from the given proto message.
|
|
func MakeFilter(name string, cfg proto.Message) (*envoy_listener_v3.Filter, error) {
|
|
any, err := anypb.New(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &envoy_listener_v3.Filter{
|
|
Name: name,
|
|
ConfigType: &envoy_listener_v3.Filter_TypedConfig{TypedConfig: any},
|
|
}, nil
|
|
}
|
|
|
|
// TrafficDirection determines whether inbound or outbound Envoy resources will be patched.
|
|
type TrafficDirection string
|
|
|
|
const (
|
|
TrafficDirectionInbound TrafficDirection = "inbound"
|
|
TrafficDirectionOutbound TrafficDirection = "outbound"
|
|
)
|
|
|
|
var TrafficDirections = StringSet{string(TrafficDirectionInbound): {}, string(TrafficDirectionOutbound): {}}
|
|
|
|
type StringSet map[string]struct{}
|
|
|
|
func (c *StringSet) CheckRequired(v, fieldName string) error {
|
|
if _, ok := (*c)[v]; !ok {
|
|
if v == "" {
|
|
return fmt.Errorf("field %s is required", fieldName)
|
|
}
|
|
|
|
var keys []string
|
|
for k := range *c {
|
|
keys = append(keys, k)
|
|
}
|
|
|
|
return fmt.Errorf("invalid %s '%q'; supported values: %s",
|
|
fieldName, v, strings.Join(keys, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NormalizeEmptyToDefault(s string) string {
|
|
if s == "" {
|
|
return "default"
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func NormalizeServiceName(sn *api.CompoundServiceName) {
|
|
sn.Namespace = NormalizeEmptyToDefault(sn.Namespace)
|
|
sn.Partition = NormalizeEmptyToDefault(sn.Partition)
|
|
}
|
|
|
|
// Payload represents a single Envoy resource to be modified by extensions.
|
|
// It associates the RuntimeConfig of the local proxy, the TrafficDirection
|
|
// of the resource, and the CompoundServiceName and UpstreamData (if outbound)
|
|
// of a service the Envoy resource corresponds to.
|
|
type Payload[K proto.Message] struct {
|
|
RuntimeConfig *RuntimeConfig
|
|
ServiceName *api.CompoundServiceName
|
|
Upstream *UpstreamData
|
|
TrafficDirection TrafficDirection
|
|
Message K
|
|
}
|
|
|
|
func (p Payload[K]) IsInbound() bool {
|
|
return p.TrafficDirection == TrafficDirectionInbound
|
|
}
|
|
|
|
type ClusterPayload = Payload[*envoy_cluster_v3.Cluster]
|
|
type ClusterLoadAssignmentPayload = Payload[*envoy_endpoint_v3.ClusterLoadAssignment]
|
|
type ListenerPayload = Payload[*envoy_listener_v3.Listener]
|
|
type FilterPayload = Payload[*envoy_listener_v3.Filter]
|
|
type RoutePayload = Payload[*envoy_route_v3.RouteConfiguration]
|
|
|
|
func (cfg *RuntimeConfig) GetClusterPayload(c *envoy_cluster_v3.Cluster) ClusterPayload {
|
|
d := TrafficDirectionOutbound
|
|
var u *UpstreamData
|
|
var sn *api.CompoundServiceName
|
|
|
|
if IsLocalAppCluster(c) {
|
|
d = TrafficDirectionInbound
|
|
} else {
|
|
u, sn = cfg.findUpstreamBySNI(c.Name)
|
|
}
|
|
|
|
return ClusterPayload{
|
|
RuntimeConfig: cfg,
|
|
ServiceName: sn,
|
|
Upstream: u,
|
|
TrafficDirection: d,
|
|
Message: c,
|
|
}
|
|
}
|
|
|
|
func (c *RuntimeConfig) GetListenerPayload(l *envoy_listener_v3.Listener) ListenerPayload {
|
|
d := TrafficDirectionOutbound
|
|
var u *UpstreamData
|
|
var sn *api.CompoundServiceName
|
|
|
|
if IsInboundPublicListener(l) {
|
|
d = TrafficDirectionInbound
|
|
} else {
|
|
u, sn = c.findUpstreamByEnvoyID(GetListenerEnvoyID(l))
|
|
}
|
|
|
|
return ListenerPayload{
|
|
RuntimeConfig: c,
|
|
ServiceName: sn,
|
|
Upstream: u,
|
|
TrafficDirection: d,
|
|
Message: l,
|
|
}
|
|
}
|
|
|
|
func (c *RuntimeConfig) GetFilterPayload(f *envoy_listener_v3.Filter, l *envoy_listener_v3.Listener) FilterPayload {
|
|
d := TrafficDirectionOutbound
|
|
var u *UpstreamData
|
|
var sn *api.CompoundServiceName
|
|
|
|
if IsInboundPublicListener(l) {
|
|
d = TrafficDirectionInbound
|
|
} else {
|
|
u, sn = c.findUpstreamByEnvoyID(GetListenerEnvoyID(l))
|
|
}
|
|
|
|
return FilterPayload{
|
|
RuntimeConfig: c,
|
|
ServiceName: sn,
|
|
Upstream: u,
|
|
TrafficDirection: d,
|
|
Message: f,
|
|
}
|
|
}
|
|
|
|
func (c *RuntimeConfig) GetRoutePayload(r *envoy_route_v3.RouteConfiguration) RoutePayload {
|
|
d := TrafficDirectionOutbound
|
|
var u *UpstreamData
|
|
var sn *api.CompoundServiceName
|
|
|
|
if IsRouteToLocalAppCluster(r) {
|
|
d = TrafficDirectionInbound
|
|
} else {
|
|
u, sn = c.findUpstreamByEnvoyID(r.Name)
|
|
}
|
|
|
|
return RoutePayload{
|
|
RuntimeConfig: c,
|
|
ServiceName: sn,
|
|
Upstream: u,
|
|
TrafficDirection: d,
|
|
Message: r,
|
|
}
|
|
}
|
|
|
|
func (cfg *RuntimeConfig) GetClusterLoadAssignmentPayload(c *envoy_endpoint_v3.ClusterLoadAssignment) ClusterLoadAssignmentPayload {
|
|
d := TrafficDirectionOutbound
|
|
var u *UpstreamData
|
|
var sn *api.CompoundServiceName
|
|
|
|
if IsLocalAppClusterLoadAssignment(c) {
|
|
d = TrafficDirectionInbound
|
|
} else {
|
|
u, sn = cfg.findUpstreamBySNI(c.ClusterName)
|
|
|
|
}
|
|
|
|
return ClusterLoadAssignmentPayload{
|
|
RuntimeConfig: cfg,
|
|
ServiceName: sn,
|
|
Upstream: u,
|
|
TrafficDirection: d,
|
|
Message: c,
|
|
}
|
|
}
|
|
|
|
func (c *RuntimeConfig) findUpstreamByEnvoyID(envoyID string) (*UpstreamData, *api.CompoundServiceName) {
|
|
for sn, u := range c.Upstreams {
|
|
if u.EnvoyID == envoyID {
|
|
return u, &sn
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (c *RuntimeConfig) findUpstreamBySNI(sni string) (*UpstreamData, *api.CompoundServiceName) {
|
|
for sn, u := range c.Upstreams {
|
|
_, ok := u.SNIs[sni]
|
|
if ok {
|
|
return u, &sn
|
|
}
|
|
|
|
if strings.HasPrefix(sni, xdscommon.FailoverClusterNamePrefix) {
|
|
parts := strings.Split(sni, "~")
|
|
|
|
if len(parts) != 3 {
|
|
continue
|
|
}
|
|
|
|
id := parts[2]
|
|
_, ok := u.SNIs[id]
|
|
if ok {
|
|
return u, &sn
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// GetListenerEnvoyID returns the Envoy ID string parsed from the name of the given Listener. If none is found, it
|
|
// returns the empty string.
|
|
func GetListenerEnvoyID(l *envoy_listener_v3.Listener) string {
|
|
if id, _, found := strings.Cut(l.Name, ":"); found {
|
|
return id
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// IsLocalAppCluster returns true if the given Cluster represents the local Cluster, which receives inbound traffic to
|
|
// the local proxy.
|
|
func IsLocalAppCluster(c *envoy_cluster_v3.Cluster) bool {
|
|
return c.Name == xdscommon.LocalAppClusterName
|
|
}
|
|
|
|
// IsLocalAppClusterLoadAssignment returns true if the given ClusterLoadAssignment represents the local Cluster, which
|
|
// receives inbound traffic to the local proxy.
|
|
func IsLocalAppClusterLoadAssignment(a *envoy_endpoint_v3.ClusterLoadAssignment) bool {
|
|
return a.ClusterName == xdscommon.LocalAppClusterName
|
|
}
|
|
|
|
// IsRouteToLocalAppCluster takes a RouteConfiguration and returns true if all routes within it target the local
|
|
// Cluster. Note that because we currently target RouteConfiguration in PatchRoute, we have to check multiple individual
|
|
// Route resources.
|
|
func IsRouteToLocalAppCluster(r *envoy_route_v3.RouteConfiguration) bool {
|
|
clusterNames := RouteClusterNames(r)
|
|
_, match := clusterNames[xdscommon.LocalAppClusterName]
|
|
|
|
return match && len(clusterNames) == 1
|
|
}
|
|
|
|
// IsInboundPublicListener returns true if the given Listener represents the inbound public Listener for the local
|
|
// service.
|
|
func IsInboundPublicListener(l *envoy_listener_v3.Listener) bool {
|
|
return GetListenerEnvoyID(l) == xdscommon.PublicListenerName
|
|
}
|
|
|
|
// IsOutboundTProxyListener returns true if the given Listener represents the outbound TProxy Listener for the local
|
|
// service.
|
|
func IsOutboundTProxyListener(l *envoy_listener_v3.Listener) bool {
|
|
return GetListenerEnvoyID(l) == xdscommon.OutboundListenerName
|
|
}
|
|
|
|
func filterChainTProxyMatch(vip string, filterChain *envoy_listener_v3.FilterChain) bool {
|
|
for _, prefixRange := range filterChain.FilterChainMatch.PrefixRanges {
|
|
// Since we always set the address prefix as the full VIP (rather than a prefix), we can just check if they are
|
|
// equal to find the matching filter chain.
|
|
if vip == prefixRange.AddressPrefix {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func FilterClusterNames(filter *envoy_listener_v3.Filter) map[string]struct{} {
|
|
clusterNames := make(map[string]struct{})
|
|
if filter == nil {
|
|
return clusterNames
|
|
}
|
|
|
|
if config := envoy_resource_v3.GetHTTPConnectionManager(filter); config != nil {
|
|
// If it's using RDS, the cluster names will be in the route, rather than in the http filter's route config, so
|
|
// we don't return any cluster names in this case. They can be gathered from the route.
|
|
if config.GetRds() != nil {
|
|
return clusterNames
|
|
}
|
|
|
|
cfg := config.GetRouteConfig()
|
|
|
|
clusterNames = RouteClusterNames(cfg)
|
|
}
|
|
|
|
if config := GetTCPProxy(filter); config != nil {
|
|
clusterNames[config.GetCluster()] = struct{}{}
|
|
}
|
|
|
|
return clusterNames
|
|
}
|
|
|
|
func RouteClusterNames(route *envoy_route_v3.RouteConfiguration) map[string]struct{} {
|
|
if route == nil {
|
|
return nil
|
|
}
|
|
|
|
clusterNames := make(map[string]struct{})
|
|
|
|
for _, virtualHost := range route.VirtualHosts {
|
|
for _, route := range virtualHost.Routes {
|
|
r := route.GetRoute()
|
|
if r == nil {
|
|
continue
|
|
}
|
|
if c := r.GetCluster(); c != "" {
|
|
clusterNames[r.GetCluster()] = struct{}{}
|
|
}
|
|
|
|
if wc := r.GetWeightedClusters(); wc != nil {
|
|
for _, c := range wc.GetClusters() {
|
|
if c.Name != "" {
|
|
clusterNames[c.Name] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return clusterNames
|
|
}
|
|
|
|
func GetTCPProxy(filter *envoy_listener_v3.Filter) *envoy_tcp_proxy_v3.TcpProxy {
|
|
if typedConfig := filter.GetTypedConfig(); typedConfig != nil {
|
|
config := &envoy_tcp_proxy_v3.TcpProxy{}
|
|
if err := anypb.UnmarshalTo(typedConfig, config, proto.UnmarshalOptions{}); err == nil {
|
|
return config
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getSNI(chain *envoy_listener_v3.FilterChain) string {
|
|
var sni string
|
|
|
|
if chain == nil {
|
|
return sni
|
|
}
|
|
|
|
if chain.FilterChainMatch == nil {
|
|
return sni
|
|
}
|
|
|
|
if len(chain.FilterChainMatch.ServerNames) == 0 {
|
|
return sni
|
|
}
|
|
|
|
return chain.FilterChainMatch.ServerNames[0]
|
|
}
|
|
|
|
// GetHTTPConnectionManager returns the Envoy HttpConnectionManager filter from the list of network filters.
|
|
// It also returns the index within the list of filters where the connection manager was found in case the
|
|
// caller needs to overwrite the original filter.
|
|
// It returns a non-nil error if the HttpConnectionManager is not found.
|
|
func GetHTTPConnectionManager(filters ...*envoy_listener_v3.Filter) (*envoy_http_v3.HttpConnectionManager, int, error) {
|
|
for idx, filter := range filters {
|
|
if filter.Name == "envoy.filters.network.http_connection_manager" {
|
|
if httpConnMgr := envoy_resource_v3.GetHTTPConnectionManager(filter); httpConnMgr != nil {
|
|
return httpConnMgr, idx, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, 0, errors.New("failed to get HTTP connection manager")
|
|
}
|
|
|
|
// InsertLocation indicates where to insert an Envoy resource within a list of resources.
|
|
type InsertLocation string
|
|
|
|
const (
|
|
// InsertFirst inserts the resource as the first entry in the list.
|
|
InsertFirst InsertLocation = "First"
|
|
// InsertLast inserts the resource as the last entry in the list.
|
|
InsertLast InsertLocation = "Last"
|
|
// InsertBeforeFirstMatch inserts the resource before the first resource with a matching name.
|
|
InsertBeforeFirstMatch InsertLocation = "BeforeFirstMatch"
|
|
// InsertAfterFirstMatch inserts the resource after the first resource with a matching name.
|
|
InsertAfterFirstMatch InsertLocation = "AfterFirstMatch"
|
|
// InsertBeforeLastMatch inserts the resource before the last resource with a matching name.
|
|
InsertBeforeLastMatch InsertLocation = "BeforeLastMatch"
|
|
// InsertAfterLastMatch inserts the resource after the last resource with a matching name.
|
|
InsertAfterLastMatch InsertLocation = "AfterLastMatch"
|
|
)
|
|
|
|
// InsertOptions controls how and where to insert Envoy resources.
|
|
type InsertOptions struct {
|
|
// Location defines where to insert the resource within the list.
|
|
Location InsertLocation
|
|
// FilterName indicates the name of the resource to insert relative to.
|
|
FilterName string
|
|
}
|
|
|
|
// InsertHTTPFilter inserts the given HTTP filter into the HttpConnectionManager's filter chain in the location
|
|
// determined by the insert options. This list of filters must include the HttpConnectionManager network
|
|
// filter or the operation will fail.
|
|
//
|
|
// It returns the modified list of filters including the updated HttpConnectionManager.
|
|
// If a matching location is not found to insert the filter, a non-nil error is returned.
|
|
func InsertHTTPFilter(filters []*envoy_listener_v3.Filter, filter *envoy_http_v3.HttpFilter, opts InsertOptions) ([]*envoy_listener_v3.Filter, error) {
|
|
httpConnMgr, idx, err := GetHTTPConnectionManager(filters...)
|
|
if err != nil {
|
|
return filters, err
|
|
}
|
|
|
|
namedFilters := make([]namedFilter, 0, len(httpConnMgr.HttpFilters)+1)
|
|
for _, f := range httpConnMgr.HttpFilters {
|
|
namedFilters = append(namedFilters, f)
|
|
}
|
|
insertIdx, err := locateInsertIndex(opts, namedFilters)
|
|
if err != nil {
|
|
return filters, fmt.Errorf("failed to insert %q filter: %w", filter.Name, err)
|
|
}
|
|
|
|
currIdx := 0
|
|
newHttpFilters := make([]*envoy_http_v3.HttpFilter, len(httpConnMgr.HttpFilters)+1)
|
|
for idx, httpFilter := range httpConnMgr.HttpFilters {
|
|
if idx == insertIdx {
|
|
newHttpFilters[currIdx] = filter
|
|
currIdx++
|
|
}
|
|
newHttpFilters[currIdx] = httpFilter
|
|
currIdx++
|
|
}
|
|
if currIdx == insertIdx {
|
|
newHttpFilters[currIdx] = filter
|
|
}
|
|
|
|
httpConnMgr.HttpFilters = newHttpFilters
|
|
newHttpConMan, err := MakeFilter("envoy.filters.network.http_connection_manager", httpConnMgr)
|
|
if err != nil {
|
|
return filters, errors.New("failed to insert new HTTP connection manager filter")
|
|
}
|
|
filtersCopy := make([]*envoy_listener_v3.Filter, len(filters))
|
|
copy(filtersCopy, filters)
|
|
filtersCopy[idx] = newHttpConMan
|
|
|
|
return filtersCopy, nil
|
|
}
|
|
|
|
// InsertNetworkFilter inserts the given network filter into the filter chain in the location
|
|
// determined by the insert options.
|
|
//
|
|
// It returns the modified list of filters including the new filter.
|
|
// If a matching location is not found to insert the filter, a non-nil error is returned.
|
|
func InsertNetworkFilter(filters []*envoy_listener_v3.Filter, filter *envoy_listener_v3.Filter, opts InsertOptions) ([]*envoy_listener_v3.Filter, error) {
|
|
namedFilters := make([]namedFilter, 0, len(filters)+1)
|
|
for _, f := range filters {
|
|
namedFilters = append(namedFilters, f)
|
|
}
|
|
insertIdx, err := locateInsertIndex(opts, namedFilters)
|
|
if err != nil {
|
|
return filters, fmt.Errorf("failed to insert %q filter: %w", filter.Name, err)
|
|
}
|
|
|
|
currIdx := 0
|
|
newFilters := make([]*envoy_listener_v3.Filter, len(filters)+1)
|
|
for idx, f := range filters {
|
|
if idx == insertIdx {
|
|
newFilters[currIdx] = filter
|
|
currIdx++
|
|
}
|
|
newFilters[currIdx] = f
|
|
currIdx++
|
|
}
|
|
if currIdx == insertIdx {
|
|
newFilters[currIdx] = filter
|
|
}
|
|
|
|
return newFilters, nil
|
|
}
|
|
|
|
// namedFilter is a convenience interface for locating Envoy filters based on name.
|
|
type namedFilter interface {
|
|
GetName() string
|
|
}
|
|
|
|
// locateInsertIndex returns the index where a filter should be inserted based on the given
|
|
// insert options.
|
|
func locateInsertIndex(opts InsertOptions, filters []namedFilter) (int, error) {
|
|
idx := 0
|
|
if opts.Location == InsertFirst {
|
|
return idx, nil
|
|
}
|
|
if opts.Location == InsertLast {
|
|
return len(filters), nil
|
|
}
|
|
|
|
matched := false
|
|
for currIdx, filter := range filters {
|
|
if filter.GetName() == opts.FilterName {
|
|
matched = true
|
|
switch opts.Location {
|
|
case InsertBeforeFirstMatch:
|
|
return currIdx, nil
|
|
case InsertAfterFirstMatch:
|
|
return currIdx + 1, nil
|
|
case InsertBeforeLastMatch:
|
|
idx = currIdx
|
|
case InsertAfterLastMatch:
|
|
idx = currIdx + 1
|
|
}
|
|
}
|
|
}
|
|
if matched {
|
|
return idx, nil
|
|
}
|
|
return idx, fmt.Errorf("failed to find insert location %q for %q", opts.Location, opts.FilterName)
|
|
}
|