Vsevolod Stakhov [Sat, 15 Nov 2025 15:53:17 +0000 (15:53 +0000)]
[Fix] Encode redirect URLs to handle unencoded spaces and special characters
This fixes issue #5525 where url_redirector fails when redirect Location
headers contain unencoded spaces or other special characters.
The http_parser_parse_url() function strictly requires percent-encoded URLs
per RFC 3986, but many servers send Location headers with unencoded spaces.
Changes:
- Add encode_url_for_redirect() function to percent-encode problematic characters
- Apply encoding to redirect Location headers before creating URL objects
- Preserve already-encoded sequences (no double-encoding)
- Log warnings for URLs that fail even after encoding
The fix is conservative - only encodes characters that http_parser rejects,
maintaining full backward compatibility with properly formatted URLs.
Vsevolod Stakhov [Sat, 15 Nov 2025 11:01:18 +0000 (11:01 +0000)]
[Fix] Rewrite lua_url_filter using available Lua string functions
- Pass URL as rspamd_text from C (for future optimizations)
- Convert to string in Lua (acceptable - called rarely on suspicious patterns)
- Use string.find() with string.char() for control character detection
- Use string.gsub() trick for counting @ signs
- Avoid non-existent memchr() method (not implemented for rspamd_text)
- Clean, simple implementation using standard Lua functions
Performance:
- Called only when C parser encounters suspicious patterns
- Conversion overhead acceptable given low frequency
- Future: can optimize with proper memspn functions if needed
Fixes:
- Runtime error: attempt to call method 'memchr' (a nil value)
- Luacheck warning: empty if branch
Vsevolod Stakhov [Sat, 15 Nov 2025 09:49:22 +0000 (09:49 +0000)]
[Test] Add comprehensive tests for URL deep processing
Unit tests (test/lua/unit/lua_url_filter.lua):
- filter_url_string basic validation (normal, long user, multiple @)
- filter_url with URL objects
- UTF-8 validation (ASCII, Cyrillic, Japanese, invalid)
- Custom filter registration and chaining
- Issue #5731 regression test (oversized user parsing)
Functional tests (test/functional/cases/001_merged/400_url_suspect.robot):
- Moved to 001_merged for shared setup/teardown
- Long user field (80 chars) - URL_USER_LONG
- Very long user field (300 chars) - URL_USER_VERY_LONG
- Numeric IP - URL_NUMERIC_IP
- Numeric IP with user - URL_NUMERIC_IP_USER
- Suspicious TLD - URL_SUSPICIOUS_TLD
- Multiple @ signs - URL_MULTIPLE_AT_SIGNS
- Normal URLs (no false positives)
- All tests verify R_SUSPICIOUS_URL backward compatibility
Vsevolod Stakhov [Fri, 14 Nov 2025 20:21:29 +0000 (20:21 +0000)]
[Fix] Complete lua_State parameter threading through codebase
- Add forward declaration in url.c for rspamd_url_lua_consult
- Add lua_State forward declaration in html_url.hxx
- Add lua_State parameter to html_append_tag_content
- Add lua_State parameter to html_process_img_tag
- Add lua_State parameter to html_process_link_tag
- Add lua_State parameter to html_url_is_phished
- Cast void* to lua_State* when calling HTML functions from task context
- Cast lua_State* to void* when calling C API from C++ functions
- All compilation errors resolved
- Build successful
Vsevolod Stakhov [Fri, 14 Nov 2025 20:06:39 +0000 (20:06 +0000)]
[Fix] Use void* for lua_state in public API
- Change lua_State* to void* in url.h public functions
- Fixes C compilation: struct lua_State and lua_State are distinct types in C
- Cast void* to lua_State* inside implementation (url.c)
- Updated: rspamd_url_parse(), rspamd_url_find_multiple()
- Updated: rspamd_web_parse() internal function
- Updated: url_callback_data structure
- Follows C convention: opaque pointers in public headers
Vsevolod Stakhov [Fri, 14 Nov 2025 19:24:26 +0000 (19:24 +0000)]
[Fix] Add forward declaration for lua_State in url.h
- Add 'struct lua_State;' forward declaration
- Fixes compilation errors in C files that include url.h but not lua.h
- Follows Rspamd convention (same pattern as other headers)
Vsevolod Stakhov [Fri, 14 Nov 2025 18:18:32 +0000 (18:18 +0000)]
[Feature] Pass lua_State through HTML URL processing
- Add lua_State parameter to html_process_url() and html_process_url_tag()
- Add lua_State parameter to html_check_displayed_url() and html_process_displayed_href_tag()
- Add lua_State parameter to html_process_query_url()
- Pass task->cfg->lua_state from html_process_input() to all URL processing functions
- All rspamd_url_parse() calls in HTML now have proper lua_State
- HTML URL processing now benefits from Lua filter consultation
- Completes lua_State plumbing - now universally available throughout URL processing chain
Vsevolod Stakhov [Fri, 14 Nov 2025 18:07:46 +0000 (18:07 +0000)]
[Refactor] Use enum and pass lua_State through url_callback_data
- Add enum rspamd_url_lua_filter_result for return values (ACCEPT/SUSPICIOUS/REJECT)
- Replace magic numbers (0/1/2) with enum constants throughout
- Add lua_state field to struct url_callback_data
- Pass lua_State through rspamd_url_find_multiple() chain
- Update all callers: task contexts pass task->cfg->lua_state, others pass NULL
- rspamd_url_trie_generic_callback_common now uses cb->lua_state
- More idiomatic C code with proper type safety
- lua_State now universally available where URL parsing happens
Vsevolod Stakhov [Fri, 14 Nov 2025 18:04:08 +0000 (18:04 +0000)]
[Feature] Wire C->Lua URL filter consultation through parser
- Add lua_State parameter to rspamd_url_parse() and rspamd_web_parse()
- Pass lua_State through entire parsing chain
- Call rspamd_url_lua_consult() at two critical points:
* Oversized user field (>max_email_user) - line 1205
* Multiple @ signs detected - line 1227
- Lua filter can now REJECT (abort), SUSPICIOUS (mark obscured), or ACCEPT
- Update all callers: pass task->cfg->lua_state when available, NULL otherwise
- HTML parser calls: pass NULL (no task context)
- URL extraction: pass NULL (callback data doesn't have task)
- Query URL parsing: pass task->cfg->lua_state (has task context)
- Completes two-level architecture: C consults Lua on ambiguous patterns
- Add rspamd_url_lua_consult() helper function in url.c
- Function calls lua_url_filter.filter_url_string() from C
- Returns ACCEPT/SUSPICIOUS/REJECT to guide C parser
- Add filter_url_string() function in lua_url_filter.lua
- Validates URL strings and rejects obvious garbage
- Checks: length, @ count, user field size, control chars, UTF-8
- Add TODO comment at oversized user field check (line 1204)
- Infrastructure ready, needs lua_State plumbing through call chain
- This completes the two-level architecture design
Vsevolod Stakhov [Fri, 14 Nov 2025 14:06:37 +0000 (14:06 +0000)]
[Fix] Update config comments to guide users to local.d
- Changed comments from 'Uncomment to enable' to 'To enable, add in local.d/url_suspect.conf:'
- Users should not edit shipped config files directly
- Follow Rspamd convention: use local.d/override.d for user customizations
- Updated all map parameter comments for consistency
- Clearer path structure: use local.d/maps/ subdirectory
Vsevolod Stakhov [Fri, 14 Nov 2025 13:56:39 +0000 (13:56 +0000)]
[Refactor] Simplify configuration by removing use_*_map flags
- Removed all use_pattern_map, use_range_map, use_tld_map, etc. flags
- Maps are now implicitly enabled if configured (not nil)
- Cleaner configuration: just uncomment the map parameter to enable
- Updated init_maps() to check map existence instead of enable flags
- Updated check functions to use maps if configured
- Simpler, more intuitive configuration approach
Vsevolod Stakhov [Fri, 14 Nov 2025 13:42:06 +0000 (13:42 +0000)]
[Cleanup] Remove example maps and add doc/ to gitignore
- Removed example map files from conf/maps.d/url_suspect/
- Added doc/ to .gitignore for transient documentation
- Added conf/maps.d/url_suspect/ to .gitignore for user-created maps
- Example maps and documentation belong in separate docs repository
- Users can create their own maps in conf/maps.d/url_suspect/ as needed
Vsevolod Stakhov [Fri, 14 Nov 2025 12:27:21 +0000 (12:27 +0000)]
[Feature] Add URL deep processing architecture
This commit implements a two-level URL processing system that addresses
issue #5731 and provides flexible URL analysis with multiple specific symbols.
Core changes:
* Modified src/libserver/url.c to handle oversized user fields (fixes #5731)
* Added lualib/lua_url_filter.lua - Fast library filter during parsing
* Added src/plugins/lua/url_suspect.lua - Deep inspection plugin
* Added conf/modules.d/url_suspect.conf - Plugin configuration
* Added conf/scores.d/url_suspect_group.conf - Symbol scores
Key features:
* No new C flags - uses existing URL flags (has_user, numeric, obscured, etc.)
* Works without maps - built-in logic for common cases
* 15+ specific symbols instead of generic R_SUSPICIOUS_URL
* Backward compatible - keeps R_SUSPICIOUS_URL working
* User extensible - custom filters and checks supported
Optional features:
* Example map files for advanced customization (disabled by default)
* Whitelist, pattern matching, TLD lists
Vsevolod Stakhov [Wed, 12 Nov 2025 10:25:05 +0000 (10:25 +0000)]
[Fix] Keep groups_*/symbols_* fields in settings for runtime processing
The Lua settings preprocessing was removing groups_disabled, groups_enabled,
symbols_disabled, and symbols_enabled fields from settings objects after
converting them to individual symbol lists. However, the C++ runtime code
(rspamd_symcache_process_settings) also needs these fields to mark symbols
as enabled/disabled in the runtime state.
Background:
- June 2019: Lua preprocessing added, removed fields to 'avoid clash'
- April 2022: C++ runtime process_settings() added, which reads these fields
- The two mechanisms were never properly synchronized
How settings work:
1. Lua preprocessing (at config load): Converts groups_* to symbol lists,
registers via rspamd_config:register_settings_id(), creates settings_elt
with allowed/forbidden symbol IDs for enforcement
2. C++ runtime (when settings applied): Reads task->settings UCL object,
processes groups_*/symbols_* fields to mark symbols as finished/not_started
in the runtime state for optimization
The issue:
When settings are applied via task:set_settings(cached.settings.apply),
the Lua table is converted to UCL and stored in task->settings. If the
groups_* fields were removed by preprocessing, process_settings() cannot
find them, so symbols don't get marked as disabled in runtime state.
The fix:
Don't remove these fields. Both mechanisms work complementarily:
- Settings element: Enforcement (checks allowed/forbidden IDs per symbol)
- Runtime processing: Optimization (marks symbols as finished to skip them)
This is safe because:
- The preprocessing still builds correct allowed/forbidden symbol lists
- The runtime processing reads the same fields and applies them correctly
- No conflict occurs - they work on different aspects (enforcement vs state)
Note: This affects settings IDs (static settings), not dynamic settings
applied directly via Settings={} header, which never went through the
removal step.
Vsevolod Stakhov [Mon, 10 Nov 2025 13:58:25 +0000 (13:58 +0000)]
[Fix] Allow custom actions with flags in config validation
The config validation was warning about unknown actions even when they
were valid custom actions defined with flags like 'no_threshold'.
Custom actions (e.g., malware, virus, hard-reject) are properly supported
by the C code but the Lua validation didn't recognize them, causing
spurious warnings.
Fix by checking if an unknown action is defined as an object with flags
before emitting the warning.
Vsevolod Stakhov [Mon, 10 Nov 2025 09:26:25 +0000 (09:26 +0000)]
[Fix] Regenerate DNS transaction ID before copying packet to TCP buffer
When switching from UDP to TCP (e.g., on truncated response), the code
was copying the packet to the TCP output buffer before regenerating the
transaction ID to avoid collisions. This resulted in both UDP and TCP
packets having the same transaction ID.
Fix by moving the ID regeneration and IO channel switch logic before the
memcpy to the TCP output buffer, ensuring the TCP packet contains the
updated transaction ID.
Vsevolod Stakhov [Mon, 10 Nov 2025 09:21:36 +0000 (09:21 +0000)]
[Fix] Preserve req->pos during reply validation to prevent packet truncation on retransmit
The rdns_request_reply_cmp function modifies req->pos as a side effect
during reply validation. This caused packet truncation on retransmits
because req->pos (which tracks the full packet length) was overwritten
with the end position of the question section.
On timeout/retry, rdns_send_request would use the corrupted req->pos
value, resulting in truncated packets missing the OPT additional section.
This made resolvers like Knot and PowerDNS unable to parse retransmitted
packets.
Fix by saving and restoring req->pos around the reply comparison logic.
[Minor] Replace GHashTable with khash in lua_textpart_get_cta_urls
Replace GHashTable with embedded khash for tracking seen URLs in
lua_textpart_get_cta_urls. khash provides better performance for
simple pointer sets and is already embedded in the project, reducing
external dependencies on GLib for this functionality.
[Fix] Force GC after final batch to release all Redis connections
Previously GC was skipped for the final batch (batch_end >= #results),
leaving those connections checked out. The subsequent DEL of idx_key
could hit the pool limit if batch_size was near the limit, leaving the
_processing key behind and exhausting the pool for the next date.
Now collectgarbage() runs after every batch including the last one,
ensuring all connections are released before the DEL operation.
[Fix] Floor batch_size to integer to prevent fractional indexing
Use math.floor() to convert batch_size to integer before using in loops.
Fractional values like 1.5 would cause batch_start (e.g. 2.5) to index
results[] with non-integer, returning nil and crashing prepare_report()
with 'string expected, got nil'.
[Fix] Normalize batch_size once in handler to fix both code paths
Move batch_size validation to handler() right after argument parsing,
ensuring both process_report_date() and send_reports_by_smtp() use the
normalized value. Previously the validation was only in the Redis loop,
so send_reports_by_smtp() would still see invalid values (0 or negative),
causing nreports <= 0 which breaks sendmail callback scheduling and
prevents finish_cb from ever firing.
[Fix] Validate batch_size to prevent loop error with invalid input
Clamp batch_size to minimum of 1 to prevent Lua error 'bad for step'
when user provides --batch-size 0 or negative values. Previously this
would crash the tool; now it processes with batch_size=1.
[Fix] Add batching and forced GC for Redis connections in dmarc_report
The coroutine version of lua_redis.request() creates connections that
are only released when Lua garbage collector runs. In tight loops
processing hundreds of domains, GC doesn't run frequently enough,
causing connection exhaustion.
Root cause: lua_redis.connect_sync() + conn:exec() returns connections
to Lua but relies on __gc metamethod for pool release. The connections
pile up until GC runs.
Solution:
- Process reports in batches (reuse existing --batch-size option)
- Force collectgarbage("collect") between batches to trigger __gc
- This ensures connections are properly released back to the pool
Each prepare_report() makes 3-4 Redis requests (EXISTS, RENAME,
ZRANGE, DEL), so batching + forced GC prevents parallel connection
buildup.
[Rework] Refactor extract_specific_urls to prevent DoS and use hash-based deduplication
Replace tostring() with url:get_hash() throughout URL extraction to avoid
filling the Lua string interning table. Critical for handling malicious
messages with 100k+ URLs where each tostring() would create an interned
string causing memory exhaustion.
Key changes:
- Use dual data structure: array for results + hash set for O(1) dedup
- Add max_urls_to_process=50000 limit with warning for DoS protection
- Track url_index for stable sorting when priorities are equal
- Fix CTA priority preservation: prevent generic phished handling from
overwriting CTA priorities which include phished/subject bonuses
- Add verbose flag to test suite for debugging
This ensures memory usage is strictly bounded regardless of malicious
input while maintaining correct URL prioritization for spam detection.
HTML_DISPLAYED URLs are phishing bait text (display-only) and should
not be considered for CTA (call-to-action) detection. Only real link
targets should be analyzed for CTA purposes.
[Feature] Add url:get_hash() method for efficient URL deduplication
Expose rspamd_cryptobox_fast_hash via Lua API to allow hash-based
URL deduplication without string conversion overhead. This is critical
for handling messages with large numbers of URLs (100k+) where tostring()
would fill the Lua string interning table and cause memory issues.
Returns 64-bit hash as Lua number, using the same hash function as
internal URL storage for consistency.
[Feature] Add task:get_cta_urls() API for proper CTA domain extraction
- C code (message.c): collect top CTA URLs per HTML part by button weight,
store in task mempool variable "html_cta_urls"
- Lua API (lua_task.c): add task:get_cta_urls([max_urls]) method
- llm_search_context: use new API instead of reimplementing CTA logic in Lua
- Benefits: single source of truth for CTA logic, uses C knowledge of HTML
structure and button weights, cleaner Lua code
This provides proper architecture where C code handles HTML structure analysis
and Lua adds domain filtering (blacklists, infrastructure domains, etc.)
[Fix] Refactor llm_search_context to use lua_cache module
- Replace manual Redis operations with lua_cache API for better consistency
- Use messagepack serialization and automatic key hashing
- Fix Leta Mullvad API URL to /search/__data.json endpoint
- Add search_engine parameter support
- Remove redundant 'or DEFAULTS.xxx' patterns (opts already has defaults merged)
- Add proper debug_module propagation throughout call chain
- Improve JSON parsing to handle Leta Mullvad's nested pointer structure
[Fix] Fix llm_search_context to follow Rspamd idioms
- Use local N = 'llm_search_context' idiom instead of constant string reuse
- Replace rspamd_logger.debugm with lua_util.debugm (rspamd_logger has no debugm method)
- Use extract_specific_urls instead of task:get_urls()
- Add task parameter to query_search_api for proper logging and HTTP requests
- Remove retry logic using non-existent rspamd_config:add_delayed_callback
- Simplify to single HTTP attempt with graceful failure
- Remove retry_count and retry_delay options from config
[Feature] Add web search context support to GPT plugin
- New module llm_search_context.lua: extracts domains from email URLs and queries search API
- Integrated into gpt.lua with parallel context fetching (user + search)
- Redis caching with configurable TTL (default 1 hour)
- Retry logic with exponential backoff for search API failures
- Disabled by default for backward compatibility
- Configuration options in gpt.conf for customization
[Fix] Fix OpenBSD kinfo_proc structure member names
OpenBSD uses p_vm_* member names similar to NetBSD, not FreeBSD's ki_* names.
Separate OpenBSD implementation from FreeBSD to use correct structure members.
[Feature] Fix BSD workflow package installation and update OS versions
- FreeBSD: Add IGNORE_OSVERSION=yes to handle package version mismatches, update to 14.3/13.5
- OpenBSD: Set PKG_PATH for package repository, fix perl package name, update to 7.8/7.7/7.6
- Use latest stable versions of both operating systems
[Fix] NetBSD workflow: setup pkgin and PKG_PATH before installing packages
The pkg_add command requires PKG_PATH to be set in NetBSD 10.0.
Install and use pkgin for easier binary package management.
Changes:
- Set PKG_PATH to NetBSD CDN repository
- Install pkgin using /usr/sbin/pkg_add
- Use pkgin for all package installations
- Change perl5 to perl (correct package name)
- Add || true for non-critical package installation failures
[Feature] Add/improve BSD build workflows with Lua version selection
Add comprehensive GitHub Actions workflows for BSD systems with the ability
to select Lua version (LuaJIT, Lua 5.1, 5.3, 5.4).
NetBSD workflow improvements:
- Add Lua version selection (luajit, lua51, lua53, lua54)
- Fix missing dependencies (libarchive, zstd, xxhash, file)
- Remove || true from pkg_add to catch installation failures
- Add -DENABLE_HYPERSCAN=OFF (not available on NetBSD)
- Add -DSYSTEM_ZSTD=ON and -DSYSTEM_XXHASH=ON flags
- Add conditional ENABLE_LUAJIT based on Lua version selection
- Add NetBSD 9.4 to supported versions
- Fix test executable paths (add ./ prefix)
New FreeBSD workflow:
- Support FreeBSD 14.2, 13.4, 13.3
- Lua version selection (luajit, lua51, lua53, lua54)
- Full dependency list including hyperscan
- Enable hyperscan support (-DENABLE_HYPERSCAN=ON)
- Use system zstd and xxhash libraries
- Proper pkg update before installation
New OpenBSD workflow:
- Support OpenBSD 7.6, 7.5, 7.4
- Lua version selection (luajit, lua51, lua53)
- Full dependency list including hyperscan
- Enable hyperscan support
- Disable jemalloc (-DENABLE_JEMALLOC=OFF)
- Use OpenBSD-specific package names (icu4c, perl-5, lua%5.x)
- Use system zstd and xxhash libraries
All workflows:
- Use workflow_dispatch trigger for manual execution
- Allow selection of OS version and Lua version
- Use vmactions VMs for BSD testing
- Run both C++ and Lua unit tests
- Use Ninja build system for faster compilation
This provides comprehensive testing across different BSD platforms and
Lua versions, ensuring Rspamd builds correctly on various configurations.
[Fix] Keep srv events active during shutdown to track auxiliary processes
When Rspamd shuts down with auxiliary processes running (e.g., neural network
training spawned by workers), the main process was stopping srv_pipe event
handlers immediately after sending SIGTERM to workers. This prevented workers
from sending RSPAMD_SRV_ON_FORK notifications when their auxiliary child
processes terminated, causing these children to remain tracked indefinitely.
The main process would then hang for 90 seconds waiting for already-dead
processes that it couldn't properly clean up from the workers hash table.
Root cause analysis:
- Direct workers have ev_child watchers and are removed via SIGCHLD handler
- Auxiliary processes (fork from workers) have NO ev_child watchers
- They are removed ONLY via srv_pipe notifications (RSPAMD_SRV_ON_FORK)
- Stopping srv events during shutdown breaks this notification channel
The original stop_srv_ev code was added in 2019 (commit eafdd221) to avoid
"false notifications" during a major refactoring. However, this is no longer
an issue because:
1. srv_ev handlers automatically stop on EOF when worker pipes close
2. There is no risk of duplicate notifications
3. Auxiliary processes critically need these events to report termination
Solution: Remove the stop_srv_ev call from rspamd_term_handler. This allows
workers to continue sending process termination notifications during shutdown.
The srv_ev handlers will stop naturally when workers close their pipes.
[Fix] Recreate invalid unserialized Hyperscan cache files on version mismatch
When Hyperscan library is updated, previously cached .unser files become
invalid due to version mismatch. Previously, these files would remain
unusable, forcing fallback to slower deserialization on every load.
This commit adds automatic detection and recreation of invalid unserialized
files:
- Extract file creation logic into create_unserialized_file() helper function
- Add error handler that deletes and recreates invalid .unser files
- Maintain file locking protection against concurrent process access
- Fall back to serialized version if recreation fails
This ensures cache files are automatically updated after Hyperscan upgrades
while protecting against race conditions in multi-process environments.
[Fix] Use runtime Hyperscan version instead of compile-time version for database validation
The issue was that the database version check used HS_DB_VERSION macro defined
in headers at compile time, while Hyperscan .so library writes the version from
its own headers. When the system updates the Hyperscan package but Rspamd isn't
recompiled, this causes a version mismatch and database validation fails.
The fix calls hs_version() at runtime to get the actual library version and uses
that for validation instead. This ensures compatibility when the Hyperscan library
is updated independently.
Resolve crashes caused by mixing system malloc and jemalloc allocators.
The issue occurred when getline() and hiredis used system malloc, but
rspamd's free() used jemalloc, causing segmentation faults on macOS and
potentially other platforms.
Changes:
- Add rspamd_getline() wrapper using g_malloc/g_realloc/g_free to avoid
system malloc in getline()
- Replace getline() with rspamd_getline() in url.c, dns.c, lua_repl.c
- Fix memory leak in lua_repl.c by freeing input buffer on exit
- Configure hiredis allocators to use glib functions (jemalloc) in
rspamd_init_libs()
This ensures all memory operations use the same allocator (jemalloc)
throughout rspamd, preventing allocator mismatch crashes.
Resolve crashes caused by mixing jemalloc and system malloc allocators
in libucl. The issue occurred when memory allocated with one allocator
(e.g., strdup using system malloc) was freed with another (e.g., jemalloc's
free), causing segmentation faults.
Changes:
- Add UCL_REALLOC and UCL_STRDUP macros to ucl.h for consistent allocation
- Replace all strdup/malloc/realloc/free calls with UCL_* macros in:
- Variable and macro registration (ucl_parser.c)
- Parser state management (ucl_util.c)
- Object copying and trash stack operations (ucl_util.c)
- URL fetching - fix critical bug where malloc'd buffers were freed
with ucl_munmap (munmap) instead of free (ucl_util.c)
This ensures all memory operations use the same allocator throughout libucl,
preventing allocator mismatch crashes on systems using jemalloc.
Cursor Agent [Sat, 1 Nov 2025 12:56:18 +0000 (12:56 +0000)]
[Fix] Fix rspamd nameserver round-robin when using /etc/resolv.conf
When nameservers are parsed from /etc/resolv.conf, rspamd was setting
the upstream rotation strategy to RSPAMD_UPSTREAM_MASTER_SLAVE, which
caused it to only use the first nameserver unless it failed.
This behavior was inconsistent with the documented round-robin strategy
and with the behavior when nameservers are explicitly configured via
the configuration file.
Fixed by changing the rotation strategy to RSPAMD_UPSTREAM_ROUND_ROBIN
when parsing /etc/resolv.conf, matching the expected behavior.