11 changed files with 390 additions and 6 deletions
-
22conf/modules.d/external_relay.conf
-
250src/plugins/lua/external_relay.lua
-
13test/functional/cases/001_merged/__init__.robot
-
41test/functional/cases/380_external_relay.robot
-
3test/functional/configs/maps/external_relay.hostname_map
-
2test/functional/configs/maps/external_relay.user_map
-
22test/functional/configs/merged-local.conf
-
3test/functional/configs/merged.conf
-
10test/functional/lua/external_relay.lua
-
13test/functional/messages/received5.eml
-
17test/functional/messages/received6.eml
@ -0,0 +1,22 @@ |
|||
# Please don't modify this file as your changes might be overwritten with |
|||
# the next update. |
|||
# |
|||
# You can modify 'local.d/external_relay.conf' to add and merge |
|||
# parameters defined inside this section |
|||
# |
|||
# You can modify 'override.d/external_relay.conf' to strictly override all |
|||
# parameters defined inside this section |
|||
# |
|||
# See https://rspamd.com/doc/faq.html#what-are-the-locald-and-overrided-directories |
|||
# for details |
|||
# |
|||
# Module documentation can be found at https://rspamd.com/doc/modules/external_relay.html |
|||
|
|||
external_relay { |
|||
# This module is default-disabled |
|||
enabled = false; |
|||
|
|||
.include(try=true,priority=5) "${DBDIR}/dynamic/external_relay.conf" |
|||
.include(try=true,priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/external_relay.conf" |
|||
.include(try=true,priority=10) "$LOCAL_CONFDIR/override.d/external_relay.conf" |
|||
} |
@ -0,0 +1,250 @@ |
|||
--[[ |
|||
Copyright (c) 2021, 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. |
|||
]]-- |
|||
|
|||
--[[ |
|||
external_relay plugin - sets IP/hostname from Received headers |
|||
]]-- |
|||
|
|||
if confighelp then |
|||
return |
|||
end |
|||
|
|||
local lua_maps = require "lua_maps" |
|||
local lua_util = require "lua_util" |
|||
local rspamd_ip = require "rspamd_ip" |
|||
local rspamd_logger = require "rspamd_logger" |
|||
local ts = require("tableshape").types |
|||
|
|||
local E = {} |
|||
local N = "external_relay" |
|||
|
|||
local settings = { |
|||
rules = {}, |
|||
} |
|||
|
|||
local config_schema = ts.shape{ |
|||
enabled = ts.boolean:is_optional(), |
|||
rules = ts.map_of( |
|||
ts.string, ts.one_of{ |
|||
ts.shape{ |
|||
priority = ts.number:is_optional(), |
|||
strategy = 'authenticated', |
|||
symbol = ts.string:is_optional(), |
|||
user_map = lua_maps.map_schema:is_optional(), |
|||
}, |
|||
ts.shape{ |
|||
count = ts.number, |
|||
priority = ts.number:is_optional(), |
|||
strategy = 'count', |
|||
symbol = ts.string:is_optional(), |
|||
}, |
|||
ts.shape{ |
|||
priority = ts.number:is_optional(), |
|||
strategy = 'local', |
|||
symbol = ts.string:is_optional(), |
|||
}, |
|||
ts.shape{ |
|||
hostname_map = lua_maps.map_schema, |
|||
priority = ts.number:is_optional(), |
|||
strategy = 'hostname_map', |
|||
symbol = ts.string:is_optional(), |
|||
}, |
|||
} |
|||
), |
|||
} |
|||
|
|||
local function set_from_rcvd(task, rcvd, remote_rcvd_ip) |
|||
if not remote_rcvd_ip then |
|||
if not rcvd.from_ip then |
|||
rspamd_logger.errx(task, 'no IP in header: %s', rcvd) |
|||
return |
|||
end |
|||
remote_rcvd_ip = rspamd_ip.from_string(rcvd.from_ip) |
|||
if not remote_rcvd_ip and remote_rcvd_ip:is_valid() then |
|||
rspamd_logger.errx(task, 'invalid remote IP: %s', rcvd.from_ip) |
|||
return |
|||
end |
|||
end |
|||
task:set_from_ip(remote_rcvd_ip) |
|||
if rcvd.from_hostname then |
|||
task:set_hostname(rcvd.from_hostname) |
|||
task:set_helo(rcvd.from_hostname) -- use fake value for HELO |
|||
else |
|||
rspamd_logger.warnx(task, "couldn't get hostname from headers") |
|||
local ipstr = string.format('[%s]', rcvd.from_ip) |
|||
task:set_hostname(ipstr) -- returns nil from task:get_hostname() |
|||
task:set_helo(ipstr) |
|||
end |
|||
return true |
|||
end |
|||
|
|||
local strategies = {} |
|||
|
|||
strategies.authenticated = function(rule) |
|||
local user_map |
|||
if rule.user_map then |
|||
user_map = lua_maps.map_add_from_ucl(rule.user_map, 'set', 'external relay usernames') |
|||
if not user_map then |
|||
rspamd_logger.errx(rspamd_config, "couldn't add map %s; won't register symbol %s", |
|||
rule.user_map, rule.symbol) |
|||
return |
|||
end |
|||
end |
|||
|
|||
return function(task) |
|||
local user = task:get_user() |
|||
if not user then |
|||
lua_util.debugm(N, task, 'sender is unauthenticated') |
|||
return |
|||
end |
|||
if user_map then |
|||
if not user_map:get_key(user) then |
|||
lua_util.debugm(N, task, 'sender (%s) is not in user_map', user) |
|||
return |
|||
end |
|||
end |
|||
|
|||
local rcvd_hdrs = task:get_received_headers() |
|||
-- Try find end of authentication chain |
|||
for _, rcvd in ipairs(rcvd_hdrs) do |
|||
if not rcvd.flags.authenticated then |
|||
-- Found unauthenticated hop, use this header |
|||
return set_from_rcvd(task, rcvd) |
|||
end |
|||
end |
|||
|
|||
rspamd_logger.errx(task, 'found nothing useful in Received headers') |
|||
end |
|||
end |
|||
|
|||
strategies.count = function(rule) |
|||
return function(task) |
|||
local rcvd_hdrs = task:get_received_headers() |
|||
-- Reduce count by 1 if artificial header is present |
|||
local hdr_count |
|||
if ((rcvd_hdrs[1] or E).flags or E).artificial then |
|||
hdr_count = rule.count - 1 |
|||
else |
|||
hdr_count = rule.count |
|||
end |
|||
|
|||
local rcvd = rcvd_hdrs[hdr_count] |
|||
if not rcvd then |
|||
rspamd_logger.errx(task, 'found no received header #%s', hdr_count) |
|||
return |
|||
end |
|||
|
|||
return set_from_rcvd(task, rcvd) |
|||
end |
|||
end |
|||
|
|||
strategies.hostname_map = function(rule) |
|||
local hostname_map = lua_maps.map_add_from_ucl(rule.hostname_map, 'map', 'external relay hostnames') |
|||
if not hostname_map then |
|||
rspamd_logger.errx(rspamd_config, "couldn't add map %s; won't register symbol %s", |
|||
rule.hostname_map, rule.symbol) |
|||
return |
|||
end |
|||
|
|||
return function(task) |
|||
local from_hn = task:get_hostname() |
|||
if not from_hn then |
|||
lua_util.debugm(N, task, 'sending hostname is missing') |
|||
return |
|||
end |
|||
|
|||
if hostname_map:get_key(from_hn) ~= 'direct' then |
|||
lua_util.debugm(N, task, 'sending hostname (%s) is not a direct relay', from_hn) |
|||
return |
|||
end |
|||
|
|||
local rcvd_hdrs = task:get_received_headers() |
|||
-- Try find sending hostname in Received headers |
|||
for _, rcvd in ipairs(rcvd_hdrs) do |
|||
if rcvd.by_hostname == from_hn and rcvd.from_ip then |
|||
if not hostname_map:get_key(rcvd.from_hostname) then |
|||
-- Remote hostname is not another relay, use this header |
|||
return set_from_rcvd(task, rcvd) |
|||
else |
|||
-- Keep checking with new hostname |
|||
from_hn = rcvd.from_hostname |
|||
end |
|||
end |
|||
end |
|||
|
|||
rspamd_logger.errx(task, 'found nothing useful in Received headers') |
|||
end |
|||
end |
|||
|
|||
strategies['local'] = function(rule) |
|||
return function(task) |
|||
local from_ip = task:get_from_ip() |
|||
if not from_ip then |
|||
lua_util.debugm(N, task, 'sending IP is missing') |
|||
return |
|||
end |
|||
|
|||
if not from_ip:is_local() then |
|||
lua_util.debugm(N, task, 'sending IP (%s) is non-local', from_ip) |
|||
return |
|||
end |
|||
|
|||
local rcvd_hdrs = task:get_received_headers() |
|||
local num_rcvd = #rcvd_hdrs |
|||
-- Try find first non-local IP in Received headers |
|||
for i, rcvd in ipairs(rcvd_hdrs) do |
|||
if rcvd.from_ip then |
|||
local remote_rcvd_ip = rspamd_ip.from_string(rcvd.from_ip) |
|||
if remote_rcvd_ip and remote_rcvd_ip:is_valid() and (not remote_rcvd_ip:is_local() or i == num_rcvd) then |
|||
return set_from_rcvd(task, rcvd, remote_rcvd_ip) |
|||
end |
|||
end |
|||
end |
|||
|
|||
rspamd_logger.errx(task, 'found nothing useful in Received headers') |
|||
end |
|||
end |
|||
|
|||
local opts = rspamd_config:get_all_opt(N) |
|||
if opts then |
|||
settings = lua_util.override_defaults(settings, opts) |
|||
|
|||
local ok, schema_err = config_schema:transform(settings) |
|||
if not ok then |
|||
rspamd_logger.errx(rspamd_config, 'config schema error: %s', schema_err) |
|||
lua_util.disable_module(N, "config") |
|||
return |
|||
end |
|||
|
|||
for k, rule in pairs(settings.rules) do |
|||
|
|||
if not rule.symbol then |
|||
rule.symbol = k |
|||
end |
|||
|
|||
local cb = strategies[rule.strategy](rule) |
|||
|
|||
if cb then |
|||
rspamd_config:register_symbol({ |
|||
name = rule.symbol, |
|||
type = 'prefilter', |
|||
priority = rule.priority or 20, |
|||
group = N, |
|||
callback = cb, |
|||
}) |
|||
end |
|||
end |
|||
end |
@ -0,0 +1,41 @@ |
|||
*** Settings *** |
|||
Suite Setup Rspamd Setup |
|||
Suite Teardown Rspamd Teardown |
|||
Library ${RSPAMD_TESTDIR}/lib/rspamd.py |
|||
Resource ${RSPAMD_TESTDIR}/lib/rspamd.robot |
|||
Variables ${RSPAMD_TESTDIR}/lib/vars.py |
|||
|
|||
*** Variables *** |
|||
${CONFIG} ${RSPAMD_TESTDIR}/configs/merged.conf |
|||
${RSPAMD_EXTERNAL_RELAY_ENABLED} true |
|||
${RSPAMD_SCOPE} Suite |
|||
|
|||
*** Test Cases *** |
|||
EXTERNAL RELAY AUTHENTICATED |
|||
Scan File ${RSPAMD_TESTDIR}/messages/received5.eml |
|||
... Settings={symbols_enabled [EXTERNAL_RELAY_TEST, EXTERNAL_RELAY_AUTHENTICATED]} |
|||
... IP=8.8.8.8 User=user@example.net |
|||
Expect Symbol With Exact Options EXTERNAL_RELAY_TEST |
|||
... IP=192.0.2.1 HOSTNAME=mail.example.org HELO=mail.example.org |
|||
|
|||
EXTERNAL RELAY COUNT |
|||
Scan File ${RSPAMD_TESTDIR}/messages/received4.eml |
|||
... Settings={symbols_enabled [EXTERNAL_RELAY_TEST, EXTERNAL_RELAY_COUNT]} |
|||
... IP=8.8.8.8 |
|||
Expect Symbol With Exact Options EXTERNAL_RELAY_TEST |
|||
... IP=151.18.193.131 HOSTNAME=ca-18-193-131.service.infuturo.it |
|||
... HELO=ca-18-193-131.service.infuturo.it |
|||
|
|||
EXTERNAL RELAY HOSTNAME MAP |
|||
Scan File ${RSPAMD_TESTDIR}/messages/received6.eml |
|||
... Settings={symbols_enabled [EXTERNAL_RELAY_TEST, EXTERNAL_RELAY_HOSTNAME_MAP]} |
|||
... Hostname=lame.example.net IP=192.0.2.10 |
|||
Expect Symbol With Exact Options EXTERNAL_RELAY_TEST |
|||
... IP=192.0.2.1 HOSTNAME=mail.example.org HELO=mail.example.org |
|||
|
|||
EXTERNAL RELAY LOCAL |
|||
Scan File ${RSPAMD_TESTDIR}/messages/ham.eml |
|||
... Settings={symbols_enabled [EXTERNAL_RELAY_TEST, EXTERNAL_RELAY_LOCAL]} |
|||
... IP=127.0.0.1 |
|||
Expect Symbol With Exact Options EXTERNAL_RELAY_TEST |
|||
... IP=4.31.198.44 HOSTNAME=mail.ietf.org HELO=mail.ietf.org |
@ -0,0 +1,3 @@ |
|||
cool.example.org direct |
|||
lame.example.net |
|||
|
@ -0,0 +1,2 @@ |
|||
user@example.net |
|||
|
@ -0,0 +1,10 @@ |
|||
rspamd_config:register_symbol({ |
|||
name = 'EXTERNAL_RELAY_TEST', |
|||
score = 0.0, |
|||
callback = function(task) |
|||
local from_ip = string.format('IP=%s', task:get_from_ip() or 'NIL') |
|||
local hostname = string.format('HOSTNAME=%s', task:get_hostname() or 'NIL') |
|||
local helo = string.format('HELO=%s', task:get_helo() or 'NIL') |
|||
return true, from_ip, hostname, helo |
|||
end |
|||
}) |
@ -0,0 +1,13 @@ |
|||
Received: from localhost (localhost [127.0.0.1]) |
|||
by ietfa.amsl.com (Postfix) with ESMTPA id 00E7712024B |
|||
for <cfrg@ietfa.amsl.com>; Tue, 7 May 2019 14:01:07 -0700 (PDT) |
|||
Received: from mail.ietf.org ([4.31.198.44]) |
|||
by localhost (ietfa.amsl.com [127.0.0.1]) (amavisd-new, port 10024) |
|||
with ESMTPA id k8UsBTUjeiTe for <cfrg@ietfa.amsl.com>; |
|||
Tue, 7 May 2019 14:01:04 -0700 (PDT) |
|||
Received: from mail.example.org ([192.0.2.1]) |
|||
by localhost (ietfa.amsl.com [127.0.0.1]) (amavisd-new, port 10024) |
|||
with ESMTP id k8UsBTUjeiTe for <cfrg@ietfa.amsl.com>; |
|||
Tue, 7 May 2019 14:01:04 -0700 (PDT) |
|||
|
|||
aa |
@ -0,0 +1,17 @@ |
|||
Received: from localhost (localhost [127.0.0.1]) |
|||
by ietfa.amsl.com (Postfix) with ESMTPA id 00E7712024B |
|||
for <cfrg@ietfa.amsl.com>; Tue, 7 May 2019 14:01:07 -0700 (PDT) |
|||
Received: from cool.example.org ([4.31.198.44]) |
|||
by lame.example.net (ietfa.amsl.com [127.0.0.1]) (amavisd-new, port 10024) |
|||
with ESMTPA id k8UsBTUjeiTe for <cfrg@ietfa.amsl.com>; |
|||
Tue, 7 May 2019 14:01:04 -0700 (PDT) |
|||
Received: from mail.example.org ([192.0.3.1]) |
|||
by localhost (ietfa.amsl.com [127.0.0.1]) (amavisd-new, port 10024) |
|||
with ESMTP id k8UsBTUjeiTe for <cfrg@ietfa.amsl.com>; |
|||
Tue, 7 May 2019 14:01:04 -0700 (PDT) |
|||
Received: from mail.example.org ([192.0.2.1]) |
|||
by cool.example.org (ietfa.amsl.com [127.0.0.1]) (amavisd-new, port 10024) |
|||
with ESMTP id k8UsBTUjeiTe for <cfrg@ietfa.amsl.com>; |
|||
Tue, 7 May 2019 14:01:04 -0700 (PDT) |
|||
|
|||
aa |
Write
Preview
Loading…
Cancel
Save
Reference in new issue