Software /
code /
prosody-modules
File
mod_dnsbl/mod_dnsbl.lua @ 6195:886c985ece61
mod_lastlog2: Skip initializing internal API (and storage) in prosodyctl
Initializing storage in the global context under prosodyctl causes the
module.command to fail to execute because the storage module has already
been loaded.
Introduced in 7b722955c59b
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Sat, 08 Feb 2025 14:12:18 +0100 |
parent | 6161:99860e1b817d |
line wrap: on
line source
local lfs = require "lfs"; local adns = require "net.adns"; local it = require "util.iterators"; local parse_cidr = require "util.ip".parse_cidr; local parse_ip = require "util.ip".new_ip; local promise = require "util.promise"; local set = require "util.set"; local st = require "util.stanza"; local render_message = require "util.interpolation".new("%b{}", function (s) return s; end); local trie = module:require("mod_anti_spam/trie"); local dnsbls_config_raw = module:get_option("dnsbls"); local default_dnsbl_flag = module:get_option_string("dnsbl_flag", "dnsbl_hit"); local default_dnsbl_message = module:get_option("dnsbl_message"); if not dnsbls_config_raw then module:log_status("error", "No 'dnsbls' in config file"); return; end local dnsbls = set.new(); local dnsbls_config = {}; for k, v in ipairs(dnsbls_config_raw) do local dnsbl_name, dnsbl_config; if type(k) == "string" then dnsbl_name = k; dnsbl_config = v; else dnsbl_name = v; dnsbl_config = {}; end dnsbls:add(dnsbl_name); dnsbls_config[dnsbl_name] = dnsbl_config; end local function read_dnsbl_file(filename) local t = trie.new(); local f, err = io.open(filename); if not f then module:log("error", "Failed to read file: %s", err); return t; end local n_line, n_added = 0, 0; for line in f:lines() do n_line = n_line + 1; line = line:gsub("#.+$", ""):match("^%s*(.-)%s*$"); if line == "" then -- luacheck: ignore 542 -- Skip else local parsed_ip, parsed_bits = parse_cidr(line); if not parsed_ip then -- Skip module:log("warn", "Failed to parse IP/CIDR on %s:%d", filename, n_line); else if not parsed_bits then -- Default to full length of IP address parsed_bits = #parsed_ip.packed * 8; end t:add_subnet(parsed_ip, parsed_bits); n_added = n_added + 1; end end end module:log("info", "Loaded %d entries from %s", n_added, filename); return t; end local ipsets = {}; local ipsets_last_updated = {}; function reload_file_dnsbls() for dnsbl in dnsbls do if dnsbl:byte(1) == 64 then -- '@' local filename = dnsbl:sub(2); local file_last_updated = lfs.attributes(filename, "change"); if (ipsets_last_updated[dnsbl] or 0) < file_last_updated then ipsets[dnsbl] = read_dnsbl_file(filename); ipsets_last_updated[dnsbl] = file_last_updated; end end end end module:hook_global("config-reloaded", reload_file_dnsbls); reload_file_dnsbls(); local mod_flags = module:depends("flags"); local function reverse(ip, suffix) local a,b,c,d = ip:match("^(%d+).(%d+).(%d+).(%d+)$"); if not a then return end return ("%d.%d.%d.%d.%s"):format(d,c,b,a, suffix); end function check_dnsbl(ip_address, dnsbl, callback, ud) if dnsbl:byte(1) == 64 then -- '@' local parsed_ip = parse_ip(ip_address); if not parsed_ip then module:log("warn", "Failed to parse IP address: %s", ip_address); callback(ud, false, dnsbl); return; end callback(ud, not not ipsets[dnsbl]:contains_ip(parsed_ip), dnsbl); return; else if ip_address:sub(1,7):lower() == "::ffff:" then ip_address = ip_address:sub(8); end local rbl_ip = reverse(ip_address, dnsbl); if not rbl_ip then return; end module:log("debug", "Sending DNSBL lookup for %s", ip_address); adns.lookup(function (reply) local hit = not not (reply and reply[1]); module:log("debug", "Received DNSBL result for %s: %s", ip_address, hit and "present" or "absent"); callback(ud, hit, dnsbl); end, rbl_ip); end end local function handle_dnsbl_register_result(registration_event, hit, dnsbl) if not hit then return; end if registration_event.dnsbl_match then return; end registration_event.dnsbl_match = true; local username = registration_event.username; local flag = dnsbls_config[dnsbl].flag or default_dnsbl_flag; module:log("info", "Flagging %s for user %s registered from %s matching %s", flag, username, registration_event.ip, dnsbl); mod_flags:add_flag(username, flag, "Matched "..dnsbl); local msg = dnsbls_config[dnsbl].message or default_dnsbl_message; if msg then module:log("debug", "Sending warning message to %s", username); local msg_stanza = st.message( { to = username.."@"..module.host; from = module.host; }, render_message(msg, { registration = registration_event }) ); module:send(msg_stanza); end end module:hook("user-registered", function (event) local session = event.session; local ip = event.ip or (session and session.ip); if not ip then return; end if not event.ip then event.ip = ip; end for dnsbl in dnsbls do check_dnsbl(ip, dnsbl, handle_dnsbl_register_result, event); end end); module:add_item("account-trait", { name = "register-dnsbl-hit"; prob_bad_true = 0.6; prob_bad_false = 0.4; }); module:hook("get-account-traits", function (event) event.traits["register-dnsbl-hit"] = mod_flags.has_flag(event.username, default_dnsbl_flag); end); module:add_item("shell-command", { section = "dnsbl"; section_desc = "Manage DNS blocklists"; name = "lists"; desc = "Show all lists currently in use on the specified host"; args = { { name = "host", type = "string" }; }; host_selector = "host"; handler = function(self, host) --luacheck: ignore 212/self 212/host local count = 0; for list in dnsbls do count = count + 1; self.session.print(list); end return true, ("%d lists"):format(count); end; }); module:add_item("shell-command", { section = "dnsbl"; section_desc = "Manage DNS blocklists"; name = "check"; desc = "Check an IP against the configured block lists"; args = { { name = "host", type = "string" }; { name = "ip_address", type = "string" }; }; host_selector = "host"; handler = function(self, host, ip_address) --luacheck: ignore 212/self 212/host local parsed_ip = parse_ip(ip_address); if not parsed_ip then return false, "Failed to parse IP address"; end local matches, total = 0, 0; local promises = {}; for dnsbl in dnsbls do total = total + 1; promises[dnsbl] = promise.new(function (resolve) check_dnsbl(parsed_ip, dnsbl, resolve, true); end); end return promise.all_settled(promises):next(function (results) for dnsbl, result in it.sorted_pairs(results) do local msg; if result.status == "fulfilled" then if result.value then msg = "[X]"; matches = matches + 1; else msg = "[ ]"; end else msg = "[?]"; end print(msg, dnsbl); end return ("Found in %d of %d lists"):format(matches, total); end); end; });