Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

ebpftest

ebpftest package contains Golang utilities and helpers that simplify testing of eBPF programs from Golang userpace.

One can invoke many eBPF program types using BPF_PROG_TEST_RUN kernel feature. It is exposed as .Test and .Run methods in cilium/ebpf's ebpf.Program.

This package additionally offers:

  • helpers to be used in tests, such as SkipIfIncapable, RequireNoError, and RequireVariableEqual;
  • Mock that brings mocking functionality to eBPF.

SkipIfIncapable

Skips current test if not running as root.

func TestFrob(t *testing.T) {
	ebpftest.SkipIfIncapable(t)
    ...
}

Many eBPF features are unavailable unless running as root (or bearing additional capabilities such as CAP_BPF). Golang developers have a habit of running tests in a project in bulk via go test ./... Gate eBPF tests with SkipIfIncapable to avoid misleading failures.

Note: one can run eBPF tests with go test -exec sudo. Another option to consider is vimto. It is more secure, allows to use a specific kernel version, and is reasonably fast as long as KVM-based virtualization is available.

RequireNoError

Asserts that a function returned no error, similar to testify's require.NoError. We suggest to use this helper when loading eBPF programs (either separately or as a part of a collection).

ebpf.VerifierError from cilium/ebpf outputs condensed summary when formatted. This helper ensures that a complete verifier log is captured to aid troubleshooting.

SetVariable and RequireVariableEqual

Manipulate global eBPF variables.

cilium/ebpf exposes global variables as ebpf.Variable instances. They are available in ebpf.Collection and are also included in structures generated by bpf2go.

Variables are handy when testing individual bits of eBPF code unit-test style. Imagine we wish to excercise a function such as

static int froblinate(const struct foo *input, struct bar *output);

Unfortunately, froblinate is not a proper eBPF program, we need a wrapper:

struct foo test_input;
struct bar test_output.
SEC("tc") int test_froblinate(struct __sk_buff *ctx) {
    return froblinate(&test_input, &test_output);
}

Use SetVariable on TestInput to provide test input.

Use RequireVaribaleEqual on TestOutput to verify the result.

Mock

eBPF code can get complex. As with other software, it is a good idea to cover individual units with tests in addition to excercising the whole thing end to end. It would be nice if we could replace some calls with stubs when testing complex functions.

This package allows Golang tests to replace a eBPF program or sub-program on-demand with a stub, without having to modify eBPF code. In addition, the user can:

  • assert that a stub has been called;
  • assert input arguments;
  • specify the return value of the stub.

Mocking tail calls

"A tail call is a mechanism that allows eBPF authors to break up their logic into multiple parts and go from one to the other."

SEC("tc") int foo(struct __sk_buff *ctx) {
    return TCX_PASS;
}

struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 1);
    __type(key, int);
    __type(value, int);
    __array(values, int (struct __sk_buff *));
} progs SEC(".maps") = {
    .values = { foo }
};

SEC("tc") int mainfunc(struct __sk_buff *ctx) {
    // complex logic HERE

    bpf_tail_call(ctx, &progs, 0); // jumps to foo
    return TCX_DROP;
}

A supposedly complex mainfunc defers to foo for certain inputs.

We wish to excercise mainfunc in isolation for a more robust and focused test. Mock can replace foo with a stub, enabling the test to assert that foo was called. The real foo is skipped, the test observes the resulting packet exactly as mainfunc left it.

To set up a mock, list functions to intercept:

type FunctionsToMock struct {
    Foo func(any) `ebpf:"foo"`
}

Use ebpftest.NewMock to instantiate a mock. It rewrites the spec to implement intercepts:

func Test(t *testing.T) {
    var spec *ebpf.CollectionSpec
    spec = loadSpec()
    mock = ebpftest.NewMock[FunctionsToMock](t, spec)
    ...
	mock.Expect().Foo(ebpftest.Any)
}

Use the augmented spec to instantiate eBPF objects as usual. Communicate expected calls with mock.Expect().

Note: FunctionsToMock includes as many functions as needed. A test fails if an expected call is missing or an unexpected call was performed.

Mocking subprograms

One can break up the logic into multiple parts with regular functions as well. The feature is known as "eBPF subprograms", available since kernel 5.2:

struct params {
    int bar;
};

int foo(struct __sk_buff *ctx, struct params *params) {
    return 0;
}

SEC("tc") int mainfunc(struct __sk_buff *ctx) {
    // complex logic HERE

    struct params params = { .bar = 42 };
    if (foo(ctx, &params) < 0) {
        return TCX_DROP;
    }
    return TCX_PASS;
}

List functions to intercept, similarly to mocking tail calls. The number of function parameters must match eBPF definition:

type FunctionsToMock struct {
    Foo func(any, any) `ebpf:"foo"`
}

Use ebpftest.NewMock to instantiate a mock. It rewrites the spec to implement intercepts:

type Params struct {
    Bar int32
}

func Test(t *testing.T) {
    var spec *ebpf.CollectionSpec
    spec = loadSpec()
    mock = ebpftest.NewMock[FunctionsToMock](t, spec)
    ...
	mock.Expect().Foo(ebpftest.Any, &Params{Bar: 42})
}

Use the augmented spec to instantiate eBPF objects as usual. Communicate expected calls with mock.Expect():

mock.Expect().Foo(ebpftest.Any, &Params{Bar: 42})

Stubs return 0 by default. One can set a different return value by declaring a function with ebpftest.Res[T] result. To simulate foo failure:

type FunctionsToMock struct {
    Foo func(any, any) ebpftest.Res[int32] `ebpf:"foo"`
}

mock.Expect().Foo(ebpftest.Any, ebpftest.Any).Return(-1)

So far we declared parameters in FunctionsToMock as any. Declaring a paramater with a specific type instead of any is also an option. It makes a test easier to maintain thanks to static type checking, but ebpftest.Any is no longer accepted:

type FunctionsToMock struct {
    Foo func(any, *Params) `ebpf:"foo"`
}

Limitations

  • It is only possible to assert that a function was called once or was not called at all.
  • No custom logic in stubs (i.e. no equivalent of .Do like in gomock).