diff --git a/libbpfgo.c b/libbpfgo.c index 053711e1..d347865b 100644 --- a/libbpfgo.c +++ b/libbpfgo.c @@ -165,6 +165,49 @@ void cgo_bpf_iter_attach_opts_free(struct bpf_iter_attach_opts *opts) free(opts); } +struct bpf_test_run_opts *cgo_bpf_test_run_opts_new(const void *data_in, + void *data_out, + __u32 data_size_in, + __u32 data_size_out, + const void *ctx_in, + void *ctx_out, + __u32 ctx_size_in, + __u32 ctx_size_out, + int repeat, + __u32 flags, + __u32 cpu, + __u32 batch_size) +{ + struct bpf_test_run_opts *opts; + opts = calloc(1, sizeof(*opts)); + if (!opts) + return NULL; + + opts->sz = sizeof(*opts); + opts->data_in = data_in; + opts->data_out = data_out; + opts->data_size_in = data_size_in; + opts->data_size_out = data_size_out; + opts->ctx_in = ctx_in; + opts->ctx_out = ctx_out; + opts->ctx_size_in = ctx_size_in; + opts->ctx_size_out = ctx_size_out; + opts->repeat = repeat; + opts->flags = flags; + opts->cpu = cpu; + opts->batch_size = batch_size; + + return opts; +} + +void cgo_bpf_test_run_opts_free(struct bpf_test_run_opts *opts) +{ + if (!opts) + return; + + free(opts); +} + struct bpf_object_open_opts *cgo_bpf_object_open_opts_new(const char *btf_file_path, const char *kconfig_path, const char *bpf_obj_name, diff --git a/libbpfgo.h b/libbpfgo.h index de936ec4..4286da5d 100644 --- a/libbpfgo.h +++ b/libbpfgo.h @@ -41,6 +41,20 @@ struct bpf_iter_attach_opts *cgo_bpf_iter_attach_opts_new(__u32 map_fd, __u32 pid_fd); void cgo_bpf_iter_attach_opts_free(struct bpf_iter_attach_opts *opts); +struct bpf_test_run_opts *cgo_bpf_test_run_opts_new(const void *data_in, + void *data_out, + __u32 data_size_in, + __u32 data_size_out, + const void *ctx_in, + void *ctx_out, + __u32 ctx_size_in, + __u32 ctx_size_out, + int repeat, + __u32 flags, + __u32 cpu, + __u32 batch_size); +void cgo_bpf_test_run_opts_free(struct bpf_test_run_opts *opts); + struct bpf_object_open_opts *cgo_bpf_object_open_opts_new(const char *btf_file_path, const char *kconfig_path, const char *bpf_obj_name, diff --git a/prog.go b/prog.go index 6c14909e..e47c8ecf 100644 --- a/prog.go +++ b/prog.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strings" "syscall" + "time" "unsafe" ) @@ -629,3 +630,160 @@ func (p *BPFProg) DetachGenericFD(targetFd int, attachType BPFAttachType) error return nil } + +// +// BPF_PROG_TEST_RUN +// + +type RunFlag uint32 + +const ( + RunFlagRunOnCPU RunFlag = C.BPF_F_TEST_RUN_ON_CPU + RunFlagXDPLiveFrames RunFlag = C.BPF_F_TEST_XDP_LIVE_FRAMES +) + +// RunOpts mirrors the C structure bpf_test_run_opts. +type RunOpts struct { + DataIn []byte + DataOut []byte + DataSizeIn uint32 + DataSizeOut uint32 + CtxIn []byte + CtxOut []byte + CtxSizeIn uint32 + CtxSizeOut uint32 + RetVal uint32 + Repeat int + Duration time.Duration + Flags RunFlag + CPU uint32 + BatchSize uint32 +} + +func runOptsToC(runOpts *RunOpts) (*C.struct_bpf_test_run_opts, error) { + if runOpts == nil { + return nil, nil + } + + var ( + dataIn unsafe.Pointer + dataSizeIn C.uint + dataOut unsafe.Pointer + dataSizeOut C.uint + ctxIn unsafe.Pointer + ctxSizeIn C.uint + ctxOut unsafe.Pointer + ctxSizeOut C.uint + ) + + if runOpts.DataIn != nil { + dataIn = unsafe.Pointer(&runOpts.DataIn[0]) + dataSizeIn = C.uint(runOpts.DataSizeIn) + } + if runOpts.DataOut != nil { + dataOut = unsafe.Pointer(&runOpts.DataOut[0]) + dataSizeOut = C.uint(runOpts.DataSizeOut) + } + if runOpts.CtxIn != nil { + ctxIn = unsafe.Pointer(&runOpts.CtxIn[0]) + ctxSizeIn = C.uint(runOpts.CtxSizeIn) + } + if runOpts.CtxOut != nil { + ctxOut = unsafe.Pointer(&runOpts.CtxOut[0]) + ctxSizeOut = C.uint(runOpts.CtxSizeOut) + } + optsC, errno := C.cgo_bpf_test_run_opts_new( + dataIn, + dataOut, + dataSizeIn, + dataSizeOut, + ctxIn, + ctxOut, + ctxSizeIn, + ctxSizeOut, + C.int(runOpts.Repeat), + C.uint(runOpts.Flags), + C.uint(runOpts.CPU), + C.uint(runOpts.BatchSize), + ) + if optsC == nil { + return nil, fmt.Errorf("failed to create bpf_test_run_opts: %w", errno) + } + + return optsC, nil +} + +func runOptsFromC(runOpts *RunOpts, optsC *C.struct_bpf_test_run_opts) { + if optsC == nil { + return + } + + if optsC.data_in != nil { + runOpts.DataIn = C.GoBytes(optsC.data_in, C.int(optsC.data_size_in)) + } + if optsC.data_out != nil { + runOpts.DataOut = C.GoBytes(optsC.data_out, C.int(optsC.data_size_out)) + } + if optsC.ctx_in != nil { + runOpts.CtxIn = C.GoBytes(optsC.ctx_in, C.int(optsC.ctx_size_in)) + } + if optsC.ctx_out != nil { + runOpts.CtxOut = C.GoBytes(optsC.ctx_out, C.int(optsC.ctx_size_out)) + } + + runOpts.RetVal = uint32(optsC.retval) + runOpts.Repeat = int(optsC.repeat) + runOpts.Duration = time.Duration(optsC.duration) * time.Nanosecond + runOpts.Flags = RunFlag(optsC.flags) + runOpts.CPU = uint32(optsC.cpu) + runOpts.BatchSize = uint32(optsC.batch_size) +} + +// Run executes the eBPF program without attaching it to actual hooks, filling +// the results in the provided RunOpts. +// Reference: +// - https://docs.kernel.org/bpf/bpf_prog_run.html +// - https://docs.kernel.org/userspace-api/ebpf/syscall.html +// +// Example Usage: +// +// /* +// SEC("tc") +// int test(struct __sk_buff *skb) +// { +// return foo() ? 1 : 0; +// } +// */ +// +// func TestFunc(t *testing.T) { +// ... +// prog, _ := module.GetProgram("test") +// opts := RunOpts{ +// DataIn: make([]byte, 0, 14), +// DataSizeIn: 14, +// DataOut: make([]byte, 0, 14), +// DataSizeOut: 14, +// Repeat: 1, +// } +// prog.Run(&opts) +// if opts.RetVal != 1 { +// t.Errorf("result = %d; want 1", opts.RetVal) +// } +// } +func (p *BPFProg) Run(opts *RunOpts) error { + optsC, err := runOptsToC(opts) + if err != nil { + return err + } + defer C.cgo_bpf_test_run_opts_free(optsC) + + retC := C.bpf_prog_test_run_opts(C.int(p.FileDescriptor()), optsC) + if retC < 0 { + return fmt.Errorf("failed to run program: %w", syscall.Errno(-retC)) + } + + // update runOpts with the values from the kernel and libbpf + runOptsFromC(opts, optsC) + + return nil +} diff --git a/selftest/common/run-4.12.sh b/selftest/common/run-4.12.sh new file mode 100755 index 00000000..e1b0669c --- /dev/null +++ b/selftest/common/run-4.12.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# SETTINGS + +TEST=$(dirname $0)/$1 # execute +TIMEOUT=10 # seconds + +# COMMON + +COMMON="$(dirname $0)/../common/common.sh" +[[ -f $COMMON ]] && { . $COMMON; } || { error "no common"; exit 1; } + +# MAIN + +kern_version gt 4.12 + +check_build +check_ppid +test_exec +test_finish + +exit 0 diff --git a/selftest/prog-run/Makefile b/selftest/prog-run/Makefile new file mode 120000 index 00000000..d981720c --- /dev/null +++ b/selftest/prog-run/Makefile @@ -0,0 +1 @@ +../common/Makefile \ No newline at end of file diff --git a/selftest/prog-run/go.mod b/selftest/prog-run/go.mod new file mode 100644 index 00000000..1071f201 --- /dev/null +++ b/selftest/prog-run/go.mod @@ -0,0 +1,7 @@ +module github.com/aquasecurity/libbpfgo/selftest/testrun + +go 1.21 + +require github.com/aquasecurity/libbpfgo v0.0.0 + +replace github.com/aquasecurity/libbpfgo => ../../ diff --git a/selftest/prog-run/go.sum b/selftest/prog-run/go.sum new file mode 100644 index 00000000..5496456e --- /dev/null +++ b/selftest/prog-run/go.sum @@ -0,0 +1,8 @@ +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/selftest/prog-run/main.bpf.c b/selftest/prog-run/main.bpf.c new file mode 100644 index 00000000..acea88e4 --- /dev/null +++ b/selftest/prog-run/main.bpf.c @@ -0,0 +1,27 @@ +//+build ignore + +#include + +#include +#include + +SEC("tc") +int test_tc(struct __sk_buff *skb) +{ + void *data = (void *) (long) skb->data; + void *data_end = (void *) (long) skb->data_end; + if (data + 4 > data_end) { + return -1; + } + + if (*(__u32 *) data == 0xdeadbeef) { + char new_data[] = {0x01, 0x02, 0x03, 0x04}; + bpf_skb_store_bytes(skb, 0, new_data, 4, 0); + bpf_skb_change_tail(skb, 14, 0); + return 1; + } + + return 2; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/selftest/prog-run/main.go b/selftest/prog-run/main.go new file mode 100644 index 00000000..32fde9c8 --- /dev/null +++ b/selftest/prog-run/main.go @@ -0,0 +1,52 @@ +package main + +import "C" + +import ( + "encoding/binary" + "log" + + bpf "github.com/aquasecurity/libbpfgo" +) + +func main() { + bpfModule, err := bpf.NewModuleFromFile("main.bpf.o") + if err != nil { + log.Fatalf("Failed to load BPF module: %v", err) + } + defer bpfModule.Close() + + err = bpfModule.BPFLoadObject() + if err != nil { + log.Fatalf("Failed to load object: %v", err) + } + + tcProg, err := bpfModule.GetProgram("test_tc") + if err != nil || tcProg == nil { + log.Fatalf("Failed to get prog test_tc: %v", err) + } + + dataIn := make([]byte, 16) + binary.LittleEndian.PutUint32(dataIn, 0xdeadbeef) + opts := bpf.RunOpts{ + DataIn: dataIn, + DataSizeIn: 16, + DataOut: make([]byte, 32), + DataSizeOut: 32, + Repeat: 1, + } + + err = tcProg.Run(&opts) + if err != nil { + log.Fatalf("Failed to run prog: %v", err) + } + if opts.RetVal != 1 { + log.Fatalf("retVal %d should be 1", opts.RetVal) + } + if len(opts.DataOut) != 14 { + log.Fatalf("dataOut len %v should be 14", opts.DataOut) + } + if binary.LittleEndian.Uint32(opts.DataOut) != 0x04030201 { + log.Fatalf("dataOut 0x%x should be 0x04030201", binary.LittleEndian.Uint32(opts.DataOut)) + } +} diff --git a/selftest/prog-run/run.sh b/selftest/prog-run/run.sh new file mode 120000 index 00000000..5f379d6e --- /dev/null +++ b/selftest/prog-run/run.sh @@ -0,0 +1 @@ +../common/run-4.12.sh \ No newline at end of file