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 bybpfilter.Kernel tests to run the end-to-end suite in virtme-ng with specific kernel releases.
Integration tests to ensure
libbpfilterand 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:
# Build and run all tests
make -C $BUILD_DIR test -j5
# 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
The test target and the suite targets will run as many tests at once as
make runs jobs: -j5 runs up to five tests concurrently, a bare -j
uses all the CPUs, and -j1 (or no -j at all) runs them one at a time.
The job count can also come from MAKEFLAGS, but -j on the command line
takes precedence: with MAKEFLAGS=-j8 in your environment, make test -j1
still runs one test at a time. This needs the Unix Makefiles generator.
Each shell end-to-end test invocation uses unique temporary directories, network namespace names, interface names, and cgroup paths. Privileged shell tests also run in separate host network and mount namespaces, so their routes and addresses do not conflict. The harness removes these resources when the test ends.
C++ end-to-end tests use separate directories in bpffs, so flushing one test’s ruleset cannot remove another test’s chains. The group fixtures flush the ruleset, tear down the context, and remove the directory when the test ends.
Test coverage is disabled by default, to enable it, configure CMake with the -DENABLE_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
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 intests/unitwill contain tests for a single source file inlibbpfilter.Add the new source file to the
tests/unit/CMakeLists.txtfile using thebf_add_c_testfunction.Implement the unit tests using the
cmockatesting 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
Create a new source file to contain the end-to-end test under
tests/e2e.Call
bf_add_e2e_shell_test()intests/e2e/CMakeLists.txtto add the new source file to the end-to-end test suite.In your test, use the functions provided by
e2e_test_util.shto 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
Create a new C++ source file under
tests/e2e/matchers/(e.g.tests/e2e/matchers/my_matcher.cpp).Add a call to
bf_add_e2e_c_test()intests/e2e/CMakeLists.txtwith your new test.Use
bf::Chain,bf::Rule,bf::Matcherfromtests/harness/to build chains, andbft::Ethernet,bft::IPv4,bft::TCP, etc. fromtests/harness/Packet.hppto craft packets.Use
MatcherTestsSuitefromtests/harness/test.hppto 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, bf_counter(), {},
{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();
}
Kernel tests¶
Kernel tests run the existing end-to-end tests in virtual machines, using virtme-ng. Each test gets its own VM, so we can check bpfilter against different kernel versions and configurations without rebooting the host.
Enable kernel tests¶
You will need virtme-ng, QEMU, the dependencies to build bpfilter,
and the tools to build a kernel. Use a clone of the Linux stable repository,
with origin pointing to that repository:
$ git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git /path/to/linux
$ cmake -S . -B build -DTESTS_E2E_KERNEL_PATH=/path/to/linux
Already have a stable clone? Use its path instead. CMake queries origin for
the requested releases and fetches missing commits. It leaves your current
branch alone and creates a detached worktree for each kernel build.
Kernel tests are disabled when TESTS_E2E_KERNEL_PATH is empty. They also
require WITH_TESTS_E2E to be enabled.
Run the tests¶
Let’s list the kernel tests before running them:
$ ctest --test-dir build -L '^kernel$' -N
Each native test has a kernel counterpart. For example, e2e.rules.gre becomes
kernel.e2e.rules.gre[v6.6.161] when run on Linux 6.6.161. Use the version
printed by CTest in the commands below.
# Run all kernel tests.
$ ctest --test-dir build -L '^kernel$' --parallel "$(nproc)" --output-on-failure
# Run all tests on one kernel version, using its label.
$ ctest --test-dir build -L '^v6\.6\.161$' --output-on-failure
# Run one test on one kernel. Escape brackets and dots in the name.
$ ctest --test-dir build -R '^kernel\.e2e\.rules\.gre\[v6\.6\.161\]$' --output-on-failure
CTest builds bpfilter, the test binaries, and the selected kernels before
starting the VMs. There is no separate build command to remember. You can also
run the whole kernel suite with make -C build kernel.
The e2e label still selects tests running on the host.
Choose the kernels¶
Add a call to bf_add_kernel_tests in tests/kernel/CMakeLists.txt, then
reconfigure CMake:
bf_add_kernel_tests(6.6)
bf_add_kernel_tests(6.12)
A series such as 6.6 selects its latest stable patch release at origin.
If it has no stable release yet, we use its latest release candidate.
An exact version, such as 6.6.161 or 7.3-rc7, selects that release.
For example, replace bf_add_kernel_tests(6.6) with
bf_add_kernel_tests(6.6.161) to pin that version.
Reconfigure CMake to pick up newer patch releases. Register a release only once
unless you give each additional configuration a name.
Change kernel options or boot arguments¶
To try a different configuration, add a named variant. For example, to run the 6.6 tests with confidentiality lockdown:
bf_add_kernel_tests(6.6
NAME lockdown
KERNEL_CONFIG
+CONFIG_SECURITY
+CONFIG_SECURITYFS
+CONFIG_SECURITY_LOCKDOWN_LSM
+CONFIG_SECURITY_LOCKDOWN_LSM_EARLY
BOOT_ARGUMENT
lockdown=confidentiality
loglevel=7
)
KERNEL_CONFIG overrides tests/kernel/kernel.config. Prefix an option
with + to enable it or - to disable it. You can omit CONFIG_.
BOOT_ARGUMENT adds arguments to the kernel command line through
virtme-ng’s --append. The harness passes them unchanged: the test must check
any runtime state it relies on.
Reconfigure CMake, then select the variant by name:
$ cmake -S . -B build
$ ctest --test-dir build -L '^v6\.6\.[0-9]+-lockdown$' --output-on-failure
The variant becomes part of the version: v6.6.161-lockdown. Its tests are
named kernel.e2e.rules.gre[v6.6.161-lockdown] and carry the
v6.6.161-lockdown label. It has its own worktree and build alongside
v6.6.161, so both configurations can be registered together. The kernel
release also gets the -lockdown suffix through LOCALVERSION.
What gets built, and where?¶
CTest uses two setup fixtures for each VM test:
Fixture |
Builds |
|---|---|
|
bpfilter and the end-to-end binaries, once per CTest run. |
|
The selected kernel, once per CTest run. Named variants use
|
The test starts after both fixtures succeed. A failed kernel build prevents its VM tests from running.
build/kernel/
v6.6.161/
sources/ # Detached Git worktree
build/ # Kernel configuration and build outputs
v6.6.161-lockdown/
sources/ # Separate worktree at the same kernel commit
build/ # Variant configuration and build outputs
The builder reuses these directories and runs an incremental build. Changing the configuration fragment or its overrides triggers configuration again. Kconfig must accept the requested settings, otherwise the build fixture fails. A clean, detached worktree can move to another commit; local edits prevent that update.
The shared configuration enables the BPF hooks, BTF, namespaces, cgroups, and network devices used by the tests. Enable lockdown explicitly in a named variant. Each VM has one virtual CPU and 1 GiB RAM. The harness mounts bpffs and cgroup2, then runs the selected native test with the prebuilt binaries. The tests still create their own sandboxes inside the VM.
Guest output is saved to build/vng-logs/TIMESTAMP/guest.log. The harness
returns the VM’s exit status, including failures reported by CTest.
Run the tools directly¶
Usually, CTest takes care of building and running everything. If you need to build a kernel separately:
$ tests/kernel/buildkernel --sources /path/to/linux \
--revision v6.6.161 --output build/kernel/v6.6.161
The revision must exist locally. The builder uses virtme-ng to configure it,
then make -j"$(nproc)" to build it. It prints the build directory to stdout
and progress to stderr. Use --config +OPTION or --config -OPTION for
configuration overrides, and --localversion SUFFIX for a version suffix
(empty by default). Concurrent builds in the same directory are rejected.
To run one prebuilt end-to-end test on that kernel:
$ tests/kernel/harness --kernel build/kernel/v6.6.161/build \
--build-dir build --test e2e.rules.gre
The harness builds nothing. Use --memory and --cpus to change the VM’s
resources, --boot-argument for each extra kernel argument, and --log-dir
to choose where to save output. --dry-run prints the command without
starting a VM.
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_dir_exists(tmpdir, name)¶
-
assert_dir_not_exists(tmpdir, name)¶
-
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_fd(fd)¶
-
assert_rule_equal(rule0, rule1)¶
-
bft_streams_flush(streams)¶
-
_free_bft_tmpdir_¶
Functions
-
bool bft_list_eq(const bf_list *lhs, const bf_list *rhs, bft_list_eq_cb cb)¶
Compare two
bf_listobjects.- 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,cbis called with the payload oflhsandrhsnode, for each node.
- Returns:
True if both lists are equal, false otherwise.
-
bool bft_set_eq_ordered(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)¶
Compares two rules for equality.
Note
has_countersis used in the comparison, but the counters values are ignored.- Returns:
True if the rules are identical, false otherwise.
-
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 atrule_idx, and asserts that the packet count equalspackets. Ifbytes >= 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.
-
int bft_setup_ctx(void **state)¶
Initialize the global test
bf_ctx.- Parameters:
state – Pointer to a custom object, provided by CMocka.
- Pre:
stateis not NULL.
- Post:
On success:
*stateis a pointer to a valid heap-allocatedbft_tmpdirobject, and the globalbf_ctxhas been initalized with the temporary directory.On failure:
*stateis unchanged.
- Returns:
0 on success, or a negative errno value on failure.
-
int bft_teardown_ctx(void **state)¶
Cleanup the global test
bf_ctx.- Parameters:
state – Pointer to a custom test object, provided by CMocka.
- Pre:
stateis not NULL,*statepoints to a validbft_tmpdir.
- Post:
The
bft_tmpdirhas been deallocated,*stateis NULL.
- Returns:
0 on success, or a negative errno value on failure.
-
struct bft_streams¶
- #include <harness/test.h>
-
struct bft_sockets¶
- #include <harness/test.h>
-
struct bft_tmpdir¶
- #include <harness/test.h>
Mocks¶
A mock replaces a function while it is enabled, so a test can control how this function behaves.
Keep mocks simple: only mock a function to simulate the environment (e.g. a system call that requires privileges or modifies the system), or to trigger a failure that a test can’t cause otherwise, such as an allocation failure. Don’t use mocks to reach code paths that a test can reach with real inputs.
Technicalities¶
Mocks are defined in the mock shared library. ctest runs the unit tests with this library in LD_PRELOAD, so the dynamic linker resolves calls to a mocked function to its mock. Run the unit tests through ctest: without the preload, the real functions may be called instead. In debug builds, ASan is preloaded first, so the functions it intercepts, such as malloc(), can’t be mocked.
To add a mock:
Implement it in its own file in
harness/mock/(e.g.harness/mock/bf_realloc.c), and add the file to themocklibrary inharness/CMakeLists.txt. When the mock is disabled, it must call the real function, found withdlsym(RTLD_NEXT, ...).Declare it with
bft_mock_declare()inharness/mock.h, and define it withbft_mock_define()inharness/mock.c.In tests, enable it with
bft_mock_get(), and use_clean_bft_mock_to disable it when leaving the scope.
For example, to check that bf_wpack_get_data() fails when bf_realloc() fails:
{
_clean_bft_mock_ bft_mock _ = bft_mock_get(bf_realloc);
assert_err(bf_wpack_get_data(pack, &data, &data_len));
}
bft_mock_syscall_set_retval() defines the value that the syscall mock returns for the bpf() system call.
Defines
-
_clean_bft_mock_¶
-
bft_mock_declare(fn)¶
-
bft_mock_get(name)¶
-
bft_mock_default()¶
-
bft_mock_real(mock)¶
-
bft_mock_define(x)¶
Functions
-
void bft_mock_bf_realloc_enable(void)¶
-
void bft_mock_bf_realloc_disable(void)¶
-
bool bft_mock_bf_realloc_is_enabled(void)¶
-
void bft_mock_btf__find_by_name_enable(void)¶
-
void bft_mock_btf__find_by_name_disable(void)¶
-
bool bft_mock_btf__find_by_name_is_enabled(void)¶
-
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_getentropy_enable(void)¶
-
void bft_mock_getentropy_disable(void)¶
-
bool bft_mock_getentropy_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>
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
lenelements, each element being a pointer tosize_tvalue. The pointed values will be from 0 tolen - 1.The
freeandpackcallbacks 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_tailorbf_list_add_headcan be used. Can be NULL iflenis 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_dummynode’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
lhsis equal torhs, false otherwise.
-
const void *bft_get_randomly_filled_buffer(size_t len)¶
-
struct bf_chain *bft_chain_dummy(bool with_rules)¶
-
struct bf_chain *bft_chain_with_set(const char *key, const char *elements, bool negate)¶
Create a TC egress chain with one set rule.
The chain is named “source” and its set is named “addresses”. The rule drops matching packets and records counters. The policy accepts packets.
- Parameters:
key – Set key in
bf_set_new_from_raw()format. Can’t be NULL.elements – Set elements in
bf_set_new_from_raw()format. Can’t be NULL.negate – If true, negate set membership.
- Returns:
A chain owned by the caller, or NULL on failure.
-
struct bf_matcher *bft_matcher_dummy(const void *data, size_t data_len)¶
-
struct bf_set *bft_set_dummy(const char *name, size_t n_elems)¶