diff --git a/doc/usage/bfcli.rst b/doc/usage/bfcli.rst index 6b04acbe..e49019fb 100644 --- a/doc/usage/bfcli.rst +++ b/doc/usage/bfcli.rst @@ -425,7 +425,7 @@ Rules are defined such as: rule [$MATCHER...] [$SET...] - [log [$HEADERS]] + [log [$HEADERS] [every $FREQUENCY]] [counter] [mark $MARK] $VERDICT @@ -436,6 +436,8 @@ With: - ``log $HEADERS``: log specific packet headers. ``$HEADERS`` is a comma-separated list of ``link`` (layer 2), ``internet`` (layer 3), and/or ``transport`` (layer 4). Only supported by packet-based hooks (XDP, TC, NF, cgroup_skb). - ``log``: log all available data for the hook type. For packet-based hooks, this is equivalent to ``log link,internet,transport``. For ``BF_HOOK_CGROUP_SOCK_ADDR_*`` hooks, this records the process ID, process name, destination address, and destination port. Sendmsg hooks additionally include the source address. + + Either form accepts an optional ``every $FREQUENCY`` suffix to rate-limit log events. ``$FREQUENCY`` is a positive number (integer or decimal) followed by a unit: ``ns``, ``us``, ``ms``, or ``s`` (e.g. ``every 1s``, ``every 500ms``, ``every 1.5s``). At most one log entry is emitted per ``$FREQUENCY`` interval per rule. Without ``every``, every match is logged. - ``counter``: optional literal. If set, the filter will count the number of events matched by the rule. For packet-based hooks, this includes both the number of packets and the total bytes. For ``BF_HOOK_CGROUP_SOCK_ADDR_*`` hooks, this counts the number of socket operations (``connect()`` or ``sendmsg()`` calls). - ``mark``: optional, ``$MARK`` must be a valid decimal or hexadecimal 32-bits value. If set, write the packet's marker value. This marker can be used later on in a rule (see ``meta.mark``) or with a TC filter. - ``$VERDICT``: action taken by the rule if the packet is matched against **all** the criteria: either ``ACCEPT``, ``DROP``, ``CONTINUE``, ``NEXT``, or ``REDIRECT``. diff --git a/src/bfcli/lexer.l b/src/bfcli/lexer.l index 09044bc8..1ecb0597 100644 --- a/src/bfcli/lexer.l +++ b/src/bfcli/lexer.l @@ -22,6 +22,7 @@ %s STATE_HOOK_OPTS %s STATE_MARK_OPTS +%s STATE_LOG_EVERY %s STATE_REDIRECT_IFACE %s STATE_REDIRECT_DIR %s STATE_MATCHER_SET @@ -97,6 +98,14 @@ mark { BEGIN(STATE_MARK_OPTS); return MARK; } /* Logs */ log { return LOG; } +every { BEGIN(STATE_LOG_EVERY); return EVERY; } +{ + {float}[a-z]+ { + BEGIN(INITIAL); + yylval.sval = strdup(yytext); + return STRING; + } +} /* Sets */ \([ \t\n\r\f\v]*([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)([ \t\n\r\f\v]*,[ \t\n\r\f\v]*([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+))*[ \t\n\r\f\v]*\) { diff --git a/src/bfcli/parser.y b/src/bfcli/parser.y index 955607bb..a5188a6e 100644 --- a/src/bfcli/parser.y +++ b/src/bfcli/parser.y @@ -26,6 +26,7 @@ #include #include #include + #include #include #include @@ -59,16 +60,49 @@ BF_RULE_OPTION_LOG = 1 << 0, BF_RULE_OPTION_COUNTER = 1 << 1, BF_RULE_OPTION_MARK = 1 << 2, + BF_RULE_OPTION_LOG_RATE = 1 << 3, }; struct bf_rule_options { uint8_t flags; uint8_t log; + uint64_t log_rate_ns; bool counter; uint32_t mark; }; + static inline int _parse_log_rate(const char *s, uint64_t *ns) + { + double multiplier; + double v; + char *end; + + v = strtod(s, &end); + if (end == s || v <= 0.0) + return -1; + + if (strcmp(end, "ns") == 0) + multiplier = 1.0; + else if (strcmp(end, "us") == 0) + multiplier = 1000.0; + else if (strcmp(end, "ms") == 0) + multiplier = 1000000.0; + else if (strcmp(end, "s") == 0) + multiplier = 1000000000.0; + else + return -1; + + if (!isfinite(v) || v > (double)UINT64_MAX / multiplier) + return -1; + + *ns = (uint64_t)(v * multiplier + 0.5); + if (*ns == 0) + return -1; + + return 0; + } + struct bfc_rule_verdict { enum bf_verdict verdict; uint32_t redirect_ifindex; @@ -83,6 +117,7 @@ bool bval; uint8_t u8; uint32_t u32; + uint64_t u64; char *sval; enum bf_verdict verdict; enum bf_hook hook; @@ -101,7 +136,7 @@ %token CHAIN %token RULE %token SET -%token NEGATE LOG COUNTER MARK +%token NEGATE LOG COUNTER MARK EVERY %token REDIRECT_TOKEN %token SET_TYPE %token SET_RAW_PAYLOAD @@ -141,6 +176,7 @@ %destructor { bf_list_free(&$$); } rules %type log_headers +%type log_rate %type rule_option %type rule_options %type rule @@ -299,6 +335,7 @@ rule : RULE matchers rule_options rule_verdict bf_parse_err("failed to create a new bf_rule\n"); rule->log = $3.flags & BF_RULE_OPTION_LOG ? $3.log : 0; + rule->log_rate_ns = $3.flags & BF_RULE_OPTION_LOG_RATE ? $3.log_rate_ns : 0; rule->has_counters = $3.flags & BF_RULE_OPTION_COUNTER ? 1 : 0; if ($3.flags & BF_RULE_OPTION_MARK) @@ -336,6 +373,22 @@ rule_option : LOG .flags = BF_RULE_OPTION_LOG, }; } + | LOG EVERY log_rate + { + $$ = (struct bf_rule_options){ + .log = BF_LOG_OPT_DEFAULT, + .log_rate_ns = $3, + .flags = BF_RULE_OPTION_LOG | BF_RULE_OPTION_LOG_RATE, + }; + } + | LOG log_headers EVERY log_rate + { + $$ = (struct bf_rule_options){ + .log = $2, + .log_rate_ns = $4, + .flags = BF_RULE_OPTION_LOG | BF_RULE_OPTION_LOG_RATE, + }; + } | COUNTER { $$ = (struct bf_rule_options){ @@ -363,6 +416,19 @@ rule_option : LOG }; } +log_rate : STRING + { + _cleanup_free_ const char *rate_str = $1; + uint64_t ns; + + if (_parse_log_rate(rate_str, &ns) < 0) + bf_parse_err("invalid rate '%s': expected ns/us/ms/s", + rate_str); + + $$ = ns; + } + ; + rule_options : %empty { $$ = (struct bf_rule_options){}; } | rule_options rule_option { if ($2.flags & BF_RULE_OPTION_LOG) { @@ -372,6 +438,11 @@ rule_options : %empty { $$ = (struct bf_rule_options){}; } $1.log = $2.log; } + if ($2.flags & BF_RULE_OPTION_LOG_RATE) { + $1.flags |= BF_RULE_OPTION_LOG_RATE; + $1.log_rate_ns = $2.log_rate_ns; + } + if ($2.flags & BF_RULE_OPTION_COUNTER) { if ($1.flags & BF_RULE_OPTION_COUNTER) bf_parse_err("duplicate keyword \"counter\" in rule"); diff --git a/src/bfcli/print.c b/src/bfcli/print.c index fc7df08d..82799510 100644 --- a/src/bfcli/print.c +++ b/src/bfcli/print.c @@ -41,6 +41,7 @@ struct bfc_chain_opts; #define INET6_ADDRSTRLEN 46 #define BF_TIME_S 1000000000 +#define BF_TIME_MS 1000000 #define BF_TIME_US 1000 #define BF_DUMP_HEXDUMP_LEN 8 @@ -262,7 +263,7 @@ void bfc_chain_dump(struct bf_chain *chain, struct bf_hookopts *hookopts, if (rule->log) { if (rule->log == BF_LOG_OPT_DEFAULT) { - (void)fprintf(stdout, " log\n"); + (void)fprintf(stdout, " log"); } else { uint8_t log = rule->log; @@ -274,9 +275,29 @@ void bfc_chain_dump(struct bf_chain *chain, struct bf_hookopts *hookopts, log &= ~BF_FLAG(hdr); (void)fprintf(stdout, "%s%s", bf_log_opt_to_str(hdr), - log ? "," : "\n"); + log ? "," : ""); } } + + if (rule->log_rate_ns) { + uint64_t r = rule->log_rate_ns; + + if (r % BF_TIME_S == 0) { + (void)fprintf(stdout, " every %llus", + (unsigned long long)(r / BF_TIME_S)); + } else if (r % BF_TIME_MS == 0) { + (void)fprintf(stdout, " every %llums", + (unsigned long long)(r / BF_TIME_MS)); + } else if (r % BF_TIME_US == 0) { + (void)fprintf(stdout, " every %lluus", + (unsigned long long)(r / BF_TIME_US)); + } else { + (void)fprintf(stdout, " every %lluns", + (unsigned long long)r); + } + } + + (void)fprintf(stdout, "\n"); } if (bf_rule_mark_is_set(rule)) diff --git a/src/libbpfilter/cgen/fixup.h b/src/libbpfilter/cgen/fixup.h index 2cda6346..0a129cc7 100644 --- a/src/libbpfilter/cgen/fixup.h +++ b/src/libbpfilter/cgen/fixup.h @@ -37,6 +37,8 @@ enum bf_fixup_type BF_FIXUP_TYPE_PRINTER_MAP_FD, /// Set the log map file descriptor in the @c BPF_LD_MAP_FD instruction. BF_FIXUP_TYPE_LOG_MAP_FD, + /// Set the state map file descriptor in the @c BPF_LD_MAP_FD instruction. + 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, /// Call an ELF stub. diff --git a/src/libbpfilter/cgen/handle.c b/src/libbpfilter/cgen/handle.c index 673e7d03..6e834fa1 100644 --- a/src/libbpfilter/cgen/handle.c +++ b/src/libbpfilter/cgen/handle.c @@ -109,6 +109,15 @@ int bf_handle_new_from_pack(struct bf_handle **handle, struct bf_lock *lock, return bf_rpack_key_err(r, "bf_handle.lmap"); } + r = bf_rpack_kv_node(node, "smap", &child); + if (r) + return bf_rpack_key_err(r, "bf_handle.smap"); + if (!bf_rpack_is_nil(child)) { + r = bf_map_new_from_pack(&_handle->smap, dir_fd, child); + if (r) + return bf_rpack_key_err(r, "bf_handle.smap"); + } + r = bf_rpack_kv_array(node, "sets", &child); if (r) return bf_rpack_key_err(r, "bf_handle.sets"); @@ -146,6 +155,7 @@ void bf_handle_free(struct bf_handle **handle) bf_map_free(&(*handle)->cmap); bf_map_free(&(*handle)->pmap); bf_map_free(&(*handle)->lmap); + bf_map_free(&(*handle)->smap); bf_list_clean(&(*handle)->sets); free(*handle); @@ -191,6 +201,14 @@ int bf_handle_pack(const struct bf_handle *handle, bf_wpack_t *pack) bf_wpack_kv_nil(pack, "lmap"); } + if (handle->smap) { + bf_wpack_open_object(pack, "smap"); + bf_map_pack(handle->smap, pack); + bf_wpack_close_object(pack); + } else { + bf_wpack_kv_nil(pack, "smap"); + } + bf_wpack_kv_list(pack, "sets", &handle->sets); return bf_wpack_is_valid(pack) ? 0 : -EINVAL; @@ -244,6 +262,15 @@ void bf_handle_dump(const struct bf_handle *handle, prefix_t *prefix) DUMP(prefix, "lmap: struct bf_map * (NULL)"); } + if (handle->smap) { + DUMP(prefix, "smap: struct bf_map *"); + bf_dump_prefix_push(prefix); + bf_map_dump(handle->smap, bf_dump_prefix_last(prefix)); + bf_dump_prefix_pop(prefix); + } else { + DUMP(prefix, "smap: struct bf_map * (NULL)"); + } + DUMP(bf_dump_prefix_last(prefix), "sets: bf_list[%lu]", bf_list_size(&handle->sets)); bf_dump_prefix_push(prefix); @@ -305,6 +332,14 @@ int bf_handle_pin(struct bf_handle *handle, struct bf_lock *lock) } } + if (handle->smap) { + r = bf_map_pin(handle->smap, dir_fd); + if (r) { + bf_err_r(r, "failed to pin BPF state map"); + goto err_unpin_all; + } + } + bf_list_foreach (&handle->sets, set_node) { struct bf_map *map = bf_list_node_get_data(set_node); @@ -348,6 +383,8 @@ void bf_handle_unpin(struct bf_handle *handle, struct bf_lock *lock) bf_map_unpin(handle->pmap, dir_fd); if (handle->lmap) bf_map_unpin(handle->lmap, dir_fd); + if (handle->smap) + bf_map_unpin(handle->smap, dir_fd); bf_list_foreach (&handle->sets, set_node) { struct bf_map *map = bf_list_node_get_data(set_node); @@ -477,5 +514,6 @@ void bf_handle_unload(struct bf_handle *handle) bf_map_free(&handle->cmap); bf_map_free(&handle->pmap); bf_map_free(&handle->lmap); + bf_map_free(&handle->smap); bf_list_clean(&handle->sets); } diff --git a/src/libbpfilter/cgen/handle.h b/src/libbpfilter/cgen/handle.h index 4b2b6697..2dfa9962 100644 --- a/src/libbpfilter/cgen/handle.h +++ b/src/libbpfilter/cgen/handle.h @@ -52,6 +52,10 @@ struct bf_handle /** Log map. NULL if not created. */ struct bf_map *lmap; + /** Per-rule state map. Single-entry array; value holds one `bf_rule_state` + * per rule. NULL if the chain has no logging rules. */ + struct bf_map *smap; + /** List of set maps. Contains at most one map for each unique key * format. */ bf_list sets; diff --git a/src/libbpfilter/cgen/prog/map.c b/src/libbpfilter/cgen/prog/map.c index 230ceca0..f723ebe1 100644 --- a/src/libbpfilter/cgen/prog/map.c +++ b/src/libbpfilter/cgen/prog/map.c @@ -23,6 +23,8 @@ #include #include +#include "cgen/runtime.h" + #define _free_bf_btf_ __attribute__((__cleanup__(_bf_btf_free))) static void _bf_btf_free(struct bf_btf **btf); @@ -111,6 +113,13 @@ static struct bf_btf *_bf_map_make_btf(const struct bf_map *map) btf__add_field(kbtf, "count", 1, 0, 0); btf__add_field(kbtf, "size", 1, 64, 0); break; + case BF_MAP_TYPE_STATE: + btf__add_int(kbtf, "u64", 8, 0); + btf->key_type_id = btf__add_int(kbtf, "u32", 4, 0); + btf->value_type_id = btf__add_struct(kbtf, "bf_rule_state", + sizeof(struct bf_rule_state)); + btf__add_field(kbtf, "last_log_ts", 1, 0, 0); + break; case BF_MAP_TYPE_PRINTER: case BF_MAP_TYPE_SET: case BF_MAP_TYPE_LOG: @@ -199,6 +208,7 @@ int bf_map_new(struct bf_map **map, const char *name, enum bf_map_type type, * valid bf_bpf_map_type value. */ [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, }; assert(map); @@ -318,6 +328,7 @@ static const char *_bf_map_type_to_str(enum bf_map_type type) [BF_MAP_TYPE_LOG] = "BF_MAP_TYPE_LOG", [BF_MAP_TYPE_SET] = "BF_MAP_TYPE_SET", [BF_MAP_TYPE_CTX] = "BF_MAP_TYPE_CTX", + [BF_MAP_TYPE_STATE] = "BF_MAP_TYPE_STATE", }; 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 19f98e81..3a51f384 100644 --- a/src/libbpfilter/cgen/prog/map.h +++ b/src/libbpfilter/cgen/prog/map.h @@ -22,6 +22,11 @@ enum bf_map_type BF_MAP_TYPE_LOG, BF_MAP_TYPE_SET, BF_MAP_TYPE_CTX, + + /** Single-entry array map holding per-rule mutable state. The value is a + * flat array of `bf_rule_state` entries, indexed by rule position. */ + BF_MAP_TYPE_STATE, + _BF_MAP_TYPE_MAX, }; diff --git a/src/libbpfilter/cgen/program.c b/src/libbpfilter/cgen/program.c index b473e77c..85282af0 100644 --- a/src/libbpfilter/cgen/program.c +++ b/src/libbpfilter/cgen/program.c @@ -62,6 +62,7 @@ #define _BF_COUNTER_MAP_NAME "bf_cmap" #define _BF_PRINTER_MAP_NAME "bf_pmap" #define _BF_LOG_MAP_NAME "bf_lmap" +#define _BF_STATE_MAP_NAME "bf_smap" static inline size_t _bf_round_next_power_of_2(size_t value) { @@ -436,6 +437,10 @@ static int _bf_program_fixup(struct bf_program *program, insn_type = BF_FIXUP_INSN_IMM; value = program->handle->lmap->fd; break; + case BF_FIXUP_TYPE_STATE_MAP_FD: + insn_type = BF_FIXUP_INSN_IMM; + value = program->handle->smap->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); @@ -562,7 +567,54 @@ static int _bf_program_generate_rule(struct bf_program *program, } } - if (rule->log) { + if (rule->log && rule->log_rate_ns) { + // Rate-limited log: check last_log_ts in the state map before logging. + // + // R9 (callee-saved) holds the pointer to this rule's state entry + // across the bpf_ktime_get_ns() call. + EMIT(program, BPF_LDX_MEM(BPF_DW, BPF_REG_9, BPF_REG_10, + BF_PROG_CTX_OFF(state_map))); + { + // Outer skip: state_map is NULL (shouldn't happen at runtime, + // but the verifier requires the NULL check). + _clean_bf_jmpctx_ struct bf_jmpctx null_ctx = + bf_jmpctx_get(program, BPF_JMP_IMM(BPF_JEQ, BPF_REG_9, 0, 0)); + + if (rule->index > 0) { + EMIT(program, + BPF_ALU64_IMM( + BPF_ADD, BPF_REG_9, + (int)(rule->index * sizeof(struct bf_rule_state)))); + } + + EMIT(program, BPF_EMIT_CALL(BPF_FUNC_ktime_get_ns)); + + EMIT(program, BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_9, 0)); + EMIT(program, BPF_MOV64_REG(BPF_REG_2, BPF_REG_0)); + EMIT(program, BPF_ALU64_REG(BPF_SUB, BPF_REG_2, BPF_REG_1)); + + { + // Load log_rate_ns as a 64-bit immediate into R1. + const struct bpf_insn rate_insn[2] = { + BPF_LD_IMM64(BPF_REG_1, rule->log_rate_ns), + }; + EMIT(program, rate_insn[0]); + EMIT(program, rate_insn[1]); + } + + { + // Inner skip: delta < log_rate_ns means still within window. + _clean_bf_jmpctx_ struct bf_jmpctx rate_ctx = bf_jmpctx_get( + program, BPF_JMP_REG(BPF_JLT, BPF_REG_2, BPF_REG_1, 0)); + + EMIT(program, BPF_STX_MEM(BPF_DW, BPF_REG_9, BPF_REG_0, 0)); + + r = program->runtime.ops->gen_inline_log(program, rule); + if (r) + return r; + } + } + } else if (rule->log) { r = program->runtime.ops->gen_inline_log(program, rule); if (r) return r; @@ -795,6 +847,20 @@ int bf_program_generate(struct bf_program *program) if (r) return r; + // Populate ctx->state_map with the base pointer from the single-entry + // state map. The key (0) is written to scratch[0..3] temporarily. + // Placed after gen_inline_prologue so R1-R5 are free: the helper call + // does not need to be followed by a ctx restore. + if (program->runtime.chain->flags & BF_FLAG(BF_CHAIN_LOG_RATELIMIT)) { + EMIT(program, BPF_ST_MEM(BPF_W, BPF_REG_10, BF_PROG_SCR_OFF(0), 0)); + EMIT_LOAD_STATE_FD_FIXUP(program, BPF_REG_1); + EMIT(program, BPF_MOV64_REG(BPF_REG_2, BPF_REG_10)); + EMIT(program, BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, BF_PROG_SCR_OFF(0))); + EMIT(program, BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem)); + EMIT(program, BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_0, + BF_PROG_CTX_OFF(state_map))); + } + bf_list_foreach (&chain->rules, rule_node) { r = _bf_program_generate_rule(program, bf_list_node_get_data(rule_node)); @@ -903,6 +969,33 @@ static int _bf_program_load_log_map(struct bf_program *program) return 0; } +static int _bf_program_load_state_map(struct bf_program *program) +{ + size_t n_rules; + int r; + + assert(program); + + if (!(program->runtime.chain->flags & BF_FLAG(BF_CHAIN_LOG_RATELIMIT))) + return 0; + + n_rules = bf_list_size(&program->runtime.chain->rules); + if (n_rules == 0) + return 0; + + r = bf_map_new(&program->handle->smap, _BF_STATE_MAP_NAME, + BF_MAP_TYPE_STATE, sizeof(uint32_t), + n_rules * sizeof(struct bf_rule_state), 1); + if (r) + return bf_err_r(r, "failed to create the state bf_map object"); + + r = _bf_program_fixup(program, BF_FIXUP_TYPE_STATE_MAP_FD); + if (r) + return bf_err_r(r, "failed to fixup state 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()); @@ -1059,6 +1152,10 @@ int bf_program_load(struct bf_program *prog) if (r) return bf_err_r(r, "failed to load the log map"); + r = _bf_program_load_state_map(prog); + if (r) + return bf_err_r(r, "failed to load the state 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 0ead9816..4682f9b1 100644 --- a/src/libbpfilter/cgen/program.h +++ b/src/libbpfilter/cgen/program.h @@ -159,6 +159,18 @@ return __r; \ }) +#define EMIT_LOAD_STATE_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_STATE_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 e8155a03..ad3227ac 100644 --- a/src/libbpfilter/cgen/runtime.h +++ b/src/libbpfilter/cgen/runtime.h @@ -70,6 +70,20 @@ struct bf_runtime_sock_addr static_assert(sizeof(struct bf_runtime_sock_addr) <= 64, "bf_runtime_sock_addr must fit in the scratch area"); +/** + * @brief Per-rule mutable state stored in the program's state map. + * + * One entry per rule, accessible from BPF programs via a single-entry array + * map looked up in the program prologue. + */ +struct bf_rule_state +{ + /** Timestamp of the last log event for this rule, in nanoseconds. + * Zero-initialised; compared against `bpf_ktime_get_ns()` to enforce + * per-rule log rate limiting. */ + __u64 last_log_ts; +}; + /** * @brief Runtime stack layout for the generated BPF programs. * @@ -114,6 +128,12 @@ struct bf_runtime /** Ring buffer map containing the logged packets. */ void *log_map; + /** Base pointer into the single-entry state map value. + * NULL if the chain has no logging rules. Set in the program prologue + * via `bpf_map_lookup_elem`; each rule's state is at + * `state_map + rule_index * sizeof(struct bf_rule_state)`. */ + void *state_map; + /** Total size of the packet, or 0 for non-packet flavors. */ __u64 pkt_size; diff --git a/src/libbpfilter/chain.c b/src/libbpfilter/chain.c index ceae479a..4da4193c 100644 --- a/src/libbpfilter/chain.c +++ b/src/libbpfilter/chain.c @@ -181,6 +181,9 @@ static int _bf_chain_check_rule(struct bf_chain *chain, struct bf_rule *rule) if (rule->log && !rule->disabled) chain->flags |= BF_FLAG(BF_CHAIN_LOG); + if (rule->log && rule->log_rate_ns && !rule->disabled) + chain->flags |= BF_FLAG(BF_CHAIN_LOG_RATELIMIT); + if (bf_rule_mark_is_set(rule) && bf_hook_to_flavor(chain->hook) != BF_FLAVOR_TC && bf_hook_to_flavor(chain->hook) != BF_FLAVOR_CGROUP_SKB) { diff --git a/src/libbpfilter/include/bpfilter/chain.h b/src/libbpfilter/include/bpfilter/chain.h index 8039dbdf..9e92c625 100644 --- a/src/libbpfilter/include/bpfilter/chain.h +++ b/src/libbpfilter/include/bpfilter/chain.h @@ -39,6 +39,9 @@ enum bf_chain_flags /** A rule will log data to the ring buffer. */ BF_CHAIN_LOG, + /** A rule uses rate-limited logging (log ... every). */ + BF_CHAIN_LOG_RATELIMIT, + /** A rule will filter on IPv6 nexthdr field. */ BF_CHAIN_STORE_NEXTHDR, diff --git a/src/libbpfilter/include/bpfilter/rule.h b/src/libbpfilter/include/bpfilter/rule.h index b30ed836..684a5974 100644 --- a/src/libbpfilter/include/bpfilter/rule.h +++ b/src/libbpfilter/include/bpfilter/rule.h @@ -59,6 +59,10 @@ struct bf_rule bf_list matchers; uint8_t log; + /** Minimum interval between two log events for this rule, in nanoseconds. + * Zero means no rate limiting: every match is logged. */ + uint64_t log_rate_ns; + /** Mark to set to the packet's `sk_buff`. Only support for some hooks. * The leftmost 32 bits are set to 1 if a mark is defined, or 0 otherwise. * See `bf_rule_mark_is_set`. */ diff --git a/src/libbpfilter/rule.c b/src/libbpfilter/rule.c index 008cb2ee..f13b0578 100644 --- a/src/libbpfilter/rule.c +++ b/src/libbpfilter/rule.c @@ -86,6 +86,12 @@ int bf_rule_new_from_pack(struct bf_rule **rule, bf_rpack_node_t node) if (r) return bf_rpack_key_err(r, "bf_rule.log"); + if (bf_rpack_kv_contains(node, "log_rate_ns")) { + r = bf_rpack_kv_u64(node, "log_rate_ns", &_rule->log_rate_ns); + if (r) + return bf_rpack_key_err(r, "bf_rule.log_rate_ns"); + } + if (bf_rpack_kv_contains(node, "has_counters")) { r = bf_rpack_kv_bool(node, "has_counters", &has_counters); if (r) @@ -159,6 +165,7 @@ int bf_rule_pack(const struct bf_rule *rule, bf_wpack_t *pack) bf_wpack_kv_u32(pack, "index", rule->index); bf_wpack_kv_u8(pack, "log", rule->log); + bf_wpack_kv_u64(pack, "log_rate_ns", rule->log_rate_ns); bf_wpack_kv_bool(pack, "has_counters", rule->has_counters); bf_wpack_kv_u64(pack, "mark", rule->mark); bf_wpack_kv_int(pack, "verdict", rule->verdict); diff --git a/tests/benchmarks/bfbencher/__init__.py b/tests/benchmarks/bfbencher/__init__.py index 00f4a236..67795a6e 100755 --- a/tests/benchmarks/bfbencher/__init__.py +++ b/tests/benchmarks/bfbencher/__init__.py @@ -887,6 +887,13 @@ def make(self, target: str) -> bool: return self._executor.run_benchmark_cmd(f"Building {target}", self._commit, cmd) + def flush(self) -> bool: + cmd = ["sudo", "rm", "-rf", "/sys/fs/bpf/bpfilter"] + + return self._executor.run_benchmark_cmd( + "Flushing leftover chains", self._commit, cmd + ) + def run_benchmark( self, bind_node: int | None = None, @@ -1259,6 +1266,8 @@ def _benchmark_commits(executor: Executor, args: argparse.Namespace) -> None: continue if not ctx.make("benchmark_bin"): continue + if not ctx.flush(): + continue if not ctx.run_benchmark( args.bind_node, args.no_preempt, args.cpu_pin, args.slice ): diff --git a/tests/e2e/rules/log.sh b/tests/e2e/rules/log.sh index 505b9c71..10b78c31 100755 --- a/tests/e2e/rules/log.sh +++ b/tests/e2e/rules/log.sh @@ -19,6 +19,15 @@ DROP" ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_TC_INGRESS ACCEPT rule ip4.proto icmp log NEXT" ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_TC_INGRESS ACCEPT rule ip4.proto icmp log REDIRECT 1 in" ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_TC_INGRESS ACCEPT rule ip4.proto icmp log mark 0x1 DROP" +# log every : integer and float values, with and without headers +${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log every 1s DROP" +${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log every 500ms DROP" +${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log every 1.5s DROP" +${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log every 0.5ms DROP" +${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log link,internet every 1s DROP" +${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log internet every 100ms DROP" +(! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log every 0s DROP") +(! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log every 1year DROP") (! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log link,ip DROP") (! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log link,,internet DROP") (! ${BFCLI} ruleset set --dry-run --from-str "chain c1 BF_HOOK_XDP ACCEPT rule ip4.proto icmp log @DROP") diff --git a/tests/harness/fake.c b/tests/harness/fake.c index 9c897274..f7afc461 100644 --- a/tests/harness/fake.c +++ b/tests/harness/fake.c @@ -190,6 +190,7 @@ struct bf_rule *bft_rule_dummy(size_t n_matchers) rule->index = 0; rule->log = BF_FLAGS(BF_LOG_OPT_INTERNET, BF_LOG_OPT_TRANSPORT); + rule->log_rate_ns = 500000000ULL; // 500ms rule->mark = 0x17; rule->has_counters = 1; rule->verdict = BF_VERDICT_ACCEPT; diff --git a/tests/harness/test.c b/tests/harness/test.c index f4ebfe1c..fa576813 100644 --- a/tests/harness/test.c +++ b/tests/harness/test.c @@ -292,6 +292,7 @@ bool bft_chain_equal(const struct bf_chain *chain0, bool bft_rule_equal(const struct bf_rule *rule0, const struct bf_rule *rule1) { return rule0->index == rule1->index && rule0->log == rule1->log && + rule0->log_rate_ns == rule1->log_rate_ns && rule0->mark == rule1->mark && rule0->has_counters == rule1->has_counters && rule0->verdict == rule1->verdict &&