diff options
author | Vsevolod Stakhov <vsevolod@highsecure.ru> | 2016-09-12 16:17:49 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-09-12 16:17:49 +0100 |
commit | 5a4f977323346cda73cd780a469ca7cbbce45225 (patch) | |
tree | 0a7e75425de53f46d52098b53200a8f05afa027e | |
parent | f17e1822fac0c2a66d6aa3d4df4eda6cd5a2af91 (diff) | |
parent | 18057a3c05af3261f277e153360d63542c43f976 (diff) | |
download | rspamd-5a4f977323346cda73cd780a469ca7cbbce45225.tar.gz rspamd-5a4f977323346cda73cd780a469ca7cbbce45225.zip |
Merge pull request #945 from fatalbanana/rlimit3
[Feature] User-defined ratelimits
-rw-r--r-- | src/plugins/lua/ratelimit.lua | 288 |
1 files changed, 150 insertions, 138 deletions
diff --git a/src/plugins/lua/ratelimit.lua b/src/plugins/lua/ratelimit.lua index 629e62323..a36012350 100644 --- a/src/plugins/lua/ratelimit.lua +++ b/src/plugins/lua/ratelimit.lua @@ -18,24 +18,6 @@ limitations under the License. -- Default settings for limits, 1-st member is burst, second is rate and the third is numeric type local settings = { - -- Limit mail per ASN (rate 12 per minute) - asn = {0, 0.199999998}, - -- Limit mail per source IP (rate 6 per minute) - ip = {0, 0.099999999}, - -- Limit for all mail per recipient (rate 2 per minute) - to = {0, 0.033333333}, - -- Limit for all mail to a recipient per source ip (rate 1.5 per minute) - to_ip = {0, 0.025}, - -- Limit for all mail per recipient/sender/source ip triplet (rate 1 per minute) - to_ip_from = {0, 0.01666666667}, - - -- Limit for all bounce mail (rate 2 per hour) - bounce_to = {0, 0.000555556}, - -- Limit for bounce mail per one source ip (rate 1 per hour) - bounce_to_ip = {0, 0.000277778}, - - -- Limit for all mail per user (authuser) (rate 1 per minute) - user = {0, 0.01666666667} } -- Senders that are considered as bounce local bounce_senders = {'postmaster', 'mailer-daemon', '', 'null', 'fetchmail-daemon', 'mdaemon'} @@ -53,6 +35,7 @@ local rl_prefix = 'rl' local ip_score_lower_bound = 10 local ip_score_ham_multiplier = 1.1 local ip_score_spam_divisor = 1.1 +local user_data = {} local rspamd_logger = require "rspamd_logger" local rspamd_redis = require "rspamd_redis" @@ -110,6 +93,117 @@ local function resize_element(x_score, x_total, element) return element end +--- Check whether this addr is bounce +local function check_bounce(from) + return fun.any(function(b) return b == from end, bounce_senders) +end + +local custom_keywords = {} + +local keywords = { + ['ip'] = { + ['get_value'] = function(task) + local ip = task:get_ip() + if ip and ip:is_valid() then return ip end + return nil + end, + }, + ['from'] = { + ['get_value'] = function(task) + local from = task:get_from(0) + if from and from[1] and from[1]['addr'] then + return from[1]['addr'] + end + return nil + end, + }, + ['bounce'] = { + ['get_value'] = function(task) + local from = task:get_from(0) + if not from and from[1] and from[1]['user'] then + return '_' + end + if check_bounce(from[1]['user']) then return '_' else return nil end + end, + }, + ['asn'] = { + ['get_value'] = function(task) + local asn = task:get_mempool():get_variable('asn') + if not asn then + return nil + else + return asn + end + end, + }, + ['user'] = { + ['get_value'] = function(task) + local auser = task:get_user() + if not auser then + return nil + else + return auser + end + end, + }, + ['to'] = { + ['get_value'] = function(task) + return '%s' -- 'to' is special + end, + }, +} + +local function dynamic_rate_key(task, rtype) + local key_t = {rl_prefix, rtype} + local key_keywords = rspamd_str_split(rtype, '_') + local have_to = false + for _, v in ipairs(key_keywords) do + if (custom_keywords[v] and type(custom_keywords[v]['condition']) == 'function') then + if not custom_keywords[v]['condition']() then return nil end + elseif (keywords[v] and type(keywords[v]['condition']) == 'function') then + if not keywords[v]['condition']() then return nil end + end + local ret + if custom_keywords[v] and custom_keywords[v]['value'] then + ret = custom_keywords[v]['value'] + elseif keywords[v] and keywords[v]['value'] then + ret = keywords[v]['value'] + end + if not ret then + if custom_keywords[v] and type(custom_keywords[v]['get_value']) == 'function' then + ret = custom_keywords[v]['get_value'](task) + if ret then custom_keywords[v]['value'] = ret end + elseif keywords[v] and type(keywords[v]['get_value']) == 'function' then + ret = keywords[v]['get_value'](task) + if ret then keywords[v]['value'] = ret end + end + end + if not ret then return nil end + if v == 'to' then have_to = true end + if type(ret) ~= 'string' then ret = tostring(ret) end + table.insert(key_t, ret) + end + if not have_to then + return table.concat(key_t, ":") + else + rate_keys = {} + rcpts = task:get_recipients(0) + if not rcpts and rcpts[1] and rcpts[1]['addr'] then + return nil + end + local key_s = table.concat(key_t, ":") + local total_rcpt = 0 + for _, r in ipairs(rcpts) do + if r['addr'] and total_rcpt < max_rcpt then + key_f = string.format(key_s, r['addr']) + table.insert(rate_keys, key_f) + total_rcpt = total_rcpt + 1 + end + end + return rate_keys + end +end + --- Check specific limit inside redis local function check_limits(task, args) @@ -180,7 +274,7 @@ local function check_limits(task, args) if mult > 0.5 then task:insert_result(ratelimit_symbol, mult, - tostring(mult)) + rtype .. ':' .. tostring(mult)) end else if bucket > threshold then @@ -290,34 +384,6 @@ local function set_limits(task, args) ) end ---- Make rate key -local function make_rate_key(rtype, args) - if rtype == 'to_ip_from' and args['from'] and args['to'] and args['ip'] and args['ip']:is_valid() then - return string.format('%s:%s:%s:%s:%s', rl_prefix, rtype, args['from'], args['to'], args['ip']:to_string()) - elseif rtype == 'to_ip' and args['to'] and args['ip'] and args['ip']:is_valid() then - return string.format('%s:%s:%s:%s', rl_prefix, rtype, args['to'], args['ip']:to_string()) - elseif rtype == 'to' and args['to'] then - return string.format('%s:%s:%s', rl_prefix, rtype, args['to']) - elseif rtype == 'bounce_to' and args['to'] then - return string.format('%s:%s:%s', rl_prefix, rtype, args['to']) - elseif rtype == 'bounce_to_ip' and args['to'] and args['ip'] and args['ip']:is_valid() then - return string.format('%s:%s:%s:%s', rl_prefix, rtype, args['to'], args['ip']:to_string()) - elseif rtype == 'asn' and args['asn'] then - return string.format('%s:%s:%s', rl_prefix, rtype, args['asn']) - elseif rtype == 'user' and args['user'] then - return string.format('%s:%s:%s', rl_prefix, rtype, args['user']) - elseif rtype == 'ip' and args['ip'] and args['ip']:is_valid() then - return string.format('%s:%s:%s', rl_prefix, rtype, args['ip']:to_string()) - else - return nil - end -end - ---- Check whether this addr is bounce -local function check_bounce(from) - return fun.any(function(b) return b == from end, bounce_senders) -end - --- Check or update ratelimit local function rate_test_set(task, func) local args = {} @@ -343,83 +409,27 @@ local function rate_test_set(task, func) return end end - -- Parse from - local from = task:get_from() - local from_user = '<>' - local from_addr = '<>' - if from and from[1] and from[1]['addr'] then - from_user = from[1]['user'] - from_addr = from[1]['addr'] - end -- Get user (authuser) - local auser = task:get_user() - local rate_key - if auser and settings['user'][1] > 0 then - if whitelisted_user and whitelisted_user:get_key(auser) then + if whitelisted_user then + local auser = task:get_user() + if whitelisted_user:get_key(auser) then rspamd_logger.infox(task, 'skip ratelimit for whitelisted user') - else - rate_key = make_rate_key ('user', {['user'] = auser}) - if rate_key then - table.insert(args, {settings['user'], rate_key}) - end + return end end - local asn - if settings['asn'][1] > 0 then - asn = task:get_mempool():get_variable('asn') - end - - local is_bounce = check_bounce(from_user) - if rcpts and not auser then - fun.each(function(r) - if is_bounce then - if settings['bounce_to'][1] > 0 then - rate_key = make_rate_key('bounce_to', {['to'] = r['addr']}) - if rate_key then - table.insert(args, {settings['bounce_to'], rate_key}) - end - end - if ip and settings['bounce_to_ip'][1] > 0 then - rate_key = make_rate_key('bounce_to_ip', {['to'] = r['addr'], ['ip'] = ip}) - if rate_key then - table.insert(args, {settings['bounce_to_ip'], rate_key}) - end - end - end - if settings['to'][1] > 0 then - rate_key = make_rate_key('to', {['to'] = r['addr']}) - if rate_key then - table.insert(args, {settings['to'], rate_key}) - end - end - if ip then - if settings['to_ip'][1] > 0 then - rate_key = make_rate_key('to_ip', {['to'] = r['addr'], ['ip'] = ip}) - if rate_key then - table.insert(args, {settings['to_ip'], rate_key}) - end - end - if settings['to_ip_from'][1] > 0 then - rate_key = make_rate_key('to_ip_from', {['from'] = from_addr, ['to'] = r['addr'], ['ip'] = ip}) - if rate_key then - table.insert(args, {settings['to_ip_from'], rate_key}) - end - end - if settings['ip'][1] > 0 then - rate_key = make_rate_key('ip', {['ip'] = ip}) - if rate_key then - table.insert(args, {settings['ip'], rate_key}) - end - end - if asn and settings['asn'][1] > 0 then - rate_key = make_rate_key('asn', {['asn'] = asn}) - if rate_key then - table.insert(args, {settings['asn'], rate_key}) - end + local rate_key + for k, v in pairs(settings) do + rate_key = dynamic_rate_key(task, k) + if rate_key then + if type(rate_key) == 'table' then + for _, rk in ipairs(rate_key) do + table.insert(args, {settings[k], rk}) end + else + table.insert(args, {settings[k], rate_key}) end - end, rcpts) + end end if #args > 0 then @@ -451,24 +461,14 @@ local function parse_limit(str) return end - if params[1] == 'to' then - set_limit(settings['to'], params[2], params[3]) - elseif params[1] == 'to_ip' then - set_limit(settings['to_ip'], params[2], params[3]) - elseif params[1] == 'to_ip_from' then - set_limit(settings['to_ip_from'], params[2], params[3]) - elseif params[1] == 'bounce_to' then - set_limit(settings['bounce_to'], params[2], params[3]) - elseif params[1] == 'bounce_to_ip' then - set_limit(settings['bounce_to_ip'], params[2], params[3]) - elseif params[1] == 'user' then - set_limit(settings['user'], params[2], params[3]) - elseif params[1] == 'ip' then - set_limit(settings['ip'], params[2], params[3]) - elseif params[1] == 'asn' then - set_limit(settings['asn'], params[2], params[3]) - else - rspamd_logger.errx(rspamd_config, 'invalid limit type: ' .. params[1]) + local key_keywords = rspamd_str_split(params[1], '_') + for _, k in ipairs(key_keywords) do + if (custom_keywords[v] and type(custom_keywords[v]['get_value']) == 'function') or + (keywords[v] and type(keywords[v]['get_value']) == 'function') then + set_limit(settings[params[1]], params[2], params[3]) + else + rspamd_logger.errx(rspamd_config, 'invalid limit type: ' .. params[1]) + end end end @@ -484,10 +484,8 @@ if opts then if opts['rates'] and type(opts['rates']) == 'table' then -- new way of setting limits fun.each(function(t, lim) - if type(lim) == 'table' and settings[t] then + if type(lim) == 'table' then settings[t] = lim - else - rspamd_logger.errx(rspamd_config, 'bad rate: %s: %s', t, lim) end end, opts['rates']) end @@ -532,6 +530,10 @@ if opts then end end + if opts['custom_keywords'] then + custom_keywords = dofile(opts['custom_keywords']) + end + redis_params = rspamd_parse_redis_server('ratelimit') if not redis_params then rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module') @@ -562,6 +564,16 @@ if opts then type = 'postfilter', callback = rate_set, }) + for _, v in pairs(keywords) do + if type(v) == 'table' and type(v['init']) == 'function' then + v['init']() + end + end + for _, v in pairs(custom_keywords) do + if type(v) == 'table' and type(v['init']) == 'function' then + v['init']() + end + end end end |