summaryrefslogtreecommitdiffstats
path: root/src/main.c
diff options
context:
space:
mode:
authorPablo Neira Ayuso <pablo@netfilter.org>2025-06-04 23:12:30 +0200
committerPablo Neira Ayuso <pablo@netfilter.org>2025-06-05 00:03:59 +0200
commit009cabe369b71cf5452286fd338eae382718789a (patch)
tree9f14eb626cee252ddf79d34cda9b98b57f5a9454 /src/main.c
initial commitHEADmaster
knft is a tool to improve test coverage for the low-level nftables kernel API by providing a relatively simple way to define a transaction batch with nftables objects without having to mingle with netlink. A set of tests 612 test (.t) files are included. knft also provides a rudimentary deterministic fuzzer (via -f option) along with several fuzzing modes that mangle existing tests in different ways to improve coverage for error unwinding paths: deltable \ delbasechain \ delchain \ delrule | - delete object in this batch delset / delelem / delobj / flushset - flush set dup - duplicate object reverse-commit - turn commit into abort reverse-abort - turn abort into commit table-dormant - inject table dormant flag table-wakeup - inject table wake-up flag swap - swap objects bogus - inject bogus object to make the transaction fail To inspect how the selected fuzzing mode mangles the test, you can use the -d option to enable debugging along with -c to run it in dry-run mode, eg. # src/./knft -c -f deltable -d tests/expr/meta/03-mark_ok.t tests/expr/meta/03-mark_ok.t... [FUZZING] tests/expr/meta/03-mark_ok.t (deltable) >>>> fuzz_loop at index 0 in state=0 add_table(NFPROTO_IPV4, "test", NULL, NULL, NULL); del_table(NFPROTO_IPV4, "test", NULL); add_chain("test", NULL, NULL, NULL, NULL); add_rule("test", "0x1", NULL, NULL, NULL); meta(NULL, "NFT_REG32_15", "3"); cmp("NFT_REG32_15", "0", "ffffffff"); commit(); <<<< fuzz_loop backtrack STACK limit reached ==== still more tries at index 0 in state=0 add_table(NFPROTO_IPV4, "test", NULL, NULL, NULL); add_chain("test", NULL, NULL, NULL, NULL); del_table(NFPROTO_IPV4, "test", NULL); add_rule("test", "0x1", NULL, NULL, NULL); meta(NULL, "NFT_REG32_15", "3"); cmp("NFT_REG32_15", "0", "ffffffff"); commit(); <<<< fuzz_loop backtrack STACK limit reached ... knft provides a few more options: -e to display the error reported by the kernel. -n to perform test runs without flushing the existing ruleset. This tool requires libmnl to build and to parse the netlink messages that are sent and received by the kernel. This tool is released under the GPLv2 (or any later). This project is funded through the NGI0 Entrust established by NLnet (https://nlnet.nl) with support from the European Commission's Next Generation Internet programme.
Diffstat (limited to 'src/main.c')
-rw-r--r--src/main.c220
1 files changed, 220 insertions, 0 deletions
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..8075592
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2024-2025 Pablo Neira Ayuso <pablo@netfilter.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 (or any
+ * later) as published by the Free Software Foundation.
+ */
+
+/* Funded through the NGI0 Entrust established by NLnet (https://nlnet.nl)
+ * with support from the European Commission's Next Generation Internet
+ * programme.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <dirent.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <getopt.h>
+
+#include "test.h"
+
+static int run_test_dir(const char *filename, struct nft_test_ctx *ctx,
+ struct nft_test_stats *stats)
+{
+ struct dirent *dent;
+ char path[PATH_MAX];
+ struct stat st;
+ int ret = 0;
+ DIR *d;
+
+ d = opendir(filename);
+ if (!d) {
+ perror("cannot open directory");
+ return -1;
+ }
+
+ dent = readdir(d);
+ while (dent) {
+ if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, "..")) {
+ dent = readdir(d);
+ continue;
+ }
+ snprintf(path, sizeof(path), "%s/%s", filename, dent->d_name);
+ if (stat(path, &st) < 0) {
+ fprintf(stderr, "cannot open file %s: %s\n",
+ path, strerror(errno));
+ return -1;
+ }
+ if (S_ISDIR(st.st_mode)) {
+ if (run_test_dir(path, ctx, stats) < 0) {
+ ret = -1;
+ break;
+ }
+ dent = readdir(d);
+ continue;
+ } else if (!S_ISREG(st.st_mode)) {
+ dent = readdir(d);
+ continue;
+ }
+ if (run_test(path, ctx) < 0)
+ stats->failed++;
+ else
+ stats->ok++;
+
+ if (ctx->fatal) {
+ ret = -1;
+ break;
+ }
+
+ dent = readdir(d);
+ }
+ closedir(d);
+
+ return ret;
+}
+
+static int parse_fuzz_mode(char *str, uint32_t *type, uint32_t *num_types)
+{
+ char *x, *token;
+ int a, idx = 0;
+
+ x = strchr(str, ',');
+
+ do {
+ if (idx >= FUZZ_STACK_MAX) {
+ printf("too many modes\n");
+ return -1;
+ }
+
+ if (!x) {
+ token = str;
+ a = fuzz_type_to_value(token);
+ if (a < 0) {
+ printf("unknown fuzzing mode %s\n", token);
+ printf("Expected:\n", token);
+ print_fuzz_modes();
+ return -1;
+ }
+ type[idx++] = a;
+ break;
+ }
+
+ *x = '\0';
+ token = str;
+ a = fuzz_type_to_value(token);
+ if (a < 0) {
+ printf("unknown fuzzing mode %s\n", token);
+ printf("Expected:\n", token);
+ print_fuzz_modes();
+ return -1;
+ }
+ type[idx++] = a;
+
+ str = x + 1;
+
+ x = strchr(str, ',');
+ } while (1);
+
+ *num_types = idx;
+
+ return idx;
+}
+
+bool errout;
+bool noflush;
+
+int main(int argc, char *argv[])
+{
+ struct timeval tv_start, tv_end, tv_diff;
+ struct nft_test_stats stats = {};
+ struct nft_test_ctx ctx = {};
+ const char *filename;
+ struct stat st;
+ int opt;
+
+ while ((opt = getopt(argc, argv, "nf:hedc")) != -1) {
+ switch (opt) {
+ case 'n':
+ noflush = true;
+ break;
+ case 'f':
+ ctx.fuzz = true;
+ if (parse_fuzz_mode(optarg, ctx.fuzz_ctx.type, &ctx.fuzz_ctx.num_types) < 0)
+ exit(EXIT_FAILURE);
+ break;
+ case 'd':
+ ctx.fuzz_ctx.debug = true;
+ break;
+ case 'e':
+ errout = true;
+ break;
+ case 'c':
+ ctx.dryrun = true;
+ break;
+ case 'h':
+ default:
+ fprintf(stderr, "Usage: %s [-e] [-d] [-n] [-c] [-f <mode,...>] <input>\n",
+ argv[0]);
+ printf("Options:\n");
+ printf("\t-e\tdisplay error reported by kernel\n");
+ printf("\t-n\tdo not flush previous ruleset\n");
+ printf("\t-d\tdisplay debugging information\n");
+ printf("\t-c\tdry run\n");
+ printf("\t-f <mode>\tfuzz test, see fuzz modes\n");
+ printf("Available fuzz modes:\n");
+ print_fuzz_modes();
+ exit(EXIT_FAILURE);
+ break;
+ }
+ }
+
+ if (optind >= argc) {
+ fprintf(stderr, "ERROR: specify path to file or folder with tests\n");
+ exit(EXIT_FAILURE);
+ }
+
+ filename = argv[optind];
+
+ /* TODO: add dummy device with netlink. */
+ system("sudo modprobe xt_cpu");
+ system("sudo ip link add dummy0 type dummy > /dev/null 2>&1");
+ system("sudo ip link add dummy1 type dummy > /dev/null 2>&1");
+ system("sudo ip link add dummy2 type dummy > /dev/null 2>&1");
+ system("sudo ip link add dummy3 type dummy > /dev/null 2>&1");
+
+ gettimeofday(&tv_start, NULL);
+
+ if (!strcmp(filename, "-"))
+ filename = "/dev/stdin";
+
+ if (stat(filename, &st) < 0) {
+ fprintf(stderr, "cannot open test file %s: %s\n",
+ filename, strerror(errno));
+ return EXIT_FAILURE;
+ }
+ if (S_ISDIR(st.st_mode)) {
+ run_test_dir(filename, &ctx, &stats);
+ printf("results: [\x1b[32mOK\x1b[37m] %d [\x1b[31mFAILED\x1b[37m] %d\n",
+ stats.ok, stats.failed);
+ } else if (S_ISREG(st.st_mode)) {
+ printf("%s...\n", filename);
+ run_test(filename, &ctx);
+ }
+
+ gettimeofday(&tv_end, NULL);
+ timersub(&tv_end, &tv_start, &tv_diff);
+
+ if (ctx.fuzz)
+ printf("total fuzz steps: %u in %u.%06u seconds\n", ctx.fuzz_ctx.total_fuzz_steps, tv_diff.tv_sec, tv_diff.tv_usec);
+ else
+ printf("done in %u.%06u seconds\n", tv_diff.tv_sec, tv_diff.tv_usec);
+
+ return EXIT_SUCCESS;
+}