1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
|
--[[
Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
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 local_config.external_relay:get_key(hdr.real_ip) 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 number %s -> %s',
local_config.external_relay, i, nhdr.real_ip)
if nhdr.real_ip then
ip = nhdr.real_ip
found = true
end
end
break
end
end
if not found then
ip = task:get_from_ip()
rspamd_logger.warnx(task,
"cannot find external relay for SPF checks in received headers; use the original IP: %s",
tostring(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,
-- We can merely estimate timeout here, as it is possible to construct an SPF record that would cause
-- many DNS requests. But we won't like to set the maximum value for that all the time, as
-- the majority of requests will typically have 1-4 subrequests
augmentations = { string.format("timeout=%f", rspamd_config:get_dns_timeout() * 4 or 0.0) },
}
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
if local_config.external_relay then
local lua_maps = require "lua_maps"
local_config.external_relay = lua_maps.map_add_from_ucl(local_config.external_relay,
"radix", "External IP SPF 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
|