From: Vsevolod Stakhov Date: Tue, 28 Jul 2026 09:12:26 +0000 (+0100) Subject: [Fix] rspamadm: resolve SRV based upstreams X-Git-Tag: 4.1.4~1 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=899a8eb62467a66dc4e114064de3a0fff5b12acb;p=thirdparty%2Frspamd.git [Fix] rspamadm: resolve SRV based upstreams rspamadm never called rspamd_upstreams_library_config(), so the upstreams context had neither an event loop nor a resolver and stayed unconfigured. Host names still worked because they are resolved synchronously while the config is parsed, but the SRV form `servers = "service=fuzzy+rspamd.com"` creates a placeholder that is not selectable until asynchronous resolution fills in its members - which was never scheduled. Every fuzzy storage configured that way looked empty: `rspamadm fuzzyping -l` printed no servers at all and pinging failed with "no fuzzy storage upstream available". Configure the library right after the resolver is created, before any command loads its own configuration, so that upstreams created later schedule resolution themselves in rspamd_upstream_set_active(). That alone is not enough for one shot commands: the resolve timer is armed for the next loop iteration while fuzzy_ping issued its first ping straight after loading the config, and the loop only runs while session events are pending. Add lua_fuzzy.wait_for_storages() which waits for the selected rules to have servers (bounded by the request timeout) and use it in fuzzy_ping and fuzzy_hash. An explicit -s/--server override skips the wait since it bypasses the configured upstreams. Also report the rule name instead of nil when a ping cannot be started without a server override. --- diff --git a/lualib/lua_fuzzy.lua b/lualib/lua_fuzzy.lua index be3610626f..38160597fa 100644 --- a/lualib/lua_fuzzy.lua +++ b/lualib/lua_fuzzy.lua @@ -554,4 +554,55 @@ exports.cleanup_rules = function() rules = {} end +-- Returns the list of servers a storage would be queried on, or nil when the +-- upstreams are not resolved yet. `list_storages` reports a single `servers` +-- field when read and write upstreams share one list, and a split pair +-- otherwise +local function storage_servers(storage) + return storage.servers or storage.read_servers +end + +--[[[ +-- @function lua_fuzzy.wait_for_storages(cfg, task, names, timeout, callback) +-- Waits until every selected fuzzy storage has at least one resolved server, +-- then calls `callback()`. `names` is a set of rule names to wait for, or nil +-- to wait for all of them; `callback` is called anyway once `timeout` seconds +-- have passed so that the caller can report a proper error. +-- +-- Server lists built from SRV records (`servers = "service=name+domain"`) or +-- from host names that failed to resolve at configuration time are filled in +-- asynchronously, so right after loading a configuration there is nothing to +-- talk to yet. Workers resolve these while they run; one shot rspamadm +-- commands have to give the event loop a chance to do it first. +--]] +exports.wait_for_storages = function(cfg, task, names, timeout, callback) + local rspamd_util = require "rspamd_util" + local deadline = rspamd_util.get_ticks() + (timeout or 5.0) + + local function all_resolved() + for name, storage in pairs(rspamd_plugins.fuzzy_check.list_storages(cfg)) do + if not names or names[name] then + local servers = storage_servers(storage) + + if not servers or #servers == 0 then + return false + end + end + end + + return true + end + + local function poll() + if all_resolved() or rspamd_util.get_ticks() >= deadline then + callback() + else + -- A timer cannot re-arm itself, so schedule a fresh one on each tick + task:add_timer(0.05, poll) + end + end + + poll() +end + return exports diff --git a/lualib/rspamadm/fuzzy_hash.lua b/lualib/rspamadm/fuzzy_hash.lua index eef68733e9..8f8ac82009 100644 --- a/lualib/rspamadm/fuzzy_hash.lua +++ b/lualib/rspamadm/fuzzy_hash.lua @@ -17,6 +17,7 @@ limitations under the License. local argparse = require "argparse" local ansicolors = require "ansicolors" local rspamd_logger = require "rspamd_logger" +local lua_fuzzy = require "lua_fuzzy" local E = {} @@ -142,12 +143,21 @@ local function make_task(fname) return task end -local function query_hashes(opts) - if not opts.rule then - print(highlight_err('-H/--hash requires an explicit rule (-r)')) - os.exit(1) +-- Storages can only be queried once their upstreams are known: SRV based +-- server lists are resolved asynchronously after the configuration is loaded +local function with_storages(opts, names, cb) + if opts.server then + -- Explicit override, the configured upstreams are not used at all + cb() + return end + -- The task is only needed to drive the event loop while resolving + lua_fuzzy.wait_for_storages(rspamd_config, make_task(nil), names, + opts.timeout, cb) +end + +local function query_hashes(opts) local task = make_task(nil) local ret, err = rspamd_plugins.fuzzy_check.check(task, print_check_result, opts.rule, opts.timeout, opts.hash, opts.server) @@ -193,7 +203,15 @@ local function handler(args) load_config(opts) if #opts.hash > 0 then - query_hashes(opts) + if not opts.rule then + print(highlight_err('-H/--hash requires an explicit rule (-r)')) + os.exit(1) + end + + with_storages(opts, { [opts.rule] = true }, function() + query_hashes(opts) + end) + return end @@ -203,8 +221,17 @@ local function handler(args) local rules = selected_rules(opts) - for _, fname in ipairs(opts.file) do - process_file(opts, fname, rules) + local function process_all() + for _, fname in ipairs(opts.file) do + process_file(opts, fname, rules) + end + end + + if opts.check then + -- `rules` is keyed by rule name, so it doubles as the set to wait for + with_storages(opts, rules, process_all) + else + process_all() end end diff --git a/lualib/rspamadm/fuzzy_ping.lua b/lualib/rspamadm/fuzzy_ping.lua index a5e78ea2ad..fb63b1045b 100644 --- a/lualib/rspamadm/fuzzy_ping.lua +++ b/lualib/rspamadm/fuzzy_ping.lua @@ -18,6 +18,7 @@ local argparse = require "argparse" local ansicolors = require "ansicolors" local rspamd_logger = require "rspamd_logger" local lua_util = require "lua_util" +local lua_fuzzy = require "lua_fuzzy" local E = {} @@ -190,11 +191,6 @@ local function handler(args) load_config(opts) - if opts.list then - print_storages(rspamd_plugins.fuzzy_check.list_storages(rspamd_config)) - os.exit(0) - end - -- Perform ping using a fake task from async stuff provided by rspamadm local rspamd_task = require "rspamd_task" @@ -203,6 +199,16 @@ local function handler(args) task:set_session(rspamadm_session) task:set_resolver(rspamadm_dns_resolver) + if opts.list then + -- The task is only needed to drive the event loop while the upstreams + -- that are resolved asynchronously (e.g. SRV based) are filled in + lua_fuzzy.wait_for_storages(rspamd_config, task, nil, opts.timeout, function() + print_storages(rspamd_plugins.fuzzy_check.list_storages(rspamd_config)) + end) + + return + end + local replied = 0 local results = {} local ping_fuzzy @@ -247,17 +253,27 @@ local function handler(args) opts.rule, opts.timeout, opts.server) if not ret then - print(highlight_err('error from %s: %s', opts.server, err)) + print(highlight_err('error from %s: %s', opts.server or opts.rule, err)) opts.number = opts.number - 1 -- To avoid issues with waiting for other replies end end - if opts.flood then - for i = 1, opts.number do - ping_fuzzy(i) + local function start_pings() + if opts.flood then + for i = 1, opts.number do + ping_fuzzy(i) + end + else + ping_fuzzy(1) end + end + + if opts.server then + -- Explicit override, the configured upstreams are not used at all + start_pings() else - ping_fuzzy(1) + lua_fuzzy.wait_for_storages(rspamd_config, task, { [opts.rule] = true }, + opts.timeout, start_pings) end end diff --git a/src/rspamadm/rspamadm.c b/src/rspamadm/rspamadm.c index e3b95895c7..45bc71a69f 100644 --- a/src/rspamadm/rspamadm.c +++ b/src/rspamadm/rspamadm.c @@ -460,6 +460,20 @@ int main(int argc, char **argv, char **env) rspamd_main->http_ctx = rspamd_http_context_create(cfg, rspamd_main->event_loop, NULL); + /* + * Wire the upstreams library to our event loop and resolver. Without this + * `ups_ctx->configured` stays FALSE, so upstreams that need DNS - SRV + * placeholders created from `service=name+domain` and hostnames deferred + * after a failed config time lookup - are never resolved and the lists stay + * empty (e.g. `rspamadm fuzzyping` would see no servers at all). + * + * This must happen before any command loads its own configuration: the + * upstreams are created later, and they only self-schedule resolution in + * rspamd_upstream_set_active() when the context is already configured. + */ + rspamd_upstreams_library_config(cfg, cfg->ups_ctx, rspamd_main->event_loop, + resolver->r); + g_log_set_default_handler(rspamd_glib_log_function, rspamd_main->logger); g_set_printerr_handler(rspamd_glib_printerr_function); rspamd_config_post_load(cfg,