#include "contrib/uthash/utlist.h"
#include "lua/lua_common.h"
#include "unix-std.h"
+#include "libutil/radix.h"
#include <math.h>
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)
{
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);
.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);
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)
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;
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)
{
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;
/* 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,
#include "rspamd_control.h"
#include "lua/lua_common.h"
#include "contrib/uthash/utlist.h"
+#include "libutil/radix.h"
+#include "libutil/addr.h"
#include <errno.h>
#include <math.h>
}
}
+ 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;
}