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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  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 epired (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 return nil end
  104. local len = body:len()
  105. if len > settings['max_data_len'] then
  106. len = settings['max_data_len']
  107. end
  108. local h = hash.create()
  109. h:update(body, len)
  110. local b32 = settings['key_prefix'] .. 'b' .. h:base32():sub(1, 20)
  111. task:get_mempool():set_variable("grey_bodyhash", b32)
  112. return b32
  113. end
  114. local function envelope_key(task)
  115. local cached = task:get_mempool():get_variable("grey_metahash")
  116. if cached then
  117. return cached
  118. end
  119. local from = task:get_from('smtp')
  120. local h = hash.create()
  121. local addr = '<>'
  122. if from and from[1] then
  123. addr = from[1]['addr']
  124. end
  125. h:update(addr)
  126. local rcpt = task:get_recipients('smtp')
  127. if rcpt then
  128. table.sort(rcpt, function(r1, r2)
  129. return r1['addr'] < r2['addr']
  130. end)
  131. fun.each(function(r)
  132. h:update(r['addr'])
  133. end, rcpt)
  134. end
  135. local ip = task:get_ip()
  136. if ip and ip:is_valid() then
  137. local s
  138. if ip:get_version() == 4 then
  139. s = tostring(ip:apply_mask(settings['ipv4_mask']))
  140. else
  141. s = tostring(ip:apply_mask(settings['ipv6_mask']))
  142. end
  143. h:update(s)
  144. end
  145. local b32 = settings['key_prefix'] .. 'm' .. h:base32():sub(1, 20)
  146. task:get_mempool():set_variable("grey_metahash", b32)
  147. return b32
  148. end
  149. -- Returns pair of booleans: found,greylisted
  150. local function check_time(task, tm, type, now)
  151. local t = tonumber(tm)
  152. if not t then
  153. rspamd_logger.errx(task, 'not a valid number: %s', tm)
  154. return false,false
  155. end
  156. if now - t < settings['timeout'] then
  157. return true,true
  158. else
  159. -- We just set variable to pass when in post-filter stage
  160. task:get_mempool():set_variable("grey_whitelisted", type)
  161. return true,false
  162. end
  163. end
  164. local function greylist_message(task, end_time, why)
  165. task:insert_result(settings['symbol'], 0.0, 'greylisted', end_time, why)
  166. if not settings.check_local and rspamd_lua_utils.is_rspamc_or_controller(task) then
  167. return
  168. end
  169. if settings.message_func then
  170. task:set_pre_result(settings['action'],
  171. settings.message_func(task, end_time), N)
  172. else
  173. local message = settings['message']
  174. if settings.report_time then
  175. message = string.format("%s: %s", message, end_time)
  176. end
  177. task:set_pre_result(settings['action'], message, N)
  178. end
  179. task:set_flag('greylisted')
  180. end
  181. local function greylist_check(task)
  182. local ip = task:get_ip()
  183. if ((not settings.check_authed and task:get_user()) or
  184. (not settings.check_local and ip and ip:is_local())) then
  185. rspamd_logger.infox(task, "skip greylisting for local networks and/or authorized users");
  186. return
  187. end
  188. if ip and ip:is_valid() and whitelisted_ip then
  189. if whitelisted_ip:get_key(ip) then
  190. -- Do not check whitelisted ip
  191. rspamd_logger.infox(task, 'skip greylisting for whitelisted IP')
  192. return
  193. end
  194. end
  195. local body_key = data_key(task)
  196. local meta_key = envelope_key(task)
  197. local hash_key = body_key .. meta_key
  198. local function redis_get_cb(err, data)
  199. local ret_body = false
  200. local greylisted_body = false
  201. local ret_meta = false
  202. local greylisted_meta = false
  203. if data then
  204. local end_time_body,end_time_meta
  205. local now = rspamd_util.get_time()
  206. if data[1] and type(data[1]) ~= 'userdata' then
  207. local tm = tonumber(data[1]) or now
  208. ret_body,greylisted_body = check_time(task, data[1], 'body', now)
  209. if greylisted_body then
  210. end_time_body = tm + settings['timeout']
  211. task:get_mempool():set_variable("grey_greylisted_body",
  212. rspamd_util.time_to_string(end_time_body))
  213. end
  214. end
  215. if data[2] and type(data[2]) ~= 'userdata' then
  216. if not ret_body or greylisted_body then
  217. local tm = tonumber(data[2]) or now
  218. ret_meta,greylisted_meta = check_time(task, data[2], 'meta', now)
  219. if greylisted_meta then
  220. end_time_meta = tm + settings['timeout']
  221. task:get_mempool():set_variable("grey_greylisted_meta",
  222. rspamd_util.time_to_string(end_time_meta))
  223. end
  224. end
  225. end
  226. local how
  227. local end_time_str
  228. if not ret_body and not ret_meta then
  229. -- no record found
  230. task:get_mempool():set_variable("grey_greylisted", 'true')
  231. elseif greylisted_body and greylisted_meta then
  232. end_time_str = rspamd_util.time_to_string(
  233. math.min(end_time_body, end_time_meta))
  234. how = 'meta and body'
  235. elseif greylisted_body then
  236. end_time_str = rspamd_util.time_to_string(end_time_body)
  237. how = 'body only'
  238. elseif greylisted_meta then
  239. end_time_str = rspamd_util.time_to_string(end_time_meta)
  240. how = 'meta only'
  241. end
  242. if how and end_time_str then
  243. rspamd_logger.infox(task, 'greylisted until "%s" (%s)',
  244. end_time_str, how)
  245. greylist_message(task, end_time_str, 'too early')
  246. end
  247. elseif err then
  248. rspamd_logger.errx(task, 'got error while getting greylisting keys: %1', err)
  249. return
  250. end
  251. end
  252. local ret = lua_redis.redis_make_request(task,
  253. redis_params, -- connect params
  254. hash_key, -- hash key
  255. false, -- is write
  256. redis_get_cb, --callback
  257. 'MGET', -- command
  258. {body_key, meta_key} -- arguments
  259. )
  260. if not ret then
  261. rspamd_logger.errx(task, 'cannot make redis request to check results')
  262. end
  263. end
  264. local function greylist_set(task)
  265. local action = task:get_metric_action()
  266. local ip = task:get_ip()
  267. -- Don't do anything if pre-result has been already set
  268. if task:has_pre_result() then return end
  269. -- Check whitelist_symbols
  270. for _,sym in ipairs(settings.whitelist_symbols) do
  271. if task:has_symbol(sym) then
  272. rspamd_logger.infox(task, 'skip greylisting as we have found symbol %s', sym)
  273. if action == 'greylist' then
  274. -- We are going to accept message
  275. rspamd_logger.infox(task, 'downgrading metric action from "greylist" to "no action"')
  276. task:disable_action('greylist')
  277. end
  278. return
  279. end
  280. end
  281. if settings.greylist_min_score then
  282. local score = task:get_metric_score('default')[1]
  283. if score < settings.greylist_min_score then
  284. rspamd_logger.infox(task, 'Score too low - skip greylisting')
  285. if action == 'greylist' then
  286. -- We are going to accept message
  287. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  288. task:disable_action('greylist')
  289. end
  290. return
  291. end
  292. end
  293. if ((not settings.check_authed and task:get_user()) or
  294. (not settings.check_local and ip and ip:is_local())) then
  295. if action == 'greylist' then
  296. -- We are going to accept message
  297. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  298. task:disable_action('greylist')
  299. end
  300. return
  301. end
  302. if ip and ip:is_valid() and whitelisted_ip then
  303. if whitelisted_ip:get_key(ip) then
  304. if action == 'greylist' then
  305. -- We are going to accept message
  306. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  307. task:disable_action('greylist')
  308. end
  309. return
  310. end
  311. end
  312. local is_whitelisted = task:get_mempool():get_variable("grey_whitelisted")
  313. local do_greylisting = task:get_mempool():get_variable("grey_greylisted")
  314. local do_greylisting_required = task:get_mempool():get_variable("grey_greylisted_required")
  315. -- Third and second level domains whitelist
  316. if not is_whitelisted and whitelist_domains_map then
  317. local hostname = task:get_hostname()
  318. if hostname then
  319. local domain = rspamd_util.get_tld(hostname)
  320. if whitelist_domains_map:get_key(hostname) or (domain and whitelist_domains_map:get_key(domain)) then
  321. is_whitelisted = 'meta'
  322. rspamd_logger.infox(task, 'skip greylisting for whitelisted domain')
  323. end
  324. end
  325. end
  326. if action == 'reject' or
  327. not do_greylisting_required and action == 'no action' then
  328. return
  329. end
  330. local body_key = data_key(task)
  331. local meta_key = envelope_key(task)
  332. local upstream, ret, conn
  333. local hash_key = body_key .. meta_key
  334. local function redis_set_cb(err)
  335. if err then
  336. rspamd_logger.errx(task, 'got error %s when setting greylisting record on server %s',
  337. err, upstream:get_addr())
  338. end
  339. end
  340. local is_rspamc = rspamd_lua_utils.is_rspamc_or_controller(task)
  341. if is_whitelisted then
  342. if action == 'greylist' then
  343. -- We are going to accept message
  344. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  345. task:disable_action('greylist')
  346. end
  347. task:insert_result(settings['symbol'], 0.0, 'pass', is_whitelisted)
  348. rspamd_logger.infox(task, 'greylisting pass (%s) until %s',
  349. is_whitelisted,
  350. rspamd_util.time_to_string(rspamd_util.get_time() + settings['expire']))
  351. if not settings.check_local and is_rspamc then return end
  352. ret,conn,upstream = lua_redis.redis_make_request(task,
  353. redis_params, -- connect params
  354. hash_key, -- hash key
  355. true, -- is write
  356. redis_set_cb, --callback
  357. 'EXPIRE', -- command
  358. {body_key, tostring(toint(settings['expire']))} -- arguments
  359. )
  360. -- Update greylisting record expire
  361. if ret then
  362. conn:add_cmd('EXPIRE', {
  363. meta_key, tostring(toint(settings['expire']))
  364. })
  365. else
  366. rspamd_logger.errx(task, 'got error while connecting to redis')
  367. end
  368. elseif do_greylisting or do_greylisting_required then
  369. if not settings.check_local and is_rspamc then return end
  370. local t = tostring(toint(rspamd_util.get_time()))
  371. local end_time = rspamd_util.time_to_string(t + settings['timeout'])
  372. rspamd_logger.infox(task, 'greylisted until "%s", new record', end_time)
  373. greylist_message(task, end_time, 'new record')
  374. -- Create new record
  375. ret,conn,upstream = lua_redis.redis_make_request(task,
  376. redis_params, -- connect params
  377. hash_key, -- hash key
  378. true, -- is write
  379. redis_set_cb, --callback
  380. 'SETEX', -- command
  381. {body_key, tostring(toint(settings['expire'])), t} -- arguments
  382. )
  383. if ret then
  384. conn:add_cmd('SETEX', {
  385. meta_key, tostring(toint(settings['expire'])), t
  386. })
  387. else
  388. rspamd_logger.errx(task, 'got error while connecting to redis')
  389. end
  390. else
  391. if action ~= 'no action' and action ~= 'reject' then
  392. local grey_res = task:get_mempool():get_variable("grey_greylisted_body")
  393. if grey_res then
  394. -- We need to delay message, hence set a temporary result
  395. rspamd_logger.infox(task, 'greylisting delayed until "%s": body', grey_res)
  396. greylist_message(task, grey_res, 'body')
  397. else
  398. grey_res = task:get_mempool():get_variable("grey_greylisted_meta")
  399. if grey_res then
  400. greylist_message(task, grey_res, 'meta')
  401. end
  402. end
  403. else
  404. task:insert_result(settings['symbol'], 0.0, 'greylisted', 'passed')
  405. end
  406. end
  407. end
  408. local opts = rspamd_config:get_all_opt('greylist')
  409. if opts then
  410. if opts['message_func'] then
  411. settings.message_func = assert(load(opts['message_func']))()
  412. end
  413. for k,v in pairs(opts) do
  414. if k ~= 'message_func' then
  415. settings[k] = v
  416. end
  417. end
  418. local auth_and_local_conf = lua_util.config_check_local_or_authed(rspamd_config, N,
  419. false, false)
  420. settings.check_local = auth_and_local_conf[1]
  421. settings.check_authed = auth_and_local_conf[2]
  422. if settings['greylist_min_score'] then
  423. settings['greylist_min_score'] = tonumber(settings['greylist_min_score'])
  424. else
  425. local greylist_threshold = rspamd_config:get_metric_action('greylist')
  426. if greylist_threshold then
  427. settings['greylist_min_score'] = greylist_threshold
  428. end
  429. end
  430. whitelisted_ip = lua_map.rspamd_map_add(N, 'whitelisted_ip', 'radix',
  431. 'Greylist whitelist ip map')
  432. whitelist_domains_map = lua_map.rspamd_map_add(N, 'whitelist_domains_url',
  433. 'map', 'Greylist whitelist domains map')
  434. redis_params = lua_redis.parse_redis_server(N)
  435. if not redis_params then
  436. rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module')
  437. rspamd_lua_utils.disable_module(N, "redis")
  438. else
  439. lua_redis.register_prefix(settings.key_prefix .. 'b[a-z0-9]{20}', N,
  440. 'Greylisting elements (body hashes)"', {
  441. type = 'string',
  442. })
  443. lua_redis.register_prefix(settings.key_prefix .. 'm[a-z0-9]{20}', N,
  444. 'Greylisting elements (meta hashes)"', {
  445. type = 'string',
  446. })
  447. rspamd_config:register_symbol({
  448. name = 'GREYLIST_SAVE',
  449. type = 'postfilter',
  450. callback = greylist_set,
  451. priority = lua_util.symbols_priorities.medium,
  452. augmentations = {string.format("timeout=%f", redis_params.timeout or 0.0)},
  453. })
  454. local id = rspamd_config:register_symbol({
  455. name = 'GREYLIST_CHECK',
  456. type = 'prefilter',
  457. callback = greylist_check,
  458. priority = lua_util.symbols_priorities.medium,
  459. augmentations = {string.format("timeout=%f", redis_params.timeout or 0.0)}
  460. })
  461. rspamd_config:register_symbol({
  462. name = settings.symbol,
  463. type = 'virtual',
  464. parent = id,
  465. score = 0,
  466. })
  467. end
  468. end