You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ratelimit.lua 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. --[[
  2. Copyright (c) 2011-2015, Vsevolod Stakhov <vsevolod@highsecure.ru>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. -- A plugin that implements ratelimits using redis or kvstorage server
  14. -- Default settings for limits, 1-st member is burst, second is rate and the third is numeric type
  15. local settings = {
  16. -- Limit for all mail per recipient (burst 100, rate 2 per minute)
  17. to = {0, 0.033333333},
  18. -- Limit for all mail per one source ip (burst 30, rate 1.5 per minute)
  19. to_ip = {0, 0.025},
  20. -- Limit for all mail per one source ip and from address (burst 20, rate 1 per minute)
  21. to_ip_from = {0, 0.01666666667},
  22. -- Limit for all bounce mail (burst 10, rate 2 per hour)
  23. bounce_to = {0, 0.000555556},
  24. -- Limit for bounce mail per one source ip (burst 5, rate 1 per hour)
  25. bounce_to_ip = {0, 0.000277778},
  26. -- Limit for all mail per user (authuser) (burst 20, rate 1 per minute)
  27. user = {0, 0.01666666667}
  28. }
  29. -- Senders that are considered as bounce
  30. local bounce_senders = {'postmaster', 'mailer-daemon', '', 'null', 'fetchmail-daemon', 'mdaemon'}
  31. -- Do not check ratelimits for these senders
  32. local whitelisted_rcpts = {'postmaster', 'mailer-daemon'}
  33. local whitelisted_ip
  34. local whitelisted_user
  35. local max_rcpt = 5
  36. local redis_params
  37. local ratelimit_symbol
  38. -- Do not delay mail after 1 day
  39. local max_delay = 24 * 3600
  40. local rspamd_logger = require "rspamd_logger"
  41. local rspamd_redis = require "rspamd_redis"
  42. local upstream_list = require "rspamd_upstream_list"
  43. local rspamd_util = require "rspamd_util"
  44. local fun = require "fun"
  45. --local dumper = require 'pl.pretty'.dump
  46. --- Parse atime and bucket of limit
  47. local function parse_limits(data)
  48. local function parse_limit_elt(str)
  49. local elts = rspamd_str_split(str, ':')
  50. if not elts or #elts < 2 then
  51. return {0, 0, 0}
  52. else
  53. local atime = tonumber(elts[1])
  54. local bucket = tonumber(elts[2])
  55. local ctime = atime
  56. if elts[3] then
  57. ctime = tonumber(elts[3])
  58. end
  59. if not ctime then
  60. ctime = atime
  61. end
  62. return {atime,bucket,ctime}
  63. end
  64. end
  65. return fun.iter(data):map(function(e)
  66. if type(e) == 'string' then
  67. return parse_limit_elt(e)
  68. else
  69. return {0, 0, 0}
  70. end
  71. end):totable()
  72. end
  73. local function generate_format_string(args, is_set)
  74. if is_set then
  75. return 'MSET'
  76. --return fun.foldl(function(acc, k) return acc .. ' %s %s' end, 'MSET', args)
  77. end
  78. return 'MGET'
  79. --return fun.foldl(function(acc, k) return acc .. ' %s' end, 'MGET', args)
  80. end
  81. --- Check specific limit inside redis
  82. local function check_limits(task, args)
  83. local key = fun.foldl(function(acc, k) return acc .. k[2] end, '', args)
  84. local ret,upstream
  85. --- Called when value is got from server
  86. local function rate_get_cb(task, err, data)
  87. if data then
  88. local ntime = rspamd_util.get_time()
  89. fun.each(function(elt, limit)
  90. local bucket = elt[2]
  91. local rate = limit[2]
  92. local threshold = limit[1]
  93. local atime = elt[1]
  94. local ctime = elt[3]
  95. if atime == 0 then return end
  96. if atime - ctime > max_delay then
  97. rspamd_logger.infox(task, 'limit is too old: %1 seconds; ignore it',
  98. atime - ctime)
  99. else
  100. bucket = bucket - rate * (ntime - atime);
  101. if bucket > 0 then
  102. if ratelimit_symbol then
  103. local mult = 2 * rspamd_util.tanh(bucket / (threshold * 2))
  104. if mult > 0.5 then
  105. task:insert_result(ratelimit_symbol, mult,
  106. tostring(mult))
  107. end
  108. else
  109. if bucket > threshold then
  110. task:set_pre_result('soft reject', 'Ratelimit exceeded')
  111. end
  112. end
  113. end
  114. end
  115. end, fun.zip(parse_limits(data), fun.map(function(a) return a[1] end, args)))
  116. elseif err then
  117. rspamd_logger.infox(task, 'got error while getting limit: %1', err)
  118. upstream:fail()
  119. end
  120. end
  121. local cmd = generate_format_string(args, false)
  122. ret,_,upstream = rspamd_redis_make_request(task,
  123. redis_params, -- connect params
  124. key, -- hash key
  125. false, -- is write
  126. rate_get_cb, --callback
  127. cmd, -- command
  128. fun.totable(fun.map(function(l) return l[2] end, args)) -- arguments
  129. )
  130. end
  131. --- Set specific limit inside redis
  132. local function set_limits(task, args)
  133. local key = fun.foldl(function(acc, k) return acc .. k[2] end, '', args)
  134. local ret, upstream
  135. local function rate_set_cb(task, err, data)
  136. if not err then
  137. upstream:ok()
  138. else
  139. rspamd_logger.infox(task, 'got error %s when setting ratelimit record on server %s',
  140. err, upstream:get_addr())
  141. end
  142. end
  143. local function rate_get_cb(task, err, data)
  144. if data then
  145. local ntime = rspamd_util.get_time()
  146. local values = {}
  147. fun.each(function(elt, limit)
  148. local bucket = elt[2]
  149. local rate = limit[1][2]
  150. local threshold = limit[1][1]
  151. local atime = elt[1]
  152. local ctime = elt[3]
  153. if atime - ctime > max_delay then
  154. rspamd_logger.infox(task, 'limit is too old: %1 seconds; start it over',
  155. atime - ctime)
  156. bucket = 1
  157. ctime = ntime
  158. atime = ntime
  159. else
  160. if bucket > 0 then
  161. bucket = bucket - rate * (ntime - atime) + 1;
  162. if bucket < 0 then
  163. bucket = 1
  164. end
  165. else
  166. bucket = 1
  167. end
  168. end
  169. if ctime == 0 then ctime = ntime end
  170. local lstr = string.format('%.3f:%.3f:%.3f', ntime, bucket, ctime)
  171. table.insert(values, {limit[2], max_delay, lstr})
  172. end, fun.zip(parse_limits(data), fun.iter(args)))
  173. if #values > 0 then
  174. local conn
  175. ret,conn,upstream = rspamd_redis_make_request(task,
  176. redis_params, -- connect params
  177. key, -- hash key
  178. true, -- is write
  179. rate_set_cb, --callback
  180. 'setex', -- command
  181. values[1] -- arguments
  182. )
  183. if conn then
  184. fun.each(function(v)
  185. conn:add_cmd('setex', v)
  186. end, fun.drop_n(1, values))
  187. else
  188. rspamd_logger.infox(task, 'got error while connecting to redis: %1', addr)
  189. upstream:fail()
  190. end
  191. elseif err then
  192. rspamd_logger.infox(task, 'got error while setting limit: %1', err)
  193. upstream:fail()
  194. end
  195. end
  196. end
  197. local cmd = generate_format_string(args, false)
  198. ret,_,upstream = rspamd_redis_make_request(task,
  199. redis_params, -- connect params
  200. key, -- hash key
  201. false, -- is write
  202. rate_get_cb, --callback
  203. cmd, -- command
  204. fun.totable(fun.map(function(l) return l[2] end, args)) -- arguments
  205. )
  206. end
  207. --- Make rate key
  208. local function make_rate_key(from, to, ip)
  209. if from and ip and ip:is_valid() then
  210. return string.format('%s:%s:%s', from, to, ip:to_string())
  211. elseif from then
  212. return string.format('%s:%s', from, to)
  213. elseif ip and ip:is_valid() then
  214. return string.format('%s:%s', to, ip:to_string())
  215. elseif to then
  216. return to
  217. else
  218. return nil
  219. end
  220. end
  221. --- Check whether this addr is bounce
  222. local function check_bounce(from)
  223. return fun.any(function(b) return b == from end, bounce_senders)
  224. end
  225. --- Check or update ratelimit
  226. local function rate_test_set(task, func)
  227. local args = {}
  228. -- Get initial task data
  229. local ip = task:get_from_ip()
  230. if ip and ip:is_valid() and whitelisted_ip then
  231. if whitelisted_ip:get_key(ip) then
  232. -- Do not check whitelisted ip
  233. rspamd_logger.infox(task, 'skip ratelimit for whitelisted IP')
  234. return
  235. end
  236. end
  237. -- Parse all rcpts
  238. local rcpts = task:get_recipients()
  239. local rcpts_user = {}
  240. if rcpts then
  241. fun.each(function(r) table.insert(rcpts_user, r['user']) end, rcpts)
  242. if fun.any(function(r)
  243. fun.any(function(w) return r == w end, whitelisted_rcpts) end,
  244. rcpts_user) then
  245. rspamd_logger.infox(task, 'skip ratelimit for whitelisted recipient')
  246. return
  247. end
  248. end
  249. -- Parse from
  250. local from = task:get_from()
  251. local from_user = '<>'
  252. local from_addr = '<>'
  253. if from and from[1] and from[1]['addr'] then
  254. from_user = from[1]['user']
  255. from_addr = from[1]['addr']
  256. end
  257. -- Get user (authuser)
  258. local auser = task:get_user()
  259. if auser and settings['user'][1] > 0 then
  260. if whitelisted_user and whitelisted_user:get_key(auser) then
  261. rspamd_logger.infox(task, 'skip ratelimit for whitelisted user')
  262. else
  263. table.insert(args, {settings['user'], make_rate_key ('user', {['user'] = auser}) })
  264. end
  265. end
  266. local is_bounce = check_bounce(from_user)
  267. if rcpts and not auser then
  268. fun.each(function(r)
  269. if is_bounce then
  270. if settings['bounce_to'][1] > 0 then
  271. table.insert(args, { settings['bounce_to'], make_rate_key('<>', r['addr'], nil) })
  272. end
  273. if ip and settings['bounce_to_ip'][1] > 0 then
  274. table.insert(args, { settings['bounce_to_ip'], make_rate_key('<>', r['addr'], ip) })
  275. end
  276. end
  277. if settings['to'][1] > 0 then
  278. table.insert(args, { settings['to'], make_rate_key(nil, r['addr'], nil) })
  279. end
  280. if ip then
  281. if settings['to_ip'][1] > 0 then
  282. table.insert(args, { settings['to_ip'], make_rate_key(nil, r['addr'], ip) })
  283. end
  284. if settings['to_ip_from'][1] > 0 then
  285. table.insert(args, { settings['to_ip_from'], make_rate_key(from_addr, r['addr'], ip) })
  286. end
  287. end
  288. end, rcpts)
  289. end
  290. if #args > 0 then
  291. func(task, args)
  292. end
  293. end
  294. --- Check limit
  295. local function rate_test(task)
  296. rate_test_set(task, check_limits)
  297. end
  298. --- Update limit
  299. local function rate_set(task)
  300. rate_test_set(task, set_limits)
  301. end
  302. --- Parse a single limit description
  303. local function parse_limit(str)
  304. local params = rspamd_str_split(str, ':')
  305. local function set_limit(limit, burst, rate)
  306. limit[1] = tonumber(burst)
  307. limit[2] = tonumber(rate)
  308. end
  309. if #params ~= 3 then
  310. rspamd_logger.errx(rspamd_config, 'invalid limit definition: ' .. str)
  311. return
  312. end
  313. if params[1] == 'to' then
  314. set_limit(settings['to'], params[2], params[3])
  315. elseif params[1] == 'to_ip' then
  316. set_limit(settings['to_ip'], params[2], params[3])
  317. elseif params[1] == 'to_ip_from' then
  318. set_limit(settings['to_ip_from'], params[2], params[3])
  319. elseif params[1] == 'bounce_to' then
  320. set_limit(settings['bounce_to'], params[2], params[3])
  321. elseif params[1] == 'bounce_to_ip' then
  322. set_limit(settings['bounce_to_ip'], params[2], params[3])
  323. elseif params[1] == 'user' then
  324. set_limit(settings['user'], params[2], params[3])
  325. else
  326. rspamd_logger.errx(rspamd_config, 'invalid limit type: ' .. params[1])
  327. end
  328. end
  329. local opts = rspamd_config:get_all_opt('ratelimit')
  330. if opts then
  331. local rates = opts['limit']
  332. if rates and type(rates) == 'table' then
  333. fun.each(parse_limit, rates)
  334. elseif rates and type(rates) == 'string' then
  335. parse_limit(rates)
  336. end
  337. if opts['rates'] and type(opts['rates']) == 'table' then
  338. -- new way of setting limits
  339. fun.each(function(t, lim)
  340. if type(lim) == 'table' and settings[t] then
  341. settings[t] = lim
  342. else
  343. rspamd_logger.errx(rspamd_config, 'bad rate: %s: %s', t, lim)
  344. end
  345. end, opts['rates'])
  346. end
  347. local enabled_limits = fun.totable(fun.map(function(t, lim)
  348. return t
  349. end, fun.filter(function(t, lim) return lim[1] > 0 end, settings)))
  350. rspamd_logger.infox(rspamd_config, 'enabled rate buckets: %s', enabled_limits)
  351. if opts['whitelisted_rcpts'] and type(opts['whitelisted_rcpts']) == 'string' then
  352. whitelisted_rcpts = rspamd_str_split(opts['whitelisted_rcpts'], ',')
  353. elseif type(opts['whitelisted_rcpts']) == 'table' then
  354. whitelisted_rcpts = opts['whitelisted_rcpts']
  355. end
  356. if opts['whitelisted_ip'] then
  357. whitelisted_ip = rspamd_config:add_radix_map(opts['whitelisted_ip'], 'Ratelimit whitelist ip map')
  358. end
  359. if opts['whitelisted_user'] then
  360. whitelisted_user = rspamd_config:add_kv_map(opts['whitelisted_user'], 'Ratelimit whitelist user map')
  361. end
  362. if opts['symbol'] then
  363. -- We want symbol instead of pre-result
  364. ratelimit_symbol = opts['symbol']
  365. end
  366. if opts['max_rcpt'] then
  367. max_rcpt = tonumber(opts['max_rcpt'])
  368. end
  369. if opts['max_delay'] then
  370. max_rcpt = tonumber(opts['max_delay'])
  371. end
  372. redis_params = rspamd_parse_redis_server('ratelimit')
  373. if not redis_params then
  374. rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module')
  375. else
  376. if not ratelimit_symbol then
  377. rspamd_config:register_symbol({
  378. name = 'RATELIMIT_CHECK',
  379. type = 'prefilter',
  380. callback = rate_test,
  381. })
  382. else
  383. rspamd_config:register_symbol({
  384. name = ratelimit_symbol,
  385. callback = rate_test,
  386. flags = 'empty'
  387. })
  388. end
  389. rspamd_config:register_symbol({
  390. name = 'RATELIMIT_SET',
  391. type = 'postfilter',
  392. callback = rate_set,
  393. })
  394. end
  395. end