diff --git a/doc/usage/bfcli.rst b/doc/usage/bfcli.rst index 16d252b1..188523d5 100644 --- a/doc/usage/bfcli.rst +++ b/doc/usage/bfcli.rst @@ -524,6 +524,11 @@ Meta * - ``range`` - ``$START-$END`` - ``$START`` and ``$END`` must be decimal or hexadecimal 32-bits integers, with ``$START <= $END``. Only compatible with ``BF_HOOK_TC_INGRESS`` and ``BF_HOOK_TC_EGRESS`` hooks. + * - Flow probability + - ``meta.flow_probability`` + - ``eq`` + - ``$PROBABILITY`` + - ``$PROBABILITY`` is a floating-point percentage value (i.e., within [0%, 100%], e.g., "50%" or "33.33%"). Unlike ``meta.probability`` which uses per-packet randomness, ``meta.flow_probability`` computes a deterministic hash from the packet's 5-tuple (source/destination IP, source/destination port, protocol) ensuring all packets from the same flow get the same match decision. Compatible with ``BF_HOOK_XDP``, ``BF_HOOK_TC_INGRESS``, ``BF_HOOK_TC_EGRESS``, ``BF_HOOK_CGROUP_INGRESS``, and ``BF_HOOK_CGROUP_EGRESS`` hooks. IPv4 #### diff --git a/src/bfcli/lexer.l b/src/bfcli/lexer.l index a262a4e0..f0956c00 100644 --- a/src/bfcli/lexer.l +++ b/src/bfcli/lexer.l @@ -32,6 +32,7 @@ %s STATE_MATCHER_META_FLOW_HASH %s STATE_MATCHER_L4_PROTO %s STATE_MATCHER_META_PROBA +%s STATE_MATCHER_META_FLOW_PROBA %s STATE_MATCHER_IPV4_ADDR %s STATE_MATCHER_IP4_NET %s STATE_MATCHER_IP4_DSCP @@ -44,6 +45,7 @@ %s STATE_MATCHER_TCP_FLAGS int (-|(0x))?[0-9a-zA-Z]+ +float [0-9]+(\.[0-9]+)? %% [ \t\n] ; @@ -162,6 +164,16 @@ meta\.probability { BEGIN(STATE_MATCHER_META_PROBA); yylval.sval = strdup(yytex } } +meta\.flow_probability { BEGIN(STATE_MATCHER_META_FLOW_PROBA); yylval.sval = strdup(yytext); return MATCHER_TYPE; } +{ + (eq) { yylval.sval = strdup(yytext); return MATCHER_OP; } + {float}% { + BEGIN(INITIAL); + yylval.sval = strdup(yytext); + return RAW_PAYLOAD; + } +} + meta\.mark { BEGIN(STATE_MATCHER_META_MARK); yylval.sval = strdup(yytext); return MATCHER_TYPE; } { (eq|not) { yylval.sval = strdup(yytext); return MATCHER_OP; } diff --git a/src/bpfilter/CMakeLists.txt b/src/bpfilter/CMakeLists.txt index 610605f8..b5c9df1c 100644 --- a/src/bpfilter/CMakeLists.txt +++ b/src/bpfilter/CMakeLists.txt @@ -62,6 +62,7 @@ bf_target_add_elfstubs(bpfilter "parse_ipv6_nh" "update_counters" "log" + "flow_hash" ) target_compile_definitions(bpfilter diff --git a/src/bpfilter/bpf/flow_hash.bpf.c b/src/bpfilter/bpf/flow_hash.bpf.c new file mode 100644 index 00000000..794717ec --- /dev/null +++ b/src/bpfilter/bpf/flow_hash.bpf.c @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) 2026 Meta Platforms, Inc. and affiliates. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "cgen/runtime.h" + +/// Number of 32-bit words in an IPv6 address. +#define IPV6_ADDR_WORDS 4 + +/// Mask to extract the 20-bit flow label from IPv6 header first word. +#define IPV6_FLOW_LABEL_MASK 0x000FFFFF + +/// xxHash32 finalizer constants. +#define XXH32_PRIME1 0x85ebca77 +#define XXH32_PRIME2 0xc2b2ae3d +#define XXH32_MAGIC1 15 +#define XXH32_MAGIC2 13 +#define XXH32_MAGIC3 16 + +/** + * xxHash32 avalanche finalizer. + * + * Provides excellent bit mixing to ensure uniform distribution across all + * 32 bits. Changing any single input bit will change approximately 50% of + * output bits on average. + * + * @param hash Input hash value to finalize. + * @return Finalized hash with improved distribution. + */ +static inline __u32 xxh32_avalanche(__u32 hash) +{ + hash ^= hash >> XXH32_MAGIC1; + hash *= XXH32_PRIME1; + hash ^= hash >> XXH32_MAGIC2; + hash *= XXH32_PRIME2; + hash ^= hash >> XXH32_MAGIC3; + + return hash; +} + +/** + * Calculate flow hash from packet 5-tuple + IPv6 flow label. + * + * Computes a 32-bit hash by combining: + * - Source and destination IP addresses + * - Source and destination ports (TCP/UDP only) + * - Protocol number + * - IPv6 flow label (IPv6 only) + * + * The raw values are accumulated via XOR, then passed through an xxHash32 + * avalanche finalizer to ensure uniform distribution across all 32 bits. + * + * The hash uses packet source/destination addresses (not socket local/remote) + * to ensure matching consistency between sender and receiver. + * + * @param ctx Runtime context with parsed packet headers. + * @param l3_proto L3 protocol ID (network byte order, ETH_P_IP or ETH_P_IPV6). + * @param l4_proto L4 protocol ID (IPPROTO_TCP, IPPROTO_UDP, etc.). + * @return Computed 32-bit flow hash with uniform distribution. + */ +__u32 bf_flow_hash(struct bf_runtime *ctx, __u16 l3_proto, __u8 l4_proto) +{ + __u32 hash = 0; + + // Hash L3 addresses based on protocol + if (l3_proto == bpf_htons(ETH_P_IP)) { + struct iphdr *ip4 = ctx->l3_hdr; + + hash ^= ip4->saddr; + hash ^= ip4->daddr; + hash ^= (__u32)l4_proto << 16; + } else if (l3_proto == bpf_htons(ETH_P_IPV6)) { + struct ipv6hdr *ip6 = ctx->l3_hdr; + __u32 *saddr = (__u32 *)&ip6->saddr; + __u32 *daddr = (__u32 *)&ip6->daddr; + + // XOR all 4 words of source address + for (int i = 0; i < IPV6_ADDR_WORDS; ++i) + hash ^= saddr[i]; + + // XOR all 4 words of destination address + for (int i = 0; i < IPV6_ADDR_WORDS; ++i) + hash ^= daddr[i]; + + // Include flow label (20 bits from version/traffic class/flow label) + // First 4 bytes contain: version (4) + traffic class (8) + flow label (20) + // After ntohl, flow label is in the lower 20 bits + hash ^= bpf_ntohl(*(__u32 *)ip6) & IPV6_FLOW_LABEL_MASK; + + hash ^= (__u32)l4_proto << 16; + } + + // Hash L4 ports for TCP and UDP + if (l4_proto == IPPROTO_TCP) { + struct tcphdr *tcp = ctx->l4_hdr; + hash ^= ((__u32)tcp->source << 16) | tcp->dest; + } else if (l4_proto == IPPROTO_UDP) { + struct udphdr *udp = ctx->l4_hdr; + hash ^= ((__u32)udp->source << 16) | udp->dest; + } + + // Apply xxHash32 avalanche finalizer for uniform distribution + return xxh32_avalanche(hash); +} diff --git a/src/bpfilter/cgen/elfstub.h b/src/bpfilter/cgen/elfstub.h index c628c842..aaf84c6d 100644 --- a/src/bpfilter/cgen/elfstub.h +++ b/src/bpfilter/cgen/elfstub.h @@ -135,6 +135,24 @@ enum bf_elfstub_id */ BF_ELFSTUB_LOG, + /** + * Calculate flow hash from packet 5-tuple + IPv6 flow label. + * + * `__u32 bf_flow_hash(struct bf_runtime *ctx, __u16 l3_proto, __u8 l4_proto)` + * + * Computes a 32-bit hash by combining source/destination addresses, + * source/destination ports (TCP/UDP), protocol, and IPv6 flow label, + * then applies an xxHash32 avalanche finalizer for uniform distribution. + * + * **Parameters** + * - `ctx`: address of the `bf_runtime` context of the program. + * - `l3_proto`: L3 protocol ID (network byte order). + * - `l4_proto`: L4 protocol ID. + * + * **Return** The computed 32-bit flow hash with uniform distribution. + */ + BF_ELFSTUB_FLOW_HASH, + _BF_ELFSTUB_MAX, }; diff --git a/src/bpfilter/cgen/matcher/meta.c b/src/bpfilter/cgen/matcher/meta.c index 0086adf8..e655b6d7 100644 --- a/src/bpfilter/cgen/matcher/meta.c +++ b/src/bpfilter/cgen/matcher/meta.c @@ -7,6 +7,7 @@ #include #include +#include #include // NOLINT #include #include @@ -19,7 +20,9 @@ #include #include +#include "cgen/elfstub.h" #include "cgen/program.h" +#include "cgen/runtime.h" #include "cgen/swich.h" #include "filter.h" @@ -202,6 +205,56 @@ static int _bf_matcher_generate_meta_flow_hash(struct bf_program *program, return 0; } +static int +_bf_matcher_generate_meta_flow_probability(struct bf_program *program, + const struct bf_matcher *matcher) +{ + float proba = *(float *)bf_matcher_payload(matcher); + + // Ensure L3 is IPv4 or IPv6, skip to next rule otherwise + EMIT(program, BPF_JMP_IMM(BPF_JEQ, BPF_REG_7, htobe16(ETH_P_IP), 2)); + EMIT(program, BPF_JMP_IMM(BPF_JEQ, BPF_REG_7, htobe16(ETH_P_IPV6), 1)); + EMIT_FIXUP_JMP_NEXT_RULE(program, BPF_JMP_A(0)); + + // Ensure L4 is TCP or UDP, skip to next rule otherwise + EMIT(program, BPF_JMP_IMM(BPF_JEQ, BPF_REG_8, IPPROTO_TCP, 2)); + EMIT(program, BPF_JMP_IMM(BPF_JEQ, BPF_REG_8, IPPROTO_UDP, 1)); + EMIT_FIXUP_JMP_NEXT_RULE(program, BPF_JMP_A(0)); + + /* Calculate flow hash using the bf_flow_hash elfstub. + * + * The elfstub computes a 32-bit hash from the packet's 5-tuple + * (src ip, dst ip, src port, dst port, protocol) plus IPv6 flow label. + * This ensures all packets in a flow get the same hash value, making + * the probability decision consistent per-flow rather than per-packet. + * + * Arguments: + * - r1: pointer to bf_runtime context + * - r2: L3 protocol ID (from r7, set by prologue) + * - r3: L4 protocol ID (from r8, set by prologue) + * + * Return: hash value in r0 */ + // Set up elfstub arguments + EMIT(program, BPF_MOV64_REG(BPF_REG_1, BPF_REG_10)); + EMIT(program, BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, + -(int)sizeof(struct bf_runtime))); // r1 = ctx + EMIT(program, BPF_MOV64_REG(BPF_REG_2, BPF_REG_7)); // r2 = l3_proto + EMIT(program, BPF_MOV64_REG(BPF_REG_3, BPF_REG_8)); // r3 = l4_proto + + // Call the elfstub - result in r0 + EMIT_FIXUP_ELFSTUB(program, BF_ELFSTUB_FLOW_HASH); + + // Compare the computed hash with the threshold based on probability + // The hash is uniformly distributed across 32 bits, so we compare against + // UINT32_MAX * (proba / 100.0) to select the desired percentage of flows + EMIT_FIXUP_JMP_NEXT_RULE( + program, + BPF_JMP32_IMM(BPF_JGT, BPF_REG_0, + (uint32_t)((double)UINT32_MAX * (proba / 100.0)), 0)); + + return 0; +} + int bf_matcher_generate_meta(struct bf_program *program, const struct bf_matcher *matcher) { @@ -230,6 +283,9 @@ int bf_matcher_generate_meta(struct bf_program *program, case BF_MATCHER_META_FLOW_HASH: r = _bf_matcher_generate_meta_flow_hash(program, matcher); break; + case BF_MATCHER_META_FLOW_PROBABILITY: + r = _bf_matcher_generate_meta_flow_probability(program, matcher); + break; default: return bf_err_r(-EINVAL, "unknown matcher type %d", bf_matcher_get_type(matcher)); diff --git a/src/bpfilter/cgen/program.c b/src/bpfilter/cgen/program.c index 88b7345c..4a11f9cb 100644 --- a/src/bpfilter/cgen/program.c +++ b/src/bpfilter/cgen/program.c @@ -528,6 +528,7 @@ static int _bf_program_generate_rule(struct bf_program *program, case BF_MATCHER_META_DPORT: case BF_MATCHER_META_MARK: case BF_MATCHER_META_FLOW_HASH: + case BF_MATCHER_META_FLOW_PROBABILITY: r = bf_matcher_generate_meta(program, matcher); if (r) return r; diff --git a/src/libbpfilter/include/bpfilter/matcher.h b/src/libbpfilter/include/bpfilter/matcher.h index 15eb970e..73812e8f 100644 --- a/src/libbpfilter/include/bpfilter/matcher.h +++ b/src/libbpfilter/include/bpfilter/matcher.h @@ -64,6 +64,8 @@ enum bf_matcher_type BF_MATCHER_META_MARK, /// Matches a specific flow hash (source/destination IP and ports). BF_MATCHER_META_FLOW_HASH, + /// Matches packets based on flow probability (consistent per flow). + BF_MATCHER_META_FLOW_PROBABILITY, /// Matches IPv4 source address. BF_MATCHER_IP4_SADDR, /// Matches IPv4 source network. diff --git a/src/libbpfilter/matcher.c b/src/libbpfilter/matcher.c index 605595ce..a422d664 100644 --- a/src/libbpfilter/matcher.c +++ b/src/libbpfilter/matcher.c @@ -3,6 +3,8 @@ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates. */ +#define _GNU_SOURCE + #include "bpfilter/matcher.h" #include @@ -21,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -397,6 +400,45 @@ void _bf_print_probability(const void *payload) (void)fprintf(stdout, "%" PRIu8 "%%", *(uint8_t *)payload); } +static int _bf_parse_flow_probability(enum bf_matcher_type type, + enum bf_matcher_op op, void *payload, + const char *raw_payload) +{ + assert(payload); + assert(raw_payload); + + double proba; + char *endptr; + + locale_t c_locale = newlocale(LC_NUMERIC_MASK, "C", (locale_t)0); + if (!c_locale) + return bf_err_r(-ENOMEM, "failed to create C locale"); + proba = strtod_l(raw_payload, &endptr, c_locale); + freelocale(c_locale); + if (endptr[0] == '%' && endptr[1] == '\0' && + proba >= 0.0 && proba <= 100.0) { + *(float *)payload = (float)proba; + return 0; + } + + bf_err( + "\"%s %s\" expects a valid percentage value (i.e., within [0%%, 100%%], e.g., \"50%%\" or \"33.33%%\"), not '%s'", + bf_matcher_type_to_str(type), bf_matcher_op_to_str(op), raw_payload); + + return -EINVAL; +} + +static void _bf_print_flow_probability(const void *payload) +{ + assert(payload); + + float proba = *(float *)payload; + if (proba == (float)(int)proba) + (void)fprintf(stdout, "%.0f%%", proba); + else + (void)fprintf(stdout, "%g%%", proba); +} + static int _bf_parse_mark(enum bf_matcher_type type, enum bf_matcher_op op, void *payload, const char *raw_payload) { @@ -924,6 +966,20 @@ static struct bf_matcher_meta _bf_matcher_metas[_BF_MATCHER_TYPE_MAX] = { _bf_parse_int_range, _bf_print_int_range), }, }, + [BF_MATCHER_META_FLOW_PROBABILITY] = + { + .layer = BF_MATCHER_NO_LAYER, + .unsupported_hooks = + BF_FLAGS(BF_HOOK_NF_FORWARD, BF_HOOK_NF_LOCAL_IN, + BF_HOOK_NF_LOCAL_OUT, BF_HOOK_NF_POST_ROUTING, + BF_HOOK_NF_PRE_ROUTING), + .ops = + { + BF_MATCHER_OPS(BF_MATCHER_EQ, sizeof(float), + _bf_parse_flow_probability, + _bf_print_flow_probability), + }, + }, [BF_MATCHER_IP4_SADDR] = { .layer = BF_MATCHER_LAYER_3, @@ -1469,6 +1525,7 @@ static const char *_bf_matcher_type_strs[] = { [BF_MATCHER_META_DPORT] = "meta.dport", [BF_MATCHER_META_MARK] = "meta.mark", [BF_MATCHER_META_FLOW_HASH] = "meta.flow_hash", + [BF_MATCHER_META_FLOW_PROBABILITY] = "meta.flow_probability", [BF_MATCHER_IP4_SADDR] = "ip4.saddr", [BF_MATCHER_IP4_SNET] = "ip4.snet", [BF_MATCHER_IP4_DADDR] = "ip4.daddr", diff --git a/tests/e2e/CMakeLists.txt b/tests/e2e/CMakeLists.txt index ee505c4d..f43e03a3 100644 --- a/tests/e2e/CMakeLists.txt +++ b/tests/e2e/CMakeLists.txt @@ -87,6 +87,7 @@ bf_add_e2e_test(e2e matchers/ip6_saddr.sh) bf_add_e2e_test(e2e matchers/ip6_snet.sh) bf_add_e2e_test(e2e matchers/meta_dport.sh) bf_add_e2e_test(e2e matchers/meta_flow_hash.sh) +bf_add_e2e_test(e2e matchers/meta_flow_probability.sh) bf_add_e2e_test(e2e matchers/meta_iface.sh) bf_add_e2e_test(e2e matchers/meta_l3_proto.sh) bf_add_e2e_test(e2e matchers/meta_l4_proto.sh) diff --git a/tests/e2e/matchers/meta_flow_probability.sh b/tests/e2e/matchers/meta_flow_probability.sh new file mode 100755 index 00000000..4c399a0a --- /dev/null +++ b/tests/e2e/matchers/meta_flow_probability.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -eux +set -o pipefail + +. "$(dirname "$0")"/../e2e_test_util.sh + +# Unsupported hooks: all NF hooks +(! bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_NF_FORWARD ACCEPT rule meta.flow_probability eq 50% counter DROP") +(! bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_NF_LOCAL_IN ACCEPT rule meta.flow_probability eq 50% counter DROP") +(! bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_NF_LOCAL_OUT ACCEPT rule meta.flow_probability eq 50% counter DROP") +(! bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_NF_POST_ROUTING ACCEPT rule meta.flow_probability eq 50% counter DROP") +(! bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_NF_PRE_ROUTING ACCEPT rule meta.flow_probability eq 50% counter DROP") + +# Supported hooks: XDP, TC, and CGROUP +bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_XDP ACCEPT rule meta.flow_probability eq 50% counter DROP" +bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq 0% counter DROP" +bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq 50% counter DROP" +bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq 100% counter DROP" +bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_EGRESS ACCEPT rule meta.flow_probability eq 50% counter DROP" +bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_CGROUP_INGRESS ACCEPT rule meta.flow_probability eq 50% counter DROP" +bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_CGROUP_EGRESS ACCEPT rule meta.flow_probability eq 50% counter DROP" + +# Floating-point percentages +bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq 33.33% counter DROP" +bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq 0.1% counter DROP" +bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq 99.99% counter DROP" +bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq 50.0% counter DROP" + +# Invalid probability values +(! bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq 0 counter DROP") +(! bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq -10% counter DROP") +(! bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq 1000 counter DROP") +(! bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq 1000% counter DROP") +(! bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq 100.01% counter DROP") +(! bfcli ruleset set --dry-run --from-str "chain test BF_HOOK_TC_INGRESS ACCEPT rule meta.flow_probability eq teapot counter DROP") diff --git a/tests/unit/libbpfilter/matcher.c b/tests/unit/libbpfilter/matcher.c index cd097c08..a7713004 100644 --- a/tests/unit/libbpfilter/matcher.c +++ b/tests/unit/libbpfilter/matcher.c @@ -1239,6 +1239,28 @@ static void print_functions(void **state) ops->print(bf_matcher_payload(matcher)); bf_matcher_free(&matcher); + // Test _bf_print_flow_probability via ops (integer value) + assert_ok(bf_matcher_new_from_raw(&matcher, + BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "50%")); + ops = bf_matcher_get_ops(BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ); + assert_non_null(ops); + assert_non_null(ops->print); + ops->print(bf_matcher_payload(matcher)); + bf_matcher_free(&matcher); + + // Test _bf_print_flow_probability via ops (fractional value) + assert_ok(bf_matcher_new_from_raw(&matcher, + BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "33.33%")); + ops = bf_matcher_get_ops(BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ); + assert_non_null(ops); + assert_non_null(ops->print); + ops->print(bf_matcher_payload(matcher)); + bf_matcher_free(&matcher); + // Test _bf_print_mark via ops assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_META_MARK, BF_MATCHER_EQ, "0x1234")); @@ -1488,6 +1510,122 @@ static void ip4_dscp(void **state) BF_MATCHER_EQ, "-1")); } +static void meta_flow_probability(void **state) +{ + _free_bf_matcher_ struct bf_matcher *matcher = NULL; + prefix_t prefix = {}; + + (void)state; + + // Test with 0% + assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "0%")); + assert_non_null(matcher); + assert_int_equal(bf_matcher_get_type(matcher), + BF_MATCHER_META_FLOW_PROBABILITY); + assert_int_equal(bf_matcher_get_op(matcher), BF_MATCHER_EQ); + assert_int_equal(bf_matcher_payload_len(matcher), sizeof(float)); + assert_true(*(float *)bf_matcher_payload(matcher) == 0.0f); + bf_matcher_dump(matcher, &prefix); + bf_matcher_free(&matcher); + + // Test with 50% + assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "50%")); + assert_non_null(matcher); + assert_true(*(float *)bf_matcher_payload(matcher) == 50.0f); + bf_matcher_dump(matcher, &prefix); + bf_matcher_free(&matcher); + + // Test with 100% + assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "100%")); + assert_non_null(matcher); + assert_true(*(float *)bf_matcher_payload(matcher) == 100.0f); + bf_matcher_dump(matcher, &prefix); + bf_matcher_free(&matcher); + + // Test with floating-point value + assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "33.33%")); + assert_non_null(matcher); + assert_true(*(float *)bf_matcher_payload(matcher) > 33.32f); + assert_true(*(float *)bf_matcher_payload(matcher) < 33.34f); + bf_matcher_dump(matcher, &prefix); + bf_matcher_free(&matcher); + + // Test with small floating-point value + assert_ok(bf_matcher_new_from_raw(&matcher, BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "0.1%")); + assert_non_null(matcher); + assert_true(*(float *)bf_matcher_payload(matcher) > 0.09f); + assert_true(*(float *)bf_matcher_payload(matcher) < 0.11f); + bf_matcher_dump(matcher, &prefix); +} + +static void meta_flow_probability_invalid(void **state) +{ + _free_bf_matcher_ struct bf_matcher *matcher = NULL; + + (void)state; + + // Test with value over 100% + assert_err(bf_matcher_new_from_raw(&matcher, BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "101%")); + + // Test with value slightly over 100% + assert_err(bf_matcher_new_from_raw(&matcher, BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "100.01%")); + + // Test without % sign + assert_err(bf_matcher_new_from_raw(&matcher, BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "50")); + + // Test with negative value + assert_err(bf_matcher_new_from_raw(&matcher, BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "-10%")); +} + +static void meta_flow_probability_pack_unpack(void **state) +{ + _free_bf_matcher_ struct bf_matcher *source = NULL; + _free_bf_matcher_ struct bf_matcher *destination = NULL; + _free_bf_wpack_ bf_wpack_t *wpack = NULL; + _free_bf_rpack_ bf_rpack_t *rpack = NULL; + const void *data; + size_t data_len; + + (void)state; + + // Test pack/unpack for EQ operator with integer percentage + assert_ok(bf_matcher_new_from_raw(&source, BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "50%")); + assert_ok(bf_wpack_new(&wpack)); + assert_ok(bf_matcher_pack(source, wpack)); + assert_ok(bf_wpack_get_data(wpack, &data, &data_len)); + + assert_ok(bf_rpack_new(&rpack, data, data_len)); + assert_ok(bf_matcher_new_from_pack(&destination, bf_rpack_root(rpack))); + + assert_true(bft_matcher_equal(source, destination)); + bf_matcher_free(&source); + bf_matcher_free(&destination); + bf_wpack_free(&wpack); + bf_rpack_free(&rpack); + + // Test pack/unpack with floating-point percentage + assert_ok(bf_matcher_new_from_raw(&source, BF_MATCHER_META_FLOW_PROBABILITY, + BF_MATCHER_EQ, "33.33%")); + assert_ok(bf_wpack_new(&wpack)); + assert_ok(bf_matcher_pack(source, wpack)); + assert_ok(bf_wpack_get_data(wpack, &data, &data_len)); + + assert_ok(bf_rpack_new(&rpack, data, data_len)); + assert_ok(bf_matcher_new_from_pack(&destination, bf_rpack_root(rpack))); + + assert_true(bft_matcher_equal(source, destination)); +} + int main(void) { const struct CMUnitTest tests[] = { @@ -1546,6 +1684,9 @@ int main(void) cmocka_unit_test(error_paths_parse), cmocka_unit_test(error_paths_print), cmocka_unit_test(ip4_dscp), + cmocka_unit_test(meta_flow_probability), + cmocka_unit_test(meta_flow_probability_invalid), + cmocka_unit_test(meta_flow_probability_pack_unpack), }; return cmocka_run_group_tests(tests, NULL, NULL); diff --git a/tools/benchmarks/main.cpp b/tools/benchmarks/main.cpp index 96749d6d..0ebbe678 100644 --- a/tools/benchmarks/main.cpp +++ b/tools/benchmarks/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -513,6 +514,72 @@ void single_rule__tcp_sport(::benchmark::State &state) } BENCHMARK(single_rule__tcp_sport); +void single_rule__meta_flow_hash(::benchmark::State &state) +{ + // meta.flow_hash is only supported on TC hooks + Chain chain("bf_benchmark", BF_HOOK_TC_INGRESS, BF_VERDICT_ACCEPT); + + // Match flow hash in full range 0-UINT32_MAX (little-endian uint32_t: min, max) + chain << Rule(BF_VERDICT_DROP, false, {}, std::vector{ + Matcher(BF_MATCHER_META_FLOW_HASH, BF_MATCHER_RANGE, {0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff}), + }); + + auto chainp = chain.get(); + int ret = bf_chain_set(chainp.get(), nullptr); + if (ret < 0) + throw std::runtime_error("failed to load chain"); + + auto prog = bf::test::Program(chain.name()); + + // TC_ACT_SHOT = 2 for DROP + while (state.KeepRunningBatch(::bf::progRunRepeat)) { + auto stats = prog.run(::bf::pkt_local_ip4_tcp); + if (stats.retval != 2) + state.SkipWithError("benchmark run failed"); + + state.SetIterationTime((double)stats.duration * stats.repeat); + } + + state.counters["nInsn"] = prog.nInsn(); + state.SetLabel("1 rule, meta.flow_hash"); +} +BENCHMARK(single_rule__meta_flow_hash); + +void single_rule__meta_flow_probability(::benchmark::State &state) +{ + // meta.flow_probability is supported on XDP, TC, and CGROUP hooks + Chain chain("bf_benchmark", BF_HOOK_TC_INGRESS, BF_VERDICT_ACCEPT); + + // Match with 100% probability (float payload: 100.0f = 100%) + float prob = 100.0f; + std::vector payload(sizeof(float)); + std::memcpy(payload.data(), &prob, sizeof(float)); + + chain << Rule(BF_VERDICT_DROP, false, {}, std::vector{ + Matcher(BF_MATCHER_META_FLOW_PROBABILITY, BF_MATCHER_EQ, payload), + }); + + auto chainp = chain.get(); + int ret = bf_chain_set(chainp.get(), nullptr); + if (ret < 0) + throw std::runtime_error("failed to load chain"); + + auto prog = bf::test::Program(chain.name()); + + // TC_ACT_SHOT = 2 for DROP + while (state.KeepRunningBatch(::bf::progRunRepeat)) { + auto stats = prog.run(::bf::pkt_local_ip4_tcp); + if (stats.retval != 2) + state.SkipWithError("benchmark run failed"); + + state.SetIterationTime((double)stats.duration * stats.repeat); + } + + state.counters["nInsn"] = prog.nInsn(); + state.SetLabel("1 rule, meta.flow_probability"); +} +BENCHMARK(single_rule__meta_flow_probability); + } // namespace int main(int argc, char *argv[])