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.

greylist.lua 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Copyright (c) 2016, Alexey Savelyev <info@homeweb.ru>
  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. --[[
  15. Example domains whitelist config:
  16. greylist {
  17. # Search "example.com" and "mail.example.com" for "mx.out.mail.example.com":
  18. whitelist_domains_url = [
  19. "$LOCAL_CONFDIR/local.d/maps.d/greylist-whitelist-domains.inc",
  20. "${CONFDIR}/maps.d/maillist.inc",
  21. "${CONFDIR}/maps.d/redirectors.inc",
  22. "${CONFDIR}/maps.d/dmarc_whitelist.inc",
  23. "${CONFDIR}/maps.d/spf_dkim_whitelist.inc",
  24. "${CONFDIR}/maps.d/surbl-whitelist.inc",
  25. "https://maps.rspamd.com/freemail/free.txt.zst"
  26. ];
  27. }
  28. Example config for exim users:
  29. greylist {
  30. action = "greylist";
  31. }
  32. --]]
  33. if confighelp then
  34. rspamd_config:add_example(nil, 'greylist',
  35. "Performs adaptive greylisting using Redis",
  36. [[
  37. greylist {
  38. # Buckets expire (1 day by default)
  39. expire = 1d;
  40. # Greylisting timeout
  41. timeout = 5m;
  42. # Redis prefix
  43. key_prefix = 'rg';
  44. # Use body hash up to this value of bytes for greylisting
  45. max_data_len = 10k;
  46. # Default greylisting message
  47. message = 'Try again later';
  48. # Append symbol on greylisting
  49. symbol = 'GREYLIST';
  50. # Default action change (for Exim use `greylist`)
  51. action = 'soft reject';
  52. # Skip greylisting if one of the following symbols has been found
  53. whitelist_symbols = [];
  54. # Mask bits for ipv4
  55. ipv4_mask = 19;
  56. # Mask bits for ipv6
  57. ipv6_mask = 64;
  58. # Tell when greylisting is expired (appended to `message`)
  59. report_time = false;
  60. # Greylist local messages
  61. check_local = false;
  62. # Greylist messages from authenticated users
  63. check_authed = false;
  64. }
  65. ]])
  66. return
  67. end
  68. -- A plugin that implements greylisting using redis
  69. local redis_params
  70. local whitelisted_ip
  71. local whitelist_domains_map
  72. local toint = math.ifloor or math.floor
  73. local settings = {
  74. expire = 86400, -- 1 day by default
  75. timeout = 300, -- 5 minutes by default
  76. key_prefix = 'rg', -- default hash name
  77. max_data_len = 10240, -- default data limit to hash
  78. message = 'Try again later', -- default greylisted message
  79. symbol = 'GREYLIST',
  80. action = 'soft reject', -- default greylisted action
  81. whitelist_symbols = {}, -- whitelist when specific symbols have been found
  82. ipv4_mask = 19, -- Mask bits for ipv4
  83. ipv6_mask = 64, -- Mask bits for ipv6
  84. report_time = false, -- Tell when greylisting is expired (appended to `message`)
  85. check_local = false,
  86. check_authed = false,
  87. }
  88. local rspamd_logger = require "rspamd_logger"
  89. local rspamd_util = require "rspamd_util"
  90. local lua_redis = require "lua_redis"
  91. local lua_util = require "lua_util"
  92. local fun = require "fun"
  93. local hash = require "rspamd_cryptobox_hash"
  94. local rspamd_lua_utils = require "lua_util"
  95. local lua_map = require "lua_maps"
  96. local N = "greylist"
  97. local function data_key(task)
  98. local cached = task:get_mempool():get_variable("grey_bodyhash")
  99. if cached then
  100. return cached
  101. end
  102. local body = task:get_rawbody()
  103. if not body then
  104. return nil
  105. end
  106. local len = body:len()
  107. if len > settings['max_data_len'] then
  108. len = settings['max_data_len']
  109. end
  110. local h = hash.create()
  111. h:update(body, len)
  112. local b32 = settings['key_prefix'] .. 'b' .. h:base32():sub(1, 20)
  113. task:get_mempool():set_variable("grey_bodyhash", b32)
  114. return b32
  115. end
  116. local function envelope_key(task)
  117. local cached = task:get_mempool():get_variable("grey_metahash")
  118. if cached then
  119. return cached
  120. end
  121. local from = task:get_from('smtp')
  122. local h = hash.create()
  123. local addr = '<>'
  124. if from and from[1] then
  125. addr = from[1]['addr']
  126. end
  127. h:update(addr)
  128. local rcpt = task:get_recipients('smtp')
  129. if rcpt then
  130. table.sort(rcpt, function(r1, r2)
  131. return r1['addr'] < r2['addr']
  132. end)
  133. fun.each(function(r)
  134. h:update(r['addr'])
  135. end, rcpt)
  136. end
  137. local ip = task:get_ip()
  138. if ip and ip:is_valid() then
  139. local s
  140. if ip:get_version() == 4 then
  141. s = tostring(ip:apply_mask(settings['ipv4_mask']))
  142. else
  143. s = tostring(ip:apply_mask(settings['ipv6_mask']))
  144. end
  145. h:update(s)
  146. end
  147. local b32 = settings['key_prefix'] .. 'm' .. h:base32():sub(1, 20)
  148. task:get_mempool():set_variable("grey_metahash", b32)
  149. return b32
  150. end
  151. -- Returns pair of booleans: found,greylisted
  152. local function check_time(task, tm, type, now)
  153. local t = tonumber(tm)
  154. if not t then
  155. rspamd_logger.errx(task, 'not a valid number: %s', tm)
  156. return false, false
  157. end
  158. if now - t < settings['timeout'] then
  159. return true, true
  160. else
  161. -- We just set variable to pass when in post-filter stage
  162. task:get_mempool():set_variable("grey_whitelisted", type)
  163. return true, false
  164. end
  165. end
  166. local function greylist_message(task, end_time, why)
  167. task:insert_result(settings['symbol'], 0.0, 'greylisted', end_time, why)
  168. if not settings.check_local and rspamd_lua_utils.is_rspamc_or_controller(task) then
  169. return
  170. end
  171. if settings.message_func then
  172. task:set_pre_result(settings['action'],
  173. settings.message_func(task, end_time), N)
  174. else
  175. local message = settings['message']
  176. if settings.report_time then
  177. message = string.format("%s: %s", message, end_time)
  178. end
  179. task:set_pre_result(settings['action'], message, N)
  180. end
  181. task:set_flag('greylisted')
  182. end
  183. local function greylist_check(task)
  184. local ip = task:get_ip()
  185. if ((not settings.check_authed and task:get_user()) or
  186. (not settings.check_local and ip and ip:is_local())) then
  187. rspamd_logger.infox(task, "skip greylisting for local networks and/or authorized users");
  188. return
  189. end
  190. if ip and ip:is_valid() and whitelisted_ip then
  191. if whitelisted_ip:get_key(ip) then
  192. -- Do not check whitelisted ip
  193. rspamd_logger.infox(task, 'skip greylisting for whitelisted IP')
  194. return
  195. end
  196. end
  197. local body_key = data_key(task)
  198. local meta_key = envelope_key(task)
  199. local hash_key = body_key .. meta_key
  200. local function redis_get_cb(err, data)
  201. local ret_body = false
  202. local greylisted_body = false
  203. local ret_meta = false
  204. local greylisted_meta = false
  205. if data then
  206. local end_time_body, end_time_meta
  207. local now = rspamd_util.get_time()
  208. if data[1] and type(data[1]) ~= 'userdata' then
  209. local tm = tonumber(data[1]) or now
  210. ret_body, greylisted_body = check_time(task, data[1], 'body', now)
  211. if greylisted_body then
  212. end_time_body = tm + settings['timeout']
  213. task:get_mempool():set_variable("grey_greylisted_body",
  214. rspamd_util.time_to_string(end_time_body))
  215. end
  216. end
  217. if data[2] and type(data[2]) ~= 'userdata' then
  218. if not ret_body or greylisted_body then
  219. local tm = tonumber(data[2]) or now
  220. ret_meta, greylisted_meta = check_time(task, data[2], 'meta', now)
  221. if greylisted_meta then
  222. end_time_meta = tm + settings['timeout']
  223. task:get_mempool():set_variable("grey_greylisted_meta",
  224. rspamd_util.time_to_string(end_time_meta))
  225. end
  226. end
  227. end
  228. local how
  229. local end_time_str
  230. if not ret_body and not ret_meta then
  231. -- no record found
  232. task:get_mempool():set_variable("grey_greylisted", 'true')
  233. elseif greylisted_body and greylisted_meta then
  234. end_time_str = rspamd_util.time_to_string(
  235. math.min(end_time_body, end_time_meta))
  236. how = 'meta and body'
  237. elseif greylisted_body then
  238. end_time_str = rspamd_util.time_to_string(end_time_body)
  239. how = 'body only'
  240. elseif greylisted_meta then
  241. end_time_str = rspamd_util.time_to_string(end_time_meta)
  242. how = 'meta only'
  243. end
  244. if how and end_time_str then
  245. rspamd_logger.infox(task, 'greylisted until "%s" (%s)',
  246. end_time_str, how)
  247. greylist_message(task, end_time_str, 'too early')
  248. end
  249. elseif err then
  250. rspamd_logger.errx(task, 'got error while getting greylisting keys: %1', err)
  251. return
  252. end
  253. end
  254. local ret = lua_redis.redis_make_request(task,
  255. redis_params, -- connect params
  256. hash_key, -- hash key
  257. false, -- is write
  258. redis_get_cb, --callback
  259. 'MGET', -- command
  260. { body_key, meta_key } -- arguments
  261. )
  262. if not ret then
  263. rspamd_logger.errx(task, 'cannot make redis request to check results')
  264. end
  265. end
  266. local function greylist_set(task)
  267. local action = task:get_metric_action()
  268. local ip = task:get_ip()
  269. -- Don't do anything if pre-result has been already set
  270. if task:has_pre_result() then
  271. return
  272. end
  273. -- Check whitelist_symbols
  274. for _, sym in ipairs(settings.whitelist_symbols) do
  275. if task:has_symbol(sym) then
  276. rspamd_logger.infox(task, 'skip greylisting as we have found symbol %s', sym)
  277. if action == 'greylist' then
  278. -- We are going to accept message
  279. rspamd_logger.infox(task, 'downgrading metric action from "greylist" to "no action"')
  280. task:disable_action('greylist')
  281. end
  282. return
  283. end
  284. end
  285. if settings.greylist_min_score then
  286. local score = task:get_metric_score('default')[1]
  287. if score < settings.greylist_min_score then
  288. rspamd_logger.infox(task, 'Score too low - skip greylisting')
  289. if action == 'greylist' then
  290. -- We are going to accept message
  291. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  292. task:disable_action('greylist')
  293. end
  294. return
  295. end
  296. end
  297. if ((not settings.check_authed and task:get_user()) or
  298. (not settings.check_local and ip and ip:is_local())) then
  299. if action == 'greylist' then
  300. -- We are going to accept message
  301. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  302. task:disable_action('greylist')
  303. end
  304. return
  305. end
  306. if ip and ip:is_valid() and whitelisted_ip then
  307. if whitelisted_ip:get_key(ip) then
  308. if action == 'greylist' then
  309. -- We are going to accept message
  310. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  311. task:disable_action('greylist')
  312. end
  313. return
  314. end
  315. end
  316. local is_whitelisted = task:get_mempool():get_variable("grey_whitelisted")
  317. local do_greylisting = task:get_mempool():get_variable("grey_greylisted")
  318. local do_greylisting_required = task:get_mempool():get_variable("grey_greylisted_required")
  319. -- Third and second level domains whitelist
  320. if not is_whitelisted and whitelist_domains_map then
  321. local hostname = task:get_hostname()
  322. if hostname then
  323. local domain = rspamd_util.get_tld(hostname)
  324. if whitelist_domains_map:get_key(hostname) or (domain and whitelist_domains_map:get_key(domain)) then
  325. is_whitelisted = 'meta'
  326. rspamd_logger.infox(task, 'skip greylisting for whitelisted domain')
  327. end
  328. end
  329. end
  330. if action == 'reject' or
  331. not do_greylisting_required and action == 'no action' then
  332. return
  333. end
  334. local body_key = data_key(task)
  335. local meta_key = envelope_key(task)
  336. local upstream, ret, conn
  337. local hash_key = body_key .. meta_key
  338. local function redis_set_cb(err)
  339. if err then
  340. rspamd_logger.errx(task, 'got error %s when setting greylisting record on server %s',
  341. err, upstream:get_addr())
  342. end
  343. end
  344. local is_rspamc = rspamd_lua_utils.is_rspamc_or_controller(task)
  345. if is_whitelisted then
  346. if action == 'greylist' then
  347. -- We are going to accept message
  348. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  349. task:disable_action('greylist')
  350. end
  351. task:insert_result(settings['symbol'], 0.0, 'pass', is_whitelisted)
  352. rspamd_logger.infox(task, 'greylisting pass (%s) until %s',
  353. is_whitelisted,
  354. rspamd_util.time_to_string(rspamd_util.get_time() + settings['expire']))
  355. if not settings.check_local and is_rspamc then
  356. return
  357. end
  358. ret, conn, upstream = lua_redis.redis_make_request(task,
  359. redis_params, -- connect params
  360. hash_key, -- hash key
  361. true, -- is write
  362. redis_set_cb, --callback
  363. 'EXPIRE', -- command
  364. { body_key, tostring(toint(settings['expire'])) } -- arguments
  365. )
  366. -- Update greylisting record expire
  367. if ret then
  368. conn:add_cmd('EXPIRE', {
  369. meta_key, tostring(toint(settings['expire']))
  370. })
  371. else
  372. rspamd_logger.errx(task, 'got error while connecting to redis')
  373. end
  374. elseif do_greylisting or do_greylisting_required then
  375. if not settings.check_local and is_rspamc then
  376. return
  377. end
  378. local t = tostring(toint(rspamd_util.get_time()))
  379. local end_time = rspamd_util.time_to_string(t + settings['timeout'])
  380. rspamd_logger.infox(task, 'greylisted until "%s", new record', end_time)
  381. greylist_message(task, end_time, 'new record')
  382. -- Create new record
  383. ret, conn, upstream = lua_redis.redis_make_request(task,
  384. redis_params, -- connect params
  385. hash_key, -- hash key
  386. true, -- is write
  387. redis_set_cb, --callback
  388. 'SETEX', -- command
  389. { body_key, tostring(toint(settings['expire'])), t } -- arguments
  390. )
  391. if ret then
  392. conn:add_cmd('SETEX', {
  393. meta_key, tostring(toint(settings['expire'])), t
  394. })
  395. else
  396. rspamd_logger.errx(task, 'got error while connecting to redis')
  397. end
  398. else
  399. if action ~= 'no action' and action ~= 'reject' then
  400. local grey_res = task:get_mempool():get_variable("grey_greylisted_body")
  401. if grey_res then
  402. -- We need to delay message, hence set a temporary result
  403. rspamd_logger.infox(task, 'greylisting delayed until "%s": body', grey_res)
  404. greylist_message(task, grey_res, 'body')
  405. else
  406. grey_res = task:get_mempool():get_variable("grey_greylisted_meta")
  407. if grey_res then
  408. greylist_message(task, grey_res, 'meta')
  409. end
  410. end
  411. else
  412. task:insert_result(settings['symbol'], 0.0, 'greylisted', 'passed')
  413. end
  414. end
  415. end
  416. local opts = rspamd_config:get_all_opt('greylist')
  417. if opts then
  418. if opts['message_func'] then
  419. settings.message_func = assert(load(opts['message_func']))()
  420. end
  421. for k, v in pairs(opts) do
  422. if k ~= 'message_func' then
  423. settings[k] = v
  424. end
  425. end
  426. local auth_and_local_conf = lua_util.config_check_local_or_authed(rspamd_config, N,
  427. false, false)
  428. settings.check_local = auth_and_local_conf[1]
  429. settings.check_authed = auth_and_local_conf[2]
  430. if settings['greylist_min_score'] then
  431. settings['greylist_min_score'] = tonumber(settings['greylist_min_score'])
  432. else
  433. local greylist_threshold = rspamd_config:get_metric_action('greylist')
  434. if greylist_threshold then
  435. settings['greylist_min_score'] = greylist_threshold
  436. end
  437. end
  438. whitelisted_ip = lua_map.rspamd_map_add(N, 'whitelisted_ip', 'radix',
  439. 'Greylist whitelist ip map')
  440. whitelist_domains_map = lua_map.rspamd_map_add(N, 'whitelist_domains_url',
  441. 'map', 'Greylist whitelist domains map')
  442. redis_params = lua_redis.parse_redis_server(N)
  443. if not redis_params then
  444. rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module')
  445. rspamd_lua_utils.disable_module(N, "redis")
  446. else
  447. lua_redis.register_prefix(settings.key_prefix .. 'b[a-z0-9]{20}', N,
  448. 'Greylisting elements (body hashes)"', {
  449. type = 'string',
  450. })
  451. lua_redis.register_prefix(settings.key_prefix .. 'm[a-z0-9]{20}', N,
  452. 'Greylisting elements (meta hashes)"', {
  453. type = 'string',
  454. })
  455. rspamd_config:register_symbol({
  456. name = 'GREYLIST_SAVE',
  457. type = 'postfilter',
  458. callback = greylist_set,
  459. priority = lua_util.symbols_priorities.medium,
  460. augmentations = { string.format("timeout=%f", redis_params.timeout or 0.0) },
  461. })
  462. local id = rspamd_config:register_symbol({
  463. name = 'GREYLIST_CHECK',
  464. type = 'prefilter',
  465. callback = greylist_check,
  466. priority = lua_util.symbols_priorities.medium,
  467. augmentations = { string.format("timeout=%f", redis_params.timeout or 0.0) }
  468. })
  469. rspamd_config:register_symbol({
  470. name = settings.symbol,
  471. type = 'virtual',
  472. parent = id,
  473. score = 0,
  474. })
  475. end
  476. end