-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmalware-filter.lua
227 lines (210 loc) · 7.78 KB
/
malware-filter.lua
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
local re
local re_pattern_adblock='^(\\|){2}(.*)\\^$' -- Matches Adblock Format
local re_chars="(.*)[$^|](.*)" -- Matches PCRE Format
local re_wild="^[*.]" -- Matches Wildcard Format
if f.isModuleAvailable("rex_pcre") then
re = require"rex_pcre"
elseif f.isModuleAvailable("rex_pcre2") then
re = require"rex_pcre2"
else
pdnslog("pdns-recursor-scripts malware-filter.lua requires rex_pcre or rex_pcre2 to be installed", pdns.loglevels.Error)
return false
end
-- loads contents of a file line by line into the given table
function load_dnsbl_file(filename, dnsbl_list, rebl_list, wildbl_list)
if f.fileExists(filename) then
local ignored_lines = 0
for line in io.lines(filename) do
if f.is_comment(line) or f.empty_str(line) then
ignored_lines = ignored_lines + 1
goto continue
end -- Ignore commented lines
if re.match(line, re_pattern_adblock) then -- ADBLOCK FORMAT
local stripped = string.gsub(line, "||", "")
stripped = string.gsub(stripped, "%^", "") -- Escape Special character ^ with %
dnsbl_list:add(stripped)
-- pdnslog("load_dnsbl_file(): ".."Adblock Pattern (Converted to wildcard): "..stripped, pdns.loglevels.Notice)
elseif re.match(line, re_chars) then -- PCRE FORMAT
table.insert(rebl_list, line)
-- pdnslog("load_dnsbl_file(): ".."PCRE Pattern: "..wilded, pdns.loglevels.Notice)
elseif re.match(line, re_wild) then -- WILDCARD FORMAT
local wilded = string.gsub(line, '*%.', "") -- Escape Special character . with %
wilded = ".*\\.*?"..string.gsub(wilded, '%.', "\\.") -- Escape Special Characters
table.insert(wildbl_list, wilded)
-- pdnslog("load_dnsbl_file(): ".."Wildcard Pattern: "..wilded, pdns.loglevels.Notice)
else -- STANDARD HOSTS FORMAT
dnsbl_list:add(line)
end
::continue::
end
pdnslog("load_dnsbl_file(): " .. filename .. " successfully loaded", pdns.loglevels.Notice)
if ignored_lines >= 1 then
pdnslog("load_dnsbl_file(): " .. ignored_lines .. " lines ignored", pdns.loglevels.Notice)
end
else
pdnslog("load_dnsbl_file(): could not open file " .. filename, pdns.loglevels.Warning)
end
end
-- loads contents of a file line by line into the given table
function load_ipbl_file(filename, ipbl_list, cidrbl_list)
if f.fileExists(filename) then
for line in io.lines(filename) do
-- pdnslog("load_ipbl_file(): parsing line " .. line, pdns.loglevels.Notice)
if f.is_comment(line) or f.empty_str(line) then goto continue end -- Ignore commented lines
if string.find(line, "/") then -- Check if it's a CIDR
-- pdnslog("load_ipbl_file(): loaded CIDR " .. line, pdns.loglevels.Notice)
cidrbl_list:addMask(line)
else -- Assume IP Address
-- pdnslog("load_ipbl_file(): loaded IP " .. line, pdns.loglevels.Notice)
ipbl_list:add(line)
end
::continue::
end
pdnslog("load_ipbl_file(): " .. filename .. " successfully loaded", pdns.loglevels.Notice)
else
pdnslog("load_ipbl_file(): could not open file " .. filename, pdns.loglevels.Warning)
end
end
local function get_list_files_in_dir(search_dir)
local files = {}
for dir in io.popen("ls -pa " .. search_dir .. " | grep -v /|grep -E \"*(.list|.txt)\""):lines()
do
table.insert(files, search_dir .. "/" .. dir)
end
return files
end
local function get_sinkhole(dq)
if dq.qtype == pdns.A or dq.qtype == pdns.ANY then
return {
["0.0.0.0"]=pdns.A
}
elseif dq.qtype == pdns.AAAA or dq.qtype == pdns.ANY then
return {
["::"]=pdns.AAAA
}
-- elseif dq.qtype == pdns.CNAME or dq.qtype == pdns.ANY then
-- return {
-- ["0.0.0.0"]=pdns.A,
-- ["::"]=pdns.AAAA
-- }
end
if dq.qtype == pdns.HTTPS then
return {
["1 . alpn=h3,h3-29,h2 ipv4hint=0.0.0.0 ipv6hint=::"]=pdns.HTTPS
}
end
return nil
end
local function is_whitelisted(qname_no_trailing_dot)
if not g.options.dnsbl_whitelist then return false end
if f.table_len(g.options.dnsbl_whitelist) < 1 then return false end
if not f.table_contains(g.options.dnsbl_whitelist, tostring(qname_no_trailing_dot)) then
return false
end
return true
end
-- this function is hooked before resolving starts
local function preresolve_mf(dq)
-- If the last char is a dot, remove it (for non-canonical format)
local qname_no_trailing_dot = f.qname_remove_trailing_dot(dq)
local dq_whitelisted = is_whitelisted(qname_no_trailing_dot)
if dq_whitelisted then return false end
local resolve_sinkhole = dnsbl:check(dq.qname) -- NORMAL BL Check
-- PCRE BL Check
if not resolve_sinkhole then
for k, p in pairs(rebl) do
if resolve_sinkhole then break end
-- Log PCRE Pattern
-- pdnslog("PCRE Pattern: ".. qname_no_trailing_dot .." | " .. tostring(p), pdns.loglevels.Notice)
resolve_sinkhole = re.match(qname_no_trailing_dot, p)
end
end
-- WILDCARD BL Check
if not resolve_sinkhole then
for k, p in pairs(wildbl) do
if resolve_sinkhole then break end
-- Log Wildcard Pattern
-- pdnslog("WILD Pattern: ".. qname_no_trailing_dot .." | " .. tostring(p), pdns.loglevels.Notice)
resolve_sinkhole = re.match(qname_no_trailing_dot, p)
end
end
-- pdnslog("Pre-Resolving " .. tostring(dq.qname), pdns.loglevels.Notice)
-- pdnslog("PCRE Check: " .. tostring(re_check), pdns.loglevels.Notice)
-- pdnslog("DNSBL Check: " .. tostring(ds_check), pdns.loglevels.Notice)
-- check blocklist
if resolve_sinkhole then
local sinkhole = get_sinkhole(dq)
if not sinkhole then
dq.appliedPolicy.policyKind = pdns.policykinds.Drop
return false
end
for dq_val, dq_qtype in pairs(sinkhole) do
dq:addAnswer(dq_qtype, dq_val, g.options.default_ttl)
end
filterlist_metric:inc()
return true
end
-- default, do not rewrite this response
return false
end
local function postresolve_mf(dq)
local dq_records = dq:getRecords()
local ip_check = false
local cidr_check = false
if dq.qtype ~= pdns.A and dq.qtype ~= pdns.AAAA then return false end
for key, dr in pairs(dq_records) do
local dr_content = dr:getContent()
if not dr_content or dr_content == nil then return false end
-- Call function without raising exception to parent process
local code, dr_ca = pcall(newCA, dr_content)
if not code then return false end
-- pdnslog("DNSR Content: " .. dr_ca:toString(), pdns.loglevels.Notice)
if not ip_check and not cidr_check then
cidr_check = cidrbl:match(dr_ca)
ip_check = ipbl:check(dr_ca)
end
if ip_check or cidr_check then
for dq_val, dq_qtype in pairs(get_sinkhole(dq)) do
dr:changeContent(dq_val)
end
end
-- pdnslog("Post-Resolving " .. tostring(dq.qname), pdns.loglevels.Notice)
-- pdnslog("IP Check: " .. tostring(ip_check), pdns.loglevels.Notice)
-- pdnslog("CIDR Check: " .. tostring(cidr_check), pdns.loglevels.Notice)
::continue::
end
if ip_check or cidr_check then
dq:setRecords(dq_records)
return true
end
return false
end
local dnsbl_file_table = get_list_files_in_dir(g.pdns_scripts_path.."/dnsbl.d")
local ipbl_file_table = get_list_files_in_dir(g.pdns_scripts_path.."/ipbl.d")
dnsbl = newDS()
rebl = {}
wildbl = {}
ipbl = newCAS()
cidrbl = newNMG()
-- get metrics
filterlist_metric = getMetric("filterlist_hits")
if g.options.use_dnsbl then
for key, filename in pairs(dnsbl_file_table) do
load_dnsbl_file(filename, dnsbl, rebl, wildbl)
end
-- pdnslog("Loading preresolve_mf into pre-resolve functions.", pdns.loglevels.Notice)
-- Add preresolve function to table
f.addResolveFunction("pre", "preresolve_mf", preresolve_mf)
else
pdnslog("DNSBL Function not enabled. Set overrides in file overrides.lua", pdns.loglevels.Notice)
end
if g.options.use_ipbl then
for key, filename in pairs(ipbl_file_table) do
load_ipbl_file(filename, ipbl, cidrbl)
end
-- pdnslog("Loading postresolve_mf into post-resolve functions.", pdns.loglevels.Notice)
-- Add postresolve function to table
f.addResolveFunction("post", "postresolve_mf", postresolve_mf)
else
pdnslog("IPBL Function not enabled. Set overrides in file overrides.lua", pdns.loglevels.Notice)
end