commit 9fddf73: [Rework] Dcc: Rework DCC plugin

Vsevolod Stakhov vsevolod at highsecure.ru
Sat Dec 29 12:21:04 UTC 2018


Author: Vsevolod Stakhov
Date: 2018-12-29 11:04:53 +0000
URL: https://github.com/rspamd/rspamd/commit/9fddf731cf2161288d70d50c182831124cbd377b

[Rework] Dcc: Rework DCC plugin

---
 lualib/lua_scanners/dcc.lua  | 227 +++++++++++++++++++++++++++++++++++++++++++
 lualib/lua_scanners/init.lua |   4 +
 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 at highsecure.ru>
+Copyright (c) 2018, Carsten Rosenberg <c.rosenberg at 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 at example.com'
+    if ef and ef[1] then
+      envfrom = ef[1]['addr']
+    end
+
+    -- Envelope To
+    local envrcpt = 'test at 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 at example.com'
-  if ef and ef[1] then
-    envfrom = ef[1]['addr']
-  end
-
-  -- Envelope To
-  local envrcpt = 'test at 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")


More information about the Commits mailing list