Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/usage/bfcli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
####
Expand Down
12 changes: 12 additions & 0 deletions src/bfcli/lexer.l
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,6 +45,7 @@
%s STATE_MATCHER_TCP_FLAGS

int (-|(0x))?[0-9a-zA-Z]+
float [0-9]+(\.[0-9]+)?
%%

[ \t\n] ;
Expand Down Expand Up @@ -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; }
<STATE_MATCHER_META_FLOW_PROBA>{
(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; }
<STATE_MATCHER_META_MARK>{
(eq|not) { yylval.sval = strdup(yytext); return MATCHER_OP; }
Expand Down
1 change: 1 addition & 0 deletions src/bpfilter/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ bf_target_add_elfstubs(bpfilter
"parse_ipv6_nh"
"update_counters"
"log"
"flow_hash"
)

target_compile_definitions(bpfilter
Expand Down
116 changes: 116 additions & 0 deletions src/bpfilter/bpf/flow_hash.bpf.c
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flow_hash.bpf.c -> flow_proba.bpf.c

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formally the code in the file still computes hash, not probability

Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
*/

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/tcp.h>
#include <linux/udp.h>

#include <bpf/bpf_endian.h>
#include <stddef.h>

#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);
}
18 changes: 18 additions & 0 deletions src/bpfilter/cgen/elfstub.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
56 changes: 56 additions & 0 deletions src/bpfilter/cgen/matcher/meta.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <linux/bpf.h>
#include <linux/bpf_common.h>
#include <linux/if_ether.h>
#include <linux/in.h> // NOLINT
#include <linux/tcp.h>
#include <linux/udp.h>
Expand All @@ -19,7 +20,9 @@
#include <bpfilter/logger.h>
#include <bpfilter/matcher.h>

#include "cgen/elfstub.h"
#include "cgen/program.h"
#include "cgen/runtime.h"
#include "cgen/swich.h"
#include "filter.h"

Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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));
Expand Down
1 change: 1 addition & 0 deletions src/bpfilter/cgen/program.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/libbpfilter/include/bpfilter/matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
57 changes: 57 additions & 0 deletions src/libbpfilter/matcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
*/

#define _GNU_SOURCE

#include "bpfilter/matcher.h"

#include <linux/icmp.h>
Expand All @@ -21,6 +23,7 @@
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <locale.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading