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 14KB

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