diff options
author | Vsevolod Stakhov <vsevolod@highsecure.ru> | 2018-09-28 20:12:51 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-09-28 20:12:51 +0100 |
commit | 5093631ddd4d3389cddaaa95865dc4a23143a10f (patch) | |
tree | a590af07f2c5201591e9722ca872c61f5460603a | |
parent | ef80e47f74eeb61e464daba93dcef754dfe210e4 (diff) | |
parent | 6c4c2662c2c44222d4205db839d7f2d1566b9a78 (diff) | |
download | rspamd-5093631ddd4d3389cddaaa95865dc4a23143a10f.tar.gz rspamd-5093631ddd4d3389cddaaa95865dc4a23143a10f.zip |
Merge pull request #2547 from HeinleinSupport/CHANGE-dcc
Change dcc / antivirus plugins
-rw-r--r-- | conf/modules.d/antivirus.conf | 8 | ||||
-rw-r--r-- | conf/modules.d/dcc.conf | 12 | ||||
-rw-r--r-- | src/plugins/lua/antivirus.lua | 240 | ||||
-rw-r--r-- | src/plugins/lua/dcc.lua | 127 |
4 files changed, 256 insertions, 131 deletions
diff --git a/conf/modules.d/antivirus.conf b/conf/modules.d/antivirus.conf index 16f38fbda..803820dbb 100644 --- a/conf/modules.d/antivirus.conf +++ b/conf/modules.d/antivirus.conf @@ -18,8 +18,12 @@ antivirus { clamav { # If set force this action if any virus is found (default unset: no action is forced) # action = "reject"; - # if `true` only messages with non-image attachments will be checked (default true) - attachments_only = true; + # message = '${SCANNER}: virus found: "${VIRUS}"'; + # Scan mime_parts seperately - otherwise the complete mail will be transfered to AV Scanner + #scan_mime_parts = true; + # Scanning Text is suitable for some av scanner databases (e.g. Sanesecurity) + #scan_text_mime = false; + #scan_image_mime = false; # If `max_size` is set, messages > n bytes in size are not scanned #max_size = 20000000; # symbol to add (add it to metric if you want non-zero weight) diff --git a/conf/modules.d/dcc.conf b/conf/modules.d/dcc.conf index ea774bdca..d7622374a 100644 --- a/conf/modules.d/dcc.conf +++ b/conf/modules.d/dcc.conf @@ -14,10 +14,14 @@ # See https://rspamd.com/doc/tutorials/writing_rules.html for details dcc { - # host = "/var/dcc/dccifd"; - # Port is only required if `dccifd` listens on a TCP socket - # port = 1234 - timeout = 2s; + + enabled = false; + + # Define local socket or TCP servers in upstreams syntax + # When sockets and servers are definined - servers is used! + socket = "/var/dcc/dccifd"; # Unix socket + #servers = "127.0.0.1:10045" # OR TCP upstreams + timeout = 2s; # Timeout to wait for checks .include(try=true,priority=5) "${DBDIR}/dynamic/dcc.conf" .include(try=true,priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/dcc.conf" diff --git a/src/plugins/lua/antivirus.lua b/src/plugins/lua/antivirus.lua index 46ea8c40d..5080a1c2a 100644 --- a/src/plugins/lua/antivirus.lua +++ b/src/plugins/lua/antivirus.lua @@ -35,8 +35,11 @@ antivirus { # action = "reject"; # If set, then rejection message is set to this value (mention single quotes) # message = '${SCANNER}: virus found: "${VIRUS}"'; - # if `true` only messages with non-image attachments will be checked (default true) - attachments_only = true; + # Scan mime_parts seperately - otherwise the complete mail will be transfered to AV Scanner + #scan_mime_parts = true; + # Scanning Text is suitable for some av scanner databases (e.g. Sanesecurity) + #scan_text_mime = false; + #scan_image_mime = false; # If `max_size` is set, messages > n bytes in size are not scanned max_size = 20000000; # symbol to add (add it to metric if you want non-zero weight) @@ -126,7 +129,9 @@ end local function clamav_config(opts) local clamav_conf = { - attachments_only = true, + scan_mime_parts = true; + scan_text_mime = false; + scan_image_mime = false; default_port = 3310, log_clean = false, timeout = 15.0, @@ -164,7 +169,9 @@ end local function fprot_config(opts) local fprot_conf = { - attachments_only = true, + scan_mime_parts = true; + scan_text_mime = false; + scan_image_mime = false; default_port = 10200, timeout = 15.0, log_clean = false, @@ -202,13 +209,17 @@ end local function sophos_config(opts) local sophos_conf = { - attachments_only = true, + scan_mime_parts = true; + scan_text_mime = false; + scan_image_mime = false; default_port = 4010, timeout = 15.0, log_clean = false, retransmits = 2, cache_expire = 3600, -- expire redis in one hour message = default_message, + savdi_report_encrypted = false, + savdi_report_oversize = false, } for k,v in pairs(opts) do @@ -240,7 +251,9 @@ end local function savapi_config(opts) local savapi_conf = { - attachments_only = true, + scan_mime_parts = true; + scan_text_mime = false; + scan_image_mime = false; default_port = 4444, -- note: You must set ListenAddress in savapi.conf product_id = 0, log_clean = false, @@ -378,30 +391,32 @@ local function fprot_check(task, content, digest, rule) local function fprot_callback(err, data) if err then - if err == 'IO timeout' then - if retransmits > 0 then - retransmits = retransmits - 1 - -- Select a different upstream! - upstream = rule.upstreams:get_upstream_round_robin() - addr = upstream:get_addr() - tcp.request({ - task = task, - host = addr:to_string(), - port = addr:get_port(), - timeout = rule['timeout'], - callback = fprot_callback, - data = { header, content, footer }, - stop_pattern = '\n' - }) - else - rspamd_logger.errx(task, 'failed to scan, maximum retransmits exceed') - task:insert_result(rule['symbol_fail'], 0.0, 'retransmits exceed') - upstream:fail() - end + -- 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 [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) + + tcp.request({ + task = task, + host = addr:to_string(), + port = addr:get_port(), + timeout = rule['timeout'], + callback = fprot_callback, + data = { header, content, footer }, + stop_pattern = '\n' + }) else - rspamd_logger.errx(task, 'failed to scan: %s', err) - task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan') - upstream:fail() + rspamd_logger.errx(task, '%s [%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 upstream:ok() @@ -411,7 +426,7 @@ local function fprot_check(task, content, digest, rule) if clean then cached = 'OK' if rule['log_clean'] then - rspamd_logger.infox(task, '%s [%s]: message is clean', rule['symbol'], rule['type']) + rspamd_logger.infox(task, '%s [%s]: message or mime_part is clean', rule['symbol'], rule['type']) end else -- returncodes: 1: infected, 2: suspicious, 3: both, 4-255: some error occured @@ -461,32 +476,35 @@ local function clamav_check(task, content, digest, rule) local function clamav_callback(err, data) if err then - if err == 'IO timeout' then - if retransmits > 0 then - retransmits = retransmits - 1 - -- Select a different upstream! - upstream = rule.upstreams:get_upstream_round_robin() - addr = upstream:get_addr() - tcp.request({ - task = task, - host = addr:to_string(), - port = addr:get_port(), - timeout = rule['timeout'], - callback = clamav_callback, - data = { header, content, footer }, - stop_pattern = '\0' - }) - else - rspamd_logger.errx(task, 'failed to scan, maximum retransmits exceed') - task:insert_result(rule['symbol_fail'], 0.0, 'retransmits exceed') - upstream:fail() - end + -- 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 [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) + + tcp.request({ + task = task, + host = addr:to_string(), + port = addr:get_port(), + timeout = rule['timeout'], + callback = clamav_callback, + data = { header, content, footer }, + stop_pattern = '\0' + }) else - rspamd_logger.errx(task, 'failed to scan: %s', err) - task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan') - upstream:fail() + rspamd_logger.errx(task, '%s [%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 upstream:ok() data = tostring(data) @@ -495,9 +513,9 @@ local function clamav_check(task, content, digest, rule) if data == 'stream: OK' then cached = 'OK' if rule['log_clean'] then - rspamd_logger.infox(task, '%s [%s]: message is clean', rule['symbol'], rule['type']) + rspamd_logger.infox(task, '%s [%s]: message or mime_part is clean', rule['symbol'], rule['type']) else - lua_util.debugm(N, task, '%s [%s]: message is clean', rule['symbol'], rule['type']) + lua_util.debugm(N, task, '%s [%s]: message or mime_part is clean', rule['symbol'], rule['type']) end else local vname = string.match(data, 'stream: (.+) FOUND') @@ -547,12 +565,21 @@ local function sophos_check(task, content, digest, rule) local function sophos_callback(err, data, conn) if err then - if err == 'IO timeout' 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 [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) + tcp.request({ task = task, host = addr:to_string(), @@ -562,18 +589,13 @@ local function sophos_check(task, content, digest, rule) data = { protocol, streamsize, content, bye } }) else - rspamd_logger.errx(task, 'failed to scan, maximum retransmits exceed') - task:insert_result(rule['symbol_fail'], 0.0, 'retransmits exceed') - upstream:fail() + rspamd_logger.errx(task, '%s [%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 - rspamd_logger.errx(task, 'failed to scan: %s', err) - task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan') - upstream:fail() - end else upstream:ok() data = tostring(data) + lua_util.debugm(N, task, '%s [%s]: got reply: %s', rule['symbol'], rule['type'], data) local vname = string.match(data, 'VIRUS (%S+) ') if vname then yield_result(task, rule, vname) @@ -581,29 +603,40 @@ local function sophos_check(task, content, digest, rule) else if string.find(data, 'DONE OK') then if rule['log_clean'] then - rspamd_logger.infox(task, '%s [%s]: message is clean', rule['symbol'], rule['type']) + rspamd_logger.infox(task, '%s [%s]: message or mime_part is clean', rule['symbol'], rule['type']) + else + lua_util.debugm(N, task, '%s [%s]: message or mime_part is clean', rule['symbol'], rule['type']) end save_av_cache(task, digest, rule, 'OK') + -- not finished - continue + elseif string.find(data, 'ACC') or string.find(data, 'OK SSSP') then + conn:add_read(sophos_callback) + -- set pseudo virus if configured, else do nothing since it's no fatal elseif string.find(data, 'FAIL 0212') then + rspamd_logger.infox(task, 'Message is ENCRYPTED (0212 SOPHOS_SAVI_ERROR_FILE_ENCRYPTED): %s', data) if rule['savdi_report_encrypted'] then - rspamd_logger.infox(task, 'Message is ENCRYPTED (0212 SOPHOS_SAVI_ERROR_FILE_ENCRYPTED): %s', data) yield_result(task, rule, "SAVDI_FILE_ENCRYPTED") save_av_cache(task, digest, rule, "SAVDI_FILE_ENCRYPTED") end + -- set pseudo virus if configured, else set fail since part was not scanned elseif string.find(data, 'REJ 4') then if rule['savdi_report_oversize'] then - rspamd_logger.infox(task, 'Message is OVERSIZED (SSSP reject code 4): %s', data) - yield_result(task, digest, rule, "SAVDI_FILE_OVERSIZED") + rspamd_logger.infox(task, 'SAVDI: Message is OVERSIZED (SSSP reject code 4): %s', data) + yield_result(task, rule, "SAVDI_FILE_OVERSIZED") save_av_cache(task, digest, rule, "SAVDI_FILE_OVERSIZED") + else + rspamd_logger.errx(task, 'SAVDI: Message is OVERSIZED (SSSP reject code 4): %s', data) + task:insert_result(rule['symbol_fail'], 0.0, 'Message is OVERSIZED (SSSP reject code 4):' .. data) end + -- excplicitly set REJ1 message when SAVDIreports a protocol error elseif string.find(data, 'REJ 1') then rspamd_logger.errx(task, 'SAVDI (Protocol error (REJ 1)): %s', data) - elseif string.find(data, 'ACC') or string.find(data, 'OK SSSP') then - conn:add_read(sophos_callback) + task:insert_result(rule['symbol_fail'], 0.0, 'SAVDI (Protocol error (REJ 1)):' .. data) else rspamd_logger.errx(task, 'unhandled response: %s', data) task:insert_result(rule['symbol_fail'], 0.0, 'unhandled response') end + end end end @@ -666,7 +699,7 @@ local function savapi_check(task, content, digest, rule) -- Terminal response - clean if string.find(result, '200') or string.find(result, '210') then if rule['log_clean'] then - rspamd_logger.infox(task, '%s: message is clean', rule['type']) + rspamd_logger.infox(task, '%s: message or mime_part is clean', rule['type']) end save_av_cache(task, digest, rule, 'OK') conn:add_write(savapi_fin_cb, 'QUIT\n') @@ -715,29 +748,32 @@ local function savapi_check(task, content, digest, rule) local function savapi_callback_init(err, data, conn) if err then - if err == 'IO timeout' then - if retransmits > 0 then - retransmits = retransmits - 1 - -- Select a different upstream! - upstream = rule.upstreams:get_upstream_round_robin() - addr = upstream:get_addr() - tcp.request({ - task = task, - host = addr:to_string(), - port = addr:get_port(), - timeout = rule['timeout'], - callback = savapi_callback_init, - stop_pattern = {'\n'}, - }) - else - rspamd_logger.errx(task, 'failed to scan, maximum retransmits exceed') - upstream:fail() - task:insert_result(rule['symbol_fail'], 0.0, 'retransmits exceed') - end + + -- 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 [%s]: retry IP: %s', rule['symbol'], rule['type'], addr) + + tcp.request({ + task = task, + host = addr:to_string(), + port = addr:get_port(), + timeout = rule['timeout'], + callback = savapi_callback_init, + stop_pattern = {'\n'}, + }) else - rspamd_logger.errx(task, 'failed to scan: %s', err) - task:insert_result(rule['symbol_fail'], 0.0, 'failed to scan') - upstream:fail() + rspamd_logger.errx(task, '%s [%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 upstream:ok() @@ -801,6 +837,14 @@ local function add_antivirus_rule(sym, opts) opts['symbol_fail'] = string.upper(opts['type']) .. '_FAIL' end + -- WORKAROUND for deprecated attachments_only + if opts['attachments_only'] ~= nil then + opts['scan_mime_parts'] = opts['attachments_only'] + rspamd_logger.warnx(rspamd_config, '%s [%s]: Using attachments_only is deprecated. '.. + 'Please use scan_mime_parts = %s instead', opts['symbol'], opts['type'], opts['attachments_only']) + end + -- WORKAROUND for deprecated attachments_only + if not cfg then rspamd_logger.errx(rspamd_config, 'unknown antivirus type: %s', opts['type']) @@ -843,11 +887,17 @@ local function add_antivirus_rule(sym, opts) end return function(task) - if rule.attachments_only then + if rule.scan_mime_parts then local parts = task:get_parts() or {} for _,p in ipairs(parts) do - if not p:is_image() and not p:is_text() and not p:is_multipart() then + if ( + (p:is_image() and rule.scan_image_mime) + or (p:is_text() and rule.scan_text_mime) + or (p:is_multipart() and rule.scan_text_mime) + or (not p:is_image() and not p:is_text() and not p:is_multipart()) + ) then + local content = p:get_content() if content and #content > 0 then diff --git a/src/plugins/lua/dcc.lua b/src/plugins/lua/dcc.lua index 131c83d36..311dc608e 100644 --- a/src/plugins/lua/dcc.lua +++ b/src/plugins/lua/dcc.lua @@ -20,18 +20,19 @@ limitations under the License. local N = 'dcc' local symbol_bulk = "DCC_BULK" local opts = rspamd_config:get_all_opt(N) -local logger = require "rspamd_logger" +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 lua_util = require "lua_util" if confighelp then rspamd_config:add_example(nil, 'dcc', "Check messages for 'bulkiness' using DCC", [[ dcc { - host = "/var/dcc/dccifd"; # Unix socket or hostname - port = 1234 # Port to use (needed for TCP socket) + socket = "/var/dcc/dccifd"; # Unix socket + servers = "127.0.0.1:10045" # OR TCP upstreams timeout = 2s; # Timeout to wait for checks } ]]) @@ -42,6 +43,22 @@ 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 @@ -74,27 +91,64 @@ local function check_dcc (task) -- Callback function to receive async result from DCC local function cb(err, data) - if (err) then - logger.warnx(task, 'DCC error: %1', err) - return - end - -- Parse the response - local _,_,result,disposition,header = tostring(data):find("(.-)\n(.-)\n(.-)\n") - logger.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 - logger.warnx(task, 'DCC returned a temporary failure result') + + 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) + + 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 - if result ~= 'A' and result ~= 'G' and result ~= 'S' then - -- Unknown result - logger.warnx(task, 'DCC result error: %1', result); + rspamd_logger.errx(task, 'failed to scan, maximum retransmits exceed') + upstream:fail() + end + else + -- Parse the response + 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 @@ -112,13 +166,12 @@ local function check_dcc (task) task:get_content() } - logger.debugm(N, task, 'sending to dcc: client=%1 helo="%2" envfrom="%3" envrcpt="%4"', - client, helo, envfrom, envrcpt) + rspamd_logger.warnx(task, "sending to %s:%s",tostring(addr), port) tcp.request({ task = task, - host = opts['host'], - port = opts['port'] or 1, + host = tostring(addr), + port = port or 1, timeout = opts['timeout'] or 2.0, shutdown = true, data = data, @@ -127,7 +180,21 @@ local function check_dcc (task) end -- Configuration -if opts and opts['host'] then + +-- WORKAROUND for deprecated host and port settings +if opts['host'] ~= nil and opts['port'] ~= nil then + opts['servers'] = opts['host'] .. ':' .. opts['port'] + rspamd_logger.warnx(rspamd_config, 'Using host and port parameters is deprecated. '.. + 'Please use servers = "%s:%s"; instead', opts['host'], opts['port']) +end +if opts['host'] ~= nil and not opts['port'] then + opts['socket'] = opts['host'] + rspamd_logger.warnx(rspamd_config, 'Using host parameters is deprecated. '.. + 'Please use socket = "%s"; instead', opts['host']) +end +-- WORKAROUND for deprecated host and port settings + +if opts and ( opts['servers'] or opts['socket'] ) then rspamd_config:register_symbol({ name = symbol_bulk, callback = check_dcc @@ -141,5 +208,5 @@ if opts and opts['host'] then }) else lua_util.disable_module(N, "config") - logger.infox('DCC module not configured'); + rspamd_logger.infox('DCC module not configured'); end |