Tests

Test harness

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

Test

Main file to include to perform tests. This header defines convenience macros to create tests and test results.

Defines

NOT_NULL

Macro to use when checking if NULL parameters are properly asserted on:

// Ensure path can't be NULL
expect_assert_failure(bf_read_file(NULL, NOT_NULL, 0));
// Ensure buf can't be NULL
expect_assert_failure(bf_read_file(NOT_NULL, NULL, 0));
Test(group, name)

Create a new test.

Tests are defined in their section so they can be easily discovered at runtime time.

Parameters:
  • group – Test group, can be filtered on to run all the tests in a single group.

  • name – Name of the test.

bf_test_fail(fmt, ...)

Fail a test with an error message.

Parameters:
  • fmt – Message format, similar to printf() format.

  • ... – Format arguments.

assert_success(x)

Assert that x evaluates to a success.

Parameters:
  • x – Expression to evaluate. If the expression evaluates to 0, it is considered succeeded, and the assertion succeeds.

assert_error(x)

Assert that x evaluates to an error.

Parameters:
  • x – Expression to evaluate. If the expression evaluates to < 0, it is considered failed, and the assertion succeeds.

_free_bf_test_
_free_bf_test_group_
_free_bf_test_suite_
_free_bf_test_filter_

Typedefs

typedef void (*bf_test_cb)(void **state)

Functions

int bf_test_new(bf_test **test, const char *name, bf_test_cb cb)
void bf_test_free(bf_test **test)
void bf_test_dump(const bf_test *test, prefix_t *prefix)
int bf_test_group_new(bf_test_group **group, const char *name)
void bf_test_group_free(bf_test_group **group)
void bf_test_group_dump(const bf_test_group *group, prefix_t *prefix)
int bf_test_group_add_test(bf_test_group *group, const char *test_name, bf_test_cb cb)
bf_test *bf_test_group_get_test(bf_test_group *group, const char *test_name)
int bf_test_group_make_cmtests(bf_test_group *group)
int bf_test_suite_new(bf_test_suite **suite)
void bf_test_suite_free(bf_test_suite **suite)
void bf_test_suite_dump(const bf_test_suite *suite, prefix_t *prefix)
void bf_test_suite_print(const bf_test_suite *suite)
int bf_test_suite_add_test(bf_test_suite *suite, const char *group_name, const char *test_name, bf_test_cb cb)
int bf_test_suite_add_symbol(bf_test_suite *suite, struct bf_test_sym *sym)
bf_test_group *bf_test_suite_get_group(bf_test_suite *suite, const char *group_name)
int bf_test_suite_make_cmtests(const bf_test_suite *suite)
int bf_test_discover_test_suite(bf_test_suite **suite)

Discover the test suite in the current ELF file.

Parse the sections in the current ELF file to discover the symbols in the .bf_test section and create the associated test suite.

Parameters:
  • suite – Discovered test suite. Can’t be NULL. On success, this argument will be point to a valid test suite.

Returns:

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

int bf_test_filter_new(bf_test_filter **filter)
void bf_test_filter_free(bf_test_filter **filter)
int bf_test_filter_add_pattern(bf_test_filter *filter, const char *pattern)
bool bf_test_filter_matches(bf_test_filter *filter, const char *str)
struct bf_test
#include <harness/test.h>

Test

Public Members

const char *name

Name of the test.

bf_test_cb cb

Test function.

struct bf_test_group
#include <harness/test.h>

Test group.

A test group contains one or more tests.

Public Members

const char *name

Name of the test group.

bf_list tests

List of tests in the group.

struct CMUnitTest *cmtests

CMocka test object, for CMocka’s primitives to run the tests.

struct bf_test_suite
#include <harness/test.h>

Test suite.

A test suite contains one or more test groups.

Public Members

bf_list groups

List of test groups.

struct bf_test_filter
#include <harness/test.h>

A filter to apply to the tests to run.

Public Members

bf_list patterns

Symbols

bpfilter stores the test functions in a custom .bf_test section in the ELF binary. This way, the tests can be fetched at runtime from the current binary, allowing for tests autodiscovery (which CMocka doesn’t support).

bf_test_get_symbols() will read the sections in the ELF file it runs from and return all the symbols located in the .bf_test section.

Defines

_free_bf_test_sym_

Functions

int bf_test_sym_new(struct bf_test_sym **sym, const char *name, void *cb)
void bf_test_sym_free(struct bf_test_sym **sym)
void bf_test_sym_dump(struct bf_test_sym *sym)
int bf_test_get_symbols(bf_list *symbols)
struct bf_test_sym
#include <harness/sym.h>

Public Members

const char *name
void *cb

Mocks

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 marsh the daemon into.

Defines

_free_tmp_file_
_clean_bf_test_mock_
bf_test_mock_get(name, retval)
bf_test_mock_empty(name)
bf_test_mock_will_return(mock, value)
bf_test_mock_will_return_always(mock, value)

Functions

char *bf_test_filepath_new_rw(void)
void bf_test_filepath_free(char **path)
void bf_test_mock_clean(bf_test_mock *mock)
struct bf_test_mock
#include <harness/mock.h>

Public Members

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

Process

The functions defined in this file are used to manage an external process. They are inspired by the Python subprocess module.

bf_test_process represents the process to manipulate, it must be initialized using bf_test_process_init() with the correct command and arguments.

bf_test_process_start() will fork the current process, and run the pre-defined command in the new thread. Two file descriptors will be available to read the forked process’ stdout and stderr streams (use bf_test_process_stdout() and bf_test_process_stderr() to do so).

The forked process can terminate by itself, in which case you need to wait for it anyway using bf_test_process_wait(). You can also kill the process manually by calling bf_test_process_kill() to send a SIGTERM signal, then calling bf_test_process_wait(). The last option is to call bf_test_process_stop() which will kill it and wait.

Lastly, cleanup the resources allocated for the process with bf_test_process_clean().

Defines

_cleanup_bf_test_process_

Functions

int bf_test_process_init(struct bf_test_process *process, const char *cmd, char **args, size_t nargs)
void bf_test_process_clean(struct bf_test_process *process)
int bf_test_process_start(struct bf_test_process *process)

Start the process.

Fork the current process to start the requested process. Open two file descriptor to communicate with the forked process (stdout and stderr). Once started, the process can be waited on, killed, or stopped. Use bf_test_process_stdout() and bf_test_process_stderr() to access it standard output and error buffers.

If this function succeeds, bf_test_process_wait() or bf_test_process_stop() must called before cleaning the process.

Parameters:
  • process – The process to start. Can’t be NULL.

Returns:

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

int bf_test_process_wait(struct bf_test_process *process)

Wait for the process to terminate.

This function will hang until the process has completed.

Parameters:
  • process – The process to wait on. Can’t be NULL.

Returns:

The return code of the process as a non-negative integer, or a negative errno value on error.

int bf_test_process_kill(struct bf_test_process *process)

Kill the process by sending SIGTERM.

Parameters:
  • process – The process to kill. Can’t be NULL.

Returns:

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

int bf_test_process_stop(struct bf_test_process *process)

Force the process to stop and wait for it.

This function is equivalent to calling bf_test_process_kill() then bf_test_process_wait().

Parameters:
  • process – The process to stop. Can’t be NULL.

Returns:

The return code of the process as a non-negative integer, or a negative errno value on error.

int bf_run(const char *cmd, char **args, size_t nargs)

Run a command in a forked process.

This function won’t kill the process but only wait on it. If you call bf_run() with a command that doesn’t return, this function will hang indefinitely.

Parameters:
  • cmd – Command to run in the process.

  • args – Array of arguments to provide to the process.

  • nargs – Number of arguments in args.

Returns:

The return code of the process as a non-negative integer, or a negative errno value on error.

const char *bf_test_process_stdout(struct bf_test_process *process)

Read the process’ stdout stream.

The buffer returned by bf_test_process_stdout() is dynamically allocated and is owned by the caller.

Parameters:
  • process – Process to read the stdout stream from.

Returns:

Buffer containing the process’ stdout stream, or NULL on error.

const char *bf_test_process_stderr(struct bf_test_process *process)

Read the process’ stderr stream.

The buffer returned by bf_test_process_stderr() is dynamically allocated and is owned by the caller.

Parameters:
  • process – Process to read the stderr stream from.

Returns:

Buffer containing the process’ stderr stream, or NULL on error.

struct bf_test_process
#include <harness/process.h>

Public Members

const char *cmd

Command to run in the process.

char **args

Array of arguments as char pointers.

size_t nargs

Number of arguments in args.

pid_t pid

PID of the process, only valid while the process is alive.

int out_fd

File descriptor of the process’ stdout stream.

int err_fd

File descriptor of the process’ stderr stream.

Daemon

bf_test_daemon represents a handle to manage the bpfilter daemon. Based on the primitives defined in harness/process.h.

Defines

_cleanup_bf_test_daemon_

Enums

enum bf_test_daemon_option

Options to configure the daemon.

Not all the options defined for bpfilter need to be defined below.

Values:

enumerator BF_TEST_DAEMON_TRANSIENT = 1 << 0
enumerator BF_TEST_DAEMON_NO_CLI = 1 << 1
enumerator BF_TEST_DAEMON_NO_IPTABLES = 1 << 2
enumerator BF_TEST_DAEMON_NO_NFTABLES = 1 << 3
enumerator _BF_TEST_DAEMON_LAST = BF_TEST_DAEMON_NO_NFTABLES

Functions

int bf_test_daemon_init(struct bf_test_daemon *daemon, const char *path, uint32_t options)

Initialize a new daemon object.

Note

bf_test_daemon_init() assumes none of the options defined in bf_test_daemon_option require an argument. If this assumption is erroneous, the logic used to parse the options need to be modified!

Parameters:
  • daemon – The daemon object to initialize. Can’t be NULL.

  • path – Path to the bpfilter binary. If NULL, the first bpfilter binary found in $PATH will be used.

  • options – Command line options to start the daemon with. See bf_test_daemon_option for the list of available options.

Returns:

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

void bf_test_daemon_clean(struct bf_test_daemon *daemon)

Cleanup a daemon object.

Parameters:
  • daemon – Daemon object to cleanup. Can’t be NULL.

int bf_test_daemon_start(struct bf_test_daemon *daemon)

Start a daemon process.

Once the process is started, this function will wait for a specific log from the daemon to validate the process is up and running (and didn’t exit).

Parameters:
  • daemon – Daemon object to start the daemon process for. Can’t be NULL.

Returns:

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

int bf_test_daemon_stop(struct bf_test_daemon *daemon)

Stop a daemon process.

Parameters:
  • daemon – Daemon object to stop the daemon process for. Can’t be NULL.

Returns:

The return code of the daemon process as an integer >= 0 on success, or a negative errno value on error.

struct bf_test_daemon
#include <harness/daemon.h>

Public Members

struct bf_test_process process

Filters

Convenience functions to easily create matchers, rules, and chains in order to test bpfilter. Those functions are wrapper around the actual API (i.e. bf_matcher_new(), bf_rule_new(), bf_chain_new()) which cut corners when it comes to error handling (e.g. you can’t retrieve the actual error code).

Some wrappers expect NULL-terminated array of pointers, they will take ownership of the pointers and free them if an error occurs during the object creation. Valid pointers in the array located after a NULL entry won’t be processed nor freed, and asan will raise an error. See bf_rule_get() and bf_chain_get().

Functions

struct bf_hook_opts bf_hook_opts_get(enum bf_hook_opt opt, ...)

Create a new hook options object.

bf_hook_opts_get() expects pairs of bf_hook_opt key and value, with the last variadic argument being -1:

bf_hook_opts_get(
    BF_HOOK_OPT_IFINDEX, 2,
    BF_HOOK_OPT_NAME, "my_bpf_program",
    -1
);
Parameters:
  • opt – First hook option. This parameter is required as C requires at least one explicit parameter.

Returns:

A bf_hook_opts structure filled with the arguments passed to the function. If an error occurs, an error message is printed and the bf_hook_opts structure is filled with 0.

struct bf_matcher *bf_matcher_get(enum bf_matcher_type type, enum bf_matcher_op op, const void *payload, size_t payload_len)

Create a new matcher.

See bf_matcher_new() for details of the arguments.

Returns:

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

struct bf_rule *bf_rule_get(bool counters, enum bf_verdict verdict, struct bf_matcher **matchers)

Create a new rule.

See bf_rule_new() for details of the arguments.

Returns:

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

struct bf_chain *bf_chain_get(enum bf_hook hook, struct bf_hook_opts hook_opts, enum bf_verdict policy, struct bf_set **sets, struct bf_rule **rules)

Create a new chain.

See bf_chain_get() for details of the arguments.

Returns:

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

Program

This module defines bf_test_prog to manipulate a BPF program generated by bpfilter.

Once a bf_test_prog object has been created, use bf_test_prog_open() to link it to a BPF program attach to the system using the program’s name.

Defines

_free_bf_test_prog_

Functions

struct bf_test_prog *bf_test_prog_get(const char *name)
int bf_test_prog_new(struct bf_test_prog **prog)
void bf_test_prog_free(struct bf_test_prog **prog)
int bf_test_prog_open(struct bf_test_prog *prog, const char *name)
int bf_test_prog_run(const struct bf_test_prog *prog, uint32_t expect, const struct bf_test_packet *pkt)

Call BPF_PROG_TEST_RUN on the program.

Parameters:
  • prog – Program to test run. Can’t be NULL.

  • expect – Expected return value of the program, depends on the program type.

  • pkt – Test packet to send to the BPF program. Can’t be NULL.

Returns:

  • 0 if the call succeeded and the BPF program’s return value is equal to expect.

  • < 0 if the call failed.

  • > 0 if the call succeeded but the BPF program’s return value is different from expect.

struct bf_test_packet
#include <harness/prog.h>

Public Members

size_t len
const void *data
struct bf_test_prog
#include <harness/prog.h>

Public Members

int fd

Unit tests

Warning

In progress.

End-to-end tests

End-to-end tests are designed to validate the bytecode generated by bpfilter through the following workflow:

  • Start the bpfilter daemon.

  • Send a chain to the daemon to be translated into a BPF program.

  • Use BPF_PROG_TEST_RUN with a dummy packet and validate the program’s return code.

Run end-to-end tests with make e2e. root privileges are required to start the daemon and call bpf(BPF_PROG_TEST_RUN).

The test packets are generated using a Python script and Scapy: the scripts creates packets.h which is included in the end-to-end tests sources. See tests/e2e/genpkts.py.

Adding a new end-to-end test

End-to-end tests are defined in tests/e2e and use cmocka as the testing library. To add a new end-to-end test:

  1. Add a new cmocka test in a source file under tests/e2e.

  2. Create a chain: use the primitives in Filters to easily create chains, rules, and matchers. Remember to set the attach=no option on the chain to avoid blocking your host’s traffic!

  3. Send the chain to the daemon. The e2e main() function should create a new instance of the daemon for each test group, so you don’t have to do it yourself. Use bf_cli_set_chain() to send the chain to the daemon.

  4. Get the file descriptor of the generated BPF program as a bf_test_prog. Use bf_test_prog_get() to avoid the boilerplate of creating the bf_test_prog object, allocating it, and opening the file descriptor. The BPF program identified by its name, which you control through the hook attribute name.

  5. Send a dummy packet to your program and validate the return value with bf_test_prog_run().

Example

The example below will create an empty chain with a default ACCEPT policy. We expect the generated XDP program to return XDP_PASS (which is 2).

Test(xdp, default_policy)
{
    _cleanup_bf_chain_ struct bf_chain *chain = bf_chain_get(
        BF_HOOK_XDP,
        bf_hook_opts_get(
            BF_HOOK_OPT_IFINDEX, 2,
            BF_HOOK_OPT_NAME, "bf_e2e_testprog",
            BF_HOOK_OPT_ATTACH, false,
            -1
        ),
        BF_VERDICT_ACCEPT,
        NULL,
        (struct bf_rule *[]) {
            NULL,
        }
    );
    _free_bf_test_prog_ struct bf_test_prog *prog = NULL;

    if (bf_cli_set_chain(chain) < 0)
        bf_test_fail("failed to send the chain to the daemon");

    assert_non_null(prog = bf_test_prog_get("bf_e2e_testprog"));
    assert_success(bf_test_prog_run(prog, 2, &pkt_local_ip6_tcp));
}

Benchmarking

Warning

In progress.