From 377265ddd970239f1b77c664111fb521ba4a0ec7 Mon Sep 17 00:00:00 2001 From: Pawel Zmarzly Date: Fri, 23 Jan 2026 17:09:49 +0000 Subject: [PATCH 1/9] lib: cgen: allow updating non-attached chains --- src/bfcli/opts.c | 2 +- src/bpfilter/cgen/cgen.c | 23 +++++++++++++---------- src/bpfilter/cgen/cgen.h | 3 +-- src/bpfilter/xlate/cli.c | 4 ---- tests/e2e/cli/chain_update.sh | 9 ++++++--- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/bfcli/opts.c b/src/bfcli/opts.c index a760d703..382a45d8 100644 --- a/src/bfcli/opts.c +++ b/src/bfcli/opts.c @@ -571,7 +571,7 @@ int bfc_opts_parse(struct bfc_opts *opts, int argc, char **argv) BFC_HELP_ENTRY(BFC_ACTION_LOGS, "Print the logged packets"), BFC_HELP_ENTRY(BFC_ACTION_LOAD, "Load a new chain, do not attach it"), BFC_HELP_ENTRY(BFC_ACTION_ATTACH, "Attach a loaded chain"), - BFC_HELP_ENTRY(BFC_ACTION_UPDATE, "Update an attached chain"), + BFC_HELP_ENTRY(BFC_ACTION_UPDATE, "Update an existing chain"), BFC_HELP_ENTRY(BFC_ACTION_FLUSH, "Remove a chain"), {.name = "help", .key = 'h', .group = -1, .doc = "Print help"}, {.name = "usage", diff --git a/src/bpfilter/cgen/cgen.c b/src/bpfilter/cgen/cgen.c index c9617fd0..0087a1b0 100644 --- a/src/bpfilter/cgen/cgen.c +++ b/src/bpfilter/cgen/cgen.c @@ -378,17 +378,20 @@ int bf_cgen_update(struct bf_cgen *cgen, struct bf_chain **new_chain) if (bf_opts_persist()) bf_program_unpin(old_prog, pindir_fd); - r = bf_link_update(old_prog->link, cgen->chain->hook, - new_prog->runtime.prog_fd); - if (r) { - bf_err_r(r, "failed to update bf_link object with new program"); - if (bf_opts_persist() && bf_program_pin(old_prog, pindir_fd) < 0) - bf_err("failed to repin old program, ignoring"); - return r; - } + if (old_prog->link->hookopts) { + // Chain is currently attached, update the link to use new program + r = bf_link_update(old_prog->link, cgen->chain->hook, + new_prog->runtime.prog_fd); + if (r) { + bf_err_r(r, "failed to update bf_link object with new program"); + if (bf_opts_persist() && bf_program_pin(old_prog, pindir_fd) < 0) + bf_err("failed to repin old program, ignoring"); + return r; + } - // We updated the old link, we need to store it in the new program - bf_swap(new_prog->link, old_prog->link); + // We updated the old link, we need to store it in the new program + bf_swap(new_prog->link, old_prog->link); + } if (bf_opts_persist()) { r = bf_program_pin(new_prog, pindir_fd); diff --git a/src/bpfilter/cgen/cgen.h b/src/bpfilter/cgen/cgen.h index a1368de5..c4467860 100644 --- a/src/bpfilter/cgen/cgen.h +++ b/src/bpfilter/cgen/cgen.h @@ -144,8 +144,7 @@ int bf_cgen_attach(struct bf_cgen *cgen, const struct bf_ns *ns, * On success, the new program is stored in the codegen, and the previous * program is unloaded and freed. * - * @param cgen Codegen to update. It should already contain a program attached - * to a hook. Can't be NULL. + * @param cgen Codegen to update. Can't be NULL. * @param new_chain Chain containing the new rules, sets, and policy. * Can't be NULL. * @return 0 on success, or negative errno value on failure. diff --git a/src/bpfilter/xlate/cli.c b/src/bpfilter/xlate/cli.c index ae5f95bb..105b0ee3 100644 --- a/src/bpfilter/xlate/cli.c +++ b/src/bpfilter/xlate/cli.c @@ -505,10 +505,6 @@ int _bf_cli_chain_update(const struct bf_request *request, if (!cgen) return -ENOENT; - if (!cgen->program->link->hookopts) { - return bf_err_r(-EINVAL, "chain '%s' is not attached", chain->name); - } - r = bf_cgen_update(cgen, &chain); if (r) return -EINVAL; diff --git a/tests/e2e/cli/chain_update.sh b/tests/e2e/cli/chain_update.sh index af1f0968..8bbac355 100755 --- a/tests/e2e/cli/chain_update.sh +++ b/tests/e2e/cli/chain_update.sh @@ -12,14 +12,17 @@ start_bpfilter (! ${FROM_NS} bfcli chain update --from-str "") (! ${FROM_NS} bfcli chain update --name invalid_name --from-str "chain chain_load_xdp_0 BF_HOOK_XDP ACCEPT") (! ${FROM_NS} bfcli chain update --name chain_load_xdp_1 --from-str "chain chain_load_xdp_1 BF_HOOK_XDP ACCEPT") + +# Chain exists and is not attached ${FROM_NS} bfcli chain set --from-str "chain chain_load_xdp_2 BF_HOOK_XDP ACCEPT" -(! ${FROM_NS} bfcli chain update --from-str "chain chain_load_xdp_2 BF_HOOK_XDP ACCEPT") +${FROM_NS} bfcli chain update --from-str "chain chain_load_xdp_2 BF_HOOK_XDP DROP" +${FROM_NS} bfcli chain flush --name chain_load_xdp_2 -# Chain exist and is attached +# Chain exists and is attached ${FROM_NS} bfcli chain set --from-str "chain chain_load_xdp_3 BF_HOOK_XDP{ifindex=${NS_IFINDEX}} ACCEPT" ping -c 1 -W 0.1 ${NS_IP_ADDR} ${FROM_NS} bfcli chain update --from-str "chain chain_load_xdp_3 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log transport counter DROP" (! ping -c 1 -W 0.1 ${NS_IP_ADDR}) ${FROM_NS} bfcli chain update --name chain_load_xdp_3 --from-str "chain chain_load_xdp_3 BF_HOOK_XDP{ifindex=${NS_IFINDEX}} ACCEPT" ping -c 1 -W 0.1 ${NS_IP_ADDR} -${FROM_NS} bfcli chain flush --name chain_load_xdp_3 \ No newline at end of file +${FROM_NS} bfcli chain flush --name chain_load_xdp_3 From ed198004bb7ad41ddb5361ed0c57f89d62dd6e95 Mon Sep 17 00:00:00 2001 From: Pawel Zmarzly Date: Fri, 30 Jan 2026 10:34:51 +0100 Subject: [PATCH 2/9] lib: turn bf_set_parse_elem() into public function --- src/libbpfilter/include/bpfilter/set.h | 11 +++++++++++ src/libbpfilter/set.c | 13 ++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/libbpfilter/include/bpfilter/set.h b/src/libbpfilter/include/bpfilter/set.h index 07eb59c4..d0c8b41d 100644 --- a/src/libbpfilter/include/bpfilter/set.h +++ b/src/libbpfilter/include/bpfilter/set.h @@ -127,3 +127,14 @@ void bf_set_dump(const struct bf_set *set, prefix_t *prefix); bool bf_set_is_empty(const struct bf_set *set); int bf_set_add_elem(struct bf_set *set, const void *elem); + +/** + * @brief Parse a raw element and insert it into a set. + * + * The element is parsed according to `set->key`. + * + * @param set Set to parse the element for. Can't be NULL. + * @param raw_elem Raw element to parse. Can't be NULL. + * @return 0 on success, or a negative error value on failure. + */ +int bf_set_add_elem_raw(struct bf_set *set, const char *raw_elem); diff --git a/src/libbpfilter/set.c b/src/libbpfilter/set.c index bd1a86e6..213c772d 100644 --- a/src/libbpfilter/set.c +++ b/src/libbpfilter/set.c @@ -138,16 +138,7 @@ static int _bf_set_parse_key(const char *raw_key, enum bf_matcher_type *key, return 0; } -/** - * @brief Parse a raw element and insert it into a set. - * - * The element is parsed according to `set->key`. - * - * @param set Set to parse the element for. Can't be NULL. - * @param raw_elem Raw element to parse. Can't be NULL. - * @return 0 on success, or a negative error value on failure. - */ -static int _bf_set_parse_elem(struct bf_set *set, const char *raw_elem) +int bf_set_add_elem_raw(struct bf_set *set, const char *raw_elem) { _cleanup_free_ void *elem = NULL; _cleanup_free_ char *_raw_elem = NULL; @@ -250,7 +241,7 @@ int bf_set_new_from_raw(struct bf_set **set, const char *name, if (raw_elem[0] == '\0') continue; - r = _bf_set_parse_elem(_set, raw_elem); + r = bf_set_add_elem_raw(_set, raw_elem); if (r) return bf_err_r(r, "failed to parse set element '%s'", raw_elem); From 8852eced00bea6d32c731864a25724251ef06f6b Mon Sep 17 00:00:00 2001 From: Pawel Zmarzly Date: Mon, 12 Jan 2026 17:30:33 +0000 Subject: [PATCH 3/9] lib: add bf_chain_get_set_by_name() --- src/libbpfilter/chain.c | 14 ++++++++++++++ src/libbpfilter/include/bpfilter/chain.h | 12 ++++++++++++ tests/unit/libbpfilter/chain.c | 11 +++++++++++ 3 files changed, 37 insertions(+) diff --git a/src/libbpfilter/chain.c b/src/libbpfilter/chain.c index be500393..6c06eaf9 100644 --- a/src/libbpfilter/chain.c +++ b/src/libbpfilter/chain.c @@ -318,3 +318,17 @@ struct bf_set *bf_chain_get_set_for_matcher(const struct bf_chain *chain, return bf_list_get_at(&chain->sets, set_id); } + +struct bf_set *bf_chain_get_set_by_name(struct bf_chain *chain, const char *set_name) +{ + assert(chain); + assert(set_name); + + bf_list_foreach (&chain->sets, set_node) { + struct bf_set *set = bf_list_node_get_data(set_node); + if (bf_streq(set->name, set_name)) + return set; + } + + return NULL; +} diff --git a/src/libbpfilter/include/bpfilter/chain.h b/src/libbpfilter/include/bpfilter/chain.h index 7fadc0c4..6edcbb44 100644 --- a/src/libbpfilter/include/bpfilter/chain.h +++ b/src/libbpfilter/include/bpfilter/chain.h @@ -145,3 +145,15 @@ int bf_chain_add_set(struct bf_chain *chain, struct bf_set *set); */ struct bf_set *bf_chain_get_set_for_matcher(const struct bf_chain *chain, const struct bf_matcher *matcher); + +/** + * @brief Get a set from the chain by name. + * + * Returns a pointer to the set with the given name. The returned pointer + * is owned by the chain and should not be freed by the caller. + * + * @param chain Chain to get the set from. Can't be NULL. + * @param set_name Name of the set to retrieve. Can't be NULL. + * @return Pointer to the set, or NULL if not found. + */ +struct bf_set *bf_chain_get_set_by_name(struct bf_chain *chain, const char *set_name); diff --git a/tests/unit/libbpfilter/chain.c b/tests/unit/libbpfilter/chain.c index c2bcbc1d..28ae0d79 100644 --- a/tests/unit/libbpfilter/chain.c +++ b/tests/unit/libbpfilter/chain.c @@ -135,6 +135,16 @@ static void mixed_enabled_disabled_log_flag(void **state) assert_int_equal(chain->flags & BF_FLAG(BF_CHAIN_LOG), 0); } +static void get_set_by_name(void **state) +{ + _free_bf_chain_ struct bf_chain *chain = bft_chain_dummy(true); + + (void)state; + + assert_non_null(bf_chain_get_set_by_name(chain, "bft_set_dummy")); + assert_null(bf_chain_get_set_by_name(chain, "bft_set_missing")); +} + int main(void) { const struct CMUnitTest tests[] = { @@ -143,6 +153,7 @@ int main(void) cmocka_unit_test(dump), cmocka_unit_test(get_set_from_matcher), cmocka_unit_test(mixed_enabled_disabled_log_flag), + cmocka_unit_test(get_set_by_name), }; return cmocka_run_group_tests(tests, NULL, NULL); From 097394fa8f86368ae7687e867467b37231f5666d Mon Sep 17 00:00:00 2001 From: Pawel Zmarzly Date: Tue, 3 Feb 2026 18:15:53 +0000 Subject: [PATCH 4/9] lib: add bf_chain_new_from_copy() --- src/libbpfilter/chain.c | 36 ++++++++++++++++++++++++ src/libbpfilter/include/bpfilter/chain.h | 9 ++++++ 2 files changed, 45 insertions(+) diff --git a/src/libbpfilter/chain.c b/src/libbpfilter/chain.c index 6c06eaf9..5e59f548 100644 --- a/src/libbpfilter/chain.c +++ b/src/libbpfilter/chain.c @@ -332,3 +332,39 @@ struct bf_set *bf_chain_get_set_by_name(struct bf_chain *chain, const char *set_ return NULL; } + +int bf_chain_new_from_copy(struct bf_chain **dest, const struct bf_chain *src) +{ + _free_bf_wpack_ bf_wpack_t *wpack = NULL; + _free_bf_rpack_ bf_rpack_t *rpack = NULL; + const void *data; + size_t data_len; + int r; + + assert(dest); + assert(src); + + // For now, we do a copy by serializing and deserializing the struct. + // @todo Implement deep copy to avoid serialization overhead. + r = bf_wpack_new(&wpack); + if (r) + return bf_err_r(r, "failed to create wpack for chain serialization"); + + r = bf_chain_pack(src, wpack); + if (r) + return bf_err_r(r, "failed to serialize chain"); + + r = bf_wpack_get_data(wpack, &data, &data_len); + if (r) + return bf_err_r(r, "failed to get serialized chain data"); + + r = bf_rpack_new(&rpack, data, data_len); + if (r) + return bf_err_r(r, "failed to create rpack for chain deserialization"); + + r = bf_chain_new_from_pack(dest, bf_rpack_root(rpack)); + if (r) + return bf_err_r(r, "failed to deserialize chain"); + + return 0; +} diff --git a/src/libbpfilter/include/bpfilter/chain.h b/src/libbpfilter/include/bpfilter/chain.h index 6edcbb44..872bab6a 100644 --- a/src/libbpfilter/include/bpfilter/chain.h +++ b/src/libbpfilter/include/bpfilter/chain.h @@ -157,3 +157,12 @@ struct bf_set *bf_chain_get_set_for_matcher(const struct bf_chain *chain, * @return Pointer to the set, or NULL if not found. */ struct bf_set *bf_chain_get_set_by_name(struct bf_chain *chain, const char *set_name); + +/** Allocate and initialize a chain as a copy of another chain. + * + * @param dest The destination chain. It will be allocated during the call. + * Can't be NULL. + * @param src The source chain, to copy from. Can't be NULL. + * @return 0 on success, negative error code on failure. + */ +int bf_chain_new_from_copy(struct bf_chain **dest, const struct bf_chain *src); From 97212ffaf724c6c5e322907d4014762aa381d1f8 Mon Sep 17 00:00:00 2001 From: Pawel Zmarzly Date: Mon, 12 Jan 2026 17:30:33 +0000 Subject: [PATCH 5/9] lib: add set arithmetic operations for bf_set --- src/libbpfilter/include/bpfilter/set.h | 29 +++++ src/libbpfilter/set.c | 101 ++++++++++++++++ tests/unit/libbpfilter/set.c | 156 +++++++++++++++++++++++++ 3 files changed, 286 insertions(+) diff --git a/src/libbpfilter/include/bpfilter/set.h b/src/libbpfilter/include/bpfilter/set.h index d0c8b41d..dbc57daa 100644 --- a/src/libbpfilter/include/bpfilter/set.h +++ b/src/libbpfilter/include/bpfilter/set.h @@ -138,3 +138,32 @@ int bf_set_add_elem(struct bf_set *set, const void *elem); * @return 0 on success, or a negative error value on failure. */ int bf_set_add_elem_raw(struct bf_set *set, const char *raw_elem); + +/** + * @brief Add elements to destination set. + * + * Adds all elements from `*to_add` to `dest`. Both sets must have the same + * key format. On success, takes ownership of `*to_add` and frees it. + * + * @param dest Destination set to add elements to. Can't be NULL. + * @param to_add Source set containing elements to add. Can't be NULL. On + * success, `*to_add` is freed and set to NULL. + * @return 0 on success, or a negative errno value on failure, including: + * - `-EINVAL`: set key format doesn't match between dest and to_add. + */ +int bf_set_add_many(struct bf_set *dest, struct bf_set **to_add); + +/** + * @brief Remove elements from destination set. + * + * Removes all elements present in `*to_remove` from `dest`. Both sets must + * have the same key format. Elements in `*to_remove` that aren't present in + * `dest` are ignored. On success, takes ownership of `*to_remove` and frees it. + * + * @param dest Destination set to remove elements from. Can't be NULL. + * @param to_remove Source set containing elements to remove. Can't be NULL. + * On success, `*to_remove` is freed and set to NULL. + * @return 0 on success, or a negative errno value on failure, including: + * - `-EINVAL`: set key format doesn't match between dest and to_remove. + */ +int bf_set_remove_many(struct bf_set *dest, struct bf_set **to_remove); diff --git a/src/libbpfilter/set.c b/src/libbpfilter/set.c index 213c772d..57c50e64 100644 --- a/src/libbpfilter/set.c +++ b/src/libbpfilter/set.c @@ -423,3 +423,104 @@ bool bf_set_is_empty(const struct bf_set *set) return bf_list_is_empty(&set->elems); } + +/** + * @brief Check if two sets have the same key format. + * + * @param first First set. Can't be NULL. + * @param second Second set. Can't be NULL. + * @return 0 if sets have matching format, or -EINVAL on mismatch. + */ +static int _bf_set_cmp_key_format(const struct bf_set *first, + const struct bf_set *second) +{ + assert(first); + assert(second); + + if (first->n_comps != second->n_comps) + return bf_err_r( + -EINVAL, + "set key format mismatch: first set has %lu components, second has %lu", + first->n_comps, second->n_comps); + + if (memcmp(first->key, second->key, + first->n_comps * sizeof(enum bf_matcher_type)) != 0) + return bf_err_r( + -EINVAL, + "set key component type mismatch"); + + return 0; +} + +int bf_set_add_many(struct bf_set *dest, struct bf_set **to_add) +{ + int r; + + assert(dest); + assert(to_add); + assert(*to_add); + + r = _bf_set_cmp_key_format(dest, *to_add); + if (r) + return r; + + // @todo This has O(n * m) complexity. We could get to O(n log n + m) by + // turning the linked list into an array and sorting it, but we should + // just replace underlying bf_list with true hashset and enjoy O(m). + bf_list_foreach (&(*to_add)->elems, elem_node) { + void *elem_to_add = bf_list_node_get_data(elem_node); + bool found = false; + + bf_list_foreach (&dest->elems, dest_elem_node) { + const void *dest_elem = bf_list_node_get_data(dest_elem_node); + + if (memcmp(dest_elem, elem_to_add, dest->elem_size) == 0) { + found = true; + break; + } + } + + if (!found) { + r = bf_list_add_tail(&dest->elems, bf_list_node_get_data(elem_node)); + if (r) + return bf_err_r(r, "failed to add element to set"); + // Take ownership of data to stop to_add cleanup from freeing it. + bf_list_node_take_data(elem_node); + } + } + + bf_set_free(to_add); + + return 0; +} + +int bf_set_remove_many(struct bf_set *dest, struct bf_set **to_remove) +{ + int r; + + assert(dest); + assert(to_remove); + assert(*to_remove); + + r = _bf_set_cmp_key_format(dest, *to_remove); + if (r) + return r; + + // @todo This has O(n * m) complexity. Could be O(m) if we used hashsets. + bf_list_foreach (&(*to_remove)->elems, elem_node) { + const void *elem_to_remove = bf_list_node_get_data(elem_node); + + bf_list_foreach (&dest->elems, dest_elem_node) { + const void *dest_elem = bf_list_node_get_data(dest_elem_node); + + if (memcmp(dest_elem, elem_to_remove, dest->elem_size) == 0) { + bf_list_delete(&dest->elems, dest_elem_node); + break; + } + } + } + + bf_set_free(to_remove); + + return 0; +} diff --git a/tests/unit/libbpfilter/set.c b/tests/unit/libbpfilter/set.c index e6e0e578..496a6527 100644 --- a/tests/unit/libbpfilter/set.c +++ b/tests/unit/libbpfilter/set.c @@ -247,6 +247,155 @@ static void new_from_raw_invalid(void **state) assert_err(bf_set_new_from_raw(&set, "test", "()", "{1.2.3.4}")); } +static void add_many_basic(void **state) +{ + _free_bf_set_ struct bf_set *dest = NULL; + _free_bf_set_ struct bf_set *to_add = NULL; + enum bf_matcher_type key[] = {BF_MATCHER_IP4_SADDR}; + uint32_t elem1 = 0x01010101; + uint32_t elem2 = 0x02020202; + uint32_t elem3 = 0x03030303; + + (void)state; + + assert_ok(bf_set_new(&dest, "dest", key, ARRAY_SIZE(key))); + assert_ok(bf_set_new(&to_add, "to_add", key, ARRAY_SIZE(key))); + + assert_ok(bf_set_add_elem(dest, &elem1)); + assert_ok(bf_set_add_elem(dest, &elem2)); + + assert_ok(bf_set_add_elem(to_add, &elem2)); + assert_ok(bf_set_add_elem(to_add, &elem3)); + + assert_ok(bf_set_add_many(dest, &to_add)); + + assert_int_equal(bf_list_size(&dest->elems), 3); + assert_int_equal(*(uint32_t *)bf_list_get_at(&dest->elems, 0), elem1); + assert_int_equal(*(uint32_t *)bf_list_get_at(&dest->elems, 1), elem2); + assert_int_equal(*(uint32_t *)bf_list_get_at(&dest->elems, 2), elem3); + assert_null(to_add); +} + +static void add_many_mismatched_key_count(void **state) +{ + _free_bf_set_ struct bf_set *dest = NULL; + _free_bf_set_ struct bf_set *to_add = NULL; + enum bf_matcher_type key1[] = {BF_MATCHER_IP4_SADDR}; + enum bf_matcher_type key2[] = {BF_MATCHER_IP4_SADDR, BF_MATCHER_TCP_SPORT}; + + (void)state; + + assert_ok(bf_set_new(&dest, "dest", key1, ARRAY_SIZE(key1))); + assert_ok(bf_set_new(&to_add, "to_add", key2, ARRAY_SIZE(key2))); + + assert_err(bf_set_add_many(dest, &to_add)); + assert_non_null(to_add); +} + +static void add_many_mismatched_key_type(void **state) +{ + _free_bf_set_ struct bf_set *dest = NULL; + _free_bf_set_ struct bf_set *to_add = NULL; + enum bf_matcher_type key1[] = {BF_MATCHER_IP4_SADDR}; + enum bf_matcher_type key2[] = {BF_MATCHER_IP4_DADDR}; + + (void)state; + + assert_ok(bf_set_new(&dest, "dest", key1, ARRAY_SIZE(key1))); + assert_ok(bf_set_new(&to_add, "to_add", key2, ARRAY_SIZE(key2))); + + assert_err(bf_set_add_many(dest, &to_add)); + assert_non_null(to_add); +} + +static void remove_many_basic(void **state) +{ + _free_bf_set_ struct bf_set *dest = NULL; + _free_bf_set_ struct bf_set *to_remove = NULL; + enum bf_matcher_type key[] = {BF_MATCHER_IP4_SADDR}; + uint32_t elem1 = 0x01010101; + uint32_t elem2 = 0x02020202; + uint32_t elem3 = 0x03030303; + + (void)state; + + assert_ok(bf_set_new(&dest, "dest", key, ARRAY_SIZE(key))); + assert_ok(bf_set_new(&to_remove, "to_remove", key, ARRAY_SIZE(key))); + + assert_ok(bf_set_add_elem(dest, &elem1)); + assert_ok(bf_set_add_elem(dest, &elem2)); + assert_ok(bf_set_add_elem(dest, &elem3)); + + assert_ok(bf_set_add_elem(to_remove, &elem2)); + + assert_ok(bf_set_remove_many(dest, &to_remove)); + + assert_int_equal(bf_list_size(&dest->elems), 2); + assert_int_equal(*(uint32_t *)bf_list_get_at(&dest->elems, 0), elem1); + assert_int_equal(*(uint32_t *)bf_list_get_at(&dest->elems, 1), elem3); + assert_null(to_remove); +} + +static void remove_many_disjoint_sets(void **state) +{ + _free_bf_set_ struct bf_set *dest = NULL; + _free_bf_set_ struct bf_set *to_remove = NULL; + enum bf_matcher_type key[] = {BF_MATCHER_IP4_SADDR}; + uint32_t elem1 = 0x01010101; + uint32_t elem2 = 0x02020202; + uint32_t elem3 = 0x03030303; + uint32_t elem4 = 0x04040404; + + (void)state; + + assert_ok(bf_set_new(&dest, "dest", key, ARRAY_SIZE(key))); + assert_ok(bf_set_new(&to_remove, "to_remove", key, ARRAY_SIZE(key))); + + assert_ok(bf_set_add_elem(dest, &elem1)); + assert_ok(bf_set_add_elem(dest, &elem2)); + + assert_ok(bf_set_add_elem(to_remove, &elem3)); + assert_ok(bf_set_add_elem(to_remove, &elem4)); + + assert_ok(bf_set_remove_many(dest, &to_remove)); + assert_int_equal(bf_list_size(&dest->elems), 2); + assert_int_equal(*(uint32_t *)bf_list_get_at(&dest->elems, 0), elem1); + assert_int_equal(*(uint32_t *)bf_list_get_at(&dest->elems, 1), elem2); + assert_null(to_remove); +} + +static void remove_many_mismatched_key_count(void **state) +{ + _free_bf_set_ struct bf_set *dest = NULL; + _free_bf_set_ struct bf_set *to_remove = NULL; + enum bf_matcher_type key1[] = {BF_MATCHER_IP4_SADDR}; + enum bf_matcher_type key2[] = {BF_MATCHER_IP4_SADDR, BF_MATCHER_TCP_SPORT}; + + (void)state; + + assert_ok(bf_set_new(&dest, "dest", key1, ARRAY_SIZE(key1))); + assert_ok(bf_set_new(&to_remove, "to_remove", key2, ARRAY_SIZE(key2))); + + assert_err(bf_set_remove_many(dest, &to_remove)); + assert_non_null(to_remove); +} + +static void remove_many_mismatched_key_type(void **state) +{ + _free_bf_set_ struct bf_set *dest = NULL; + _free_bf_set_ struct bf_set *to_remove = NULL; + enum bf_matcher_type key1[] = {BF_MATCHER_IP4_SADDR}; + enum bf_matcher_type key2[] = {BF_MATCHER_IP4_DADDR}; + + (void)state; + + assert_ok(bf_set_new(&dest, "dest", key1, ARRAY_SIZE(key1))); + assert_ok(bf_set_new(&to_remove, "to_remove", key2, ARRAY_SIZE(key2))); + + assert_err(bf_set_remove_many(dest, &to_remove)); + assert_non_null(to_remove); +} + int main(void) { const struct CMUnitTest tests[] = { @@ -264,6 +413,13 @@ int main(void) cmocka_unit_test(new_from_raw), cmocka_unit_test(new_from_raw_multiple_keys), cmocka_unit_test(new_from_raw_invalid), + cmocka_unit_test(add_many_basic), + cmocka_unit_test(add_many_mismatched_key_count), + cmocka_unit_test(add_many_mismatched_key_type), + cmocka_unit_test(remove_many_basic), + cmocka_unit_test(remove_many_disjoint_sets), + cmocka_unit_test(remove_many_mismatched_key_count), + cmocka_unit_test(remove_many_mismatched_key_type), }; return cmocka_run_group_tests(tests, NULL, NULL); From 2aa6dd95cf8b4b962f731fd58b40945adb785729 Mon Sep 17 00:00:00 2001 From: Pawel Zmarzly Date: Mon, 12 Jan 2026 17:30:33 +0000 Subject: [PATCH 6/9] daemon: add update-set operation --- src/bpfilter/main.c | 1 + src/bpfilter/xlate/cli.c | 74 ++++++++++++++++++++++ src/libbpfilter/include/bpfilter/request.h | 1 + src/libbpfilter/request.c | 1 + tests/unit/libbpfilter/request.c | 3 + 5 files changed, 80 insertions(+) diff --git a/src/bpfilter/main.c b/src/bpfilter/main.c index fc69bda3..ffdcd89a 100644 --- a/src/bpfilter/main.c +++ b/src/bpfilter/main.c @@ -360,6 +360,7 @@ static int _bf_process_request(struct bf_request *request, bf_request_cmd(request) == BF_REQ_CHAIN_LOAD || bf_request_cmd(request) == BF_REQ_CHAIN_ATTACH || bf_request_cmd(request) == BF_REQ_CHAIN_UPDATE || + bf_request_cmd(request) == BF_REQ_CHAIN_UPDATE_SET || bf_request_cmd(request) == BF_REQ_CHAIN_FLUSH)) r = _bf_save(ctx_path); diff --git a/src/bpfilter/xlate/cli.c b/src/bpfilter/xlate/cli.c index 105b0ee3..8519da15 100644 --- a/src/bpfilter/xlate/cli.c +++ b/src/bpfilter/xlate/cli.c @@ -17,6 +17,7 @@ #include #include +#include "bpfilter/set.h" #include "cgen/cgen.h" #include "cgen/prog/link.h" #include "cgen/prog/map.h" @@ -540,6 +541,76 @@ int _bf_cli_chain_flush(const struct bf_request *request, return bf_ctx_delete_cgen(cgen, true); } +int _bf_cli_chain_update_set(const struct bf_request *request, + struct bf_response **response) +{ + _free_bf_set_ struct bf_set *to_add = NULL; + _free_bf_set_ struct bf_set *to_remove = NULL; + _free_bf_chain_ struct bf_chain *new_chain = NULL; + _free_bf_rpack_ bf_rpack_t *pack = NULL; + struct bf_set *dest_set = NULL; + _cleanup_free_ char *chain_name = NULL; + struct bf_cgen *cgen = NULL; + bf_rpack_node_t child; + int r; + + assert(request); + + (void)response; + + r = bf_rpack_new(&pack, bf_request_data(request), + bf_request_data_len(request)); + if (r) + return r; + + r = bf_rpack_kv_str(bf_rpack_root(pack), "name", &chain_name); + if (r) + return r; + + r = bf_rpack_kv_obj(bf_rpack_root(pack), "to_add", &child); + if (r) + return r; + r = bf_set_new_from_pack(&to_add, child); + if (r) + return r; + + r = bf_rpack_kv_obj(bf_rpack_root(pack), "to_remove", &child); + if (r) + return r; + r = bf_set_new_from_pack(&to_remove, child); + if (r) + return r; + + if (!bf_streq(to_add->name, to_remove->name)) + return bf_err_r(-EINVAL, "to_add->name must match to_remove->name"); + + cgen = bf_ctx_get_cgen(chain_name); + if (!cgen) + return bf_err_r(-ENOENT, "chain '%s' does not exist", chain_name); + + r = bf_chain_new_from_copy(&new_chain, cgen->chain); + if (r) + return r; + + dest_set = bf_chain_get_set_by_name(new_chain, to_add->name); + if (!dest_set) + return bf_err_r(-ENOENT, "set '%s' does not exist", to_add->name); + + r = bf_set_add_many(dest_set, &to_add); + if (r) + return bf_err_r(r, "failed to calculate set union"); + + r = bf_set_remove_many(dest_set, &to_remove); + if (r) + return bf_err_r(r, "failed to calculate set difference"); + + r = bf_cgen_update(cgen, &new_chain); + if (r) + return bf_err_r(r, "failed to update chain with new set data"); + + return 0; +} + static int _bf_cli_request_handler(const struct bf_request *request, struct bf_response **response) { @@ -582,6 +653,9 @@ static int _bf_cli_request_handler(const struct bf_request *request, case BF_REQ_CHAIN_FLUSH: r = _bf_cli_chain_flush(request, response); break; + case BF_REQ_CHAIN_UPDATE_SET: + r = _bf_cli_chain_update_set(request, response); + break; default: r = bf_err_r(-EINVAL, "unsupported command %d for CLI front-end", bf_request_cmd(request)); diff --git a/src/libbpfilter/include/bpfilter/request.h b/src/libbpfilter/include/bpfilter/request.h index f2d4e051..2477f84e 100644 --- a/src/libbpfilter/include/bpfilter/request.h +++ b/src/libbpfilter/include/bpfilter/request.h @@ -45,6 +45,7 @@ enum bf_request_cmd BF_REQ_CHAIN_LOAD, BF_REQ_CHAIN_ATTACH, BF_REQ_CHAIN_UPDATE, + BF_REQ_CHAIN_UPDATE_SET, BF_REQ_CHAIN_FLUSH, BF_REQ_COUNTERS_SET, diff --git a/src/libbpfilter/request.c b/src/libbpfilter/request.c index 0c8946ee..1980e4fc 100644 --- a/src/libbpfilter/request.c +++ b/src/libbpfilter/request.c @@ -224,6 +224,7 @@ const char *bf_request_cmd_to_str(enum bf_request_cmd cmd) [BF_REQ_CHAIN_PROG_FD] = "BF_REQ_CHAIN_PROG_FD", [BF_REQ_CHAIN_LOGS_FD] = "BF_REQ_CHAIN_LOGS_FD", [BF_REQ_CHAIN_FLUSH] = "BF_REQ_CHAIN_FLUSH", + [BF_REQ_CHAIN_UPDATE_SET] = "BF_REQ_CHAIN_UPDATE_SET", [BF_REQ_COUNTERS_SET] = "BF_REQ_COUNTERS_SET", [BF_REQ_COUNTERS_GET] = "BF_REQ_COUNTERS_GET", [BF_REQ_CUSTOM] = "BF_REQ_CUSTOM", diff --git a/tests/unit/libbpfilter/request.c b/tests/unit/libbpfilter/request.c index 429a30f4..88e7b187 100644 --- a/tests/unit/libbpfilter/request.c +++ b/tests/unit/libbpfilter/request.c @@ -229,6 +229,7 @@ static void cmd_to_str(void **state) assert_non_null(bf_request_cmd_to_str(BF_REQ_CHAIN_PROG_FD)); assert_non_null(bf_request_cmd_to_str(BF_REQ_CHAIN_LOGS_FD)); assert_non_null(bf_request_cmd_to_str(BF_REQ_CHAIN_FLUSH)); + assert_non_null(bf_request_cmd_to_str(BF_REQ_CHAIN_UPDATE_SET)); assert_non_null(bf_request_cmd_to_str(BF_REQ_COUNTERS_SET)); assert_non_null(bf_request_cmd_to_str(BF_REQ_COUNTERS_GET)); assert_non_null(bf_request_cmd_to_str(BF_REQ_CUSTOM)); @@ -236,6 +237,8 @@ static void cmd_to_str(void **state) // Verify specific strings assert_string_equal(bf_request_cmd_to_str(BF_REQ_CHAIN_GET), "BF_REQ_CHAIN_GET"); + assert_string_equal(bf_request_cmd_to_str(BF_REQ_CHAIN_UPDATE_SET), + "BF_REQ_CHAIN_UPDATE_SET"); assert_string_equal(bf_request_cmd_to_str(BF_REQ_CUSTOM), "BF_REQ_CUSTOM"); } From 727bc9ea5ed8810c71670c101d5afeabc33ee747 Mon Sep 17 00:00:00 2001 From: Pawel Zmarzly Date: Mon, 12 Jan 2026 17:30:33 +0000 Subject: [PATCH 7/9] cli: add update-set operation --- src/bfcli/chain.c | 58 +++++++++++++++ src/bfcli/chain.h | 1 + src/bfcli/opts.c | 81 +++++++++++++++++++++ src/bfcli/opts.h | 11 ++- src/libbpfilter/cli.c | 46 ++++++++++++ src/libbpfilter/include/bpfilter/bpfilter.h | 34 +++++++++ 6 files changed, 230 insertions(+), 1 deletion(-) diff --git a/src/bfcli/chain.c b/src/bfcli/chain.c index 7b19f6ea..aed59d02 100644 --- a/src/bfcli/chain.c +++ b/src/bfcli/chain.c @@ -24,6 +24,7 @@ #include #include #include +#include #include "helper.h" #include "opts.h" @@ -264,3 +265,60 @@ int bfc_chain_flush(const struct bfc_opts *opts) return r; } + +int bfc_chain_update_set(const struct bfc_opts *opts) +{ + _free_bf_set_ struct bf_set *to_add = NULL; + _free_bf_set_ struct bf_set *to_remove = NULL; + _free_bf_chain_ struct bf_chain *chain = NULL; + _free_bf_hookopts_ struct bf_hookopts *hookopts = NULL; + _clean_bf_list_ bf_list counters = bf_list_default(bf_counter_free, NULL); + struct bf_set *dest_set = NULL; + int r; + + if (bf_list_is_empty(&opts->set_add) && bf_list_is_empty(&opts->set_remove)) + return bf_err_r(-EINVAL, "no elements to add or remove"); + + // Fetch dest_set to get key format + r = bf_chain_get(opts->name, &chain, &hookopts, &counters); + if (r == -ENOENT) + return bf_err_r(r, "chain '%s' not found", opts->name); + if (r) + return bf_err_r(r, "unknown error"); + + dest_set = bf_chain_get_set_by_name(chain, opts->set_name); + if (!dest_set) + return bf_err_r(-ENOENT, "set '%s' does not exist", opts->set_name); + + r = bf_set_new(&to_add, opts->set_name, dest_set->key, dest_set->n_comps); + if (r) + return bf_err_r(r, "failed to create set"); + + bf_list_foreach (&opts->set_add, node) { + const char *raw_elem = bf_list_node_get_data(node); + + r = bf_set_add_elem_raw(to_add, raw_elem); + if (r) + return bf_err_r(r, "failed to parse set element '%s'", raw_elem); + } + + r = bf_set_new(&to_remove, opts->set_name, dest_set->key, dest_set->n_comps); + if (r) + return bf_err_r(r, "failed to create set"); + + bf_list_foreach (&opts->set_remove, node) { + const char *raw_elem = bf_list_node_get_data(node); + + r = bf_set_add_elem_raw(to_remove, raw_elem); + if (r) + return bf_err_r(r, "failed to parse set element '%s'", raw_elem); + } + + r = bf_chain_update_set(opts->name, to_add, to_remove); + if (r) + return bf_err_r(r, "failed to update set '%s' in chain '%s'", opts->set_name, opts->name); + + bf_info("updated set '%s' in chain '%s'", opts->set_name, opts->name); + + return 0; +} diff --git a/src/bfcli/chain.h b/src/bfcli/chain.h index 68cf50ef..916551c4 100644 --- a/src/bfcli/chain.h +++ b/src/bfcli/chain.h @@ -14,4 +14,5 @@ int bfc_chain_logs(const struct bfc_opts *opts); int bfc_chain_load(const struct bfc_opts *opts); int bfc_chain_attach(const struct bfc_opts *opts); int bfc_chain_update(const struct bfc_opts *opts); +int bfc_chain_update_set(const struct bfc_opts *opts); int bfc_chain_flush(const struct bfc_opts *opts); diff --git a/src/bfcli/opts.c b/src/bfcli/opts.c index 382a45d8..3b5f5e68 100644 --- a/src/bfcli/opts.c +++ b/src/bfcli/opts.c @@ -9,6 +9,7 @@ #include +#include "bpfilter/list.h" #include "chain.h" #include "ruleset.h" @@ -73,6 +74,7 @@ static const char * const _bfc_action_strs[] = { "load", // BFC_ACTION_LOAD "attach", // BFC_ACTION_ATTACH "update", // BFC_ACTION_UPDATE + "update-set", // BFC_ACTION_UPDATE_SET "flush", // BFC_ACTION_FLUSH }; static_assert(ARRAY_SIZE(_bfc_action_strs) == _BFC_ACTION_MAX, @@ -106,6 +108,9 @@ enum bfc_opts_option_id BFC_OPT_CHAIN_FROM_FILE, BFC_OPT_CHAIN_NAME, BFC_OPT_CHAIN_HOOK_OPTS, + BFC_OPT_SET_NAME, + BFC_OPT_SET_ADD, + BFC_OPT_SET_REMOVE, BFC_OPT_DRY_RUN, _BFC_OPT_MAX, }; @@ -224,6 +229,19 @@ static const struct bfc_opts_cmd _bfc_opts_cmds[] = { "definition provided by --from-str or --from-file.", .cb = bfc_chain_update, }, + { + .name = "bfcli chain update-set", + .object = BFC_OBJECT_CHAIN, + .action = BFC_ACTION_UPDATE_SET, + .valid_opts = BF_FLAGS(BFC_OPT_CHAIN_NAME, BFC_OPT_SET_NAME, + BFC_OPT_SET_ADD, BFC_OPT_SET_REMOVE), + .required_opts = BF_FLAGS(BFC_OPT_CHAIN_NAME, BFC_OPT_SET_NAME), + .doc = "Update a set in a chain\vAtomically update the content of a " + "named set in a chain using delta operation. Use --add to " + "add elements and --remove to remove elements. At least one " + "of --add or --remove must be specified.", + .cb = bfc_chain_update_set, + }, { .name = "bfcli chain flush", .object = BFC_OBJECT_CHAIN, @@ -354,6 +372,42 @@ static void _bfc_opts_chain_hook_opts_cb(struct argp_state *state, argp_error(state, "failed to parse hook option '%s'", arg); }; +static void _bfc_opts_set_name_cb(struct argp_state *state, const char *arg, + struct bfc_opts *opts) +{ + if (strlen(arg) == 0) + argp_error(state, "--set-name can't be empty"); + + opts->set_name = arg; +}; + +static void _bfc_opts_set_add_cb(struct argp_state *state, + const char *arg, struct bfc_opts *opts) +{ + int r; + + if (strlen(arg) == 0) + argp_error(state, "--add requires an element"); + + r = bf_list_add_tail(&opts->set_add, (void *)arg); + if (r) + argp_error(state, "failed to add element to list"); +}; + +static void _bfc_opts_set_remove_cb(struct argp_state *state, + const char *arg, + struct bfc_opts *opts) +{ + int r; + + if (strlen(arg) == 0) + argp_error(state, "--remove requires an element"); + + r = bf_list_add_tail(&opts->set_remove, (void *)arg); + if (r) + argp_error(state, "failed to add element to list"); +}; + static void _bfc_opts_dry_run(struct argp_state *state, const char *arg, struct bfc_opts *opts) { @@ -447,6 +501,30 @@ struct bfc_opts_opt .doc = "Hook option to attach the chain", .parser = _bfc_opts_chain_hook_opts_cb, }, + { + .id = BFC_OPT_SET_NAME, + .key = 'S', + .name = "set-name", + .arg = "NAME", + .doc = "Name of the set to update", + .parser = _bfc_opts_set_name_cb, + }, + { + .id = BFC_OPT_SET_ADD, + .key = 'A', + .name = "add", + .arg = "ELEMENT", + .doc = "Element to add to the set. Can be specified multiple times.", + .parser = _bfc_opts_set_add_cb, + }, + { + .id = BFC_OPT_SET_REMOVE, + .key = 'R', + .name = "remove", + .arg = "ELEMENT", + .doc = "Element to remove from the set. Can be specified multiple times.", + .parser = _bfc_opts_set_remove_cb, + }, { .id = BFC_OPT_DRY_RUN, .key = 'd', @@ -550,6 +628,8 @@ void bfc_opts_clean(struct bfc_opts *opts) assert(opts); bf_hookopts_clean(&opts->hookopts); + bf_list_clean(&opts->set_add); + bf_list_clean(&opts->set_remove); } #define _BFC_NAME_LEN (PATH_MAX + 32) @@ -572,6 +652,7 @@ int bfc_opts_parse(struct bfc_opts *opts, int argc, char **argv) BFC_HELP_ENTRY(BFC_ACTION_LOAD, "Load a new chain, do not attach it"), BFC_HELP_ENTRY(BFC_ACTION_ATTACH, "Attach a loaded chain"), BFC_HELP_ENTRY(BFC_ACTION_UPDATE, "Update an existing chain"), + BFC_HELP_ENTRY(BFC_ACTION_UPDATE_SET, "Update a set in a chain"), BFC_HELP_ENTRY(BFC_ACTION_FLUSH, "Remove a chain"), {.name = "help", .key = 'h', .group = -1, .doc = "Print help"}, {.name = "usage", diff --git a/src/bfcli/opts.h b/src/bfcli/opts.h index 41573969..766cf70f 100644 --- a/src/bfcli/opts.h +++ b/src/bfcli/opts.h @@ -48,6 +48,7 @@ enum bfc_action BFC_ACTION_LOAD, BFC_ACTION_ATTACH, BFC_ACTION_UPDATE, + BFC_ACTION_UPDATE_SET, BFC_ACTION_FLUSH, _BFC_ACTION_MAX, }; @@ -71,6 +72,12 @@ struct bfc_opts const char *name; struct bf_hookopts hookopts; + const char *set_name; + // set_add and set_remove don't own their data. + // They store strings from argv. + bf_list set_add; + bf_list set_remove; + bool dry_run; }; @@ -89,7 +96,9 @@ struct bfc_opts_cmd * @brief Initialize a `bfc_opts` object to default values. */ #define bfc_opts_default() \ - {.object = _BFC_OBJECT_MAX, .action = _BFC_ACTION_MAX}; + {.object = _BFC_OBJECT_MAX, .action = _BFC_ACTION_MAX, \ + .set_add = bf_list_default(NULL, NULL), \ + .set_remove = bf_list_default(NULL, NULL)}; void bfc_opts_clean(struct bfc_opts *opts); int bfc_opts_parse(struct bfc_opts *opts, int argc, char **argv); diff --git a/src/libbpfilter/cli.c b/src/libbpfilter/cli.c index f74dedd0..b25c1623 100644 --- a/src/libbpfilter/cli.c +++ b/src/libbpfilter/cli.c @@ -17,6 +17,7 @@ #include "bpfilter/pack.h" #include "bpfilter/request.h" #include "bpfilter/response.h" +#include "bpfilter/set.h" int bf_ruleset_get(bf_list *chains, bf_list *hookopts, bf_list *counters) { @@ -505,6 +506,51 @@ int bf_chain_update(const struct bf_chain *chain) return bf_response_status(response); } +int bf_chain_update_set(const char *name, const struct bf_set *to_add, + const struct bf_set *to_remove) +{ + _cleanup_close_ int fd = -1; + _free_bf_request_ struct bf_request *request = NULL; + _free_bf_response_ struct bf_response *response = NULL; + _free_bf_wpack_ bf_wpack_t *wpack = NULL; + int r; + + assert(name); + + r = bf_wpack_new(&wpack); + if (r) + return r; + + bf_wpack_kv_str(wpack, "name", name); + + bf_wpack_open_object(wpack, "to_add"); + r = bf_set_pack(to_add, wpack); + if (r) + return r; + bf_wpack_close_object(wpack); + + bf_wpack_open_object(wpack, "to_remove"); + r = bf_set_pack(to_remove, wpack); + if (r) + return r; + bf_wpack_close_object(wpack); + + r = bf_request_new_from_pack(&request, BF_FRONT_CLI, BF_REQ_CHAIN_UPDATE_SET, + wpack); + if (r) + return bf_err_r(r, "bf_chain_update_set: failed to create a new request"); + + fd = bf_connect_to_daemon(); + if (fd < 0) + return bf_err_r(fd, "failed to connect to the daemon"); + + r = bf_send(fd, request, &response, NULL); + if (r) + return bf_err_r(r, "bf_chain_update_set: failed to send request"); + + return bf_response_status(response); +} + int bf_chain_flush(const char *name) { _cleanup_close_ int fd = -1; diff --git a/src/libbpfilter/include/bpfilter/bpfilter.h b/src/libbpfilter/include/bpfilter/bpfilter.h index cfb0e7ba..4f462ffb 100644 --- a/src/libbpfilter/include/bpfilter/bpfilter.h +++ b/src/libbpfilter/include/bpfilter/bpfilter.h @@ -12,6 +12,7 @@ struct bf_response; struct bf_chain; +struct bf_set; struct ipt_getinfo; struct ipt_get_entries; struct ipt_replace; @@ -289,6 +290,39 @@ int bf_chain_attach(const char *name, const struct bf_hookopts *hookopts); */ int bf_chain_update(const struct bf_chain *chain); +/** + * @brief Update a named set in an existing chain using delta operations. + * + * The chain to update must exist. This operation triggers regeneration of + * the chain's BPF program with the updated set data. Elements from `to_add` + * are added to the set, and elements from `to_remove` are removed. If + * `to_remove` has elements that already aren't present in the program, + * these elements are ignored. + * + * **Request payload format** + * @code{.json} + * { + * "chain_name": "", + * "to_add": { }, // bf_set object + * "to_remove": { } // bf_set object + * } + * @endcode + * + * **Response payload format** + * The response doesn't contain data. + * + * @param name Name of the chain containing the set. Can't be NULL. + * @param to_add Set containing elements to add. The set name and key format + * must match the existing set in the chain. Can't be NULL. + * @param to_remove Set containing elements to remove. The set name and key + * format must match the existing set in the chain. Can't be NULL. + * @return 0 on success, or a negative errno value on failure, including: + * - `-ENOENT`: no chain found for this name or set not found in chain. + * - `-EINVAL`: set key format doesn't match existing set. + */ +int bf_chain_update_set(const char *name, const struct bf_set *to_add, + const struct bf_set *to_remove); + /** * @brief Remove a chain. * From 9a278eaa7ac139cac5c5b99232de5dde5d2e3751 Mon Sep 17 00:00:00 2001 From: Pawel Zmarzly Date: Mon, 12 Jan 2026 17:30:33 +0000 Subject: [PATCH 8/9] tests: add update-set tests --- tests/e2e/CMakeLists.txt | 1 + tests/e2e/cli/chain_update_set.sh | 207 ++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100755 tests/e2e/cli/chain_update_set.sh diff --git a/tests/e2e/CMakeLists.txt b/tests/e2e/CMakeLists.txt index ee505c4d..dbfe8d1d 100644 --- a/tests/e2e/CMakeLists.txt +++ b/tests/e2e/CMakeLists.txt @@ -56,6 +56,7 @@ bf_add_e2e_test(e2e cli/chain_attach.sh ROOT) bf_add_e2e_test(e2e cli/chain_load.sh ROOT) bf_add_e2e_test(e2e cli/chain_set.sh ROOT) bf_add_e2e_test(e2e cli/chain_update.sh ROOT) +bf_add_e2e_test(e2e cli/chain_update_set.sh ROOT) bf_add_e2e_test(e2e cli/hookopts.sh) bf_add_e2e_test(e2e cli/nf_inet_dual_stack.sh ROOT) bf_add_e2e_test(e2e cli/options_error.sh) diff --git a/tests/e2e/cli/chain_update_set.sh b/tests/e2e/cli/chain_update_set.sh new file mode 100755 index 00000000..8466ef87 --- /dev/null +++ b/tests/e2e/cli/chain_update_set.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash + +set -eux +set -o pipefail + +. "$(dirname "$0")"/../e2e_test_util.sh +make_sandbox +start_bpfilter + +# Adding new elements +${FROM_NS} bfcli chain set --from-str "chain test_xdp BF_HOOK_XDP{ifindex=${NS_IFINDEX}} ACCEPT + set blocked_ips (ip4.saddr) in { + 10.0.0.1; + 10.0.0.2 + } + rule + (ip4.saddr) in blocked_ips + counter + DROP +" + +${FROM_NS} bfcli chain update-set \ + --name test_xdp \ + --set-name blocked_ips \ + --add 10.0.0.3 --add 10.0.0.4 + +chain_output=$(${FROM_NS} bfcli chain get --name test_xdp) +echo "$chain_output" +echo "$chain_output" | grep -q '10.0.0.1' +echo "$chain_output" | grep -q '10.0.0.2' +echo "$chain_output" | grep -q '10.0.0.3' +echo "$chain_output" | grep -q '10.0.0.4' + +# Removing elements +${FROM_NS} bfcli chain set --from-str "chain test_xdp BF_HOOK_XDP{ifindex=${NS_IFINDEX}} ACCEPT + set blocked_ips (ip4.saddr) in { + 10.0.0.1; + 10.0.0.2; + 10.0.0.3; + 10.0.0.4 + } + rule + (ip4.saddr) in blocked_ips + counter + DROP +" + +${FROM_NS} bfcli chain update-set \ + --name test_xdp \ + --set-name blocked_ips \ + --remove 10.0.0.3 --remove 10.0.0.4 + +chain_output=$(${FROM_NS} bfcli chain get --name test_xdp) +echo "$chain_output" +echo "$chain_output" | grep -q '10.0.0.1' +echo "$chain_output" | grep -q '10.0.0.2' +(! echo "$chain_output" | grep -q '10.0.0.3') +(! echo "$chain_output" | grep -q '10.0.0.4') + +# Adding and removing in one operation +${FROM_NS} bfcli chain set --from-str "chain test_xdp BF_HOOK_XDP{ifindex=${NS_IFINDEX}} ACCEPT + set blocked_ips (ip4.saddr) in { + 10.0.0.1; + 10.0.0.2 + } + rule + (ip4.saddr) in blocked_ips + counter + DROP +" + +${FROM_NS} bfcli chain update-set \ + --name test_xdp \ + --set-name blocked_ips \ + --add 10.0.0.3 --add 10.0.0.4 \ + --remove 10.0.0.1 --remove 10.0.0.4 + +chain_output=$(${FROM_NS} bfcli chain get --name test_xdp) +echo "$chain_output" +(! echo "$chain_output" | grep -q '10.0.0.1') +echo "$chain_output" | grep -q '10.0.0.2' +echo "$chain_output" | grep -q '10.0.0.3' +(! echo "$chain_output" | grep -q '10.0.0.4') + +# Trying to update non-existent set should fail +${FROM_NS} bfcli chain set --from-str "chain test_xdp BF_HOOK_XDP{ifindex=${NS_IFINDEX}} ACCEPT + set blocked_ips (ip4.saddr) in { + 10.0.0.1; + 10.0.0.2 + } + rule + (ip4.saddr) in blocked_ips + counter + DROP +" + +(! ${FROM_NS} bfcli chain update-set \ + --name test_xdp \ + --set-name nonexistent_set \ + --add 10.0.0.3 2>&1) + +chain_output=$(${FROM_NS} bfcli chain get --name test_xdp) +echo "$chain_output" +echo "$chain_output" | grep -q '10.0.0.1' +echo "$chain_output" | grep -q '10.0.0.2' + +# Trying to update with mismatched key format should fail +${FROM_NS} bfcli chain set --from-str "chain test_xdp BF_HOOK_XDP{ifindex=${NS_IFINDEX}} ACCEPT + set blocked_ips (ip4.saddr) in { + 10.0.0.1; + 10.0.0.2 + } + rule + (ip4.saddr) in blocked_ips + counter + DROP +" + +(! ${FROM_NS} bfcli chain update-set \ + --name test_xdp \ + --set-name blocked_ips \ + --add 10.0.0.1,tcp 2>&1) + +chain_output=$(${FROM_NS} bfcli chain get --name test_xdp) +echo "$chain_output" +echo "$chain_output" | grep -q '10.0.0.1' +echo "$chain_output" | grep -q '10.0.0.2' + +# Trying to update with nothing to add or remove should fail +${FROM_NS} bfcli chain set --from-str "chain test_xdp BF_HOOK_XDP{ifindex=${NS_IFINDEX}} ACCEPT + set blocked_ips (ip4.saddr) in { + 10.0.0.1; + 10.0.0.2 + } + rule + (ip4.saddr) in blocked_ips + counter + DROP +" + +(! ${FROM_NS} bfcli chain update-set \ + --name test_xdp \ + --set-name blocked_ips 2>&1) + +# Trying to add duplicate elements is no-op +${FROM_NS} bfcli chain set --from-str "chain test_xdp BF_HOOK_XDP{ifindex=${NS_IFINDEX}} ACCEPT + set blocked_ips (ip4.saddr) in { + 10.0.0.1 + } + rule + (ip4.saddr) in blocked_ips + counter + DROP +" + +${FROM_NS} bfcli chain update-set \ + --name test_xdp \ + --set-name blocked_ips \ + --add 10.0.0.1 + +chain_output=$(${FROM_NS} bfcli chain get --name test_xdp) +echo "$chain_output" +count=$(echo "$chain_output" | grep -o '10.0.0.1' | wc -l) +if [ "$count" -ne 1 ]; then + echo "Expected 1 occurrence of 10.0.0.1, got $count" + exit 1 +fi + +# Works with compound keys +${FROM_NS} bfcli chain set --from-str "chain test_xdp BF_HOOK_XDP{ifindex=${NS_IFINDEX}} ACCEPT + set blocked_addrs (ip4.saddr, tcp.sport) in { + 10.0.0.1, 10001; + 10.0.0.2, 10002 + } + rule + (ip4.saddr, tcp.sport) in blocked_addrs + counter + DROP +" + +${FROM_NS} bfcli chain update-set \ + --name test_xdp \ + --set-name blocked_addrs \ + --add 10.0.0.3,10003 --add '10.0.0.4, 10004' + +chain_output=$(${FROM_NS} bfcli chain get --name test_xdp) +echo "$chain_output" +echo "$chain_output" | grep -q '10.0.0.1, 10001' +echo "$chain_output" | grep -q '10.0.0.2, 10002' +echo "$chain_output" | grep -q '10.0.0.3, 10003' +echo "$chain_output" | grep -q '10.0.0.4, 10004' + +# Unattached chain can be updated +${FROM_NS} bfcli chain set --from-str "chain test_xdp BF_HOOK_XDP ACCEPT + set test_set (ip4.saddr) in { 10.0.0.1 } + rule (ip4.saddr) in test_set ACCEPT +" + +${FROM_NS} bfcli chain update-set \ + --name test_xdp \ + --set-name test_set \ + --add 10.0.0.2 + +chain_output=$(${FROM_NS} bfcli chain get --name test_xdp) +echo "$chain_output" +echo "$chain_output" | grep -q '10.0.0.1' +echo "$chain_output" | grep -q '10.0.0.2' From b476886e6711068e24d62cd4a03197b1abf3d09d Mon Sep 17 00:00:00 2001 From: Pawel Zmarzly Date: Wed, 4 Feb 2026 16:02:31 +0000 Subject: [PATCH 9/9] doc: document update-set --- doc/usage/bfcli.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/doc/usage/bfcli.rst b/doc/usage/bfcli.rst index 16d252b1..eb64c432 100644 --- a/doc/usage/bfcli.rst +++ b/doc/usage/bfcli.rst @@ -239,6 +239,37 @@ If you want to modify the hook options, use ``bfcli chain set`` instead. counters 0 packets 0 bytes DROP +``chain update-set`` +~~~~~~~~~~~~~~~~~~~~ + +Atomically update the content of a named set in a chain using delta operations. This is more efficient than replacing the entire chain when you only need to modify set membership. + +**Options** + - ``--name NAME``: name of the chain containing the set. + - ``--set-name NAME``: name of the set to update. + - ``--add ELEMENT``: element to add to the set. Can be specified multiple times. + - ``--remove ELEMENT``: element to remove from the set. Can be specified multiple times. + +At least one of ``--add`` or ``--remove`` must be specified. + +**Examples** + +.. code:: shell + + $ # Create a chain with a named set + $ sudo bfcli chain set --from-str " + chain my_filter BF_HOOK_XDP{ifindex=2} ACCEPT + set blocklist (ip4.saddr) in { 192.168.1.100 } + rule + (ip4.saddr) in blocklist + DROP" + + $ # Add addresses to the blocklist + $ sudo bfcli chain update-set --name my_filter --set-name blocklist --add 192.168.1.101 --add 192.168.1.102 + + $ # Remove an address from the blocklist + $ sudo bfcli chain update-set --name my_filter --set-name blocklist --remove 192.168.1.100 + ``chain flush`` ~~~~~~~~~~~~~~~