aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkorgoth1 <vladislav.stakhov@gmail.com>2019-12-02 20:51:17 +0300
committerkorgoth1 <vladislav.stakhov@gmail.com>2019-12-02 20:51:17 +0300
commitff4dadc87a5a6073001f4105f08c0e83578c80da (patch)
tree2d12b40395e1972077d9d66754138b3c7770b47b
parent388c82ffff6cf9dee3942212f5fc94f0230e211b (diff)
parente2dfcf15cc37650eee23ff00150bee9348ff11bb (diff)
downloadrspamd-ff4dadc87a5a6073001f4105f08c0e83578c80da.tar.gz
rspamd-ff4dadc87a5a6073001f4105f08c0e83578c80da.zip
[Test] 115 Dmarc is now separated by 115 dmarc, 116 dkim, 117 spf.
-rw-r--r--conf/options.inc2
-rw-r--r--lualib/lua_util.lua58
-rw-r--r--src/CMakeLists.txt3
-rw-r--r--src/libserver/spf.c7
-rw-r--r--src/lua/lua_spf.c106
-rw-r--r--src/lua/lua_task.c21
-rw-r--r--src/plugins/dkim_check.c3
-rw-r--r--src/plugins/lua/dmarc.lua31
-rw-r--r--src/plugins/lua/spf.lua230
-rw-r--r--src/plugins/spf.c670
-rw-r--r--test/functional/configs/dmarc.conf1
11 files changed, 392 insertions, 740 deletions
diff --git a/conf/options.inc b/conf/options.inc
index e635262e3..78fd3bee6 100644
--- a/conf/options.inc
+++ b/conf/options.inc
@@ -13,7 +13,7 @@
#
# Relevant documentation: https://rspamd.com/doc/configuration/options.html
-filters = "chartable,dkim,spf,regexp,fuzzy_check";
+filters = "chartable,dkim,regexp,fuzzy_check";
raw_mode = false;
one_shot = false;
cache_file = "$DBDIR/symbols.cache";
diff --git a/lualib/lua_util.lua b/lualib/lua_util.lua
index 207bdc0dc..89a4016b2 100644
--- a/lualib/lua_util.lua
+++ b/lualib/lua_util.lua
@@ -1293,4 +1293,62 @@ exports.toboolean = function(v)
end
end
+---[[[
+-- @function lua_util.config_check_local_or_authed(config, modname)
+-- Reads check_local and check_authed from the config as this is used in many modules
+-- @param {rspamd_config} config `rspamd_config` global
+-- @param {name} module name
+-- @return {boolean} v converted to boolean
+--]]]
+exports.config_check_local_or_authed = function(rspamd_config, modname, def_local, def_authed)
+ local check_local = def_local or false
+ local check_authed = def_authed or false
+
+ local function try_section(where)
+ local ret = false
+ local opts = rspamd_config:get_all_opt(where)
+ if type(opts) == 'table' then
+ if type(opts['check_local']) == 'boolean' then
+ check_local = opts['check_local']
+ ret = true
+ end
+ if type(opts['check_authed']) == 'boolean' then
+ check_authed = opts['check_authed']
+ ret = true
+ end
+ end
+
+ return ret
+ end
+
+ if not try_section(modname) then
+ try_section('options')
+ end
+
+ return {check_local, check_authed}
+end
+
+---[[[
+-- @function lua_util.is_skip_local_or_authed(task, conf[, ip])
+-- Returns `true` if local or authenticated task should be skipped for this module
+-- @param {rspamd_task} task
+-- @param {table} conf table returned from `config_check_local_or_authed`
+-- @param {rspamd_ip} ip optional ip address (can be obtained from a task)
+-- @return {boolean} true if check should be skipped
+--]]]
+exports.is_skip_local_or_authed = function(task, conf, ip)
+ if not ip then
+ ip = task:get_from_ip()
+ end
+ if not conf then
+ conf = {false, false}
+ end
+ if ((not conf[2] and task:get_user()) or
+ (not conf[1] and type(ip) == 'userdata' and ip:is_local())) then
+ return true
+ end
+
+ return false
+end
+
return exports
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 480578831..9a34d2ac4 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -96,11 +96,10 @@ SET(RSPAMDSRC controller.c
SET(PLUGINSSRC plugins/regexp.c
plugins/chartable.c
plugins/fuzzy_check.c
- plugins/spf.c
plugins/dkim_check.c
libserver/rspamd_control.c)
-SET(MODULES_LIST regexp chartable fuzzy_check spf dkim)
+SET(MODULES_LIST regexp chartable fuzzy_check dkim)
SET(WORKERS_LIST normal controller fuzzy rspamd_proxy)
IF (ENABLE_HYPERSCAN MATCHES "ON")
LIST(APPEND WORKERS_LIST "hs_helper")
diff --git a/src/libserver/spf.c b/src/libserver/spf.c
index 5fac43aa1..ec8decb50 100644
--- a/src/libserver/spf.c
+++ b/src/libserver/spf.c
@@ -2436,11 +2436,10 @@ rspamd_spf_resolve (struct rspamd_task *task, spf_cb_t callback,
if (cached) {
cached->flags |= RSPAMD_SPF_FLAG_CACHED;
- }
-
- callback (cached, task, cbdata);
+ callback (cached, task, cbdata);
- return TRUE;
+ return TRUE;
+ }
}
diff --git a/src/lua/lua_spf.c b/src/lua/lua_spf.c
index cf88bc838..478a7bbc2 100644
--- a/src/lua/lua_spf.c
+++ b/src/lua/lua_spf.c
@@ -54,6 +54,7 @@ static luaL_reg rspamd_spf_record_m[] = {
struct rspamd_lua_spf_cbdata {
struct rspamd_task *task;
lua_State *L;
+ struct rspamd_symcache_item *item;
gint cbref;
ref_entry_t ref;
};
@@ -153,6 +154,10 @@ lua_spf_dtor (struct rspamd_lua_spf_cbdata *cbd)
{
if (cbd) {
luaL_unref (cbd->L, LUA_REGISTRYINDEX, cbd->cbref);
+ if (cbd->item) {
+ rspamd_symcache_item_async_dec_check (cbd->task, cbd->item,
+ "lua_spf");
+ }
}
}
@@ -162,26 +167,36 @@ spf_lua_lib_callback (struct spf_resolved *record, struct rspamd_task *task,
{
struct rspamd_lua_spf_cbdata *cbd = (struct rspamd_lua_spf_cbdata *)ud;
- if (record && (record->flags & RSPAMD_SPF_RESOLVED_NA)) {
- lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_NA, record,
- "no record found");
- }
- else if (record && record->elts->len == 0 && (record->flags & RSPAMD_SPF_RESOLVED_TEMP_FAILED)) {
- lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_TEMP_FAILED, record,
- "temporary resolution error");
- }
- else if (record && record->elts->len == 0 && (record->flags & RSPAMD_SPF_RESOLVED_PERM_FAILED)) {
- lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_PERM_FAILED, record,
- "permanent resolution error");
- }
- else if (record && record->elts->len == 0) {
- lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_PERM_FAILED, record,
- "record is empty");
+ if (record) {
+ if ((record->flags & RSPAMD_SPF_RESOLVED_NA)) {
+ lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_NA, NULL,
+ "no record found");
+ }
+ else if (record->elts->len == 0 && (record->flags & RSPAMD_SPF_RESOLVED_TEMP_FAILED)) {
+ lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_TEMP_FAILED, NULL,
+ "temporary resolution error");
+ }
+ else if (record->elts->len == 0 && (record->flags & RSPAMD_SPF_RESOLVED_PERM_FAILED)) {
+ lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_PERM_FAILED, NULL,
+ "permanent resolution error");
+ }
+ else if (record->elts->len == 0) {
+ lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_PERM_FAILED, NULL,
+ "record is empty");
+ }
+ else if (record->domain) {
+ spf_record_ref (record);
+ lua_spf_push_result (cbd, record->flags, record, NULL);
+ spf_record_unref (record);
+ }
+ else {
+ lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_PERM_FAILED, NULL,
+ "internal error: non empty record for no domain");
+ }
}
- else if (record && record->domain) {
- spf_record_ref (record);
- lua_spf_push_result (cbd, record->flags, record, NULL);
- spf_record_unref (record);
+ else {
+ lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_PERM_FAILED, NULL,
+ "internal error: no record");
}
REF_RELEASE (cbd);
@@ -209,12 +224,21 @@ lua_spf_resolve (lua_State * L)
cbd->cbref = luaL_ref (L, LUA_REGISTRYINDEX);
/* TODO: make it as an optional parameter */
spf_cred = rspamd_spf_get_cred (task);
+ cbd->item = rspamd_symcache_get_cur_item (task);
+ rspamd_symcache_item_async_inc (task, cbd->item, "lua_spf");
REF_INIT_RETAIN (cbd, lua_spf_dtor);
if (!rspamd_spf_resolve (task, spf_lua_lib_callback, cbd, spf_cred)) {
- msg_info_task ("cannot make spf request for %s", spf_cred->domain);
- lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_TEMP_FAILED,
- NULL, "DNS failed");
+ msg_info_task ("cannot make spf request for %s",
+ spf_cred ? spf_cred->domain : "empty domain");
+ if (spf_cred) {
+ lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_TEMP_FAILED,
+ NULL, "DNS failed");
+ }
+ else {
+ lua_spf_push_result (cbd, RSPAMD_SPF_RESOLVED_NA,
+ NULL, "No domain");
+ }
REF_RELEASE (cbd);
}
}
@@ -276,11 +300,8 @@ spf_check_element (lua_State *L, struct spf_resolved *rec, struct spf_addr *addr
if (addr->flags & RSPAMD_SPF_FLAG_TEMPFAIL) {
/* Ignore failed addresses */
- lua_pushboolean (L, false);
- lua_pushinteger (L, RSPAMD_SPF_FLAG_TEMPFAIL);
- lua_pushstring (L, "temp failed");
- return 3;
+ return -1;
}
af = rspamd_inet_address_get_af (ip->addr);
@@ -333,12 +354,12 @@ spf_check_element (lua_State *L, struct spf_resolved *rec, struct spf_addr *addr
if (rec->flags & RSPAMD_SPF_RESOLVED_PERM_FAILED) {
lua_pushboolean (L, false);
lua_pushinteger (L, RSPAMD_SPF_RESOLVED_PERM_FAILED);
- lua_spf_push_spf_addr (L, addr);
+ lua_pushstring (L, "any perm fail");
}
else if (rec->flags & RSPAMD_SPF_RESOLVED_TEMP_FAILED) {
lua_pushboolean (L, false);
lua_pushinteger (L, RSPAMD_SPF_RESOLVED_TEMP_FAILED);
- lua_spf_push_spf_addr (L, addr);
+ lua_pushfstring (L, "any temp fail");
}
else {
lua_pushboolean (L, true);
@@ -365,7 +386,7 @@ spf_check_element (lua_State *L, struct spf_resolved *rec, struct spf_addr *addr
* 1. Boolean check result
* 2. If result is `false` then the second value is the error flag (e.g. rspamd_spf.flags.temp_fail), otherwise it will be an SPF method
* 3. If result is `false` then this will be an error string, otherwise - an SPF string (e.g. `mx` or `ip4:x.y.z.1`)
- * @param {rspamd_ip} ip address
+ * @param {rspamd_ip|string} ip address
* @return {result,flag_or_policy,error_or_addr} - triplet
*/
static gint
@@ -374,8 +395,29 @@ lua_spf_record_check_ip (lua_State *L)
struct spf_resolved *record =
* (struct spf_resolved **)rspamd_lua_check_udata (L, 1,
SPF_RECORD_CLASS);
- struct rspamd_lua_ip *ip = lua_check_ip (L, 2);
+ struct rspamd_lua_ip *ip = NULL;
gint nres = 0;
+ gboolean need_free_ip = FALSE;
+
+ if (lua_type (L, 2) == LUA_TUSERDATA) {
+ ip = lua_check_ip (L, 2);
+ }
+ else if (lua_type (L, 2) == LUA_TSTRING) {
+ const gchar *ip_str;
+ gsize iplen;
+
+ ip = g_malloc0 (sizeof (struct rspamd_lua_ip));
+ ip_str = lua_tolstring (L, 2, &iplen);
+
+ if (!rspamd_parse_inet_address (&ip->addr,
+ ip_str, iplen, RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) {
+ g_free (ip);
+ ip = NULL;
+ }
+ else {
+ need_free_ip = TRUE;
+ }
+ }
if (record && ip && ip->addr) {
for (guint i = 0; i < record->elts->len; i ++) {
@@ -389,6 +431,10 @@ lua_spf_record_check_ip (lua_State *L)
return luaL_error (L, "invalid arguments");
}
+ if (need_free_ip) {
+ g_free (ip);
+ }
+
lua_pushboolean (L, false);
lua_pushinteger (L, RSPAMD_SPF_RESOLVED_NA);
lua_pushstring (L, "no result");
diff --git a/src/lua/lua_task.c b/src/lua/lua_task.c
index aff6d8499..26ad15dec 100644
--- a/src/lua/lua_task.c
+++ b/src/lua/lua_task.c
@@ -4889,14 +4889,19 @@ lua_task_get_timeval (lua_State *L)
struct timeval tv;
if (task != NULL) {
- double_to_tv (task->task_timestamp, &tv);
- lua_createtable (L, 0, 2);
- lua_pushstring (L, "tv_sec");
- lua_pushinteger (L, (lua_Integer)tv.tv_sec);
- lua_settable (L, -3);
- lua_pushstring (L, "tv_usec");
- lua_pushinteger (L, (lua_Integer)tv.tv_usec);
- lua_settable (L, -3);
+ if (lua_isboolean (L, 2) && !!lua_toboolean (L, 2)) {
+ lua_pushnumber (L, task->task_timestamp);
+ }
+ else {
+ double_to_tv (task->task_timestamp, &tv);
+ lua_createtable (L, 0, 2);
+ lua_pushstring (L, "tv_sec");
+ lua_pushinteger (L, (lua_Integer) tv.tv_sec);
+ lua_settable (L, -3);
+ lua_pushstring (L, "tv_usec");
+ lua_pushinteger (L, (lua_Integer) tv.tv_usec);
+ lua_settable (L, -3);
+ }
}
else {
return luaL_error (L, "invalid arguments");
diff --git a/src/plugins/dkim_check.c b/src/plugins/dkim_check.c
index 3a88d9bb6..133feef2f 100644
--- a/src/plugins/dkim_check.c
+++ b/src/plugins/dkim_check.c
@@ -1121,7 +1121,8 @@ dkim_symbol_callback (struct rspamd_task *task,
GError *err = NULL;
struct rspamd_mime_header *rh, *rh_cur;
struct dkim_check_result *res = NULL, *cur;
- guint checked = 0, *dmarc_checks;
+ guint checked = 0;
+ gdouble *dmarc_checks;
struct dkim_ctx *dkim_module_ctx = dkim_get_context (task->cfg);
/* Allow dmarc */
diff --git a/src/plugins/lua/dmarc.lua b/src/plugins/lua/dmarc.lua
index c2fc4d9bb..e6a520e8e 100644
--- a/src/plugins/lua/dmarc.lua
+++ b/src/plugins/lua/dmarc.lua
@@ -23,8 +23,7 @@ local rspamd_url = require "rspamd_url"
local rspamd_util = require "rspamd_util"
local rspamd_redis = require "lua_redis"
local lua_util = require "lua_util"
-local check_local = false
-local check_authed = false
+local auth_and_local_conf
if confighelp then
return
@@ -559,7 +558,7 @@ local function dmarc_callback(task)
local hfromdom = ((from or E)[1] or E).domain
local dmarc_domain
local ip_addr = task:get_ip()
- local dmarc_checks = task:get_mempool():get_variable('dmarc_checks', 'int') or 0
+ local dmarc_checks = task:get_mempool():get_variable('dmarc_checks', 'double') or 0
local seen_invalid = false
if dmarc_checks ~= 2 then
@@ -567,8 +566,7 @@ local function dmarc_callback(task)
return
end
- if ((not check_authed and task:get_user()) or
- (not check_local and ip_addr and ip_addr:is_local())) then
+ if lua_util.is_skip_local_or_authed(task, auth_and_local_conf, ip_addr) then
rspamd_logger.infox(task, "skip DMARC checks for local networks and authorized users")
return
end
@@ -709,29 +707,14 @@ local function dmarc_callback(task)
end
-local function try_opts(where)
- local ret = false
- local opts = rspamd_config:get_all_opt(where)
- if type(opts) == 'table' then
- if type(opts['check_local']) == 'boolean' then
- check_local = opts['check_local']
- ret = true
- end
- if type(opts['check_authed']) == 'boolean' then
- check_authed = opts['check_authed']
- ret = true
- end
- end
-
- return ret
-end
-
-if not try_opts(N) then try_opts('options') end
-
local opts = rspamd_config:get_all_opt('dmarc')
if not opts or type(opts) ~= 'table' then
return
end
+
+auth_and_local_conf = lua_util.config_check_local_or_authed(rspamd_config, N,
+ false, false)
+
no_sampling_domains = rspamd_map_add(N, 'no_sampling_domains', 'map', 'Domains not to apply DMARC sampling to')
no_reporting_domains = rspamd_map_add(N, 'no_reporting_domains', 'map', 'Domains not to apply DMARC reporting to')
diff --git a/src/plugins/lua/spf.lua b/src/plugins/lua/spf.lua
new file mode 100644
index 000000000..f664661f9
--- /dev/null
+++ b/src/plugins/lua/spf.lua
@@ -0,0 +1,230 @@
+--[[
+Copyright (c) 2019, 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.
+]]--
+
+local N = "spf"
+local lua_util = require "lua_util"
+local rspamd_spf = require "rspamd_spf"
+local bit = require "bit"
+local rspamd_logger = require "rspamd_logger"
+
+if confighelp then
+ rspamd_config:add_example(nil, N,
+ 'Performs SPF checks',
+ [[
+spf {
+ # Enable module
+ enabled = true
+ # Number of elements in the cache of parsed SPF records
+ spf_cache_size = 2048;
+ # Default max expire for an element in this cache
+ spf_cache_expire = 1d;
+ # Whitelist IPs from checks
+ whitelist = "/path/to/some/file";
+ # Maximum number of recursive DNS subrequests (e.g. includes chanin length)
+ max_dns_nesting = 10;
+ # Maximum count of DNS requests per record
+ max_dns_requests = 30;
+ # Minimum TTL enforced for all elements in SPF records
+ min_cache_ttl = 5m;
+ # Disable all IPv6 lookups
+ disable_ipv6 = false;
+ # Use IP address from a received header produced by this relay (using by attribute)
+ external_relay = "192.168.1.1";
+}
+ ]])
+ return
+end
+
+local symbols = {
+ fail = "R_SPF_FAIL",
+ softfail = "R_SPF_SOFTFAIL",
+ neutral = "R_SPF_NEUTRAL",
+ allow = "R_SPF_ALLOW",
+ dnsfail = "R_SPF_DNSFAIL",
+ permfail = "R_SPF_PERMFAIL",
+ na = "R_SPF_NA",
+}
+
+local default_config = {
+ spf_cache_size = 2048,
+ max_dns_nesting = 10,
+ max_dns_requests = 30,
+ whitelist = nil,
+ min_cache_ttl = 60 * 5,
+ disable_ipv6 = false,
+ symbols = symbols,
+ external_relay = nil,
+}
+
+local local_config = rspamd_config:get_all_opt('spf')
+local auth_and_local_conf = lua_util.config_check_local_or_authed(rspamd_config, N,
+ false, false)
+
+if local_config then
+ local_config = lua_util.override_defaults(default_config, local_config)
+else
+ local_config = default_config
+end
+
+local function spf_check_callback(task)
+
+ local ip
+
+ if local_config.external_relay then
+ -- Search received headers to get header produced by an external relay
+ local rh = task:get_received_headers() or {}
+ local found = false
+
+ for i,hdr in ipairs(rh) do
+ if hdr.real_ip and hdr.real_ip == local_config.external_relay then
+ -- We can use the next header as a source of IP address
+ if rh[i + 1] then
+ local nhdr = rh[i + 1]
+ lua_util.debugm(N, task, 'found external relay %s at received header %s -> %s',
+ local_config.external_relay, hdr, nhdr.real_ip)
+
+ if nhdr.real_ip then
+ ip = nhdr.real_ip
+ found = true
+ end
+ end
+
+ break
+ end
+ end
+ if not found then
+ rspamd_logger.warnx(task, "cannot find external relay with IP %s",
+ local_config.external_relay)
+ ip = task:get_from_ip()
+ end
+ else
+ ip = task:get_from_ip()
+ end
+
+ local function flag_to_symbol(fl)
+ if bit.band(fl, rspamd_spf.flags.temp_fail) ~= 0 then
+ return local_config.symbols.dnsfail
+ elseif bit.band(fl, rspamd_spf.flags.perm_fail) ~= 0 then
+ return local_config.symbols.permfail
+ elseif bit.band(fl, rspamd_spf.flags.na) ~= 0 then
+ return local_config.symbols.na
+ end
+
+ return 'SPF_UNKNOWN'
+ end
+
+ local function policy_decode(res)
+ if res == rspamd_spf.policy.fail then
+ return local_config.symbols.fail,'-'
+ elseif res == rspamd_spf.policy.pass then
+ return local_config.symbols.allow,'+'
+ elseif res == rspamd_spf.policy.soft_fail then
+ return local_config.symbols.softfail,'~'
+ elseif res == rspamd_spf.policy.neutral then
+ return local_config.symbols.neutral,'?'
+ end
+
+ return 'SPF_UNKNOWN','?'
+ end
+
+ local function spf_resolved_cb(record, flags, err)
+ lua_util.debugm(N, task, 'got spf results: %s flags, %s err',
+ flags, err)
+
+ if record then
+ local result, flag_or_policy, error_or_addr = record:check_ip(ip)
+
+ lua_util.debugm(N, task,
+ 'checked ip %s: result=%s, flag_or_policy=%s, error_or_addr=%s',
+ ip, flags, err, error_or_addr)
+
+ if result then
+ local sym,code = policy_decode(flag_or_policy)
+ local opt = string.format('%s%s', code, error_or_addr.str or '???')
+ if bit.band(flags, rspamd_spf.flags.cached) ~= 0 then
+ opt = opt .. ':c'
+ rspamd_logger.infox(task,
+ "use cached record for %s (0x%s) in LRU cache for %s seconds",
+ record:get_domain(),
+ record:get_digest(),
+ record:get_ttl() - math.floor(task:get_timeval(true) -
+ record:get_timestamp()));
+ end
+ task:insert_result(sym, 1.0, opt)
+ else
+ local sym = flag_to_symbol(flag_or_policy)
+ task:insert_result(sym, 1.0, error_or_addr)
+ end
+ else
+ local sym = flag_to_symbol(flags)
+ task:insert_result(sym, 1.0, err)
+ end
+ end
+
+ if ip then
+ if local_config.whitelist and ip and local_config.whitelist:get_key(ip) then
+ rspamd_logger.infox(task, 'whitelisted SPF checks from %s',
+ tostring(ip))
+ return
+ end
+
+ if lua_util.is_skip_local_or_authed(task, auth_and_local_conf, ip) then
+ rspamd_logger.infox(task, 'skip SPF checks for local networks and authorized users')
+ return
+ end
+
+ rspamd_spf.resolve(task, spf_resolved_cb)
+ else
+ lua_util.debugm(N, task, "spf checks are not possible as no source IP address is defined")
+ end
+
+ -- FIXME: we actually need to set this variable when we really checked SPF
+ -- However, the old C module has set it all the times
+ -- Hence, we follow the same rule for now. It should be better designed at some day
+ local mpool = task:get_mempool()
+ local dmarc_checks = mpool:get_variable('dmarc_checks', 'double') or 0
+ dmarc_checks = dmarc_checks + 1
+ mpool:set_variable('dmarc_checks', dmarc_checks)
+end
+
+-- Register all symbols and init rspamd_spf library
+rspamd_spf.config(local_config)
+local sym_id = rspamd_config:register_symbol{
+ name = 'SPF_CHECK',
+ type = 'callback',
+ flags = 'fine,empty',
+ groups = {'policies','spf'},
+ score = 0.0,
+ callback = spf_check_callback
+}
+
+if local_config.whitelist then
+ local lua_maps = require "lua_maps"
+
+ local_config.whitelist = lua_maps.map_add_from_ucl(local_config.whitelist,
+ "radix", "SPF whitelist map")
+end
+
+for _,sym in pairs(local_config.symbols) do
+ rspamd_config:register_symbol{
+ name = sym,
+ type = 'virtual',
+ parent = sym_id,
+ groups = {'policies', 'spf'},
+ }
+end
+
+
diff --git a/src/plugins/spf.c b/src/plugins/spf.c
deleted file mode 100644
index f24bea004..000000000
--- a/src/plugins/spf.c
+++ /dev/null
@@ -1,670 +0,0 @@
-/*-
- * Copyright 2016 Vsevolod Stakhov
- *
- * 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:spf
- * rspamd module that checks spf records of incoming email
- *
- * Allowed options:
- * - symbol_allow (string): symbol to insert (default: 'R_SPF_ALLOW')
- * - symbol_fail (string): symbol to insert (default: 'R_SPF_FAIL')
- * - symbol_softfail (string): symbol to insert (default: 'R_SPF_SOFTFAIL')
- * - symbol_na (string): symbol to insert (default: 'R_SPF_NA')
- * - symbol_dnsfail (string): symbol to insert (default: 'R_SPF_DNSFAIL')
- * - symbol_permfail (string): symbol to insert (default: 'R_SPF_PERMFAIL')
- * - whitelist (map): map of whitelisted networks
- */
-
-
-#include "config.h"
-#include "libmime/message.h"
-#include "libserver/spf.h"
-#include "libutil/hash.h"
-#include "libutil/map.h"
-#include "libutil/map_helpers.h"
-#include "rspamd.h"
-#include "libserver/mempool_vars_internal.h"
-
-#define DEFAULT_SYMBOL_FAIL "R_SPF_FAIL"
-#define DEFAULT_SYMBOL_SOFTFAIL "R_SPF_SOFTFAIL"
-#define DEFAULT_SYMBOL_NEUTRAL "R_SPF_NEUTRAL"
-#define DEFAULT_SYMBOL_ALLOW "R_SPF_ALLOW"
-#define DEFAULT_SYMBOL_DNSFAIL "R_SPF_DNSFAIL"
-#define DEFAULT_SYMBOL_PERMFAIL "R_SPF_PERMFAIL"
-#define DEFAULT_SYMBOL_NA "R_SPF_NA"
-#define DEFAULT_CACHE_SIZE 2048
-
-static const gchar *M = "rspamd spf plugin";
-
-struct spf_ctx {
- struct module_ctx ctx;
- const gchar *symbol_fail;
- const gchar *symbol_softfail;
- const gchar *symbol_neutral;
- const gchar *symbol_allow;
- const gchar *symbol_dnsfail;
- const gchar *symbol_na;
- const gchar *symbol_permfail;
-
- struct rspamd_radix_map_helper *whitelist_ip;
-
- gboolean check_local;
- gboolean check_authed;
-};
-
-static void spf_symbol_callback (struct rspamd_task *task,
- struct rspamd_symcache_item *item,
- void *unused);
-
-/* Initialization */
-gint spf_module_init (struct rspamd_config *cfg, struct module_ctx **ctx);
-gint spf_module_config (struct rspamd_config *cfg);
-gint spf_module_reconfig (struct rspamd_config *cfg);
-
-module_t spf_module = {
- "spf",
- spf_module_init,
- spf_module_config,
- spf_module_reconfig,
- NULL,
- RSPAMD_MODULE_VER,
- (guint)-1,
-};
-
-static inline struct spf_ctx *
-spf_get_context (struct rspamd_config *cfg)
-{
- return (struct spf_ctx *)g_ptr_array_index (cfg->c_modules,
- spf_module.ctx_offset);
-}
-
-
-gint
-spf_module_init (struct rspamd_config *cfg, struct module_ctx **ctx)
-{
- struct spf_ctx *spf_module_ctx;
-
- spf_module_ctx = rspamd_mempool_alloc0 (cfg->cfg_pool,
- sizeof (*spf_module_ctx));
- *ctx = (struct module_ctx *)spf_module_ctx;
-
- rspamd_rcl_add_doc_by_path (cfg,
- NULL,
- "SPF check plugin",
- "spf",
- UCL_OBJECT,
- NULL,
- 0,
- NULL,
- 0);
-
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Map of IP addresses that should be excluded from SPF checks (in addition to `local_networks`)",
- "whitelist",
- UCL_STRING,
- NULL,
- 0,
- NULL,
- 0);
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Symbol that is added if SPF check is successful",
- "symbol_allow",
- UCL_STRING,
- NULL,
- 0,
- NULL,
- 0);
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Symbol that is added if SPF policy is set to 'deny'",
- "symbol_fail",
- UCL_STRING,
- NULL,
- 0,
- NULL,
- 0);
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Symbol that is added if SPF policy is set to 'undefined'",
- "symbol_softfail",
- UCL_STRING,
- NULL,
- 0,
- NULL,
- 0);
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Symbol that is added if SPF policy is set to 'neutral'",
- "symbol_neutral",
- UCL_STRING,
- NULL,
- 0,
- NULL,
- 0);
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Symbol that is added if SPF policy is failed due to DNS failure",
- "symbol_dnsfail",
- UCL_STRING,
- NULL,
- 0,
- NULL,
- 0);
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Symbol that is added if no SPF policy is found",
- "symbol_na",
- UCL_STRING,
- NULL,
- 0,
- NULL,
- 0);
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Symbol that is added if SPF policy is invalid",
- "symbol_permfail",
- UCL_STRING,
- NULL,
- 0,
- NULL,
- 0);
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Size of SPF parsed records cache",
- "spf_cache_size",
- UCL_INT,
- NULL,
- 0,
- NULL,
- 0);
-
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Minimum cached records TTL, 0 to disable (default: 5min)",
- "min_cache_ttl",
- UCL_INT,
- NULL,
- RSPAMD_CL_FLAG_UINT,
- NULL,
- 0);
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Maximum number of nested requests (default: " G_STRINGIFY(SPF_MAX_NESTING) ")",
- "max_dns_nesting",
- UCL_INT,
- NULL,
- RSPAMD_CL_FLAG_UINT,
- NULL,
- 0);
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Maximum number of dns requests to resolve SPF (default: " G_STRINGIFY(SPF_MAX_DNS_REQUESTS) ")",
- "max_dns_requests",
- UCL_INT,
- NULL,
- RSPAMD_CL_FLAG_UINT,
- NULL,
- 0);
- rspamd_rcl_add_doc_by_path (cfg,
- "spf",
- "Disable ipv6 resolving when doing SPF resolution",
- "disable_ipv6",
- UCL_BOOLEAN,
- NULL,
- 0,
- NULL,
- 0);
-
- return 0;
-}
-
-
-gint
-spf_module_config (struct rspamd_config *cfg)
-{
- const ucl_object_t *value;
- gint res = TRUE, cb_id;
- struct spf_ctx *spf_module_ctx = spf_get_context (cfg);
-
- if (!rspamd_config_is_module_enabled (cfg, "spf")) {
- return TRUE;
- }
-
- spf_module_ctx->whitelist_ip = NULL;
-
- value = rspamd_config_get_module_opt (cfg, "spf", "check_local");
-
- if (value == NULL) {
- rspamd_config_get_module_opt (cfg, "options", "check_local");
- }
-
- if (value != NULL) {
- spf_module_ctx->check_local = ucl_obj_toboolean (value);
- }
- else {
- spf_module_ctx->check_local = FALSE;
- }
-
- value = rspamd_config_get_module_opt (cfg, "spf", "check_authed");
-
- if (value == NULL) {
- rspamd_config_get_module_opt (cfg, "options", "check_authed");
- }
-
- if (value != NULL) {
- spf_module_ctx->check_authed = ucl_obj_toboolean (value);
- }
- else {
- spf_module_ctx->check_authed = FALSE;
- }
- if ((value =
- rspamd_config_get_module_opt (cfg, "spf", "symbol_fail")) != NULL) {
- spf_module_ctx->symbol_fail = ucl_obj_tostring (value);
- }
- else {
- spf_module_ctx->symbol_fail = DEFAULT_SYMBOL_FAIL;
- }
- if ((value =
- rspamd_config_get_module_opt (cfg, "spf", "symbol_softfail")) != NULL) {
- spf_module_ctx->symbol_softfail = ucl_obj_tostring (value);
- }
- else {
- spf_module_ctx->symbol_softfail = DEFAULT_SYMBOL_SOFTFAIL;
- }
- if ((value =
- rspamd_config_get_module_opt (cfg, "spf", "symbol_neutral")) != NULL) {
- spf_module_ctx->symbol_neutral = ucl_obj_tostring (value);
- }
- else {
- spf_module_ctx->symbol_neutral = DEFAULT_SYMBOL_NEUTRAL;
- }
- if ((value =
- rspamd_config_get_module_opt (cfg, "spf", "symbol_allow")) != NULL) {
- spf_module_ctx->symbol_allow = ucl_obj_tostring (value);
- }
- else {
- spf_module_ctx->symbol_allow = DEFAULT_SYMBOL_ALLOW;
- }
- if ((value =
- rspamd_config_get_module_opt (cfg, "spf", "symbol_dnsfail")) != NULL) {
- spf_module_ctx->symbol_dnsfail = ucl_obj_tostring (value);
- }
- else {
- spf_module_ctx->symbol_dnsfail = DEFAULT_SYMBOL_DNSFAIL;
- }
- if ((value =
- rspamd_config_get_module_opt (cfg, "spf", "symbol_na")) != NULL) {
- spf_module_ctx->symbol_na = ucl_obj_tostring (value);
- }
- else {
- spf_module_ctx->symbol_na = DEFAULT_SYMBOL_NA;
- }
- if ((value =
- rspamd_config_get_module_opt (cfg, "spf", "symbol_permfail")) != NULL) {
- spf_module_ctx->symbol_permfail = ucl_obj_tostring (value);
- }
- else {
- spf_module_ctx->symbol_permfail = DEFAULT_SYMBOL_PERMFAIL;
- }
-
- spf_library_config (ucl_obj_get_key (cfg->rcl_obj, "spf"));
-
- if ((value =
- rspamd_config_get_module_opt (cfg, "spf", "whitelist")) != NULL) {
-
- rspamd_config_radix_from_ucl (cfg, value, "SPF whitelist",
- &spf_module_ctx->whitelist_ip, NULL, NULL);
- }
-
- cb_id = rspamd_symcache_add_symbol (cfg->cache,
- "SPF_CHECK",
- 0,
- spf_symbol_callback,
- NULL,
- SYMBOL_TYPE_CALLBACK | SYMBOL_TYPE_FINE | SYMBOL_TYPE_EMPTY, -1);
- rspamd_config_add_symbol (cfg,
- "SPF_CHECK",
- 0.0,
- "SPF check callback",
- "policies",
- RSPAMD_SYMBOL_FLAG_IGNORE_METRIC,
- 1,
- 1);
- rspamd_config_add_symbol_group (cfg, "SPF_CHECK", "spf");
-
- rspamd_symcache_add_symbol (cfg->cache,
- spf_module_ctx->symbol_fail, 0,
- NULL, NULL,
- SYMBOL_TYPE_VIRTUAL,
- cb_id);
- rspamd_symcache_add_symbol (cfg->cache,
- spf_module_ctx->symbol_softfail, 0,
- NULL, NULL,
- SYMBOL_TYPE_VIRTUAL,
- cb_id);
- rspamd_symcache_add_symbol (cfg->cache,
- spf_module_ctx->symbol_permfail, 0,
- NULL, NULL,
- SYMBOL_TYPE_VIRTUAL,
- cb_id);
- rspamd_symcache_add_symbol (cfg->cache,
- spf_module_ctx->symbol_na, 0,
- NULL, NULL,
- SYMBOL_TYPE_VIRTUAL,
- cb_id);
- rspamd_symcache_add_symbol (cfg->cache,
- spf_module_ctx->symbol_neutral, 0,
- NULL, NULL,
- SYMBOL_TYPE_VIRTUAL,
- cb_id);
- rspamd_symcache_add_symbol (cfg->cache,
- spf_module_ctx->symbol_allow, 0,
- NULL, NULL,
- SYMBOL_TYPE_VIRTUAL,
- cb_id);
- rspamd_symcache_add_symbol (cfg->cache,
- spf_module_ctx->symbol_dnsfail, 0,
- NULL, NULL,
- SYMBOL_TYPE_VIRTUAL,
- cb_id);
-
-
- rspamd_mempool_add_destructor (cfg->cfg_pool,
- (rspamd_mempool_destruct_t)rspamd_map_helper_destroy_radix,
- spf_module_ctx->whitelist_ip);
-
- msg_info_config ("init internal spf module");
-
- return res;
-}
-
-gint
-spf_module_reconfig (struct rspamd_config *cfg)
-{
- return spf_module_config (cfg);
-}
-
-static gboolean
-spf_check_element (struct spf_resolved *rec, struct spf_addr *addr,
- struct rspamd_task *task, gboolean cached)
-{
- gboolean res = FALSE;
- const guint8 *s, *d;
- gchar *spf_result;
- guint af, mask, bmask, addrlen;
- const gchar *spf_message, *spf_symbol;
- struct spf_ctx *spf_module_ctx = spf_get_context (task->cfg);
-
- if (task->from_addr == NULL) {
- return FALSE;
- }
-
- if (addr->flags & RSPAMD_SPF_FLAG_TEMPFAIL) {
- /* Ignore failed addresses */
- return FALSE;
- }
-
- af = rspamd_inet_address_get_af (task->from_addr);
- /* Basic comparing algorithm */
- if (((addr->flags & RSPAMD_SPF_FLAG_IPV6) && af == AF_INET6) ||
- ((addr->flags & RSPAMD_SPF_FLAG_IPV4) && af == AF_INET)) {
- d = rspamd_inet_address_get_hash_key (task->from_addr, &addrlen);
-
- if (af == AF_INET6) {
- s = (const guint8 *)addr->addr6;
- mask = addr->m.dual.mask_v6;
- }
- else {
- s = (const guint8 *)addr->addr4;
- mask = addr->m.dual.mask_v4;
- }
-
- /* Compare the first bytes */
- bmask = mask / CHAR_BIT;
- if (mask > addrlen * CHAR_BIT) {
- msg_info_task ("bad mask length: %d", mask);
- }
- else if (memcmp (s, d, bmask) == 0) {
- if (bmask * CHAR_BIT < mask) {
- /* Compare the remaining bits */
- s += bmask;
- d += bmask;
- mask = (0xff << (CHAR_BIT - (mask - bmask * 8))) & 0xff;
-
- if ((*s & mask) == (*d & mask)) {
- res = TRUE;
- }
- }
- else {
- res = TRUE;
- }
- }
- }
- else {
- if (addr->flags & RSPAMD_SPF_FLAG_ANY) {
- res = TRUE;
- }
- else {
- res = FALSE;
- }
- }
-
- if (res) {
- spf_result = rspamd_mempool_alloc (task->task_pool,
- strlen (addr->spf_string) + 5);
-
- switch (addr->mech) {
- case SPF_FAIL:
- spf_symbol = spf_module_ctx->symbol_fail;
- spf_result[0] = '-';
- spf_message = "(SPF): spf fail";
- if (addr->flags & RSPAMD_SPF_FLAG_ANY) {
- if (rec->flags & RSPAMD_SPF_RESOLVED_PERM_FAILED) {
- msg_info_task ("do not apply SPF failed policy, as we have "
- "some addresses unresolved");
- spf_symbol = spf_module_ctx->symbol_permfail;
- }
- else if (rec->flags & RSPAMD_SPF_RESOLVED_TEMP_FAILED) {
- msg_info_task ("do not apply SPF failed policy, as we have "
- "some addresses unresolved");
- spf_symbol = spf_module_ctx->symbol_dnsfail;
- spf_message = "(SPF): spf DNS fail";
- }
- }
- break;
- case SPF_SOFT_FAIL:
- spf_symbol = spf_module_ctx->symbol_softfail;
- spf_message = "(SPF): spf softfail";
- spf_result[0] = '~';
-
- if (addr->flags & RSPAMD_SPF_FLAG_ANY) {
- if (rec->flags & RSPAMD_SPF_RESOLVED_PERM_FAILED) {
- msg_info_task ("do not apply SPF failed policy, as we have "
- "some addresses unresolved");
- spf_symbol = spf_module_ctx->symbol_permfail;
- }
- else if (rec->flags & RSPAMD_SPF_RESOLVED_TEMP_FAILED) {
- msg_info_task ("do not apply SPF failed policy, as we have "
- "some addresses unresolved");
- spf_symbol = spf_module_ctx->symbol_dnsfail;
- spf_message = "(SPF): spf DNS fail";
- }
- }
- break;
- case SPF_NEUTRAL:
- spf_symbol = spf_module_ctx->symbol_neutral;
- spf_message = "(SPF): spf neutral";
- spf_result[0] = '?';
- break;
- default:
- spf_symbol = spf_module_ctx->symbol_allow;
- spf_message = "(SPF): spf allow";
- spf_result[0] = '+';
- break;
- }
-
- gint r = rspamd_strlcpy (spf_result + 1, addr->spf_string,
- strlen (addr->spf_string) + 1);
-
- if (cached) {
- rspamd_strlcpy (spf_result + r + 1, ":c", 3);
- }
-
- rspamd_task_insert_result (task,
- spf_symbol,
- 1,
- spf_result);
- ucl_object_insert_key (task->messages,
- ucl_object_fromstring (spf_message), "spf", 0,
- false);
-
- return TRUE;
- }
-
- return FALSE;
-}
-
-static void
-spf_check_list (struct spf_resolved *rec, struct rspamd_task *task, gboolean cached)
-{
- guint i;
- struct spf_addr *addr;
-
- if (cached) {
- msg_info_task ("use cached record for %s (0x%xuL) in LRU cache for %d seconds",
- rec->domain,
- rec->digest,
- rec->ttl - (guint)(task->task_timestamp - rec->timestamp));
- }
-
- for (i = 0; i < rec->elts->len; i ++) {
- addr = &g_array_index (rec->elts, struct spf_addr, i);
- if (spf_check_element (rec, addr, task, cached)) {
- break;
- }
- }
-}
-
-static void
-spf_plugin_callback (struct spf_resolved *record, struct rspamd_task *task,
- gpointer ud)
-{
- struct rspamd_symcache_item *item = (struct rspamd_symcache_item *)ud;
- struct spf_ctx *spf_module_ctx = spf_get_context (task->cfg);
-
- if (record && (record->flags & RSPAMD_SPF_RESOLVED_NA)) {
- rspamd_task_insert_result (task,
- spf_module_ctx->symbol_na,
- 1,
- NULL);
- }
- else if (record && record->elts->len == 0 && (record->flags & RSPAMD_SPF_RESOLVED_TEMP_FAILED)) {
- rspamd_task_insert_result (task,
- spf_module_ctx->symbol_dnsfail,
- 1,
- NULL);
- }
- else if (record && record->elts->len == 0 && (record->flags & RSPAMD_SPF_RESOLVED_PERM_FAILED)) {
- rspamd_task_insert_result (task,
- spf_module_ctx->symbol_permfail,
- 1,
- NULL);
- }
- else if (record && record->elts->len == 0) {
- rspamd_task_insert_result (task,
- spf_module_ctx->symbol_permfail,
- 1,
- NULL);
- }
- else if (record && record->domain) {
- spf_record_ref (record);
- spf_check_list (record, task, record->flags & RSPAMD_SPF_FLAG_CACHED);
- spf_record_unref (record);
- }
-
- rspamd_symcache_item_async_dec_check (task, item, M);
-}
-
-
-static void
-spf_symbol_callback (struct rspamd_task *task,
- struct rspamd_symcache_item *item,
- void *unused)
-{
- struct rspamd_spf_cred *spf_cred;
- gint *dmarc_checks;
- struct spf_ctx *spf_module_ctx = spf_get_context (task->cfg);
-
- /* Allow dmarc */
- dmarc_checks = rspamd_mempool_get_variable (task->task_pool,
- RSPAMD_MEMPOOL_DMARC_CHECKS);
-
- if (dmarc_checks) {
- (*dmarc_checks) ++;
- }
- else {
- dmarc_checks = rspamd_mempool_alloc (task->task_pool,
- sizeof (*dmarc_checks));
- *dmarc_checks = 1;
- rspamd_mempool_set_variable (task->task_pool,
- RSPAMD_MEMPOOL_DMARC_CHECKS,
- dmarc_checks, NULL);
- }
-
- if (rspamd_match_radix_map_addr (spf_module_ctx->whitelist_ip,
- task->from_addr) != NULL) {
- rspamd_symcache_finalize_item (task, item);
- return;
- }
-
- if ((!spf_module_ctx->check_authed && task->user != NULL)
- || (!spf_module_ctx->check_local &&
- rspamd_inet_address_is_local (task->from_addr, TRUE))) {
- msg_info_task ("skip SPF checks for local networks and authorized users");
- rspamd_symcache_finalize_item (task, item);
-
- return;
- }
-
- spf_cred = rspamd_spf_get_cred (task);
- /* Refcount = 1 */
- rspamd_symcache_item_async_inc (task, item, M);
-
- if (spf_cred && spf_cred->domain) {
- /* Refcount = 2 */
- rspamd_symcache_item_async_inc (task, item, M);
-
- /* spf_plugin_callback can be called immediately */
- if (!rspamd_spf_resolve (task, spf_plugin_callback, item, spf_cred)) {
- msg_info_task ("cannot make spf request for %s", spf_cred->domain);
- rspamd_task_insert_result (task,
- spf_module_ctx->symbol_dnsfail,
- 1,
- "(SPF): spf DNS fail");
- }
- else {
- /* Refcount is either 2 or 1, so it'll be 3 or 2 upon increase */
- rspamd_symcache_item_async_inc (task, item, M);
- }
-
- /* Refcount 3 or 2 */
- rspamd_symcache_item_async_dec_check (task, item, M);
- /* Refcount 2 or 1 */
- }
-
- /* Refcount 1 or 0 */
- rspamd_symcache_item_async_dec_check (task, item, M);
-}
diff --git a/test/functional/configs/dmarc.conf b/test/functional/configs/dmarc.conf
index ddfc1ac61..0d931b7d3 100644
--- a/test/functional/configs/dmarc.conf
+++ b/test/functional/configs/dmarc.conf
@@ -1 +1,2 @@
dmarc { }
+spf { }