From: Vsevolod Stakhov Date: Thu, 2 Apr 2026 09:30:43 +0000 (+0100) Subject: [Feature] Add dynamic block API to fuzzy storage worker X-Git-Tag: 4.0.1~5^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=98324cd57a5aba47f8abd77a42ff8e31d081111d;p=thirdparty%2Frspamd.git [Feature] Add dynamic block API to fuzzy storage worker Adds worker:block_fuzzy_client(addr, prefix_len[, expire_ts[, reason]]) Lua method that inserts a CIDR network block into a per-worker btrie (dynamic_blocked_nets). Checked in rspamd_fuzzy_check_client() after the static blocked_ips map. expire_ts is an absolute monotonic timestamp (0 = permanent). Callers compute get_time() once and pass it to a batch of block calls — C never calls get_time() itself. Re-inserting an existing prefix updates the expire_ts and reason in-place (idempotent). Each fuzzy worker independently applies bans via Redis polling; no new IPC commands or control socket extensions are needed. --- diff --git a/src/fuzzy_storage.c b/src/fuzzy_storage.c index 285bce8cae..fa2cbc3c89 100644 --- a/src/fuzzy_storage.c +++ b/src/fuzzy_storage.c @@ -38,6 +38,7 @@ #include "contrib/uthash/utlist.h" #include "lua/lua_common.h" #include "unix-std.h" +#include "libutil/radix.h" #include @@ -2700,6 +2701,44 @@ lua_fuzzy_add_blacklist_handler(lua_State *L) return 0; } +/* + * worker:block_fuzzy_client(addr_string, prefix_len[, expire_ts[, reason]]) + * + * Dynamically block an IP address or CIDR network in this worker. + * expire_ts is an absolute monotonic timestamp (rspamd_util.get_time() + ttl). + * Pass nil or 0 for a permanent block (until worker restart). + * Lua callers can call get_time() once and pass the result to a batch of + * block calls — the C layer never calls get_time() itself. + * + * Returns: true on success; false, errmsg on parse failure. + */ +static int +lua_fuzzy_block_client(lua_State *L) +{ + struct rspamd_worker **pwrk = (struct rspamd_worker **) + rspamd_lua_check_udata(L, 1, rspamd_worker_classname); + + if (!pwrk) { + return luaL_error(L, "invalid self: worker expected"); + } + + struct rspamd_fuzzy_storage_ctx *ctx = (struct rspamd_fuzzy_storage_ctx *) (*pwrk)->ctx; + + const char *addr_str = luaL_checkstring(L, 2); + unsigned int prefix_len = (unsigned int) luaL_checkinteger(L, 3); + double expire_ts = luaL_optnumber(L, 4, 0.0); + const char *reason = luaL_optstring(L, 5, "lua"); + + if (rspamd_fuzzy_block_addr(ctx, addr_str, prefix_len, expire_ts, reason)) { + lua_pushboolean(L, TRUE); + return 1; + } + + lua_pushboolean(L, FALSE); + lua_pushfstring(L, "failed to parse address: %s", addr_str); + return 2; +} + gpointer init_fuzzy(struct rspamd_config *cfg) { @@ -3251,6 +3290,7 @@ start_fuzzy(struct rspamd_worker *worker) ctx->ratelimit_buckets = rspamd_lru_hash_new_full(ctx->max_buckets, NULL, fuzzy_rl_bucket_free, rspamd_inet_address_hash, rspamd_inet_address_equal); + ctx->dynamic_blocked_nets = radix_create_compressed("dynamic_blocked_nets"); rspamd_fuzzy_maybe_load_ratelimits(ctx); @@ -3304,6 +3344,11 @@ start_fuzzy(struct rspamd_worker *worker) .func = lua_fuzzy_add_blacklist_handler, }; rspamd_lua_add_metamethod(ctx->cfg->lua_state, rspamd_worker_classname, &fuzzy_lua_reg); + fuzzy_lua_reg = (luaL_Reg) { + .name = "block_fuzzy_client", + .func = lua_fuzzy_block_client, + }; + rspamd_lua_add_metamethod(ctx->cfg->lua_state, rspamd_worker_classname, &fuzzy_lua_reg); rspamd_lua_run_postloads(ctx->cfg->lua_state, ctx->cfg, ctx->event_loop, worker); @@ -3352,6 +3397,13 @@ start_fuzzy(struct rspamd_worker *worker) rspamd_lru_hash_destroy(ctx->ratelimit_buckets); } + if (ctx->dynamic_blocked_nets) { + /* Ban structs are pool-allocated inside the radix tree's pool, + * so radix_destroy_compressed frees them all at once */ + radix_destroy_compressed(ctx->dynamic_blocked_nets); + ctx->dynamic_blocked_nets = NULL; + } + struct rspamd_lua_fuzzy_script *cur, *tmp; LL_FOREACH_SAFE(ctx->lua_pre_handlers, cur, tmp) diff --git a/src/libserver/fuzzy_storage_internal.h b/src/libserver/fuzzy_storage_internal.h index f0c1b274bf..3a8410183f 100644 --- a/src/libserver/fuzzy_storage_internal.h +++ b/src/libserver/fuzzy_storage_internal.h @@ -32,6 +32,7 @@ struct map_cb_data; struct rspamd_rcl_section; struct rspamd_rcl_struct_parser; struct rspamd_control_command; +typedef struct radix_tree_compressed radix_compressed_t; struct rspamd_main; struct rspamd_worker; @@ -80,6 +81,11 @@ struct rspamd_leaky_bucket_elt { double cur; }; +struct rspamd_fuzzy_dynamic_ban { + double expire_ts; /* monotonic clock; 0.0 = never expires */ + char reason[64]; +}; + static inline int64_t fuzzy_kp_hash(const unsigned char *p) { @@ -146,6 +152,7 @@ struct rspamd_fuzzy_storage_ctx { struct rspamd_radix_map_helper *update_ips; struct rspamd_hash_map_helper *update_keys; struct rspamd_radix_map_helper *blocked_ips; + radix_compressed_t *dynamic_blocked_nets; struct rspamd_radix_map_helper *ratelimit_whitelist; struct rspamd_radix_map_helper *delay_whitelist; @@ -283,6 +290,12 @@ gboolean fuzzy_parse_keypair(rspamd_mempool_t *pool, const ucl_object_t *obj, /* Ratelimit */ void fuzzy_rl_bucket_free(gpointer p); +gboolean rspamd_fuzzy_block_addr(struct rspamd_fuzzy_storage_ctx *ctx, + const char *addr_str, + unsigned int prefix_len, + double expire_ts, + const char *reason); + enum rspamd_ratelimit_check_result rspamd_fuzzy_check_ratelimit_bucket( struct rspamd_fuzzy_storage_ctx *ctx, rspamd_inet_addr_t *addr, diff --git a/src/libserver/fuzzy_storage_ratelimit.c b/src/libserver/fuzzy_storage_ratelimit.c index 1f7c1e8fa8..0f4f496a24 100644 --- a/src/libserver/fuzzy_storage_ratelimit.c +++ b/src/libserver/fuzzy_storage_ratelimit.c @@ -25,6 +25,8 @@ #include "rspamd_control.h" #include "lua/lua_common.h" #include "contrib/uthash/utlist.h" +#include "libutil/radix.h" +#include "libutil/addr.h" #include #include @@ -406,6 +408,108 @@ rspamd_fuzzy_check_client(struct rspamd_fuzzy_storage_ctx *ctx, } } + if (ctx->dynamic_blocked_nets != NULL) { + uintptr_t val = radix_find_compressed_addr(ctx->dynamic_blocked_nets, addr); + + if (val != RADIX_NO_VALUE) { + struct rspamd_fuzzy_dynamic_ban *ban = (struct rspamd_fuzzy_dynamic_ban *) val; + double now = rspamd_get_ticks(FALSE); + + if (ban->expire_ts == 0.0 || ban->expire_ts > now) { + rspamd_fuzzy_maybe_call_blacklisted(ctx, addr, "dynamic_blocked"); + return FALSE; + } + } + } + + return TRUE; +} + +gboolean +rspamd_fuzzy_block_addr(struct rspamd_fuzzy_storage_ctx *ctx, + const char *addr_str, + unsigned int prefix_len, + double expire_ts, + const char *reason) +{ + rspamd_inet_addr_t *addr; + + if (!rspamd_parse_inet_address(&addr, addr_str, strlen(addr_str), + RSPAMD_INET_ADDRESS_PARSE_NO_UNIX | RSPAMD_INET_ADDRESS_PARSE_NO_PORT)) { + msg_err("block_fuzzy_client: cannot parse address: %s", addr_str); + return FALSE; + } + + /* Apply network mask to zero out host bits */ + rspamd_inet_address_apply_mask(addr, prefix_len); + + /* Allocate ban struct from the tree's memory pool so it is freed automatically + * when the tree is destroyed at shutdown */ + struct rspamd_fuzzy_dynamic_ban *new_ban = + rspamd_mempool_alloc0(radix_get_pool(ctx->dynamic_blocked_nets), + sizeof(*new_ban)); + new_ban->expire_ts = expire_ts; + + if (reason != NULL) { + rspamd_strlcpy(new_ban->reason, reason, sizeof(new_ban->reason)); + } + + /* Build the 16-byte IPv6-mapped key and compute masklen */ + const unsigned char *raw; + unsigned int klen = 0; + unsigned char key[16]; + gsize masklen; + + raw = rspamd_inet_address_get_hash_key(addr, &klen); + + if (raw == NULL || klen == 0) { + rspamd_inet_address_free(addr); + return FALSE; + } + + if (klen == 4) { + /* IPv4: map to ::ffff:a.b.c.d */ + memset(key, 0, 10); + key[10] = 0xffu; + key[11] = 0xffu; + memcpy(key + 12, raw, 4); + if (prefix_len > 32) { + prefix_len = 32; + } + /* In the 128-bit IPv6-mapped form, the prefix occupies prefix_len+96 bits */ + masklen = 32 - prefix_len; /* = 128 - (prefix_len + 96) */ + } + else { + /* IPv6 */ + memcpy(key, raw, 16); + if (prefix_len > 128) { + prefix_len = 128; + } + masklen = 128 - prefix_len; + } + + uintptr_t old = radix_insert_compressed(ctx->dynamic_blocked_nets, + key, sizeof(key), masklen, + (uintptr_t) new_ban); + + if (old != RADIX_NO_VALUE) { + /* Duplicate prefix: btrie was NOT updated (it returned the existing value). + * Update the existing ban struct in-place so the same memory location now + * reflects the new TTL and reason. new_ban is pool-allocated so it leaks + * a tiny struct, but that is acceptable. */ + struct rspamd_fuzzy_dynamic_ban *existing = (struct rspamd_fuzzy_dynamic_ban *) old; + existing->expire_ts = expire_ts; + + if (reason != NULL) { + rspamd_strlcpy(existing->reason, reason, sizeof(existing->reason)); + } + } + + msg_info("dynamic block added for %s/%ud, expire_ts=%.0f, reason=%s", + addr_str, prefix_len, expire_ts, reason ? reason : ""); + + rspamd_inet_address_free(addr); + return TRUE; }