Add traffic permissions integration tests. (#19008)

Add traffic permissions integration tests.
This commit is contained in:
Eric Haberkorn 2023-10-06 12:06:12 -04:00 committed by GitHub
parent ed882e2522
commit ad3aab1ef7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 798 additions and 30 deletions

View File

@ -384,7 +384,7 @@ jobs:
contents: read contents: read
env: env:
ENVOY_VERSION: "1.25.4" ENVOY_VERSION: "1.25.4"
CONSUL_DATAPLANE_IMAGE: "docker.io/hashicorppreview/consul-dataplane:1.3-dev" CONSUL_DATAPLANE_IMAGE: "docker.io/hashicorppreview/consul-dataplane:1.3-dev-ubi"
steps: steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
# NOTE: This step is specifically needed for ENT. It allows us to access the required private HashiCorp repos. # NOTE: This step is specifically needed for ENT. It allows us to access the required private HashiCorp repos.
@ -417,7 +417,7 @@ jobs:
if: steps.buildConsulEnvoyImage.outcome == 'failure' if: steps.buildConsulEnvoyImage.outcome == 'failure'
run: docker build -t consul-envoy:target-version --build-arg CONSUL_IMAGE=${{ env.CONSUL_LATEST_IMAGE_NAME }}:local --build-arg ENVOY_VERSION=${{ env.ENVOY_VERSION }} -f ./test/integration/consul-container/assets/Dockerfile-consul-envoy ./test/integration/consul-container/assets run: docker build -t consul-envoy:target-version --build-arg CONSUL_IMAGE=${{ env.CONSUL_LATEST_IMAGE_NAME }}:local --build-arg ENVOY_VERSION=${{ env.ENVOY_VERSION }} -f ./test/integration/consul-container/assets/Dockerfile-consul-envoy ./test/integration/consul-container/assets
- name: Build consul-dataplane:local image - name: Build consul-dataplane:local image
run: docker build -t consul-dataplane:local --build-arg CONSUL_DATAPLANE_IMAGE=${{ env.CONSUL_DATAPLANE_IMAGE }} -f ./test/integration/consul-container/assets/Dockerfile-consul-dataplane ./test/integration/consul-container/assets run: docker build -t consul-dataplane:local --build-arg CONSUL_IMAGE=${{ env.CONSUL_LATEST_IMAGE_NAME }}:local --build-arg CONSUL_DATAPLANE_IMAGE=${{ env.CONSUL_DATAPLANE_IMAGE }} -f ./test/integration/consul-container/assets/Dockerfile-consul-dataplane ./test/integration/consul-container/assets
- name: Configure GH workaround for ipv6 loopback - name: Configure GH workaround for ipv6 loopback
if: ${{ !endsWith(github.repository, '-enterprise') }} if: ${{ !endsWith(github.repository, '-enterprise') }}
run: | run: |

View File

@ -66,7 +66,7 @@ UI_BUILD_TAG?=consul-build-ui
BUILD_CONTAINER_NAME?=consul-builder BUILD_CONTAINER_NAME?=consul-builder
CONSUL_IMAGE_VERSION?=latest CONSUL_IMAGE_VERSION?=latest
ENVOY_VERSION?='1.25.4' ENVOY_VERSION?='1.25.4'
CONSUL_DATAPLANE_IMAGE := $(or $(CONSUL_DATAPLANE_IMAGE),"docker.io/hashicorppreview/consul-dataplane:1.3-dev") CONSUL_DATAPLANE_IMAGE := $(or $(CONSUL_DATAPLANE_IMAGE),"docker.io/hashicorppreview/consul-dataplane:1.3-dev-ubi")
CONSUL_VERSION?=$(shell cat version/VERSION) CONSUL_VERSION?=$(shell cat version/VERSION)
@ -349,7 +349,7 @@ test-compat-integ-setup: dev-docker
@docker run --rm -t $(CONSUL_COMPAT_TEST_IMAGE):local consul version @docker run --rm -t $(CONSUL_COMPAT_TEST_IMAGE):local consul version
@# 'consul-envoy:target-version' is needed by compatibility integ test @# 'consul-envoy:target-version' is needed by compatibility integ test
@docker build -t consul-envoy:target-version --build-arg CONSUL_IMAGE=$(CONSUL_COMPAT_TEST_IMAGE):local --build-arg ENVOY_VERSION=${ENVOY_VERSION} -f ./test/integration/consul-container/assets/Dockerfile-consul-envoy ./test/integration/consul-container/assets @docker build -t consul-envoy:target-version --build-arg CONSUL_IMAGE=$(CONSUL_COMPAT_TEST_IMAGE):local --build-arg ENVOY_VERSION=${ENVOY_VERSION} -f ./test/integration/consul-container/assets/Dockerfile-consul-envoy ./test/integration/consul-container/assets
@docker build -t consul-dataplane:local --build-arg CONSUL_DATAPLANE_IMAGE=${CONSUL_DATAPLANE_IMAGE} -f ./test/integration/consul-container/assets/Dockerfile-consul-dataplane ./test/integration/consul-container/assets @docker build -t consul-dataplane:local --build-arg CONSUL_IMAGE=$(CONSUL_COMPAT_TEST_IMAGE):local --build-arg CONSUL_DATAPLANE_IMAGE=${CONSUL_DATAPLANE_IMAGE} -f ./test/integration/consul-container/assets/Dockerfile-consul-dataplane ./test/integration/consul-container/assets
.PHONY: test-compat-integ .PHONY: test-compat-integ
test-compat-integ: test-compat-integ-setup ## Test compat integ test-compat-integ: test-compat-integ-setup ## Test compat integ

View File

@ -4,6 +4,7 @@
package resourcetest package resourcetest
import ( import (
"context"
"strings" "strings"
"github.com/oklog/ulid/v2" "github.com/oklog/ulid/v2"
@ -134,7 +135,14 @@ func (b *resourceBuilder) ReferenceNoSection() *pbresource.Reference {
func (b *resourceBuilder) Write(t T, client pbresource.ResourceServiceClient) *pbresource.Resource { func (b *resourceBuilder) Write(t T, client pbresource.ResourceServiceClient) *pbresource.Resource {
t.Helper() t.Helper()
ctx := testutil.TestContext(t) var ctx context.Context
rtestClient, ok := client.(*Client)
if ok {
ctx = rtestClient.Context(t)
} else {
ctx = testutil.TestContext(t)
rtestClient = NewClient(client)
}
res := b.resource res := b.resource
@ -170,7 +178,7 @@ func (b *resourceBuilder) Write(t T, client pbresource.ResourceServiceClient) *p
id := proto.Clone(rsp.Resource.Id).(*pbresource.ID) id := proto.Clone(rsp.Resource.Id).(*pbresource.ID)
id.Uid = "" id.Uid = ""
t.Cleanup(func() { t.Cleanup(func() {
NewClient(client).MustDelete(t, id) rtestClient.MustDelete(t, id)
}) })
} }

View File

@ -4,6 +4,7 @@
package resourcetest package resourcetest
import ( import (
"context"
"fmt" "fmt"
"math/rand" "math/rand"
"time" "time"
@ -11,6 +12,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/resource"
@ -24,13 +26,19 @@ type Client struct {
timeout time.Duration timeout time.Duration
wait time.Duration wait time.Duration
token string
} }
func NewClient(client pbresource.ResourceServiceClient) *Client { func NewClient(client pbresource.ResourceServiceClient) *Client {
return NewClientWithACLToken(client, "")
}
func NewClientWithACLToken(client pbresource.ResourceServiceClient, token string) *Client {
return &Client{ return &Client{
ResourceServiceClient: client, ResourceServiceClient: client,
timeout: 7 * time.Second, timeout: 7 * time.Second,
wait: 25 * time.Millisecond, wait: 25 * time.Millisecond,
token: token,
} }
} }
@ -46,7 +54,7 @@ func (client *Client) retry(t T, fn func(r *retry.R)) {
} }
func (client *Client) PublishResources(t T, resources []*pbresource.Resource) { func (client *Client) PublishResources(t T, resources []*pbresource.Resource) {
ctx := testutil.TestContext(t) ctx := client.Context(t)
// Randomize the order of insertion. Generally insertion order shouldn't matter as the // Randomize the order of insertion. Generally insertion order shouldn't matter as the
// controllers should eventually converge on the desired state. The exception to this // controllers should eventually converge on the desired state. The exception to this
@ -111,10 +119,23 @@ func (client *Client) PublishResources(t T, resources []*pbresource.Resource) {
require.Empty(t, resources, "Could not publish all resources - some resources have invalid owner references") require.Empty(t, resources, "Could not publish all resources - some resources have invalid owner references")
} }
func (client *Client) Context(t T) context.Context {
ctx := testutil.TestContext(t)
if client.token != "" {
md := metadata.New(map[string]string{
"x-consul-token": client.token,
})
ctx = metadata.NewOutgoingContext(ctx, md)
}
return ctx
}
func (client *Client) RequireResourceNotFound(t T, id *pbresource.ID) { func (client *Client) RequireResourceNotFound(t T, id *pbresource.ID) {
t.Helper() t.Helper()
rsp, err := client.Read(testutil.TestContext(t), &pbresource.ReadRequest{Id: id}) rsp, err := client.Read(client.Context(t), &pbresource.ReadRequest{Id: id})
require.Error(t, err) require.Error(t, err)
require.Equal(t, codes.NotFound, status.Code(err)) require.Equal(t, codes.NotFound, status.Code(err))
require.Nil(t, rsp) require.Nil(t, rsp)
@ -123,7 +144,7 @@ func (client *Client) RequireResourceNotFound(t T, id *pbresource.ID) {
func (client *Client) RequireResourceExists(t T, id *pbresource.ID) *pbresource.Resource { func (client *Client) RequireResourceExists(t T, id *pbresource.ID) *pbresource.Resource {
t.Helper() t.Helper()
rsp, err := client.Read(testutil.TestContext(t), &pbresource.ReadRequest{Id: id}) rsp, err := client.Read(client.Context(t), &pbresource.ReadRequest{Id: id})
require.NoError(t, err, "error reading %s with type %s", id.Name, resource.ToGVK(id.Type)) require.NoError(t, err, "error reading %s with type %s", id.Name, resource.ToGVK(id.Type))
require.NotNil(t, rsp) require.NotNil(t, rsp)
return rsp.Resource return rsp.Resource
@ -261,7 +282,7 @@ func (client *Client) ResolveResourceID(t T, id *pbresource.ID) *pbresource.ID {
func (client *Client) MustDelete(t T, id *pbresource.ID) { func (client *Client) MustDelete(t T, id *pbresource.ID) {
t.Helper() t.Helper()
ctx := testutil.TestContext(t) ctx := client.Context(t)
client.retry(t, func(r *retry.R) { client.retry(t, func(r *retry.R) {
_, err := client.Delete(ctx, &pbresource.DeleteRequest{Id: id}) _, err := client.Delete(ctx, &pbresource.DeleteRequest{Id: id})

View File

@ -2,7 +2,30 @@
# SPDX-License-Identifier: BUSL-1.1 # SPDX-License-Identifier: BUSL-1.1
ARG CONSUL_DATAPLANE_IMAGE ARG CONSUL_DATAPLANE_IMAGE
ARG CONSUL_IMAGE
# Docker doesn't support expansion in COPY --copy, so we need to create an intermediate image.
FROM ${CONSUL_IMAGE} as consul
FROM ${CONSUL_DATAPLANE_IMAGE} as consuldataplane FROM ${CONSUL_DATAPLANE_IMAGE} as consuldataplane
COPY --from=busybox:uclibc /bin/sh /bin/sh
COPY --from=ghcr.io/tarampampam/curl:latest /bin/curl /bin/curl USER root
# On Mac M1s when TProxy is enabled, consul-dataplane that are spawned from this image
# (only used in consul-container integration tests) will terminate with the below error.
# It is related to tproxy-startup.sh calling iptables SDK which then calls the underly
# iptables. We are investigating how this works on M1s with consul-envoy images which
# do not have this problem. For the time being tproxy tests on Mac M1s will fail locally
# but pass in CI.
#
# Error setting up traffic redirection rules: failed to run command: /sbin/iptables -t nat -N CONSUL_PROXY_INBOUND, err: exit status 1, output: iptables: Failed to initialize nft: Protocol not supported
RUN microdnf install -y iptables sudo nc \
&& usermod -a -G wheel consul-dataplane \
&& echo 'consul-dataplane ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
COPY --from=consul /bin/consul /bin/consul
COPY tproxy-startup.sh /bin/tproxy-startup.sh
RUN chmod +x /bin/tproxy-startup.sh && chown root:root /bin/tproxy-startup.sh
USER 100

View File

@ -273,6 +273,11 @@ func (b *Builder) Peering(enable bool) *Builder {
return b return b
} }
func (b *Builder) SetACLToken(token string) *Builder {
b.conf.Set("acl.tokens.agent", token)
return b
}
func (b *Builder) NodeID(nodeID string) *Builder { func (b *Builder) NodeID(nodeID string) *Builder {
b.conf.Set("node_id", nodeID) b.conf.Set("node_id", nodeID)
return b return b

View File

@ -6,11 +6,14 @@ package cluster
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"strconv"
"strings"
"time"
"github.com/hashicorp/consul/test/integration/consul-container/libs/utils" "github.com/hashicorp/consul/test/integration/consul-container/libs/utils"
"github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait" "github.com/testcontainers/testcontainers-go/wait"
"strconv"
"time"
) )
type ConsulDataplaneContainer struct { type ConsulDataplaneContainer struct {
@ -27,6 +30,10 @@ func (g ConsulDataplaneContainer) GetAddr() (string, int) {
return g.ip, g.appPort[0] return g.ip, g.appPort[0]
} }
func (g ConsulDataplaneContainer) GetServiceName() string {
return g.serviceName
}
// GetAdminAddr returns the external admin port // GetAdminAddr returns the external admin port
func (g ConsulDataplaneContainer) GetAdminAddr() (string, int) { func (g ConsulDataplaneContainer) GetAdminAddr() (string, int) {
return "localhost", g.externalAdminPort return "localhost", g.externalAdminPort
@ -36,13 +43,28 @@ func (c ConsulDataplaneContainer) Terminate() error {
return TerminateContainer(c.ctx, c.container, true) return TerminateContainer(c.ctx, c.container, true)
} }
func (g ConsulDataplaneContainer) Exec(ctx context.Context, cmd []string) (string, error) {
exitCode, reader, err := g.container.Exec(ctx, cmd)
if err != nil {
return "", fmt.Errorf("exec with error %s", err)
}
if exitCode != 0 {
return "", fmt.Errorf("exec with exit code %d", exitCode)
}
buf, err := io.ReadAll(reader)
if err != nil {
return "", fmt.Errorf("error reading from exec output: %w", err)
}
return string(buf), nil
}
func (g ConsulDataplaneContainer) GetStatus() (string, error) { func (g ConsulDataplaneContainer) GetStatus() (string, error) {
state, err := g.container.State(g.ctx) state, err := g.container.State(g.ctx)
return state.Status, err return state.Status, err
} }
func NewConsulDataplane(ctx context.Context, proxyID string, serverAddresses string, grpcPort int, serviceBindPorts []int, func NewConsulDataplane(ctx context.Context, proxyID string, serverAddresses string, grpcPort int, serviceBindPorts []int,
node Agent, containerArgs ...string) (*ConsulDataplaneContainer, error) { node Agent, tproxy bool, bootstrapToken string, containerArgs ...string) (*ConsulDataplaneContainer, error) {
namePrefix := fmt.Sprintf("%s-consul-dataplane-%s", node.GetDatacenter(), proxyID) namePrefix := fmt.Sprintf("%s-consul-dataplane-%s", node.GetDatacenter(), proxyID)
containerName := utils.RandName(namePrefix) containerName := utils.RandName(namePrefix)
@ -70,7 +92,39 @@ func NewConsulDataplane(ctx context.Context, proxyID string, serverAddresses str
copy(exposedPorts, appPortStrs) copy(exposedPorts, appPortStrs)
exposedPorts = append(exposedPorts, adminPortStr) exposedPorts = append(exposedPorts, adminPortStr)
command := []string{ req := testcontainers.ContainerRequest{
Image: "consul-dataplane:local",
WaitingFor: wait.ForLog("").WithStartupTimeout(60 * time.Second),
AutoRemove: false,
Name: containerName,
Env: map[string]string{},
}
var command []string
if tproxy {
req.Entrypoint = []string{"sh", "/bin/tproxy-startup.sh"}
req.Env["REDIRECT_TRAFFIC_ARGS"] = strings.Join(
[]string{
// TODO once we run this on a different pod from Consul agents, we can eliminate most of this.
"-exclude-inbound-port", fmt.Sprint(internalAdminPort),
"-exclude-inbound-port", "8300",
"-exclude-inbound-port", "8301",
"-exclude-inbound-port", "8302",
"-exclude-inbound-port", "8500",
"-exclude-inbound-port", "8502",
"-exclude-inbound-port", "8600",
"-proxy-inbound-port", "20000",
"-consul-dns-ip", "127.0.0.1",
"-consul-dns-port", "8600",
},
" ",
)
req.CapAdd = append(req.CapAdd, "NET_ADMIN")
command = append(command, "consul-dataplane")
}
command = append(command,
"-addresses", serverAddresses, "-addresses", serverAddresses,
fmt.Sprintf("-grpc-port=%d", grpcPort), fmt.Sprintf("-grpc-port=%d", grpcPort),
fmt.Sprintf("-proxy-id=%s", proxyID), fmt.Sprintf("-proxy-id=%s", proxyID),
@ -81,18 +135,15 @@ func NewConsulDataplane(ctx context.Context, proxyID string, serverAddresses str
"-envoy-concurrency=2", "-envoy-concurrency=2",
"-tls-disabled", "-tls-disabled",
fmt.Sprintf("-envoy-admin-bind-port=%d", internalAdminPort), fmt.Sprintf("-envoy-admin-bind-port=%d", internalAdminPort),
)
if bootstrapToken != "" {
command = append(command,
"-credential-type=static",
fmt.Sprintf("-static-token=%s", bootstrapToken))
} }
command = append(command, containerArgs...) req.Cmd = append(command, containerArgs...)
req := testcontainers.ContainerRequest{
Image: "consul-dataplane:local",
WaitingFor: wait.ForLog("").WithStartupTimeout(60 * time.Second),
AutoRemove: false,
Name: containerName,
Cmd: command,
Env: map[string]string{},
}
info, err := LaunchContainerOnNode(ctx, node, req, exposedPorts) info, err := LaunchContainerOnNode(ctx, node, req, exposedPorts)
if err != nil { if err != nil {

View File

@ -272,6 +272,9 @@ func NewClusterWithConfig(
Client(). Client().
Peering(true). Peering(true).
RetryJoin(retryJoin...) RetryJoin(retryJoin...)
if cluster.TokenBootstrap != "" {
configBuilder.SetACLToken(cluster.TokenBootstrap)
}
clientConf := configBuilder.ToAgentConfig(t) clientConf := configBuilder.ToAgentConfig(t)
t.Logf("%s client config: \n%s", opts.Datacenter, clientConf.JSON) t.Logf("%s client config: \n%s", opts.Datacenter, clientConf.JSON)
if clientHclConfig != "" { if clientHclConfig != "" {

View File

@ -87,7 +87,7 @@ func createServiceAndDataplane(t *testing.T, node libcluster.Agent, proxyID, ser
}) })
// Create Consul Dataplane // Create Consul Dataplane
dp, err := libcluster.NewConsulDataplane(context.Background(), proxyID, "0.0.0.0", 8502, serviceBindPorts, node) dp, err := libcluster.NewConsulDataplane(context.Background(), proxyID, "0.0.0.0", 8502, serviceBindPorts, node, false, "")
require.NoError(t, err) require.NoError(t, err)
deferClean.Add(func() { deferClean.Add(func() {
_ = dp.Terminate() _ = dp.Terminate()

View File

@ -0,0 +1,657 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package trafficpermissions
import (
"context"
"fmt"
"strings"
"testing"
"github.com/hashicorp/consul/sdk/testutil/retry"
rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
libcluster "github.com/hashicorp/consul/test/integration/consul-container/libs/cluster"
libservice "github.com/hashicorp/consul/test/integration/consul-container/libs/service"
"github.com/hashicorp/consul/test/integration/consul-container/libs/topology"
"github.com/hashicorp/consul/test/integration/consul-container/libs/utils"
"github.com/stretchr/testify/require"
)
const (
echoPort = 9999
tcpPort = 8888
staticServerVIP = "240.0.0.1"
staticServerReturnValue = "static-server"
staticServerIdentity = "static-server-identity"
)
type trafficPermissionsCase struct {
tp1 *pbauth.TrafficPermissions
tp2 *pbauth.TrafficPermissions
client1TCPSuccess bool
client1EchoSuccess bool
client2TCPSuccess bool
client2EchoSuccess bool
}
// We are using tproxy to test traffic permissions now because explicitly specifying destinations
// doesn't work when multiple downstreams specify the same destination yet. In the future, we will need
// to update this to use explicit destinations once we infer tproxy destinations from traffic permissions.
//
// This also explicitly uses virtual IPs and virtual ports because Consul DNS doesn't support v2 resources yet.
// We should update this to use Consul DNS when it is working.
func runTrafficPermissionsTests(t *testing.T, aclsEnabled bool, cases map[string]trafficPermissionsCase) {
t.Parallel()
cluster, resourceClient := createCluster(t, aclsEnabled)
serverDataplane := createServerResources(t, resourceClient, cluster, cluster.Agents[1])
client1Dataplane := createClientResources(t, resourceClient, cluster, cluster.Agents[2], 1)
client2Dataplane := createClientResources(t, resourceClient, cluster, cluster.Agents[3], 2)
assertDataplaneContainerState(t, client1Dataplane, "running")
assertDataplaneContainerState(t, client2Dataplane, "running")
assertDataplaneContainerState(t, serverDataplane, "running")
for n, tc := range cases {
t.Run(n, func(t *testing.T) {
storeStaticServerTrafficPermissions(t, resourceClient, tc.tp1, 1)
storeStaticServerTrafficPermissions(t, resourceClient, tc.tp2, 2)
// We must establish a new TCP connection each time because TCP traffic permissions are
// enforced at the connection level.
retry.Run(t, func(r *retry.R) {
assertPassing(r, httpRequestToVirtualAddress, client1Dataplane, tc.client1TCPSuccess)
assertPassing(r, echoToVirtualAddress, client1Dataplane, tc.client1EchoSuccess)
assertPassing(r, httpRequestToVirtualAddress, client2Dataplane, tc.client2TCPSuccess)
assertPassing(r, echoToVirtualAddress, client2Dataplane, tc.client2EchoSuccess)
})
})
}
}
func TestTrafficPermission_TCP_DefaultDeny(t *testing.T) {
cases := map[string]trafficPermissionsCase{
"default deny": {
tp1: nil,
client1TCPSuccess: false,
client1EchoSuccess: false,
client2TCPSuccess: false,
client2EchoSuccess: false,
},
"allow everything": {
tp1: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
// IdentityName: "static-client-1-identity",
Namespace: "default",
Partition: "default",
Peer: "local",
},
},
},
},
},
client1TCPSuccess: true,
client1EchoSuccess: true,
client2TCPSuccess: true,
client2EchoSuccess: true,
},
"allow tcp": {
tp1: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
// IdentityName: "static-client-1-identity",
Namespace: "default",
Partition: "default",
Peer: "local",
},
},
DestinationRules: []*pbauth.DestinationRule{
{
PortNames: []string{"tcp"},
},
},
},
},
},
client1TCPSuccess: true,
client1EchoSuccess: false,
client2TCPSuccess: true,
client2EchoSuccess: false,
},
"client 1 only": {
tp1: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
IdentityName: "static-client-1-identity",
Namespace: "default",
Partition: "default",
Peer: "local",
},
},
},
},
},
client1TCPSuccess: true,
client1EchoSuccess: true,
client2TCPSuccess: false,
client2EchoSuccess: false,
},
"allow all exclude client 1": {
tp1: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
Namespace: "default",
Partition: "default",
Peer: "local",
Exclude: []*pbauth.ExcludeSource{
{
IdentityName: "static-client-1-identity",
Namespace: "default",
Partition: "default",
Peer: "local",
},
},
},
},
},
},
},
client1TCPSuccess: false,
client1EchoSuccess: false,
client2TCPSuccess: true,
client2EchoSuccess: true,
},
"deny takes precedence over allow": {
tp1: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_DENY,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
IdentityName: "static-client-1-identity",
Namespace: "default",
Partition: "default",
Peer: "local",
},
},
},
},
},
tp2: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
IdentityName: "static-client-1-identity",
Namespace: "default",
Partition: "default",
Peer: "local",
},
},
},
},
},
client1TCPSuccess: false,
client1EchoSuccess: false,
client2TCPSuccess: false,
client2EchoSuccess: false,
},
"deny all exclude service + allow on that service": {
tp1: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_DENY,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
Namespace: "default",
Partition: "default",
Peer: "local",
Exclude: []*pbauth.ExcludeSource{
{
IdentityName: "static-client-1-identity",
Namespace: "default",
Partition: "default",
Peer: "local",
},
},
},
},
},
},
},
tp2: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
IdentityName: "static-client-1-identity",
Namespace: "default",
Partition: "default",
Peer: "local",
},
},
},
},
},
client1TCPSuccess: true,
client1EchoSuccess: true,
client2TCPSuccess: false,
client2EchoSuccess: false,
},
}
runTrafficPermissionsTests(t, true, cases)
}
func TestTrafficPermission_TCP_DefaultAllow(t *testing.T) {
cases := map[string]trafficPermissionsCase{
"default allow": {
tp1: nil,
client1TCPSuccess: true,
client1EchoSuccess: true,
client2TCPSuccess: true,
client2EchoSuccess: true,
},
"empty allow denies everything": {
tp1: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_ALLOW,
},
client1TCPSuccess: false,
client1EchoSuccess: false,
client2TCPSuccess: false,
client2EchoSuccess: false,
},
"empty deny denies everything": {
tp1: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_DENY,
},
client1TCPSuccess: false,
client1EchoSuccess: false,
client2TCPSuccess: false,
client2EchoSuccess: false,
},
"allow everything": {
tp1: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
Namespace: "default",
Partition: "default",
Peer: "local",
},
},
},
},
},
client1TCPSuccess: true,
client1EchoSuccess: true,
client2TCPSuccess: true,
client2EchoSuccess: true,
},
"allow one protocol denies the other protocol": {
tp1: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
Namespace: "default",
Partition: "default",
Peer: "local",
},
},
DestinationRules: []*pbauth.DestinationRule{
{
PortNames: []string{"tcp"},
},
},
},
},
},
client1TCPSuccess: true,
client1EchoSuccess: false,
client2TCPSuccess: true,
client2EchoSuccess: false,
},
"allow something unrelated": {
tp1: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: staticServerIdentity,
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
IdentityName: "something-else",
Namespace: "default",
Partition: "default",
Peer: "local",
},
},
},
},
},
client1TCPSuccess: false,
client1EchoSuccess: false,
client2TCPSuccess: false,
client2EchoSuccess: false,
},
}
runTrafficPermissionsTests(t, false, cases)
}
func createServiceAndDataplane(t *testing.T, node libcluster.Agent, cluster *libcluster.Cluster, proxyID, serviceName string, httpPort, grpcPort int, serviceBindPorts []int) (*libcluster.ConsulDataplaneContainer, error) {
leader, err := cluster.Leader()
require.NoError(t, err)
leaderIP := leader.GetIP()
token := cluster.TokenBootstrap
// Do some trickery to ensure that partial completion is correctly torn
// down, but successful execution is not.
var deferClean utils.ResettableDefer
defer deferClean.Execute()
// Create a service and proxy instance
svc, err := libservice.NewExampleService(context.Background(), serviceName, httpPort, grpcPort, node)
if err != nil {
return nil, err
}
deferClean.Add(func() {
_ = svc.Terminate()
})
// Create Consul Dataplane
dp, err := libcluster.NewConsulDataplane(context.Background(), proxyID, leaderIP, 8502, serviceBindPorts, node, true, token)
require.NoError(t, err)
deferClean.Add(func() {
_ = dp.Terminate()
})
// disable cleanup functions now that we have an object with a Terminate() function
deferClean.Reset()
return dp, nil
}
func storeStaticServerTrafficPermissions(t *testing.T, resourceClient *rtest.Client, tp *pbauth.TrafficPermissions, i int) {
id := &pbresource.ID{
Name: fmt.Sprintf("static-server-tp-%d", i),
Type: pbauth.TrafficPermissionsType,
}
if tp == nil {
resourceClient.Delete(resourceClient.Context(t), &pbresource.DeleteRequest{
Id: id,
})
} else {
rtest.ResourceID(id).
WithData(t, tp).
Write(t, resourceClient)
}
}
func createServerResources(t *testing.T, resourceClient *rtest.Client, cluster *libcluster.Cluster, node libcluster.Agent) *libcluster.ConsulDataplaneContainer {
rtest.ResourceID(&pbresource.ID{
Name: "static-server-service",
Type: pbcatalog.ServiceType,
}).
WithData(t, &pbcatalog.Service{
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{"static-server"}},
Ports: []*pbcatalog.ServicePort{
{
TargetPort: "tcp",
Protocol: pbcatalog.Protocol_PROTOCOL_TCP,
VirtualPort: 8888,
},
{
TargetPort: "echo",
Protocol: pbcatalog.Protocol_PROTOCOL_TCP,
VirtualPort: 9999,
},
{TargetPort: "mesh", Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
},
VirtualIps: []string{"240.0.0.1"},
}).Write(t, resourceClient)
workloadPortMap := map[string]*pbcatalog.WorkloadPort{
"tcp": {
Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_TCP,
},
"echo": {
Port: 8078, Protocol: pbcatalog.Protocol_PROTOCOL_TCP,
},
"mesh": {
Port: 20000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH,
},
}
rtest.ResourceID(&pbresource.ID{
Name: "static-server-workload",
Type: pbcatalog.WorkloadType,
}).
WithData(t, &pbcatalog.Workload{
Addresses: []*pbcatalog.WorkloadAddress{
{Host: node.GetIP()},
},
Ports: workloadPortMap,
Identity: staticServerIdentity,
}).
Write(t, resourceClient)
rtest.ResourceID(&pbresource.ID{
Name: staticServerIdentity,
Type: pbauth.WorkloadIdentityType,
}).
Write(t, resourceClient)
serverDataplane, err := createServiceAndDataplane(t, node, cluster, "static-server-workload", "static-server", 8080, 8079, []int{})
require.NoError(t, err)
return serverDataplane
}
func createClientResources(t *testing.T, resourceClient *rtest.Client, cluster *libcluster.Cluster, node libcluster.Agent, idx int) *libcluster.ConsulDataplaneContainer {
prefix := fmt.Sprintf("static-client-%d", idx)
rtest.ResourceID(&pbresource.ID{
Name: prefix + "-service",
Type: pbcatalog.ServiceType,
}).
WithData(t, &pbcatalog.Service{
Workloads: &pbcatalog.WorkloadSelector{Prefixes: []string{prefix}},
Ports: []*pbcatalog.ServicePort{
{TargetPort: "tcp", Protocol: pbcatalog.Protocol_PROTOCOL_TCP},
{TargetPort: "mesh", Protocol: pbcatalog.Protocol_PROTOCOL_MESH},
},
}).Write(t, resourceClient)
workloadPortMap := map[string]*pbcatalog.WorkloadPort{
"tcp": {
Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_TCP,
},
"mesh": {
Port: 20000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH,
},
}
rtest.ResourceID(&pbresource.ID{
Name: prefix + "-workload",
Type: pbcatalog.WorkloadType,
}).
WithData(t, &pbcatalog.Workload{
Addresses: []*pbcatalog.WorkloadAddress{
{Host: node.GetIP()},
},
Ports: workloadPortMap,
Identity: prefix + "-identity",
}).
Write(t, resourceClient)
rtest.ResourceID(&pbresource.ID{
Name: prefix + "-identity",
Type: pbauth.WorkloadIdentityType,
}).
Write(t, resourceClient)
rtest.ResourceID(&pbresource.ID{
Name: prefix + "-proxy-configuration",
Type: pbmesh.ProxyConfigurationType,
}).
WithData(t, &pbmesh.ProxyConfiguration{
Workloads: &pbcatalog.WorkloadSelector{
Prefixes: []string{"static-client"},
},
DynamicConfig: &pbmesh.DynamicConfig{
Mode: pbmesh.ProxyMode_PROXY_MODE_TRANSPARENT,
},
}).
Write(t, resourceClient)
dp, err := createServiceAndDataplane(t, node, cluster, fmt.Sprintf("static-client-%d-workload", idx), "static-client", 8080, 8079, []int{})
require.NoError(t, err)
return dp
}
func createCluster(t *testing.T, aclsEnabled bool) (*libcluster.Cluster, *rtest.Client) {
cluster, _, _ := topology.NewCluster(t, &topology.ClusterConfig{
NumServers: 1,
NumClients: 3,
BuildOpts: &libcluster.BuildOptions{
Datacenter: "dc1",
InjectAutoEncryption: true,
InjectGossipEncryption: true,
AllowHTTPAnyway: true,
ACLEnabled: aclsEnabled,
},
Cmd: `-hcl=experiments=["resource-apis"] log_level="TRACE"`,
})
leader, err := cluster.Leader()
require.NoError(t, err)
client := pbresource.NewResourceServiceClient(leader.GetGRPCConn())
resourceClient := rtest.NewClientWithACLToken(client, cluster.TokenBootstrap)
return cluster, resourceClient
}
// assertDataplaneContainerState validates service container status
func assertDataplaneContainerState(t *testing.T, dataplane *libcluster.ConsulDataplaneContainer, state string) {
containerStatus, err := dataplane.GetStatus()
require.NoError(t, err)
require.Equal(t, containerStatus, state, fmt.Sprintf("Expected: %s. Got %s", state, containerStatus))
}
func httpRequestToVirtualAddress(dp *libcluster.ConsulDataplaneContainer) (string, error) {
addr := fmt.Sprintf("%s:%d", staticServerVIP, tcpPort)
out, err := dp.Exec(
context.Background(),
[]string{"sudo", "sh", "-c", fmt.Sprintf(`
set -e
curl -s "%s/debug?env=dump"
`, addr),
},
)
if err != nil {
return out, fmt.Errorf("curl request to upstream virtual address %q\nerr = %v\nout = %s\nservice=%s", addr, err, out, dp.GetServiceName())
}
expected := fmt.Sprintf("FORTIO_NAME=%s", staticServerReturnValue)
if !strings.Contains(out, expected) {
return out, fmt.Errorf("expected %q to contain %q", out, expected)
}
return out, nil
}
func echoToVirtualAddress(dp *libcluster.ConsulDataplaneContainer) (string, error) {
out, err := dp.Exec(
context.Background(),
[]string{"sudo", "sh", "-c", fmt.Sprintf(`
set -e
echo foo | nc %s %d
`, staticServerVIP, echoPort),
},
)
if err != nil {
return out, fmt.Errorf("nc request to upstream virtual address %s:%d\nerr = %v\nout = %s\nservice=%s", staticServerVIP, echoPort, err, out, dp.GetServiceName())
}
if !strings.Contains(out, "foo") {
return out, fmt.Errorf("expected %q to contain 'foo'", out)
}
return out, err
}
func assertPassing(t *retry.R, fn func(*libcluster.ConsulDataplaneContainer) (string, error), dp *libcluster.ConsulDataplaneContainer, success bool) {
_, err := fn(dp)
if success {
require.NoError(t, err)
} else {
require.Error(t, err)
}
}