您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

greylist.lua 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  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. -- We need to update this on each scan, as it can vary per settings or be redefined dynamically
  286. local greylist_min_score = settings.greylist_min_score or task:get_metric_threshold('greylist')
  287. if greylist_min_score then
  288. local score = task:get_metric_score()[1]
  289. if score < greylist_min_score then
  290. rspamd_logger.infox(task, 'Score too low - skip greylisting')
  291. if action == 'greylist' then
  292. -- We are going to accept message
  293. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  294. task:disable_action('greylist')
  295. end
  296. return
  297. end
  298. end
  299. if ((not settings.check_authed and task:get_user()) or
  300. (not settings.check_local and ip and ip:is_local())) then
  301. if action == 'greylist' then
  302. -- We are going to accept message
  303. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  304. task:disable_action('greylist')
  305. end
  306. return
  307. end
  308. if ip and ip:is_valid() and whitelisted_ip then
  309. if whitelisted_ip:get_key(ip) then
  310. if action == 'greylist' then
  311. -- We are going to accept message
  312. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  313. task:disable_action('greylist')
  314. end
  315. return
  316. end
  317. end
  318. local is_whitelisted = task:get_mempool():get_variable("grey_whitelisted")
  319. local do_greylisting = task:get_mempool():get_variable("grey_greylisted")
  320. local do_greylisting_required = task:get_mempool():get_variable("grey_greylisted_required")
  321. -- Third and second level domains whitelist
  322. if not is_whitelisted and whitelist_domains_map then
  323. local hostname = task:get_hostname()
  324. if hostname then
  325. local domain = rspamd_util.get_tld(hostname)
  326. if whitelist_domains_map:get_key(hostname) or (domain and whitelist_domains_map:get_key(domain)) then
  327. is_whitelisted = 'meta'
  328. rspamd_logger.infox(task, 'skip greylisting for whitelisted domain')
  329. end
  330. end
  331. end
  332. if action == 'reject' or
  333. not do_greylisting_required and action == 'no action' then
  334. return
  335. end
  336. local body_key = data_key(task)
  337. local meta_key = envelope_key(task)
  338. local upstream, ret, conn
  339. local hash_key = body_key .. meta_key
  340. local function redis_set_cb(err)
  341. if err then
  342. rspamd_logger.errx(task, 'got error %s when setting greylisting record on server %s',
  343. err, upstream:get_addr())
  344. end
  345. end
  346. local is_rspamc = rspamd_lua_utils.is_rspamc_or_controller(task)
  347. if is_whitelisted then
  348. if action == 'greylist' then
  349. -- We are going to accept message
  350. rspamd_logger.infox(task, 'Downgrading metric action from "greylist" to "no action"')
  351. task:disable_action('greylist')
  352. end
  353. task:insert_result(settings['symbol'], 0.0, 'pass', is_whitelisted)
  354. rspamd_logger.infox(task, 'greylisting pass (%s) until %s',
  355. is_whitelisted,
  356. rspamd_util.time_to_string(rspamd_util.get_time() + settings['expire']))
  357. if not settings.check_local and is_rspamc then
  358. return
  359. end
  360. ret, conn, upstream = lua_redis.redis_make_request(task,
  361. redis_params, -- connect params
  362. hash_key, -- hash key
  363. true, -- is write
  364. redis_set_cb, --callback
  365. 'EXPIRE', -- command
  366. { body_key, tostring(toint(settings['expire'])) } -- arguments
  367. )
  368. -- Update greylisting record expire
  369. if ret then
  370. conn:add_cmd('EXPIRE', {
  371. meta_key, tostring(toint(settings['expire']))
  372. })
  373. else
  374. rspamd_logger.errx(task, 'got error while connecting to redis')
  375. end
  376. elseif do_greylisting or do_greylisting_required then
  377. if not settings.check_local and is_rspamc then
  378. return
  379. end
  380. local t = tostring(toint(rspamd_util.get_time()))
  381. local end_time = rspamd_util.time_to_string(t + settings['timeout'])
  382. rspamd_logger.infox(task, 'greylisted until "%s", new record', end_time)
  383. greylist_message(task, end_time, 'new record')
  384. -- Create new record
  385. ret, conn, upstream = lua_redis.redis_make_request(task,
  386. redis_params, -- connect params
  387. hash_key, -- hash key
  388. true, -- is write
  389. redis_set_cb, --callback
  390. 'SETEX', -- command
  391. { body_key, tostring(toint(settings['expire'])), t } -- arguments
  392. )
  393. if ret then
  394. conn:add_cmd('SETEX', {
  395. meta_key, tostring(toint(settings['expire'])), t
  396. })
  397. else
  398. rspamd_logger.errx(task, 'got error while connecting to redis')
  399. end
  400. else
  401. if action ~= 'no action' and action ~= 'reject' then
  402. local grey_res = task:get_mempool():get_variable("grey_greylisted_body")
  403. if grey_res then
  404. -- We need to delay message, hence set a temporary result
  405. rspamd_logger.infox(task, 'greylisting delayed until "%s": body', grey_res)
  406. greylist_message(task, grey_res, 'body')
  407. else
  408. grey_res = task:get_mempool():get_variable("grey_greylisted_meta")
  409. if grey_res then
  410. greylist_message(task, grey_res, 'meta')
  411. end
  412. end
  413. else
  414. task:insert_result(settings['symbol'], 0.0, 'greylisted', 'passed')
  415. end
  416. end
  417. end
  418. local opts = rspamd_config:get_all_opt('greylist')
  419. if opts then
  420. if opts['message_func'] then
  421. settings.message_func = assert(load(opts['message_func']))()
  422. end
  423. for k, v in pairs(opts) do
  424. if k ~= 'message_func' then
  425. settings[k] = v
  426. end
  427. end
  428. local auth_and_local_conf = lua_util.config_check_local_or_authed(rspamd_config, N,
  429. false, false)
  430. settings.check_local = auth_and_local_conf[1]
  431. settings.check_authed = auth_and_local_conf[2]
  432. if settings['greylist_min_score'] then
  433. settings['greylist_min_score'] = tonumber(settings['greylist_min_score'])
  434. end
  435. whitelisted_ip = lua_map.rspamd_map_add(N, 'whitelisted_ip', 'radix',
  436. 'Greylist whitelist ip map')
  437. whitelist_domains_map = lua_map.rspamd_map_add(N, 'whitelist_domains_url',
  438. 'map', 'Greylist whitelist domains map')
  439. redis_params = lua_redis.parse_redis_server(N)
  440. if not redis_params then
  441. rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module')
  442. rspamd_lua_utils.disable_module(N, "redis")
  443. else
  444. lua_redis.register_prefix(settings.key_prefix .. 'b[a-z0-9]{20}', N,
  445. 'Greylisting elements (body hashes)"', {
  446. type = 'string',
  447. })
  448. lua_redis.register_prefix(settings.key_prefix .. 'm[a-z0-9]{20}', N,
  449. 'Greylisting elements (meta hashes)"', {
  450. type = 'string',
  451. })
  452. rspamd_config:register_symbol({
  453. name = 'GREYLIST_SAVE',
  454. type = 'postfilter',
  455. callback = greylist_set,
  456. priority = lua_util.symbols_priorities.medium,
  457. augmentations = { string.format("timeout=%f", redis_params.timeout or 0.0) },
  458. })
  459. local id = rspamd_config:register_symbol({
  460. name = 'GREYLIST_CHECK',
  461. type = 'prefilter',
  462. callback = greylist_check,
  463. priority = lua_util.symbols_priorities.medium,
  464. augmentations = { string.format("timeout=%f", redis_params.timeout or 0.0) }
  465. })
  466. rspamd_config:register_symbol({
  467. name = settings.symbol,
  468. type = 'virtual',
  469. parent = id,
  470. score = 0,
  471. })
  472. end
  473. end