]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Rework] Completely rewrite DMARC checks logic
authorVsevolod Stakhov <vsevolod@highsecure.ru>
Fri, 12 Oct 2018 15:44:21 +0000 (16:44 +0100)
committerVsevolod Stakhov <vsevolod@highsecure.ru>
Fri, 12 Oct 2018 15:44:21 +0000 (16:44 +0100)
src/plugins/lua/dmarc.lua

index 69e210e4717bf1978841dffba93cce45b1642209..62168addd05547224a88326e4ccdebca7033f157 100644 (file)
@@ -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