aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins
diff options
context:
space:
mode:
authorVsevolod Stakhov <vsevolod@highsecure.ru>2016-05-03 13:16:47 +0100
committerVsevolod Stakhov <vsevolod@highsecure.ru>2016-05-03 13:16:47 +0100
commit37dcda40cc7453994ec1449cd7a9e2f7a1a78c4e (patch)
treef67d72b30ee68269f76f25ae3612170eeb727435 /src/plugins
parentd9282bbd545d1a1978b29a4d63b161cce9ccf8ed (diff)
downloadrspamd-37dcda40cc7453994ec1449cd7a9e2f7a1a78c4e.tar.gz
rspamd-37dcda40cc7453994ec1449cd7a9e2f7a1a78c4e.zip
[Feature] Add preliminary greylist plugin
Issue: #611
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/lua/greylist.lua270
1 files changed, 270 insertions, 0 deletions
diff --git a/src/plugins/lua/greylist.lua b/src/plugins/lua/greylist.lua
new file mode 100644
index 000000000..2f058e9a6
--- /dev/null
+++ b/src/plugins/lua/greylist.lua
@@ -0,0 +1,270 @@
+--[[
+Copyright (c) 2016, Vsevolod Stakhov <vsevolod@highsecure.ru>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+]]--
+
+-- A plugin that implements greylisting using redis
+
+-- Default port for redis upstreams
+local default_port = 6379
+local upstreams
+local whitelisted_ip
+local settings = {
+ expire = 86400, -- 1 day by default
+ timeout = 300, -- 5 minutes by default
+ key_prefix = 'grey.', -- default hash name
+ max_data_len = 10240, -- default data limit to hash
+ message = 'Try again later', -- default greylisted message
+ symbol = 'GREYLIST',
+ ipv4_mask = 19, -- Mask bits for ipv4
+ ipv6_mask = 64, -- Mask bits for ipv6
+}
+
+local rspamd_logger = require "rspamd_logger"
+local rspamd_redis = require "rspamd_redis"
+local upstream_list = require "rspamd_upstream_list"
+local rspamd_util = require "rspamd_util"
+local fun = require "fun"
+local rspamd_cryptobox = require "rspamd_cryptobox"
+local hash = require "rspamd_cryptobox_hash"
+
+local function data_key(task)
+ local cached = task:get_mempool():get_variable("grey_bodyhash")
+ if cached then
+ return cached
+ end
+
+ local body = task:get_rawbody()
+
+ if not body then return nil end
+
+ local len = body:len()
+ if len > settings['max_data_len'] then
+ len = settings['max_data_len']
+ end
+
+ local h = hash.create()
+ h:update(body, len)
+
+ local b32 = settings['key_prefix'] .. h:base32()
+ task:get_mempool():set_variable("grey_bodyhash", b32)
+ return b32
+end
+
+local function envelope_key(task)
+ local cached = task:get_mempool():get_variable("grey_metahash")
+ if cached then
+ return cached
+ end
+
+ local from = task:get_from('smtp')
+ local h = hash.create()
+
+ local addr = '<>'
+ if from and from[1] then
+ addr = from[1]['addr']
+ end
+
+ h:update(addr)
+
+ local rcpt = task:get_recipients('smtp')
+ if rcpt then
+ table.sort(rcpt, function(r1, r2)
+ return r1['addr'] < r2['addr']
+ end)
+
+ fun.each(function(r)
+ h:update(r['addr'])
+ end, rcpt)
+ end
+
+ local ip = task:get_from_ip()
+
+ if ip and ip:is_valid() then
+ local s
+ if ip:get_version() == 4 then
+ s = tostring(ip:apply_mask(settings['ipv4_mask']))
+ else
+ s = tostring(ip:apply_mask(settings['ipv6_mask']))
+ end
+ h:update(s)
+ end
+
+ local b32 = settings['key_prefix'] .. h:base32()
+ task:get_mempool():set_variable("grey_metahash", b32)
+ return b32
+end
+
+local function check_time(task, tm, type)
+ local t = tonumber(tm)
+
+ if not t then
+ rspamd_logger.infox(task, 'not a valid number: %s', tm)
+ return false
+ end
+
+ local now = rspamd_util.get_time()
+ if now - t < settings['timeout'] then
+ local end_time = rspamd_util.time_to_string(t + settings['timeout'])
+ rspamd_logger.infox(task, 'greylisted till "%s" using %s key',
+ end_time, type)
+ task:set_pre_result('soft reject', settings['message'])
+ task:get_mempool():set_variable("grey_greylisted", end_time)
+
+ return true
+ else
+ -- We just set variable to pass when in post-filter stage
+ task:get_mempool():set_variable("grey_whitelisted", type)
+
+ return true
+ end
+
+ return false
+end
+
+local function greylist_check(task)
+ local body_key = data_key(task)
+ local meta_key = envelope_key(task)
+ local upstream = upstreams:get_upstream_by_hash(body_key .. meta_key)
+ local addr = upstream:get_addr()
+
+ local function redis_get_cb(task, err, data)
+ local ret = false
+ if data then
+ if data[1] and type(data[1]) ~= 'userdata' then
+ ret = check_time(task, data[1], 'body')
+ end
+ if not ret and type(data[2]) ~= 'userdata' then
+ ret = check_time(task, data[2], 'meta')
+ end
+ upstream:ok()
+
+ if not ret then
+ rspamd_logger.infox(task, 'greylisted till "%s", new record',
+ rspamd_util.time_to_string(rspamd_util.get_time() + settings['timeout']),
+ type)
+ task:set_pre_result('soft reject', settings['message'])
+ -- Create new record
+ if addr then
+ local conn = rspamd_redis.connect({
+ task = task,
+ host = addr
+ })
+
+ local t = tostring(math.floor(rspamd_util.get_time()))
+ if conn then
+ conn:add_cmd('SETEX', {
+ body_key, tostring(settings['expire']), t
+ })
+ conn:add_cmd('SETEX', {
+ meta_key, tostring(settings['expire']), t
+ })
+ else
+ rspamd_logger.infox(task, 'got error while connecting to redis: %1', addr)
+ upstream:fail()
+ end
+ end
+ end
+ elseif err then
+ rspamd_logger.infox(task, 'got error while getting greylisting keys: %1', err)
+ upstream:fail()
+ end
+ end
+
+ if addr then
+ rspamd_redis.make_request(task, addr, redis_get_cb, 'MGET',
+ {body_key, meta_key})
+ end
+end
+
+local function greylist_set(task)
+ local is_whitelisted = task:get_mempool():get_variable("grey_whitelisted")
+ local is_greylisted = task:get_mempool():get_variable("grey_greylisted")
+ local action = task:get_metric_action('default')
+ local body_key = data_key(task)
+ local meta_key = envelope_key(task)
+ local upstream = upstreams:get_upstream_by_hash(body_key .. meta_key)
+ local addr = upstream:get_addr()
+
+ if is_greylisted then
+ task:insert_result(settings['symbol'], 0.0, 'delayed', is_greylisted)
+ rspamd_logger.infox(task, 'greylisting delayed till %s',
+ is_greylisted)
+
+ return
+ end
+ if is_whitelisted then
+ if action == 'greylist' then
+ -- We are going to accept message
+ task:set_metric_action('default', 'no action')
+ end
+
+ task:insert_result(settings['symbol'], 0.0, 'pass', is_whitelisted)
+ rspamd_logger.infox(task, 'greylisting pass (%s) till %s',
+ is_whitelisted,
+ rspamd_util.time_to_string(rspamd_util.get_time() + settings['expire']))
+
+ -- Update greylisting record expire
+ if addr then
+ local conn = rspamd_redis.connect({
+ task = task,
+ host = addr
+ })
+
+ if conn then
+ conn:add_cmd('EXPIRE', {
+ body_key, tostring(settings['expire'])
+ })
+ conn:add_cmd('EXPIRE', {
+ meta_key, tostring(settings['expire'])
+ })
+ else
+ rspamd_logger.infox(task, 'got error while connecting to redis: %1', addr)
+ upstream:fail()
+ end
+ end
+ else
+ if action ~= 'no action' then
+ -- We need to delay message, hence set a temporary result
+ task:insert_result(settings['symbol'], 0.0, 'greylisted', 'delayed')
+ task:set_metric_action('default', 'soft reject')
+ task:set_pre_result('soft reject', settings['message'])
+ else
+ task:insert_result(settings['symbol'], 0.0, 'greylisted', 'passed')
+ end
+ end
+end
+
+local opts = rspamd_config:get_all_opt('greylist')
+if opts then
+ if opts['whitelisted_ip'] then
+ whitelisted_ip = rspamd_config:add_radix_map(opts['whitelisted_ip'],
+ 'Greylist whitelist ip map')
+ end
+ if not opts['servers'] then
+ rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module')
+ else
+ upstreams = upstream_list.create(rspamd_config, opts['servers'], default_port)
+ if not upstreams then
+ rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module')
+ else
+ rspamd_config:register_pre_filter(greylist_check)
+ rspamd_config:register_post_filter(greylist_set, 10)
+ end
+ end
+
+ for k,v in pairs(opts) do
+ settings[k] = v
+ end
+end