[NET-4799] [OSS] xdsv2: listeners L4 support for connect proxies (#18436)

* refactor to avoid future import cycles
This commit is contained in:
Nitya Dhanushkodi 2023-08-15 11:57:07 -07:00 committed by GitHub
parent 0e94f48ce0
commit 6b7ccd06cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 3104 additions and 89 deletions

View File

@ -19,6 +19,8 @@ import (
envoy_upstreams_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
envoy_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/hashicorp/consul/agent/xds/config"
"github.com/hashicorp/consul/agent/xds/naming"
"github.com/hashicorp/go-hclog"
"google.golang.org/protobuf/encoding/protojson"
@ -366,7 +368,7 @@ func makePassthroughClusters(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message,
!meshConf.TransparentProxy.MeshDestinationsOnly {
clusters = append(clusters, &envoy_cluster_v3.Cluster{
Name: OriginalDestinationClusterName,
Name: naming.OriginalDestinationClusterName,
ClusterDiscoveryType: &envoy_cluster_v3.Cluster_Type{
Type: envoy_cluster_v3.Cluster_ORIGINAL_DST,
},
@ -1041,7 +1043,7 @@ func (s *ResourceGenerator) configIngressUpstreamCluster(c *envoy_cluster_v3.Clu
if svc != nil {
override = svc.PassiveHealthCheck
}
outlierDetection := ToOutlierDetection(cfgSnap.IngressGateway.Defaults.PassiveHealthCheck, override, false)
outlierDetection := config.ToOutlierDetection(cfgSnap.IngressGateway.Defaults.PassiveHealthCheck, override, false)
c.OutlierDetection = outlierDetection
}
@ -1050,7 +1052,7 @@ func (s *ResourceGenerator) makeAppCluster(cfgSnap *proxycfg.ConfigSnapshot, nam
var c *envoy_cluster_v3.Cluster
var err error
cfg, err := ParseProxyConfig(cfgSnap.Proxy.Config)
cfg, err := config.ParseProxyConfig(cfgSnap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -1144,7 +1146,7 @@ func (s *ResourceGenerator) makeUpstreamClusterForPeerService(
clusterName := generatePeeredClusterName(uid, tbs)
outlierDetection := ToOutlierDetection(upstreamConfig.PassiveHealthCheck, nil, true)
outlierDetection := config.ToOutlierDetection(upstreamConfig.PassiveHealthCheck, nil, true)
// We can't rely on health checks for services on cluster peers because they
// don't take into account service resolvers, splitters and routers. Setting
// MaxEjectionPercent too 100% gives outlier detection the power to eject the
@ -1279,7 +1281,7 @@ func (s *ResourceGenerator) makeUpstreamClusterForPreparedQuery(upstream structs
CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{
Thresholds: makeThresholdsIfNeeded(cfg.Limits),
},
OutlierDetection: ToOutlierDetection(cfg.PassiveHealthCheck, nil, true),
OutlierDetection: config.ToOutlierDetection(cfg.PassiveHealthCheck, nil, true),
}
if cfg.Protocol == "http2" || cfg.Protocol == "grpc" {
if err := s.setHttp2ProtocolOptions(c); err != nil {
@ -1499,7 +1501,7 @@ func (s *ResourceGenerator) makeUpstreamClustersForDiscoveryChain(
CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{
Thresholds: makeThresholdsIfNeeded(upstreamConfig.Limits),
},
OutlierDetection: ToOutlierDetection(upstreamConfig.PassiveHealthCheck, nil, true),
OutlierDetection: config.ToOutlierDetection(upstreamConfig.PassiveHealthCheck, nil, true),
}
var lb *structs.LoadBalancer
@ -1676,7 +1678,7 @@ type clusterOpts struct {
// makeGatewayCluster creates an Envoy cluster for a mesh or terminating gateway
func (s *ResourceGenerator) makeGatewayCluster(snap *proxycfg.ConfigSnapshot, opts clusterOpts) *envoy_cluster_v3.Cluster {
cfg, err := ParseGatewayConfig(snap.Proxy.Config)
cfg, err := config.ParseGatewayConfig(snap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -1819,7 +1821,7 @@ func configureClusterWithHostnames(
// makeExternalIPCluster creates an Envoy cluster for routing to IP addresses outside of Consul
// This is used by terminating gateways for Destinations
func (s *ResourceGenerator) makeExternalIPCluster(snap *proxycfg.ConfigSnapshot, opts clusterOpts) *envoy_cluster_v3.Cluster {
cfg, err := ParseGatewayConfig(snap.Proxy.Config)
cfg, err := config.ParseGatewayConfig(snap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -1858,7 +1860,7 @@ func (s *ResourceGenerator) makeExternalIPCluster(snap *proxycfg.ConfigSnapshot,
// makeExternalHostnameCluster creates an Envoy cluster for hostname endpoints that will be resolved with DNS
// This is used by both terminating gateways for Destinations, and Mesh Gateways for peering control plane traffice
func (s *ResourceGenerator) makeExternalHostnameCluster(snap *proxycfg.ConfigSnapshot, opts clusterOpts) *envoy_cluster_v3.Cluster {
cfg, err := ParseGatewayConfig(snap.Proxy.Config)
cfg, err := config.ParseGatewayConfig(snap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -2044,7 +2046,7 @@ func (s *ResourceGenerator) getTargetClusterName(upstreamsSnapshot *proxycfg.Con
clusterName = generatePeeredClusterName(targetUID, tbs)
}
clusterName = CustomizeClusterName(clusterName, chain)
clusterName = naming.CustomizeClusterName(clusterName, chain)
if forMeshGateway {
clusterName = meshGatewayExportedClusterNamePrefix + clusterName
}

View File

@ -1,7 +1,7 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package xds
package config
import (
"strings"

View File

@ -1,7 +1,7 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package xds
package config
import (
"testing"

View File

@ -0,0 +1,7 @@
package configfetcher
// ConfigFetcher is the interface the agent needs to expose
// for the xDS server to fetch agent config, currently only one field is fetched
type ConfigFetcher interface {
AdvertiseAddrLAN() string
}

View File

@ -29,6 +29,9 @@ import (
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_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/hashicorp/consul/agent/xds/config"
"github.com/hashicorp/consul/agent/xds/naming"
"github.com/hashicorp/consul/agent/xds/platform"
"github.com/hashicorp/go-hclog"
"google.golang.org/protobuf/encoding/protojson"
@ -50,8 +53,6 @@ import (
"github.com/hashicorp/consul/types"
)
const virtualIPTag = "virtual"
// listenersFromSnapshot returns the xDS API representation of the "listeners" in the snapshot.
func (s *ResourceGenerator) listenersFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
if cfgSnap == nil {
@ -118,7 +119,7 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg.
}
}
proxyCfg, err := ParseProxyConfig(cfgSnap.Proxy.Config)
proxyCfg, err := config.ParseProxyConfig(cfgSnap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -258,7 +259,7 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg.
// We only match on this virtual IP if the upstream is in the proxy's partition.
// This is because the IP is not guaranteed to be unique across k8s clusters.
if acl.EqualPartitions(e.Node.PartitionOrDefault(), cfgSnap.ProxyID.PartitionOrDefault()) {
if vip := e.Service.TaggedAddresses[virtualIPTag]; vip.Address != "" {
if vip := e.Service.TaggedAddresses[naming.VirtualIPTag]; vip.Address != "" {
uniqueAddrs[vip.Address] = struct{}{}
}
}
@ -462,7 +463,7 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg.
// The virtualIPTag is used by consul-k8s to store the ClusterIP for a service.
// For services imported from a peer,the partition will be equal in all cases.
if acl.EqualPartitions(e.Node.PartitionOrDefault(), cfgSnap.ProxyID.PartitionOrDefault()) {
if vip := e.Service.TaggedAddresses[virtualIPTag]; vip.Address != "" {
if vip := e.Service.TaggedAddresses[naming.VirtualIPTag]; vip.Address != "" {
uniqueAddrs[vip.Address] = struct{}{}
}
}
@ -552,8 +553,8 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg.
filterChain, err := s.makeUpstreamFilterChain(filterChainOpts{
accessLogs: &cfgSnap.Proxy.AccessLogs,
clusterName: OriginalDestinationClusterName,
filterName: OriginalDestinationClusterName,
clusterName: naming.OriginalDestinationClusterName,
filterName: naming.OriginalDestinationClusterName,
protocol: "tcp",
})
if err != nil {
@ -787,7 +788,7 @@ func parseCheckPath(check structs.CheckType) (structs.ExposePath, error) {
// listenersFromSnapshotGateway returns the "listener" for a terminating-gateway or mesh-gateway service
func (s *ResourceGenerator) listenersFromSnapshotGateway(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
cfg, err := ParseGatewayConfig(cfgSnap.Proxy.Config)
cfg, err := config.ParseGatewayConfig(cfgSnap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -1171,7 +1172,7 @@ func createDownstreamTransportSocketForConnectTLS(cfgSnap *proxycfg.ConfigSnapsh
// Determine listener protocol type from configured service protocol. Don't hard fail on a config typo,
//The parse func returns default config if there is an error, so it's safe to continue.
cfg, _ := ParseProxyConfig(cfgSnap.Proxy.Config)
cfg, _ := config.ParseProxyConfig(cfgSnap.Proxy.Config)
// Create TLS validation context for mTLS with leaf certificate and root certs.
tlsContext := makeCommonTLSContext(
@ -1263,7 +1264,7 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
var l *envoy_listener_v3.Listener
var err error
cfg, err := ParseProxyConfig(cfgSnap.Proxy.Config)
cfg, err := config.ParseProxyConfig(cfgSnap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -1513,7 +1514,7 @@ func (s *ResourceGenerator) finalizePublicListenerFromConfig(l *envoy_listener_v
}
func (s *ResourceGenerator) makeExposedCheckListener(cfgSnap *proxycfg.ConfigSnapshot, cluster string, path structs.ExposePath) (proto.Message, error) {
cfg, err := ParseProxyConfig(cfgSnap.Proxy.Config)
cfg, err := config.ParseProxyConfig(cfgSnap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -1588,7 +1589,7 @@ func (s *ResourceGenerator) makeExposedCheckListener(cfgSnap *proxycfg.ConfigSna
&envoy_core_v3.CidrRange{AddressPrefix: advertise, PrefixLen: &wrapperspb.UInt32Value{Value: uint32(advertiseLen)}},
)
if ok, err := kernelSupportsIPv6(); err != nil {
if ok, err := platform.SupportsIPv6(); err != nil {
return nil, err
} else if ok {
ranges = append(ranges,
@ -1639,7 +1640,7 @@ func (s *ResourceGenerator) makeTerminatingGatewayListener(
intentions := cfgSnap.TerminatingGateway.Intentions[svc]
svcConfig := cfgSnap.TerminatingGateway.ServiceConfigs[svc]
cfg, err := ParseProxyConfig(svcConfig.ProxyConfig)
cfg, err := config.ParseProxyConfig(svcConfig.ProxyConfig)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -1683,7 +1684,7 @@ func (s *ResourceGenerator) makeTerminatingGatewayListener(
intentions := cfgSnap.TerminatingGateway.Intentions[svc]
svcConfig := cfgSnap.TerminatingGateway.ServiceConfigs[svc]
cfg, err := ParseProxyConfig(svcConfig.ProxyConfig)
cfg, err := config.ParseProxyConfig(svcConfig.ProxyConfig)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -1807,7 +1808,7 @@ func (s *ResourceGenerator) makeFilterChainTerminatingGateway(cfgSnap *proxycfg.
filterChain.Filters = append(filterChain.Filters, authFilter)
}
proxyCfg, err := ParseProxyConfig(cfgSnap.Proxy.Config)
proxyCfg, err := config.ParseProxyConfig(cfgSnap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -2128,7 +2129,7 @@ func (s *ResourceGenerator) makeMeshGatewayPeerFilterChain(
if err != nil {
return nil, err
}
clusterName = meshGatewayExportedClusterNamePrefix + CustomizeClusterName(target.Name, chain)
clusterName = meshGatewayExportedClusterNamePrefix + naming.CustomizeClusterName(target.Name, chain)
}
uid := proxycfg.NewUpstreamIDFromServiceName(svc)

View File

@ -9,6 +9,7 @@ import (
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
"github.com/hashicorp/consul/agent/xds/naming"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/wrapperspb"
@ -70,7 +71,7 @@ func (s *ResourceGenerator) makeAPIGatewayListeners(address string, cfgSnap *pro
if err != nil {
return nil, err
}
clusterName = CustomizeClusterName(target.Name, chain)
clusterName = naming.CustomizeClusterName(target.Name, chain)
}
filterName := fmt.Sprintf("%s.%s.%s.%s", chain.ServiceName, chain.Namespace, chain.Partition, chain.Datacenter)

View File

@ -9,6 +9,7 @@ import (
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
"github.com/hashicorp/consul/agent/xds/naming"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
@ -62,7 +63,7 @@ func (s *ResourceGenerator) makeIngressGatewayListeners(address string, cfgSnap
if err != nil {
return nil, err
}
clusterName = CustomizeClusterName(target.Name, chain)
clusterName = naming.CustomizeClusterName(target.Name, chain)
}
filterName := fmt.Sprintf("%s.%s.%s.%s", chain.ServiceName, chain.Namespace, chain.Partition, chain.Datacenter)

View File

@ -11,8 +11,7 @@ import (
"text/template"
"github.com/stretchr/testify/assert"
"github.com/hashicorp/consul/agent/xds/testcommon"
"google.golang.org/protobuf/proto"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
testinf "github.com/mitchellh/go-testing-interface"
@ -20,6 +19,10 @@ import (
"github.com/hashicorp/consul/agent/proxycfg"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/xds/configfetcher"
"github.com/hashicorp/consul/agent/xds/proxystateconverter"
"github.com/hashicorp/consul/agent/xds/testcommon"
"github.com/hashicorp/consul/agent/xdsv2"
"github.com/hashicorp/consul/envoyextensions/xdscommon"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/types"
@ -33,6 +36,7 @@ type listenerTestCase struct {
// test input.
overrideGoldenName string
generatorSetup func(*ResourceGenerator)
alsoRunTestForV2 bool
}
func makeListenerDiscoChainTests(enterprise bool) []listenerTestCase {
@ -70,6 +74,7 @@ func makeListenerDiscoChainTests(enterprise bool) []listenerTestCase {
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "simple", enterprise, nil, nil)
},
alsoRunTestForV2: true,
},
{
name: "connect-proxy-with-http-chain",
@ -118,6 +123,7 @@ func makeListenerDiscoChainTests(enterprise bool) []listenerTestCase {
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "external-sni", enterprise, nil, nil)
},
alsoRunTestForV2: true,
},
{
name: "connect-proxy-with-chain-and-overrides",
@ -130,12 +136,14 @@ func makeListenerDiscoChainTests(enterprise bool) []listenerTestCase {
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "failover-through-remote-gateway", enterprise, nil, nil)
},
alsoRunTestForV2: true,
},
{
name: "connect-proxy-with-tcp-chain-failover-through-local-gateway",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotDiscoveryChain(t, "failover-through-local-gateway", enterprise, nil, nil)
},
alsoRunTestForV2: true,
},
{
name: "connect-proxy-with-jwt-config-entry-with-local",
@ -226,6 +234,7 @@ func TestListenersFromSnapshot(t *testing.T) {
},
})
},
alsoRunTestForV2: true,
},
{
name: "connect-proxy-with-tls-incoming-min-version",
@ -245,6 +254,7 @@ func TestListenersFromSnapshot(t *testing.T) {
},
})
},
alsoRunTestForV2: true,
},
{
name: "connect-proxy-with-tls-incoming-max-version",
@ -264,6 +274,7 @@ func TestListenersFromSnapshot(t *testing.T) {
},
})
},
alsoRunTestForV2: true,
},
{
name: "connect-proxy-with-tls-incoming-cipher-suites",
@ -286,6 +297,7 @@ func TestListenersFromSnapshot(t *testing.T) {
},
})
},
alsoRunTestForV2: true,
},
{
name: "grpc-public-listener",
@ -302,6 +314,7 @@ func TestListenersFromSnapshot(t *testing.T) {
ns.Proxy.Config["bind_address"] = "127.0.0.2"
}, nil)
},
alsoRunTestForV2: true,
},
{
name: "listener-bind-port",
@ -310,6 +323,7 @@ func TestListenersFromSnapshot(t *testing.T) {
ns.Proxy.Config["bind_port"] = 8888
}, nil)
},
alsoRunTestForV2: true,
},
{
name: "listener-bind-address-port",
@ -319,6 +333,7 @@ func TestListenersFromSnapshot(t *testing.T) {
ns.Proxy.Config["bind_port"] = 8888
}, nil)
},
alsoRunTestForV2: true,
},
{
name: "listener-unix-domain-socket",
@ -330,6 +345,7 @@ func TestListenersFromSnapshot(t *testing.T) {
ns.Proxy.Upstreams[0].LocalBindSocketMode = "0640"
}, nil)
},
alsoRunTestForV2: true,
},
{
name: "listener-max-inbound-connections",
@ -338,6 +354,7 @@ func TestListenersFromSnapshot(t *testing.T) {
ns.Proxy.Config["max_inbound_connections"] = 222
}, nil)
},
alsoRunTestForV2: true,
},
{
name: "http2-public-listener",
@ -354,6 +371,7 @@ func TestListenersFromSnapshot(t *testing.T) {
ns.Proxy.Config["balance_inbound_connections"] = "exact_balance"
}, nil)
},
alsoRunTestForV2: true,
},
{
name: "listener-balance-outbound-connections-bind-port",
@ -362,6 +380,7 @@ func TestListenersFromSnapshot(t *testing.T) {
ns.Proxy.Upstreams[0].Config["balance_outbound_connections"] = "exact_balance"
}, nil)
},
alsoRunTestForV2: true,
},
{
name: "http-public-listener",
@ -559,7 +578,7 @@ func TestListenersFromSnapshot(t *testing.T) {
},
{
// NOTE: if IPv6 is not supported in the kernel per
// kernelSupportsIPv6() then this test will fail because the golden
// platform.SupportsIPv6() then this test will fail because the golden
// files were generated assuming ipv6 support was present
name: "expose-checks-http",
create: proxycfg.TestConfigSnapshotExposeChecks,
@ -571,7 +590,7 @@ func TestListenersFromSnapshot(t *testing.T) {
},
{
// NOTE: if IPv6 is not supported in the kernel per
// kernelSupportsIPv6() then this test will fail because the golden
// platform.SupportsIPv6() then this test will fail because the golden
// files were generated assuming ipv6 support was present
name: "expose-checks-http-with-bind-override",
create: proxycfg.TestConfigSnapshotExposeChecksWithBindOverride,
@ -583,7 +602,7 @@ func TestListenersFromSnapshot(t *testing.T) {
},
{
// NOTE: if IPv6 is not supported in the kernel per
// kernelSupportsIPv6() then this test will fail because the golden
// platform.SupportsIPv6() then this test will fail because the golden
// files were generated assuming ipv6 support was present
name: "expose-checks-grpc",
create: proxycfg.TestConfigSnapshotExposeChecksGRPC,
@ -1172,14 +1191,17 @@ func TestListenersFromSnapshot(t *testing.T) {
{
name: "transparent-proxy-catalog-destinations-only",
create: proxycfg.TestConfigSnapshotTransparentProxyCatalogDestinationsOnly,
alsoRunTestForV2: true,
},
{
name: "transparent-proxy-dial-instances-directly",
create: proxycfg.TestConfigSnapshotTransparentProxyDialDirectly,
alsoRunTestForV2: true,
},
{
name: "transparent-proxy-terminating-gateway",
create: proxycfg.TestConfigSnapshotTransparentProxyTerminatingGatewayCatalogDestinationsOnly,
alsoRunTestForV2: true,
},
{
name: "custom-trace-listener",
@ -1242,6 +1264,7 @@ func TestListenersFromSnapshot(t *testing.T) {
},
nil)
},
alsoRunTestForV2: true,
},
{
name: "connect-proxy-without-tproxy-and-permissive-mtls",
@ -1251,6 +1274,7 @@ func TestListenersFromSnapshot(t *testing.T) {
},
nil)
},
alsoRunTestForV2: true,
},
}
@ -1275,16 +1299,17 @@ func TestListenersFromSnapshot(t *testing.T) {
// golder files for every test case and so not be any use!
testcommon.SetupTLSRootsAndLeaf(t, snap)
var listeners []proto.Message
t.Run("current-xdsv1", func(t *testing.T) {
// Need server just for logger dependency
g := NewResourceGenerator(testutil.Logger(t), nil, false)
g.ProxyFeatures = sf
if tt.generatorSetup != nil {
tt.generatorSetup(g)
}
listeners, err := g.listenersFromSnapshot(snap)
listeners, err = g.listenersFromSnapshot(snap)
require.NoError(t, err)
// The order of listeners returned via LDS isn't relevant, so it's safe
// to sort these for the purposes of test comparisons.
sort.Slice(listeners, func(i, j int) bool {
@ -1294,7 +1319,6 @@ func TestListenersFromSnapshot(t *testing.T) {
r, err := createResponse(xdscommon.ListenerType, "00000001", "00000001", listeners)
require.NoError(t, err)
t.Run("current", func(t *testing.T) {
gotJSON := protoToJSON(t, r)
gName := tt.name
@ -1305,6 +1329,39 @@ func TestListenersFromSnapshot(t *testing.T) {
expectedJSON := goldenEnvoy(t, filepath.Join("listeners", gName), envoyVersion, latestEnvoyVersion, gotJSON)
require.JSONEq(t, expectedJSON, gotJSON)
})
if tt.alsoRunTestForV2 {
t.Run("current-xdsv2", func(t *testing.T) {
generator := xdsv2.NewResourceGenerator(testutil.Logger(t))
converter := proxystateconverter.NewConverter(testutil.Logger(t), nil)
proxyState, err := converter.ProxyStateFromSnapshot(snap)
require.NoError(t, err)
res, err := generator.AllResourcesFromIR(proxyState)
require.NoError(t, err)
listeners = res[xdscommon.ListenerType]
// The order of listeners returned via LDS isn't relevant, so it's safe
// to sort these for the purposes of test comparisons.
sort.Slice(listeners, func(i, j int) bool {
return listeners[i].(*envoy_listener_v3.Listener).Name < listeners[j].(*envoy_listener_v3.Listener).Name
})
r, err := createResponse(xdscommon.ListenerType, "00000001", "00000001", listeners)
require.NoError(t, err)
gotJSON := protoToJSON(t, r)
gName := tt.name
if tt.overrideGoldenName != "" {
gName = tt.overrideGoldenName
}
expectedJSON := goldenEnvoy(t, filepath.Join("listeners", gName), envoyVersion, latestEnvoyVersion, gotJSON)
require.JSONEq(t, expectedJSON, gotJSON)
})
}
})
}
})
@ -1462,7 +1519,7 @@ func customTraceJSON(t testinf.T) string {
type configFetcherFunc func() string
var _ ConfigFetcher = (configFetcherFunc)(nil)
var _ configfetcher.ConfigFetcher = (configFetcherFunc)(nil)
func (f configFetcherFunc) AdvertiseAddrLAN() string {
return f()

View File

@ -1,7 +1,7 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package xds
package naming
import (
"fmt"
@ -9,6 +9,16 @@ import (
"github.com/hashicorp/consul/agent/structs"
)
const (
// OriginalDestinationClusterName is the name we give to the passthrough
// cluster which redirects transparently-proxied requests to their original
// destination outside the mesh. This cluster prevents Consul from blocking
// connections to destinations outside of the catalog when in transparent
// proxy mode.
OriginalDestinationClusterName = "original-destination"
VirtualIPTag = "virtual"
)
func CustomizeClusterName(clusterName string, chain *structs.CompiledDiscoveryChain) string {
if chain == nil || chain.CustomizationHash == "" {
return clusterName

View File

@ -4,8 +4,8 @@
//go:build !linux
// +build !linux
package xds
package platform
func kernelSupportsIPv6() (bool, error) {
func SupportsIPv6() (bool, error) {
return true, nil
}

View File

@ -4,7 +4,7 @@
//go:build linux
// +build linux
package xds
package platform
import (
"fmt"
@ -20,7 +20,7 @@ var (
ipv6SupportedErr error
)
func kernelSupportsIPv6() (bool, error) {
func SupportsIPv6() (bool, error) {
ipv6SupportOnce.Do(func() {
ipv6Supported, ipv6SupportedErr = checkIfKernelSupportsIPv6()
})

View File

@ -0,0 +1,74 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package proxystateconverter
import (
"fmt"
"strings"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/proxycfg"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/xds/naming"
"github.com/hashicorp/consul/proto/private/pbpeering"
)
const (
meshGatewayExportedClusterNamePrefix = "exported~"
)
func makeExposeClusterName(destinationPort int) string {
return fmt.Sprintf("exposed_cluster_%d", destinationPort)
}
func clusterNameForDestination(cfgSnap *proxycfg.ConfigSnapshot, name string, address string, namespace string, partition string) string {
name = destinationSpecificServiceName(name, address)
sni := connect.ServiceSNI(name, "", namespace, partition, cfgSnap.Datacenter, cfgSnap.Roots.TrustDomain)
// Prefixed with destination to distinguish from non-passthrough clusters for the same upstream.
return "destination." + sni
}
func destinationSpecificServiceName(name string, address string) string {
address = strings.ReplaceAll(address, ":", "-")
address = strings.ReplaceAll(address, ".", "-")
return fmt.Sprintf("%s.%s", address, name)
}
// generatePeeredClusterName returns an SNI-like cluster name which mimics PeeredServiceSNI
// but excludes partition information which could be ambiguous (local vs remote partition).
func generatePeeredClusterName(uid proxycfg.UpstreamID, tb *pbpeering.PeeringTrustBundle) string {
return strings.Join([]string{
uid.Name,
uid.NamespaceOrDefault(),
uid.Peer,
"external",
tb.TrustDomain,
}, ".")
}
func (s *Converter) getTargetClusterName(upstreamsSnapshot *proxycfg.ConfigSnapshotUpstreams, chain *structs.CompiledDiscoveryChain, tid string, forMeshGateway bool) string {
target := chain.Targets[tid]
clusterName := target.Name
targetUID := proxycfg.NewUpstreamIDFromTargetID(tid)
if targetUID.Peer != "" {
tbs, ok := upstreamsSnapshot.UpstreamPeerTrustBundles.Get(targetUID.Peer)
// We can't generate cluster on peers without the trust bundle. The
// trust bundle should be ready soon.
if !ok {
s.Logger.Debug("peer trust bundle not ready for discovery chain target",
"peer", targetUID.Peer,
"target", tid,
)
return ""
}
clusterName = generatePeeredClusterName(targetUID, tbs)
}
clusterName = naming.CustomizeClusterName(clusterName, chain)
if forMeshGateway {
clusterName = meshGatewayExportedClusterNamePrefix + clusterName
}
return clusterName
}

View File

@ -0,0 +1,118 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package proxystateconverter
import (
"fmt"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/consul/agent/proxycfg"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/xds/configfetcher"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v1alpha1"
"github.com/hashicorp/consul/proto-public/pbmesh/v1alpha1/pbproxystate"
)
// Converter converts a single snapshot into a ProxyState.
type Converter struct {
Logger hclog.Logger
CfgFetcher configfetcher.ConfigFetcher
proxyState *pbmesh.ProxyState
}
func NewConverter(
logger hclog.Logger,
cfgFetcher configfetcher.ConfigFetcher,
) *Converter {
return &Converter{
Logger: logger,
CfgFetcher: cfgFetcher,
proxyState: &pbmesh.ProxyState{
Listeners: make([]*pbproxystate.Listener, 0),
Clusters: make(map[string]*pbproxystate.Cluster),
Routes: make(map[string]*pbproxystate.Route),
Endpoints: make(map[string]*pbproxystate.Endpoints),
},
}
}
func (g *Converter) ProxyStateFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot) (*pbmesh.ProxyState, error) {
err := g.resourcesFromSnapshot(cfgSnap)
if err != nil {
return nil, fmt.Errorf("failed to generate FullProxyState: %v", err)
}
return g.proxyState, nil
}
func (g *Converter) resourcesFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot) error {
err := g.tlsConfigFromSnapshot(cfgSnap)
if err != nil {
return err
}
err = g.listenersFromSnapshot(cfgSnap)
if err != nil {
return err
}
return nil
}
const localPeerKey = "local"
func (g *Converter) tlsConfigFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot) error {
proxyStateTLS := &pbproxystate.TLS{}
g.proxyState.TrustBundles = make(map[string]*pbproxystate.TrustBundle)
g.proxyState.LeafCertificates = make(map[string]*pbproxystate.LeafCertificate)
// Set the TLS in the top level proxyState
g.proxyState.Tls = proxyStateTLS
// Add local trust bundle
g.proxyState.TrustBundles[localPeerKey] = &pbproxystate.TrustBundle{
TrustDomain: cfgSnap.Roots.TrustDomain,
Roots: []string{cfgSnap.RootPEMs()},
}
// Add peered trust bundles for remote peers that will dial this proxy.
for _, peeringTrustBundle := range cfgSnap.PeeringTrustBundles() {
g.proxyState.TrustBundles[peeringTrustBundle.PeerName] = &pbproxystate.TrustBundle{
TrustDomain: peeringTrustBundle.GetTrustDomain(),
Roots: peeringTrustBundle.RootPEMs,
}
}
// Add upstream peer trust bundles for dialing upstreams in remote peers.
upstreamsSnapshot, err := cfgSnap.ToConfigSnapshotUpstreams()
if err != nil {
if !(cfgSnap.Kind == structs.ServiceKindMeshGateway || cfgSnap.Kind == structs.ServiceKindTerminatingGateway) {
return err
}
}
if upstreamsSnapshot != nil {
upstreamsSnapshot.UpstreamPeerTrustBundles.ForEachKeyE(func(k proxycfg.PeerName) error {
tbs, ok := upstreamsSnapshot.UpstreamPeerTrustBundles.Get(k)
if ok {
g.proxyState.TrustBundles[k] = &pbproxystate.TrustBundle{
TrustDomain: tbs.TrustDomain,
Roots: tbs.RootPEMs,
}
}
return nil
})
}
if cfgSnap.MeshConfigTLSOutgoing() != nil {
proxyStateTLS.OutboundTlsParameters = makeTLSParametersFromTLSConfig(cfgSnap.MeshConfigTLSOutgoing().TLSMinVersion,
cfgSnap.MeshConfigTLSOutgoing().TLSMaxVersion, cfgSnap.MeshConfigTLSOutgoing().CipherSuites)
}
if cfgSnap.MeshConfigTLSIncoming() != nil {
proxyStateTLS.InboundTlsParameters = makeTLSParametersFromTLSConfig(cfgSnap.MeshConfigTLSIncoming().TLSMinVersion,
cfgSnap.MeshConfigTLSIncoming().TLSMaxVersion, cfgSnap.MeshConfigTLSIncoming().CipherSuites)
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ package xds
import (
"fmt"
"github.com/hashicorp/consul/agent/xds/configfetcher"
"github.com/hashicorp/go-hclog"
"google.golang.org/protobuf/proto"
@ -18,7 +19,7 @@ import (
// resources for a single client.
type ResourceGenerator struct {
Logger hclog.Logger
CfgFetcher ConfigFetcher
CfgFetcher configfetcher.ConfigFetcher
IncrementalXDS bool
ProxyFeatures xdscommon.SupportedProxyFeatures
@ -26,7 +27,7 @@ type ResourceGenerator struct {
func NewResourceGenerator(
logger hclog.Logger,
cfgFetcher ConfigFetcher,
cfgFetcher configfetcher.ConfigFetcher,
incrementalXDS bool,
) *ResourceGenerator {
return &ResourceGenerator{

View File

@ -22,6 +22,7 @@ import (
"github.com/hashicorp/consul/agent/consul/discoverychain"
"github.com/hashicorp/consul/agent/proxycfg"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/xds/config"
)
// routesFromSnapshot returns the xDS API representation of the "routes" in the
@ -140,7 +141,7 @@ func (s *ResourceGenerator) routesForTerminatingGateway(cfgSnap *proxycfg.Config
var resources []proto.Message
for _, svc := range cfgSnap.TerminatingGateway.ValidServices() {
clusterName := connect.ServiceSNI(svc.Name, "", svc.NamespaceOrDefault(), svc.PartitionOrDefault(), cfgSnap.Datacenter, cfgSnap.Roots.TrustDomain)
cfg, err := ParseProxyConfig(cfgSnap.TerminatingGateway.ServiceConfigs[svc].ProxyConfig)
cfg, err := config.ParseProxyConfig(cfgSnap.TerminatingGateway.ServiceConfigs[svc].ProxyConfig)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
@ -168,7 +169,7 @@ func (s *ResourceGenerator) routesForTerminatingGateway(cfgSnap *proxycfg.Config
for _, address := range svcConfig.Destination.Addresses {
clusterName := clusterNameForDestination(cfgSnap, svc.Name, address, svc.NamespaceOrDefault(), svc.PartitionOrDefault())
cfg, err := ParseProxyConfig(cfgSnap.TerminatingGateway.ServiceConfigs[svc].ProxyConfig)
cfg, err := config.ParseProxyConfig(cfgSnap.TerminatingGateway.ServiceConfigs[svc].ProxyConfig)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.

View File

@ -10,6 +10,7 @@ import (
"time"
envoy_discovery_v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
"github.com/hashicorp/consul/agent/xds/configfetcher"
"github.com/hashicorp/consul/envoyextensions/xdscommon"
@ -70,13 +71,6 @@ const (
// services named "local_agent" in the future.
LocalAgentClusterName = "local_agent"
// OriginalDestinationClusterName is the name we give to the passthrough
// cluster which redirects transparently-proxied requests to their original
// destination outside the mesh. This cluster prevents Consul from blocking
// connections to destinations outside of the catalog when in transparent
// proxy mode.
OriginalDestinationClusterName = "original-destination"
// DefaultAuthCheckFrequency is the default value for
// Server.AuthCheckFrequency to use when the zero value is provided.
DefaultAuthCheckFrequency = 5 * time.Minute
@ -88,12 +82,6 @@ const (
// coupling this to the agent.
type ACLResolverFunc func(id string) (acl.Authorizer, error)
// ConfigFetcher is the interface the agent needs to expose
// for the xDS server to fetch agent config, currently only one field is fetched
type ConfigFetcher interface {
AdvertiseAddrLAN() string
}
// ProxyConfigSource is the interface xds.Server requires to consume proxy
// config updates.
type ProxyConfigSource interface {
@ -110,7 +98,7 @@ type Server struct {
Logger hclog.Logger
CfgSrc ProxyConfigSource
ResolveToken ACLResolverFunc
CfgFetcher ConfigFetcher
CfgFetcher configfetcher.ConfigFetcher
// AuthCheckFrequency is how often we should re-check the credentials used
// during a long-lived gRPC Stream after it has been initially established.
@ -161,7 +149,7 @@ func NewServer(
logger hclog.Logger,
cfgMgr ProxyConfigSource,
resolveTokenSecret ACLResolverFunc,
cfgFetcher ConfigFetcher,
cfgFetcher configfetcher.ConfigFetcher,
) *Server {
return &Server{
NodeName: nodeName,

View File

@ -0,0 +1,6 @@
package xdsv2
// TODO(proxystate): In a future PR this will create clusters and add it to ProxyResources.proxyState
func (pr *ProxyResources) makeCluster(name string) error {
return nil
}

View File

@ -0,0 +1,967 @@
package xdsv2
import (
"fmt"
"sort"
"strconv"
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
envoy_grpc_http1_bridge_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_http1_bridge/v3"
envoy_grpc_stats_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/grpc_stats/v3"
envoy_http_router_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
envoy_extensions_filters_listener_http_inspector_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/http_inspector/v3"
envoy_original_dst_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/original_dst/v3"
envoy_tls_inspector_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3"
envoy_connection_limit_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/connection_limit/v3"
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
envoy_network_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3"
envoy_sni_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/sni_cluster/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_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/hashicorp/consul/envoyextensions/xdscommon"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/proto-public/pbmesh/v1alpha1/pbproxystate"
)
const (
envoyNetworkFilterName = "envoy.filters.network.tcp_proxy"
envoyOriginalDestinationListenerFilterName = "envoy.filters.listener.original_dst"
envoyTLSInspectorListenerFilterName = "envoy.filters.listener.tls_inspector"
envoyHttpInspectorListenerFilterName = "envoy.filters.listener.http_inspector"
envoyHttpConnectionManagerFilterName = "envoy.filters.network.http_connection_manager"
)
func (pr *ProxyResources) makeListener(listener *pbproxystate.Listener) (*envoy_listener_v3.Listener, error) {
envoyListener := &envoy_listener_v3.Listener{}
// Listener Address
var address *envoy_core_v3.Address
switch listener.BindAddress.(type) {
case *pbproxystate.Listener_HostPort:
address = makeIpPortEnvoyAddress(listener.BindAddress.(*pbproxystate.Listener_HostPort))
case *pbproxystate.Listener_UnixSocket:
address = makeUnixSocketEnvoyAddress(listener.BindAddress.(*pbproxystate.Listener_UnixSocket))
default:
// This should be impossible to reach because we're using protobufs.
return nil, fmt.Errorf("invalid listener bind address type: %t", listener.BindAddress)
}
envoyListener.Address = address
// Listener Direction
var direction envoy_core_v3.TrafficDirection
switch listener.Direction {
case pbproxystate.Direction_DIRECTION_OUTBOUND:
direction = envoy_core_v3.TrafficDirection_OUTBOUND
case pbproxystate.Direction_DIRECTION_INBOUND:
direction = envoy_core_v3.TrafficDirection_INBOUND
case pbproxystate.Direction_DIRECTION_UNSPECIFIED:
direction = envoy_core_v3.TrafficDirection_UNSPECIFIED
default:
return nil, fmt.Errorf("no direction for listener %+v", listener.Name)
}
envoyListener.TrafficDirection = direction
// Before creating the filter chains, sort routers by match to avoid draining if the list is provided out of order.
sortRouters(listener.Routers)
// Listener filter chains
for _, r := range listener.Routers {
filterChain, err := pr.makeEnvoyListenerFilterChain(r)
if err != nil {
return nil, fmt.Errorf("could not make filter chain: %w", err)
}
envoyListener.FilterChains = append(envoyListener.FilterChains, filterChain)
}
if listener.DefaultRouter != nil {
defaultFilterChain, err := pr.makeEnvoyListenerFilterChain(listener.DefaultRouter)
if err != nil {
return nil, fmt.Errorf("could not make filter chain: %w", err)
}
envoyListener.DefaultFilterChain = defaultFilterChain
}
// Envoy builtin listener filters
for _, c := range listener.Capabilities {
listenerFilter, err := makeEnvoyListenerFilter(c)
if err != nil {
return nil, fmt.Errorf("could not make listener filter: %w", err)
}
envoyListener.ListenerFilters = append(envoyListener.ListenerFilters, listenerFilter)
}
err := addEnvoyListenerConnectionBalanceConfig(listener.BalanceConnections, envoyListener)
if err != nil {
return nil, err
}
envoyListener.Name = listener.Name
envoyListener.Address = address
envoyListener.TrafficDirection = direction
return envoyListener, nil
}
func makeEnvoyConnectionLimitFilter(maxInboundConns uint64) (*envoy_listener_v3.Filter, error) {
cfg := &envoy_connection_limit_v3.ConnectionLimit{
StatPrefix: "inbound_connection_limit",
MaxConnections: wrapperspb.UInt64(maxInboundConns),
}
return makeEnvoyFilter("envoy.filters.network.connection_limit", cfg)
}
func addEnvoyListenerConnectionBalanceConfig(balanceType pbproxystate.BalanceConnections, listener *envoy_listener_v3.Listener) error {
switch balanceType {
case pbproxystate.BalanceConnections_BALANCE_CONNECTIONS_DEFAULT:
// Default with no balancing.
return nil
case pbproxystate.BalanceConnections_BALANCE_CONNECTIONS_EXACT:
listener.ConnectionBalanceConfig = &envoy_listener_v3.Listener_ConnectionBalanceConfig{
BalanceType: &envoy_listener_v3.Listener_ConnectionBalanceConfig_ExactBalance_{},
}
return nil
default:
// This should be impossible using protobufs.
return fmt.Errorf("unsupported connection balance option: %+v", balanceType)
}
}
func makeIpPortEnvoyAddress(address *pbproxystate.Listener_HostPort) *envoy_core_v3.Address {
return &envoy_core_v3.Address{
Address: &envoy_core_v3.Address_SocketAddress{
SocketAddress: &envoy_core_v3.SocketAddress{
Address: address.HostPort.Host,
PortSpecifier: &envoy_core_v3.SocketAddress_PortValue{
PortValue: address.HostPort.Port,
},
},
},
}
}
func makeUnixSocketEnvoyAddress(address *pbproxystate.Listener_UnixSocket) *envoy_core_v3.Address {
modeInt, err := strconv.ParseUint(address.UnixSocket.Mode, 0, 32)
if err != nil {
modeInt = 0
}
return &envoy_core_v3.Address{
Address: &envoy_core_v3.Address_Pipe{
Pipe: &envoy_core_v3.Pipe{
Path: address.UnixSocket.Path,
Mode: uint32(modeInt),
},
},
}
}
func (pr *ProxyResources) makeEnvoyListenerFilterChain(router *pbproxystate.Router) (*envoy_listener_v3.FilterChain, error) {
envoyFilterChain := &envoy_listener_v3.FilterChain{}
if router == nil {
return nil, fmt.Errorf("no router to create filter chain")
}
// Router Match
match := makeEnvoyFilterChainMatch(router.Match)
if match != nil {
envoyFilterChain.FilterChainMatch = match
}
// Router Destination
var envoyFilters []*envoy_listener_v3.Filter
switch router.Destination.(type) {
case *pbproxystate.Router_L4:
l4Filters, err := pr.makeEnvoyResourcesForL4Destination(router.Destination.(*pbproxystate.Router_L4))
if err != nil {
return nil, err
}
envoyFilters = append(envoyFilters, l4Filters...)
case *pbproxystate.Router_L7:
l7 := router.Destination.(*pbproxystate.Router_L7)
l7Filters, err := pr.makeEnvoyResourcesForL7Destination(l7)
if err != nil {
return nil, err
}
// Inject ALPN protocols to router's TLS if destination is L7
if router.InboundTls != nil {
router.InboundTls.AlpnProtocols = getAlpnProtocols(l7.L7.Protocol)
}
envoyFilters = append(envoyFilters, l7Filters...)
case *pbproxystate.Router_Sni:
sniFilters, err := pr.makeEnvoyResourcesForSNIDestination(router.Destination.(*pbproxystate.Router_Sni))
if err != nil {
return nil, err
}
envoyFilters = append(envoyFilters, sniFilters...)
default:
// This should be impossible using protobufs.
return nil, fmt.Errorf("unsupported destination type: %t", router.Destination)
}
// Router TLS
ts, err := pr.makeEnvoyTransportSocket(router.InboundTls)
if err != nil {
return nil, err
}
envoyFilterChain.TransportSocket = ts
envoyFilterChain.Filters = envoyFilters
return envoyFilterChain, err
}
func makeEnvoyFilterChainMatch(routerMatch *pbproxystate.Match) *envoy_listener_v3.FilterChainMatch {
var envoyFilterChainMatch *envoy_listener_v3.FilterChainMatch
if routerMatch != nil {
envoyFilterChainMatch = &envoy_listener_v3.FilterChainMatch{}
envoyFilterChainMatch.DestinationPort = routerMatch.DestinationPort
if len(routerMatch.ServerNames) > 0 {
var serverNames []string
for _, n := range routerMatch.ServerNames {
serverNames = append(serverNames, n)
}
envoyFilterChainMatch.ServerNames = serverNames
}
if len(routerMatch.PrefixRanges) > 0 {
sortPrefixRanges(routerMatch.PrefixRanges)
var ranges []*envoy_core_v3.CidrRange
for _, r := range routerMatch.PrefixRanges {
cidrRange := &envoy_core_v3.CidrRange{
PrefixLen: r.PrefixLen,
AddressPrefix: r.AddressPrefix,
}
ranges = append(ranges, cidrRange)
}
envoyFilterChainMatch.PrefixRanges = ranges
}
if len(routerMatch.SourcePrefixRanges) > 0 {
var ranges []*envoy_core_v3.CidrRange
for _, r := range routerMatch.SourcePrefixRanges {
cidrRange := &envoy_core_v3.CidrRange{
PrefixLen: r.PrefixLen,
AddressPrefix: r.AddressPrefix,
}
ranges = append(ranges, cidrRange)
}
envoyFilterChainMatch.SourcePrefixRanges = ranges
}
}
return envoyFilterChainMatch
}
func (pr *ProxyResources) makeEnvoyResourcesForSNIDestination(sni *pbproxystate.Router_Sni) ([]*envoy_listener_v3.Filter, error) {
var envoyFilters []*envoy_listener_v3.Filter
sniFilter, err := makeEnvoyFilter("envoy.filters.network.sni_cluster", &envoy_sni_cluster_v3.SniCluster{})
if err != nil {
return nil, err
}
tcp := &envoy_tcp_proxy_v3.TcpProxy{
StatPrefix: sni.Sni.StatPrefix,
ClusterSpecifier: &envoy_tcp_proxy_v3.TcpProxy_Cluster{Cluster: ""},
}
tcpFilter, err := makeEnvoyFilter(envoyNetworkFilterName, tcp)
if err != nil {
return nil, err
}
envoyFilters = append(envoyFilters, sniFilter, tcpFilter)
return envoyFilters, err
}
func (pr *ProxyResources) makeEnvoyResourcesForL4Destination(l4 *pbproxystate.Router_L4) ([]*envoy_listener_v3.Filter, error) {
err := pr.makeCluster(l4.L4.Name)
if err != nil {
return nil, err
}
envoyFilters, err := makeL4Filters(l4.L4)
return envoyFilters, err
}
func (pr *ProxyResources) makeEnvoyResourcesForL7Destination(l7 *pbproxystate.Router_L7) ([]*envoy_listener_v3.Filter, error) {
envoyFilters, err := pr.makeL7Filters(l7.L7)
if err != nil {
return nil, err
}
return envoyFilters, err
}
func getAlpnProtocols(protocol pbproxystate.L7Protocol) []string {
var alpnProtocols []string
switch protocol {
case pbproxystate.L7Protocol_L7_PROTOCOL_GRPC, pbproxystate.L7Protocol_L7_PROTOCOL_HTTP2:
alpnProtocols = append(alpnProtocols, "h2", "http/1.1")
case pbproxystate.L7Protocol_L7_PROTOCOL_HTTP:
alpnProtocols = append(alpnProtocols, "http/1.1")
}
return alpnProtocols
}
func makeL4Filters(l4 *pbproxystate.L4Destination) ([]*envoy_listener_v3.Filter, error) {
var envoyFilters []*envoy_listener_v3.Filter
if l4 != nil {
// Add rbac filter. RBAC filter needs to be added first so any
// unauthorized connections will get rejected.
// TODO(proxystate): Intentions will be added in the future.
if l4.AddEmptyIntention {
rbacFilter, err := makeEmptyRBACNetworkFilter()
if err != nil {
return nil, err
}
envoyFilters = append(envoyFilters, rbacFilter)
}
if l4.MaxInboundConnections > 0 {
connectionLimitFilter, err := makeEnvoyConnectionLimitFilter(l4.MaxInboundConnections)
if err != nil {
return nil, err
}
envoyFilters = append(envoyFilters, connectionLimitFilter)
}
// Add tcp proxy filter
tcp := &envoy_tcp_proxy_v3.TcpProxy{
ClusterSpecifier: &envoy_tcp_proxy_v3.TcpProxy_Cluster{Cluster: l4.Name},
StatPrefix: l4.StatPrefix,
}
tcpFilter, err := makeEnvoyFilter(envoyNetworkFilterName, tcp)
if err != nil {
return nil, err
}
envoyFilters = append(envoyFilters, tcpFilter)
}
return envoyFilters, nil
}
func makeEmptyRBACNetworkFilter() (*envoy_listener_v3.Filter, error) {
cfg := &envoy_network_rbac_v3.RBAC{
StatPrefix: "connect_authz",
Rules: &envoy_rbac_v3.RBAC{},
}
filter, err := makeEnvoyFilter("envoy.filters.network.rbac", cfg)
if err != nil {
return nil, err
}
return filter, nil
}
// TODO: Forward client cert details will be added as part of L7 listeners task.
func (pr *ProxyResources) makeL7Filters(l7 *pbproxystate.L7Destination) ([]*envoy_listener_v3.Filter, error) {
var envoyFilters []*envoy_listener_v3.Filter
var httpConnMgr *envoy_http_v3.HttpConnectionManager
if l7 != nil {
// TODO: Intentions will be added in the future.
if l7.MaxInboundConnections > 0 {
connLimitFilter, err := makeEnvoyConnectionLimitFilter(l7.MaxInboundConnections)
if err != nil {
return nil, err
}
envoyFilters = append(envoyFilters, connLimitFilter)
}
envoyHttpRouter, err := makeEnvoyHTTPFilter("envoy.filters.http.router", &envoy_http_router_v3.Router{})
if err != nil {
return nil, err
}
httpConnMgr = &envoy_http_v3.HttpConnectionManager{
StatPrefix: l7.StatPrefix,
CodecType: envoy_http_v3.HttpConnectionManager_AUTO,
HttpFilters: []*envoy_http_v3.HttpFilter{
envoyHttpRouter,
},
Tracing: &envoy_http_v3.HttpConnectionManager_Tracing{
// Don't trace any requests by default unless the client application
// explicitly propagates trace headers that indicate this should be
// sampled.
RandomSampling: &envoy_type_v3.Percent{Value: 0.0},
},
// Explicitly enable WebSocket upgrades for all HTTP listeners
UpgradeConfigs: []*envoy_http_v3.HttpConnectionManager_UpgradeConfig{
{UpgradeType: "websocket"},
},
}
routeConfig, err := pr.makeRoute(l7.Name)
if err != nil {
return nil, err
}
if l7.StaticRoute {
httpConnMgr.RouteSpecifier = &envoy_http_v3.HttpConnectionManager_RouteConfig{
RouteConfig: routeConfig,
}
} else {
// Add Envoy route under the route resource since it's not inlined.
pr.envoyResources[xdscommon.RouteType] = append(pr.envoyResources[xdscommon.RouteType], routeConfig)
httpConnMgr.RouteSpecifier = &envoy_http_v3.HttpConnectionManager_Rds{
Rds: &envoy_http_v3.Rds{
RouteConfigName: l7.Name,
ConfigSource: &envoy_core_v3.ConfigSource{
ResourceApiVersion: envoy_core_v3.ApiVersion_V3,
ConfigSourceSpecifier: &envoy_core_v3.ConfigSource_Ads{
Ads: &envoy_core_v3.AggregatedConfigSource{},
},
},
},
}
}
// Add http2 protocol options
if l7.Protocol == pbproxystate.L7Protocol_L7_PROTOCOL_HTTP2 || l7.Protocol == pbproxystate.L7Protocol_L7_PROTOCOL_GRPC {
httpConnMgr.Http2ProtocolOptions = &envoy_core_v3.Http2ProtocolOptions{}
}
// Add grpc envoy http filters.
if l7.Protocol == pbproxystate.L7Protocol_L7_PROTOCOL_GRPC {
grpcHttp1Bridge, err := makeEnvoyHTTPFilter(
"envoy.filters.http.grpc_http1_bridge",
&envoy_grpc_http1_bridge_v3.Config{},
)
if err != nil {
return nil, err
}
// In envoy 1.14.x the default value "stats_for_all_methods=true" was
// deprecated, and was changed to "false" in 1.18.x. Avoid using the
// default. TODO: we may want to expose this to users somehow easily.
grpcStatsFilter, err := makeEnvoyHTTPFilter(
"envoy.filters.http.grpc_stats",
&envoy_grpc_stats_v3.FilterConfig{
PerMethodStatSpecifier: &envoy_grpc_stats_v3.FilterConfig_StatsForAllMethods{
StatsForAllMethods: &wrapperspb.BoolValue{Value: true},
},
},
)
if err != nil {
return nil, err
}
// Add grpc bridge before envoyRouter and authz, and the stats in front of that.
httpConnMgr.HttpFilters = append([]*envoy_http_v3.HttpFilter{
grpcStatsFilter,
grpcHttp1Bridge,
}, httpConnMgr.HttpFilters...)
}
httpFilter, err := makeEnvoyFilter(envoyHttpConnectionManagerFilterName, httpConnMgr)
if err != nil {
return nil, err
}
envoyFilters = append(envoyFilters, httpFilter)
}
return envoyFilters, nil
}
func (pr *ProxyResources) makeEnvoyTLSParameters(defaultParams *pbproxystate.TLSParameters, overrideParams *pbproxystate.TLSParameters) *envoy_tls_v3.TlsParameters {
tlsParams := &envoy_tls_v3.TlsParameters{}
if overrideParams != nil {
if overrideParams.MinVersion != pbproxystate.TLSVersion_TLS_VERSION_UNSPECIFIED {
if minVersion, ok := envoyTLSVersions[overrideParams.MinVersion]; ok {
tlsParams.TlsMinimumProtocolVersion = minVersion
}
}
if overrideParams.MaxVersion != pbproxystate.TLSVersion_TLS_VERSION_UNSPECIFIED {
if maxVersion, ok := envoyTLSVersions[overrideParams.MaxVersion]; ok {
tlsParams.TlsMaximumProtocolVersion = maxVersion
}
}
if len(overrideParams.CipherSuites) != 0 {
tlsParams.CipherSuites = marshalEnvoyTLSCipherSuiteStrings(overrideParams.CipherSuites)
}
return tlsParams
}
if defaultParams != nil {
if defaultParams.MinVersion != pbproxystate.TLSVersion_TLS_VERSION_UNSPECIFIED {
if minVersion, ok := envoyTLSVersions[defaultParams.MinVersion]; ok {
tlsParams.TlsMinimumProtocolVersion = minVersion
}
}
if defaultParams.MaxVersion != pbproxystate.TLSVersion_TLS_VERSION_UNSPECIFIED {
if maxVersion, ok := envoyTLSVersions[defaultParams.MaxVersion]; ok {
tlsParams.TlsMaximumProtocolVersion = maxVersion
}
}
if len(defaultParams.CipherSuites) != 0 {
tlsParams.CipherSuites = marshalEnvoyTLSCipherSuiteStrings(defaultParams.CipherSuites)
}
return tlsParams
}
return tlsParams
}
func (pr *ProxyResources) makeEnvoyTransportSocket(ts *pbproxystate.TransportSocket) (*envoy_core_v3.TransportSocket, error) {
if ts == nil {
return nil, nil
}
commonTLSContext := &envoy_tls_v3.CommonTlsContext{}
// Create connection TLS. Listeners should only look at inbound TLS.
switch ts.ConnectionTls.(type) {
case *pbproxystate.TransportSocket_InboundMesh:
downstreamContext := &envoy_tls_v3.DownstreamTlsContext{}
downstreamContext.CommonTlsContext = commonTLSContext
// Set TLS Parameters.
tlsParams := pr.makeEnvoyTLSParameters(pr.proxyState.Tls.InboundTlsParameters, ts.TlsParameters)
commonTLSContext.TlsParams = tlsParams
// Set the certificate config on the tls context.
// For inbound mesh, we need to add the identity certificate
// and the validation context for the mesh depending on the provided trust bundle names.
if pr.proxyState.Tls == nil {
// if tls is nil but connection tls is provided, then the proxy state is misconfigured
return nil, fmt.Errorf("proxyState.Tls is required to generate router's transport socket")
}
im := ts.ConnectionTls.(*pbproxystate.TransportSocket_InboundMesh).InboundMesh
leaf, ok := pr.proxyState.LeafCertificates[im.IdentityKey]
if !ok {
return nil, fmt.Errorf("failed to create transport socket: leaf certificate %q not found", im.IdentityKey)
}
err := pr.makeEnvoyCertConfig(commonTLSContext, leaf)
if err != nil {
return nil, fmt.Errorf("failed to create transport socket: %w", err)
}
// Create validation context.
// When there's only one trust bundle name, we create a simple validation context
if len(im.ValidationContext.TrustBundlePeerNameKeys) == 1 {
peerName := im.ValidationContext.TrustBundlePeerNameKeys[0]
tb, ok := pr.proxyState.TrustBundles[peerName]
if !ok {
return nil, fmt.Errorf("failed to create transport socket: provided trust bundle name does not exist in proxystate trust bundle map: %s", peerName)
}
commonTLSContext.ValidationContextType = &envoy_tls_v3.CommonTlsContext_ValidationContext{
ValidationContext: &envoy_tls_v3.CertificateValidationContext{
// TODO(banks): later for L7 support we may need to configure ALPN here.
TrustedCa: &envoy_core_v3.DataSource{
Specifier: &envoy_core_v3.DataSource_InlineString{
InlineString: RootPEMsAsString(tb.Roots),
},
},
},
}
} else if len(im.ValidationContext.TrustBundlePeerNameKeys) > 1 {
cfg := &envoy_tls_v3.SPIFFECertValidatorConfig{
TrustDomains: make([]*envoy_tls_v3.SPIFFECertValidatorConfig_TrustDomain, 0, len(im.ValidationContext.TrustBundlePeerNameKeys)),
}
for _, peerName := range im.ValidationContext.TrustBundlePeerNameKeys {
// Look up the trust bundle ca in the map.
tb, ok := pr.proxyState.TrustBundles[peerName]
if !ok {
return nil, fmt.Errorf("failed to create transport socket: provided bundle name does not exist in trust bundle map: %s", peerName)
}
cfg.TrustDomains = append(cfg.TrustDomains, &envoy_tls_v3.SPIFFECertValidatorConfig_TrustDomain{
Name: tb.TrustDomain,
TrustBundle: &envoy_core_v3.DataSource{
Specifier: &envoy_core_v3.DataSource_InlineString{
InlineString: RootPEMsAsString(tb.Roots),
},
},
})
}
// Sort the trust domains so the output is stable.
sortTrustDomains(cfg.TrustDomains)
spiffeConfig, err := anypb.New(cfg)
if err != nil {
return nil, err
}
commonTLSContext.ValidationContextType = &envoy_tls_v3.CommonTlsContext_ValidationContext{
ValidationContext: &envoy_tls_v3.CertificateValidationContext{
CustomValidatorConfig: &envoy_core_v3.TypedExtensionConfig{
// The typed config name is hard-coded because it is not available as a wellknown var in the control plane lib.
Name: "envoy.tls.cert_validator.spiffe",
TypedConfig: spiffeConfig,
},
},
}
}
// Always require client certificate
downstreamContext.RequireClientCertificate = &wrapperspb.BoolValue{Value: true}
transportSocket, err := makeTransportSocket("tls", downstreamContext)
if err != nil {
return nil, err
}
return transportSocket, nil
case *pbproxystate.TransportSocket_InboundNonMesh:
downstreamContext := &envoy_tls_v3.DownstreamTlsContext{}
downstreamContext.CommonTlsContext = commonTLSContext
// Set TLS Parameters
tlsParams := pr.makeEnvoyTLSParameters(pr.proxyState.Tls.InboundTlsParameters, ts.TlsParameters)
commonTLSContext.TlsParams = tlsParams
// For non-mesh, we don't care about validation context as currently we don't support mTLS for non-mesh connections.
nonMeshTLS := ts.ConnectionTls.(*pbproxystate.TransportSocket_InboundNonMesh).InboundNonMesh
err := pr.addNonMeshCertConfig(commonTLSContext, nonMeshTLS)
if err != nil {
return nil, fmt.Errorf("failed to create transport socket: %w", err)
}
transportSocket, err := makeTransportSocket("tls", downstreamContext)
if err != nil {
return nil, err
}
return transportSocket, nil
case *pbproxystate.TransportSocket_OutboundMesh:
upstreamContext := &envoy_tls_v3.UpstreamTlsContext{}
upstreamContext.CommonTlsContext = commonTLSContext
// Set TLS Parameters
tlsParams := pr.makeEnvoyTLSParameters(pr.proxyState.Tls.OutboundTlsParameters, ts.TlsParameters)
commonTLSContext.TlsParams = tlsParams
// For outbound mesh, we need to insert the mesh identity certificate
// and the validation context for the mesh depending on the provided trust bundle names.
if pr.proxyState.Tls == nil {
// if tls is nil but connection tls is provided, then the proxy state is misconfigured
return nil, fmt.Errorf("proxyState.Tls is required to generate router's transport socket")
}
om := ts.ConnectionTls.(*pbproxystate.TransportSocket_OutboundMesh).OutboundMesh
leaf, ok := pr.proxyState.LeafCertificates[om.IdentityKey]
if !ok {
return nil, fmt.Errorf("leaf %s not found in proxyState", om.IdentityKey)
}
err := pr.makeEnvoyCertConfig(commonTLSContext, leaf)
if err != nil {
return nil, fmt.Errorf("failed to create transport socket: %w", err)
}
// Create validation context
peerName := om.ValidationContext.TrustBundlePeerNameKey
tb, ok := pr.proxyState.TrustBundles[peerName]
if !ok {
return nil, fmt.Errorf("failed to create transport socket: provided peer name does not exist in trust bundle map: %s", peerName)
}
var matchers []*envoy_matcher_v3.StringMatcher
if len(om.ValidationContext.SpiffeIds) > 0 {
matchers = make([]*envoy_matcher_v3.StringMatcher, 0)
for _, m := range om.ValidationContext.SpiffeIds {
matchers = append(matchers, &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{
Exact: m,
},
})
}
}
commonTLSContext.ValidationContextType = &envoy_tls_v3.CommonTlsContext_ValidationContext{
ValidationContext: &envoy_tls_v3.CertificateValidationContext{
// TODO(banks): later for L7 support we may need to configure ALPN here.
TrustedCa: &envoy_core_v3.DataSource{
Specifier: &envoy_core_v3.DataSource_InlineString{
InlineString: RootPEMsAsString(tb.Roots),
},
},
MatchSubjectAltNames: matchers,
},
}
upstreamContext.Sni = om.Sni
transportSocket, err := makeTransportSocket("tls", upstreamContext)
if err != nil {
return nil, err
}
return transportSocket, nil
default:
return nil, nil
}
}
func (pr *ProxyResources) makeEnvoyCertConfig(common *envoy_tls_v3.CommonTlsContext, certificate *pbproxystate.LeafCertificate) error {
if certificate == nil {
return fmt.Errorf("no leaf certificate provided")
}
common.TlsCertificates = []*envoy_tls_v3.TlsCertificate{
{
CertificateChain: &envoy_core_v3.DataSource{
Specifier: &envoy_core_v3.DataSource_InlineString{
InlineString: lib.EnsureTrailingNewline(certificate.Cert),
},
},
PrivateKey: &envoy_core_v3.DataSource{
Specifier: &envoy_core_v3.DataSource_InlineString{
InlineString: lib.EnsureTrailingNewline(certificate.Key),
},
},
},
}
return nil
}
func (pr *ProxyResources) makeEnvoySDSCertConfig(common *envoy_tls_v3.CommonTlsContext, certificate *pbproxystate.SDSCertificate) error {
if certificate == nil {
return fmt.Errorf("no SDS certificate provided")
}
common.TlsCertificateSdsSecretConfigs = []*envoy_tls_v3.SdsSecretConfig{
{
Name: certificate.CertResource,
SdsConfig: &envoy_core_v3.ConfigSource{
ConfigSourceSpecifier: &envoy_core_v3.ConfigSource_ApiConfigSource{
ApiConfigSource: &envoy_core_v3.ApiConfigSource{
ApiType: envoy_core_v3.ApiConfigSource_GRPC,
TransportApiVersion: envoy_core_v3.ApiVersion_V3,
// Note ClusterNames can't be set here - that's only for REST type
// we need a full GRPC config instead.
GrpcServices: []*envoy_core_v3.GrpcService{
{
TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{
ClusterName: certificate.ClusterName,
},
},
Timeout: &durationpb.Duration{Seconds: 5},
},
},
},
},
ResourceApiVersion: envoy_core_v3.ApiVersion_V3,
},
},
}
return nil
}
func (pr *ProxyResources) addNonMeshCertConfig(common *envoy_tls_v3.CommonTlsContext, tls *pbproxystate.InboundNonMeshTLS) error {
if tls == nil {
return fmt.Errorf("no inbound non-mesh TLS provided")
}
switch tls.Identity.(type) {
case *pbproxystate.InboundNonMeshTLS_LeafKey:
leafKey := tls.Identity.(*pbproxystate.InboundNonMeshTLS_LeafKey).LeafKey
leaf, ok := pr.proxyState.LeafCertificates[leafKey]
if !ok {
return fmt.Errorf("leaf key %s not found in leaf certificate map", leafKey)
}
common.TlsCertificates = []*envoy_tls_v3.TlsCertificate{
{
CertificateChain: &envoy_core_v3.DataSource{
Specifier: &envoy_core_v3.DataSource_InlineString{
InlineString: lib.EnsureTrailingNewline(leaf.Cert),
},
},
PrivateKey: &envoy_core_v3.DataSource{
Specifier: &envoy_core_v3.DataSource_InlineString{
InlineString: lib.EnsureTrailingNewline(leaf.Key),
},
},
},
}
case *pbproxystate.InboundNonMeshTLS_Sds:
c := tls.Identity.(*pbproxystate.InboundNonMeshTLS_Sds).Sds
common.TlsCertificateSdsSecretConfigs = []*envoy_tls_v3.SdsSecretConfig{
{
Name: c.CertResource,
SdsConfig: &envoy_core_v3.ConfigSource{
ConfigSourceSpecifier: &envoy_core_v3.ConfigSource_ApiConfigSource{
ApiConfigSource: &envoy_core_v3.ApiConfigSource{
ApiType: envoy_core_v3.ApiConfigSource_GRPC,
TransportApiVersion: envoy_core_v3.ApiVersion_V3,
// Note ClusterNames can't be set here - that's only for REST type
// we need a full GRPC config instead.
GrpcServices: []*envoy_core_v3.GrpcService{
{
TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{
ClusterName: c.ClusterName,
},
},
Timeout: &durationpb.Duration{Seconds: 5},
},
},
},
},
ResourceApiVersion: envoy_core_v3.ApiVersion_V3,
},
},
}
}
return nil
}
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
}
func makeEnvoyListenerFilter(c pbproxystate.Capability) (*envoy_listener_v3.ListenerFilter, error) {
var lf proto.Message
var name string
switch c {
case pbproxystate.Capability_CAPABILITY_TRANSPARENT:
lf = &envoy_original_dst_v3.OriginalDst{}
name = envoyOriginalDestinationListenerFilterName
case pbproxystate.Capability_CAPABILITY_L4_TLS_INSPECTION:
name = envoyTLSInspectorListenerFilterName
lf = &envoy_tls_inspector_v3.TlsInspector{}
case pbproxystate.Capability_CAPABILITY_L7_PROTOCOL_INSPECTION:
name = envoyHttpInspectorListenerFilterName
lf = &envoy_extensions_filters_listener_http_inspector_v3.HttpInspector{}
default:
return nil, fmt.Errorf("unsupported listener captability: %s", c)
}
lfAsAny, err := anypb.New(lf)
if err != nil {
return nil, err
}
return &envoy_listener_v3.ListenerFilter{
Name: name,
ConfigType: &envoy_listener_v3.ListenerFilter_TypedConfig{TypedConfig: lfAsAny},
}, nil
}
func makeEnvoyFilter(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
}
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
}
func RootPEMsAsString(rootPEMs []string) string {
var rootPEMsString string
for _, root := range rootPEMs {
rootPEMsString += lib.EnsureTrailingNewline(root)
}
return rootPEMsString
}
func marshalEnvoyTLSCipherSuiteStrings(cipherSuites []pbproxystate.TLSCipherSuite) []string {
envoyTLSCipherSuiteStrings := map[pbproxystate.TLSCipherSuite]string{
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_ECDHE_ECDSA_AES128_GCM_SHA256: "ECDHE-ECDSA-AES128-GCM-SHA256",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_ECDHE_ECDSA_CHACHA20_POLY1305: "ECDHE-ECDSA-CHACHA20-POLY1305",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_ECDHE_RSA_AES128_GCM_SHA256: "ECDHE-RSA-AES128-GCM-SHA256",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_ECDHE_RSA_CHACHA20_POLY1305: "ECDHE-RSA-CHACHA20-POLY1305",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_ECDHE_ECDSA_AES128_SHA: "ECDHE-ECDSA-AES128-SHA",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_ECDHE_RSA_AES128_SHA: "ECDHE-RSA-AES128-SHA",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_AES128_GCM_SHA256: "AES128-GCM-SHA256",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_AES128_SHA: "AES128-SHA",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_ECDHE_ECDSA_AES256_GCM_SHA384: "ECDHE-ECDSA-AES256-GCM-SHA384",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_ECDHE_RSA_AES256_GCM_SHA384: "ECDHE-RSA-AES256-GCM-SHA384",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_ECDHE_ECDSA_AES256_SHA: "ECDHE-ECDSA-AES256-SHA",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_ECDHE_RSA_AES256_SHA: "ECDHE-RSA-AES256-SHA",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_AES256_GCM_SHA384: "AES256-GCM-SHA384",
pbproxystate.TLSCipherSuite_TLS_CIPHER_SUITE_AES256_SHA: "AES256-SHA",
}
var cipherSuiteStrings []string
for _, c := range cipherSuites {
if s, ok := envoyTLSCipherSuiteStrings[c]; ok {
cipherSuiteStrings = append(cipherSuiteStrings, s)
}
}
return cipherSuiteStrings
}
var envoyTLSVersions = map[pbproxystate.TLSVersion]envoy_tls_v3.TlsParameters_TlsProtocol{
pbproxystate.TLSVersion_TLS_VERSION_AUTO: envoy_tls_v3.TlsParameters_TLS_AUTO,
pbproxystate.TLSVersion_TLS_VERSION_1_0: envoy_tls_v3.TlsParameters_TLSv1_0,
pbproxystate.TLSVersion_TLS_VERSION_1_1: envoy_tls_v3.TlsParameters_TLSv1_1,
pbproxystate.TLSVersion_TLS_VERSION_1_2: envoy_tls_v3.TlsParameters_TLSv1_2,
pbproxystate.TLSVersion_TLS_VERSION_1_3: envoy_tls_v3.TlsParameters_TLSv1_3,
}
// Sort the trust domains so that the output is stable.
// This benefits tests but also prevents Envoy from mistakenly thinking the listener
// changed and needs to be drained only because this ordering is different.
func sortTrustDomains(trustDomains []*envoy_tls_v3.SPIFFECertValidatorConfig_TrustDomain) {
sort.Slice(trustDomains, func(i int, j int) bool {
return trustDomains[i].Name < trustDomains[j].Name
})
}
// sortRouters stable sorts routers with a Match to avoid draining if the list is provided out of order.
// xdsv1 used to sort the filter chains on outbound listeners, so this adds that functionality by sorting routers with matches.
func sortRouters(routers []*pbproxystate.Router) {
if routers == nil {
return
}
sort.SliceStable(routers, func(i, j int) bool {
si := ""
sj := ""
if routers[i].Match != nil {
if len(routers[i].Match.PrefixRanges) > 0 {
si += routers[i].Match.PrefixRanges[0].AddressPrefix +
"/" + routers[i].Match.PrefixRanges[0].PrefixLen.String() +
":" + routers[i].Match.DestinationPort.String()
}
if len(routers[i].Match.ServerNames) > 0 {
si += routers[i].Match.ServerNames[0] +
":" + routers[i].Match.DestinationPort.String()
} else {
si += routers[i].Match.DestinationPort.String()
}
}
if routers[j].Match != nil {
if len(routers[j].Match.PrefixRanges) > 0 {
sj += routers[j].Match.PrefixRanges[0].AddressPrefix +
"/" + routers[j].Match.PrefixRanges[0].PrefixLen.String() +
":" + routers[j].Match.DestinationPort.String()
}
if len(routers[j].Match.ServerNames) > 0 {
sj += routers[j].Match.ServerNames[0] +
":" + routers[j].Match.DestinationPort.String()
} else {
sj += routers[j].Match.DestinationPort.String()
}
}
return si < sj
})
}
func sortPrefixRanges(prefixRanges []*pbproxystate.CidrRange) {
if prefixRanges == nil {
return
}
sort.SliceStable(prefixRanges, func(i, j int) bool {
return prefixRanges[i].AddressPrefix < prefixRanges[j].AddressPrefix
})
}

69
agent/xdsv2/resources.go Normal file
View File

@ -0,0 +1,69 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package xdsv2
import (
"fmt"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v1alpha1"
"github.com/hashicorp/go-hclog"
"google.golang.org/protobuf/proto"
"github.com/hashicorp/consul/envoyextensions/xdscommon"
)
// ResourceGenerator is associated with a single gRPC stream and creates xDS
// resources for a single client.
type ResourceGenerator struct {
Logger hclog.Logger
ProxyFeatures xdscommon.SupportedProxyFeatures
}
func NewResourceGenerator(
logger hclog.Logger,
) *ResourceGenerator {
return &ResourceGenerator{
Logger: logger,
}
}
type ProxyResources struct {
proxyState *pbmesh.ProxyState
envoyResources map[string][]proto.Message
}
func (g *ResourceGenerator) AllResourcesFromIR(proxyState *pbmesh.ProxyState) (map[string][]proto.Message, error) {
pr := &ProxyResources{
proxyState: proxyState,
envoyResources: make(map[string][]proto.Message),
}
err := pr.generateXDSResources()
if err != nil {
return nil, fmt.Errorf("failed to generate xDS resources for ProxyState: %v", err)
}
return pr.envoyResources, nil
}
func (pr *ProxyResources) generateXDSResources() error {
listeners := make([]proto.Message, 0)
clusters := make([]proto.Message, 0)
routes := make([]proto.Message, 0)
endpoints := make([]proto.Message, 0)
for _, l := range pr.proxyState.Listeners {
protoListener, err := pr.makeListener(l)
// TODO: aggregate errors for listeners and still return any properly formed listeners.
if err != nil {
return err
}
listeners = append(listeners, protoListener)
}
pr.envoyResources[xdscommon.ListenerType] = listeners
pr.envoyResources[xdscommon.ClusterType] = clusters
pr.envoyResources[xdscommon.RouteType] = routes
pr.envoyResources[xdscommon.EndpointType] = endpoints
return nil
}

View File

@ -0,0 +1,20 @@
package xdsv2
import (
"fmt"
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
)
func (pr *ProxyResources) makeRoute(name string) (*envoy_route_v3.RouteConfiguration, error) {
var route *envoy_route_v3.RouteConfiguration
// TODO(proxystate): This will make routes in the future. This function should distinguish between static routes
// inlined into listeners and non-static routes that should be added as top level Envoy resources.
_, ok := pr.proxyState.Routes[name]
if !ok {
// This should not happen with a valid proxy state.
return nil, fmt.Errorf("could not find route in ProxyState: %s", name)
}
return route, nil
}

View File

@ -105,8 +105,7 @@ type ProxyState struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// id is this proxy's identity. This should correspond to the workload identity that this proxy of
// the workload this proxy represents.
// identity is a reference to the WorkloadIdentity associated with this proxy.
Identity *pbresource.Reference `protobuf:"bytes,1,opt,name=identity,proto3" json:"identity,omitempty"`
// listeners is a list of listeners for this proxy.
Listeners []*pbproxystate.Listener `protobuf:"bytes,2,rep,name=listeners,proto3" json:"listeners,omitempty"`

View File

@ -30,8 +30,7 @@ message ProxyStateTemplate {
}
message ProxyState {
// id is this proxy's identity. This should correspond to the workload identity that this proxy of
// the workload this proxy represents.
// identity is a reference to the WorkloadIdentity associated with this proxy.
hashicorp.consul.resource.Reference identity = 1;
// listeners is a list of listeners for this proxy.
repeated pbproxystate.Listener listeners = 2;

View File

@ -210,7 +210,7 @@ function assert_envoy_expose_checks_listener_count {
RANGES=$(echo "$BODY" | jq '.active_state.listener.filter_chains[0].filter_chain_match.source_prefix_ranges | length')
echo "RANGES = $RANGES (expect 3)"
# note: if IPv6 is not supported in the kernel per
# agent/xds:kernelSupportsIPv6() then this will only be 2
# agent/xds/platform:SupportsIPv6() then this will only be 2
[ "${RANGES:-0}" -eq 3 ]
HCM=$(echo "$BODY" | jq '.active_state.listener.filter_chains[0].filters[0]')

View File

@ -255,7 +255,7 @@ function assert_envoy_expose_checks_listener_count {
RANGES=$(echo "$BODY" | jq '.active_state.listener.filter_chains[0].filter_chain_match.source_prefix_ranges | length')
echo "RANGES = $RANGES (expect 3)"
# note: if IPv6 is not supported in the kernel per
# agent/xds:kernelSupportsIPv6() then this will only be 2
# agent/xds/platform:SupportsIPv6() then this will only be 2
[ "${RANGES:-0}" -eq 3 ]
HCM=$(echo "$BODY" | jq '.active_state.listener.filter_chains[0].filters[0]')