]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] rspamadm: resolve SRV based upstreams
authorVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 28 Jul 2026 09:12:26 +0000 (10:12 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 28 Jul 2026 09:15:18 +0000 (10:15 +0100)
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.

lualib/lua_fuzzy.lua
lualib/rspamadm/fuzzy_hash.lua
lualib/rspamadm/fuzzy_ping.lua
src/rspamadm/rspamadm.c

index be3610626f5d11001c624956f200131d1200b6a8..38160597fa3968b5d96d0b19089db35214d7d22a 100644 (file)
@@ -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
index eef68733e948d83ea84d68a59ddf304e5cf009df..8f8ac82009cd618f33aef6b724a885cb5c67a5d8 100644 (file)
@@ -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
 
index a5e78ea2adf425be56bf8aeb9efd75f79bd848fc..fb63b1045bbeea7256db05ae01027ebccbd64a4c 100644 (file)
@@ -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
 
index e3b95895c7377a0c27678886f9cd707bc973be05..45bc71a69f1ad5e6f5891e3d097dade52a98bf1a 100644 (file)
@@ -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,