aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins
diff options
context:
space:
mode:
authorVsevolod Stakhov <vsevolod@highsecure.ru>2018-10-12 16:44:21 +0100
committerVsevolod Stakhov <vsevolod@highsecure.ru>2018-10-12 16:44:21 +0100
commita2ea0822d2bf76a1f77dfb9bbd48d6a946c82565 (patch)
treef99a9ad9a0ca3d5c14816fdfdd4d507900bdb701 /src/plugins
parentc5721acc86dc48a171684441699d2a960c102d53 (diff)
downloadrspamd-a2ea0822d2bf76a1f77dfb9bbd48d6a946c82565.tar.gz
rspamd-a2ea0822d2bf76a1f77dfb9bbd48d6a946c82565.zip
[Rework] Completely rewrite DMARC checks logic
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/lua/dmarc.lua719
1 files changed, 423 insertions, 296 deletions
diff --git a/src/plugins/lua/dmarc.lua b/src/plugins/lua/dmarc.lua
index 69e210e47..62168addd 100644
--- a/src/plugins/lua/dmarc.lua
+++ b/src/plugins/lua/dmarc.lua
@@ -195,8 +195,8 @@ local function dmarc_report(task, spf_ok, dkim_ok, disposition,
return res
end
-local function dmarc_callback(task)
- local function maybe_force_action(disposition)
+local function maybe_force_action(task, disposition)
+ if disposition then
local force_action = dmarc_actions[disposition]
if force_action then
-- Don't do anything if pre-result has been already set
@@ -204,363 +204,489 @@ local function dmarc_callback(task)
task:set_pre_result(force_action, 'Action set by DMARC')
end
end
- local from = task:get_from(2)
- local hfromdom = ((from or E)[1] or E).domain
- local dmarc_domain, spf_domain
- local ip_addr = task:get_ip()
- local dkim_results = {}
- local dmarc_checks = task:get_mempool():get_variable('dmarc_checks', 'int') or 0
+end
- if dmarc_checks ~= 2 then
- rspamd_logger.infox(task, "skip DMARC checks as either SPF or DKIM were not checked");
- return
- end
+--[[
+-- Used to check dmarc record, check elements and produce dmarc policy processed
+-- result.
+-- Returns:
+-- false,false - record is garbadge
+-- false,error_message - record is invalid
+-- true,policy_table - record is valid and parsed
+]]
+local function dmarc_check_record(task, record, is_tld)
+ local failed_policy
+ local result = {
+ dmarc_policy = 'none'
+ }
+
+ local elts = dmarc_grammar:match(record)
+ lua_util.debugm(N, task, "got DMARC record: %s, tld_flag=%s, processed=%s",
+ record, is_tld, elts)
+
+ if elts then
+ local dkim_pol = elts['adkim']
+ if dkim_pol then
+ if dkim_pol == 's' then
+ result.strict_dkim = true
+ elseif dkim_pol ~= 'r' then
+ failed_policy = 'adkim tag has invalid value: ' .. dkim_pol
+ return false,failed_policy
+ end
+ end
- if ((not check_authed and task:get_user()) or
- (not check_local and ip_addr and ip_addr:is_local())) then
- rspamd_logger.infox(task, "skip DMARC checks for local networks and authorized users");
- return
- end
- if hfromdom and hfromdom ~= '' and not (from or E)[2] then
- dmarc_domain = rspamd_util.get_tld(hfromdom)
- elseif (from or E)[2] then
- task:insert_result(dmarc_symbols['na'], 1.0, 'Duplicate From header')
- return maybe_force_action('na')
- elseif (from or E)[1] then
- task:insert_result(dmarc_symbols['na'], 1.0, 'No domain in From header')
- return maybe_force_action('na')
+ local spf_pol = elts['aspf']
+ if spf_pol then
+ if spf_pol == 's' then
+ result.strict_spf = true
+ elseif spf_pol ~= 'r' then
+ failed_policy = 'aspf tag has invalid value: ' .. spf_pol
+ return false,failed_policy
+ end
+ end
+
+ local policy = elts['p']
+ if policy then
+ if (policy == 'reject') then
+ result.dmarc_policy = 'reject'
+ elseif (policy == 'quarantine') then
+ result.dmarc_policy = 'quarantine'
+ elseif (policy ~= 'none') then
+ failed_policy = 'p tag has invalid value: ' .. policy
+ return false,failed_policy
+ end
+ end
+
+ -- Adjust policy if we are in tld mode
+ local subdomain_policy = elts['sp']
+ if elts['sp'] and is_tld then
+ result.subdomain_policy = elts['sp']
+
+ if (subdomain_policy == 'reject') then
+ result.dmarc_policy = 'reject'
+ elseif (subdomain_policy == 'quarantine') then
+ result.dmarc_policy = 'quarantine'
+ elseif (subdomain_policy == 'none') then
+ result.dmarc_policy = 'none'
+ elseif (subdomain_policy ~= 'none') then
+ failed_policy = 'sp tag has invalid value: ' .. subdomain_policy
+ return false,failed_policy
+ end
+ end
+ result.pct = elts['pct']
+ if result.pct then
+ result.pct = tonumber(result.pct)
+ end
+
+ if elts.rua then
+ result.rua = elts['rua']
+ end
else
- task:insert_result(dmarc_symbols['na'], 1.0, 'No From header')
- return maybe_force_action('na')
+ return false,false -- Ignore garbadge
end
- local function dmarc_report_cb(err)
- if not err then
- rspamd_logger.infox(task, '<%1> dmarc report saved for %2',
- task:get_message_id(), hfromdom)
+ return true, result
+end
+
+local function dmarc_validate_policy(task, policy, hdrfromdom)
+ local reason = {}
+
+ -- Check dkim and spf symbols
+ local spf_ok = false
+ local dkim_ok = false
+ local spf_tmpfail = false
+ local dkim_tmpfail = false
+
+ local spf_domain = ((task:get_from(1) or E)[1] or E).domain
+
+ if not spf_domain or spf_domain == '' then
+ spf_domain = task:get_helo() or ''
+ end
+
+ if task:has_symbol(symbols['spf_allow_symbol']) then
+ if policy.strict_spf then
+ if rspamd_util.strequal_caseless(spf_domain, hdrfromdom) then
+ spf_ok = true
+ else
+ table.insert(reason, "SPF not aligned (strict)")
+ end
else
- rspamd_logger.errx(task, '<%1> dmarc report is not saved for %2: %3',
- task:get_message_id(), hfromdom, err)
+ local spf_tld = rspamd_util.get_tld(spf_domain)
+ if rspamd_util.strequal_caseless(spf_tld, policy.domain) then
+ spf_ok = true
+ else
+ table.insert(reason, "SPF not aligned (relaxed)")
+ end
end
+ else
+ if task:has_symbol(symbols['spf_tempfail_symbol']) then
+ if policy.strict_spf then
+ if rspamd_util.strequal_caseless(spf_domain, hdrfromdom) then
+ spf_tmpfail = true
+ end
+ else
+ local spf_tld = rspamd_util.get_tld(spf_domain)
+ if rspamd_util.strequal_caseless(spf_tld, policy.domain) then
+ spf_tmpfail = true
+ end
+ end
+ end
+
+ table.insert(reason, "No valid SPF")
end
- local function dmarc_dns_cb(_, to_resolve, results, err)
- local lookup_domain = string.sub(to_resolve, 8)
- if err and (err ~= 'requested record is not found' and err ~= 'no records with this name') then
- task:insert_result(dmarc_symbols['dnsfail'], 1.0, lookup_domain .. ' : ' .. err)
- return maybe_force_action('dnsfail')
- elseif err and (err == 'requested record is not found' or err == 'no records with this name') and
- lookup_domain == dmarc_domain then
- task:insert_result(dmarc_symbols['na'], 1.0, lookup_domain)
- return maybe_force_action('na')
- end
+ local opts = ((task:get_symbol('DKIM_TRACE') or E)[1] or E).options
+ local dkim_results = {
+ pass = {},
+ temperror = {},
+ permerror = {},
+ fail = {},
+ }
- if not results then
- if lookup_domain ~= dmarc_domain then
- local resolve_name = '_dmarc.' .. dmarc_domain
- task:get_resolver():resolve_txt({
- task=task,
- name = resolve_name,
- callback = dmarc_dns_cb,
- forced = true})
- return
- end
- task:insert_result(dmarc_symbols['na'], 1.0, lookup_domain)
- return maybe_force_action('na')
- end
+ if opts then
+ dkim_results.pass = {}
+ local dkim_violated
- local pct
- local reason = {}
- local strict_spf = false
- local strict_dkim = false
- local dmarc_policy = 'none'
- local found_policy = false
- local failed_policy
- local rua
-
- for _,r in ipairs(results) do
- if failed_policy then break end
- local function try()
- local elts = dmarc_grammar:match(r)
- if not elts then
- return
+ for _,opt in ipairs(opts) do
+ local check_res = string.sub(opt, -1)
+ local domain = string.sub(opt, 1, -3)
+
+ if check_res == '+' then
+ table.insert(dkim_results.pass, domain)
+
+ if policy.strict_dkim then
+ if rspamd_util.strequal_caseless(hdrfromdom, domain) then
+ dkim_ok = true
+ else
+ dkim_violated = "DKIM not aligned (strict)"
+ end
else
- if found_policy then
- failed_policy = 'Multiple policies defined in DNS'
- return
+ local dkim_tld = rspamd_util.get_tld(domain)
+
+ if rspamd_util.strequal_caseless(dkim_tld, policy.domain) then
+ dkim_ok = true
else
- found_policy = true
+ dkim_violated = "DKIM not aligned (relaxed)"
end
end
-
- if elts then
- local dkim_pol = elts['adkim']
- if dkim_pol then
- if dkim_pol == 's' then
- strict_dkim = true
- elseif dkim_pol ~= 'r' then
- failed_policy = 'adkim tag has invalid value: ' .. dkim_pol
- return
+ elseif check_res == '?' then
+ -- Check for dkim tempfail
+ if not dkim_ok then
+ if policy.strict_dkim then
+ if rspamd_util.strequal_caseless(hdrfromdom, domain) then
+ dkim_tmpfail = true
end
- end
+ else
+ local dkim_tld = rspamd_util.get_tld(domain)
- local spf_pol = elts['aspf']
- if spf_pol then
- if spf_pol == 's' then
- strict_spf = true
- elseif spf_pol ~= 'r' then
- failed_policy = 'aspf tag has invalid value: ' .. spf_pol
- return
+ if rspamd_util.strequal_caseless(dkim_tld, policy.domain) then
+ dkim_tmpfail = true
end
end
+ end
+ table.insert(dkim_results.temperror, domain)
+ elseif check_res == '-' then
+ table.insert(dkim_results.fail, domain)
+ else
+ table.insert(dkim_results.permerror, domain)
+ end
+ end
- local policy = elts['p']
- if policy then
- if (policy == 'reject') then
- dmarc_policy = 'reject'
- elseif (policy == 'quarantine') then
- dmarc_policy = 'quarantine'
- elseif (policy ~= 'none') then
- failed_policy = 'p tag has invalid value: ' .. policy
- return
- end
- end
+ if not dkim_ok and dkim_violated then
+ table.insert(reason, dkim_violated)
+ end
+ else
+ table.insert(reason, "No valid DKIM")
+ end
- local subdomain_policy = elts['sp']
- if subdomain_policy and lookup_domain == dmarc_domain then
- if (subdomain_policy == 'reject') then
- if dmarc_domain ~= hfromdom then
- dmarc_policy = 'reject'
- end
- elseif (subdomain_policy == 'quarantine') then
- if dmarc_domain ~= hfromdom then
- dmarc_policy = 'quarantine'
- end
- elseif (subdomain_policy == 'none') then
- if dmarc_domain ~= hfromdom then
- dmarc_policy = 'none'
- end
- elseif (subdomain_policy ~= 'none') then
- failed_policy = 'sp tag has invalid value: ' .. subdomain_policy
- return
- end
- end
+ lua_util.debugm(N, task, "validated dmarc policy for %s: %s; dkim_ok=%s, dkim_tempfail=%s, spf_ok=%s, spf_tempfail=%s",
+ policy.domain, policy.dmarc_policy,
+ dkim_ok, dkim_tmpfail,
+ spf_ok, spf_tmpfail)
- pct = elts['pct']
- if pct then
- pct = tonumber(pct)
- end
+ local disposition = 'none'
+ local sampled_out = false
- if not rua then
- rua = elts['rua']
- end
+ local function handle_dmarc_failure(what, reason_str)
+ if not policy.pct or policy.pct == 100 then
+ task:insert_result(what, 1.0,
+ policy.domain .. ' : ' .. reason_str, policy.dmarc_policy)
+ disposition = "quarantine"
+ else
+ if (math.random(100) > policy.pct) then
+ if (not no_sampling_domains or
+ not no_sampling_domains:get_key(policy.domain)) then
+ task:insert_result(dmarc_symbols['softfail'], 1.0,
+ policy.domain .. ' : ' .. reason_str, policy.dmarc_policy, "sampled_out")
+ sampled_out = true
+ else
+ task:insert_result(what, 1.0,
+ policy.domain .. ' : ' .. reason_str, policy.dmarc_policy, "local_policy")
+ disposition = what
end
+ else
+ task:insert_result(dmarc_symbols[what], 1.0,
+ policy.domain .. ' : ' .. reason_str, policy.dmarc_policy)
+ disposition = what
end
- try()
end
- if not found_policy then
- if lookup_domain ~= dmarc_domain then
- local resolve_name = '_dmarc.' .. dmarc_domain
- task:get_resolver():resolve_txt({
- task=task,
- name = resolve_name,
- callback = dmarc_dns_cb,
- forced = true})
+ maybe_force_action(task, disposition)
+ end
- return
+ if spf_ok or dkim_ok then
+ --[[
+ https://tools.ietf.org/html/rfc7489#section-6.6.2
+ DMARC evaluation can only yield a "pass" result after one of the
+ underlying authentication mechanisms passes for an aligned
+ identifier.
+ ]]--
+ task:insert_result(dmarc_symbols['allow'], 1.0, policy.domain,
+ policy.dmarc_policy)
+ else
+ --[[
+ https://tools.ietf.org/html/rfc7489#section-6.6.2
+
+ If neither passes and one or both of them fail due to a
+ temporary error, the Receiver evaluating the message is unable to
+ conclude that the DMARC mechanism had a permanent failure; they
+ therefore cannot apply the advertised DMARC policy.
+ ]]--
+ if spf_tmpfail or dkim_tmpfail then
+ task:insert_result(dmarc_symbols['dnsfail'], 1.0, policy.domain..
+ ' : ' .. 'SPF/DKIM temp error', policy.dmarc_policy)
+ else
+ -- We can now check the failed policy and maybe send report data elt
+ local reason_str = table.concat(reason, ',')
+
+ if policy.dmarc_policy == 'quarantine' then
+ handle_dmarc_failure('quarantine', reason_str)
+ elseif policy.dmarc_policy == 'reject' then
+ handle_dmarc_failure('reject', reason_str)
else
- task:insert_result(dmarc_symbols['na'], 1.0, lookup_domain)
- return maybe_force_action('na')
+ task:insert_result(dmarc_symbols['softfail'], 1.0,
+ policy.domain .. ' : ' .. reason_str,
+ policy.dmarc_policy)
end
end
+ end
- local res = 0.5
- if failed_policy then
- task:insert_result(dmarc_symbols['badpolicy'], res, lookup_domain .. ' : ' .. failed_policy)
- return maybe_force_action('badpolicy')
+ if policy.rua and redis_params and dmarc_reporting then
+ if no_reporting_domains then
+ if no_reporting_domains:get_key(policy.domain) or
+ no_reporting_domains:get_key(rspamd_util.get_tld(policy.domain)) then
+ rspamd_logger.infox(task, 'DMARC reporting suppressed for %1', policy.domain)
+ return
+ end
end
- -- Check dkim and spf symbols
- local spf_ok = false
- local dkim_ok = false
- spf_domain = ((task:get_from(1) or E)[1] or E).domain
- if not spf_domain or spf_domain == '' then
- spf_domain = task:get_helo() or ''
+ local function dmarc_report_cb(err)
+ if not err then
+ rspamd_logger.infox(task, '<%1> dmarc report saved for %2',
+ task:get_message_id(), hdrfromdom)
+ else
+ rspamd_logger.errx(task, '<%1> dmarc report is not saved for %2: %3',
+ task:get_message_id(), hdrfromdom, err)
+ end
end
- if task:has_symbol(symbols['spf_allow_symbol']) then
- if strict_spf and rspamd_util.strequal_caseless(spf_domain, hfromdom) then
- spf_ok = true
- elseif strict_spf then
- table.insert(reason, "SPF not aligned (strict)")
- end
- if not strict_spf then
- local spf_tld = rspamd_util.get_tld(spf_domain)
- if rspamd_util.strequal_caseless(spf_tld, dmarc_domain) then
- spf_ok = true
- else
- table.insert(reason, "SPF not aligned (relaxed)")
- end
- end
+ local spf_result
+ if spf_ok then
+ spf_result = 'pass'
+ elseif spf_tmpfail then
+ spf_result = 'temperror'
else
- table.insert(reason, "No valid SPF")
- end
- local das = task:get_symbol(symbols['dkim_allow_symbol'])
- if ((das or E)[1] or E).options then
- dkim_results.pass = {}
- for _,domain in ipairs(das[1]['options']) do
- table.insert(dkim_results.pass, domain)
- if strict_dkim and rspamd_util.strequal_caseless(hfromdom, domain) then
- dkim_ok = true
- elseif strict_dkim then
- table.insert(reason, "DKIM not aligned (strict)")
- end
- if not strict_dkim then
- local dkim_tld = rspamd_util.get_tld(domain)
- if rspamd_util.strequal_caseless(dkim_tld, dmarc_domain) then
- dkim_ok = true
- else
- table.insert(reason, "DKIM not aligned (relaxed)")
- end
- end
+ if task:get_symbol(symbols.spf_deny_symbol) then
+ spf_result = 'fail'
+ elseif task:get_symbol(symbols.spf_softfail_symbol) then
+ spf_result = 'softfail'
+ elseif task:get_symbol(symbols.spf_neutral_symbol) then
+ spf_result = 'neutral'
+ elseif task:get_symbol(symbols.spf_permfail_symbol) then
+ spf_result = 'permerror'
+ else
+ spf_result = 'none'
end
- else
- table.insert(reason, "No valid DKIM")
end
- local disposition = 'none'
- local sampled_out = false
- local spf_tmpfail, dkim_tmpfail
-
- if not (spf_ok or dkim_ok) then
- local reason_str = table.concat(reason, ", ")
- res = 1.0
- spf_tmpfail = task:get_symbol(symbols['spf_tempfail_symbol'])
- dkim_tmpfail = task:get_symbol(symbols['dkim_tempfail_symbol'])
- if (spf_tmpfail or dkim_tmpfail) then
- if ((dkim_tmpfail or E)[1] or E).options then
- dkim_results.tempfail = {}
- for _,domain in ipairs(dkim_tmpfail[1]['options']) do
- table.insert(dkim_results.tempfail, domain)
- end
- end
- task:insert_result(dmarc_symbols['dnsfail'], 1.0, lookup_domain .. ' : ' .. 'SPF/DKIM temp error', dmarc_policy)
- return maybe_force_action('dnsfail')
+ -- Prepare and send redis report element
+ local period = os.date('%Y%m%d',
+ task:get_date({format = 'connect', gmt = true}))
+ local dmarc_domain_key = table.concat(
+ {redis_keys.report_prefix, hdrfromdom, period}, redis_keys.join_char)
+ local report_data = dmarc_report(task,
+ spf_ok and 'pass' or 'fail',
+ dkim_ok and 'pass' or 'fail',
+ disposition,
+ sampled_out,
+ hdrfromdom,
+ spf_domain,
+ dkim_results,
+ spf_result)
+
+ local idx_key = table.concat({redis_keys.index_prefix, period},
+ redis_keys.join_char)
+
+ if report_data then
+ rspamd_redis.exec_redis_script(take_report_id,
+ {task = task, is_write = true},
+ dmarc_report_cb,
+ {idx_key, dmarc_domain_key},
+ {hdrfromdom, report_data})
+ end
+ end
+end
+
+local function dmarc_callback(task)
+ local from = task:get_from(2)
+ local hfromdom = ((from or E)[1] or E).domain
+ local dmarc_domain
+ local ip_addr = task:get_ip()
+ local dmarc_checks = task:get_mempool():get_variable('dmarc_checks', 'int') or 0
+ local seen_invalid = false
+
+ if dmarc_checks ~= 2 then
+ rspamd_logger.infox(task, "skip DMARC checks as either SPF or DKIM were not checked");
+ return
+ end
+
+ if ((not check_authed and task:get_user()) or
+ (not check_local and ip_addr and ip_addr:is_local())) then
+ rspamd_logger.infox(task, "skip DMARC checks for local networks and authorized users");
+ return
+ end
+
+ -- Do some initial sanity checks, detect tld domain if different
+ if hfromdom and hfromdom ~= '' and not (from or E)[2] then
+ dmarc_domain = rspamd_util.get_tld(hfromdom)
+ elseif (from or E)[2] then
+ task:insert_result(dmarc_symbols['na'], 1.0, 'Duplicate From header')
+ return maybe_force_action(task, 'na')
+ elseif (from or E)[1] then
+ task:insert_result(dmarc_symbols['na'], 1.0, 'No domain in From header')
+ return maybe_force_action(task,'na')
+ else
+ task:insert_result(dmarc_symbols['na'], 1.0, 'No From header')
+ return maybe_force_action(task,'na')
+ end
+
+
+ local dns_checks_inflight = 0
+ local dmarc_domain_policy = {}
+ local dmarc_tld_policy = {}
+
+ local function process_dmarc_policy(policy, is_tld)
+ lua_util.debugm(N, task, "validate DMARC policy (is_tld=%s): %s",
+ is_tld, policy)
+ if policy.err and policy.symbol then
+ -- In case of fatal errors or final check for tld, we give up and
+ -- insert result
+ if is_tld or policy.fatal then
+ task:insert_result(policy.symbol, 1.0, policy.err)
+ maybe_force_action(task, policy.disposition)
+
+ return true
end
- if dmarc_policy == 'quarantine' then
- if not pct or pct == 100 then
- task:insert_result(dmarc_symbols['quarantine'], res, lookup_domain .. ' : ' .. reason_str, dmarc_policy)
- disposition = "quarantine"
- else
- if (math.random(100) > pct) then
- if (not no_sampling_domains or not no_sampling_domains:get_key(dmarc_domain)) then
- task:insert_result(dmarc_symbols['softfail'], res, lookup_domain .. ' : ' .. reason_str, dmarc_policy, "sampled_out")
- sampled_out = true
- else
- task:insert_result(dmarc_symbols['quarantine'], res, lookup_domain .. ' : ' .. reason_str, dmarc_policy, "local_policy")
- disposition = "quarantine"
- end
+ elseif policy.dmarc_policy then
+ dmarc_validate_policy(task, policy, hfromdom)
+
+ return true -- We have a more specific version, use it
+ end
+
+ return false -- Missing record
+ end
+
+ local function gen_dmarc_cb(lookup_domain, is_tld)
+ local policy_target = dmarc_domain_policy
+ if is_tld then
+ policy_target = dmarc_tld_policy
+ end
+
+ return function (_, _, results, err)
+ dns_checks_inflight = dns_checks_inflight - 1
+
+ if not seen_invalid then
+ policy_target.domain = lookup_domain
+
+ if err then
+ if (err ~= 'requested record is not found' and
+ err ~= 'no records with this name') then
+ policy_target.err = lookup_domain .. ' : ' .. err
+ policy_target.symbol = dmarc_symbols['dnsfail']
else
- task:insert_result(dmarc_symbols['quarantine'], res, lookup_domain .. ' : ' .. reason_str, dmarc_policy)
- disposition = "quarantine"
+ policy_target.err = lookup_domain
+ policy_target.symbol = dmarc_symbols['na']
end
- end
- elseif dmarc_policy == 'reject' then
- if not pct or pct == 100 then
- task:insert_result(dmarc_symbols['reject'], res, lookup_domain .. ' : ' .. reason_str, dmarc_policy)
- disposition = "reject"
else
- if (math.random(100) > pct) then
- if (not no_sampling_domains or not no_sampling_domains:get_key(dmarc_domain)) then
- task:insert_result(dmarc_symbols['quarantine'], res, lookup_domain .. ' : ' .. reason_str, dmarc_policy, "sampled_out")
- disposition = "quarantine"
- sampled_out = true
+ local has_valid_policy = false
+
+ for _,rec in ipairs(results) do
+ local ret,results_or_err = dmarc_check_record(task, rec, is_tld)
+
+ if not ret then
+ if results_or_err then
+ -- We have a fatal parsing error, give up
+ policy_target.err = lookup_domain .. ' : ' .. results_or_err
+ policy_target.symbol = dmarc_symbols['badpolicy']
+ policy_target.fatal = true
+ seen_invalid = true
+ end
else
- task:insert_result(dmarc_symbols['reject'], res, lookup_domain .. ' : ' .. reason_str, dmarc_policy, "local_policy")
- disposition = "reject"
+ if has_valid_policy then
+ policy_target.err = lookup_domain .. ' : ' ..
+ 'Multiple policies defined in DNS'
+ policy_target.symbol = dmarc_symbols['badpolicy']
+ policy_target.fatal = true
+ seen_invalid = true
+ end
+ has_valid_policy = true
+
+ for k,v in pairs(results_or_err) do
+ policy_target[k] = v
+ end
end
- else
- task:insert_result(dmarc_symbols['reject'], res, lookup_domain .. ' : ' .. reason_str, dmarc_policy)
- disposition = "reject"
end
end
- else
- task:insert_result(dmarc_symbols['softfail'], res, lookup_domain .. ' : ' .. reason_str, dmarc_policy)
- end
- else
- task:insert_result(dmarc_symbols['allow'], res, lookup_domain, dmarc_policy)
- end
-
- if rua and redis_params and dmarc_reporting then
-
- if no_reporting_domains then
- if no_reporting_domains:get_key(dmarc_domain) or no_reporting_domains:get_key(rspamd_util.get_tld(dmarc_domain)) then
- rspamd_logger.infox(task, 'DMARC reporting suppressed for %1', dmarc_domain)
- return maybe_force_action(disposition)
- end
end
- local spf_result
- if spf_ok then
- spf_result = 'pass'
- elseif spf_tmpfail then
- spf_result = 'temperror'
- else
- if task:get_symbol(symbols.spf_deny_symbol) then
- spf_result = 'fail'
- elseif task:get_symbol(symbols.spf_softfail_symbol) then
- spf_result = 'softfail'
- elseif task:get_symbol(symbols.spf_neutral_symbol) then
- spf_result = 'neutral'
- elseif task:get_symbol(symbols.spf_permfail_symbol) then
- spf_result = 'permerror'
- else
- spf_result = 'none'
- end
- end
- local dkim_deny = ((task:get_symbol(symbols.dkim_deny_symbol) or E)[1] or E).options
- if dkim_deny then
- dkim_results.fail = {}
- for _, domain in ipairs(dkim_deny) do
- table.insert(dkim_results.fail, domain)
- end
- end
- local dkim_permerror = ((task:get_symbol(symbols.dkim_permfail_symbol) or E)[1] or E).options
- if dkim_permerror then
- dkim_results.permerror = {}
- for _, domain in ipairs(dkim_permerror) do
- table.insert(dkim_results.permerror, domain)
+ if dns_checks_inflight == 0 then
+ lua_util.debugm(N, task, "finished DNS queries, validate policies")
+ -- We have checked both tld and real domain (if different)
+ if not process_dmarc_policy(dmarc_domain_policy, false) then
+ -- Try tld policy as well
+ process_dmarc_policy(dmarc_tld_policy, true)
end
end
- -- Prepare and send redis report element
- local period = os.date('%Y%m%d', task:get_date({format = 'connect', gmt = true}))
- local dmarc_domain_key = table.concat({redis_keys.report_prefix, hfromdom, period}, redis_keys.join_char)
- local report_data = dmarc_report(task, spf_ok and 'pass' or 'fail', dkim_ok and 'pass' or 'fail', disposition, sampled_out,
- hfromdom, spf_domain, dkim_results, spf_result)
- local idx_key = table.concat({redis_keys.index_prefix, period}, redis_keys.join_char)
-
- if report_data then
- rspamd_redis.exec_redis_script(take_report_id, {task = task, is_write = true}, dmarc_report_cb,
- {idx_key, dmarc_domain_key}, {hfromdom, report_data})
- end
end
-
- return maybe_force_action(disposition)
-
end
- -- Do initial request
local resolve_name = '_dmarc.' .. hfromdom
+
task:get_resolver():resolve_txt({
task=task,
name = resolve_name,
- callback = dmarc_dns_cb,
- forced = true})
+ callback = gen_dmarc_cb(hfromdom, false),
+ forced = true
+ })
+ dns_checks_inflight = dns_checks_inflight + 1
+
+ if dmarc_domain ~= hfromdom then
+ resolve_name = '_dmarc.' .. dmarc_domain
+
+ task:get_resolver():resolve_txt({
+ task=task,
+ name = resolve_name,
+ callback = gen_dmarc_cb(dmarc_domain, true),
+ forced = true
+ })
+
+ dns_checks_inflight = dns_checks_inflight + 1
+ end
end
+
local function try_opts(where)
local ret = false
local opts = rspamd_config:get_all_opt(where)
@@ -595,6 +721,7 @@ if opts['symbols'] then
end
end
+-- XXX: rework this shitty code some day please
if opts['reporting'] == true then
redis_params = rspamd_parse_redis_server('dmarc')
if not redis_params then