diff options
author | Vsevolod Stakhov <vsevolod@highsecure.ru> | 2018-12-29 11:04:53 +0000 |
---|---|---|
committer | Vsevolod Stakhov <vsevolod@highsecure.ru> | 2018-12-29 11:04:53 +0000 |
commit | 9fddf731cf2161288d70d50c182831124cbd377b (patch) | |
tree | 483f51d8bb3ef8e2fa0f2426901f69ccb0efd18c | |
parent | 2f2526e7005bc151a032dacec6a182107dde5a74 (diff) | |
download | rspamd-9fddf731cf2161288d70d50c182831124cbd377b.tar.gz rspamd-9fddf731cf2161288d70d50c182831124cbd377b.zip |
[Rework] Dcc: Rework DCC plugin
-rw-r--r-- | lualib/lua_scanners/dcc.lua | 227 | ||||
-rw-r--r-- | lualib/lua_scanners/init.lua | 4 | ||||
-rw-r--r-- | src/plugins/lua/dcc.lua | 156 |
3 files changed, 242 insertions, 145 deletions
diff --git a/lualib/lua_scanners/dcc.lua b/lualib/lua_scanners/dcc.lua new file mode 100644 index 000000000..27230fd6f --- /dev/null +++ b/lualib/lua_scanners/dcc.lua @@ -0,0 +1,227 @@ +--[[ +Copyright (c) 2018, Vsevolod Stakhov <vsevolod@highsecure.ru> +Copyright (c) 2018, Carsten Rosenberg <c.rosenberg@heinlein-support.de> + +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. +]]-- + +--[[[ +-- @module fprot +-- This module contains dcc access functions +--]] + +local lua_util = require "lua_util" +local tcp = require "rspamd_tcp" +local upstream_list = require "rspamd_upstream_list" +local rspamd_logger = require "rspamd_logger" +local common = require "lua_scanners/common" +local fun = require "fun" + +local N = 'dcc' + +local function dcc_check(task, content, _, rule) + local function dcc_check_uncached () + local upstream = rule.upstreams:get_upstream_round_robin() + local addr = upstream:get_addr() + local retransmits = rule.retransmits + local client = rule.client + + local client_ip = task:get_from_ip() + if client_ip and client_ip:is_valid() then + client = client_ip:to_string() + end + local client_host = task:get_hostname() + if client_host then + client = client .. "\r" .. client_host + end + + -- HELO + local helo = task:get_helo() or '' + + -- Envelope From + local ef = task:get_from() + local envfrom = 'test@example.com' + if ef and ef[1] then + envfrom = ef[1]['addr'] + end + + -- Envelope To + local envrcpt = 'test@example.com' + local rcpts = task:get_recipients(); + if rcpts then + local dcc_recipients = table.concat(fun.totable(fun.map(function(rcpt) + return rcpt['addr'] end, + rcpts)), '\n') + if dcc_recipients then + envrcpt = dcc_recipients + end + end + + -- Build the DCC query + -- https://www.dcc-servers.net/dcc/dcc-tree/dccifd.html#Protocol + local request_data = { + "header\n", + client .. "\n", + helo .. "\n", + envfrom .. "\n", + envrcpt .. "\n", + "\n", + content + } + + local function dcc_callback(err, data, conn) + + if err then + + -- set current upstream to fail because an error occurred + upstream:fail() + + -- retry with another upstream until retransmits exceeds + if retransmits > 0 then + + retransmits = retransmits - 1 + + -- Select a different upstream! + upstream = rule.upstreams:get_upstream_round_robin() + addr = upstream:get_addr() + + lua_util.debugm(N, task, '%s: retry IP: %s', rule.log_prefix, addr) + + tcp.request({ + task = task, + host = addr:to_string(), + port = addr:get_port(), + timeout = rule.timeout or 2.0, + shutdown = true, + data = request_data, + callback = dcc_callback + }) + else + rspamd_logger.errx(task, '%s: failed to scan, maximum retransmits exceed', rule['symbol'], rule['type']) + task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan and retransmits exceed') + end + else + -- Parse the response + if upstream then upstream:ok() end + local _,_,result,disposition,header = tostring(data):find("(.-)\n(.-)\n(.-)\n") + lua_util.debugm(N, task, 'DCC result=%1 disposition=%2 header="%3"', + result, disposition, header) + + --[[ + @todo: Implement math function to calc the score dynamically based on return values. Maybe check spamassassin implementation. + ]] -- + + if header then + local _,_,info = header:find("; (.-)$") + if (result == 'R') then + -- Reject + common.yield_result(task, rule, info, rule.default_score) + elseif (result == 'T') then + -- Temporary failure + rspamd_logger.warnx(task, 'DCC returned a temporary failure result: %s', result) + task:insert_result(rule['symbol_fail'], 0.0, 'DCC returned a temporary failure result:' .. result) + elseif result == 'A' then + -- do nothing + if rule.log_clean then + rspamd_logger.infox(task, '%s: clean, returned result A - info: %s', rule.log_prefix, info) + else + lua_util.debugm(N, task, '%s: returned result A - info: %s', rule.log_prefix, info) + end + elseif result == 'G' then + -- do nothing + if rule.log_clean then + rspamd_logger.infox(task, '%s: clean, returned result G - info: %s', rule.log_prefix, info) + else + lua_util.debugm(N, task, '%s: returned result G - info: %s', rule.log_prefix, info) + end + elseif result == 'S' then + -- do nothing + if rule.log_clean then + rspamd_logger.infox(task, '%s: clean, returned result S - info: %s', rule.log_prefix, info) + else + lua_util.debugm(N, task, '%s: returned result S - info: %s', rule.log_prefix, info) + end + else + -- Unknown result + rspamd_logger.warnx(task, 'DCC result error: %1', result); + task:insert_result(rule['symbol_fail'], 0.0, 'DCC result error: ' .. result) + end + end + end + end + + tcp.request({ + task = task, + host = addr:to_string(), + port = addr:get_port(), + timeout = rule.timeout or 2.0, + shutdown = true, + data = request_data, + callback = dcc_callback + }) + end + if common.need_av_check(task, content, rule) then + dcc_check_uncached() + end +end + +local function dcc_config(opts) + + local dcc_conf = { + default_port = 10045, + timeout = 5.0, + log_clean = false, + retransmits = 2, + message = '${SCANNER}: bulk message found: "${VIRUS}"', + detection_category = "hash", + default_score = 1, + action = false, + client = '0.0.0.0', + } + + dcc_conf = lua_util.override_defaults(dcc_conf, opts) + + if not dcc_conf.log_prefix then + dcc_conf.log_prefix = N + end + + if not dcc_conf.servers and dcc_conf.socket then + dcc_conf.servers = dcc_conf.socket + end + + if not dcc_conf.servers then + rspamd_logger.errx(rspamd_config, 'no servers defined') + + return nil + end + + dcc_conf.upstreams = upstream_list.create(rspamd_config, + dcc_conf.servers, + dcc_conf.default_port) + + if dcc_conf.upstreams then + return dcc_conf + end + + rspamd_logger.errx(rspamd_config, 'cannot parse servers %s', + dcc_conf['servers']) + return nil +end + +return { + type = {'dcc','spam scan'}, + description = 'dcc bulk scanner', + configure = dcc_config, + check = dcc_check, + name = 'dcc' +}
\ No newline at end of file diff --git a/lualib/lua_scanners/init.lua b/lualib/lua_scanners/init.lua index e91feecfd..f769eb5a5 100644 --- a/lualib/lua_scanners/init.lua +++ b/lualib/lua_scanners/init.lua @@ -30,12 +30,16 @@ local function require_scanner(name) exports[sc.name or name] = sc end +-- Antiviruses require_scanner('clamav') require_scanner('fprot') require_scanner('kaspersky_av') require_scanner('savapi') require_scanner('sophos') +-- Other scanners +require_scanner('dcc') + exports.add_scanner = function(name, t, conf_func, check_func) assert(type(conf_func) == 'function' and type(check_func) == 'function', 'bad arguments') diff --git a/src/plugins/lua/dcc.lua b/src/plugins/lua/dcc.lua index 8c5dddeeb..d82ceda94 100644 --- a/src/plugins/lua/dcc.lua +++ b/src/plugins/lua/dcc.lua @@ -21,10 +21,8 @@ local N = 'dcc' local symbol_bulk = "DCC_BULK" local opts = rspamd_config:get_all_opt(N) local rspamd_logger = require "rspamd_logger" -local lua_util = require "lua_util" -local tcp = require "rspamd_tcp" -local upstream_list = require "rspamd_upstream_list" -local fun = require "fun" +local dcc = require("lua_scanners").filter('dcc').dcc + if confighelp then rspamd_config:add_example(nil, 'dcc', @@ -39,145 +37,10 @@ dcc { return end -local function check_dcc (task) - -- Connection - local client = '0.0.0.0' - local client_ip = task:get_from_ip() - local dcc_upstream - local upstream - local addr - local port - local retransmits = 2 - - if opts['servers'] then - dcc_upstream = upstream_list.create(rspamd_config, opts['servers']) - upstream = dcc_upstream:get_upstream_round_robin() - addr = upstream:get_addr() - port = addr:get_port() - else - lua_util.debugm(N, task, 'using socket %s', opts['socket']) - addr = opts['socket'] - end - - if client_ip and client_ip:is_valid() then - client = client_ip:to_string() - end - local client_host = task:get_hostname() - if client_host then - client = client .. "\r" .. client_host - end - - -- HELO - local helo = task:get_helo() or '' - - -- Envelope From - local ef = task:get_from() - local envfrom = 'test@example.com' - if ef and ef[1] then - envfrom = ef[1]['addr'] - end - - -- Envelope To - local envrcpt = 'test@example.com' - local rcpts = task:get_recipients(); - if rcpts then - local r = table.concat(fun.totable(fun.map(function(rcpt) - return rcpt['addr'] end, - rcpts)), '\n') - if r then - envrcpt = r - end - end - - -- Callback function to receive async result from DCC - local function cb(err, data) - - if err then - if retransmits > 0 then - retransmits = retransmits - 1 - -- Select a different upstream or the socket again - if opts['servers'] then - upstream = dcc_upstream:get_upstream_round_robin() - addr = upstream:get_addr() - port = addr:get_port() - else - addr = opts['socket'] - end - - lua_util.debugm(N, task, "sending query to %s:%s",tostring(addr), port) +local rule - data = { - "header\n", - client .. "\n", - helo .. "\n", - envfrom .. "\n", - envrcpt .. "\n", - "\n", - task:get_content() - } - - tcp.request({ - task = task, - host = tostring(addr), - port = port or 1, - timeout = opts['timeout'] or 2.0, - shutdown = true, - data = data, - callback = cb - }) - - else - rspamd_logger.errx(task, 'failed to scan, maximum retransmits exceed') - if upstream then upstream:fail() end - end - else - -- Parse the response - if upstream then upstream:ok() end - local _,_,result,disposition,header = tostring(data):find("(.-)\n(.-)\n(.-)\n") - lua_util.debugm(N, task, 'DCC result=%1 disposition=%2 header="%3"', - result, disposition, header) - - if header then - local _,_,info = header:find("; (.-)$") - if (result == 'R') then - -- Reject - task:insert_result(symbol_bulk, 1.0, info) - elseif (result == 'T') then - -- Temporary failure - rspamd_logger.warnx(task, 'DCC returned a temporary failure result') - else - if result ~= 'A' and result ~= 'G' and result ~= 'S' then - -- Unknown result - rspamd_logger.warnx(task, 'DCC result error: %1', result); - end - end - end - end - end - - -- Build the DCC query - -- https://www.dcc-servers.net/dcc/dcc-tree/dccifd.html#Protocol - local data = { - "header\n", - client .. "\n", - helo .. "\n", - envfrom .. "\n", - envrcpt .. "\n", - "\n", - task:get_content() - } - - rspamd_logger.warnx(task, "sending to %s:%s",tostring(addr), port) - - tcp.request({ - task = task, - host = tostring(addr), - port = port or 1, - timeout = opts['timeout'] or 2.0, - shutdown = true, - data = data, - callback = cb - }) +local function check_dcc (task) + dcc.check(task, task:get_content(), nil, rule) end -- Configuration @@ -195,9 +58,12 @@ if opts['host'] ~= nil and not opts['port'] then end -- WORKAROUND for deprecated host and port settings -if opts and ( opts['servers'] or opts['socket'] ) then +if not opts.symbol then opts.symbol = symbol_bulk end +rule = dcc.configure(opts) + +if rule then rspamd_config:register_symbol({ - name = symbol_bulk, + name = opts.symbol, callback = check_dcc }) rspamd_config:set_metric_symbol({ @@ -205,7 +71,7 @@ if opts and ( opts['servers'] or opts['socket'] ) then score = 2.0, description = 'Detected as bulk mail by DCC', one_shot = true, - name = symbol_bulk + name = opts.symbol, }) else lua_util.disable_module(N, "config") |