diff --git a/.github/workflows/multikernel-tester.yml b/.github/workflows/multikernel-tester.yml index 7713bb21..a24f45e1 100644 --- a/.github/workflows/multikernel-tester.yml +++ b/.github/workflows/multikernel-tester.yml @@ -64,7 +64,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: '1.17' + go-version: '1.22' - name: Run tests run: make run-multikernel-test IMG_FILTER=${{ matrix.kernel_flavor }} ARCH=${{ inputs.architecture }} ARTIFACTS_PATH=${PWD}/artifacts - name: Prepare for archival diff --git a/.gitignore b/.gitignore index b320c581..f803cbe1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ ctags cscope.out # Test results and other stuff in testing/ +testing/testrunner/testrunner.test testing/bpf-check-summary.txt testing/*.cpio testing/*.tar diff --git a/Makefile b/Makefile index 063d0659..5f4cb0db 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,9 @@ CONTAINER_LOCAL_TAG ?= ebpf-builder:${USER}-latest IMAGEPACK_REPOSITORY ?= ghcr.io/elastic/ebpf-imagepack IMAGEPACK_PULL_TAG ?= 20231006-0053 +TESTBIN_SRC = $(wildcard testing/test_bins/*.c) +TESTBIN_PROGS = $(patsubst testing/test_bins/%.c,testing/test_bins/bin/${ARCH}/%,$(TESTBIN_SRC)) + ifdef BUILD_CONTAINER_IMAGE CONTAINER_IMAGE = ${CONTAINER_LOCAL_TAG} else @@ -170,5 +173,14 @@ endif go install github.com/florianl/bluebox@b8590fb1850f56df6e6d7786931fcabdc1e9173d cd testing && ./run_tests.sh ${ARCH} ${ARTIFACTS_PATH} ${PWD}/kernel-images/${IMG_FILTER}/${ARCH}/* +testbins: testbinpath $(TESTBIN_PROGS) + +testbinpath: + mkdir -p testing/test_bins/bin/${ARCH} + +testing/test_bins/bin/${ARCH}/%: testing/test_bins/%.c + $(CC) -g -static -o $@ $< + clean: ${SUDO} rm -rf artifacts-* + rm -r testing/test_bins/bin/* diff --git a/non-GPL/Events/EventsTrace/EventsTrace.c b/non-GPL/Events/EventsTrace/EventsTrace.c index ac07548f..f27d5ab6 100644 --- a/non-GPL/Events/EventsTrace/EventsTrace.c +++ b/non-GPL/Events/EventsTrace/EventsTrace.c @@ -280,7 +280,10 @@ static void out_escaped_string(const char *value) break; default: if (!isascii(c) || iscntrl(c)) - printf("\\x%02x", c); + // \x is not a valid escape character in json, + // and something like '\xff' will break a remarkable number of JSON parsers. + // we have to print as '0xff' + printf("0x%02x", (uint8_t)c); else printf("%c", c); } diff --git a/testing/README.md b/testing/README.md index 8afcf194..b3e89352 100644 --- a/testing/README.md +++ b/testing/README.md @@ -55,6 +55,22 @@ By default `run_tests.sh` will pass `-j$(nproc)` to `parallel` (i.e. spin up as many jobs as there are CPU cores). You can change this by passing `-j ` to `run-tests.sh`. +## Running tests locally + +This test framework leverages the go stdlib test suite, so all ebpf tests can +be run as normal go tests, outside of the bluebox VMs: + +``` +#compile the test in advance, so we don't need root to have a go environment +go test -c +# run all tests +sudo ./testrunner.test +# run a single test +sudo ./testrunner.test -test.run TestEbpf/Tcpv6ConnectionClose +# run in verbose mode +sudo ./testrunner.test -test.run TestEbpf/Tcpv6ConnectionClose -test.v +``` + ## Building Kernels A dockerized setup is provided at `kernel_builder/` to build mainline kernel diff --git a/testing/run_tests.sh b/testing/run_tests.sh index 2f66f72d..10ace2f6 100755 --- a/testing/run_tests.sh +++ b/testing/run_tests.sh @@ -8,7 +8,7 @@ readonly PROGNAME=$(basename $0) readonly ARGS="$@" -readonly SUCCESS_STRING="ALL BPF TESTS PASSED" +readonly SUCCESS_STRING="exit status 0" readonly SUMMARY_FILE="bpf-check-summary.txt" readonly RESULTS_DIR="results" @@ -70,8 +70,7 @@ EOF } main() { - local arch=$1 - local artifacts="$2" + local jobs=$(nproc) while getopts "j:" opt; do @@ -85,6 +84,9 @@ main() { esac done + local arch=$1 + local artifacts="$2" + shift 2 is_empty $arch \ diff --git a/testing/scripts/gen_initramfs.sh b/testing/scripts/gen_initramfs.sh index 15b140c3..d8f3fcfb 100755 --- a/testing/scripts/gen_initramfs.sh +++ b/testing/scripts/gen_initramfs.sh @@ -37,7 +37,7 @@ build_testrunner() { pushd testrunner > /dev/null go clean - GOARCH=$goarch go build + GOARCH=$goarch CGO_ENABLED=0 go test -c -o testrunner -ldflags '-extldflags "-static"' if [[ $? -ne 0 ]] then @@ -78,7 +78,7 @@ invoke_bluebox() { local cmd="bluebox" cmd+=" -a $goarch" - cmd+=" -e testrunner/testrunner" + cmd+=" -e testrunner/testrunner:-test.v" cmd+=" -r $eventstrace" cmd+=" -r $tcfiltertests" cmd+=" -r $tcfilterbpf" diff --git a/testing/test_bins/common.h b/testing/test_bins/common.h index c8569441..55c60569 100644 --- a/testing/test_bins/common.h +++ b/testing/test_bins/common.h @@ -45,3 +45,20 @@ void gen_pid_info_json(char *buf, size_t size) (pid_t)syscall(SYS_gettid), getppid(), getpid(), getsid(0), getpgid(0), cap_permitted, cap_effective); } + +int dump_info(int client_port, int server_port) +{ + char pid_info[8192]; + gen_pid_info_json(pid_info, sizeof(pid_info)); + + char netns[128]; + ssize_t nbytes; + CHECK(nbytes = readlink("/proc/self/ns/net", netns, sizeof(netns)), -1); + netns[nbytes] = '\0'; + + uint64_t netns_inode; + sscanf(netns, "net:[%lu]", &netns_inode); + + printf("{ \"pid_info\": %s, \"client_port\": %d, \"server_port\": %d, \"netns\": %lu }\n", + pid_info, client_port, server_port, netns_inode); +} \ No newline at end of file diff --git a/testing/test_bins/create_rename_delete_file_container.c b/testing/test_bins/create_rename_delete_file_container.c index 1039f37d..16bc01e2 100644 --- a/testing/test_bins/create_rename_delete_file_container.c +++ b/testing/test_bins/create_rename_delete_file_container.c @@ -135,6 +135,11 @@ int main() cleanup: // Clean up directories created by child + + // in the 5.10 test kernels, this umount call fails, so don't check. + // Can't reproduce the issue locally, but `unmount` operations on overlayfs have historically + // been quirky. + umount2(ovl_mountpoint, MNT_FORCE); CHECK(rm_recursive(ovl_mountpoint), -1); CHECK(rm_recursive(ovl_upperdir), -1); CHECK(rm_recursive(ovl_lowerdir), -1); diff --git a/testing/test_bins/tcpv4_connect.c b/testing/test_bins/tcpv4_connect.c index 8e41d596..935dff34 100644 --- a/testing/test_bins/tcpv4_connect.c +++ b/testing/test_bins/tcpv4_connect.c @@ -25,23 +25,6 @@ #define BOUND_PORT 2048 -int dump_info(int client_port, int server_port) -{ - char pid_info[8192]; - gen_pid_info_json(pid_info, sizeof(pid_info)); - - char netns[128]; - ssize_t nbytes; - CHECK(nbytes = readlink("/proc/self/ns/net", netns, sizeof(netns)), -1); - netns[nbytes] = '\0'; - - uint64_t netns_inode; - sscanf(netns, "net:[%lu]", &netns_inode); - - printf("{ \"pid_info\": %s, \"client_port\": %d, \"server_port\": %d, \"netns\": %lu }\n", - pid_info, client_port, server_port, netns_inode); -} - int main() { struct sockaddr_in serveraddr; diff --git a/testing/test_bins/tcpv6_connect.c b/testing/test_bins/tcpv6_connect.c index d2ffb5c1..c29b2c96 100644 --- a/testing/test_bins/tcpv6_connect.c +++ b/testing/test_bins/tcpv6_connect.c @@ -25,23 +25,6 @@ #define BOUND_PORT 2048 -int dump_info(int client_port, int server_port) -{ - char pid_info[8192]; - gen_pid_info_json(pid_info, sizeof(pid_info)); - - char netns[128]; - ssize_t nbytes; - CHECK(nbytes = readlink("/proc/self/ns/net", netns, sizeof(netns)), -1); - netns[nbytes] = '\0'; - - uint64_t netns_inode; - sscanf(netns, "net:[%lu]", &netns_inode); - - printf("{ \"pid_info\": %s, \"client_port\": %d, \"server_port\": %d, \"netns\": %lu }\n", - pid_info, client_port, server_port, netns_inode); -} - int main() { struct sockaddr_in6 serveraddr; diff --git a/testing/test_bins/udp_send.c b/testing/test_bins/udp_send.c index 04a99d20..30e56d14 100644 --- a/testing/test_bins/udp_send.c +++ b/testing/test_bins/udp_send.c @@ -13,6 +13,8 @@ #include #include +#include "common.h" + int main(int argc, char *argv[]) { struct sockaddr_in sin; @@ -38,5 +40,7 @@ int main(int argc, char *argv[]) else if (n != sizeof(buf)) errx(1, "sendto: shortcount"); + dump_info(53, 0); + return (0); } \ No newline at end of file diff --git a/testing/testrunner/ebpf_test.go b/testing/testrunner/ebpf_test.go new file mode 100644 index 00000000..cc654eb4 --- /dev/null +++ b/testing/testrunner/ebpf_test.go @@ -0,0 +1,669 @@ +// SPDX-License-Identifier: Elastic-2.0 + +/* + * Copyright 2022 Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under + * one or more contributor license agreements. Licensed under the Elastic + * License 2.0; you may not use this file except in compliance with the Elastic + * License 2.0. + */ + +package testrunner + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func FeaturesCorrect(t *testing.T, et *Runner) { + var utsname syscall.Utsname + err := syscall.Uname(&utsname) + require.NoError(t, err) + + int8ArrayToString := func(arr [65]int8) string { + var buf []byte + for _, el := range arr { + if el == 0 { + break + } + buf = append(buf, byte(el)) + } + return string(buf) + } + contains := func(s []string, str string) bool { + for _, el := range s { + if el == str { + return true + } + } + return false + } + + arch := int8ArrayToString(utsname.Machine) + kernelVersion := int8ArrayToString(utsname.Release) + + switch arch { + case "x86_64": + // All x86 kernels in the CI test matrix currently enable bpf + // trampolines (it's super ubiquitous on x86 as far as I can see), so + // just assertTrue on BPF tramp support on x86. If a kernel is added + // that doesn't enable BPF tramps on x86, logic should be added to + // handle it here. + require.True(t, et.InitMsg.Features.BpfTramp) + case "aarch64": + hasBpfTramp := []string{"6.4.0", "6.4.16", "6.5.0"} + + if contains(hasBpfTramp, kernelVersion) { + require.True(t, et.InitMsg.Features.BpfTramp) + } else { + require.False(t, et.InitMsg.Features.BpfTramp) + } + default: + t.Fatalf("unknown arch %s, please add to the TestFeaturesCorrect test", arch) + } +} + +func ForkExit(t *testing.T, et *Runner) { + var binOutput TestPidInfo + runTestUnmarshalOutput(t, "fork_exit", &binOutput) + + var forkEvent ProcessForkEvent + for { + et.UnmarshalNextEvent(&forkEvent, "PROCESS_FORK") + + if forkEvent.ParentPids.Tid == binOutput.Tid { + break + } + } + + // Verify forkEvent.ParentPids against bin output + TestPidEqual(t, binOutput, forkEvent.ParentPids) + + // We don't have the child pid info but can do some internal validations + // knowing that the parent did a fork(), thus the child process is in the + // same process group / session but a different thread group + require.Equal(t, forkEvent.ChildPids.Ppid, forkEvent.ParentPids.Tgid) + require.Equal(t, forkEvent.ChildPids.Tid, forkEvent.ChildPids.Tgid) + require.Equal(t, forkEvent.ChildPids.Sid, forkEvent.ParentPids.Sid) + require.Equal(t, forkEvent.ChildPids.Pgid, forkEvent.ParentPids.Pgid) + require.NotEqual(t, forkEvent.ChildPids.Tgid, forkEvent.ParentPids.Tgid) +} + +func ForkExec(t *testing.T, et *Runner) { + if testBinaryPath != "/" { + t.Skipf("Test will not work outside test framework") + } + var binOutput struct { + ParentPidInfo TestPidInfo `json:"parent_info"` + ChildPid int64 `json:"child_pid"` + } + runTestUnmarshalOutput(t, "fork_exec", &binOutput) + + var forkEvent *ProcessForkEvent + var execEvent *ProcessExecEvent + // execEvent currently does not work outside the test environment; + // the calls to capset() break excevl() depending on the path passed to the call. + // we may want to rewrite that to use a more "correct" set of capabilities. + for forkEvent == nil || execEvent == nil { + line := et.GetNextEventOut("PROCESS_FORK", "PROCESS_EXEC") + + eventType := getEventType(t, line) + + switch eventType { + case "PROCESS_FORK": + if forkEvent == nil { + forkEvent = new(ProcessForkEvent) + err := json.Unmarshal([]byte(line), &forkEvent) + require.NoError(t, err, "error unmarshaling forkEvent") + + if forkEvent.ChildPids.Tgid != binOutput.ChildPid { + forkEvent = nil + } else { + t.Logf("got fork event...") + } + } + case "PROCESS_EXEC": + if execEvent == nil { + execEvent = new(ProcessExecEvent) + err := json.Unmarshal([]byte(line), &execEvent) + require.NoError(t, err, "error unmarshaling processExecEvent") + if execEvent.Pids.Tgid != binOutput.ChildPid { + execEvent = nil + } else { + t.Logf("got exec event...") + } + } + + } + } + + require.Equal(t, forkEvent.Creds.CapPermitted, uint64(0x00000000ffffffff)) + require.Equal(t, forkEvent.Creds.CapEffective, uint64(0x00000000f0f0f0f0)) + + require.Equal(t, execEvent.Creds.CapPermitted, uint64(0x000001ffffffffff)) + require.Equal(t, execEvent.Creds.CapEffective, uint64(0x000001ffffffffff)) + require.Equal(t, execEvent.FileName, "./do_nothing") + require.Equal(t, execEvent.Argv[0], "./do_nothing") + require.Equal(t, execEvent.Env[0], "TEST_ENV_KEY1=TEST_ENV_VAL1") + require.Equal(t, execEvent.Env[1], "TEST_ENV_KEY2=TEST_ENV_VAL2") + require.Equal(t, execEvent.Cwd, "/") + +} + +func FileCreate(t *testing.T, et *Runner) { + var binOutput struct { + PidInfo TestPidInfo `json:"pid_info"` + FileNameOrig string `json:"filename_orig"` + FileNameNew string `json:"filename_new"` + } + runTestUnmarshalOutput(t, "create_rename_delete_file", &binOutput) + + var fileCreateEvent FileCreateEvent + for { + et.UnmarshalNextEvent(&fileCreateEvent, "FILE_CREATE") + if fileCreateEvent.Pids.Tid == binOutput.PidInfo.Tid { + break + } + } + + TestPidEqual(t, binOutput.PidInfo, fileCreateEvent.Pids) + require.Equal(t, fileCreateEvent.Path, binOutput.FileNameOrig) + // File Info + require.Equal(t, fileCreateEvent.Finfo.Type, "FILE") + require.NotEqual(t, fileCreateEvent.Finfo.Inode, uint64(0)) + require.Equal(t, fileCreateEvent.Finfo.Mode, uint64(100644)) + require.Equal(t, fileCreateEvent.Finfo.Size, uint64(0)) + require.Equal(t, fileCreateEvent.Finfo.Uid, uint64(0)) + require.Equal(t, fileCreateEvent.Finfo.Gid, uint64(0)) +} + +func FileDelete(t *testing.T, et *Runner) { + + var binOutput struct { + PidInfo TestPidInfo `json:"pid_info"` + FileNameOrig string `json:"filename_orig"` + FileNameNew string `json:"filename_new"` + } + runTestUnmarshalOutput(t, "create_rename_delete_file", &binOutput) + + var fileDeleteEvent FileDeleteEvent + for { + et.UnmarshalNextEvent(&fileDeleteEvent, "FILE_DELETE") + if fileDeleteEvent.Pids.Tid == binOutput.PidInfo.Tid { + break + } + } + + TestPidEqual(t, binOutput.PidInfo, fileDeleteEvent.Pids) + require.Equal(t, fileDeleteEvent.Path, binOutput.FileNameNew) + // File Info + require.Equal(t, fileDeleteEvent.Finfo.Type, "FILE") + require.NotEqual(t, fileDeleteEvent.Finfo.Inode, 0) + require.Equal(t, fileDeleteEvent.Finfo.Mode, uint64(100777)) + require.Equal(t, fileDeleteEvent.Finfo.Size, uint64(0)) + require.Equal(t, fileDeleteEvent.Finfo.Uid, uint64(0)) + require.Equal(t, fileDeleteEvent.Finfo.Gid, uint64(0)) +} + +func FileRename(t *testing.T, et *Runner) { + var binOutput struct { + PidInfo TestPidInfo `json:"pid_info"` + FileNameOrig string `json:"filename_orig"` + FileNameNew string `json:"filename_new"` + } + runTestUnmarshalOutput(t, "create_rename_delete_file", &binOutput) + + var fileRenameEvent FileRenameEvent + for { + et.UnmarshalNextEvent(&fileRenameEvent, "FILE_RENAME") + if fileRenameEvent.Pids.Tid == binOutput.PidInfo.Tid { + break + } + } + + TestPidEqual(t, binOutput.PidInfo, fileRenameEvent.Pids) + require.Equal(t, fileRenameEvent.OldPath, binOutput.FileNameOrig) + require.Equal(t, fileRenameEvent.NewPath, binOutput.FileNameNew) + // File Info + require.Equal(t, fileRenameEvent.Finfo.Type, "FILE") + require.NotEqual(t, fileRenameEvent.Finfo.Inode, uint64(0)) + require.Equal(t, fileRenameEvent.Finfo.Mode, uint64(100644)) + require.Equal(t, fileRenameEvent.Finfo.Size, uint64(0)) + require.Equal(t, fileRenameEvent.Finfo.Uid, uint64(0)) + require.Equal(t, fileRenameEvent.Finfo.Gid, uint64(0)) +} + +func Setuid(t *testing.T, et *Runner) { + var binOutput struct { + PidInfo TestPidInfo `json:"pid_info"` + NewRuid int64 `json:"new_ruid"` + NewEuid int64 `json:"new_euid"` + } + runTestUnmarshalOutput(t, "setreuid", &binOutput) + + var setUidEvent SetUidEvent + for { + et.UnmarshalNextEvent(&setUidEvent, "PROCESS_SETUID") + if setUidEvent.Pids.Tid == binOutput.PidInfo.Tid { + break + } + } + + require.Equal(t, binOutput.NewRuid, setUidEvent.NewRuid) + require.Equal(t, binOutput.NewEuid, setUidEvent.NewEuid) + TestPidEqual(t, binOutput.PidInfo, setUidEvent.Pids) +} + +func Setgid(t *testing.T, et *Runner) { + var binOutput struct { + PidInfo TestPidInfo `json:"pid_info"` + NewRgid int64 `json:"new_rgid"` + NewEgid int64 `json:"new_egid"` + } + runTestUnmarshalOutput(t, "setregid", &binOutput) + + var setGidEvent SetGidEvent + for { + et.UnmarshalNextEvent(&setGidEvent, "PROCESS_SETGID") + if setGidEvent.Pids.Tid == binOutput.PidInfo.Tid { + break + } + } + + require.Equal(t, binOutput.NewRgid, setGidEvent.NewRgid) + require.Equal(t, binOutput.NewEgid, setGidEvent.NewEgid) + TestPidEqual(t, binOutput.PidInfo, setGidEvent.Pids) +} + +func FileCreateContainer(t *testing.T, et *Runner) { + var binOutput struct { + ChildPid int64 `json:"child_pid"` + FileNameOrig string `json:"filename_orig"` + FileNameNew string `json:"filename_new"` + } + runTestUnmarshalOutput(t, "create_rename_delete_file_container", &binOutput) + + var fileCreateEvent FileCreateEvent + for { + et.UnmarshalNextEvent(&fileCreateEvent, "FILE_CREATE") + if fileCreateEvent.Pids.Tgid == binOutput.ChildPid { + break + } + } + require.Equal(t, fileCreateEvent.Path, binOutput.FileNameOrig) +} + +func FileRenameContainer(t *testing.T, et *Runner) { + var binOutput struct { + ChildPid int64 `json:"child_pid"` + FileNameOrig string `json:"filename_orig"` + FileNameNew string `json:"filename_new"` + } + runTestUnmarshalOutput(t, "create_rename_delete_file_container", &binOutput) + + var fileRenameEvent FileRenameEvent + for { + et.UnmarshalNextEvent(&fileRenameEvent, "FILE_RENAME") + if fileRenameEvent.Pids.Tgid == binOutput.ChildPid { + break + } + } + + require.Equal(t, fileRenameEvent.OldPath, binOutput.FileNameOrig) + require.Equal(t, fileRenameEvent.NewPath, binOutput.FileNameNew) +} + +func FileDeleteContainer(t *testing.T, et *Runner) { + var binOutput struct { + ChildPid int64 `json:"child_pid"` + FileNameOrig string `json:"filename_orig"` + FileNameNew string `json:"filename_new"` + } + runTestUnmarshalOutput(t, "create_rename_delete_file_container", &binOutput) + + var fileDeleteEvent FileDeleteEvent + for { + et.UnmarshalNextEvent(&fileDeleteEvent, "FILE_DELETE") + if fileDeleteEvent.Pids.Tgid == binOutput.ChildPid { + break + } + } + + require.Equal(t, fileDeleteEvent.Path, binOutput.FileNameNew) +} + +func FileModify(t *testing.T, et *Runner) { + var binOutput struct { + PidInfo TestPidInfo `json:"pid_info"` + FileNameOrig string `json:"filename_orig"` + FileNameNew string `json:"filename_new"` + } + runTestUnmarshalOutput(t, "create_rename_delete_file", &binOutput) + + eventsCount := 4 // chmod, write, writev, truncate + events := make([]FileModifyEvent, 0, eventsCount) + for { + var event FileModifyEvent + et.UnmarshalNextEvent(&event, "FILE_MODIFY") + + if event.Pids.Tid == binOutput.PidInfo.Tid { + events = append(events, event) + eventsCount-- + if eventsCount == 0 { + break + } + } + } + + // chmod + require.Equal(t, events[0].Path, binOutput.FileNameNew) + require.Equal(t, events[0].ChangeType, "PERMISSIONS") + require.Equal(t, events[0].Finfo.Mode, uint64(100777)) + + // write + require.Equal(t, events[1].Path, binOutput.FileNameNew) + require.Equal(t, events[1].ChangeType, "CONTENT") + require.Equal(t, events[1].Finfo.Size, uint64(4)) + + // writev + require.Equal(t, events[2].Path, binOutput.FileNameNew) + require.Equal(t, events[2].ChangeType, "CONTENT") + require.Equal(t, events[2].Finfo.Size, uint64(4+5+5)) + + // truncate + require.Equal(t, events[3].Path, binOutput.FileNameNew) + require.Equal(t, events[3].ChangeType, "CONTENT") + require.Equal(t, events[3].Finfo.Size, uint64(0)) +} + +func TtyWrite(t *testing.T, et *Runner) { + var output struct { + Pid int64 `json:"pid"` + } + runTestUnmarshalOutput(t, "tty_write", &output) + + var ev TtyWriteEvent + for { + et.UnmarshalNextEvent(&ev, "PROCESS_TTY_WRITE") + if ev.Pids.Tgid == output.Pid { + break + } + } + + require.Equal(t, ev.Truncated, int64(0)) + require.Equal(t, ev.Out, "--- OK\n") + // This is a virtual console, not a pseudo terminal. + require.Equal(t, ev.TtyDev.Major, int64(4)) + require.Equal(t, ev.TtyDev.WinsizeRows, int64(0)) + require.Equal(t, ev.TtyDev.WinsizeCols, int64(0)) +} + +func Tcpv4ConnectionAttempt(t *testing.T, et *Runner) { + binOutput := NetBinOut{} + runTestUnmarshalOutput(t, "tcpv4_connect", &binOutput) + + var ev NetConnAttemptEvent + for { + et.UnmarshalNextEvent(&ev, "NETWORK_CONNECTION_ATTEMPTED") + if ev.Pids.Tgid == binOutput.PidInfo.Tgid { + break + } + } + + TestPidEqual(t, binOutput.PidInfo, ev.Pids) + require.Equal(t, ev.Net.Transport, "TCP") + require.Equal(t, ev.Net.Family, "AF_INET") + require.Equal(t, ev.Net.SourceAddr, "127.0.0.1") + require.Equal(t, ev.Net.SourcePort, binOutput.ClientPort) + require.Equal(t, ev.Net.DestAddr, "127.0.0.1") + require.Equal(t, ev.Net.DestPort, binOutput.ServerPort) + require.Equal(t, ev.Net.NetNs, binOutput.NetNs) + require.Equal(t, ev.Comm, "tcpv4_connect") + +} + +func Tcpv4ConnectionAccept(t *testing.T, et *Runner) { + binOutput := NetBinOut{} + runTestUnmarshalOutput(t, "tcpv4_connect", &binOutput) + + var ev NetConnAcceptEvent + for { + et.UnmarshalNextEvent(&ev, "NETWORK_CONNECTION_ACCEPTED") + if ev.Pids.Tgid == binOutput.PidInfo.Tgid { + break + } + } + + TestPidEqual(t, binOutput.PidInfo, ev.Pids) + require.Equal(t, ev.Net.Transport, "TCP") + require.Equal(t, ev.Net.Family, "AF_INET") + require.Equal(t, ev.Net.SourceAddr, "127.0.0.1") + require.Equal(t, ev.Net.SourcePort, binOutput.ServerPort) + require.Equal(t, ev.Net.DestAddr, "127.0.0.1") + require.Equal(t, ev.Net.DestPort, binOutput.ClientPort) + require.Equal(t, ev.Net.NetNs, binOutput.NetNs) + require.Equal(t, ev.Comm, "tcpv4_connect") +} + +func Tcpv4ConnectionClose(t *testing.T, et *Runner) { + binOutput := NetBinOut{} + runTestUnmarshalOutput(t, "tcpv4_connect", &binOutput) + + var ev NetConnCloseEvent + for { + et.UnmarshalNextEvent(&ev, "NETWORK_CONNECTION_CLOSED") + if ev.Pids.Tgid == binOutput.PidInfo.Tgid { + break + } + } + + // NETWORK_CONNECTION_CLOSED is an interesting case. + // + // While NETWORK_CONNECTION_ATTEMPTED is generated exclusively on the + // client-side via a connect(...) and NETWORK_CONNECTION_ACCEPTED is + // generated exclusively on the server side via an accept(...) + // NETWORK_CONNECTION_CLOSED may be generated on either side upon a + // close(...) of a socket fd. This means that the source and desination + // ports might be "flipped" depending on what side the connection is on + // (server/client) for a close event. + // + // Our tcpv4_connect binary creates a server and client socket on the same + // machine, so what port is reported as the source and destination port + // will vary depending on which socket is closed first (client / server). + // + // The test binary closes the server socket first, which counterintuitively + // results in the _client_ socket being torn down first in the kernel. + // Thus, our BPF probes report the source/dest ports from the client + // socket's point of view for the close event. The SourcePort and DestPort + // assertions below verify this is correct. + + TestPidEqual(t, binOutput.PidInfo, ev.Pids) + require.Equal(t, ev.Net.Transport, "TCP") + require.Equal(t, ev.Net.Family, "AF_INET") + require.Equal(t, ev.Net.SourceAddr, "127.0.0.1") + require.Equal(t, ev.Net.SourcePort, binOutput.ClientPort) + require.Equal(t, ev.Net.DestAddr, "127.0.0.1") + require.Equal(t, ev.Net.DestPort, binOutput.ServerPort) + require.Equal(t, ev.Net.NetNs, binOutput.NetNs) + require.Equal(t, ev.Comm, "tcpv4_connect") +} + +func Tcpv6ConnectionAttempt(t *testing.T, et *Runner) { + binOutput := NetBinOut{} + runTestUnmarshalOutput(t, "tcpv6_connect", &binOutput) + + var ev NetConnAttemptEvent + for { + et.UnmarshalNextEvent(&ev, "NETWORK_CONNECTION_ATTEMPTED") + if ev.Pids.Tgid == binOutput.PidInfo.Tgid { + break + } + } + + TestPidEqual(t, binOutput.PidInfo, ev.Pids) + require.Equal(t, ev.Net.Transport, "TCP") + require.Equal(t, ev.Net.Family, "AF_INET6") + require.Equal(t, ev.Net.SourceAddr, "::1") + require.Equal(t, ev.Net.SourcePort, binOutput.ClientPort) + require.Equal(t, ev.Net.DestAddr, "::1") + require.Equal(t, ev.Net.DestPort, binOutput.ServerPort) + require.Equal(t, ev.Net.NetNs, binOutput.NetNs) + require.Equal(t, ev.Comm, "tcpv6_connect") +} + +func Tcpv6ConnectionAccept(t *testing.T, et *Runner) { + binOutput := NetBinOut{} + runTestUnmarshalOutput(t, "tcpv6_connect", &binOutput) + + var ev NetConnAttemptEvent + for { + et.UnmarshalNextEvent(&ev, "NETWORK_CONNECTION_ACCEPTED") + if ev.Pids.Tgid == binOutput.PidInfo.Tgid { + break + } + } + + TestPidEqual(t, binOutput.PidInfo, ev.Pids) + require.Equal(t, ev.Net.Transport, "TCP") + require.Equal(t, ev.Net.Family, "AF_INET6") + require.Equal(t, ev.Net.SourceAddr, "::1") + require.Equal(t, ev.Net.SourcePort, binOutput.ServerPort) + require.Equal(t, ev.Net.DestAddr, "::1") + require.Equal(t, ev.Net.DestPort, binOutput.ClientPort) + require.Equal(t, ev.Net.NetNs, binOutput.NetNs) + require.Equal(t, ev.Comm, "tcpv6_connect") +} + +func Tcpv6ConnectionClose(t *testing.T, et *Runner) { + binOutput := NetBinOut{} + runTestUnmarshalOutput(t, "tcpv6_connect", &binOutput) + + var ev NetConnCloseEvent + for { + et.UnmarshalNextEvent(&ev, "NETWORK_CONNECTION_CLOSED") + if ev.Pids.Tgid == binOutput.PidInfo.Tgid { + break + } + } + + TestPidEqual(t, binOutput.PidInfo, ev.Pids) + require.Equal(t, ev.Net.Transport, "TCP") + require.Equal(t, ev.Net.Family, "AF_INET6") + require.Equal(t, ev.Net.SourceAddr, "::1") + require.Equal(t, ev.Net.SourcePort, binOutput.ClientPort) + require.Equal(t, ev.Net.DestAddr, "::1") + require.Equal(t, ev.Net.DestPort, binOutput.ServerPort) + require.Equal(t, ev.Net.NetNs, binOutput.NetNs) + require.Equal(t, ev.Comm, "tcpv6_connect") +} + +func DNSMonitor(t *testing.T, et *Runner) { + binOutput := NetBinOut{} + runTestUnmarshalOutput(t, "udp_send", &binOutput) + type dnsOutput struct { + Data []uint8 `json:"data"` + Pids PidInfo `json:"pids"` + Net NetInfo `json:"net"` + Comm string `json:"comm"` + } + runTestBin(t, "udp_send") + + lineData := dnsOutput{} + et.UnmarshalNextEvent(&lineData, "DNS_EVENT") + + require.Equal(t, lineData.Net.DestAddr, "127.0.0.1") + require.Equal(t, lineData.Net.SourceAddr, "127.0.0.1") + TestPidEqual(t, binOutput.PidInfo, lineData.Pids) + require.Equal(t, lineData.Net.Transport, "UDP") + require.Equal(t, lineData.Net.Family, "AF_INET") + + require.NotZero(t, lineData.Data[0]) + require.NotZero(t, lineData.Data[1]) +} + +func TcFilter(t *testing.T, et *Runner) { + // TC test is weird, and doesn't actually use the + // return-json-and-check-eventsTrace-output the other tests use + cmd := exec.Command(tcTestPath) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("ELASTIC_EBPF_TC_FILTER_OBJ_PATH=%s", tcObjPath)) + output, err := cmd.Output() + require.NoError(t, err, "error running Tc filter tests: %s\n", string(output)) +} + +func TestEbpf(t *testing.T) { + hasOverlayFS := IsOverlayFsSupported(t) + + testCases := []struct { + name string + handle func(t *testing.T, et *Runner) + args []string + requireOverlayFS bool + }{ + {"FeaturesCorrect", FeaturesCorrect, []string{}, false}, + {"ForkExit", ForkExit, []string{"--process-fork"}, false}, + {"ForkExec", ForkExec, []string{"--process-fork", "--process-exec"}, false}, + {"FileCreate", FileCreate, []string{"--file-create"}, false}, + {"FileDelete", FileDelete, []string{"--file-delete"}, false}, + {"FileRename", FileRename, []string{"--file-rename"}, false}, + {"Setuid", Setuid, []string{"--process-setuid"}, false}, + {"Setgid", Setgid, []string{"--process-setgid"}, false}, + {"FileModify", FileModify, []string{"--file-modify"}, false}, + {"TtyWrite", TtyWrite, []string{"--process-tty-write"}, false}, + {"Tcpv4ConnectionAttempt", Tcpv4ConnectionAttempt, []string{"--net-conn-attempt"}, false}, + {"Tcpv4ConnectionAccept", Tcpv4ConnectionAccept, []string{"--net-conn-accept"}, false}, + {"Tcpv4ConnectionClose", Tcpv4ConnectionClose, []string{"--net-conn-close"}, false}, + {"Tcpv6ConnectionAttempt", Tcpv6ConnectionAttempt, []string{"--net-conn-attempt"}, false}, + {"Tcpv6ConnectionAccept", Tcpv6ConnectionAccept, []string{"--net-conn-accept"}, false}, + {"Tcpv6ConnectionClose", Tcpv6ConnectionClose, []string{"--net-conn-close"}, false}, + {"DNSMonitor", DNSMonitor, []string{"--net-conn-dns-pkt"}, false}, + + {"TcFilter", TcFilter, []string{}, false}, + + {"FileCreateContainer", FileCreateContainer, []string{"--file-create"}, true}, + {"FileRenameContainer", FileRenameContainer, []string{"--file-rename"}, true}, + {"FileDeleteContainer", FileDeleteContainer, []string{"--file-delete"}, true}, + } + + failed := false + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + if test.requireOverlayFS && !hasOverlayFS { + t.Skipf("Test requires OverlayFS, not available") + } + if failed { + // small hack to make sure we don't continue to run tests when the first one fails, + // since a single test failure will dump tons of logs to the console + // we do this instead of a hard return in order to preserve an exit code + t.Skip("tests already failed") + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() + + run := NewEbpfRunner(ctx, t, test.args...) + // on return, check for failure. If we've failed, dump stderr and stdout + defer func() { + if t.Failed() { + PrintDebugOutputOnFail() + run.Dump() + failed = true + } + }() + + run.Start() + // actually run test + test.handle(t, run) + run.Stop() + }) + } + +} diff --git a/testing/testrunner/ebpfrunner.go b/testing/testrunner/ebpfrunner.go new file mode 100644 index 00000000..b565edf7 --- /dev/null +++ b/testing/testrunner/ebpfrunner.go @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Elastic-2.0 + +/* + * Copyright 2022 Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under + * one or more contributor license agreements. Licensed under the Elastic + * License 2.0; you may not use this file except in compliance with the Elastic + * License 2.0. + */ + +package testrunner + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "os/exec" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type Runner struct { + ctx context.Context + Cmd *exec.Cmd + Stdout io.ReadCloser + Stderr io.ReadCloser + + StdoutChan chan string + StderrChan chan string + readChan chan string + doneChan chan struct{} + errChan chan error + + InitMsg InitMsg + t *testing.T + + // buffers carry lifelong copy of stderr/stdout + errBuff []string + outBuff []string + readCursor int +} + +func runStreamChannel(sender chan string, errChan chan error, buffer *bufio.Scanner) { + buf := make([]byte, 0, 64*1024) + buffer.Buffer(buf, 1024*1024) + go func() { + for buffer.Scan() { + line := buffer.Text() + txt := strings.TrimSpace(line) + if len(txt) > 0 { + sender <- txt + + } + } + // the go testing libraries don't like it when you call + // t.Fail() in a child thread; so we have to trickle down the failure + if err := buffer.Err(); err != nil { + errChan <- fmt.Errorf("error in buffer: %w", err) + return + } + + }() +} + +func (runner *Runner) runIORead() { + runner.readCursor = 0 + defer func() { + close(runner.readChan) + }() + for { + // this select case must never block, or else the underlying write() syscalls in EventsTrace + // could block. + select { + case <-runner.doneChan: + return + case <-runner.ctx.Done(): + runner.t.Logf("got context done") + return + case line := <-runner.StderrChan: + runner.errBuff = append(runner.errBuff, line) + case line := <-runner.StdoutChan: + runner.outBuff = append(runner.outBuff, line) + select { + case runner.readChan <- runner.outBuff[runner.readCursor]: + runner.readCursor += 1 + default: + } + } + } +} + +func (runner *Runner) Start() { + err := runner.Cmd.Start() + require.NoError(runner.t, err, "error starting EventsTrace. You may need to run `make build` and `make package`") + stderrStream := bufio.NewScanner(runner.Stderr) + stdoutStream := bufio.NewScanner(runner.Stdout) + + runStreamChannel(runner.StdoutChan, runner.errChan, stdoutStream) + runStreamChannel(runner.StderrChan, runner.errChan, stderrStream) + + go func() { + runner.runIORead() + }() + + // run until we get the first log line + select { + case <-runner.ctx.Done(): + runner.t.Fatalf("timed out while waiting for initial response from EventsTrace") + case line := <-runner.readChan: + err := json.Unmarshal([]byte(line), &runner.InitMsg) + require.NoError(runner.t, err, "could not unmarshall json of first line. Stderr: \n", runner.errBuff) + } +} + +func (runner *Runner) GetNextEventOut(types ...string) string { + ctx, cancel := context.WithTimeout(runner.ctx, time.Minute) + defer cancel() + + type baseEvent struct { + EventType string `json:"event_type"` + } + + for { + select { + case <-ctx.Done(): + runner.t.Fatalf("timed out waiting for %v events", types) + case err := <-runner.errChan: + require.NoError(runner.t, err, "error reading from stdout/stderr in buffer") + case line := <-runner.readChan: + var resp baseEvent + err := json.Unmarshal([]byte(line), &resp) + require.NoError(runner.t, err, "error unmarshaling event_type from event %s", line) + for _, evtType := range types { + if evtType == resp.EventType { + return line + } + } + } + } +} + +func (runner *Runner) UnmarshalNextEvent(body any, types ...string) { + line := runner.GetNextEventOut(types...) + err := json.Unmarshal([]byte(line), &body) + require.NoError(runner.t, err, "error unmarshaling JSON for types %v", types) +} + +func (runner *Runner) Stop() { + runner.doneChan <- struct{}{} + err := runner.Cmd.Process.Kill() + require.NoError(runner.t, err) + + _, err = runner.Cmd.Process.Wait() + require.NoError(runner.t, err) +} + +func (runner *Runner) Dump() { + runner.t.Logf("STDOUT: \n") + for _, line := range runner.outBuff { + runner.t.Logf("%s", line) + } + runner.t.Logf("STDERR: \n") + for _, line := range runner.errBuff { + runner.t.Logf("%s", line) + } +} + +func NewEbpfRunner(ctx context.Context, t *testing.T, args ...string) *Runner { + testRunner := &Runner{ + ctx: ctx, + StdoutChan: make(chan string, 1024), + StderrChan: make(chan string, 1024), + readChan: make(chan string, 1024), + doneChan: make(chan struct{}), + errChan: make(chan error, 1), + t: t, + } + args = append(args, "--print-features-on-init", "--unbuffer-stdout", "--libbpf-verbose") + testRunner.Cmd = exec.CommandContext(ctx, eventsTracePath, args...) + + var err error + testRunner.Stdout, err = testRunner.Cmd.StdoutPipe() + require.NoError(t, err, "failed to redirect stdout") + + testRunner.Stderr, err = testRunner.Cmd.StderrPipe() + require.NoError(t, err, "failed to redirect stderr") + return testRunner +} diff --git a/testing/testrunner/eventstrace.go b/testing/testrunner/eventstrace.go deleted file mode 100644 index 04c81663..00000000 --- a/testing/testrunner/eventstrace.go +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-License-Identifier: Elastic-2.0 - -/* - * Copyright 2022 Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under - * one or more contributor license agreements. Licensed under the Elastic - * License 2.0; you may not use this file except in compliance with the Elastic - * License 2.0. - */ - -package main - -import ( - "bufio" - "context" - "encoding/json" - "fmt" - "io" - "os/exec" - "time" -) - -type EventsTraceInstance struct { - Cmd *exec.Cmd - Stdout io.ReadCloser - Stderr io.ReadCloser - StdoutChan chan string - StderrChan chan string - InitMsg InitMsg -} - -const streamChanSize = 200000 -const eventsTraceBinPath = "/EventsTrace" - -func (et *EventsTraceInstance) Start(ctx context.Context) { - if err := et.Cmd.Start(); err != nil { - fmt.Println("failed to start EventsTrace: ", err) - TestFail() - } - - readStreamFunc := func(streamCtx context.Context, c chan string, stream io.ReadCloser) { - defer close(c) - - for { - select { - case <-streamCtx.Done(): - return - default: - scanner := bufio.NewScanner(stream) - for scanner.Scan() { - select { - case c <- scanner.Text(): - break - default: - // If we don't have room in the channel, we _must_ drop - // incoming lines, otherwise EventsTrace will block - // forever trying to write to stdout/stderr and the - // test will time out - fmt.Println("dropped EventsTrace stdout/stderr due to full channel") - } - } - - if err := scanner.Err(); err != nil { - fmt.Println("failed to read from EventsTrace stdout: ", err) - return - } - } - } - } - - et.StdoutChan = make(chan string, streamChanSize) - et.StderrChan = make(chan string, streamChanSize) - - go readStreamFunc(ctx, et.StdoutChan, et.Stdout) - go readStreamFunc(ctx, et.StderrChan, et.Stderr) - - // Block until EventsTrace logs its "probes ready" line, indicating it's - // done loading - select { - case jsonLine := <-et.StdoutChan: - err := json.Unmarshal([]byte(jsonLine), &et.InitMsg) - if err != nil { - TestFail(fmt.Sprintf("Could not unmarshal EventsTrace init message: %s", err)) - } - break - case <-ctx.Done(): - et.DumpStderr() - TestFail("timed out waiting for EventsTrace to get ready, dumped stderr above") - } -} - -func (et *EventsTraceInstance) DumpStderr() { - fmt.Println("===== EventsTrace Stderr =====") - for line := range et.StderrChan { - fmt.Println(line) - } -} - -func (et *EventsTraceInstance) GetNextEventJson(types ...string) string { - var line string -loop: - for { - select { - case line = <-et.StdoutChan: - eventType, err := getJsonEventType(line) - if err != nil { - et.DumpStderr() - TestFail(fmt.Sprintf("Failed to unmarshal the following JSON: \"%s\": %s", line, err)) - } - - for _, a := range types { - if a == eventType { - break loop - } - } - case <-time.After(60 * time.Second): - et.DumpStderr() - TestFail("timed out waiting for EventsTrace output, dumped stderr above") - } - } - - return line -} - -func (et *EventsTraceInstance) Stop() error { - if err := et.Cmd.Process.Kill(); err != nil { - return err - } - - _, err := et.Cmd.Process.Wait() - return err -} - -func NewEventsTrace(ctx context.Context, args ...string) *EventsTraceInstance { - var et EventsTraceInstance - args = append(args, "--print-features-on-init", "--unbuffer-stdout", "--libbpf-verbose") - et.Cmd = exec.CommandContext(ctx, eventsTraceBinPath, args...) - - stdout, err := et.Cmd.StdoutPipe() - if err != nil { - fmt.Println("failed to redirect stdout: ", err) - TestFail() - } - et.Stdout = stdout - - stderr, err := et.Cmd.StderrPipe() - if err != nil { - fmt.Println("failed to redirect stderr: ", err) - TestFail() - } - et.Stderr = stderr - - return &et -} diff --git a/testing/testrunner/go.mod b/testing/testrunner/go.mod index b0bdf1a1..2ff52bcd 100644 --- a/testing/testrunner/go.mod +++ b/testing/testrunner/go.mod @@ -1,3 +1,10 @@ module github.com/elastic/ebpf/testrunner -go 1.17 +go 1.22 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/testing/testrunner/go.sum b/testing/testrunner/go.sum new file mode 100644 index 00000000..e20fa14b --- /dev/null +++ b/testing/testrunner/go.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/testing/testrunner/main.go b/testing/testrunner/main.go deleted file mode 100644 index e73781f1..00000000 --- a/testing/testrunner/main.go +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: Elastic-2.0 - -/* - * Copyright 2022 Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under - * one or more contributor license agreements. Licensed under the Elastic - * License 2.0; you may not use this file except in compliance with the Elastic - * License 2.0. - */ - -package main - -import "fmt" - -func main() { - RunEventsTest(TestFeaturesCorrect) - RunEventsTest(TestForkExit, "--process-fork") - RunEventsTest(TestForkExec, "--process-fork", "--process-exec") - RunEventsTest(TestSetuid, "--process-setuid") - RunEventsTest(TestSetgid, "--process-setgid") - RunEventsTest(TestTtyWrite, "--process-tty-write") - - RunEventsTest(TestFileCreate, "--file-create") - RunEventsTest(TestFileDelete, "--file-delete") - RunEventsTest(TestFileRename, "--file-rename") - RunEventsTest(TestFileModify, "--file-modify") - - RunEventsTest(TestTcpv4ConnectionAttempt, "--net-conn-attempt") - RunEventsTest(TestTcpv4ConnectionAccept, "--net-conn-accept") - RunEventsTest(TestTcpv4ConnectionClose, "--net-conn-close") - RunEventsTest(TestTcpv6ConnectionAttempt, "--net-conn-attempt") - RunEventsTest(TestTcpv6ConnectionAccept, "--net-conn-accept") - RunEventsTest(TestTcpv6ConnectionClose, "--net-conn-close") - RunEventsTest(TestDNSMonitor, "--net-conn-dns-pkt") - - RunTest(TestTcFilter) - - // These tests rely on overlayfs support. Distro kernels commonly compile - // overlayfs as a module, thus it's not available to us in our - // minimal/bzImage-only approach (attempting to mount an overlay fs will - // result in ENODEV if the module isn't loaded). The mainline kernel build - // script ensures overlayfs is compiled into the kernel, so just skip these - // tests if we're on a distro kernel that we can't use overlayfs on. - if IsOverlayFsSupported() { - RunEventsTest(TestFileCreateContainer, "--file-create") - RunEventsTest(TestFileRenameContainer, "--file-rename") - RunEventsTest(TestFileDeleteContainer, "--file-delete") - } else { - fmt.Println("Overlayfs kernel module not loaded, not running ovl tests") - } - - AllTestsPassed() -} diff --git a/testing/testrunner/tests.go b/testing/testrunner/tests.go deleted file mode 100644 index e254d8a3..00000000 --- a/testing/testrunner/tests.go +++ /dev/null @@ -1,741 +0,0 @@ -// SPDX-License-Identifier: Elastic-2.0 - -/* - * Copyright 2022 Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under - * one or more contributor license agreements. Licensed under the Elastic - * License 2.0; you may not use this file except in compliance with the Elastic - * License 2.0. - */ - -package main - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - "syscall" -) - -func TestFeaturesCorrect(et *EventsTraceInstance) { - var utsname syscall.Utsname - if err := syscall.Uname(&utsname); err != nil { - TestFail(fmt.Sprintf("Failed to run uname: %s", err)) - } - - int8ArrayToString := func(arr [65]int8) string { - var buf []byte - for _, el := range arr { - if el == 0 { - break - } - buf = append(buf, byte(el)) - } - return string(buf) - } - contains := func(s []string, str string) bool { - for _, el := range s { - if el == str { - return true - } - } - return false - } - - arch := int8ArrayToString(utsname.Machine) - kernelVersion := int8ArrayToString(utsname.Release) - - switch arch { - case "x86_64": - // All x86 kernels in the CI test matrix currently enable bpf - // trampolines (it's super ubiquitious on x86 as far as I can see), so - // just assertTrue on BPF tramp support on x86. If a kernel is added - // that doesn't enable BPF tramps on x86, logic should be added to - // handle it here. - AssertTrue(et.InitMsg.Features.BpfTramp) - case "aarch64": - hasBpfTramp := []string{"6.4.0", "6.4.16", "6.5.0"} - - if contains(hasBpfTramp, kernelVersion) { - AssertTrue(et.InitMsg.Features.BpfTramp) - } else { - AssertFalse(et.InitMsg.Features.BpfTramp) - } - default: - TestFail(fmt.Sprintf("unknown arch %s, please add to the TestFeaturesCorrect test", arch)) - } -} - -func TestForkExit(et *EventsTraceInstance) { - outputStr := runTestBin("fork_exit") - var binOutput TestPidInfo - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json: ", err) - } - - var forkEvent ProcessForkEvent - for { - line := et.GetNextEventJson("PROCESS_FORK") - - if err := json.Unmarshal([]byte(line), &forkEvent); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if forkEvent.ParentPids.Tid == binOutput.Tid { - break - } - } - - // Verify forkEvent.ParentPids against bin output - AssertPidInfoEqual(binOutput, forkEvent.ParentPids) - - // We don't have the child pid info but can do some internal validations - // knowing that the parent did a fork(), thus the child process is in the - // same process group / session but a different thread group - AssertInt64Equal(forkEvent.ChildPids.Ppid, forkEvent.ParentPids.Tgid) - AssertInt64Equal(forkEvent.ChildPids.Tid, forkEvent.ChildPids.Tgid) - AssertInt64Equal(forkEvent.ChildPids.Sid, forkEvent.ParentPids.Sid) - AssertInt64Equal(forkEvent.ChildPids.Pgid, forkEvent.ParentPids.Pgid) - AssertInt64NotEqual(forkEvent.ChildPids.Tgid, forkEvent.ParentPids.Tgid) -} - -func TestForkExec(et *EventsTraceInstance) { - outputStr := runTestBin("fork_exec") - var binOutput struct { - ParentPidInfo TestPidInfo `json:"parent_info"` - ChildPid int64 `json:"child_pid"` - } - - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var forkEvent *ProcessForkEvent - var execEvent *ProcessExecEvent - for forkEvent == nil || execEvent == nil { - line := et.GetNextEventJson("PROCESS_FORK", "PROCESS_EXEC") - - eventType, err := getJsonEventType(line) - if err != nil { - et.DumpStderr() - TestFail(fmt.Sprintf("Failed to unmarshal the following JSON: \"%s\": %s", line, err)) - } - - switch eventType { - case "PROCESS_FORK": - forkEvent = new(ProcessForkEvent) - if err := json.Unmarshal([]byte(line), &forkEvent); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - if forkEvent.ChildPids.Tgid != binOutput.ChildPid { - forkEvent = nil - } - case "PROCESS_EXEC": - execEvent = new(ProcessExecEvent) - if err := json.Unmarshal([]byte(line), &execEvent); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - if execEvent.Pids.Tgid != binOutput.ChildPid { - execEvent = nil - } - } - } - - AssertUint64Equal(uint64(forkEvent.Creds.CapPermitted), uint64(0x00000000ffffffff)) - AssertUint64Equal(uint64(forkEvent.Creds.CapEffective), uint64(0x00000000f0f0f0f0)) - AssertUint64Equal(uint64(execEvent.Creds.CapPermitted), uint64(0x000001ffffffffff)) - AssertUint64Equal(uint64(execEvent.Creds.CapEffective), uint64(0x000001ffffffffff)) - AssertStringsEqual(execEvent.FileName, "./do_nothing") - AssertStringsEqual(execEvent.Argv[0], "./do_nothing") - AssertStringsEqual(execEvent.Env[0], "TEST_ENV_KEY1=TEST_ENV_VAL1") - AssertStringsEqual(execEvent.Env[1], "TEST_ENV_KEY2=TEST_ENV_VAL2") - AssertStringsEqual(execEvent.Cwd, "/") -} - -func TestDNSMonitor(et *EventsTraceInstance) { - runTestBin("udp_send") - - type dnsOutput struct { - Data []uint8 `json:"data"` - NetConnAcceptEvent - } - - line := et.GetNextEventJson("DNS_EVENT") - lineData := dnsOutput{} - err := json.Unmarshal([]byte(line), &lineData) - if err != nil { - TestFail("failed to unmarshal JSON body", err) - } - - AssertStringsEqual(lineData.Net.Transport, "UDP") - AssertStringsEqual(lineData.Net.Family, "AF_INET") - // first two bytes of a DNS body will be the session ID for the query, and - // should not be zero - AssertNotZero(lineData.Data[0]) - AssertNotZero(lineData.Data[1]) - -} - -func TestFileCreate(et *EventsTraceInstance) { - outputStr := runTestBin("create_rename_delete_file") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - FileNameOrig string `json:"filename_orig"` - FileNameNew string `json:"filename_new"` - } - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var fileCreateEvent FileCreateEvent - for { - line := et.GetNextEventJson("FILE_CREATE") - if err := json.Unmarshal([]byte(line), &fileCreateEvent); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if fileCreateEvent.Pids.Tid == binOutput.PidInfo.Tid { - break - } - } - - AssertPidInfoEqual(binOutput.PidInfo, fileCreateEvent.Pids) - AssertStringsEqual(fileCreateEvent.Path, binOutput.FileNameOrig) - // File Info - AssertStringsEqual(fileCreateEvent.Finfo.Type, "FILE") - AssertUint64NotEqual(fileCreateEvent.Finfo.Inode, 0) - AssertUint64Equal(fileCreateEvent.Finfo.Mode, 100644) - AssertUint64Equal(fileCreateEvent.Finfo.Size, 0) - AssertUint64Equal(fileCreateEvent.Finfo.Uid, 0) - AssertUint64Equal(fileCreateEvent.Finfo.Gid, 0) -} - -func TestFileDelete(et *EventsTraceInstance) { - outputStr := runTestBin("create_rename_delete_file") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - FileNameOrig string `json:"filename_orig"` - FileNameNew string `json:"filename_new"` - } - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var fileDeleteEvent FileDeleteEvent - for { - line := et.GetNextEventJson("FILE_DELETE") - if err := json.Unmarshal([]byte(line), &fileDeleteEvent); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if fileDeleteEvent.Pids.Tid == binOutput.PidInfo.Tid { - break - } - } - - AssertPidInfoEqual(binOutput.PidInfo, fileDeleteEvent.Pids) - AssertStringsEqual(fileDeleteEvent.Path, binOutput.FileNameNew) - // File Info - AssertStringsEqual(fileDeleteEvent.Finfo.Type, "FILE") - AssertUint64NotEqual(fileDeleteEvent.Finfo.Inode, 0) - AssertUint64Equal(fileDeleteEvent.Finfo.Mode, 100777) - AssertUint64Equal(fileDeleteEvent.Finfo.Size, 0) - AssertUint64Equal(fileDeleteEvent.Finfo.Uid, 0) - AssertUint64Equal(fileDeleteEvent.Finfo.Gid, 0) -} - -func TestFileRename(et *EventsTraceInstance) { - outputStr := runTestBin("create_rename_delete_file") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - FileNameOrig string `json:"filename_orig"` - FileNameNew string `json:"filename_new"` - } - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var fileRenameEvent FileRenameEvent - for { - line := et.GetNextEventJson("FILE_RENAME") - if err := json.Unmarshal([]byte(line), &fileRenameEvent); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if fileRenameEvent.Pids.Tid == binOutput.PidInfo.Tid { - break - } - } - - AssertPidInfoEqual(binOutput.PidInfo, fileRenameEvent.Pids) - AssertStringsEqual(fileRenameEvent.OldPath, binOutput.FileNameOrig) - AssertStringsEqual(fileRenameEvent.NewPath, binOutput.FileNameNew) - // File Info - AssertStringsEqual(fileRenameEvent.Finfo.Type, "FILE") - AssertUint64NotEqual(fileRenameEvent.Finfo.Inode, 0) - AssertUint64Equal(fileRenameEvent.Finfo.Mode, 100644) - AssertUint64Equal(fileRenameEvent.Finfo.Size, 0) - AssertUint64Equal(fileRenameEvent.Finfo.Uid, 0) - AssertUint64Equal(fileRenameEvent.Finfo.Gid, 0) -} - -func TestSetuid(et *EventsTraceInstance) { - outputStr := runTestBin("setreuid") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - NewRuid int64 `json:"new_ruid"` - NewEuid int64 `json:"new_euid"` - } - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var setUidEvent SetUidEvent - for { - line := et.GetNextEventJson("PROCESS_SETUID") - if err := json.Unmarshal([]byte(line), &setUidEvent); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if setUidEvent.Pids.Tid == binOutput.PidInfo.Tid { - break - } - } - - AssertInt64Equal(binOutput.NewRuid, setUidEvent.NewRuid) - AssertInt64Equal(binOutput.NewEuid, setUidEvent.NewEuid) - AssertPidInfoEqual(binOutput.PidInfo, setUidEvent.Pids) -} - -func TestSetgid(et *EventsTraceInstance) { - outputStr := runTestBin("setregid") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - NewRgid int64 `json:"new_rgid"` - NewEgid int64 `json:"new_egid"` - } - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var setGidEvent SetGidEvent - for { - line := et.GetNextEventJson("PROCESS_SETGID") - if err := json.Unmarshal([]byte(line), &setGidEvent); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if setGidEvent.Pids.Tid == binOutput.PidInfo.Tid { - break - } - } - - AssertInt64Equal(binOutput.NewRgid, setGidEvent.NewRgid) - AssertInt64Equal(binOutput.NewEgid, setGidEvent.NewEgid) - AssertPidInfoEqual(binOutput.PidInfo, setGidEvent.Pids) -} - -func TestFileCreateContainer(et *EventsTraceInstance) { - outputStr := runTestBin("create_rename_delete_file_container") - var binOutput struct { - ChildPid int64 `json:"child_pid"` - FileNameOrig string `json:"filename_orig"` - FileNameNew string `json:"filename_new"` - } - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var fileCreateEvent FileCreateEvent - for { - line := et.GetNextEventJson("FILE_CREATE") - if err := json.Unmarshal([]byte(line), &fileCreateEvent); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if fileCreateEvent.Pids.Tgid == binOutput.ChildPid { - break - } - } - - AssertStringsEqual(fileCreateEvent.Path, binOutput.FileNameOrig) -} - -func TestFileRenameContainer(et *EventsTraceInstance) { - outputStr := runTestBin("create_rename_delete_file_container") - var binOutput struct { - ChildPid int64 `json:"child_pid"` - FileNameOrig string `json:"filename_orig"` - FileNameNew string `json:"filename_new"` - } - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var fileRenameEvent FileRenameEvent - for { - line := et.GetNextEventJson("FILE_RENAME") - if err := json.Unmarshal([]byte(line), &fileRenameEvent); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if fileRenameEvent.Pids.Tgid == binOutput.ChildPid { - break - } - } - - AssertStringsEqual(fileRenameEvent.OldPath, binOutput.FileNameOrig) - AssertStringsEqual(fileRenameEvent.NewPath, binOutput.FileNameNew) -} - -func TestFileDeleteContainer(et *EventsTraceInstance) { - outputStr := runTestBin("create_rename_delete_file_container") - var binOutput struct { - ChildPid int64 `json:"child_pid"` - FileNameOrig string `json:"filename_orig"` - FileNameNew string `json:"filename_new"` - } - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var fileDeleteEvent FileDeleteEvent - for { - line := et.GetNextEventJson("FILE_DELETE") - if err := json.Unmarshal([]byte(line), &fileDeleteEvent); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if fileDeleteEvent.Pids.Tgid == binOutput.ChildPid { - break - } - } - - AssertStringsEqual(fileDeleteEvent.Path, binOutput.FileNameNew) -} - -func TestFileModify(et *EventsTraceInstance) { - outputStr := runTestBin("create_rename_delete_file") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - FileNameOrig string `json:"filename_orig"` - FileNameNew string `json:"filename_new"` - } - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - eventsCount := 4 // chmod, write, writev, truncate - events := make([]FileModifyEvent, 0, eventsCount) - for { - var event FileModifyEvent - line := et.GetNextEventJson("FILE_MODIFY") - if err := json.Unmarshal([]byte(line), &event); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if event.Pids.Tid == binOutput.PidInfo.Tid { - events = append(events, event) - eventsCount-- - if eventsCount == 0 { - break - } - } - } - - // chmod - AssertStringsEqual(events[0].Path, binOutput.FileNameNew) - AssertStringsEqual(events[0].ChangeType, "PERMISSIONS") - AssertUint64Equal(events[0].Finfo.Mode, 100777) - - // write - AssertStringsEqual(events[1].Path, binOutput.FileNameNew) - AssertStringsEqual(events[1].ChangeType, "CONTENT") - AssertUint64Equal(events[1].Finfo.Size, 4) - - // writev - AssertStringsEqual(events[2].Path, binOutput.FileNameNew) - AssertStringsEqual(events[2].ChangeType, "CONTENT") - AssertUint64Equal(events[2].Finfo.Size, 4+5+5) - - // truncate - AssertStringsEqual(events[3].Path, binOutput.FileNameNew) - AssertStringsEqual(events[3].ChangeType, "CONTENT") - AssertUint64Equal(events[3].Finfo.Size, 0) -} - -func TestTtyWrite(et *EventsTraceInstance) { - out := runTestBin("tty_write") - var output struct { - Pid int64 `json:"pid"` - } - if err := json.Unmarshal(out, &output); err != nil { - TestFail("failed to unmarshal json", err) - } - - var ev TtyWriteEvent - for { - line := et.GetNextEventJson("PROCESS_TTY_WRITE") - if err := json.Unmarshal([]byte(line), &ev); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - if ev.Pids.Tgid == output.Pid { - break - } - } - - AssertInt64Equal(ev.Truncated, 0) - AssertStringsEqual(ev.Out, "--- OK\n") - // This is a virtual console, not a pseudo terminal. - AssertInt64Equal(ev.TtyDev.Major, 4) - AssertInt64Equal(ev.TtyDev.WinsizeRows, 0) - AssertInt64Equal(ev.TtyDev.WinsizeCols, 0) -} - -func TestTcpv4ConnectionAttempt(et *EventsTraceInstance) { - outputStr := runTestBin("tcpv4_connect") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - ClientPort int64 `json:"client_port"` - ServerPort int64 `json:"server_port"` - NetNs int64 `json:"netns"` - } - - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var ev NetConnAttemptEvent - for { - line := et.GetNextEventJson("NETWORK_CONNECTION_ATTEMPTED") - if err := json.Unmarshal([]byte(line), &ev); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if ev.Pids.Tgid == binOutput.PidInfo.Tgid { - break - } - } - - AssertPidInfoEqual(binOutput.PidInfo, ev.Pids) - AssertStringsEqual(ev.Net.Transport, "TCP") - AssertStringsEqual(ev.Net.Family, "AF_INET") - AssertStringsEqual(ev.Net.SourceAddr, "127.0.0.1") - AssertInt64Equal(ev.Net.SourcePort, binOutput.ClientPort) - AssertStringsEqual(ev.Net.DestAddr, "127.0.0.1") - AssertInt64Equal(ev.Net.DestPort, binOutput.ServerPort) - AssertInt64Equal(ev.Net.NetNs, binOutput.NetNs) - AssertStringsEqual(ev.Comm, "tcpv4_connect") -} - -func TestTcpv4ConnectionAccept(et *EventsTraceInstance) { - outputStr := runTestBin("tcpv4_connect") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - ClientPort int64 `json:"client_port"` - ServerPort int64 `json:"server_port"` - NetNs int64 `json:"netns"` - } - - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var ev NetConnAcceptEvent - for { - line := et.GetNextEventJson("NETWORK_CONNECTION_ACCEPTED") - if err := json.Unmarshal([]byte(line), &ev); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if ev.Pids.Tgid == binOutput.PidInfo.Tgid { - break - } - } - - AssertPidInfoEqual(binOutput.PidInfo, ev.Pids) - AssertStringsEqual(ev.Net.Transport, "TCP") - AssertStringsEqual(ev.Net.Family, "AF_INET") - AssertStringsEqual(ev.Net.SourceAddr, "127.0.0.1") - AssertInt64Equal(ev.Net.SourcePort, binOutput.ServerPort) - AssertStringsEqual(ev.Net.DestAddr, "127.0.0.1") - AssertInt64Equal(ev.Net.DestPort, binOutput.ClientPort) - AssertInt64Equal(ev.Net.NetNs, binOutput.NetNs) - AssertStringsEqual(ev.Comm, "tcpv4_connect") -} - -func TestTcpv4ConnectionClose(et *EventsTraceInstance) { - outputStr := runTestBin("tcpv4_connect") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - ClientPort int64 `json:"client_port"` - ServerPort int64 `json:"server_port"` - NetNs int64 `json:"netns"` - } - - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var ev NetConnCloseEvent - for { - line := et.GetNextEventJson("NETWORK_CONNECTION_CLOSED") - if err := json.Unmarshal([]byte(line), &ev); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if ev.Pids.Tgid == binOutput.PidInfo.Tgid { - break - } - } - - // NETWORK_CONNECTION_CLOSED is an interesting case. - // - // While NETWORK_CONNECTION_ATTEMPTED is generated exclusively on the - // client-side via a connect(...) and NETWORK_CONNECTION_ACCEPTED is - // generated exclusively on the server side via an accept(...) - // NETWORK_CONNECTION_CLOSED may be generated on either side upon a - // close(...) of a socket fd. This means that the source and desination - // ports might be "flipped" depending on what side the connection is on - // (server/client) for a close event. - // - // Our tcpv4_connect binary creates a server and client socket on the same - // machine, so what port is reported as the source and destination port - // will vary depending on which socket is closed first (client / server). - // - // The test binary closes the server socket first, which counterintuitively - // results in the _client_ socket being torn down first in the kernel. - // Thus, our BPF probes report the source/dest ports from the client - // socket's point of view for the close event. The SourcePort and DestPort - // assertions below verify this is correct. - - AssertPidInfoEqual(binOutput.PidInfo, ev.Pids) - AssertStringsEqual(ev.Net.Transport, "TCP") - AssertStringsEqual(ev.Net.Family, "AF_INET") - AssertStringsEqual(ev.Net.SourceAddr, "127.0.0.1") - AssertInt64Equal(ev.Net.SourcePort, binOutput.ClientPort) - AssertStringsEqual(ev.Net.DestAddr, "127.0.0.1") - AssertInt64Equal(ev.Net.DestPort, binOutput.ServerPort) - AssertInt64Equal(ev.Net.NetNs, binOutput.NetNs) - AssertStringsEqual(ev.Comm, "tcpv4_connect") -} - -func TestTcpv6ConnectionAttempt(et *EventsTraceInstance) { - outputStr := runTestBin("tcpv6_connect") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - ClientPort int64 `json:"client_port"` - ServerPort int64 `json:"server_port"` - NetNs int64 `json:"netns"` - } - - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var ev NetConnAttemptEvent - for { - line := et.GetNextEventJson("NETWORK_CONNECTION_ATTEMPTED") - if err := json.Unmarshal([]byte(line), &ev); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if ev.Pids.Tgid == binOutput.PidInfo.Tgid { - break - } - } - - AssertPidInfoEqual(binOutput.PidInfo, ev.Pids) - AssertStringsEqual(ev.Net.Transport, "TCP") - AssertStringsEqual(ev.Net.Family, "AF_INET6") - AssertStringsEqual(ev.Net.SourceAddr, "::1") - AssertInt64Equal(ev.Net.SourcePort, binOutput.ClientPort) - AssertStringsEqual(ev.Net.DestAddr, "::1") - AssertInt64Equal(ev.Net.DestPort, binOutput.ServerPort) - AssertInt64Equal(ev.Net.NetNs, binOutput.NetNs) - AssertStringsEqual(ev.Comm, "tcpv6_connect") -} - -func TestTcpv6ConnectionAccept(et *EventsTraceInstance) { - outputStr := runTestBin("tcpv6_connect") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - ClientPort int64 `json:"client_port"` - ServerPort int64 `json:"server_port"` - NetNs int64 `json:"netns"` - } - - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var ev NetConnAttemptEvent - for { - line := et.GetNextEventJson("NETWORK_CONNECTION_ACCEPTED") - if err := json.Unmarshal([]byte(line), &ev); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if ev.Pids.Tgid == binOutput.PidInfo.Tgid { - break - } - } - - AssertPidInfoEqual(binOutput.PidInfo, ev.Pids) - AssertStringsEqual(ev.Net.Transport, "TCP") - AssertStringsEqual(ev.Net.Family, "AF_INET6") - AssertStringsEqual(ev.Net.SourceAddr, "::1") - AssertInt64Equal(ev.Net.SourcePort, binOutput.ServerPort) - AssertStringsEqual(ev.Net.DestAddr, "::1") - AssertInt64Equal(ev.Net.DestPort, binOutput.ClientPort) - AssertInt64Equal(ev.Net.NetNs, binOutput.NetNs) - AssertStringsEqual(ev.Comm, "tcpv6_connect") -} - -func TestTcpv6ConnectionClose(et *EventsTraceInstance) { - outputStr := runTestBin("tcpv6_connect") - var binOutput struct { - PidInfo TestPidInfo `json:"pid_info"` - ClientPort int64 `json:"client_port"` - ServerPort int64 `json:"server_port"` - NetNs int64 `json:"netns"` - } - - if err := json.Unmarshal(outputStr, &binOutput); err != nil { - TestFail("failed to unmarshal json", err) - } - - var ev NetConnCloseEvent - for { - line := et.GetNextEventJson("NETWORK_CONNECTION_CLOSED") - if err := json.Unmarshal([]byte(line), &ev); err != nil { - TestFail("failed to unmarshal JSON: ", err) - } - - if ev.Pids.Tgid == binOutput.PidInfo.Tgid { - break - } - } - - AssertPidInfoEqual(binOutput.PidInfo, ev.Pids) - AssertStringsEqual(ev.Net.Transport, "TCP") - AssertStringsEqual(ev.Net.Family, "AF_INET6") - AssertStringsEqual(ev.Net.SourceAddr, "::1") - AssertInt64Equal(ev.Net.SourcePort, binOutput.ClientPort) - AssertStringsEqual(ev.Net.DestAddr, "::1") - AssertInt64Equal(ev.Net.DestPort, binOutput.ServerPort) - AssertInt64Equal(ev.Net.NetNs, binOutput.NetNs) - AssertStringsEqual(ev.Comm, "tcpv6_connect") -} - -func TestTcFilter() { - cmd := exec.Command("/BPFTcFilterTests") - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, "ELASTIC_EBPF_TC_FILTER_OBJ_PATH=/TcFilter.bpf.o") - output, err := cmd.Output() - - if err != nil { - fmt.Println(string(output)) - TestFail(fmt.Sprintf("BPFTcFilterTests failed: %s", err)) - } -} diff --git a/testing/testrunner/utils.go b/testing/testrunner/utils.go index 345d04b1..301ed07a 100644 --- a/testing/testrunner/utils.go +++ b/testing/testrunner/utils.go @@ -7,20 +7,21 @@ * License 2.0. */ -package main +package testrunner import ( "bufio" - "context" "encoding/json" "fmt" "io" "os" "os/exec" - "reflect" + "path/filepath" "runtime" "strings" - "time" + "testing" + + "github.com/stretchr/testify/require" ) // This is a JSON type printed by the test binaries (not by EventsTrace), it's @@ -174,98 +175,124 @@ type NetConnAcceptEvent struct { Comm string `json:"comm"` } +type NetBinOut struct { + PidInfo TestPidInfo `json:"pid_info"` + ClientPort int64 `json:"client_port"` + ServerPort int64 `json:"server_port"` + NetNs int64 `json:"netns"` +} + type NetConnCloseEvent struct { Pids PidInfo `json:"pids"` Net NetInfo `json:"net"` Comm string `json:"comm"` } -func getJsonEventType(jsonLine string) (string, error) { - var jsonUnmarshaled struct { - EventType string `json:"event_type"` - } +// path to the test binaries we use to create events for EventsTrace +var testBinaryPath = "/" - err := json.Unmarshal([]byte(jsonLine), &jsonUnmarshaled) +// path to the EventsTrace binary +var eventsTracePath = "/EventsTrace" + +// Path to the TC filter test binary and probe. This one is weird and lives outside the rest of the test binaries +var tcTestPath = "/BPFTcFilterTests" +var tcObjPath = "/TcFilter.bpf.o" + +// init will run at startup and figure out if we're running in the bluebox test env or not, +// and set paths for the binaries as needed +func init() { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + gitRootPath, err := cmd.CombinedOutput() + // if there's an error, assume that we're in the test environment, + // and we're using the root path if err != nil { - return "", err + fmt.Printf("using root path '%s' for test binary path\n", testBinaryPath) + return } + // if we have a root path, create the path to test_bins + // convert GOARCH values to the gcc tuple values + var arch string + switch runtime.GOARCH { + case "amd64": + arch = "x86_64" + case "arm64": + arch = "aarch64" + default: + fmt.Printf("unsupported arch %s, reverting to root path for test binaries\n", runtime.GOARCH) + return + } + rootEbpfPath := strings.TrimSpace(string(gitRootPath)) + testBinaryPath = filepath.Join(rootEbpfPath, "testing/test_bins/bin", arch) + fmt.Printf("using root path '%s' for binary path\n", testBinaryPath) - return jsonUnmarshaled.EventType, nil -} - -func runTestBin(binName string) []byte { - cmd := exec.Command(fmt.Sprintf("/%s", binName)) + // if running in a non-root path, assume we're not in bluebox, set binary path accordingly - output, err := cmd.Output() - if err != nil { - fmt.Printf("===== stderr of %s =====\n", binName) - fmt.Println(err) - fmt.Printf("===== end stderr of %s =====\n", binName) + artifactDir := fmt.Sprintf("artifacts-%s", arch) + eventsTracePath = filepath.Join(rootEbpfPath, artifactDir, "package/bin/EventsTrace") + tcTestPath = filepath.Join(rootEbpfPath, artifactDir, "package/bin/BPFTcFilterTests") + tcObjPath = filepath.Join(rootEbpfPath, artifactDir, "package/probes/TcFilter.bpf.o") - fmt.Printf("===== stdout of %s =====\n", binName) - fmt.Println(string(output)) - fmt.Printf("===== end stdout of %s =====\n", binName) + fmt.Printf("using path '%s' for EventsTrace\n", eventsTracePath) + fmt.Printf("using path '%s' for BPFTcFilterTests\n", tcTestPath) +} - TestFail(fmt.Sprintf("Could not run test binary %s (see output above)", binName)) +func getEventType(t *testing.T, jsonLine string) string { + var jsonUnmarshaled struct { + EventType string `json:"event_type"` } - return output -} + err := json.Unmarshal([]byte(jsonLine), &jsonUnmarshaled) + require.NoError(t, err, "error unmarshaling JSON to fetch event type") -func AssertPidInfoEqual(tpi TestPidInfo, pi PidInfo) { - AssertInt64Equal(pi.Tid, tpi.Tid) - AssertInt64Equal(pi.Tgid, tpi.Tgid) - AssertInt64Equal(pi.Ppid, tpi.Ppid) - AssertInt64Equal(pi.Pgid, tpi.Pgid) - AssertInt64Equal(pi.Sid, tpi.Sid) + return jsonUnmarshaled.EventType } -func AssertNotZero(a uint8) { - if a == 0 { - TestFail(fmt.Sprintf("Test assertion failed %d == 0", a)) - } -} +func runTestBin(t *testing.T, binName string, args ...string) []byte { + cmd := exec.Command(filepath.Join(testBinaryPath, binName), args...) -func AssertTrue(val bool) { - if !val { - TestFail(fmt.Sprintf("Expected %t to be true", val)) + output, err := cmd.CombinedOutput() + // the "correct" way to do this would be errors.Is(), but it doesn't seem to work reliably for the errors that exec returns + if err != nil { + if strings.Contains(err.Error(), "no such file") { + require.NoError(t, err, "test binary %s not found; try `make testbins` to compile test binaries", binName) + } } -} -func AssertFalse(val bool) { - if val { - TestFail(fmt.Sprintf("Expected %t to be false", val)) - } + require.NoError(t, err, "error running command %s\n OUTPUT: \n %s", binName, string(output)) + return output } -func AssertStringsEqual(a, b string) { - if a != b { - TestFail(fmt.Sprintf("Test assertion failed %s != %s", a, b)) - } +func runTestUnmarshalOutput(t *testing.T, binName string, body any) { + raw := runTestBin(t, binName) + err := json.Unmarshal(raw, &body) + require.NoError(t, err, "error unmarshaling output from %s, got:\n %s", binName, string(raw)) } -func AssertInt64Equal(a, b int64) { - if a != b { - TestFail(fmt.Sprintf("Test assertion failed %d != %d", a, b)) - } +func TestPidEqual(t *testing.T, tpi TestPidInfo, pi PidInfo) { + require.Equal(t, pi.Tid, tpi.Tid) + require.Equal(t, pi.Tgid, tpi.Tgid) + require.Equal(t, pi.Ppid, tpi.Ppid) + require.Equal(t, pi.Pgid, tpi.Pgid) + require.Equal(t, pi.Sid, tpi.Sid) } -func AssertInt64NotEqual(a, b int64) { - if a == b { - TestFail(fmt.Sprintf("Test assertion failed %d == %d", a, b)) - } -} +func IsOverlayFsSupported(t *testing.T) bool { + file, err := os.Open("/proc/filesystems") + require.NoError(t, err) + defer file.Close() -func AssertUint64Equal(a, b uint64) { - if a != b { - TestFail(fmt.Sprintf("Test assertion failed 0x%016x != 0x%016x", a, b)) + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasSuffix(line, "overlay") { + return true + } } -} -func AssertUint64NotEqual(a, b uint64) { - if a == b { - TestFail(fmt.Sprintf("Test assertion failed 0x%016x == 0x%016x", a, b)) - } + err = scanner.Err() + require.NoError(t, err) + + return false } func PrintBPFDebugOutput() { @@ -285,21 +312,7 @@ func PrintBPFDebugOutput() { fmt.Print(string(b)) } -func TestFail(v ...interface{}) { - fmt.Println(v...) - - fmt.Println("===== STACKTRACE FOR FAILED TEST =====") - // Don't use debug.PrintStack here. It prints to stderr, which can cause - // Bluebox's init process to Log the stderr/stdout lines out of order (this - // is hard on the eyes when reading). Instead manually print the stacktrace - // to stdout so everything is going to the same stream and is serialized - // nicely. - b := make([]byte, 16384) - n := runtime.Stack(b, false) - s := string(b[:n]) - fmt.Print(s) - fmt.Println("===== END STACKTRACE FOR FAILED TEST =====") - +func PrintDebugOutputOnFail() { fmt.Println("===== CONTENTS OF /sys/kernel/debug/tracing/trace =====") PrintBPFDebugOutput() fmt.Println("===== END CONTENTS OF /sys/kernel/debug/tracing/trace =====") @@ -313,56 +326,4 @@ func TestFail(v ...interface{}) { fmt.Print("\n") fmt.Println("BPF test failed, see errors and stacktrace above") - os.Exit(1) -} - -func AllTestsPassed() { - fmt.Println("ALL BPF TESTS PASSED") -} - -func IsOverlayFsSupported() bool { - file, err := os.Open("/proc/filesystems") - if err != nil { - TestFail(fmt.Sprintf("Could not open /proc/filesystems: %s", err)) - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if strings.HasSuffix(line, "overlay") { - return true - } - } - - if err := scanner.Err(); err != nil { - TestFail(fmt.Sprintf("Could not read from /proc/filesystems: %s", err)) - } - - return false -} - -func RunTest(f func()) { - testFuncName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() - f() // Will dump info and shutdown if test fails - fmt.Println("test passed: ", testFuncName) -} - -func RunEventsTest(f func(*EventsTraceInstance), args ...string) { - testFuncName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() - ctx, cancel := context.WithTimeout(context.TODO(), 90*time.Second) - - et := NewEventsTrace(ctx, args...) - et.Start(ctx) - - f(et) // Will dump info and shutdown if test fails - - // Shuts down eventstrace and goroutines listening on stdout/stderr - cancel() - - fmt.Println("test passed: ", testFuncName) - - if err := et.Stop(); err != nil { - TestFail(fmt.Sprintf("Could not stop EventsTrace binary: %s", err)) - } }