aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorVsevolod Stakhov <vsevolod@highsecure.ru>2015-04-03 17:02:45 +0100
committerVsevolod Stakhov <vsevolod@highsecure.ru>2015-04-03 17:02:45 +0100
commit19a1c5809141ca04e26fc0a68cc811cef346a5c4 (patch)
treee408348e26c37a80fab495b66838a17caec7d550 /src
parent6ab17702cbe6089f8755c6573d5a94f1f6c21e3c (diff)
downloadrspamd-19a1c5809141ca04e26fc0a68cc811cef346a5c4.tar.gz
rspamd-19a1c5809141ca04e26fc0a68cc811cef346a5c4.zip
Add preliminary version of DMARC plugin.
Diffstat (limited to 'src')
-rw-r--r--src/plugins/lua/dmarc.lua196
1 files changed, 196 insertions, 0 deletions
diff --git a/src/plugins/lua/dmarc.lua b/src/plugins/lua/dmarc.lua
new file mode 100644
index 000000000..2d6cb5830
--- /dev/null
+++ b/src/plugins/lua/dmarc.lua
@@ -0,0 +1,196 @@
+--[[
+Copyright (c) 2011-2015, Vsevolod Stakhov <vsevolod@highsecure.ru>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+]]--
+
+-- Dmarc policy filter
+
+local rspamd_regexp = require "rspamd_regexp"
+local rspamd_logger = require "rspamd_logger"
+local rspamd_redis = require "rspamd_redis"
+local upstream_list = require "rspamd_upstream_list"
+local dumper = require 'pl.pretty'.dump
+
+local symbols = {
+ spf_allow_symbol = 'R_SPF_ALLOW',
+ spf_deny_symbol = 'R_SPF_FAIL',
+ spf_softfail_symbol = 'R_SPF_SOFTFAIL',
+ spf_neutral_symbol = 'R_SPF_NEUTRAL',
+
+ dkim_allow_symbol = 'R_DKIM_ALLOW',
+ dkim_deny_symbol = 'R_DKIM_REJECT',
+}
+-- Default port for redis upstreams
+local default_port = 6379
+local upstreams = nil
+local dmarc_redis_key_prefix = "dmarc_"
+
+local elts_re = rspamd_regexp.create_cached(";\\s+")
+
+local function dmarc_report(task, spf_ok, dkim_ok)
+ local ip = task:get_from_ip()
+ if not ip:is_valid() then
+ return nil
+ end
+ local res = string.format('%d:%s:%s:%s', task:get_date(0),
+ ip:to_string(), tostring(spf_ok), tostring(dkim_ok))
+
+ return res
+end
+
+local function dmarc_callback(task)
+ local from = task:get_from()
+
+ local function dmarc_report_cb(task, err, data)
+ if not err then
+ rspamd_logger.info(string.format('<%s> dmarc report saved for %s',
+ task:get_message_id(), from[1]['domain']))
+ else
+ rspamd_logger.err(string.format('<%s> dmarc report is not saved for %s: %s',
+ task:get_message_id(), from[1]['domain'], err))
+ end
+ end
+
+ local function dmarc_dns_cb(resolver, to_resolve, results, err, key)
+ local strict_spf = false
+ local strict_dkim = false
+ local strict_policy = false
+ local rua
+
+ if results then
+ for _,r in ipairs(results) do
+ local elts = elts_re:split(r)
+
+ if elts then
+ for _,e in ipairs(elts) do
+ dkim_pol = string.match(e, '^adkim=([sr])$')
+ if dkim_pol and dkim_pol == 's' then
+ strict_dkim = true
+ end
+ spf_pol = string.match(e, '^aspf=([sr])$')
+ if spf_pol and spf_pol == 's' then
+ strict_spf = true
+ end
+ policy = string.match(e, '^p=reject$')
+ if policy then
+ strict_policy = true
+ end
+
+ if not rua then
+ rua = string.match(e, '^rua=([^%s]+)$')
+ end
+ end
+ end
+ end
+ end
+
+ if strict_spf then
+ -- Handle subdomains
+ end
+ if strict_dkim then
+ -- Handle subdomain
+ end
+
+ -- Check dkim and spf symbols
+ local spf_ok = false
+ local dkim_ok = false
+ if task:get_symbol(symbols['spf_allow_symbol']) then spf_ok = true end
+ if task:get_symbol(symbols['dkim_allow_symbol']) then dkim_ok = true end
+
+ if strict_policy and (not spf_ok or not dkim_ok) then
+ local res = 0.5
+ if not dkim_ok and not spf_ok then res = 1.0 end
+
+ task:insert_result('DMARC_STRICT_DENY', res)
+
+ elseif strict_policy then
+ task:insert_result('DMARC_STRICT_ALLOW', res)
+ end
+
+ if rua and (not spf_ok or not dkim_ok) and upstreams then
+ -- Prepare and send redis report element
+ local upstream = upstreams:get_upstream_by_hash(from[1]['domain'])
+ local redis_key = dmarc_redis_key_prefix .. from[1]['domain']
+ local addr = upstream:get_addr()
+ local report_data = dmarc_report(task, spf_ok, dkim_ok)
+
+ if report_data then
+ rspamd_redis.make_request(task, addr, dmarc_report_cb,
+ 'LPUSH', {redis_key, report_data})
+ end
+ end
+
+ -- XXX: handle rua and push data to redis
+ end
+
+ if from and from[1]['domain'] then
+ -- XXX: use tld list here and generate top level domain
+ local dmarc_domain = '_dmarc.' .. from[1]['domain']
+ task:get_resolver():resolve_txt(task:get_session(), task:get_mempool(),
+ dmarc_domain, dmarc_dns_cb)
+ end
+end
+
+local opts = rspamd_config:get_all_opt('dmarc')
+if not opts or type(opts) ~= 'table' then
+ return
+end
+
+if not opts['servers'] then
+ rspamd_logger.err('no servers are specified for dmarc stats')
+else
+ upstreams = upstream_list.create(opts['servers'], default_port)
+ if not upstreams then
+ rspamd_logger.err('cannot parse servers parameter')
+ end
+end
+
+if opts['key_prefix'] then
+ dmarc_redis_key_prefix = opts['key_prefix']
+end
+
+-- Check spf and dkim sections for changed symbols
+local function check_mopt(var, opts, name)
+ if opts[name] then
+ symbols['var'] = tostring(opts[name])
+ end
+end
+
+local spf_opts = rspamd_config:get_all_opt('spf')
+if spf_opts then
+ check_mopt('spf_deny_symbol', spf_opts, 'symbol_fail')
+ check_mopt('spf_allow_symbol', spf_opts, 'symbol_allow')
+ check_mopt('spf_softfail_symbol', spf_opts, 'symbol_softfail')
+ check_mopt('spf_neutral_symbol', spf_opts, 'symbol_neutral')
+end
+
+local dkim_opts = rspamd_config:get_all_opt('dkim')
+if dkim_opts then
+ check_mopt('dkim_deny_symbol', 'symbol_reject')
+ check_mopt('dkim_allow_symbol', 'symbol_allow')
+end
+
+rspamd_config:register_virtual_symbol('DMARC_POLICY_ALLOW', -1)
+rspamd_config:register_virtual_symbol('DMARC_POLICY_REJECT', 1)
+rspamd_config:register_post_filter(dmarc_callback) \ No newline at end of file