diff --git a/pkg/agent/protocol/nettest/containers/cutter/Dockerfile b/pkg/agent/protocol/nettest/containers/cutter/Dockerfile new file mode 100644 index 00000000..0f769482 --- /dev/null +++ b/pkg/agent/protocol/nettest/containers/cutter/Dockerfile @@ -0,0 +1,9 @@ +FROM debian:bookworm + +RUN apt update && apt install -y curl zstd + +# Upstream for cutter website is unavailable at the time of writing, the arch linux package is the most stable +# source I was able to find. +RUN curl -vL https://archive.archlinux.org/packages/c/cutter/cutter-1.04-3-x86_64.pkg.tar.zst \ + | tar --zstd -xvC / + diff --git a/pkg/agent/protocol/nettest/containers/iptables/Dockerfile b/pkg/agent/protocol/nettest/containers/iptables/Dockerfile new file mode 100644 index 00000000..d4dace98 --- /dev/null +++ b/pkg/agent/protocol/nettest/containers/iptables/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine:3.18 + +RUN apk update && apk add iptables + diff --git a/pkg/agent/protocol/nettest/containers/redis-go/Dockerfile b/pkg/agent/protocol/nettest/containers/redis-go/Dockerfile new file mode 100644 index 00000000..b0ffe7a4 --- /dev/null +++ b/pkg/agent/protocol/nettest/containers/redis-go/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.21-bookworm as build + +WORKDIR /app +COPY . . +RUN go build -o redis-go + +FROM debian:bookworm +COPY --from=build /app/redis-go /bin +ENTRYPOINT [ "/bin/redis-go" ] diff --git a/pkg/agent/protocol/nettest/containers/redis-go/go.mod b/pkg/agent/protocol/nettest/containers/redis-go/go.mod new file mode 100644 index 00000000..961db484 --- /dev/null +++ b/pkg/agent/protocol/nettest/containers/redis-go/go.mod @@ -0,0 +1,13 @@ +module redisgo + +go 1.19 + +require ( + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect +) + +require ( + github.com/go-redis/redis/v8 v8.11.5 + github.com/onsi/gomega v1.27.10 // indirect +) diff --git a/pkg/agent/protocol/nettest/containers/redis-go/go.sum b/pkg/agent/protocol/nettest/containers/redis-go/go.sum new file mode 100644 index 00000000..da4a8515 --- /dev/null +++ b/pkg/agent/protocol/nettest/containers/redis-go/go.sum @@ -0,0 +1,17 @@ +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/agent/protocol/nettest/containers/redis-go/main.go b/pkg/agent/protocol/nettest/containers/redis-go/main.go new file mode 100644 index 00000000..db9834b8 --- /dev/null +++ b/pkg/agent/protocol/nettest/containers/redis-go/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "log" + "os" + "time" + + "github.com/go-redis/redis/v8" +) + +func main() { + rdb := redis.NewClient(&redis.Options{ + Addr: os.Args[1], + Password: "", // no password set + DB: 0, // use default DB + }) + + ctx := context.Background() + + err := rdb.Set(ctx, "counter", 0.0, 0).Err() + if err != nil { + log.Fatalf("creating redis key: %v", err) + } + + for { + err = rdb.Incr(ctx, "counter").Err() + if err != nil { + log.Fatalf("incrementing counter: %v", err) + } + + cmd := rdb.Get(ctx, "counter") + if err := cmd.Err(); err != nil { + log.Fatalf("getting current value: %v", err) + } + + current, _ := cmd.Float64() + log.Printf("Current value: %f", current) + time.Sleep(time.Second) + } +} diff --git a/pkg/agent/protocol/nettest/containers/tcpdump/Dockerfile b/pkg/agent/protocol/nettest/containers/tcpdump/Dockerfile new file mode 100644 index 00000000..44d128fd --- /dev/null +++ b/pkg/agent/protocol/nettest/containers/tcpdump/Dockerfile @@ -0,0 +1,5 @@ +FROM debian:bookworm + +RUN apt update && apt install -y tcpdump + +ENTRYPOINT ["/bin/tcpdump"] diff --git a/pkg/agent/protocol/nettest/containers/tcpkill/Dockerfile b/pkg/agent/protocol/nettest/containers/tcpkill/Dockerfile new file mode 100644 index 00000000..e5c6503e --- /dev/null +++ b/pkg/agent/protocol/nettest/containers/tcpkill/Dockerfile @@ -0,0 +1,3 @@ +FROM debian:bookworm + +RUN apt update && apt install -y dsniff diff --git a/pkg/agent/protocol/nettest/redis/cutter_test.go b/pkg/agent/protocol/nettest/redis/cutter_test.go new file mode 100644 index 00000000..750aa293 --- /dev/null +++ b/pkg/agent/protocol/nettest/redis/cutter_test.go @@ -0,0 +1,126 @@ +package redis_test + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + + "github.com/grafana/xk6-disruptor/pkg/agent/protocol/nettest/util" +) + +func Test_Redis_Cutter(t *testing.T) { + t.Parallel() + + if os.Getenv("NETTEST") == "" { + t.Skip("Skipping network protocol test as NETTEST is not set") + } + + ctx := context.TODO() + + redis, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ProviderType: testcontainers.ProviderDocker, + ContainerRequest: testcontainers.ContainerRequest{ + Image: "redis", + ExposedPorts: []string{"6379/tcp"}, + WaitingFor: wait.ForExposedPort(), + }, + Started: true, + }) + if err != nil { + t.Fatalf("failed to create redis container %v", err) + } + + t.Cleanup(func() { + _ = redis.Terminate(ctx) + }) + + redisGo, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ProviderType: testcontainers.ProviderDocker, + ContainerRequest: testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ + Dockerfile: "Dockerfile", + Context: filepath.Join("..", "containers", "redis-go"), + }, + Cmd: []string{"localhost:6379"}, + NetworkMode: container.NetworkMode("container:" + redis.GetContainerID()), + }, + Started: true, + }) + if err != nil { + t.Fatalf("failed to create agent container %v", err) + } + + // TODO: Calling terminate with a log attached makes the test hang. + // See: https://github.com/testcontainers/testcontainers-go/issues/1669 + // t.Cleanup(func() { + // _ = redisGo.Terminate(ctx) + // }) + + redisGo.FollowOutput(util.Mirror{T: t, Name: "redis-go"}) + err = redisGo.StartLogProducer(ctx) + if err != nil { + t.Fatal(err) + } + // TODO: See above. + // t.Cleanup(func() { + // redisGo.StopLogProducer() + // }) + + redisGoStatus, err := redisGo.State(ctx) + if err != nil { + t.Fatal(err) + } + if !redisGoStatus.Running { + t.Fatalf("Redis client container failed") + } + + time.Sleep(3 * time.Second) + + cutter, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ProviderType: testcontainers.ProviderDocker, + ContainerRequest: testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ + Dockerfile: "Dockerfile", + Context: filepath.Join("..", "containers", "cutter"), + }, + NetworkMode: container.NetworkMode("container:" + redis.GetContainerID()), + Cmd: []string{"/bin/sh", "-c", "tcp-cutter 127.0.0.1 6379"}, + Privileged: true, + }, + Started: true, + }) + if err != nil { + t.Fatalf("failed to create cutter container %v", err) + } + + // t.Cleanup(func() { + // _ = cutter.Terminate(ctx) + // }) + + cutter.FollowOutput(util.Mirror{T: t, Name: "cutter"}) + err = cutter.StartLogProducer(ctx) + if err != nil { + t.Fatal(err) + } + + // t.Cleanup(func() { + // cutter.StopLogProducer() + // }) + + time.Sleep(2 * time.Second) + + redisGoStatus, err = redisGo.State(ctx) + if err != nil { + t.Fatal(err) + } + + if !redisGoStatus.Running { + t.Fatalf("Redis client container failed") + } +} diff --git a/pkg/agent/protocol/nettest/redis/iptables_test.go b/pkg/agent/protocol/nettest/redis/iptables_test.go new file mode 100644 index 00000000..d40e8f08 --- /dev/null +++ b/pkg/agent/protocol/nettest/redis/iptables_test.go @@ -0,0 +1,123 @@ +package redis_test + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + + "github.com/grafana/xk6-disruptor/pkg/agent/protocol/nettest/util" +) + +const iptablesRule = "INPUT -p tcp --dport 6379 -j REJECT --reject-with tcp-reset" + +func Test_Redis_Iptables(t *testing.T) { + t.Parallel() + + if os.Getenv("NETTEST") == "" { + t.Skip("Skipping network protocol test as NETTEST is not set") + } + + ctx := context.TODO() + + redis, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ProviderType: testcontainers.ProviderDocker, + ContainerRequest: testcontainers.ContainerRequest{ + Image: "redis", + ExposedPorts: []string{"6379/tcp"}, + WaitingFor: wait.ForExposedPort(), + }, + Started: true, + }) + if err != nil { + t.Fatalf("failed to create redis container %v", err) + } + + t.Cleanup(func() { + _ = redis.Terminate(ctx) + }) + + iptables, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ProviderType: testcontainers.ProviderDocker, + ContainerRequest: testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ + Dockerfile: "Dockerfile", + Context: filepath.Join("..", "containers", "iptables"), + }, + NetworkMode: container.NetworkMode("container:" + redis.GetContainerID()), + Cmd: []string{"/bin/sh", "-c", "echo ready && sleep infinity"}, + Privileged: true, + WaitingFor: wait.ForLog("ready"), + }, + Started: true, + }) + if err != nil { + t.Fatalf("failed to create agent container %v", err) + } + + t.Cleanup(func() { + _ = iptables.Terminate(ctx) + }) + + redisGo, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ProviderType: testcontainers.ProviderDocker, + ContainerRequest: testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ + Dockerfile: "Dockerfile", + Context: filepath.Join("..", "containers", "redis-go"), + }, + Cmd: []string{"localhost:6379"}, + NetworkMode: container.NetworkMode("container:" + redis.GetContainerID()), + }, + Started: true, + }) + if err != nil { + t.Fatalf("failed to create agent container %v", err) + } + + // TODO: Calling terminate with a log attached makes the test hang. + // See: https://github.com/testcontainers/testcontainers-go/issues/1669 + // t.Cleanup(func() { + // _ = redisGo.Terminate(ctx) + // }) + + redisGo.FollowOutput(util.Mirror{T: t, Name: "redis-go"}) + err = redisGo.StartLogProducer(ctx) + if err != nil { + t.Fatal(err) + } + // TODO: See above. + // t.Cleanup(func() { + // redisGo.StopLogProducer() + // }) + + redisGoStatus, err := redisGo.State(ctx) + if err != nil { + t.Fatal(err) + } + if !redisGoStatus.Running { + t.Fatalf("Redis client container failed") + } + + //nolint:errcheck,gosec // Error checking elided for brevity. TODO: Wrap this in a helper function. + iptables.Exec(context.TODO(), []string{"/bin/sh", "-c", "iptables -I " + iptablesRule}) + + time.Sleep(2 * time.Second) + + //nolint:errcheck,gosec // Error checking elided for brevity. TODO: Wrap this in a helper function. + iptables.Exec(context.TODO(), []string{"/bin/sh", "-c", "iptables -D " + iptablesRule}) + + redisGoStatus, err = redisGo.State(ctx) + if err != nil { + t.Fatal(err) + } + + if !redisGoStatus.Running { + t.Fatalf("Redis client container failed") + } +} diff --git a/pkg/agent/protocol/nettest/redis/tcpkill_test.go b/pkg/agent/protocol/nettest/redis/tcpkill_test.go new file mode 100644 index 00000000..c896b409 --- /dev/null +++ b/pkg/agent/protocol/nettest/redis/tcpkill_test.go @@ -0,0 +1,126 @@ +package redis_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + + "github.com/grafana/xk6-disruptor/pkg/agent/protocol/nettest/util" +) + +func Test_Redis_TCPKill(t *testing.T) { + t.Parallel() + + // if os.Getenv("NETTEST") == "" { + // t.Skip("Skipping network protocol test as NETTEST is not set") + // } + + ctx := context.TODO() + + redis, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ProviderType: testcontainers.ProviderDocker, + ContainerRequest: testcontainers.ContainerRequest{ + Image: "redis", + ExposedPorts: []string{"6379/tcp"}, + WaitingFor: wait.ForExposedPort(), + }, + Started: true, + }) + if err != nil { + t.Fatalf("failed to create redis container %v", err) + } + + t.Cleanup(func() { + _ = redis.Terminate(ctx) + }) + + redisGo, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ProviderType: testcontainers.ProviderDocker, + ContainerRequest: testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ + Dockerfile: "Dockerfile", + Context: filepath.Join("..", "containers", "redis-go"), + }, + Cmd: []string{"localhost:6379"}, + NetworkMode: container.NetworkMode("container:" + redis.GetContainerID()), + }, + Started: true, + }) + if err != nil { + t.Fatalf("failed to create agent container %v", err) + } + + // TODO: Calling terminate with a log attached makes the test hang. + // See: https://github.com/testcontainers/testcontainers-go/issues/1669 + // t.Cleanup(func() { + // _ = redisGo.Terminate(ctx) + // }) + + redisGo.FollowOutput(util.Mirror{T: t, Name: "redis-go"}) + err = redisGo.StartLogProducer(ctx) + if err != nil { + t.Fatal(err) + } + // TODO: See above. + // t.Cleanup(func() { + // redisGo.StopLogProducer() + // }) + + redisGoStatus, err := redisGo.State(ctx) + if err != nil { + t.Fatal(err) + } + if !redisGoStatus.Running { + t.Fatalf("Redis client container failed") + } + + time.Sleep(3 * time.Second) + + tcpkill, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ProviderType: testcontainers.ProviderDocker, + ContainerRequest: testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ + Dockerfile: "Dockerfile", + Context: filepath.Join("..", "containers", "tcpkill"), + }, + NetworkMode: container.NetworkMode("container:" + redis.GetContainerID()), + Cmd: []string{"/bin/sh", "-c", "tcpkill -i any -5 port 6379"}, + Privileged: true, + WaitingFor: wait.ForLog("tcpkill: listening"), + }, + Started: true, + }) + if err != nil { + t.Fatalf("failed to create tcpkill container %v", err) + } + + t.Cleanup(func() { + _ = tcpkill.Terminate(ctx) + }) + + tcpkill.FollowOutput(util.Mirror{T: t, Name: "tcpkill"}) + err = tcpkill.StartLogProducer(ctx) + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + tcpkill.StopLogProducer() + }) + + time.Sleep(7 * time.Second) + + redisGoStatus, err = redisGo.State(ctx) + if err != nil { + t.Fatal(err) + } + + if !redisGoStatus.Running { + t.Fatalf("Redis client container failed") + } +} diff --git a/pkg/agent/protocol/nettest/util/util.go b/pkg/agent/protocol/nettest/util/util.go new file mode 100644 index 00000000..da0cc706 --- /dev/null +++ b/pkg/agent/protocol/nettest/util/util.go @@ -0,0 +1,58 @@ +// Package util implements misc utilities for network tests +package util + +import ( + "bufio" + "context" + "strings" + "testing" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/exec" +) + +// Mirror is a testcontainers log adapter that mirrors container output to testing.T.Log. +type Mirror struct { + T *testing.T + Name string +} + +// Accept implements the testcontainers adapter interface by writing received output to the test logger. +func (m Mirror) Accept(log testcontainers.Log) { + prefix := "" + if m.Name != "" { + prefix += m.Name + "/" + } + prefix += log.LogType + + m.T.Logf("%s: %s", prefix, log.Content) +} + +// TCExec runs a command on a container in a shell, echoing the output and failing the test if it cannot be run. +func TCExec(t *testing.T, c testcontainers.Container, shellcmd string) { + t.Helper() + + cmd := []string{"/bin/sh", "-c", shellcmd} + + t.Logf("%s: running %q", c.GetContainerID(), shellcmd) + rc, out, err := c.Exec(context.TODO(), cmd, exec.Multiplexed()) + if err != nil { + t.Fatalf("running command on %s: %v", c.GetContainerID(), err) + } + + if rc != 0 { + t.Errorf("%s:%s exited with %d", c.GetContainerID(), cmd, rc) + } + + go func() { + buf := bufio.NewReader(out) + for { + line, err := buf.ReadString('\n') + if err != nil { + return + } + + t.Logf("%s:%s: %s", c.GetContainerID(), cmd, strings.TrimSpace(line)) + } + }() +}