You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/envoyextensions/extensioncommon/resources.go

570 lines
17 KiB

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
1 year ago
// 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)
}