Software /
code /
prosody-modules
Comparison
mod_anti_spam/mod_anti_spam.lua @ 6132:ffec70ddbffc
mod_flags: trunk version backported to 0.12
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Sat, 04 Jan 2025 17:50:35 +0000 (2 months ago) |
parent | 6130:5a0e47ad7d6b |
child | 6134:00b55c7ef393 |
comparison
equal
deleted
inserted
replaced
6131:f80db102fdb1 | 6132:ffec70ddbffc |
---|---|
1 local cache = require "util.cache"; | |
1 local ip = require "util.ip"; | 2 local ip = require "util.ip"; |
2 local jid_bare = require "util.jid".bare; | 3 local jid_bare = require "util.jid".bare; |
4 local jid_host = require "util.jid".host; | |
3 local jid_split = require "util.jid".split; | 5 local jid_split = require "util.jid".split; |
4 local set = require "util.set"; | 6 local set = require "util.set"; |
5 local sha256 = require "util.hashes".sha256; | 7 local sha256 = require "util.hashes".sha256; |
6 local st = require"util.stanza"; | 8 local st = require"util.stanza"; |
7 local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; | 9 local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; |
9 | 11 |
10 local user_exists = require "core.usermanager".user_exists; | 12 local user_exists = require "core.usermanager".user_exists; |
11 | 13 |
12 local new_rtbl_subscription = module:require("rtbl").new_rtbl_subscription; | 14 local new_rtbl_subscription = module:require("rtbl").new_rtbl_subscription; |
13 local trie = module:require("trie"); | 15 local trie = module:require("trie"); |
14 | 16 local pset = module:require("pset"); |
15 local spam_source_domains = set.new(); | 17 |
16 local spam_source_ips = trie.new(); | 18 -- { [service_jid] = set, ... } |
17 local spam_source_jids = set.new(); | 19 local spam_source_domains_by_service = {}; |
20 local spam_source_ips_by_service = {}; | |
21 local spam_source_jids_by_service = {}; | |
22 | |
23 local service_probabilities = { | |
24 -- if_present = probability the address is a spammer if they are on the list | |
25 -- if_absent (optional): probability the address is a spammer if they are not on the list | |
26 -- [service_jid] = { if_present = 0.9, if_absent = 0.5 }; | |
27 }; | |
28 | |
29 | |
30 -- These "probabilistic sets" combine the multiple lists according to their weights | |
31 local p_spam_source_domains = pset.new(spam_source_domains_by_service, service_probabilities); | |
32 local p_spam_source_ips = pset.new(spam_source_ips_by_service, service_probabilities); | |
33 local p_spam_source_jids = pset.new(spam_source_jids_by_service, service_probabilities); | |
34 | |
35 local domain_local_report_threshold = module:get_option_number("anti_spam_local_report_threshold", 2); | |
18 | 36 |
19 local count_spam_blocked = module:metric("counter", "anti_spam_blocked", "stanzas", "Stanzas blocked as spam", {"reason"}); | 37 local count_spam_blocked = module:metric("counter", "anti_spam_blocked", "stanzas", "Stanzas blocked as spam", {"reason"}); |
20 | 38 |
21 local hosts = prosody.hosts; | 39 local hosts = prosody.hosts; |
22 | 40 |
65 return true; -- Stranger danger | 83 return true; -- Stranger danger |
66 end | 84 end |
67 end | 85 end |
68 | 86 |
69 function is_spammy_server(session) | 87 function is_spammy_server(session) |
70 if spam_source_domains:contains(session.from_host) then | 88 if p_spam_source_domains:contains(session.from_host) then |
71 return true; | 89 return true; |
72 end | 90 end |
73 local raw_ip = session.ip; | 91 local raw_ip = session.ip; |
74 local parsed_ip = raw_ip and ip.new_ip(session.ip); | 92 local parsed_ip = raw_ip and ip.new_ip(session.ip); |
75 -- Not every session has an ip - for example, stanzas sent from a | 93 -- Not every session has an ip - for example, stanzas sent from a |
76 -- local host session | 94 -- local host session |
77 if parsed_ip and spam_source_ips:contains_ip(parsed_ip) then | 95 if parsed_ip and p_spam_source_ips:contains_ip(parsed_ip) then |
78 return true; | 96 return true; |
79 end | 97 end |
80 end | 98 end |
81 | 99 |
82 function is_spammy_sender(sender_jid) | 100 function is_spammy_sender(sender_jid) |
83 return spam_source_jids:contains(sha256(sender_jid, true)); | 101 return p_spam_source_jids:contains(sha256(sender_jid, true)); |
84 end | 102 end |
85 | 103 |
86 local spammy_strings = module:get_option_array("anti_spam_block_strings"); | 104 local spammy_strings = module:get_option_array("anti_spam_block_strings"); |
87 local spammy_patterns = module:get_option_array("anti_spam_block_patterns"); | 105 local spammy_patterns = module:get_option_array("anti_spam_block_patterns"); |
88 | 106 |
113 -- Set up RTBLs | 131 -- Set up RTBLs |
114 | 132 |
115 local anti_spam_services = module:get_option_array("anti_spam_services", {}); | 133 local anti_spam_services = module:get_option_array("anti_spam_services", {}); |
116 | 134 |
117 for _, rtbl_service_jid in ipairs(anti_spam_services) do | 135 for _, rtbl_service_jid in ipairs(anti_spam_services) do |
136 service_probabilities[rtbl_service_jid] = { if_present = 0.95 }; | |
137 | |
138 local spam_source_domains = set.new(); | |
139 local spam_source_ips = trie.new(); | |
140 local spam_source_jids = set.new(); | |
141 | |
142 spam_source_domains_by_service[rtbl_service_jid] = spam_source_domains; | |
143 spam_source_ips_by_service[rtbl_service_jid] = spam_source_ips; | |
144 spam_source_jids_by_service[rtbl_service_jid] = spam_source_jids; | |
145 | |
118 new_rtbl_subscription(rtbl_service_jid, "spam_source_domains", { | 146 new_rtbl_subscription(rtbl_service_jid, "spam_source_domains", { |
119 added = function (item) | 147 added = function (item) |
120 spam_source_domains:add(item); | 148 spam_source_domains:add(item); |
121 end; | 149 end; |
122 removed = function (item) | 150 removed = function (item) |
147 spam_source_jids:remove(item); | 175 spam_source_jids:remove(item); |
148 end; | 176 end; |
149 }); | 177 }); |
150 end | 178 end |
151 | 179 |
180 -- And local reports... | |
181 | |
182 do | |
183 local spam_source_domains = set.new(); | |
184 local spam_source_ips = set.new(); | |
185 | |
186 local domain_counts = cache.new(100); | |
187 | |
188 service_probabilities[module.host] = { if_present = 0.6, if_absent = 0.4 }; | |
189 | |
190 module:hook("mod_spam_reporting/spam-report", function (event) | |
191 -- TODO: check for >= prosody:member | |
192 local reported_jid = event.jid; | |
193 local reported_domain = jid_host(reported_jid); | |
194 local report_count = (domain_counts:get(reported_domain) or 0) + 1; | |
195 domain_counts:set(reported_domain, report_count); | |
196 | |
197 if report_count >= domain_local_report_threshold then | |
198 spam_source_domains:add(reported_domain); | |
199 end | |
200 end); | |
201 | |
202 module:add_item("shell-command", { | |
203 section = "antispam"; | |
204 section_desc = "Anti-spam management commands"; | |
205 name = "filter_domain"; | |
206 desc = "Restrict interactions from a remote domain to a virtual host"; | |
207 args = { | |
208 { name = "host", type = "string" }; | |
209 { name = "remote_domain", type = "string" }; | |
210 }; | |
211 host_selector = "host"; | |
212 handler = function(self, host, remote_domain) --luacheck: ignore 212/self 212/host | |
213 spam_source_domains:add(remote_domain); | |
214 return true, "Remote domain now restricted: "..remote_domain; | |
215 end; | |
216 }); | |
217 | |
218 module:add_item("shell-command", { | |
219 section = "antispam"; | |
220 section_desc = "Anti-spam management commands"; | |
221 name = "filter_ip"; | |
222 desc = "Restrict interactions from a remote IP/CIDR to a virtual host"; | |
223 args = { | |
224 { name = "host", type = "string" }; | |
225 { name = "remote_ip", type = "string" }; | |
226 }; | |
227 host_selector = "host"; | |
228 handler = function(self, host, remote_ip) --luacheck: ignore 212/self 212/host | |
229 local subnet_ip, subnet_bits = ip.parse_cidr(remote_ip); | |
230 if not subnet_ip then | |
231 return false, subnet_bits; -- false, err | |
232 end | |
233 | |
234 spam_source_ips:add_subnet(subnet_ip, subnet_bits); | |
235 | |
236 return true, "Remote IP now restricted: "..remote_ip; | |
237 end; | |
238 }); | |
239 | |
240 end | |
241 | |
152 module:hook("message/bare", function (event) | 242 module:hook("message/bare", function (event) |
153 local to_user, to_host = jid_split(event.stanza.attr.to); | 243 local to_user, to_host = jid_split(event.stanza.attr.to); |
154 | 244 |
155 if not hosts[to_host] then | 245 if not hosts[to_host] then |
156 module:log("warn", "Skipping filtering of message to unknown host <%s>", to_host); | 246 module:log("warn", "Skipping filtering of message to unknown host <%s>", to_host); |
198 | 288 |
199 module:log("debug", "Not from known spam source JID"); | 289 module:log("debug", "Not from known spam source JID"); |
200 | 290 |
201 module:log("debug", "Allowing subscription request through"); | 291 module:log("debug", "Allowing subscription request through"); |
202 end, 500); | 292 end, 500); |
293 |