diff --git a/src/bfcli/lexer.l b/src/bfcli/lexer.l index 1ecb0597b..906501c63 100644 --- a/src/bfcli/lexer.l +++ b/src/bfcli/lexer.l @@ -42,6 +42,7 @@ %s STATE_MATCHER_ICMP_TYPE %s STATE_MATCHER_ICMP_CODE %s STATE_MATCHER_TCP_FLAGS +%s STATE_MATCHER_META_LIMIT int (-|(0x))?[0-9a-zA-Z]+ float [0-9]+(\.[0-9]+)? @@ -186,6 +187,16 @@ meta\.flow_hash { BEGIN(STATE_MATCHER_META_FLOW_HASH); yylval.sval = strdup( } } +meta\.limit { BEGIN(STATE_MATCHER_META_LIMIT); yylval.sval = strdup(yytext); return MATCHER_TYPE; } +{ + (eq) { yylval.sval = strdup(yytext); return MATCHER_OP; } + {int}\/[a-zA-Z] { + BEGIN(INITIAL); + yylval.sval = strdup(yytext); + return RAW_PAYLOAD; + } +} + ip4\.saddr { BEGIN(STATE_MATCHER_IPV4_ADDR); yylval.sval = strdup(yytext); return MATCHER_TYPE; } ip4\.daddr { BEGIN(STATE_MATCHER_IPV4_ADDR); yylval.sval = strdup(yytext); return MATCHER_TYPE; } { diff --git a/src/libbpfilter/CMakeLists.txt b/src/libbpfilter/CMakeLists.txt index 4d3bf86a6..83b7f4fcd 100644 --- a/src/libbpfilter/CMakeLists.txt +++ b/src/libbpfilter/CMakeLists.txt @@ -117,6 +117,7 @@ bf_target_add_elfstubs(libbpfilter "pkt_log" "flow_hash" "sock_addr_log" + "limit" ) target_compile_definitions(libbpfilter diff --git a/src/libbpfilter/bpf/limit.bpf.c b/src/libbpfilter/bpf/limit.bpf.c new file mode 100644 index 000000000..244a25d8b --- /dev/null +++ b/src/libbpfilter/bpf/limit.bpf.c @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) 2026 Meta Platforms, Inc. and affiliates. + */ + +#include + +#include +#include +#include + +#include "cgen/runtime.h" + +#define BF_TIME_S 1000000000 + +__u8 bf_ratelimit(void *map, const __u16 limit, __u8 letter) +{ + struct bf_ratelimit *ratelimit; + __u64 current_time_ns = bpf_ktime_get_ns(); + __u32 key = 0; // NTODO: should not be 0, but dynamic (WIP) + + ratelimit = bpf_map_lookup_elem(map, &key); + if (!ratelimit) { + bpf_printk("failed to fetch the rule's ratelimit"); + return 1; + } + + switch (letter) { + case 's': + if (current_time_ns > ratelimit->last_time + BF_TIME_S) { + ratelimit->current = 0; + ratelimit->last_time = current_time_ns; + } + break; + default: + bpf_printk("the time unit '%c' is not recognized", letter); + return 1; + } + + ratelimit->current++; + return (ratelimit->current > limit); +} diff --git a/src/libbpfilter/cgen/fixup.h b/src/libbpfilter/cgen/fixup.h index 0a129cc78..3457d852c 100644 --- a/src/libbpfilter/cgen/fixup.h +++ b/src/libbpfilter/cgen/fixup.h @@ -41,6 +41,8 @@ enum bf_fixup_type BF_FIXUP_TYPE_STATE_MAP_FD, /// Set a set map file descriptor in the @c BPF_LD_MAP_FD instruction. BF_FIXUP_TYPE_SET_MAP_FD, + /// Set the limit map file descriptor in the @c BPF_LD_MAP_FD instruction. + BF_FIXUP_TYPE_LIMIT_MAP_FD, /// Call an ELF stub. BF_FIXUP_ELFSTUB_CALL, _BF_FIXUP_TYPE_MAX diff --git a/src/libbpfilter/cgen/handle.c b/src/libbpfilter/cgen/handle.c index 6e834fa10..d7c9d2069 100644 --- a/src/libbpfilter/cgen/handle.c +++ b/src/libbpfilter/cgen/handle.c @@ -118,6 +118,15 @@ int bf_handle_new_from_pack(struct bf_handle **handle, struct bf_lock *lock, return bf_rpack_key_err(r, "bf_handle.smap"); } + r = bf_rpack_kv_node(node, "rmap", &child); + if (r) + return bf_rpack_key_err(r, "bf_handle.rmap"); + if (!bf_rpack_is_nil(child)) { + r = bf_map_new_from_pack(&_handle->rmap, dir_fd, child); + if (r) + return bf_rpack_key_err(r, "bf_handle.rmap"); + } + r = bf_rpack_kv_array(node, "sets", &child); if (r) return bf_rpack_key_err(r, "bf_handle.sets"); @@ -156,6 +165,7 @@ void bf_handle_free(struct bf_handle **handle) bf_map_free(&(*handle)->pmap); bf_map_free(&(*handle)->lmap); bf_map_free(&(*handle)->smap); + bf_map_free(&(*handle)->rmap); bf_list_clean(&(*handle)->sets); free(*handle); @@ -209,6 +219,14 @@ int bf_handle_pack(const struct bf_handle *handle, bf_wpack_t *pack) bf_wpack_kv_nil(pack, "smap"); } + if (handle->rmap) { + bf_wpack_open_object(pack, "rmap"); + bf_map_pack(handle->rmap, pack); + bf_wpack_close_object(pack); + } else { + bf_wpack_kv_nil(pack, "rmap"); + } + bf_wpack_kv_list(pack, "sets", &handle->sets); return bf_wpack_is_valid(pack) ? 0 : -EINVAL; @@ -271,6 +289,15 @@ void bf_handle_dump(const struct bf_handle *handle, prefix_t *prefix) DUMP(prefix, "smap: struct bf_map * (NULL)"); } + if (handle->rmap) { + DUMP(prefix, "rmap: struct bf_map *"); + bf_dump_prefix_push(prefix); + bf_map_dump(handle->rmap, bf_dump_prefix_last(prefix)); + bf_dump_prefix_pop(prefix); + } else { + DUMP(prefix, "rmap: struct bf_map * (NULL)"); + } + DUMP(bf_dump_prefix_last(prefix), "sets: bf_list[%lu]", bf_list_size(&handle->sets)); bf_dump_prefix_push(prefix); @@ -340,6 +367,14 @@ int bf_handle_pin(struct bf_handle *handle, struct bf_lock *lock) } } + if (handle->rmap) { + r = bf_map_pin(handle->rmap, dir_fd); + if (r) { + bf_err_r(r, "failed to pin BPF rate limit map"); + goto err_unpin_all; + } + } + bf_list_foreach (&handle->sets, set_node) { struct bf_map *map = bf_list_node_get_data(set_node); @@ -385,6 +420,8 @@ void bf_handle_unpin(struct bf_handle *handle, struct bf_lock *lock) bf_map_unpin(handle->lmap, dir_fd); if (handle->smap) bf_map_unpin(handle->smap, dir_fd); + if (handle->rmap) + bf_map_unpin(handle->rmap, dir_fd); bf_list_foreach (&handle->sets, set_node) { struct bf_map *map = bf_list_node_get_data(set_node); @@ -515,5 +552,6 @@ void bf_handle_unload(struct bf_handle *handle) bf_map_free(&handle->pmap); bf_map_free(&handle->lmap); bf_map_free(&handle->smap); + bf_map_free(&handle->rmap); bf_list_clean(&handle->sets); } diff --git a/src/libbpfilter/cgen/handle.h b/src/libbpfilter/cgen/handle.h index 2dfa99626..fe94f5c24 100644 --- a/src/libbpfilter/cgen/handle.h +++ b/src/libbpfilter/cgen/handle.h @@ -56,6 +56,9 @@ struct bf_handle * per rule. NULL if the chain has no logging rules. */ struct bf_map *smap; + /** Rate limit map. NULL if not created. */ + struct bf_map *rmap; + /** List of set maps. Contains at most one map for each unique key * format. */ bf_list sets; diff --git a/src/libbpfilter/cgen/matcher/meta.c b/src/libbpfilter/cgen/matcher/meta.c index 44a4d5c32..b26db0231 100644 --- a/src/libbpfilter/cgen/matcher/meta.c +++ b/src/libbpfilter/cgen/matcher/meta.c @@ -157,6 +157,31 @@ _bf_matcher_generate_meta_flow_probability(struct bf_program *program, return 0; } +static int _bf_matcher_generate_meta_limit(struct bf_program *program, + const struct bf_matcher *matcher) +{ + uint32_t tmp = *(uint32_t *)bf_matcher_payload(matcher); + uint16_t limit = tmp; + uint8_t letter = tmp >> 16; + + EMIT_LOAD_LIMIT_FD_FIXUP(program, BPF_REG_1); + EMIT(program, BPF_MOV32_IMM(BPF_REG_2, limit)); + EMIT(program, BPF_MOV32_IMM(BPF_REG_3, letter)); + EMIT(program, + BPF_MOV32_IMM(BPF_REG_4, bf_program_chain_counter_idx(program))); + EMIT_FIXUP_ELFSTUB(program, BF_ELFSTUB_LIMIT); + + if (bf_matcher_get_negate(matcher)) { + EMIT_FIXUP_JMP_NEXT_RULE(program, + BPF_JMP32_IMM(BPF_JEQ, BPF_REG_0, 0, 0)); + } else { + EMIT_FIXUP_JMP_NEXT_RULE(program, + BPF_JMP32_IMM(BPF_JNE, BPF_REG_0, 0, 0)); + } + + return 0; +} + int bf_matcher_generate_meta(struct bf_program *program, const struct bf_matcher *matcher) { @@ -177,6 +202,8 @@ int bf_matcher_generate_meta(struct bf_program *program, return _bf_matcher_generate_meta_port(program, matcher); case BF_MATCHER_META_FLOW_PROBABILITY: return _bf_matcher_generate_meta_flow_probability(program, matcher); + case BF_MATCHER_META_LIMIT: + return _bf_matcher_generate_meta_limit(program, matcher); case BF_MATCHER_META_MARK: case BF_MATCHER_META_FLOW_HASH: return bf_err_r(-ENOTSUP, diff --git a/src/libbpfilter/cgen/packet.c b/src/libbpfilter/cgen/packet.c index ef078c255..701ac10c5 100644 --- a/src/libbpfilter/cgen/packet.c +++ b/src/libbpfilter/cgen/packet.c @@ -376,6 +376,7 @@ int bf_packet_gen_inline_matcher(struct bf_program *program, case BF_MATCHER_META_SPORT: case BF_MATCHER_META_DPORT: case BF_MATCHER_META_FLOW_PROBABILITY: + case BF_MATCHER_META_LIMIT: return bf_matcher_generate_meta(program, matcher); case BF_MATCHER_META_MARK: case BF_MATCHER_META_FLOW_HASH: diff --git a/src/libbpfilter/cgen/prog/map.c b/src/libbpfilter/cgen/prog/map.c index f723ebe1c..6cf875bf8 100644 --- a/src/libbpfilter/cgen/prog/map.c +++ b/src/libbpfilter/cgen/prog/map.c @@ -124,6 +124,7 @@ static struct bf_btf *_bf_map_make_btf(const struct bf_map *map) case BF_MAP_TYPE_SET: case BF_MAP_TYPE_LOG: case BF_MAP_TYPE_CTX: + case BF_MAP_TYPE_LIMIT: // No BTF data available for these map types return NULL; default: @@ -209,6 +210,7 @@ int bf_map_new(struct bf_map **map, const char *name, enum bf_map_type type, [BF_MAP_TYPE_SET] = BF_BPF_MAP_TYPE_HASH, [BF_MAP_TYPE_CTX] = BF_BPF_MAP_TYPE_ARRAY, [BF_MAP_TYPE_STATE] = BF_BPF_MAP_TYPE_ARRAY, + [BF_MAP_TYPE_LIMIT] = BF_BPF_MAP_TYPE_ARRAY, }; assert(map); @@ -329,6 +331,7 @@ static const char *_bf_map_type_to_str(enum bf_map_type type) [BF_MAP_TYPE_SET] = "BF_MAP_TYPE_SET", [BF_MAP_TYPE_CTX] = "BF_MAP_TYPE_CTX", [BF_MAP_TYPE_STATE] = "BF_MAP_TYPE_STATE", + [BF_MAP_TYPE_LIMIT] = "BF_MAP_TYPE_LIMIT", }; static_assert_enum_mapping(type_strs, _BF_MAP_TYPE_MAX); diff --git a/src/libbpfilter/cgen/prog/map.h b/src/libbpfilter/cgen/prog/map.h index 3a51f384d..1c497026d 100644 --- a/src/libbpfilter/cgen/prog/map.h +++ b/src/libbpfilter/cgen/prog/map.h @@ -22,6 +22,7 @@ enum bf_map_type BF_MAP_TYPE_LOG, BF_MAP_TYPE_SET, BF_MAP_TYPE_CTX, + BF_MAP_TYPE_LIMIT, /** Single-entry array map holding per-rule mutable state. The value is a * flat array of `bf_rule_state` entries, indexed by rule position. */ diff --git a/src/libbpfilter/cgen/program.c b/src/libbpfilter/cgen/program.c index 85282af0c..16c3c2faf 100644 --- a/src/libbpfilter/cgen/program.c +++ b/src/libbpfilter/cgen/program.c @@ -63,6 +63,7 @@ #define _BF_PRINTER_MAP_NAME "bf_pmap" #define _BF_LOG_MAP_NAME "bf_lmap" #define _BF_STATE_MAP_NAME "bf_smap" +#define _BF_LIMIT_MAP_NAME "bf_rmap" static inline size_t _bf_round_next_power_of_2(size_t value) { @@ -441,6 +442,10 @@ static int _bf_program_fixup(struct bf_program *program, insn_type = BF_FIXUP_INSN_IMM; value = program->handle->smap->fd; break; + case BF_FIXUP_TYPE_LIMIT_MAP_FD: + insn_type = BF_FIXUP_INSN_IMM; + value = program->handle->rmap->fd; + break; case BF_FIXUP_TYPE_SET_MAP_FD: { const struct bf_set_group *group = _bf_program_find_set_group(program, fixup->attr.set_ptr); @@ -975,7 +980,6 @@ static int _bf_program_load_state_map(struct bf_program *program) int r; assert(program); - if (!(program->runtime.chain->flags & BF_FLAG(BF_CHAIN_LOG_RATELIMIT))) return 0; @@ -996,6 +1000,31 @@ static int _bf_program_load_state_map(struct bf_program *program) return 0; } +static int _bf_program_load_limit_map(struct bf_program *program) +{ + _cleanup_free_ void *pstr = NULL; + uint32_t key = 0; + struct bf_ratelimit val = {.current = 0, .last_time = 0}; + int r; + + assert(program); + r = bf_map_new(&program->handle->rmap, _BF_LIMIT_MAP_NAME, + BF_MAP_TYPE_LIMIT, sizeof(uint32_t), + sizeof(struct bf_ratelimit), 1); + if (r) + return bf_err_r(r, "failed to create the rate limit bf_map object"); + + r = bf_map_set_elem(program->handle->rmap, &key, &val); + if (r) + return bf_err_r(r, "failed to set rate limit map elem"); + + r = _bf_program_fixup(program, BF_FIXUP_TYPE_LIMIT_MAP_FD); + if (r) + return bf_err_r(r, "failed to fixup rate limit map FD"); + + return 0; +} + static uint64_t _bf_dedup_hash(const void *data, void *ctx) { return bf_fnv1a(data, *(const size_t *)ctx, bf_fnv1a_init()); @@ -1156,6 +1185,10 @@ int bf_program_load(struct bf_program *prog) if (r) return bf_err_r(r, "failed to load the state map"); + r = _bf_program_load_limit_map(prog); + if (r) + return bf_err_r(r, "failed to load the rate limit map"); + if (bf_ctx_is_verbose(BF_VERBOSE_DEBUG)) { log_buf = malloc(_BF_LOG_BUF_SIZE); if (!log_buf) { diff --git a/src/libbpfilter/cgen/program.h b/src/libbpfilter/cgen/program.h index 4682f9b1e..adc608f2b 100644 --- a/src/libbpfilter/cgen/program.h +++ b/src/libbpfilter/cgen/program.h @@ -171,6 +171,18 @@ return __r; \ }) +#define EMIT_LOAD_LIMIT_FD_FIXUP(program, reg) \ + ({ \ + const struct bpf_insn ld_insn[2] = {BPF_LD_MAP_FD(reg, 0)}; \ + int __r = bf_program_emit_fixup((program), BF_FIXUP_TYPE_LIMIT_MAP_FD, \ + ld_insn[0], NULL); \ + if (__r < 0) \ + return __r; \ + __r = bf_program_emit((program), ld_insn[1]); \ + if (__r < 0) \ + return __r; \ + }) + /** * Load a specific set's file descriptor. * diff --git a/src/libbpfilter/cgen/runtime.h b/src/libbpfilter/cgen/runtime.h index ad3227acb..5ecee9153 100644 --- a/src/libbpfilter/cgen/runtime.h +++ b/src/libbpfilter/cgen/runtime.h @@ -179,5 +179,20 @@ struct bf_runtime static_assert(sizeof(struct bf_runtime) % 8 == 0, "bf_runtime should be aligned to 8 bytes"); +/** + * @brief Keep timestamps to allow for rate limiting + * + */ +struct bf_ratelimit +{ + /** Current timestamp (at the last call of the BPF) */ + __u64 current; + + /** Last time the rate limit was reset */ + __u64 last_time; +}; + +// I don't think we need a static_assert ? + extern void *bpf_dynptr_slice(const struct bpf_dynptr *, __u32, void *, __u32); extern int bpf_dynptr_from_xdp(struct xdp_md *, __u64, struct bpf_dynptr *); diff --git a/src/libbpfilter/include/bpfilter/elfstub.h b/src/libbpfilter/include/bpfilter/elfstub.h index 3f40334c7..1b37a50da 100644 --- a/src/libbpfilter/include/bpfilter/elfstub.h +++ b/src/libbpfilter/include/bpfilter/elfstub.h @@ -170,6 +170,20 @@ enum bf_elfstub_id */ BF_ELFSTUB_SOCK_ADDR_LOG, + /** + * Check if `limit` packets have already been seen in the last unit of time + * + * `__u8 bf_ratelimit(void *map, __u16 limit, __u8 letter)` + * + * **Parameters** + * - `map`: address of the rate limit map. + * - `limit`: number of packets allowed to pass in one unit of time. + * - `letter`: the unit of time to reset the rate limit. + * + * **Return** 0 if in the allowed limit (inclusive), or 1 if over the limit. + */ + BF_ELFSTUB_LIMIT, + _BF_ELFSTUB_MAX, }; diff --git a/src/libbpfilter/include/bpfilter/matcher.h b/src/libbpfilter/include/bpfilter/matcher.h index 54786e9af..d5cd9608e 100644 --- a/src/libbpfilter/include/bpfilter/matcher.h +++ b/src/libbpfilter/include/bpfilter/matcher.h @@ -67,6 +67,8 @@ enum bf_matcher_type BF_MATCHER_META_FLOW_HASH, /// Matches packets based on flow probability (consistent per flow). BF_MATCHER_META_FLOW_PROBABILITY, + /// Matches a number of packets per unit of time + BF_MATCHER_META_LIMIT, /// 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 a3d920cc3..66d26b20a 100644 --- a/src/libbpfilter/matcher.c +++ b/src/libbpfilter/matcher.c @@ -23,9 +23,11 @@ #include #include #include +#include #include #include +#include "asm-generic/errno-base.h" #include "bpfilter/dump.h" #include "bpfilter/helper.h" #include "bpfilter/hook.h" @@ -47,7 +49,7 @@ extern const char *inet_ntop(int, const void *, char *, socklen_t); /** * Matcher definition. * - * Matchers are criterias to match the packet against. A set of matcher defines + * Matchers are criterias to match the packet against. A set of bf_matcher_limit defines * what a rule should match on. * * @todo `bf_matcher`'s payload should be a union of all the possible payload @@ -408,6 +410,40 @@ static void _bf_print_probability(const void *payload) (void)fprintf(stdout, "%g%%", proba); } +static int _bf_parse_limit(enum bf_matcher_type type, enum bf_matcher_op op, + void *payload, const char *raw_payload) +{ + assert(payload); + assert(raw_payload); + + uint16_t limit; + char *endptr; + + (void)op; + + limit = strtoul(raw_payload, &endptr, BF_BASE_10); + if (endptr[0] == '/' && endptr[1] == 's' && limit > 0 && + limit <= UINT16_MAX) { + // Maybe a struct is better + *(uint32_t *)payload = (limit + ((uint32_t)endptr[1] << 16)); + return 0; + } + + bf_err( + "\"%s\" expect a number and a time unit (the only time unit as of now is 's', e.g., 20/s), not '%s'", + bf_matcher_type_to_str(type), raw_payload); + + return -EINVAL; +} + +static void _bf_print_limit(const void *payload) +{ + assert(payload); + + (void)fprintf(stdout, "%d/%c" PRIu32, *(uint16_t *)payload, + *(uint32_t *)payload >> 16); +} + static int _bf_parse_mark(enum bf_matcher_type type, enum bf_matcher_op op, void *payload, const char *raw_payload) { @@ -924,6 +960,15 @@ static struct bf_matcher_meta _bf_matcher_metas[_BF_MATCHER_TYPE_MAX] = { _bf_print_probability), }, }, + [BF_MATCHER_META_LIMIT] = + { + .layer = BF_MATCHER_NO_LAYER, + .ops = + { + BF_MATCHER_OPS(BF_MATCHER_EQ, sizeof(uint32_t), + _bf_parse_limit, _bf_print_limit), + }, + }, [BF_MATCHER_IP4_SADDR] = { .layer = BF_MATCHER_LAYER_3, @@ -1473,6 +1518,7 @@ static const char *_bf_matcher_type_strs[] = { [BF_MATCHER_META_MARK] = "meta.mark", [BF_MATCHER_META_FLOW_HASH] = "meta.flow_hash", [BF_MATCHER_META_FLOW_PROBABILITY] = "meta.flow_probability", + [BF_MATCHER_META_LIMIT] = "meta.limit", [BF_MATCHER_IP4_SADDR] = "ip4.saddr", [BF_MATCHER_IP4_SNET] = "ip4.snet", [BF_MATCHER_IP4_DADDR] = "ip4.daddr",