Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

rbl.lua 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. --[[
  2. Copyright (c) 2011-2015, Vsevolod Stakhov <vsevolod@highsecure.ru>
  3. Copyright (c) 2013-2015, Andrew Lewis <nerf@judo.za.org>
  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. if confighelp then
  15. return
  16. end
  17. local hash = require 'rspamd_cryptobox_hash'
  18. local rspamd_logger = require 'rspamd_logger'
  19. local rspamd_util = require 'rspamd_util'
  20. local fun = require 'fun'
  21. local lua_util = require 'lua_util'
  22. local ts = require("tableshape").types
  23. -- This plugin implements various types of RBL checks
  24. -- Documentation can be found here:
  25. -- https://rspamd.com/doc/modules/rbl.html
  26. local E = {}
  27. local N = 'rbl'
  28. local local_exclusions
  29. local default_monitored = '1.0.0.127'
  30. local function validate_dns(lstr)
  31. if lstr:match('%.%.') then
  32. -- two dots in a row
  33. return false
  34. end
  35. for v in lstr:gmatch('[^%.]+') do
  36. if not v:match('^[%w-]+$') or v:len() > 63
  37. or v:match('^-') or v:match('-$') then
  38. -- too long label or weird labels
  39. return false
  40. end
  41. end
  42. return true
  43. end
  44. local function maybe_make_hash(data, rule)
  45. if rule.hash then
  46. local h = hash.create_specific(rule.hash, data)
  47. local s
  48. if rule.hash_format then
  49. if rule.hash_format == 'base32' then
  50. s = h:base32()
  51. elseif rule.hash_format == 'base64' then
  52. s = h:base64()
  53. else
  54. s = h:hex()
  55. end
  56. else
  57. s = h:hex()
  58. end
  59. if rule.hash_len then
  60. s = s:sub(1, rule.hash_len)
  61. end
  62. return s
  63. else
  64. return data
  65. end
  66. end
  67. local function is_excluded_ip(rip)
  68. if local_exclusions and local_exclusions:get_key(rip) then
  69. return true
  70. end
  71. return false
  72. end
  73. local function ip_to_rbl(ip)
  74. return table.concat(ip:inversed_str_octets(), '.')
  75. end
  76. local function gen_check_rcvd_conditions(rbl, received_total)
  77. local min_pos = tonumber(rbl['received_min_pos'])
  78. local max_pos = tonumber(rbl['received_max_pos'])
  79. local match_flags = rbl['received_flags']
  80. local nmatch_flags = rbl['received_nflags']
  81. local function basic_received_check(rh)
  82. if not (rh['real_ip'] and rh['real_ip']:is_valid()) then return false end
  83. if ((rh['real_ip']:get_version() == 6 and rbl['ipv6']) or
  84. (rh['real_ip']:get_version() == 4 and rbl['ipv4'])) and
  85. ((rbl['exclude_private_ips'] and not rh['real_ip']:is_local()) or
  86. not rbl['exclude_private_ips']) and ((rbl['exclude_local_ips'] and
  87. not is_excluded_ip(rh['real_ip'])) or not rbl['exclude_local_ips']) then
  88. return true
  89. else
  90. return false
  91. end
  92. end
  93. if not (max_pos or min_pos or match_flags or nmatch_flags) then
  94. return basic_received_check
  95. end
  96. return function(rh, pos)
  97. if not basic_received_check() then return false end
  98. local got_flags = rh['flags'] or E
  99. if min_pos then
  100. if min_pos < 0 then
  101. if min_pos == -1 then
  102. if (pos ~= received_total) then
  103. return false
  104. end
  105. else
  106. if pos <= (received_total - (min_pos*-1)) then
  107. return false
  108. end
  109. end
  110. elseif pos < min_pos then
  111. return false
  112. end
  113. end
  114. if max_pos then
  115. if max_pos < -1 then
  116. if (received_total - (max_pos*-1)) >= pos then
  117. return false
  118. end
  119. elseif max_pos > 0 then
  120. if pos > max_pos then
  121. return false
  122. end
  123. end
  124. end
  125. if match_flags then
  126. for _, flag in ipairs(match_flags) do
  127. if not got_flags[flag] then
  128. return false
  129. end
  130. end
  131. end
  132. if nmatch_flags then
  133. for _, flag in ipairs(nmatch_flags) do
  134. if got_flags[flag] then
  135. return false
  136. end
  137. end
  138. end
  139. return true
  140. end
  141. end
  142. local function rbl_dns_process(task, rbl, to_resolve, results, err)
  143. if err and (err ~= 'requested record is not found' and
  144. err ~= 'no records with this name') then
  145. rspamd_logger.errx(task, 'error looking up %s: %s', to_resolve, err)
  146. end
  147. if not results then
  148. lua_util.debugm(N, task,
  149. 'DNS RESPONSE: label=%1 results=%2 error=%3 rbl=%4',
  150. to_resolve, false, err, rbl.symbol)
  151. return
  152. else
  153. lua_util.debugm(N, task,
  154. 'DNS RESPONSE: label=%1 results=%2 error=%3 rbl=%4',
  155. to_resolve, true, err, rbl.symbol)
  156. end
  157. if rbl.returncodes == nil and rbl.symbol ~= nil then
  158. task:insert_result(rbl.symbol, 1, to_resolve)
  159. return
  160. end
  161. for _,result in ipairs(results) do
  162. local ipstr = result:to_string()
  163. lua_util.debugm(N, task, '%s DNS result %s', to_resolve, ipstr)
  164. local foundrc = false
  165. -- Check return codes
  166. for s,i in pairs(rbl.returncodes) do
  167. for _,v in ipairs(i) do
  168. if string.find(ipstr, '^' .. v .. '$') then
  169. foundrc = true
  170. task:insert_result(s, 1, to_resolve .. ' : ' .. ipstr)
  171. break
  172. end
  173. end
  174. end
  175. if not foundrc then
  176. if rbl.unknown and rbl.symbol then
  177. task:insert_result(rbl.symbol, 1, to_resolve)
  178. else
  179. rspamd_logger.errx(task, 'RBL %1 returned unknown result: %2',
  180. rbl.rbl, ipstr)
  181. end
  182. end
  183. end
  184. end
  185. local function gen_rbl_callback(rule)
  186. -- Here, we have functional approach: we form a pipeline of functions
  187. -- f1, f2, ... fn. Each function accepts task and return boolean value
  188. -- that allows to process pipeline further
  189. -- Each function in the pipeline can add something to `dns_req` vector as a side effect
  190. local function add_dns_request(req, forced, requests_table)
  191. if requests_table[req] then
  192. -- Duplicate request
  193. if forced and not requests_table[req].forced then
  194. requests_table[req].forced = true
  195. end
  196. else
  197. local nreq = {
  198. forced = forced,
  199. n = string.format('%s.%s',
  200. maybe_make_hash(req, rule),
  201. rule.rbl)
  202. }
  203. requests_table[req] = nreq
  204. end
  205. end
  206. local function is_alive(_, _)
  207. if rule.monitored then
  208. if not rule.monitored:alive() then
  209. return false
  210. end
  211. end
  212. return true
  213. end
  214. local function check_user(task, _)
  215. if task:get_user() then
  216. return false
  217. end
  218. return true
  219. end
  220. local function check_local(task, _)
  221. local ip = task:get_from_ip()
  222. if not ip:is_valid() then
  223. ip = nil
  224. end
  225. if ip and ip:is_local() or is_excluded_ip(ip) then
  226. return false
  227. end
  228. return true
  229. end
  230. local function check_helo(task, requests_table)
  231. local helo = task:get_helo()
  232. if not helo then
  233. return false
  234. end
  235. add_dns_request(helo, true, requests_table)
  236. end
  237. local function check_dkim(task, requests_table)
  238. local das = task:get_symbol('DKIM_TRACE')
  239. local mime_from_domain
  240. local ret = false
  241. if das and das[1] and das[1].options then
  242. if rule.dkim_match_from then
  243. -- We check merely mime from
  244. mime_from_domain = ((task:get_from('mime') or E)[1] or E).domain
  245. if mime_from_domain then
  246. mime_from_domain = rspamd_util.get_tld(mime_from_domain)
  247. end
  248. end
  249. for _, d in ipairs(das[1].options) do
  250. local domain,result = d:match('^([^%:]*):([%+%-%~])$')
  251. -- We must ignore bad signatures, omg
  252. if domain and result and result == '+' then
  253. if rule.dkim_match_from then
  254. -- We check merely mime from
  255. local domain_tld = domain
  256. if not rule.dkim_domainonly then
  257. -- Adjust
  258. domain_tld = rspamd_util.get_tld(domain)
  259. end
  260. if mime_from_domain and mime_from_domain == domain_tld then
  261. add_dns_request(domain_tld, true, requests_table)
  262. ret = true
  263. end
  264. else
  265. if rule.dkim_domainonly then
  266. add_dns_request(rspamd_util.get_tld(domain), false, requests_table)
  267. ret = true
  268. else
  269. add_dns_request(domain, false, requests_table)
  270. ret = true
  271. end
  272. end
  273. end
  274. end
  275. end
  276. return ret
  277. end
  278. local function check_emails(task, requests_table)
  279. local emails = task:get_emails()
  280. if not emails then
  281. return false
  282. end
  283. for _,email in ipairs(emails) do
  284. if rule.emails_domainonly then
  285. add_dns_request(email:get_tld(), fale, requests_table)
  286. else
  287. if rule.hash then
  288. -- Leave @ as is
  289. add_dns_request(string.format('%s@%s',
  290. email:get_user(), email:get_domain()), false, requests_table)
  291. else
  292. -- Replace @ with .
  293. add_dns_request(string.format('%s.%s',
  294. email:get_user(), email:get_domain()), false, requests_table)
  295. end
  296. end
  297. end
  298. return true
  299. end
  300. local function check_from(task, requests_table)
  301. local ip = task:get_from_ip()
  302. if not ip or not ip:is_valid() then
  303. return true
  304. end
  305. if (ip:get_version() == 6 and rule.ipv6) or
  306. (ip:get_version() == 4 and rule.ipv4) then
  307. add_dns_request(ip_to_rbl(ip), true, requests_table)
  308. end
  309. return true
  310. end
  311. local function check_received(task, requests_table)
  312. local received = fun.filter(function(h)
  313. return not h['flags']['artificial']
  314. end, task:get_received_headers()):totable()
  315. local received_total = #received
  316. local check_conditions = gen_check_rcvd_conditions(rule, received_total)
  317. for pos,rh in ipairs(received) do
  318. if check_conditions(rh, pos) then
  319. add_dns_request(ip_to_rbl(rh.real_ip), false, requests_table)
  320. end
  321. end
  322. return true
  323. end
  324. local function check_rdns(task, requests_table)
  325. local hostname = task:get_hostname()
  326. if hostname == nil or hostname == 'unknown' then
  327. return false
  328. end
  329. add_dns_request(hostname, true, requests_table)
  330. return true
  331. end
  332. -- Create function pipeline depending on rbl settings
  333. local pipeline = {
  334. is_alive, -- generic for all
  335. }
  336. if rule.exclude_users then
  337. pipeline[#pipeline + 1] = check_user
  338. end
  339. if rule.exclude_local or rule.exclude_private_ips then
  340. pipeline[#pipeline + 1] = check_local
  341. end
  342. if rule.helo then
  343. pipeline[#pipeline + 1] = check_helo
  344. end
  345. if rule.dkim then
  346. pipeline[#pipeline + 1] = check_dkim
  347. end
  348. if rule.emails then
  349. pipeline[#pipeline + 1] = check_emails
  350. end
  351. if rule.from then
  352. pipeline[#pipeline + 1] = check_from
  353. end
  354. if rule.received then
  355. pipeline[#pipeline + 1] = check_received
  356. end
  357. if rule.rdns then
  358. pipeline[#pipeline + 1] = check_rdns
  359. end
  360. return function(task)
  361. -- DNS requests to issue (might be hashed afterwards)
  362. local dns_req = {}
  363. local function rbl_dns_callback(_, to_resolve, results, err)
  364. rspamd_logger.errx(task, 'dns results: %s', results)
  365. rbl_dns_process(task, rule, to_resolve, results, err)
  366. end
  367. -- Execute functions pipeline
  368. for _,f in ipairs(pipeline) do
  369. if not f(task, dns_req) then
  370. lua_util.debugm(N, task, "skip rbl check: %s; pipeline condition returned false",
  371. rule.symbol)
  372. return
  373. end
  374. end
  375. -- Now check all DNS requests pending and emit them
  376. local r = task:get_resolver()
  377. for name,p in pairs(dns_req) do
  378. if validate_dns(p.n) then
  379. lua_util.debugm(N, task, "rbl %s; resolve %s -> %s",
  380. rule.symbol, name, p.n)
  381. r:resolve_a({
  382. task = task,
  383. name = p.n,
  384. callback = rbl_dns_callback,
  385. forced = p.forced
  386. })
  387. else
  388. rspamd_logger.warnx(task, 'cannot send invalid DNS request %s for %s',
  389. p.n, rule.symbol)
  390. end
  391. end
  392. end
  393. end
  394. -- Configuration
  395. local opts = rspamd_config:get_all_opt(N)
  396. if not (opts and type(opts) == 'table') then
  397. rspamd_logger.infox(rspamd_config, 'Module is unconfigured')
  398. lua_util.disable_module(N, "config")
  399. return
  400. end
  401. -- Plugin defaults should not be changed - override these in config
  402. -- New defaults should not alter behaviour
  403. local default_defaults = {
  404. ['default_enabled'] = true,
  405. ['default_ipv4'] = true,
  406. ['default_ipv6'] = true,
  407. ['default_received'] = false,
  408. ['default_from'] = true,
  409. ['default_unknown'] = false,
  410. ['default_rdns'] = false,
  411. ['default_helo'] = false,
  412. ['default_dkim'] = false,
  413. ['default_dkim_domainonly'] = true,
  414. ['default_emails'] = false,
  415. ['default_emails_domainonly'] = false,
  416. ['default_exclude_private_ips'] = true,
  417. ['default_exclude_users'] = false,
  418. ['default_exclude_local'] = true,
  419. ['default_is_whitelist'] = false,
  420. ['default_ignore_whitelist'] = false,
  421. }
  422. -- Enrich with defaults
  423. for default, default_v in pairs(default_defaults) do
  424. if opts[default] == nil then
  425. opts[default] = default_v
  426. end
  427. end
  428. if(opts['local_exclude_ip_map'] ~= nil) then
  429. local_exclusions = rspamd_map_add(N, 'local_exclude_ip_map', 'radix',
  430. 'RBL exclusions map')
  431. end
  432. local white_symbols = {}
  433. local black_symbols = {}
  434. local rule_schema = ts.shape({
  435. enabled = ts.boolean:is_optional(),
  436. disabled = ts.boolean:is_optional(),
  437. rbl = ts.string,
  438. symbol = ts.string:is_optional(),
  439. returncodes = ts.map_of(
  440. ts.string / string.upper,
  441. (
  442. ts.array_of(ts.string) + (ts.string / function(s)
  443. return { s }
  444. end)
  445. )
  446. ):is_optional(),
  447. whitelist_exception = (
  448. ts.array_of(ts.string) + (ts.string / function(s) return {s} end)
  449. ):is_optional(),
  450. local_exclude_ip_map = ts.string:is_optional(),
  451. hash = ts.one_of{"sha1", "sha256", "sha384", "sha512", "md5", "blake2"}:is_optional(),
  452. hash_format = ts.one_of{"hex", "base32", "base64"}:is_optional(),
  453. hash_len = (ts.integer + ts.string / tonumber):is_optional(),
  454. }, {
  455. extra_fields = ts.map_of(ts.string, ts.boolean)
  456. })
  457. local monitored_addresses = {}
  458. local function add_rbl(key, rbl)
  459. if not rbl.symbol then
  460. rbl.symbol = key:upper()
  461. end
  462. local flags_tbl = {}
  463. if rbl.is_whitelist then
  464. flags_tbl[#flags_tbl + 1] = 'nice'
  465. end
  466. if not (rbl.dkim or rbl.emails) then
  467. flags_tbl[#flags_tbl + 1] = 'empty'
  468. end
  469. local id = rspamd_config:register_symbol{
  470. type = 'callback',
  471. callback = gen_rbl_callback(rbl),
  472. name = rbl.symbol,
  473. flags = table.concat(flags_tbl, ',')
  474. }
  475. if rbl.dkim then
  476. rspamd_config:register_dependency(rbl.symbol, 'DKIM_CHECK')
  477. end
  478. if rbl.returncodes then
  479. for s,_ in pairs(rbl['returncodes']) do
  480. rspamd_config:register_symbol({
  481. name = s,
  482. parent = id,
  483. type = 'virtual'
  484. })
  485. if rbl.is_whitelist then
  486. if rbl.whitelist_exception then
  487. local foundException = false
  488. for _, e in ipairs(rbl.whitelist_exception) do
  489. if e == s then
  490. foundException = true
  491. break
  492. end
  493. end
  494. if not foundException then
  495. table.insert(white_symbols, s)
  496. end
  497. else
  498. table.insert(white_symbols, s)
  499. end
  500. else
  501. if rbl.ignore_whitelist == false then
  502. table.insert(black_symbols, s)
  503. end
  504. end
  505. end
  506. end
  507. if not rbl.is_whitelist and rbl.ignore_whitelist == false then
  508. table.insert(black_symbols, rbl.symbol)
  509. end
  510. -- Process monitored
  511. if not rbl.disable_monitoring and not rbl.is_whitelist then
  512. if not monitored_addresses[rbl.rbl] then
  513. monitored_addresses[rbl.rbl] = true
  514. rbl.monitored = rspamd_config:register_monitored(rbl['rbl'], 'dns',
  515. {
  516. rcode = 'nxdomain',
  517. prefix = rbl.monitored_address or default_monitored
  518. })
  519. end
  520. end
  521. end
  522. for key,rbl in pairs(opts.rbls or opts.rules) do
  523. if type(rbl) ~= 'table' or rbl.disabled == true or rbl.enabled == false then
  524. rspamd_logger.infox(rspamd_config, 'disable rbl "%s"', key)
  525. else
  526. for default,_ in pairs(default_defaults) do
  527. local rbl_opt = default:sub(#('default_') + 1)
  528. if rbl[rbl_opt] == nil then
  529. rbl[rbl_opt] = opts[default]
  530. end
  531. end
  532. local res,err = rule_schema:transform(rbl)
  533. if not res then
  534. rspamd_logger.errx(rspamd_config, 'invalid config for %s: %s, RBL is DISABLED',
  535. key, err)
  536. else
  537. add_rbl(key, res)
  538. end
  539. end -- rbl.enabled
  540. end
  541. -- We now create two symbols:
  542. -- * RBL_CALLBACK_WHITE that depends on all symbols white
  543. -- * RBL_CALLBACK that depends on all symbols black to participate in depends chains
  544. local function rbl_callback_white(task)
  545. local found_whitelist = false
  546. for _, w in ipairs(white_symbols) do
  547. if task:has_symbol(w) then
  548. lua_util.debugm(N, task,'found whitelist %s', w)
  549. found_whitelist = true
  550. break
  551. end
  552. end
  553. if found_whitelist then
  554. -- Disable all symbols black
  555. for _, b in ipairs(black_symbols) do
  556. lua_util.debugm(N, task,'disable %s, whitelist found', b)
  557. task:disable_symbol(b)
  558. end
  559. end
  560. lua_util.debugm(N, task, "finished rbl whitelists processing")
  561. end
  562. local function rbl_callback_fin(task)
  563. -- Do nothing
  564. lua_util.debugm(N, task, "finished rbl processing")
  565. end
  566. rspamd_config:register_symbol{
  567. type = 'callback',
  568. callback = rbl_callback_white,
  569. name = 'RBL_CALLBACK_WHITE',
  570. flags = 'nice,empty'
  571. }
  572. rspamd_config:register_symbol{
  573. type = 'callback',
  574. callback = rbl_callback_fin,
  575. name = 'RBL_CALLBACK',
  576. flags = 'empty'
  577. }
  578. for _, w in ipairs(white_symbols) do
  579. rspamd_config:register_dependency('RBL_CALLBACK_WHITE', w)
  580. end
  581. for _, b in ipairs(black_symbols) do
  582. rspamd_config:register_dependency(b, 'RBL_CALLBACK_WHITE')
  583. rspamd_config:register_dependency('RBL_CALLBACK', b)
  584. end