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
|
--[[
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 argparse = require "argparse"
local rspamd_logger = require "rspamd_logger"
local ansicolors = require "ansicolors"
local bit = require "bit"
local parser = argparse()
:name "rspamadm dnstool"
:description "DNS tools provided by Rspamd"
:help_description_margin(30)
:command_target("command")
:require_command(true)
parser:option "-c --config"
:description "Path to config file"
:argname("<cfg>")
:default(rspamd_paths["CONFDIR"] .. "/" .. "rspamd.conf")
local spf = parser:command "spf"
:description "Extracts spf records"
spf:mutex(
spf:option "-d --domain"
:description "Domain to use"
:argname("<domain>"),
spf:option "-f --from"
:description "SMTP from to use"
:argname("<from>")
)
spf:option "-i --ip"
:description "Source IP address to use"
:argname("<ip>")
spf:flag "-a --all"
:description "Print all records"
local function printf(fmt, ...)
if fmt then
io.write(string.format(fmt, ...))
end
io.write('\n')
end
local function highlight(str)
return ansicolors.white .. str .. ansicolors.reset
end
local function green(str)
return ansicolors.green .. str .. ansicolors.reset
end
local function red(str)
return ansicolors.red .. str .. ansicolors.reset
end
local function load_config(opts)
local _r, err = rspamd_config:load_ucl(opts['config'])
if not _r then
rspamd_logger.errx('cannot parse %s: %s', opts['config'], err)
os.exit(1)
end
_r, err = rspamd_config:parse_rcl({ 'logging', 'worker' })
if not _r then
rspamd_logger.errx('cannot process %s: %s', opts['config'], err)
os.exit(1)
end
end
local function spf_handler(opts)
local rspamd_spf = require "rspamd_spf"
local rspamd_task = require "rspamd_task"
local rspamd_ip = require "rspamd_ip"
local task = rspamd_task:create(rspamd_config, rspamadm_ev_base)
task:set_session(rspamadm_session)
task:set_resolver(rspamadm_dns_resolver)
if opts.ip then
opts.ip = rspamd_ip.fromstring(opts.ip)
task:set_from_ip(opts.ip)
else
opts.all = true
end
if opts.from then
local rspamd_parsers = require "rspamd_parsers"
local addr_parsed = rspamd_parsers.parse_mail_address(opts.from)
if addr_parsed then
task:set_from('smtp', addr_parsed[1])
else
io.stderr:write('Invalid from addr\n')
os.exit(1)
end
elseif opts.domain then
task:set_from('smtp', { user = 'user', domain = opts.domain })
else
io.stderr:write('Neither domain nor from specified\n')
os.exit(1)
end
local function flag_to_str(fl)
if bit.band(fl, rspamd_spf.flags.temp_fail) ~= 0 then
return "temporary failure"
elseif bit.band(fl, rspamd_spf.flags.perm_fail) ~= 0 then
return "permanent failure"
elseif bit.band(fl, rspamd_spf.flags.na) ~= 0 then
return "no spf record"
end
return "unknown flag: " .. tostring(fl)
end
local function display_spf_results(elt, colored)
local dec = function(e)
return e
end
local policy_decode = function(e)
if e == rspamd_spf.policy.fail then
return 'reject'
elseif e == rspamd_spf.policy.pass then
return 'pass'
elseif e == rspamd_spf.policy.soft_fail then
return 'soft fail'
elseif e == rspamd_spf.policy.neutral then
return 'neutral'
end
return 'unknown'
end
if colored then
dec = function(e)
return highlight(e)
end
if elt.result == rspamd_spf.policy.pass then
dec = function(e)
return green(e)
end
elseif elt.result == rspamd_spf.policy.fail then
dec = function(e)
return red(e)
end
end
end
printf('%s: %s', highlight('Policy'), dec(policy_decode(elt.result)))
printf('%s: %s', highlight('Network'), dec(elt.addr))
if elt.str then
printf('%s: %s', highlight('Original'), elt.str)
end
end
local function cb(record, flags, err)
if record then
local result, flag_or_policy, error_or_addr
if opts.ip then
result, flag_or_policy, error_or_addr = record:check_ip(opts.ip)
elseif opts.all then
result = true
end
if opts.ip and not opts.all then
if result then
display_spf_results(error_or_addr, true)
else
printf('Not matched: %s', error_or_addr)
end
os.exit(0)
end
if result then
printf('SPF record for %s; digest: %s',
highlight(opts.domain or opts.from), highlight(record:get_digest()))
for _, elt in ipairs(record:get_elts()) do
if result and error_or_addr and elt.str and elt.str == error_or_addr.str then
printf("%s", highlight('*** Matched ***'))
display_spf_results(elt, true)
printf('------')
else
display_spf_results(elt, false)
printf('------')
end
end
else
printf('Error getting SPF record: %s (%s flag)', err,
flag_to_str(flag_or_policy or flags))
end
else
printf('Cannot get SPF record: %s', err)
end
end
rspamd_spf.resolve(task, cb)
end
local function handler(args)
local opts = parser:parse(args)
load_config(opts)
local command = opts.command
if command == 'spf' then
spf_handler(opts)
else
parser:error('command %s is not implemented', command)
end
end
return {
name = 'dnstool',
aliases = { 'dns', 'dns_tool' },
handler = handler,
description = parser._description
}
|