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.

rbl.lua 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  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. local bit = require 'bit'
  24. -- This plugin implements various types of RBL checks
  25. -- Documentation can be found here:
  26. -- https://rspamd.com/doc/modules/rbl.html
  27. local E = {}
  28. local N = 'rbl'
  29. local local_exclusions
  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, orig)
  143. if err and (err ~= 'requested record is not found' and
  144. err ~= 'no records with this name') then
  145. rspamd_logger.infox(task, 'error looking up %s: %s', to_resolve, err)
  146. task:insert_result(rbl.symbol .. '_FAIL', 1, string.format('%s:%s',
  147. orig, err))
  148. return
  149. end
  150. if not results then
  151. lua_util.debugm(N, task,
  152. 'DNS RESPONSE: label=%1 results=%2 error=%3 rbl=%4',
  153. to_resolve, false, err, rbl.symbol)
  154. return
  155. else
  156. lua_util.debugm(N, task,
  157. 'DNS RESPONSE: label=%1 results=%2 error=%3 rbl=%4',
  158. to_resolve, true, err, rbl.symbol)
  159. end
  160. if rbl.returncodes == nil and rbl.returnbits == nil and rbl.symbol ~= nil then
  161. task:insert_result(rbl.symbol, 1, orig)
  162. return
  163. end
  164. for _,result in ipairs(results) do
  165. local ipstr = result:to_string()
  166. lua_util.debugm(N, task, '%s DNS result %s', to_resolve, ipstr)
  167. local foundrc = false
  168. -- Check return codes
  169. if rbl.returnbits then
  170. local ipnum = result:to_number()
  171. for s,bits in pairs(rbl.returnbits) do
  172. for _,check_bit in ipairs(bits) do
  173. if bit.band(ipnum, check_bit) == check_bit then
  174. foundrc = true
  175. task:insert_result(s, 1, orig .. ' : ' .. ipstr)
  176. -- Here, we continue with other bits
  177. end
  178. end
  179. end
  180. elseif rbl.returncodes then
  181. for s, codes in pairs(rbl.returncodes) do
  182. for _,v in ipairs(codes) do
  183. if string.find(ipstr, '^' .. v .. '$') then
  184. foundrc = true
  185. task:insert_result(s, 1, orig .. ' : ' .. ipstr)
  186. break
  187. end
  188. end
  189. end
  190. end
  191. if not foundrc then
  192. if rbl.unknown and rbl.symbol then
  193. task:insert_result(rbl.symbol, 1, orig)
  194. else
  195. rspamd_logger.errx(task, 'RBL %1 returned unknown result: %2',
  196. rbl.rbl, ipstr)
  197. end
  198. end
  199. end
  200. end
  201. local function gen_rbl_callback(rule)
  202. -- Here, we have functional approach: we form a pipeline of functions
  203. -- f1, f2, ... fn. Each function accepts task and return boolean value
  204. -- that allows to process pipeline further
  205. -- Each function in the pipeline can add something to `dns_req` vector as a side effect
  206. local function add_dns_request(req, forced, requests_table)
  207. if requests_table[req] then
  208. -- Duplicate request
  209. if forced and not requests_table[req].forced then
  210. requests_table[req].forced = true
  211. end
  212. else
  213. local orign = maybe_make_hash(req, rule)
  214. local nreq = {
  215. forced = forced,
  216. n = string.format('%s.%s',
  217. orign,
  218. rule.rbl),
  219. orig = orign
  220. }
  221. requests_table[req] = nreq
  222. end
  223. end
  224. local function is_alive(_, _)
  225. if rule.monitored then
  226. if not rule.monitored:alive() then
  227. return false
  228. end
  229. end
  230. return true
  231. end
  232. local function check_user(task, _)
  233. if task:get_user() then
  234. return false
  235. end
  236. return true
  237. end
  238. local function check_local(task, _)
  239. local ip = task:get_from_ip()
  240. if not ip:is_valid() then
  241. ip = nil
  242. end
  243. if ip and ip:is_local() or is_excluded_ip(ip) then
  244. return false
  245. end
  246. return true
  247. end
  248. local function check_helo(task, requests_table)
  249. local helo = task:get_helo()
  250. if not helo then
  251. return false
  252. end
  253. add_dns_request(helo, true, requests_table)
  254. end
  255. local function check_dkim(task, requests_table)
  256. local das = task:get_symbol('DKIM_TRACE')
  257. local mime_from_domain
  258. local ret = false
  259. if das and das[1] and das[1].options then
  260. if rule.dkim_match_from then
  261. -- We check merely mime from
  262. mime_from_domain = ((task:get_from('mime') or E)[1] or E).domain
  263. if mime_from_domain then
  264. mime_from_domain = rspamd_util.get_tld(mime_from_domain)
  265. end
  266. end
  267. for _, d in ipairs(das[1].options) do
  268. local domain,result = d:match('^([^%:]*):([%+%-%~])$')
  269. -- We must ignore bad signatures, omg
  270. if domain and result and result == '+' then
  271. if rule.dkim_match_from then
  272. -- We check merely mime from
  273. local domain_tld = domain
  274. if not rule.dkim_domainonly then
  275. -- Adjust
  276. domain_tld = rspamd_util.get_tld(domain)
  277. end
  278. if mime_from_domain and mime_from_domain == domain_tld then
  279. add_dns_request(domain_tld, true, requests_table)
  280. ret = true
  281. end
  282. else
  283. if rule.dkim_domainonly then
  284. add_dns_request(rspamd_util.get_tld(domain), false, requests_table)
  285. ret = true
  286. else
  287. add_dns_request(domain, false, requests_table)
  288. ret = true
  289. end
  290. end
  291. end
  292. end
  293. end
  294. return ret
  295. end
  296. local function check_emails(task, requests_table)
  297. local emails = task:get_emails()
  298. if not emails then
  299. return false
  300. end
  301. for _,email in ipairs(emails) do
  302. if rule.emails_domainonly then
  303. add_dns_request(email:get_tld(), false, requests_table)
  304. else
  305. if rule.hash then
  306. -- Leave @ as is
  307. add_dns_request(string.format('%s@%s',
  308. email:get_user(), email:get_host()), false, requests_table)
  309. else
  310. -- Replace @ with .
  311. add_dns_request(string.format('%s.%s',
  312. email:get_user(), email:get_host()), false, requests_table)
  313. end
  314. end
  315. end
  316. return true
  317. end
  318. local function check_from(task, requests_table)
  319. local ip = task:get_from_ip()
  320. if not ip or not ip:is_valid() then
  321. return true
  322. end
  323. if (ip:get_version() == 6 and rule.ipv6) or
  324. (ip:get_version() == 4 and rule.ipv4) then
  325. add_dns_request(ip_to_rbl(ip), true, requests_table)
  326. end
  327. return true
  328. end
  329. local function check_received(task, requests_table)
  330. local received = fun.filter(function(h)
  331. return not h['flags']['artificial']
  332. end, task:get_received_headers()):totable()
  333. local received_total = #received
  334. local check_conditions = gen_check_rcvd_conditions(rule, received_total)
  335. for pos,rh in ipairs(received) do
  336. if check_conditions(rh, pos) then
  337. add_dns_request(ip_to_rbl(rh.real_ip), false, requests_table)
  338. end
  339. end
  340. return true
  341. end
  342. local function check_rdns(task, requests_table)
  343. local hostname = task:get_hostname()
  344. if hostname == nil or hostname == 'unknown' then
  345. return false
  346. end
  347. add_dns_request(hostname, true, requests_table)
  348. return true
  349. end
  350. -- Create function pipeline depending on rbl settings
  351. local pipeline = {
  352. is_alive, -- generic for all
  353. }
  354. if rule.exclude_users then
  355. pipeline[#pipeline + 1] = check_user
  356. end
  357. if rule.exclude_local or rule.exclude_private_ips then
  358. pipeline[#pipeline + 1] = check_local
  359. end
  360. if rule.helo then
  361. pipeline[#pipeline + 1] = check_helo
  362. end
  363. if rule.dkim then
  364. pipeline[#pipeline + 1] = check_dkim
  365. end
  366. if rule.emails then
  367. pipeline[#pipeline + 1] = check_emails
  368. end
  369. if rule.from then
  370. pipeline[#pipeline + 1] = check_from
  371. end
  372. if rule.received then
  373. pipeline[#pipeline + 1] = check_received
  374. end
  375. if rule.rdns then
  376. pipeline[#pipeline + 1] = check_rdns
  377. end
  378. return function(task)
  379. -- DNS requests to issue (might be hashed afterwards)
  380. local dns_req = {}
  381. local function gen_rbl_dns_callback(orig)
  382. return function(_, to_resolve, results, err)
  383. rbl_dns_process(task, rule, to_resolve, results, err, orig)
  384. end
  385. end
  386. -- Execute functions pipeline
  387. for _,f in ipairs(pipeline) do
  388. if not f(task, dns_req) then
  389. lua_util.debugm(N, task, "skip rbl check: %s; pipeline condition returned false",
  390. rule.symbol)
  391. return
  392. end
  393. end
  394. -- Now check all DNS requests pending and emit them
  395. local r = task:get_resolver()
  396. for name,p in pairs(dns_req) do
  397. if validate_dns(p.n) then
  398. lua_util.debugm(N, task, "rbl %s; resolve %s -> %s",
  399. rule.symbol, name, p.n)
  400. r:resolve_a({
  401. task = task,
  402. name = p.n,
  403. callback = gen_rbl_dns_callback(p.orig),
  404. forced = p.forced
  405. })
  406. else
  407. rspamd_logger.warnx(task, 'cannot send invalid DNS request %s for %s',
  408. p.n, rule.symbol)
  409. end
  410. end
  411. end
  412. end
  413. -- Configuration
  414. local opts = rspamd_config:get_all_opt(N)
  415. if not (opts and type(opts) == 'table') then
  416. rspamd_logger.infox(rspamd_config, 'Module is unconfigured')
  417. lua_util.disable_module(N, "config")
  418. return
  419. end
  420. -- Plugin defaults should not be changed - override these in config
  421. -- New defaults should not alter behaviour
  422. local default_defaults = {
  423. ['default_enabled'] = true,
  424. ['default_ipv4'] = true,
  425. ['default_ipv6'] = true,
  426. ['default_received'] = false,
  427. ['default_from'] = true,
  428. ['default_unknown'] = false,
  429. ['default_rdns'] = false,
  430. ['default_helo'] = false,
  431. ['default_dkim'] = false,
  432. ['default_dkim_domainonly'] = true,
  433. ['default_emails'] = false,
  434. ['default_emails_domainonly'] = false,
  435. ['default_exclude_private_ips'] = true,
  436. ['default_exclude_users'] = false,
  437. ['default_exclude_local'] = true,
  438. ['default_is_whitelist'] = false,
  439. ['default_ignore_whitelist'] = false,
  440. }
  441. -- Enrich with defaults
  442. for default, default_v in pairs(default_defaults) do
  443. if opts[default] == nil then
  444. opts[default] = default_v
  445. end
  446. end
  447. if(opts['local_exclude_ip_map'] ~= nil) then
  448. local_exclusions = rspamd_map_add(N, 'local_exclude_ip_map', 'radix',
  449. 'RBL exclusions map')
  450. end
  451. local white_symbols = {}
  452. local black_symbols = {}
  453. local rule_schema = ts.shape({
  454. enabled = ts.boolean:is_optional(),
  455. disabled = ts.boolean:is_optional(),
  456. rbl = ts.string,
  457. symbol = ts.string:is_optional(),
  458. returncodes = ts.map_of(
  459. ts.string / string.upper, -- Symbol name
  460. (
  461. ts.array_of(ts.string) +
  462. (ts.string / function(s)
  463. return { s }
  464. end) -- List of IP patterns
  465. )
  466. ):is_optional(),
  467. returnbits = ts.map_of(
  468. ts.string / string.upper, -- Symbol name
  469. (
  470. ts.array_of(ts.number + ts.string / tonumber) +
  471. (ts.string / function(s)
  472. return { tonumber(s) }
  473. end) +
  474. (ts.number / function(s)
  475. return { s }
  476. end)
  477. )
  478. ):is_optional(),
  479. whitelist_exception = (
  480. ts.array_of(ts.string) + (ts.string / function(s) return {s} end)
  481. ):is_optional(),
  482. local_exclude_ip_map = ts.string:is_optional(),
  483. hash = ts.one_of{"sha1", "sha256", "sha384", "sha512", "md5", "blake2"}:is_optional(),
  484. hash_format = ts.one_of{"hex", "base32", "base64"}:is_optional(),
  485. hash_len = (ts.integer + ts.string / tonumber):is_optional(),
  486. monitored_address = ts.string:is_optional(),
  487. }, {
  488. extra_fields = ts.map_of(ts.string, ts.boolean)
  489. })
  490. local monitored_addresses = {}
  491. local function get_monitored(rbl)
  492. local default_monitored = '1.0.0.127'
  493. if rbl.monitored_address then
  494. return rbl.monitored_address
  495. end
  496. if rbl.dkim or rbl.url or rbl.email then
  497. default_monitored = 'facebook.com' -- should never be blacklisted
  498. end
  499. return default_monitored
  500. end
  501. local function add_rbl(key, rbl)
  502. if not rbl.symbol then
  503. rbl.symbol = key:upper()
  504. end
  505. local flags_tbl = {'no_squeeze'}
  506. if rbl.is_whitelist then
  507. flags_tbl[#flags_tbl + 1] = 'nice'
  508. end
  509. if not (rbl.dkim or rbl.emails or rbl.received) then
  510. flags_tbl[#flags_tbl + 1] = 'empty'
  511. end
  512. local id = rspamd_config:register_symbol{
  513. type = 'callback',
  514. callback = gen_rbl_callback(rbl),
  515. name = rbl.symbol,
  516. flags = table.concat(flags_tbl, ',')
  517. }
  518. if rbl.dkim then
  519. rspamd_config:register_dependency(rbl.symbol, 'DKIM_CHECK')
  520. end
  521. -- Failure symbol
  522. rspamd_config:register_symbol{
  523. type = 'virtual,nostat',
  524. name = rbl.symbol .. '_FAIL',
  525. parent = id,
  526. score = 0.0,
  527. }
  528. local function process_return_code(s)
  529. rspamd_config:register_symbol({
  530. name = s,
  531. parent = id,
  532. type = 'virtual'
  533. })
  534. if rbl.is_whitelist then
  535. if rbl.whitelist_exception then
  536. local found_exception = false
  537. for _, e in ipairs(rbl.whitelist_exception) do
  538. if e == s then
  539. found_exception = true
  540. break
  541. end
  542. end
  543. if not found_exception then
  544. table.insert(white_symbols, s)
  545. end
  546. else
  547. table.insert(white_symbols, s)
  548. end
  549. else
  550. if rbl.ignore_whitelist == false then
  551. table.insert(black_symbols, s)
  552. end
  553. end
  554. end
  555. if rbl.returncodes then
  556. for s,_ in pairs(rbl.returncodes) do
  557. process_return_code(s)
  558. end
  559. end
  560. if rbl.returnbits then
  561. for s,_ in pairs(rbl.returnbits) do
  562. process_return_code(s)
  563. end
  564. end
  565. if not rbl.is_whitelist and rbl.ignore_whitelist == false then
  566. table.insert(black_symbols, rbl.symbol)
  567. end
  568. -- Process monitored
  569. if not rbl.disable_monitoring and not rbl.is_whitelist then
  570. if not monitored_addresses[rbl.rbl] then
  571. monitored_addresses[rbl.rbl] = true
  572. rbl.monitored = rspamd_config:register_monitored(rbl.rbl, 'dns',
  573. {
  574. rcode = 'nxdomain',
  575. prefix = get_monitored(rbl)
  576. })
  577. end
  578. end
  579. end
  580. for key,rbl in pairs(opts.rbls or opts.rules) do
  581. if type(rbl) ~= 'table' or rbl.disabled == true or rbl.enabled == false then
  582. rspamd_logger.infox(rspamd_config, 'disable rbl "%s"', key)
  583. else
  584. for default,_ in pairs(default_defaults) do
  585. local rbl_opt = default:sub(#('default_') + 1)
  586. if rbl[rbl_opt] == nil then
  587. rbl[rbl_opt] = opts[default]
  588. end
  589. end
  590. local res,err = rule_schema:transform(rbl)
  591. if not res then
  592. rspamd_logger.errx(rspamd_config, 'invalid config for %s: %s, RBL is DISABLED',
  593. key, err)
  594. else
  595. add_rbl(key, res)
  596. end
  597. end -- rbl.enabled
  598. end
  599. -- We now create two symbols:
  600. -- * RBL_CALLBACK_WHITE that depends on all symbols white
  601. -- * RBL_CALLBACK that depends on all symbols black to participate in depends chains
  602. local function rbl_callback_white(task)
  603. local found_whitelist = false
  604. for _, w in ipairs(white_symbols) do
  605. if task:has_symbol(w) then
  606. lua_util.debugm(N, task,'found whitelist %s', w)
  607. found_whitelist = true
  608. break
  609. end
  610. end
  611. if found_whitelist then
  612. -- Disable all symbols black
  613. for _, b in ipairs(black_symbols) do
  614. lua_util.debugm(N, task,'disable %s, whitelist found', b)
  615. task:disable_symbol(b)
  616. end
  617. end
  618. lua_util.debugm(N, task, "finished rbl whitelists processing")
  619. end
  620. local function rbl_callback_fin(task)
  621. -- Do nothing
  622. lua_util.debugm(N, task, "finished rbl processing")
  623. end
  624. rspamd_config:register_symbol{
  625. type = 'callback',
  626. callback = rbl_callback_white,
  627. name = 'RBL_CALLBACK_WHITE',
  628. flags = 'nice,empty,no_squeeze'
  629. }
  630. rspamd_config:register_symbol{
  631. type = 'callback',
  632. callback = rbl_callback_fin,
  633. name = 'RBL_CALLBACK',
  634. flags = 'empty,no_squeeze'
  635. }
  636. for _, w in ipairs(white_symbols) do
  637. rspamd_config:register_dependency('RBL_CALLBACK_WHITE', w)
  638. end
  639. for _, b in ipairs(black_symbols) do
  640. rspamd_config:register_dependency(b, 'RBL_CALLBACK_WHITE')
  641. rspamd_config:register_dependency('RBL_CALLBACK', b)
  642. end