Tests

Efficient and thorough testing is key to ensure bpfilter is stable and reliable. As manual testing is time-consuming and error-prone, bpfilter relies on automated tests to validate its features. Different type of tests are implemented to cover all the components of the project:

  • Unit tests to test every function part of the public API of libbpfilter.

  • End-to-end tests to validate bfcli’s language, and the behaviour of the BPF programs generated by bpfilter.

  • Integration tests to ensure libbpfilter and the core module can be integrated into other projects without issues.

  • Check tests to validate code style and quality.

bpfilter uses CMake and CTest to manage and run its tests, a test suite is defined for each type of test. You can run the tests using:

# Run all the tests
make -C $BUILD_DIR test_bin test

# Run a specific test suite (unit, e2e, integration, check)
make -C $BUILD_DIR $TEST_SUITE

# Run a single test: use the test's path under tests/, replacing / with .
# e.g. tests/e2e/matchers/ip4_daddr.cpp becomes e2e.matchers.ip4_daddr
ctest --test-dir $BUILD_DIR -R $TEST_NAME --output-on-failure

Each test suite has a corresponding make target to build its binaries (e.g. make -C $BUILD_DIR test_bin builds all test binaries). By default, CTest doesn’t build the test binaries when running tests.

Test coverage is disabled by default, to enable it, configure CMake with the -DWITH_COVERAGE=1 flag. Then, you can generate a coverage report using: make -C $BUILD_DIR coverage doc.

Unit tests

Unit tests are limited to the libbpfilter library. They are designed to validate the different components of the library in isolation, using mocks and fakes when necessary. We expect every public function of the library to be covered by at least one unit test.

Adding a new unit test

  1. Create a new source file to contain the unit test under tests/unit. You can use existing unit tests as examples. Usually, a source file in tests/unit will contain tests for a single source file in libbpfilter.

  2. Add the new source file to the tests/unit/CMakeLists.txt file using the bf_add_c_test function.

  3. Implement the unit tests using the cmocka testing library. You can use existing unit tests as examples.

CMocka supports test fixtures to setup and teardown common test data. You can use them to avoid code duplication when multiple tests require the same setup.

Example

The example below is used to test the bf_version() function from libbpfilter. All the tests source file must include the test.h header, define a main function, and register the tests to be run.

/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
*/

#include <bpfilter/version.h>

#include "test.h"

static void get_version(void **state)
{
    (void)state;

    assert_non_null(bf_version());
}

int main(void)
{
    const struct CMUnitTest tests[] = {
        cmocka_unit_test(get_version),
    };

    return cmocka_run_group_tests(tests, NULL, NULL);
}

End-to-end tests

End-to-end tests are designed to validate bpfilter’s behaviour as seen by the user. They cover the entire stack, from bfcli to the BPF programs generated by bpfilter. End-to-end tests ensure that the filtering rules defined using bfcli are correctly translated into BPF programs, and that these programs behave as expected when executed.

Adding a new end-to-end test

  1. Create a new source file to contain the end-to-end test under tests/e2e.

  2. Call bf_add_e2e_shell_test() in tests/e2e/CMakeLists.txt to add the new source file to the end-to-end test suite.

  3. In your test, use the functions provided by e2e_test_util.sh to setup the test environment. Any shell command failure will stop the test immediately and return a failure.

Example

e2e_test_util.sh provides functions to create a sandboxed environment. Here is an example of a simple end-to-end test that creates a sandbox:

#!/usr/bin/env bash

set -eux
set -o pipefail

. "$(dirname "$0")"/../e2e_test_util.sh

make_sandbox

# Ping the sandbox's IPv4 address from the sandboxed namespace
${FROM_NS} ping -c 1 -W 0.1 ${NS_IP_ADDR}

Matcher tests

Matcher tests (e2e.matchers.*) validate that the BPF programs generated by bpfilter correctly match packets. They use BPF_PROG_TEST_RUN to run the generated programs against crafted packet buffers in kernel space, without requiring network namespaces or real traffic.

Each matcher test is a C++ binary located under tests/e2e/matchers/ that builds chains with specific matchers, crafts packets using the bft::Packet builder, and asserts the BPF program’s verdict using bft_assert_prog_run(). Tests are automatically parameterized across all hooks that support the matcher under test using the MatcherTestsSuite class.

Adding a new matcher test

  1. Create a new C++ source file under tests/e2e/matchers/ (e.g. tests/e2e/matchers/my_matcher.cpp).

  2. Add a call to bf_add_e2e_c_test() in tests/e2e/CMakeLists.txt with your new test.

  3. Use bf::Chain, bf::Rule, bf::Matcher from tests/harness/ to build chains, and bft::Ethernet, bft::IPv4, bft::TCP, etc. from tests/harness/Packet.hpp to craft packets.

  4. Use MatcherTestsSuite from tests/harness/test.hpp to automatically run the test across all supported hooks.

Example

#include "Chain.hpp"
#include "Matcher.hpp"
#include "Rule.hpp"
#include "test.hpp"

extern "C" {
#include <bpfilter/bpfilter.h>
}

static void my_matcher_eq(void **state)
{
    auto *test = static_cast<MatcherTest *>(*state);

    BFT_CHAIN_SET(
        bf::Chain("test_chain", test->hook(), BF_VERDICT_ACCEPT)
        << bf::Rule(BF_VERDICT_DROP, true, {},
                    {bf::Matcher(BF_MATCHER_MY_TYPE, BF_MATCHER_EQ, {42})}));

    // Packet matching the rule -> DROP
    bft_assert_prog_run(
        "test_chain", test->hook(),
        bft::Ethernet() /
            bft::IPv4 {.saddr = "127.0.0.1", .daddr = "127.0.0.2"} /
            bft::TCP {.dport = 42},
        test->verdictDrop());

    // Packet not matching -> ACCEPT (policy)
    bft_assert_prog_run(
        "test_chain", test->hook(),
        bft::Ethernet() /
            bft::IPv4 {.saddr = "127.0.0.1", .daddr = "127.0.0.2"} /
            bft::TCP {.dport = 80},
        test->verdictAccept());
}

int main()
{
    auto suite = MatcherTestsSuite(BF_MATCHER_MY_TYPE);

    suite << MatcherTest(BF_MATCHER_MY_TYPE, BF_MATCHER_EQ, my_matcher_eq);

    return suite.run();
}

Integration tests

Integration tests are designed to ensure that libbpfilter and the core bpfilter module can be integrated into other projects without issues. They validate that the public API of libbpfilter is stable and behaves as expected when used in different contexts.

Check tests

Check tests leverage the clang-tidy and clang-format tools to ensure code quality and style consistency across the codebase. They help identify potential issues early in the development process. Any warning or error reported by these tools will cause the check tests to fail.

Harness

The test harness is a set of convenience functions used to ease testing of bpfilter.

Test

Defines

assert_ok(expr)
assert_err(expr)
assert_int_gt(expr, ref)
assert_int_gte(expr, ref)
assert_int_lt(expr, ref)
assert_int_lte(expr, ref)
assert_enum_to_str(type, to_str, first, max)
assert_enum_to_from_str(type, to_str, from_str, first, max)
assert_fd_equal(fd0, fd1)
assert_fd_empty(fd)
assert_rule_equal(rule0, rule1)
bft_streams_flush(streams)

Functions

bool bft_list_eq(const bf_list *lhs, const bf_list *rhs, bft_list_eq_cb cb)

Compare two bf_list objects.

Parameters:
  • lhs – First list to compare.

  • rhs – Second list to compare.

  • cb – Callback used to compare the nodes payload. If NULL, the node’s payload is not compared. If set, cb is called with the payload of lhs and rhs node, for each node.

Returns:

True if both lists are equal, false otherwise.

bool bft_set_eq(const struct bf_set *lhs, const struct bf_set *rhs)
bool bft_counter_eq(const struct bf_counter *lhs, const struct bf_counter *rhs)
bool bft_chain_equal(const struct bf_chain *chain0, const struct bf_chain *chain1)
bool bft_rule_equal(const struct bf_rule *rule0, const struct bf_rule *rule1)
bool bft_matcher_equal(const struct bf_matcher *matcher0, const struct bf_matcher *matcher1)
int btf_setup_redirect_streams(void **state)
int bft_teardown_redirect_streams(void **state)
int bft_streams_new(struct bft_streams **streams)
void bft_stream_free(struct bft_streams **streams)
int btf_setup_create_sockets(void **state)
int bft_teardown_close_sockets(void **state)
int bft_sockets_new(struct bft_sockets **sockets)
void bft_sockets_free(struct bft_sockets **sockets)
void bft_assert_counter_eq(const char *chain_name, size_t rule_idx, uint64_t packets, int64_t bytes)

Assert that a rule’s counter matches expected values.

Fetches the counters for chain_name, walks to the counter at rule_idx, and asserts that the packet count equals packets. If bytes >= 0, the byte count is also checked.

Parameters:
  • chain_name – Name of the chain to query. Can’t be NULL.

  • rule_idx – Zero-based index of the rule whose counter to check.

  • packets – Expected packet count.

  • bytes – Expected byte count, or -1 to skip the check.

int btf_setup_create_tmpdir(void **state)
int bft_teardown_close_tmpdir(void **state)
int bft_tmpdir_new(struct bft_tmpdir **tmpdir)
void bft_tmpdir_free(struct bft_tmpdir **tmpdir)
int bft_hook_accept(enum bf_hook hook)

Return the BPF “accept” return value for the given hook.

Parameters:
  • hook – Hook to query.

Returns:

The flavor-specific accept verdict (e.g. XDP_PASS, NF_ACCEPT).

int bft_hook_drop(enum bf_hook hook)

Return the BPF “drop” return value for the given hook.

Parameters:
  • hook – Hook to query.

Returns:

The flavor-specific drop verdict (e.g. XDP_DROP, NF_DROP).

int bft_hook_next(enum bf_hook hook)

Return the BPF “next” return value for the given hook.

Maps BF_VERDICT_NEXT to its flavor-specific return code: TCX_NEXT (-1) for TC, XDP_PASS for XDP, NF_ACCEPT for NF, and 1 for cgroup_skb.

Parameters:
  • hook – Hook to query.

Returns:

The flavor-specific next verdict.

struct bft_streams
#include <harness/test.h>

Public Members

FILE *new_stdout
FILE *old_stdout
char *stdout_buf
size_t stdout_len
FILE *new_stderr
FILE *old_stderr
char *stderr_buf
size_t stderr_len
struct bft_sockets
#include <harness/test.h>

Public Members

int client_fd
int server_fd
struct bft_tmpdir
#include <harness/test.h>

Public Members

char path_template[1024]
char *dir_path

Mocks

Mock functions are used to wrap a system call or an external library function in order to simplify the test of a libbpfilter function, or prevent it from modifying the underlying system.

Technicalities

Mock functions from bpfilter or from the standard library. Mocking function allows the tester to call a stub and force the function to return a predefined value. Mocks can be used to trigger a specific code path or prevent a system call (which would modify the system or require elevated privileges).

Mocks must be declared in harness/mock.h with bf_test_mock_declare() and implemented in harness/mock.c with bf_test_mock_define(). Then, add the mocked function to bf_test_mock() in harness/CMakeLists.txt.

In your tests, create the mock with bf_test_mock_get(function, retval). retval is the value you expect the mock to return when called. By default, the mock expects to return this value only once and never be called again. To configure a different behavior, use bf_test_mock_get_empty() and bf_test_mock_will_return() or bf_test_mock_will_return_always(). Use _clean_bf_test_mock_ to limit your mock to the current scope.

Using a mock to ensure _bf_print_msg_new() fails if malloc() fails:

// Create a mock for malloc which will return NULL once.
_clean_bf_test_mock bf_test_mock _ bf_test_mock_get(malloc, NULL);

// Expect the function to fail if malloc fails.
assert_error(_bf_printer_msg_new(&msg));

This module also defines convenience function to simulate a runtime environment such as creating a temporary file to serialize the context into.

MOCKING IS ONLY TO MOCK, not to trigger different code path during testing -> KISS

Defines

_clean_bft_mock_
bft_mock_declare(fn)
bft_mock_get(name)
bft_mock_real(mock)
bft_mock_define(x)

Functions

void bft_mock_clean(bft_mock *mock)
void bft_mock_btf__load_vmlinux_btf_enable(void)
void bft_mock_btf__load_vmlinux_btf_disable(void)
bool bft_mock_btf__load_vmlinux_btf_is_enabled(void)
void bft_mock_isatty_enable(void)
void bft_mock_isatty_disable(void)
bool bft_mock_isatty_is_enabled(void)
void bft_mock_setns_enable(void)
void bft_mock_setns_disable(void)
bool bft_mock_setns_is_enabled(void)
void bft_mock_syscall_enable(void)
void bft_mock_syscall_disable(void)
bool bft_mock_syscall_is_enabled(void)
void bft_mock_syscall_set_retval(long retval)
long bft_mock_syscall_get_retval(void)
struct bft_mock
#include <harness/mock.h>

Public Members

void (*disable)(void)
const char *wrap_name

Fake

Typedefs

typedef bool (*bft_list_eq_cb)(const void*, const void*)
typedef int (*bft_list_dummy_inserter)(bf_list*, void*)

Functions

bf_list *bft_list_dummy(size_t len, bft_list_dummy_inserter inserter)

Create a test list with fake data.

The list will be filled with len elements, each element being a pointer to size_t value. The pointed values will be from 0 to len - 1.

The free and pack callbacks are populated, so the list can be cleaned up and serialized, list any other list.

Parameters:
  • len – Number of elements to insert in the list.

  • inserter – Callback to insert data in the list. bf_list_add_tail or bf_list_add_head can be used. Can be NULL if len is 0.

Returns:

A pointer to a valid list on success, or a NULL pointer on failure.

int bft_list_dummy_pack(const void *data, bf_wpack_t *pack)

Packing callback for bft_list_dummy node’s payload.

Parameters:
  • data – Node’s payload to pack.

  • pack – Packing object.

Returns:

0 on success, or a negative error value on failure.

bool bft_list_dummy_eq(const void *lhs, const void *rhs)

Comparaison callback for lists filled using bft_list_dummy.

Parameters:
  • lhs – First list’s node payload.

  • rhs – Second list’s node payload.

Returns:

True if the lhs is equal to rhs, false otherwise.

const void *bft_get_randomly_filled_buffer(size_t len)
struct bf_chain *bft_chain_dummy(bool with_rules)
struct bf_rule *bft_rule_dummy(size_t n_matchers)
struct bf_matcher *bft_matcher_dummy(const void *data, size_t data_len)
struct bf_set *bft_set_dummy(size_t n_elems)