diff options
author | Vsevolod Stakhov <vsevolod@highsecure.ru> | 2018-10-12 16:44:21 +0100 |
---|---|---|
committer | Vsevolod Stakhov <vsevolod@highsecure.ru> | 2018-10-12 16:44:21 +0100 |
commit | a2ea0822d2bf76a1f77dfb9bbd48d6a946c82565 (patch) | |
tree | f99a9ad9a0ca3d5c14816fdfdd4d507900bdb701 /src/plugins | |
parent | c5721acc86dc48a171684441699d2a960c102d53 (diff) | |
download | rspamd-a2ea0822d2bf76a1f77dfb9bbd48d6a946c82565.tar.gz rspamd-a2ea0822d2bf76a1f77dfb9bbd48d6a946c82565.zip |
[Rework] Completely rewrite DMARC checks logic
Diffstat (limited to 'src/plugins')
-rw-r--r-- | src/plugins/lua/dmarc.lua | 719 |
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 |