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.

known_senders.lua 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. --[[
  2. Copyright (c) 2023, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. -- This plugin implements known senders logic for Rspamd
  14. local rspamd_logger = require "rspamd_logger"
  15. local ts = (require "tableshape").types
  16. local N = 'known_senders'
  17. local lua_util = require "lua_util"
  18. local lua_redis = require "lua_redis"
  19. local lua_maps = require "lua_maps"
  20. local rspamd_cryptobox_hash = require "rspamd_cryptobox_hash"
  21. if confighelp then
  22. rspamd_config:add_example(nil, 'known_senders',
  23. "Maintain a list of known senders using Redis",
  24. [[
  25. known_senders {
  26. # Domains to track senders
  27. domains = "https://maps.rspamd.com/freemail/free.txt.zst";
  28. # Maximum number of elements
  29. max_senders = 100000;
  30. # Maximum time to live (when not using bloom filters)
  31. max_ttl = 30d;
  32. # Use bloom filters (must be enabled in Redis as a plugin)
  33. use_bloom = false;
  34. # Insert symbol for new senders from the specific domains
  35. symbol_unknown = 'UNKNOWN_SENDER';
  36. }
  37. ]])
  38. return
  39. end
  40. local redis_params
  41. local settings = {
  42. domains = {},
  43. max_senders = 100000,
  44. max_ttl = 30 * 86400,
  45. use_bloom = false,
  46. symbol = 'KNOWN_SENDER',
  47. symbol_unknown = 'UNKNOWN_SENDER',
  48. symbol_check_mail_global = 'INC_MAIL_KNOWN_GLOBALLY',
  49. symbol_check_mail_local = 'INC_MAIL_KNOWN_LOCALLY',
  50. max_recipients = 15,
  51. redis_key = 'rs_known_senders',
  52. sender_prefix = 'rsrk',
  53. sender_key_global = 'verified_senders',
  54. sender_key_size = 20,
  55. reply_sender_privacy = false,
  56. reply_sender_privacy_alg = 'blake2',
  57. reply_sender_privacy_prefix = 'obf',
  58. reply_sender_privacy_length = 16,
  59. }
  60. local settings_schema = lua_redis.enrich_schema({
  61. domains = lua_maps.map_schema,
  62. enabled = ts.boolean:is_optional(),
  63. max_senders = (ts.integer + ts.string / tonumber):is_optional(),
  64. max_ttl = (ts.integer + ts.string / tonumber):is_optional(),
  65. use_bloom = ts.boolean:is_optional(),
  66. redis_key = ts.string:is_optional(),
  67. symbol = ts.string:is_optional(),
  68. symbol_unknown = ts.string:is_optional(),
  69. })
  70. local function make_key(input)
  71. local hash = rspamd_cryptobox_hash.create_specific('md5')
  72. hash:update(input.addr)
  73. return hash:hex()
  74. end
  75. local function make_key_replies(goop, sz, prefix)
  76. local h = rspamd_cryptobox_hash.create()
  77. h:update(goop)
  78. local key = (prefix or '') .. h:base32():sub(1, sz)
  79. return key
  80. end
  81. local zscore_script_id
  82. local function configure_scripts(_, _, _)
  83. -- script checks if given recipients are in the local replies set of the sender
  84. local redis_zscore_script = [[
  85. local results = {}
  86. local replies_recipients_addrs = {}
  87. replies_recipients_addrs = ARGV
  88. if replies_recipients_addrs ~= nil then
  89. for _, rcpt in ipairs(replies_recipients_addrs) do
  90. local score = redis.call('ZSCORE', KEYS[1], rcpt)
  91. if type(score) == 'boolean' then
  92. score = nil
  93. table.insert(results, score)
  94. -- 0 is stand for failure code
  95. return { 0, results }
  96. end
  97. table.insert(results, score)
  98. end
  99. -- first number in return statement is stands for the success/failure code
  100. -- where success code is 1 and failure code is 0
  101. return { 1, results }
  102. else
  103. -- 0 is a failure code
  104. return { 0, results }
  105. end
  106. ]]
  107. local zscore_script = lua_util.jinja_template(redis_zscore_script, { })
  108. rspamd_logger.debugm(N, rspamd_config, 'added check for recipients in local replies set script %s', zscore_script)
  109. zscore_script_id = lua_redis.add_redis_script(zscore_script, redis_params)
  110. end
  111. local function check_redis_key(task, key, key_ty)
  112. lua_util.debugm(N, task, 'check key %s, type: %s', key, key_ty)
  113. local function redis_zset_callback(err, data)
  114. lua_util.debugm(N, task, 'got data: %s', data)
  115. if err then
  116. rspamd_logger.errx(task, 'redis error: %s', err)
  117. elseif data then
  118. if type(data) ~= 'userdata' then
  119. -- non-null reply
  120. task:insert_result(settings.symbol, 1.0, string.format("%s:%s", key_ty, key))
  121. else
  122. if settings.symbol_unknown then
  123. task:insert_result(settings.symbol_unknown, 1.0, string.format("%s:%s", key_ty, key))
  124. end
  125. lua_util.debugm(N, task, 'insert key %s, type: %s', key, key_ty)
  126. -- Insert key to zset and trim it's cardinality
  127. lua_redis.redis_make_request(task,
  128. redis_params, -- connect params
  129. key, -- hash key
  130. true, -- is write
  131. nil, --callback
  132. 'ZADD', -- command
  133. { settings.redis_key, tostring(task:get_timeval(true)), key } -- arguments
  134. )
  135. lua_redis.redis_make_request(task,
  136. redis_params, -- connect params
  137. key, -- hash key
  138. true, -- is write
  139. nil, --callback
  140. 'ZREMRANGEBYRANK', -- command
  141. { settings.redis_key, '0',
  142. tostring(-(settings.max_senders + 1)) } -- arguments
  143. )
  144. end
  145. end
  146. end
  147. local function redis_bloom_callback(err, data)
  148. lua_util.debugm(N, task, 'got data: %s', data)
  149. if err then
  150. rspamd_logger.errx(task, 'redis error: %s', err)
  151. elseif data then
  152. if type(data) ~= 'userdata' and data == 1 then
  153. -- non-null reply equal to `1`
  154. task:insert_result(settings.symbol, 1.0, string.format("%s:%s", key_ty, key))
  155. else
  156. if settings.symbol_unknown then
  157. task:insert_result(settings.symbol_unknown, 1.0, string.format("%s:%s", key_ty, key))
  158. end
  159. lua_util.debugm(N, task, 'insert key %s, type: %s', key, key_ty)
  160. -- Reserve bloom filter space
  161. lua_redis.redis_make_request(task,
  162. redis_params, -- connect params
  163. key, -- hash key
  164. true, -- is write
  165. nil, --callback
  166. 'BF.RESERVE', -- command
  167. { settings.redis_key, tostring(settings.max_senders), '0.01', '1000', 'NONSCALING' } -- arguments
  168. )
  169. -- Insert key and adjust bloom filter
  170. lua_redis.redis_make_request(task,
  171. redis_params, -- connect params
  172. key, -- hash key
  173. true, -- is write
  174. nil, --callback
  175. 'BF.ADD', -- command
  176. { settings.redis_key, key } -- arguments
  177. )
  178. end
  179. end
  180. end
  181. if settings.use_bloom then
  182. lua_redis.redis_make_request(task,
  183. redis_params, -- connect params
  184. key, -- hash key
  185. false, -- is write
  186. redis_bloom_callback, --callback
  187. 'BF.EXISTS', -- command
  188. { settings.redis_key, key } -- arguments
  189. )
  190. else
  191. lua_redis.redis_make_request(task,
  192. redis_params, -- connect params
  193. key, -- hash key
  194. false, -- is write
  195. redis_zset_callback, --callback
  196. 'ZSCORE', -- command
  197. { settings.redis_key, key } -- arguments
  198. )
  199. end
  200. end
  201. local function known_senders_callback(task)
  202. local mime_from = (task:get_from('mime') or {})[1]
  203. local smtp_from = (task:get_from('smtp') or {})[1]
  204. local mime_key, smtp_key
  205. if mime_from and mime_from.addr then
  206. if settings.domains:get_key(mime_from.domain) then
  207. mime_key = make_key(mime_from)
  208. else
  209. lua_util.debugm(N, task, 'skip mime from domain %s', mime_from.domain)
  210. end
  211. end
  212. if smtp_from and smtp_from.addr then
  213. if settings.domains:get_key(smtp_from.domain) then
  214. smtp_key = make_key(smtp_from)
  215. else
  216. lua_util.debugm(N, task, 'skip smtp from domain %s', smtp_from.domain)
  217. end
  218. end
  219. if mime_key and smtp_key and mime_key ~= smtp_key then
  220. -- Check both keys
  221. check_redis_key(task, mime_key, 'mime')
  222. check_redis_key(task, smtp_key, 'smtp')
  223. elseif mime_key then
  224. -- Check mime key
  225. check_redis_key(task, mime_key, 'mime')
  226. elseif smtp_key then
  227. -- Check smtp key
  228. check_redis_key(task, smtp_key, 'smtp')
  229. end
  230. end
  231. local function verify_local_replies_set(task)
  232. local replies_sender = task:get_reply_sender()
  233. if not replies_sender then
  234. lua_util.debugm(N, task, 'Could not get sender')
  235. return nil
  236. end
  237. local replies_recipients = task:get_recipients('mime')
  238. local replies_sender_string = lua_util.maybe_obfuscate_string(tostring(replies_sender), settings, settings.sender_prefix)
  239. local replies_sender_key = make_key_replies(replies_sender_string:lower(), 8)
  240. local function redis_zscore_script_cb(err, data)
  241. if err ~= nil then
  242. rspamd_logger.errx(task, 'Could not verify %s local replies set %s', replies_sender_key, err)
  243. end
  244. if data[1] ~= 1 then
  245. rspamd_logger.infox(task, 'Recipients was not verified')
  246. else
  247. rspamd_logger.infox(task, 'Recipients was verified')
  248. task:insert_result(settings.symbol_check_mail_local, 1.0, replies_sender_key)
  249. end
  250. end
  251. local replies_recipients_addrs = {}
  252. -- assigning addresses of recipients for params and limiting number of recipients to be checked
  253. for i, rcpt in ipairs(replies_recipients) do
  254. if i > settings['max_recipients'] then
  255. break
  256. end
  257. table.insert(replies_recipients_addrs, rcpt.addr)
  258. end
  259. lua_util.debugm(N, task, 'Making redis request to local replies set')
  260. lua_redis.exec_redis_script(zscore_script_id,
  261. {task = task, is_write = true},
  262. redis_zscore_script_cb,
  263. { replies_sender_key },
  264. replies_recipients_addrs )
  265. end
  266. local function check_known_incoming_mail_callback(task)
  267. local replies_sender = task:get_reply_sender()
  268. if not replies_sender then
  269. lua_util.debugm(N, task, 'Could not get sender')
  270. return nil
  271. end
  272. -- making sender key
  273. lua_util.debugm(N, task, 'Sender: %s', replies_sender)
  274. local replies_sender_string = lua_util.maybe_obfuscate_string(tostring(replies_sender), settings, settings.sender_prefix)
  275. local replies_sender_key = make_key_replies(replies_sender_string:lower(), 8)
  276. lua_util.debugm(N, task, 'Sender key: %s', replies_sender_key)
  277. local function redis_zscore_global_cb(err, data)
  278. if err ~= nil then
  279. rspamd_logger.errx(task, 'Couldn\'t find sender %s in global replies set. Ended with error: %s', replies_sender, err)
  280. return
  281. end
  282. --checking if zcore have not found score of a sender
  283. if data ~= nil and data ~= '' and type(data) ~= 'userdata' then
  284. rspamd_logger.infox(task, 'Sender: %s verified. Output: %s', replies_sender, data)
  285. task:insert_result(settings.symbol_check_mail_global, 1.0, replies_sender)
  286. else
  287. rspamd_logger.infox(task, 'Sender: %s was not verified', replies_sender)
  288. end
  289. end
  290. -- key for global replies set
  291. local replies_global_key = make_key_replies(settings.sender_key_global, settings.sender_key_size, settings.sender_prefix)
  292. -- using zscore to find sender in global set
  293. lua_util.debugm(N, task, 'Making redis request to global replies set')
  294. lua_redis.redis_make_request(task,
  295. redis_params, -- connect params
  296. replies_sender_key, -- hash key
  297. false, -- is write
  298. redis_zscore_global_cb, --callback
  299. 'ZSCORE', -- command
  300. { replies_global_key, replies_sender } -- arguments
  301. )
  302. end
  303. local opts = rspamd_config:get_all_opt('known_senders')
  304. if opts then
  305. settings = lua_util.override_defaults(settings, opts)
  306. local res, err = settings_schema:transform(settings)
  307. if not res then
  308. rspamd_logger.errx(rspamd_config, 'cannot parse known_senders options: %1', err)
  309. else
  310. settings = res
  311. end
  312. redis_params = lua_redis.parse_redis_server(N, opts)
  313. if redis_params then
  314. local map_conf = settings.domains
  315. settings.domains = lua_maps.map_add_from_ucl(settings.domains,
  316. 'set', 'domains to track senders from')
  317. if not settings.domains then
  318. rspamd_logger.errx(rspamd_config, "couldn't add map %s, disable module",
  319. map_conf)
  320. lua_util.disable_module(N, "config")
  321. return
  322. end
  323. lua_redis.register_prefix(settings.redis_key, N,
  324. 'Known elements redis key', {
  325. type = 'zset/bloom filter',
  326. })
  327. lua_redis.register_prefix(settings.sender_prefix, N,
  328. 'Prefix to identify replies sets')
  329. local id = rspamd_config:register_symbol({
  330. name = settings.symbol,
  331. type = 'normal',
  332. callback = known_senders_callback,
  333. one_shot = true,
  334. score = -1.0,
  335. augmentations = { string.format("timeout=%f", redis_params.timeout or 0.0) }
  336. })
  337. rspamd_config:register_symbol({
  338. name = settings.symbol_check_mail_local,
  339. type = 'normal',
  340. callback = verify_local_replies_set,
  341. score = 1.0
  342. })
  343. rspamd_config:register_symbol({
  344. name = settings.symbol_check_mail_global,
  345. type = 'normal',
  346. callback = check_known_incoming_mail_callback,
  347. score = 1.0
  348. })
  349. if settings.symbol_unknown and #settings.symbol_unknown > 0 then
  350. rspamd_config:register_symbol({
  351. name = settings.symbol_unknown,
  352. type = 'virtual',
  353. parent = id,
  354. one_shot = true,
  355. score = 0.5,
  356. })
  357. end
  358. else
  359. lua_util.disable_module(N, "redis")
  360. end
  361. end
  362. rspamd_config:add_post_init(function(cfg, ev_base, worker)
  363. configure_scripts(cfg, ev_base, worker)
  364. end)