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.

spf.lua 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. --[[
  2. Copyright (c) 2022, 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. local N = "spf"
  14. local lua_util = require "lua_util"
  15. local rspamd_spf = require "rspamd_spf"
  16. local bit = require "bit"
  17. local rspamd_logger = require "rspamd_logger"
  18. if confighelp then
  19. rspamd_config:add_example(nil, N,
  20. 'Performs SPF checks',
  21. [[
  22. spf {
  23. # Enable module
  24. enabled = true
  25. # Number of elements in the cache of parsed SPF records
  26. spf_cache_size = 2048;
  27. # Default max expire for an element in this cache
  28. spf_cache_expire = 1d;
  29. # Whitelist IPs from checks
  30. whitelist = "/path/to/some/file";
  31. # Maximum number of recursive DNS subrequests (e.g. includes chanin length)
  32. max_dns_nesting = 10;
  33. # Maximum count of DNS requests per record
  34. max_dns_requests = 30;
  35. # Minimum TTL enforced for all elements in SPF records
  36. min_cache_ttl = 5min;
  37. # Disable all IPv6 lookups
  38. disable_ipv6 = false;
  39. # Use IP address from a received header produced by this relay (using by attribute)
  40. external_relay = ["192.168.1.1"];
  41. }
  42. ]])
  43. return
  44. end
  45. local symbols = {
  46. fail = "R_SPF_FAIL",
  47. softfail = "R_SPF_SOFTFAIL",
  48. neutral = "R_SPF_NEUTRAL",
  49. allow = "R_SPF_ALLOW",
  50. dnsfail = "R_SPF_DNSFAIL",
  51. permfail = "R_SPF_PERMFAIL",
  52. na = "R_SPF_NA",
  53. }
  54. local default_config = {
  55. spf_cache_size = 2048,
  56. max_dns_nesting = 10,
  57. max_dns_requests = 30,
  58. whitelist = nil,
  59. min_cache_ttl = 60 * 5,
  60. disable_ipv6 = false,
  61. symbols = symbols,
  62. external_relay = nil,
  63. }
  64. local local_config = rspamd_config:get_all_opt('spf')
  65. local auth_and_local_conf = lua_util.config_check_local_or_authed(rspamd_config, N,
  66. false, false)
  67. if local_config then
  68. local_config = lua_util.override_defaults(default_config, local_config)
  69. else
  70. local_config = default_config
  71. end
  72. local function spf_check_callback(task)
  73. local ip
  74. if local_config.external_relay then
  75. -- Search received headers to get header produced by an external relay
  76. local rh = task:get_received_headers() or {}
  77. local found = false
  78. for i, hdr in ipairs(rh) do
  79. if hdr.real_ip and local_config.external_relay:get_key(hdr.real_ip) then
  80. -- We can use the next header as a source of IP address
  81. if rh[i + 1] then
  82. local nhdr = rh[i + 1]
  83. lua_util.debugm(N, task, 'found external relay %s at received header number %s -> %s',
  84. local_config.external_relay, i, nhdr.real_ip)
  85. if nhdr.real_ip then
  86. ip = nhdr.real_ip
  87. found = true
  88. end
  89. end
  90. break
  91. end
  92. end
  93. if not found then
  94. ip = task:get_from_ip()
  95. rspamd_logger.warnx(task,
  96. "cannot find external relay for SPF checks in received headers; use the original IP: %s",
  97. tostring(ip))
  98. end
  99. else
  100. ip = task:get_from_ip()
  101. end
  102. local function flag_to_symbol(fl)
  103. if bit.band(fl, rspamd_spf.flags.temp_fail) ~= 0 then
  104. return local_config.symbols.dnsfail
  105. elseif bit.band(fl, rspamd_spf.flags.perm_fail) ~= 0 then
  106. return local_config.symbols.permfail
  107. elseif bit.band(fl, rspamd_spf.flags.na) ~= 0 then
  108. return local_config.symbols.na
  109. end
  110. return 'SPF_UNKNOWN'
  111. end
  112. local function policy_decode(res)
  113. if res == rspamd_spf.policy.fail then
  114. return local_config.symbols.fail, '-'
  115. elseif res == rspamd_spf.policy.pass then
  116. return local_config.symbols.allow, '+'
  117. elseif res == rspamd_spf.policy.soft_fail then
  118. return local_config.symbols.softfail, '~'
  119. elseif res == rspamd_spf.policy.neutral then
  120. return local_config.symbols.neutral, '?'
  121. end
  122. return 'SPF_UNKNOWN', '?'
  123. end
  124. local function spf_resolved_cb(record, flags, err)
  125. lua_util.debugm(N, task, 'got spf results: %s flags, %s err',
  126. flags, err)
  127. if record then
  128. local result, flag_or_policy, error_or_addr = record:check_ip(ip)
  129. lua_util.debugm(N, task,
  130. 'checked ip %s: result=%s, flag_or_policy=%s, error_or_addr=%s',
  131. ip, flags, err, error_or_addr)
  132. if result then
  133. local sym, code = policy_decode(flag_or_policy)
  134. local opt = string.format('%s%s', code, error_or_addr.str or '???')
  135. if bit.band(flags, rspamd_spf.flags.cached) ~= 0 then
  136. opt = opt .. ':c'
  137. rspamd_logger.infox(task,
  138. "use cached record for %s (0x%s) in LRU cache for %s seconds",
  139. record:get_domain(),
  140. record:get_digest(),
  141. record:get_ttl() - math.floor(task:get_timeval(true) -
  142. record:get_timestamp()));
  143. end
  144. task:insert_result(sym, 1.0, opt)
  145. else
  146. local sym = flag_to_symbol(flag_or_policy)
  147. task:insert_result(sym, 1.0, error_or_addr)
  148. end
  149. else
  150. local sym = flag_to_symbol(flags)
  151. task:insert_result(sym, 1.0, err)
  152. end
  153. end
  154. if ip then
  155. if local_config.whitelist and ip and local_config.whitelist:get_key(ip) then
  156. rspamd_logger.infox(task, 'whitelisted SPF checks from %s',
  157. tostring(ip))
  158. return
  159. end
  160. if lua_util.is_skip_local_or_authed(task, auth_and_local_conf, ip) then
  161. rspamd_logger.infox(task, 'skip SPF checks for local networks and authorized users')
  162. return
  163. end
  164. rspamd_spf.resolve(task, spf_resolved_cb)
  165. else
  166. lua_util.debugm(N, task, "spf checks are not possible as no source IP address is defined")
  167. end
  168. -- FIXME: we actually need to set this variable when we really checked SPF
  169. -- However, the old C module has set it all the times
  170. -- Hence, we follow the same rule for now. It should be better designed at some day
  171. local mpool = task:get_mempool()
  172. local dmarc_checks = mpool:get_variable('dmarc_checks', 'double') or 0
  173. dmarc_checks = dmarc_checks + 1
  174. mpool:set_variable('dmarc_checks', dmarc_checks)
  175. end
  176. -- Register all symbols and init rspamd_spf library
  177. rspamd_spf.config(local_config)
  178. local sym_id = rspamd_config:register_symbol {
  179. name = 'SPF_CHECK',
  180. type = 'callback',
  181. flags = 'fine,empty',
  182. groups = { 'policies', 'spf' },
  183. score = 0.0,
  184. callback = spf_check_callback,
  185. -- We can merely estimate timeout here, as it is possible to construct an SPF record that would cause
  186. -- many DNS requests. But we won't like to set the maximum value for that all the time, as
  187. -- the majority of requests will typically have 1-4 subrequests
  188. augmentations = { string.format("timeout=%f", rspamd_config:get_dns_timeout() * 4 or 0.0) },
  189. }
  190. if local_config.whitelist then
  191. local lua_maps = require "lua_maps"
  192. local_config.whitelist = lua_maps.map_add_from_ucl(local_config.whitelist,
  193. "radix", "SPF whitelist map")
  194. end
  195. if local_config.external_relay then
  196. local lua_maps = require "lua_maps"
  197. local_config.external_relay = lua_maps.map_add_from_ucl(local_config.external_relay,
  198. "radix", "External IP SPF map")
  199. end
  200. for _, sym in pairs(local_config.symbols) do
  201. rspamd_config:register_symbol {
  202. name = sym,
  203. type = 'virtual',
  204. parent = sym_id,
  205. groups = { 'policies', 'spf' },
  206. }
  207. end