Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

ratelimit.lua 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. --[[
  2. Copyright (c) 2011-2017, Vsevolod Stakhov <vsevolod@highsecure.ru>
  3. Copyright (c) 2016-2017, Andrew Lewis <nerf@judo.za.org>
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. ]]--
  14. if confighelp then
  15. return
  16. end
  17. -- A plugin that implements ratelimits using redis
  18. local E = {}
  19. local N = 'ratelimit'
  20. local redis_params
  21. -- Senders that are considered as bounce
  22. local settings = {
  23. bounce_senders = { 'postmaster', 'mailer-daemon', '', 'null', 'fetchmail-daemon', 'mdaemon' },
  24. -- Do not check ratelimits for these recipients
  25. whitelisted_rcpts = { 'postmaster', 'mailer-daemon' },
  26. prefix = 'RL',
  27. ham_factor_rate = 1.01,
  28. spam_factor_rate = 0.99,
  29. ham_factor_burst = 1.02,
  30. spam_factor_burst = 0.98,
  31. max_rate_mult = 5,
  32. max_bucket_mult = 10,
  33. expire = 60 * 60 * 24 * 2, -- 2 days by default
  34. limits = {},
  35. allow_local = false,
  36. }
  37. -- Checks bucket, updating it if needed
  38. -- KEYS[1] - prefix to update, e.g. RL_<triplet>_<seconds>
  39. -- KEYS[2] - current time in milliseconds
  40. -- KEYS[3] - bucket leak rate (messages per millisecond)
  41. -- KEYS[4] - bucket burst
  42. -- KEYS[5] - expire for a bucket
  43. -- return 1 if message should be ratelimited and 0 if not
  44. -- Redis keys used:
  45. -- l - last hit
  46. -- b - current burst
  47. -- dr - current dynamic rate multiplier (*10000)
  48. -- db - current dynamic burst multiplier (*10000)
  49. local bucket_check_script = [[
  50. local last = redis.call('HGET', KEYS[1], 'l')
  51. local now = tonumber(KEYS[2])
  52. local dynr, dynb = 0, 0
  53. if not last then
  54. -- New bucket
  55. redis.call('HSET', KEYS[1], 'l', KEYS[2])
  56. redis.call('HSET', KEYS[1], 'b', '0')
  57. redis.call('HSET', KEYS[1], 'dr', '10000')
  58. redis.call('HSET', KEYS[1], 'db', '10000')
  59. redis.call('EXPIRE', KEYS[1], KEYS[5])
  60. return {0, 0, 1, 1}
  61. end
  62. last = tonumber(last)
  63. local burst = tonumber(redis.call('HGET', KEYS[1], 'b'))
  64. -- Perform leak
  65. if burst > 0 then
  66. if last < tonumber(KEYS[2]) then
  67. local rate = tonumber(KEYS[3])
  68. dynr = tonumber(redis.call('HGET', KEYS[1], 'dr')) / 10000.0
  69. rate = rate * dynr
  70. local leaked = ((now - last) * rate)
  71. burst = burst - leaked
  72. redis.call('HINCRBYFLOAT', KEYS[1], 'b', -(leaked))
  73. end
  74. dynb = tonumber(redis.call('HGET', KEYS[1], 'db')) / 10000.0
  75. if burst * dynb > tonumber(KEYS[4]) then
  76. return {1, burst, dynr, dynb}
  77. end
  78. else
  79. burst = 0
  80. redis.call('HSET', KEYS[1], 'b', '0')
  81. end
  82. return {0, burst, tostring(dynr), tostring(dynb)}
  83. ]]
  84. local bucket_check_id
  85. -- Updates a bucket
  86. -- KEYS[1] - prefix to update, e.g. RL_<triplet>_<seconds>
  87. -- KEYS[2] - current time in milliseconds
  88. -- KEYS[3] - dynamic rate multiplier
  89. -- KEYS[4] - dynamic burst multiplier
  90. -- KEYS[5] - max dyn rate (min: 1/x)
  91. -- KEYS[6] - max burst rate (min: 1/x)
  92. -- KEYS[7] - expire for a bucket
  93. -- Redis keys used:
  94. -- l - last hit
  95. -- b - current burst
  96. -- dr - current dynamic rate multiplier
  97. -- db - current dynamic burst multiplier
  98. local bucket_update_script = [[
  99. local last = redis.call('HGET', KEYS[1], 'l')
  100. local now = tonumber(KEYS[2])
  101. if not last then
  102. -- New bucket
  103. redis.call('HSET', KEYS[1], 'l', KEYS[2])
  104. redis.call('HSET', KEYS[1], 'b', '1')
  105. redis.call('HSET', KEYS[1], 'dr', '10000')
  106. redis.call('HSET', KEYS[1], 'db', '10000')
  107. redis.call('EXPIRE', KEYS[1], KEYS[7])
  108. return {1, 1, 1}
  109. end
  110. local burst = tonumber(redis.call('HGET', KEYS[1], 'b'))
  111. local db = tonumber(redis.call('HGET', KEYS[1], 'db')) / 10000
  112. local dr = tonumber(redis.call('HGET', KEYS[1], 'dr')) / 10000
  113. if dr < tonumber(KEYS[5]) and dr > 1.0 / tonumber(KEYS[5]) then
  114. dr = dr * tonumber(KEYS[3])
  115. redis.call('HSET', KEYS[1], 'dr', tostring(math.floor(dr * 10000)))
  116. end
  117. if db < tonumber(KEYS[6]) and db > 1.0 / tonumber(KEYS[6]) then
  118. db = db * tonumber(KEYS[4])
  119. redis.call('HSET', KEYS[1], 'db', tostring(math.floor(db * 10000)))
  120. end
  121. redis.call('HINCRBYFLOAT', KEYS[1], 'b', 1)
  122. redis.call('HSET', KEYS[1], 'l', KEYS[2])
  123. redis.call('EXPIRE', KEYS[1], KEYS[7])
  124. return {burst, tostring(dr), tostring(db)}
  125. ]]
  126. local bucket_update_id
  127. -- message_func(task, limit_type, prefix, bucket)
  128. local message_func = function(_, limit_type, _, _)
  129. return string.format('Ratelimit "%s" exceeded', limit_type)
  130. end
  131. local rspamd_logger = require "rspamd_logger"
  132. local rspamd_util = require "rspamd_util"
  133. local rspamd_lua_utils = require "lua_util"
  134. local lua_redis = require "lua_redis"
  135. local fun = require "fun"
  136. local lua_maps = require "lua_maps"
  137. local lua_util = require "lua_util"
  138. local rspamd_hash = require "rspamd_cryptobox_hash"
  139. local function load_scripts(cfg, ev_base)
  140. bucket_check_id = lua_redis.add_redis_script(bucket_check_script, redis_params)
  141. bucket_update_id = lua_redis.add_redis_script(bucket_update_script, redis_params)
  142. end
  143. local limit_parser
  144. local function parse_string_limit(lim, no_error)
  145. local function parse_time_suffix(s)
  146. if s == 's' then
  147. return 1
  148. elseif s == 'm' then
  149. return 60
  150. elseif s == 'h' then
  151. return 3600
  152. elseif s == 'd' then
  153. return 86400
  154. end
  155. end
  156. local function parse_num_suffix(s)
  157. if s == '' then
  158. return 1
  159. elseif s == 'k' then
  160. return 1000
  161. elseif s == 'm' then
  162. return 1000000
  163. elseif s == 'g' then
  164. return 1000000000
  165. end
  166. end
  167. local lpeg = require "lpeg"
  168. if not limit_parser then
  169. local digit = lpeg.R("09")
  170. limit_parser = {}
  171. limit_parser.integer =
  172. (lpeg.S("+-") ^ -1) *
  173. (digit ^ 1)
  174. limit_parser.fractional =
  175. (lpeg.P(".") ) *
  176. (digit ^ 1)
  177. limit_parser.number =
  178. (limit_parser.integer *
  179. (limit_parser.fractional ^ -1)) +
  180. (lpeg.S("+-") * limit_parser.fractional)
  181. limit_parser.time = lpeg.Cf(lpeg.Cc(1) *
  182. (limit_parser.number / tonumber) *
  183. ((lpeg.S("smhd") / parse_time_suffix) ^ -1),
  184. function (acc, val) return acc * val end)
  185. limit_parser.suffixed_number = lpeg.Cf(lpeg.Cc(1) *
  186. (limit_parser.number / tonumber) *
  187. ((lpeg.S("kmg") / parse_num_suffix) ^ -1),
  188. function (acc, val) return acc * val end)
  189. limit_parser.limit = lpeg.Ct(limit_parser.suffixed_number *
  190. (lpeg.S(" ") ^ 0) * lpeg.S("/") * (lpeg.S(" ") ^ 0) *
  191. limit_parser.time)
  192. end
  193. local t = lpeg.match(limit_parser.limit, lim)
  194. if t and t[1] and t[2] and t[2] ~= 0 then
  195. return t[2], t[1]
  196. end
  197. if not no_error then
  198. rspamd_logger.errx(rspamd_config, 'bad limit: %s', lim)
  199. end
  200. return nil
  201. end
  202. --- Check whether this addr is bounce
  203. local function check_bounce(from)
  204. return fun.any(function(b) return b == from end, settings.bounce_senders)
  205. end
  206. local keywords = {
  207. ['ip'] = {
  208. ['get_value'] = function(task)
  209. local ip = task:get_ip()
  210. if ip and ip:is_valid() then return tostring(ip) end
  211. return nil
  212. end,
  213. },
  214. ['rip'] = {
  215. ['get_value'] = function(task)
  216. local ip = task:get_ip()
  217. if ip and ip:is_valid() and not ip:is_local() then return tostring(ip) end
  218. return nil
  219. end,
  220. },
  221. ['from'] = {
  222. ['get_value'] = function(task)
  223. local from = task:get_from(0)
  224. if ((from or E)[1] or E).addr then
  225. return string.lower(from[1]['addr'])
  226. end
  227. return nil
  228. end,
  229. },
  230. ['bounce'] = {
  231. ['get_value'] = function(task)
  232. local from = task:get_from(0)
  233. if not ((from or E)[1] or E).user then
  234. return '_'
  235. end
  236. if check_bounce(from[1]['user']) then return '_' else return nil end
  237. end,
  238. },
  239. ['asn'] = {
  240. ['get_value'] = function(task)
  241. local asn = task:get_mempool():get_variable('asn')
  242. if not asn then
  243. return nil
  244. else
  245. return asn
  246. end
  247. end,
  248. },
  249. ['user'] = {
  250. ['get_value'] = function(task)
  251. local auser = task:get_user()
  252. if not auser then
  253. return nil
  254. else
  255. return auser
  256. end
  257. end,
  258. },
  259. ['to'] = {
  260. ['get_value'] = function(task)
  261. return task:get_principal_recipient()
  262. end,
  263. },
  264. }
  265. local function gen_rate_key(task, rtype, bucket)
  266. local key_t = {tostring(lua_util.round(100000.0 / bucket[1]))}
  267. local key_keywords = lua_util.str_split(rtype, '_')
  268. local have_user = false
  269. for _, v in ipairs(key_keywords) do
  270. local ret
  271. if keywords[v] and type(keywords[v]['get_value']) == 'function' then
  272. ret = keywords[v]['get_value'](task)
  273. end
  274. if not ret then return nil end
  275. if v == 'user' then have_user = true end
  276. if type(ret) ~= 'string' then ret = tostring(ret) end
  277. table.insert(key_t, ret)
  278. end
  279. if have_user and not task:get_user() then
  280. return nil
  281. end
  282. return table.concat(key_t, ":")
  283. end
  284. local function ratelimit_cb(task)
  285. if not settings.allow_local and
  286. rspamd_lua_utils.is_rspamc_or_controller(task) then return end
  287. -- Get initial task data
  288. local ip = task:get_from_ip()
  289. if ip and ip:is_valid() and settings.whitelisted_ip then
  290. if settings.whitelisted_ip:get_key(ip) then
  291. -- Do not check whitelisted ip
  292. rspamd_logger.infox(task, 'skip ratelimit for whitelisted IP')
  293. return
  294. end
  295. end
  296. -- Parse all rcpts
  297. local rcpts = task:get_recipients()
  298. local rcpts_user = {}
  299. if rcpts then
  300. fun.each(function(r)
  301. fun.each(function(type) table.insert(rcpts_user, r[type]) end, {'user', 'addr'})
  302. end, rcpts)
  303. if fun.any(function(r) return settings.whitelisted_rcpts:get_key(r) end, rcpts_user) then
  304. rspamd_logger.infox(task, 'skip ratelimit for whitelisted recipient')
  305. return
  306. end
  307. end
  308. -- Get user (authuser)
  309. if settings.whitelisted_user then
  310. local auser = task:get_user()
  311. if settings.whitelisted_user:get_key(auser) then
  312. rspamd_logger.infox(task, 'skip ratelimit for whitelisted user')
  313. return
  314. end
  315. end
  316. -- Now create all ratelimit prefixes
  317. local prefixes = {}
  318. local nprefixes = 0
  319. for k,v in pairs(settings.limits) do
  320. for _,bucket in ipairs(v) do
  321. local prefix = gen_rate_key(task, k, bucket)
  322. if prefix then
  323. local hash_len = 24
  324. if hash_len > #prefix then hash_len = #prefix end
  325. local hash = settings.prefix ..
  326. string.sub(rspamd_hash.create(prefix):base32(), 1, hash_len)
  327. prefixes[prefix] = {
  328. bucket = bucket,
  329. name = k,
  330. hash = hash
  331. }
  332. nprefixes = nprefixes + 1
  333. end
  334. end
  335. end
  336. local function gen_check_cb(prefix, bucket, lim_name)
  337. return function(err, data)
  338. if err then
  339. rspamd_logger.errx('cannot check limit %s: %s %s', prefix, err, data)
  340. elseif type(data) == 'table' and data[1] and data[1] == 1 then
  341. -- set symbol only and do NOT soft reject
  342. if settings.symbol then
  343. task:insert_result(settings.symbol, 0.0, lim_name .. "(" .. prefix .. ")")
  344. rspamd_logger.infox(task,
  345. 'set_symbol_only: ratelimit "%s(%s)" exceeded, (%s / %s): %s (%s:%s dyn)',
  346. lim_name, prefix, bucket[2], bucket[1], data[2], data[3], data[4])
  347. return
  348. -- set INFO symbol and soft reject
  349. elseif settings.info_symbol then
  350. task:insert_result(settings.info_symbol, 1.0, lim_name .. "(" .. prefix .. ")")
  351. end
  352. rspamd_logger.infox(task,
  353. 'ratelimit "%s(%s)" exceeded, (%s / %s): %s (%s:%s dyn)',
  354. lim_name, prefix, bucket[2], bucket[1], data[2], data[3], data[4])
  355. task:set_pre_result('soft reject',
  356. message_func(task, lim_name, prefix, bucket))
  357. end
  358. end
  359. end
  360. if nprefixes > 0 then
  361. -- Save prefixes to the cache to allow update
  362. task:cache_set('ratelimit_prefixes', prefixes)
  363. local now = rspamd_util.get_time()
  364. now = lua_util.round(now * 1000.0) -- Get milliseconds
  365. -- Now call check script for all defined prefixes
  366. for pr,value in pairs(prefixes) do
  367. local bucket = value.bucket
  368. local rate = (1.0 / bucket[1]) / 1000.0 -- Leak rate in messages/ms
  369. rspamd_logger.debugm(N, task, "check limit %s:%s -> %s (%s/%s)",
  370. value.name, pr, value.hash, bucket[2], bucket[1])
  371. lua_redis.exec_redis_script(bucket_check_id,
  372. {key = value.hash, task = task, is_write = true},
  373. gen_check_cb(pr, bucket, value.name),
  374. {value.hash, tostring(now), tostring(rate), tostring(bucket[2]),
  375. tostring(settings.expire)})
  376. end
  377. end
  378. end
  379. local function ratelimit_update_cb(task)
  380. local prefixes = task:cache_get('ratelimit_prefixes')
  381. if prefixes then
  382. local action = task:get_metric_action()
  383. local is_spam = true
  384. if action == 'soft reject' then
  385. -- Already rate limited/greylisted, do nothing
  386. rspamd_logger.debugm(N, task, 'already soft rejected, do not update')
  387. return
  388. elseif action == 'no action' then
  389. is_spam = false
  390. end
  391. local mult_burst = settings.ham_factor_burst
  392. local mult_rate = settings.ham_factor_burst
  393. if is_spam then
  394. mult_burst = settings.spam_factor_burst
  395. mult_rate = settings.spam_factor_rate
  396. end
  397. -- Update each bucket
  398. for k, v in pairs(prefixes) do
  399. local bucket = v.bucket
  400. local function update_bucket_cb(err, data)
  401. if err then
  402. rspamd_logger.errx(task, 'cannot update rate bucket %s: %s',
  403. k, err)
  404. else
  405. rspamd_logger.debugm(N, task,
  406. "updated limit %s:%s -> %s (%s/%s), burst: %s, dyn_rate: %s, dyn_burst: %s",
  407. v.name, k, v.hash, bucket[2], bucket[1], data[1], data[2], data[3])
  408. end
  409. end
  410. local now = rspamd_util.get_time()
  411. now = lua_util.round(now * 1000.0) -- Get milliseconds
  412. lua_redis.exec_redis_script(bucket_update_id,
  413. {key = v.hash, task = task, is_write = true},
  414. update_bucket_cb,
  415. {v.hash, tostring(now), tostring(mult_rate), tostring(mult_burst),
  416. tostring(settings.max_rate_mult), tostring(settings.max_bucket_mult),
  417. tostring(settings.expire)})
  418. end
  419. end
  420. end
  421. local opts = rspamd_config:get_all_opt(N)
  422. if opts then
  423. settings = lua_util.override_defaults(settings, opts)
  424. if opts['limit'] then
  425. rspamd_logger.errx(rspamd_config, 'Legacy ratelimit config format no longer supported')
  426. end
  427. if opts['rates'] and type(opts['rates']) == 'table' then
  428. -- new way of setting limits
  429. fun.each(function(t, lim)
  430. if type(lim) == 'table' then
  431. settings.limits[t] = {}
  432. if #lim == 2 and tonumber(lim[1]) and tonumber(lim[2]) then
  433. -- Old style ratelimit
  434. rspamd_logger.warnx(rspamd_config, 'old style ratelimit for %s', t)
  435. if tonumber(lim[1]) > 0 and tonumber(lim[2]) > 0 then
  436. table.insert(settings.limits[t], {1.0/lim[2], lim[1]})
  437. elseif lim[1] ~= 0 then
  438. rspamd_logger.warnx(rspamd_config, 'invalid numbers for %s', t)
  439. else
  440. rspamd_logger.infox(rspamd_config, 'disable limit %s, burst is zero', t)
  441. end
  442. else
  443. fun.each(function(l)
  444. local plim, size = parse_string_limit(l)
  445. if plim then
  446. table.insert(settings.limits[t], {plim, size})
  447. end
  448. end, lim)
  449. end
  450. elseif type(lim) == 'string' then
  451. local plim, size = parse_string_limit(lim)
  452. if plim then
  453. settings.limits[t] = { {plim, size} }
  454. end
  455. end
  456. end, opts['rates'])
  457. end
  458. local enabled_limits = fun.totable(fun.map(function(t)
  459. return t
  460. end, settings.limits))
  461. rspamd_logger.infox(rspamd_config,
  462. 'enabled rate buckets: [%1]', table.concat(enabled_limits, ','))
  463. -- Ret, ret, ret: stupid legacy stuff:
  464. -- If we have a string with commas then load it as as static map
  465. -- otherwise, apply normal logic of Rspamd maps
  466. local wrcpts = opts['whitelisted_rcpts']
  467. if type(wrcpts) == 'string' then
  468. if string.find(wrcpts, ',') then
  469. settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(
  470. lua_util.rspamd_str_split(wrcpts, ','), 'set', 'Ratelimit whitelisted rcpts')
  471. else
  472. settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(wrcpts, 'set',
  473. 'Ratelimit whitelisted rcpts')
  474. end
  475. elseif type(opts['whitelisted_rcpts']) == 'table' then
  476. settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(wrcpts, 'set',
  477. 'Ratelimit whitelisted rcpts')
  478. else
  479. -- Stupid default...
  480. settings.whitelisted_rcpts = lua_maps.rspamd_map_add_from_ucl(
  481. settings.whitelisted_rcpts, 'set', 'Ratelimit whitelisted rcpts')
  482. end
  483. if opts['whitelisted_ip'] then
  484. settings.whitelisted_ip = lua_maps.rspamd_map_add('ratelimit', 'whitelisted_ip', 'radix',
  485. 'Ratelimit whitelist ip map')
  486. end
  487. if opts['whitelisted_user'] then
  488. settings.whitelisted_user = lua_maps.rspamd_map_add('ratelimit', 'whitelisted_user', 'set',
  489. 'Ratelimit whitelist user map')
  490. end
  491. if opts['custom_keywords'] then
  492. settings.custom_keywords = dofile(opts['custom_keywords'])
  493. end
  494. if opts['message_func'] then
  495. message_func = assert(load(opts['message_func']))()
  496. end
  497. redis_params = lua_redis.parse_redis_server('ratelimit')
  498. if not redis_params then
  499. rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module')
  500. lua_util.disable_module(N, "redis")
  501. else
  502. local s = {
  503. type = 'prefilter,nostat',
  504. name = 'RATELIMIT_CHECK',
  505. priority = 4,
  506. callback = ratelimit_cb,
  507. flags = 'empty',
  508. }
  509. if settings.symbol then
  510. s.name = settings.symbol
  511. elseif settings.info_symbol then
  512. s.name = settings.info_symbol
  513. end
  514. rspamd_config:register_symbol(s)
  515. rspamd_config:register_symbol {
  516. type = 'idempotent',
  517. name = 'RATELIMIT_UPDATE',
  518. callback = ratelimit_update_cb,
  519. }
  520. if settings.custom_keywords then
  521. for _, v in pairs(settings.custom_keywords) do
  522. if type(v) == 'table' and type(v['init']) == 'function' then
  523. v['init']()
  524. end
  525. end
  526. end
  527. end
  528. end
  529. rspamd_config:add_on_load(function(cfg, ev_base, worker)
  530. load_scripts(cfg, ev_base)
  531. end)