Software /
code /
prosody-modules
Changeset
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 |
parents | 6131:f80db102fdb1 |
children | 6133:a0429c322454 |
files | mod_anti_spam/mod_anti_spam.lua mod_flags/mod_flags.lua |
diffstat | 2 files changed, 291 insertions(+), 6 deletions(-) [+] |
line wrap: on
line diff
--- a/mod_anti_spam/mod_anti_spam.lua Wed Jan 01 14:15:20 2025 +0000 +++ b/mod_anti_spam/mod_anti_spam.lua Sat Jan 04 17:50:35 2025 +0000 @@ -1,5 +1,7 @@ +local cache = require "util.cache"; local ip = require "util.ip"; local jid_bare = require "util.jid".bare; +local jid_host = require "util.jid".host; local jid_split = require "util.jid".split; local set = require "util.set"; local sha256 = require "util.hashes".sha256; @@ -11,10 +13,26 @@ local new_rtbl_subscription = module:require("rtbl").new_rtbl_subscription; local trie = module:require("trie"); +local pset = module:require("pset"); -local spam_source_domains = set.new(); -local spam_source_ips = trie.new(); -local spam_source_jids = set.new(); +-- { [service_jid] = set, ... } +local spam_source_domains_by_service = {}; +local spam_source_ips_by_service = {}; +local spam_source_jids_by_service = {}; + +local service_probabilities = { + -- if_present = probability the address is a spammer if they are on the list + -- if_absent (optional): probability the address is a spammer if they are not on the list + -- [service_jid] = { if_present = 0.9, if_absent = 0.5 }; +}; + + +-- These "probabilistic sets" combine the multiple lists according to their weights +local p_spam_source_domains = pset.new(spam_source_domains_by_service, service_probabilities); +local p_spam_source_ips = pset.new(spam_source_ips_by_service, service_probabilities); +local p_spam_source_jids = pset.new(spam_source_jids_by_service, service_probabilities); + +local domain_local_report_threshold = module:get_option_number("anti_spam_local_report_threshold", 2); local count_spam_blocked = module:metric("counter", "anti_spam_blocked", "stanzas", "Stanzas blocked as spam", {"reason"}); @@ -67,20 +85,20 @@ end function is_spammy_server(session) - if spam_source_domains:contains(session.from_host) then + if p_spam_source_domains:contains(session.from_host) then return true; end local raw_ip = session.ip; local parsed_ip = raw_ip and ip.new_ip(session.ip); -- Not every session has an ip - for example, stanzas sent from a -- local host session - if parsed_ip and spam_source_ips:contains_ip(parsed_ip) then + if parsed_ip and p_spam_source_ips:contains_ip(parsed_ip) then return true; end end function is_spammy_sender(sender_jid) - return spam_source_jids:contains(sha256(sender_jid, true)); + return p_spam_source_jids:contains(sha256(sender_jid, true)); end local spammy_strings = module:get_option_array("anti_spam_block_strings"); @@ -115,6 +133,16 @@ local anti_spam_services = module:get_option_array("anti_spam_services", {}); for _, rtbl_service_jid in ipairs(anti_spam_services) do + service_probabilities[rtbl_service_jid] = { if_present = 0.95 }; + + local spam_source_domains = set.new(); + local spam_source_ips = trie.new(); + local spam_source_jids = set.new(); + + spam_source_domains_by_service[rtbl_service_jid] = spam_source_domains; + spam_source_ips_by_service[rtbl_service_jid] = spam_source_ips; + spam_source_jids_by_service[rtbl_service_jid] = spam_source_jids; + new_rtbl_subscription(rtbl_service_jid, "spam_source_domains", { added = function (item) spam_source_domains:add(item); @@ -149,6 +177,68 @@ }); end +-- And local reports... + +do + local spam_source_domains = set.new(); + local spam_source_ips = set.new(); + + local domain_counts = cache.new(100); + + service_probabilities[module.host] = { if_present = 0.6, if_absent = 0.4 }; + + module:hook("mod_spam_reporting/spam-report", function (event) + -- TODO: check for >= prosody:member + local reported_jid = event.jid; + local reported_domain = jid_host(reported_jid); + local report_count = (domain_counts:get(reported_domain) or 0) + 1; + domain_counts:set(reported_domain, report_count); + + if report_count >= domain_local_report_threshold then + spam_source_domains:add(reported_domain); + end + end); + + module:add_item("shell-command", { + section = "antispam"; + section_desc = "Anti-spam management commands"; + name = "filter_domain"; + desc = "Restrict interactions from a remote domain to a virtual host"; + args = { + { name = "host", type = "string" }; + { name = "remote_domain", type = "string" }; + }; + host_selector = "host"; + handler = function(self, host, remote_domain) --luacheck: ignore 212/self 212/host + spam_source_domains:add(remote_domain); + return true, "Remote domain now restricted: "..remote_domain; + end; + }); + + module:add_item("shell-command", { + section = "antispam"; + section_desc = "Anti-spam management commands"; + name = "filter_ip"; + desc = "Restrict interactions from a remote IP/CIDR to a virtual host"; + args = { + { name = "host", type = "string" }; + { name = "remote_ip", type = "string" }; + }; + host_selector = "host"; + handler = function(self, host, remote_ip) --luacheck: ignore 212/self 212/host + local subnet_ip, subnet_bits = ip.parse_cidr(remote_ip); + if not subnet_ip then + return false, subnet_bits; -- false, err + end + + spam_source_ips:add_subnet(subnet_ip, subnet_bits); + + return true, "Remote IP now restricted: "..remote_ip; + end; + }); + +end + module:hook("message/bare", function (event) local to_user, to_host = jid_split(event.stanza.attr.to); @@ -200,3 +290,4 @@ module:log("debug", "Allowing subscription request through"); end, 500); +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_flags/mod_flags.lua Sat Jan 04 17:50:35 2025 +0000 @@ -0,0 +1,194 @@ +-- This module is only for 0.12, later versions have mod_flags bundled +--% conflicts: mod_flags + +local flags_map; +if prosody.process_type ~= "prosodyctl" then + flags_map = module:open_store("account_flags", "map"); +end + +-- API + +function add_flag(username, flag, comment) -- luacheck: ignore 131/add_flag + local flag_data = { + when = os.time(); + comment = comment; + }; + + local ok, err = flags_map:set(username, flag, flag_data); + if not ok then + return nil, err; + end + + module:fire_event("user-flag-added/"..flag, { + user = username; + flag = flag; + data = flag_data; + }); + + return true; +end + +function remove_flag(username, flag) -- luacheck: ignore 131/remove_flag + local ok, err = flags_map:set(username, flag, nil); + if not ok then + return nil, err; + end + + module:fire_event("user-flag-removed/"..flag, { + user = username; + flag = flag; + }); + + return true; +end + +function has_flag(username, flag) -- luacheck: ignore 131/has_flag + local ok, err = flags_map:get(username, flag); + if not ok and err then + error("Failed to check flags for user: "..err); + end + return not not ok; +end + +function get_flag_info(username, flag) -- luacheck: ignore 131/get_flag_info + return flags_map:get(username, flag); +end + + +-- Migration from mod_firewall marks + +local function migrate_marks(host) + local usermanager = require "core.usermanager"; + + local flag_storage = module:open_store("account_flags"); + local mark_storage = module:open_store("firewall_marks"); + + local migration_comment = "Migrated from mod_firewall marks at "..os.date("%Y-%m-%d %R"); + + local migrated, empty, errors = 0, 0, 0; + for username in usermanager.users(host) do + local marks, err = mark_storage:get(username); + if marks then + local flags = {}; + for mark_name, mark_timestamp in pairs(marks) do + flags[mark_name] = { + when = mark_timestamp; + comment = migration_comment; + }; + end + local saved_ok, saved_err = flag_storage:set(username, flags); + if saved_ok then + prosody.log("error", "Failed to save flags for %s: %s", username, saved_err); + migrated = migrated + 1; + else + errors = errors + 1; + end + elseif err then + prosody.log("error", "Failed to load marks for %s: %s", username, err); + errors = errors + 1; + else + empty = empty + 1; + end + end + + print(("Finished - %d migrated, %d users with no marks, %d errors"):format(migrated, empty, errors)); +end + +function module.command(arg) + local storagemanager = require "core.storagemanager"; + local usermanager = require "core.usermanager"; + local jid = require "util.jid"; + local warn = require"util.prosodyctl".show_warning; + + local command = arg[1]; + if not command then + warn("Valid subcommands: migrate_marks"); + return 0; + end + table.remove(arg, 1); + + local node, host = jid.prepped_split(arg[1]); + if not host then + warn("Please specify a host or JID after the command"); + return 1; + elseif not prosody.hosts[host] then + warn("Unknown host: "..host); + return 1; + end + + table.remove(arg, 1); + + module.host = host; -- luacheck: ignore 122 + storagemanager.initialize_host(host); + usermanager.initialize_host(host); + + flags_map = module:open_store("account_flags", "map"); + + if command == "migrate_marks" then + migrate_marks(host); + return 0; + elseif command == "find" then + local flag = assert(arg[1], "expected argument: flag"); + local flags = module:open_store("account_flags", "map"); + local users_with_flag = flags:get_all(flag); + + local c = 0; + for user, flag_data in pairs(users_with_flag) do + print(user, os.date("%Y-%m-%d %R", flag_data.when), flag_data.comment or ""); + c = c + 1; + end + + print(("%d accounts listed"):format(c)); + return 1; + elseif command == "add" then + local username = assert(node, "expected a user JID, got "..host); + local flag = assert(arg[1], "expected argument: flag"); + local comment = arg[2]; + + local ok, err = add_flag(username, flag, comment); + if not ok then + print("Failed to add flag: "..err); + return 1; + end + + print("Flag added"); + return 1; + elseif command == "remove" then + local username = assert(node, "expected a user JID, got "..host); + local flag = assert(arg[1], "expected argument: flag"); + + local ok, err = remove_flag(username, flag); + if not ok then + print("Failed to remove flag: "..err); + return 1; + end + + print("Flag removed"); + return 1; + elseif command == "list" then + local username = assert(node, "expected a user JID, got "..host); + + local c = 0; + + local flags = module:open_store("account_flags"); + local user_flags, err = flags:get(username); + + if not user_flags and err then + print("Unable to list flags: "..err); + return 1; + end + + if user_flags then + for flag_name, flag_data in pairs(user_flags) do + print(flag_name, os.date("%Y-%m-%d %R", flag_data.when), flag_data.comment or ""); + c = c + 1; + end + end + + print(("%d flags listed"):format(c)); + return 0; + else + warn("Unknown command: %s", command); + return 1; + end +end