diff options
author | Vsevolod Stakhov <vsevolod@rspamd.com> | 2024-09-17 19:17:14 +0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-17 19:17:14 +0600 |
commit | 3dda59641af8826d50dd07bc82d67c9ffecef403 (patch) | |
tree | 59c47ac8e260cdac03431445879149c6bb472495 /src | |
parent | f7cac80b96de3e6a49109602f9622d00c26b4ec9 (diff) | |
parent | 986814257147e9e79df032f5f157d90c0ee430e4 (diff) | |
download | rspamd-3dda59641af8826d50dd07bc82d67c9ffecef403.tar.gz rspamd-3dda59641af8826d50dd07bc82d67c9ffecef403.zip |
Merge branch 'master' into vstakhov-utf8-mime
Diffstat (limited to 'src')
-rw-r--r-- | src/fuzzy_storage.c | 328 | ||||
-rw-r--r-- | src/lua/lua_cryptobox.c | 20 | ||||
-rw-r--r-- | src/lua/lua_rsa.c | 18 | ||||
-rw-r--r-- | src/plugins/fuzzy_check.c | 110 | ||||
-rw-r--r-- | src/plugins/lua/aws_s3.lua | 2 | ||||
-rw-r--r-- | src/plugins/lua/bimi.lua | 4 | ||||
-rw-r--r-- | src/plugins/lua/history_redis.lua | 2 | ||||
-rw-r--r-- | src/plugins/lua/ratelimit.lua | 143 |
8 files changed, 380 insertions, 247 deletions
diff --git a/src/fuzzy_storage.c b/src/fuzzy_storage.c index 5fd3303dc..841d040b2 100644 --- a/src/fuzzy_storage.c +++ b/src/fuzzy_storage.c @@ -128,11 +128,17 @@ KHASH_SET_INIT_INT(fuzzy_key_ids_set); KHASH_INIT(fuzzy_key_flag_stat, int, struct fuzzy_key_stat, 1, kh_int_hash_func, kh_int_hash_equal); struct fuzzy_key { + char *name; struct rspamd_cryptobox_keypair *key; struct rspamd_cryptobox_pubkey *pk; struct fuzzy_key_stat *stat; khash_t(fuzzy_key_flag_stat) * flags_stat; khash_t(fuzzy_key_ids_set) * forbidden_ids; + struct rspamd_leaky_bucket_elt *rl_bucket; + double burst; + double rate; + ev_tstamp expire; + bool expired; ref_entry_t ref; }; @@ -258,7 +264,8 @@ static gboolean rspamd_fuzzy_check_client(struct rspamd_fuzzy_storage_ctx *ctx, static void rspamd_fuzzy_maybe_call_blacklisted(struct rspamd_fuzzy_storage_ctx *ctx, rspamd_inet_addr_t *addr, const char *reason); -static struct fuzzy_key *fuzzy_add_keypair_from_ucl(const ucl_object_t *obj, +static struct fuzzy_key *fuzzy_add_keypair_from_ucl(struct rspamd_config *cfg, + const ucl_object_t *obj, khash_t(rspamd_fuzzy_keys_hash) * target); struct fuzzy_keymap_ucl_buf { @@ -366,7 +373,7 @@ ucl_keymap_fin_cb(struct map_cb_data *data, void **target) while ((cur = ucl_object_iterate(top, &it, true)) != NULL) { struct fuzzy_key *nk; - nk = fuzzy_add_keypair_from_ucl(cur, jb->ctx->dynamic_keys); + nk = fuzzy_add_keypair_from_ucl(cfg, cur, jb->ctx->dynamic_keys); if (nk == NULL) { msg_warn_config("cannot add dynamic keypair"); @@ -404,6 +411,78 @@ ucl_keymap_dtor_cb(struct map_cb_data *data) } } +enum rspamd_ratelimit_check_result { + ratelimit_pass, + ratelimit_new, + ratelimit_existing, +}; + +enum rspamd_ratelimit_check_policy { + ratelimit_policy_permanent, + ratelimit_policy_normal, +}; + +static enum rspamd_ratelimit_check_result +rspamd_fuzzy_check_ratelimit_bucket(struct fuzzy_session *session, struct rspamd_leaky_bucket_elt *elt, + enum rspamd_ratelimit_check_policy policy, double max_burst, double max_rate) +{ + gboolean ratelimited = FALSE, new_ratelimit = FALSE; + + if (isnan(elt->cur)) { + /* There is an issue with the previous logic: the TTL is updated each time + * we see that new bucket. Hence, we need to check the `last` and act accordingly + */ + if (elt->last < session->timestamp && session->timestamp - elt->last >= session->ctx->leaky_bucket_ttl) { + /* + * We reset bucket to it's 90% capacity to allow some requests + * This should cope with the issue when we block an IP network for some burst and never unblock it + */ + elt->cur = max_burst * 0.9; + elt->last = session->timestamp; + } + else { + ratelimited = TRUE; + } + } + else { + /* Update bucket: leak some elements */ + if (elt->last < session->timestamp) { + elt->cur -= max_rate * (session->timestamp - elt->last); + elt->last = session->timestamp; + + if (elt->cur < 0) { + elt->cur = 0; + } + } + else { + elt->last = session->timestamp; + } + + /* Check the bucket */ + if (elt->cur >= max_burst) { + + if (policy == ratelimit_policy_permanent) { + elt->cur = NAN; + } + new_ratelimit = TRUE; + ratelimited = TRUE; + } + else { + elt->cur++; /* Allow one more request */ + } + } + + if (ratelimited) { + rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit"); + } + + if (new_ratelimit) { + return ratelimit_new; + } + + return ratelimited ? ratelimit_existing : ratelimit_pass; +} + static gboolean rspamd_fuzzy_check_ratelimit(struct fuzzy_session *session) { @@ -443,59 +522,17 @@ rspamd_fuzzy_check_ratelimit(struct fuzzy_session *session) (time_t) session->timestamp); if (elt) { - gboolean ratelimited = FALSE, new_ratelimit = FALSE; - - if (isnan(elt->cur)) { - /* There is an issue with the previous logic: the TTL is updated each time - * we see that new bucket. Hence, we need to check the `last` and act accordingly - */ - if (elt->last < session->timestamp && session->timestamp - elt->last >= session->ctx->leaky_bucket_ttl) { - /* - * We reset bucket to it's 90% capacity to allow some requests - * This should cope with the issue when we block an IP network for some burst and never unblock it - */ - elt->cur = session->ctx->leaky_bucket_burst * 0.9; - elt->last = session->timestamp; - } - else { - ratelimited = TRUE; - } - } - else { - /* Update bucket: leak some elements */ - if (elt->last < session->timestamp) { - elt->cur -= session->ctx->leaky_bucket_rate * (session->timestamp - elt->last); - elt->last = session->timestamp; - - if (elt->cur < 0) { - elt->cur = 0; - } - } - else { - elt->last = session->timestamp; - } - - /* Check the bucket */ - if (elt->cur >= session->ctx->leaky_bucket_burst) { - - msg_info("ratelimiting %s (%s), %.1f max elts", - rspamd_inet_address_to_string(session->addr), - rspamd_inet_address_to_string(masked), - session->ctx->leaky_bucket_burst); - elt->cur = NAN; - new_ratelimit = TRUE; - ratelimited = TRUE; - } - else { - elt->cur++; /* Allow one more request */ - } - } + enum rspamd_ratelimit_check_result res = rspamd_fuzzy_check_ratelimit_bucket(session, elt, + ratelimit_policy_permanent, + session->ctx->leaky_bucket_burst, + session->ctx->leaky_bucket_rate); - if (ratelimited) { - rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit"); - } + if (res == ratelimit_new) { + msg_info("ratelimiting %s (%s), %.1f max elts", + rspamd_inet_address_to_string(session->addr), + rspamd_inet_address_to_string(masked), + session->ctx->leaky_bucket_burst); - if (new_ratelimit) { struct rspamd_srv_command srv_cmd; srv_cmd.type = RSPAMD_SRV_FUZZY_BLOCKED; @@ -514,11 +551,16 @@ rspamd_fuzzy_check_ratelimit(struct fuzzy_session *session) msg_err("bad address length: %d, expected to be %d", (int) slen, (int) sizeof(srv_cmd.cmd.fuzzy_blocked.addr)); } } + + rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit"); + } + else if (res == ratelimit_existing) { + rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit"); } rspamd_inet_address_free(masked); - return !ratelimited; + return res == ratelimit_pass; } else { /* New bucket */ @@ -659,6 +701,15 @@ fuzzy_key_dtor(gpointer p) kh_destroy(fuzzy_key_ids_set, key->forbidden_ids); } + if (key->rl_bucket) { + /* TODO: save bucket stats */ + g_free(key->rl_bucket); + } + + if (key->name) { + g_free(key->name); + } + g_free(key); } } @@ -1464,7 +1515,14 @@ rspamd_fuzzy_process_command(struct fuzzy_session *session) if (session->ctx->encrypted_only && !encrypted) { /* Do not accept unencrypted commands */ - result.v1.value = 403; + result.v1.value = 415; + result.v1.prob = 0.0f; + rspamd_fuzzy_make_reply(cmd, &result, session, send_flags); + return; + } + + if (!rspamd_fuzzy_check_client(session->ctx, session->addr)) { + result.v1.value = 503; result.v1.prob = 0.0f; rspamd_fuzzy_make_reply(cmd, &result, session, send_flags); return; @@ -1487,23 +1545,95 @@ rspamd_fuzzy_process_command(struct fuzzy_session *session) } if (cmd->cmd == FUZZY_CHECK) { - bool can_continue = true; + bool is_rate_allowed = true; if (session->ctx->ratelimit_buckets) { if (session->ctx->ratelimit_log_only) { (void) rspamd_fuzzy_check_ratelimit(session); /* Check but ignore */ } else { - can_continue = rspamd_fuzzy_check_ratelimit(session); + is_rate_allowed = rspamd_fuzzy_check_ratelimit(session); + } + } + + if (session->key && session->key->rl_bucket) { + /* Check per-key bucket */ + + enum rspamd_ratelimit_check_result res = rspamd_fuzzy_check_ratelimit_bucket(session, session->key->rl_bucket, + ratelimit_policy_normal, + session->key->burst, + session->key->rate); + + if (res == ratelimit_new) { + msg_info("ratelimiting key %s %.1f max elts", + session->key->name ? session->key->name : "unknown", + session->key->burst); + + struct rspamd_srv_command srv_cmd; + + srv_cmd.type = RSPAMD_SRV_FUZZY_BLOCKED; + srv_cmd.cmd.fuzzy_blocked.af = rspamd_inet_address_get_af(session->addr); + + if (srv_cmd.cmd.fuzzy_blocked.af == AF_INET || srv_cmd.cmd.fuzzy_blocked.af == AF_INET6) { + socklen_t slen; + struct sockaddr *sa = rspamd_inet_address_get_sa(session->addr, &slen); + + if (slen <= sizeof(srv_cmd.cmd.fuzzy_blocked.addr)) { + memcpy(&srv_cmd.cmd.fuzzy_blocked.addr, sa, slen); + msg_debug("propagating blocked address to other workers"); + rspamd_srv_send_command(session->worker, + session->ctx->event_loop, + &srv_cmd, -1, NULL, NULL); + } + else { + msg_err("bad address length: %d, expected to be %d", + (int) slen, (int) sizeof(srv_cmd.cmd.fuzzy_blocked.addr)); + } + } + + rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit"); + is_rate_allowed = session->ctx->ratelimit_log_only ? true : false; + } + else if (res == ratelimit_existing) { + rspamd_fuzzy_maybe_call_blacklisted(session->ctx, session->addr, "ratelimit"); + is_rate_allowed = session->ctx->ratelimit_log_only ? true : false; + } + } + + if (session->key && !isnan(session->key->expire)) { + /* Check expire */ + static ev_tstamp today = NAN; + + /* + * Update `today` sometimes + */ + if (isnan(today)) { + today = ev_time(); + } + else if (rspamd_random_uint64_fast() > 0xFFFF000000000000ULL) { + today = ev_time(); + } + + if (today > session->key->expire) { + if (!session->key->expired) { + msg_info("key %s is expired", session->key->name); + session->key->expired = true; + } + + result.v1.value = 503; + result.v1.prob = 0.0f; + rspamd_fuzzy_make_reply(cmd, &result, session, send_flags); + return; } } - if (can_continue) { + if (is_rate_allowed) { REF_RETAIN(session); rspamd_fuzzy_backend_check(session->ctx->backend, cmd, rspamd_fuzzy_check_callback, session); } else { + /* Should be 429 but we keep compatibility */ result.v1.value = 403; result.v1.prob = 0.0f; result.v1.flag = 0; @@ -1574,7 +1704,7 @@ rspamd_fuzzy_process_command(struct fuzzy_session *session) result.v1.prob = 1.0f; } else { - result.v1.value = 403; + result.v1.value = 503; result.v1.prob = 0.0f; } reply: @@ -2041,11 +2171,6 @@ accept_fuzzy_socket(EV_P_ ev_io *w, int revents) if (MSG_FIELD(msg[i], msg_namelen) >= sizeof(struct sockaddr)) { client_addr = rspamd_inet_address_from_sa(MSG_FIELD(msg[i], msg_name), MSG_FIELD(msg[i], msg_namelen)); - if (!rspamd_fuzzy_check_client(worker->ctx, client_addr)) { - /* Disallow forbidden clients silently */ - rspamd_inet_address_free(client_addr); - continue; - } } else { client_addr = NULL; @@ -2761,7 +2886,8 @@ fuzzy_parse_ids(rspamd_mempool_t *pool, } static struct fuzzy_key * -fuzzy_add_keypair_from_ucl(const ucl_object_t *obj, khash_t(rspamd_fuzzy_keys_hash) * target) +fuzzy_add_keypair_from_ucl(struct rspamd_config *cfg, const ucl_object_t *obj, + khash_t(rspamd_fuzzy_keys_hash) * target) { struct rspamd_cryptobox_keypair *kp = rspamd_keypair_from_ucl(obj); @@ -2785,6 +2911,10 @@ fuzzy_add_keypair_from_ucl(const ucl_object_t *obj, khash_t(rspamd_fuzzy_keys_ha rspamd_inet_address_hash, rspamd_inet_address_equal); key->stat = keystat; key->flags_stat = kh_init(fuzzy_key_flag_stat); + key->burst = NAN; + key->rate = NAN; + key->expire = NAN; + key->rl_bucket = NULL; /* Preallocate some space for flags */ kh_resize(fuzzy_key_flag_stat, key->flags_stat, 8); const unsigned char *pk = rspamd_keypair_component(kp, RSPAMD_KEYPAIR_COMPONENT_PK, @@ -2816,6 +2946,7 @@ fuzzy_add_keypair_from_ucl(const ucl_object_t *obj, khash_t(rspamd_fuzzy_keys_ha const ucl_object_t *extensions = rspamd_keypair_get_extensions(kp); if (extensions) { + lua_State *L = RSPAMD_LUA_CFG_STATE(cfg); const ucl_object_t *forbidden_ids = ucl_object_lookup(extensions, "forbidden_ids"); if (forbidden_ids && ucl_object_type(forbidden_ids) == UCL_ARRAY) { @@ -2832,9 +2963,72 @@ fuzzy_add_keypair_from_ucl(const ucl_object_t *obj, khash_t(rspamd_fuzzy_keys_ha } } } + + const ucl_object_t *ratelimit = ucl_object_lookup(extensions, "ratelimit"); + + static int ratelimit_lua_id = -1; + + if (ratelimit_lua_id == -1) { + /* Load ratelimit parsing function */ + if (!rspamd_lua_require_function(L, "plugins/ratelimit", "parse_limit")) { + msg_err_config("cannot load ratelimit parser from ratelimit plugin"); + } + else { + ratelimit_lua_id = luaL_ref(L, LUA_REGISTRYINDEX); + } + } + + if (ratelimit && ratelimit_lua_id != -1) { + lua_rawgeti(L, LUA_REGISTRYINDEX, ratelimit_lua_id); + lua_pushstring(L, "fuzzy_key_ratelimit"); + ucl_object_push_lua(L, ratelimit, false); + + if (lua_pcall(L, 2, 1, 0) != 0) { + msg_err_config("cannot call ratelimit parser from ratelimit plugin"); + } + else { + if (lua_type(L, -1) == LUA_TTABLE) { + /* + * The returned table is in form { rate = xx, burst = yy } + */ + lua_getfield(L, -1, "rate"); + key->rate = lua_tonumber(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "burst"); + key->burst = lua_tonumber(L, -1); + lua_pop(L, 1); + + key->rl_bucket = g_malloc0(sizeof(*key->rl_bucket)); + } + } + + lua_settop(L, 0); + } + + const ucl_object_t *expire = ucl_object_lookup(extensions, "expire"); + if (expire && ucl_object_type(expire) == UCL_STRING) { + struct tm tm; + + /* DD-MM-YYYY */ + char *end = strptime(ucl_object_tostring(expire), "%d-%m-%Y", &tm); + + if (end != NULL && *end != '\0') { + msg_err_config("cannot parse expire date: %s", ucl_object_tostring(expire)); + } + else { + key->expire = mktime(&tm); + } + } + + const ucl_object_t *name = ucl_object_lookup(extensions, "name"); + if (name && ucl_object_type(name) == UCL_STRING) { + key->name = g_strdup(ucl_object_tostring(name)); + } } - msg_debug("loaded keypair %*bs", crypto_box_publickeybytes(), pk); + msg_debug("loaded keypair %*bs; expire=%f; rate=%f; burst=%f; name=%s", (int) crypto_box_publickeybytes(), pk, + key->expire, key->rate, key->burst, key->name); return key; } @@ -2867,7 +3061,7 @@ fuzzy_parse_keypair(rspamd_mempool_t *pool, return ret; } - key = fuzzy_add_keypair_from_ucl(obj, ctx->keys); + key = fuzzy_add_keypair_from_ucl(ctx->cfg, obj, ctx->keys); if (key == NULL) { return FALSE; diff --git a/src/lua/lua_cryptobox.c b/src/lua/lua_cryptobox.c index fbd44cecd..c9cac1562 100644 --- a/src/lua/lua_cryptobox.c +++ b/src/lua/lua_cryptobox.c @@ -998,25 +998,13 @@ rspamd_lua_ssl_hmac_create(struct rspamd_lua_cryptobox_hash *h, const EVP_MD *ht bool insecure) { h->type = LUA_CRYPTOBOX_HASH_HMAC; - OSSL_PROVIDER *dflt = OSSL_PROVIDER_load(NULL, "default"); - -#if OPENSSL_VERSION_NUMBER > 0x10100000L - if (insecure) { - /* Should never ever be used for crypto/security purposes! */ -#ifdef EVP_MD_CTX_FLAG_NON_FIPS_ALLOW -#if OPENSSL_VERSION_MAJOR >= 3 - OSSL_PROVIDER *fips = OSSL_PROVIDER_load(NULL, "fips"); -#endif - } -#endif -#endif #if OPENSSL_VERSION_NUMBER < 0x10100000L || \ (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x30500000) h->content.hmac_c = g_malloc0(sizeof(*h->content.hmac_c)); #else #if OPENSSL_VERSION_MAJOR >= 3 - EVP_MAC* mac = EVP_MAC_fetch(NULL, "HMAC", NULL); + EVP_MAC *mac = EVP_MAC_fetch(NULL, "HMAC", NULL); h->content.hmac_c = EVP_MAC_CTX_new(mac); EVP_MAC_free(mac); #else @@ -1038,7 +1026,7 @@ rspamd_lua_ssl_hmac_create(struct rspamd_lua_cryptobox_hash *h, const EVP_MD *ht h->out_len = EVP_MD_size(htype); #if OPENSSL_VERSION_MAJOR >= 3 OSSL_PARAM params[2]; - params[0] = OSSL_PARAM_construct_utf8_string("digest", EVP_MD_get0_name(htype), 0); + params[0] = OSSL_PARAM_construct_utf8_string("digest", (char *) EVP_MD_get0_name(htype), 0); params[1] = OSSL_PARAM_construct_end(); EVP_MAC_init(h->content.hmac_c, key, keylen, params); @@ -1500,7 +1488,7 @@ lua_cryptobox_hash_finish(struct rspamd_lua_cryptobox_hash *h) g_assert(ssl_outlen <= sizeof(h->out)); memcpy(h->out, out, ssl_outlen); break; - case LUA_CRYPTOBOX_HASH_HMAC: + case LUA_CRYPTOBOX_HASH_HMAC: { #if OPENSSL_VERSION_MAJOR >= 3 size_t ssl_outlen_size_t = ssl_outlen; EVP_MAC_final(h->content.hmac_c, out, &ssl_outlen_size_t, sizeof(out)); @@ -1512,6 +1500,7 @@ lua_cryptobox_hash_finish(struct rspamd_lua_cryptobox_hash *h) g_assert(ssl_outlen <= sizeof(h->out)); memcpy(h->out, out, ssl_outlen); break; + } case LUA_CRYPTOBOX_HASH_XXHASH64: case LUA_CRYPTOBOX_HASH_XXHASH32: case LUA_CRYPTOBOX_HASH_XXHASH3: @@ -2520,7 +2509,6 @@ lua_cryptobox_gen_dkim_keypair(lua_State *L) if (strcmp(alg_str, "rsa") == 0) { BIGNUM *e; - RSA *r; EVP_PKEY *pk; e = BN_new(); diff --git a/src/lua/lua_rsa.c b/src/lua/lua_rsa.c index 0c56b223b..b7be612b0 100644 --- a/src/lua/lua_rsa.c +++ b/src/lua/lua_rsa.c @@ -1,11 +1,11 @@ -/*- - * Copyright 2016 Vsevolod Stakhov +/* + * Copyright 2024 Vsevolod Stakhov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -184,7 +184,14 @@ lua_rsa_privkey_save(lua_State *L) else { if (f != stdout) { /* Set secure permissions for the private key file */ - chmod(filename, S_IRUSR | S_IWUSR); + if (fchmod(fileno(f), S_IRUSR | S_IWUSR) == -1) { + msg_err("cannot set permissions for private key file: %s, %s", + filename, + strerror(errno)); + fclose(f); + lua_pushboolean(L, FALSE); + return 1; + } } if (strcmp(type, "der") == 0) { @@ -463,7 +470,6 @@ lua_rsa_privkey_load_base64(lua_State *L) rspamd_lua_setclass(L, rspamd_rsa_privkey_classname, -1); *ppkey = pkey; } - } else { msg_err("cannot open EVP private key from data, %s", @@ -706,7 +712,7 @@ lua_rsa_verify_memory(lua_State *L) if (pkey != NULL && signature != NULL && data != NULL) { EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new(pkey, NULL); - g_assert(pctx != NULL); + g_assert(pctx != NULL); g_assert(EVP_PKEY_verify_init(pctx) == 1); ret = EVP_PKEY_verify(pctx, signature->str, signature->len, data, sz); diff --git a/src/plugins/fuzzy_check.c b/src/plugins/fuzzy_check.c index 91b77c702..ece9a91e0 100644 --- a/src/plugins/fuzzy_check.c +++ b/src/plugins/fuzzy_check.c @@ -49,6 +49,9 @@ #include "libutil/libev_helper.h" #define DEFAULT_SYMBOL "R_FUZZY_HASH" +#define RSPAMD_FUZZY_SYMBOL_FORBIDDEN "FUZZY_FORBIDDEN" +#define RSPAMD_FUZZY_SYMBOL_RATELIMITED "FUZZY_RATELIMITED" +#define RSPAMD_FUZZY_SYMBOL_ENCRYPTION_REQUIRED "FUZZY_ENCRYPTION_REQUIRED" #define DEFAULT_IO_TIMEOUT 1.0 #define DEFAULT_RETRANSMITS 3 @@ -68,6 +71,12 @@ struct fuzzy_mapping { double weight; }; +enum fuzzy_rule_mode { + fuzzy_rule_read_only, + fuzzy_rule_write_only, + fuzzy_rule_read_write +}; + struct fuzzy_rule { struct upstream_list *servers; const char *symbol; @@ -84,7 +93,7 @@ struct fuzzy_rule { struct rspamd_cryptobox_pubkey *peer_key; double max_score; double weight_threshold; - gboolean read_only; + enum fuzzy_rule_mode mode; gboolean skip_unknown; gboolean no_share; gboolean no_subject; @@ -328,7 +337,7 @@ fuzzy_rule_new(const char *default_symbol, rspamd_mempool_t *pool) rspamd_mempool_add_destructor(pool, (rspamd_mempool_destruct_t) g_hash_table_unref, rule->mappings); - rule->read_only = FALSE; + rule->mode = fuzzy_rule_read_write; rule->weight_threshold = NAN; return rule; @@ -458,7 +467,26 @@ fuzzy_parse_rule(struct rspamd_config *cfg, const ucl_object_t *obj, if ((value = ucl_object_lookup(obj, "read_only")) != NULL) { - rule->read_only = ucl_obj_toboolean(value); + rule->mode = ucl_obj_toboolean(value) ? fuzzy_rule_read_only : fuzzy_rule_read_write; + } + + if ((value = ucl_object_lookup(obj, "mode")) != NULL) { + const char *mode_str = ucl_object_tostring(value); + + if (g_ascii_strcasecmp(mode_str, "read_only") == 0) { + rule->mode = fuzzy_rule_read_only; + } + else if (g_ascii_strcasecmp(mode_str, "write_only") == 0) { + rule->mode = fuzzy_rule_write_only; + } + else if (g_ascii_strcasecmp(mode_str, "read_write") == 0) { + rule->mode = fuzzy_rule_read_write; + } + else { + msg_warn_config("unknown mode: %s, use read_write by default", + mode_str); + rule->mode = fuzzy_rule_read_write; + } } if ((value = ucl_object_lookup(obj, "skip_unknown")) != NULL) { @@ -1153,6 +1181,44 @@ int fuzzy_check_module_config(struct rspamd_config *cfg, bool validate) 1, 1); + /* Register meta symbols (blocked, ratelimited, etc) */ + rspamd_symcache_add_symbol(cfg->cache, + RSPAMD_FUZZY_SYMBOL_FORBIDDEN, 0, NULL, NULL, + SYMBOL_TYPE_VIRTUAL, + cb_id); + rspamd_config_add_symbol(cfg, + RSPAMD_FUZZY_SYMBOL_FORBIDDEN, + 0.0, + "Fuzzy access denied", + "fuzzy", + 0, + 1, + 1); + rspamd_symcache_add_symbol(cfg->cache, + RSPAMD_FUZZY_SYMBOL_RATELIMITED, 0, NULL, NULL, + SYMBOL_TYPE_VIRTUAL, + cb_id); + rspamd_config_add_symbol(cfg, + RSPAMD_FUZZY_SYMBOL_RATELIMITED, + 0.0, + "Fuzzy rate limit is reached", + "fuzzy", + 0, + 1, + 1); + rspamd_symcache_add_symbol(cfg->cache, + RSPAMD_FUZZY_SYMBOL_ENCRYPTION_REQUIRED, 0, NULL, NULL, + SYMBOL_TYPE_VIRTUAL, + cb_id); + rspamd_config_add_symbol(cfg, + RSPAMD_FUZZY_SYMBOL_ENCRYPTION_REQUIRED, + 0.0, + "Fuzzy encryption is required by a server", + "fuzzy", + 0, + 1, + 1); + /* * Here we can have 2 possibilities: * @@ -2486,7 +2552,16 @@ fuzzy_check_try_read(struct fuzzy_client_session *session) } } else if (rep->v1.value == 403) { - rspamd_task_insert_result(task, "FUZZY_BLOCKED", 0.0, + /* In fact, it should be 429, but we preserve compatibility */ + rspamd_task_insert_result(task, RSPAMD_FUZZY_SYMBOL_RATELIMITED, 1.0, + session->rule->name); + } + else if (rep->v1.value == 503) { + rspamd_task_insert_result(task, RSPAMD_FUZZY_SYMBOL_FORBIDDEN, 1.0, + session->rule->name); + } + else if (rep->v1.value == 415) { + rspamd_task_insert_result(task, RSPAMD_FUZZY_SYMBOL_ENCRYPTION_REQUIRED, 1.0, session->rule->name); } else if (rep->v1.value == 401) { @@ -3400,11 +3475,14 @@ fuzzy_symbol_callback(struct rspamd_task *task, PTR_ARRAY_FOREACH(fuzzy_module_ctx->fuzzy_rules, i, rule) { - commands = fuzzy_generate_commands(task, rule, FUZZY_CHECK, 0, 0, 0); + if (rule->mode != fuzzy_rule_write_only) { + commands = fuzzy_generate_commands(task, rule, FUZZY_CHECK, 0, 0, 0); - if (commands != NULL) { - register_fuzzy_client_call(task, rule, commands); + if (commands != NULL) { + register_fuzzy_client_call(task, rule, commands); + } } + /* Skip write only rules from checks */ } rspamd_symcache_item_async_dec_check(task, item, M); @@ -3491,9 +3569,9 @@ register_fuzzy_controller_call(struct rspamd_http_connection_entry *entry, } static void -fuzzy_process_handler(struct rspamd_http_connection_entry *conn_ent, - struct rspamd_http_message *msg, int cmd, int value, int flag, - struct fuzzy_ctx *ctx, gboolean is_hash, unsigned int flags) +fuzzy_modify_handler(struct rspamd_http_connection_entry *conn_ent, + struct rspamd_http_message *msg, int cmd, int value, int flag, + struct fuzzy_ctx *ctx, gboolean is_hash, unsigned int flags) { struct fuzzy_rule *rule; struct rspamd_controller_session *session = conn_ent->ud; @@ -3541,7 +3619,7 @@ fuzzy_process_handler(struct rspamd_http_connection_entry *conn_ent, PTR_ARRAY_FOREACH(fuzzy_module_ctx->fuzzy_rules, i, rule) { - if (rule->read_only) { + if (rule->mode == fuzzy_rule_read_only) { continue; } @@ -3796,8 +3874,8 @@ fuzzy_controller_handler(struct rspamd_http_connection_entry *conn_ent, send_flags |= FUZZY_CHECK_FLAG_NOTEXT; } - fuzzy_process_handler(conn_ent, msg, cmd, value, flag, - (struct fuzzy_ctx *) ctx, is_hash, send_flags); + fuzzy_modify_handler(conn_ent, msg, cmd, value, flag, + (struct fuzzy_ctx *) ctx, is_hash, send_flags); return 0; } @@ -3879,7 +3957,7 @@ fuzzy_check_lua_process_learn(struct rspamd_task *task, if (!res) { break; } - if (rule->read_only) { + if (rule->mode == fuzzy_rule_read_only) { continue; } @@ -4181,7 +4259,7 @@ fuzzy_lua_gen_hashes_handler(lua_State *L) PTR_ARRAY_FOREACH(fuzzy_module_ctx->fuzzy_rules, i, rule) { - if (rule->read_only) { + if (rule->mode == fuzzy_rule_read_only) { continue; } @@ -4409,7 +4487,7 @@ fuzzy_lua_list_storages(lua_State *L) { lua_newtable(L); - lua_pushboolean(L, rule->read_only); + lua_pushboolean(L, rule->mode == fuzzy_rule_read_only); lua_setfield(L, -2, "read_only"); /* Push servers */ diff --git a/src/plugins/lua/aws_s3.lua b/src/plugins/lua/aws_s3.lua index 30e88d2cd..ac344d86c 100644 --- a/src/plugins/lua/aws_s3.lua +++ b/src/plugins/lua/aws_s3.lua @@ -238,7 +238,7 @@ settings = lua_util.override_defaults(settings, opts) local res, err = settings_schema:transform(settings) if not res then - rspamd_logger.warnx(rspamd_config, 'plugin is misconfigured: %s', err) + rspamd_logger.warnx(rspamd_config, 'plugin %s is misconfigured: %s', N, err) lua_util.disable_module(N, "config") return end diff --git a/src/plugins/lua/bimi.lua b/src/plugins/lua/bimi.lua index 278359069..78949a5c0 100644 --- a/src/plugins/lua/bimi.lua +++ b/src/plugins/lua/bimi.lua @@ -265,7 +265,7 @@ local function check_bimi_vmc(task, domain, record) end if redis_params.username then if redis_params.password then - password = string.format( '%s:%s@', redis_params.username, redis_params.password) + password = string.format('%s:%s@', redis_params.username, redis_params.password) else rspamd_logger.warnx(task, "Redis requires a password when username is supplied") end @@ -358,7 +358,7 @@ settings = lua_util.override_defaults(settings, opts) local res, err = settings_schema:transform(settings) if not res then - rspamd_logger.warnx(rspamd_config, 'plugin is misconfigured: %s', err) + rspamd_logger.warnx(rspamd_config, 'plugin %s is misconfigured: %s', N, err) local err_msg = string.format("schema error: %s", res) lua_util.config_utils.push_config_error(N, err_msg) lua_util.disable_module(N, "failed", err_msg) diff --git a/src/plugins/lua/history_redis.lua b/src/plugins/lua/history_redis.lua index fff9f46b3..a3fdb0ec4 100644 --- a/src/plugins/lua/history_redis.lua +++ b/src/plugins/lua/history_redis.lua @@ -281,7 +281,7 @@ if opts then local res, err = settings_schema:transform(settings) if not res then - rspamd_logger.warnx(rspamd_config, '%s: plugin is misconfigured: %s', N, err) + rspamd_logger.warnx(rspamd_config, 'plugin %s is misconfigured: %s', N, err) lua_util.disable_module(N, "config") return end diff --git a/src/plugins/lua/ratelimit.lua b/src/plugins/lua/ratelimit.lua index f3331e850..168d8d63a 100644 --- a/src/plugins/lua/ratelimit.lua +++ b/src/plugins/lua/ratelimit.lua @@ -29,8 +29,7 @@ local lua_util = require "lua_util" local lua_verdict = require "lua_verdict" local rspamd_hash = require "rspamd_cryptobox_hash" local lua_selectors = require "lua_selectors" -local ts = require("tableshape").types - +local ratelimit_common = require "plugins/ratelimit" -- A plugin that implements ratelimits using redis local E = {} @@ -76,138 +75,6 @@ local function load_scripts(_, _) bucket_cleanup_id = lua_redis.load_redis_script_from_file(bucket_cleanup_script, redis_params) end -local limit_parser -local function parse_string_limit(lim, no_error) - local function parse_time_suffix(s) - if s == 's' then - return 1 - elseif s == 'm' then - return 60 - elseif s == 'h' then - return 3600 - elseif s == 'd' then - return 86400 - end - end - local function parse_num_suffix(s) - if s == '' then - return 1 - elseif s == 'k' then - return 1000 - elseif s == 'm' then - return 1000000 - elseif s == 'g' then - return 1000000000 - end - end - local lpeg = require "lpeg" - - if not limit_parser then - local digit = lpeg.R("09") - limit_parser = {} - limit_parser.integer = (lpeg.S("+-") ^ -1) * - (digit ^ 1) - limit_parser.fractional = (lpeg.P(".")) * - (digit ^ 1) - limit_parser.number = (limit_parser.integer * - (limit_parser.fractional ^ -1)) + - (lpeg.S("+-") * limit_parser.fractional) - limit_parser.time = lpeg.Cf(lpeg.Cc(1) * - (limit_parser.number / tonumber) * - ((lpeg.S("smhd") / parse_time_suffix) ^ -1), - function(acc, val) - return acc * val - end) - limit_parser.suffixed_number = lpeg.Cf(lpeg.Cc(1) * - (limit_parser.number / tonumber) * - ((lpeg.S("kmg") / parse_num_suffix) ^ -1), - function(acc, val) - return acc * val - end) - limit_parser.limit = lpeg.Ct(limit_parser.suffixed_number * - (lpeg.S(" ") ^ 0) * lpeg.S("/") * (lpeg.S(" ") ^ 0) * - limit_parser.time) - end - local t = lpeg.match(limit_parser.limit, lim) - - if t and t[1] and t[2] and t[2] ~= 0 then - return t[2], t[1] - end - - if not no_error then - rspamd_logger.errx(rspamd_config, 'bad limit: %s', lim) - end - - return nil -end - -local function str_to_rate(str) - local divider, divisor = parse_string_limit(str, false) - - if not divisor then - rspamd_logger.errx(rspamd_config, 'bad rate string: %s', str) - - return nil - end - - return divisor / divider -end - -local bucket_schema = ts.shape { - burst = ts.number + ts.string / lua_util.dehumanize_number, - rate = ts.number + ts.string / str_to_rate, - skip_recipients = ts.boolean:is_optional(), - symbol = ts.string:is_optional(), - message = ts.string:is_optional(), - skip_soft_reject = ts.boolean:is_optional(), -} - -local function parse_limit(name, data) - if type(data) == 'table' then - -- 2 cases here: - -- * old limit in format [burst, rate] - -- * vector of strings in Andrew's string format (removed from 1.8.2) - -- * proper bucket table - if #data == 2 and tonumber(data[1]) and tonumber(data[2]) then - -- Old style ratelimit - rspamd_logger.warnx(rspamd_config, 'old style ratelimit for %s', name) - if tonumber(data[1]) > 0 and tonumber(data[2]) > 0 then - return { - burst = data[1], - rate = data[2] - } - elseif data[1] ~= 0 then - rspamd_logger.warnx(rspamd_config, 'invalid numbers for %s', name) - else - rspamd_logger.infox(rspamd_config, 'disable limit %s, burst is zero', name) - end - - return nil - else - local parsed_bucket, err = bucket_schema:transform(data) - - if not parsed_bucket or err then - rspamd_logger.errx(rspamd_config, 'cannot parse bucket for %s: %s; original value: %s', - name, err, data) - else - return parsed_bucket - end - end - elseif type(data) == 'string' then - local rep_rate, burst = parse_string_limit(data) - rspamd_logger.warnx(rspamd_config, 'old style rate bucket config detected for %s: %s', - name, data) - if rep_rate and burst then - return { - burst = burst, - rate = burst / rep_rate -- reciprocal - } - end - end - - return nil -end - --- Check whether this addr is bounce local function check_bounce(from) return fun.any(function(b) @@ -490,7 +357,7 @@ local function ratelimit_cb(task) local ret, redis_key, bd = pcall(hdl, task) if ret then - local bucket = parse_limit(k, bd) + local bucket = ratelimit_common.parse_limit(k, bd) if bucket then prefixes[redis_key] = make_prefix(redis_key, k, bucket) end @@ -718,7 +585,7 @@ if opts then if lim.bucket[1] then for _, bucket in ipairs(lim.bucket) do - local b = parse_limit(t, bucket) + local b = ratelimit_common.parse_limit(t, bucket) if not b then rspamd_logger.errx(rspamd_config, 'bad ratelimit bucket for %s: "%s"', @@ -729,7 +596,7 @@ if opts then table.insert(buckets, b) end else - local bucket = parse_limit(t, lim.bucket) + local bucket = ratelimit_common.parse_limit(t, lim.bucket) if not bucket then rspamd_logger.errx(rspamd_config, 'bad ratelimit bucket for %s: "%s"', @@ -757,7 +624,7 @@ if opts then end else rspamd_logger.warnx(rspamd_config, 'old syntax for ratelimits: %s', lim) - buckets = parse_limit(t, lim) + buckets = ratelimit_common.parse_limit(t, lim) if buckets then settings.limits[t] = { buckets = { buckets } |