Skip to content

Commit c21810d

Browse files
committed
Add agent-side UDP/TCP Syslog listener support to Logcollector
Add native agent-side Syslog listener support so the Wazuh agent can receive basic Syslog messages directly over UDP or TCP, instead of requiring an external service to write them to a file first. Received messages enter the existing Logcollector processing path and are sent through the standard agent-to-manager pipeline, remaining associated with the receiving agent. - New SyslogReader (IReader) implementing UDP and TCP listeners with the Boost.Asio coroutine model used by the other readers. - SetupSyslogReaders parses a new logcollector.syslog configuration section and creates one listener per definition; multiple independent listeners are supported. - Validation before any listener starts: protocol (udp/tcp), port range (1-65535), bind address, and duplicate protocol+address+port detection. Invalid or duplicate entries are logged and skipped, so no listener is silently enabled. bind_address defaults to 127.0.0.1. - Clean startup/shutdown: sockets are closed on the io_context thread and TCP client sockets are tracked and closed on stop. - Backward compatibility preserved for file, journald, windows and macOS collectors; manager-side remote Syslog is unchanged. - Unit tests for configuration parsing/validation and runtime UDP/TCP receipt, plus reference documentation and sample configuration. Related to wazuh/wazuh#15178.
1 parent 927df48 commit c21810d

14 files changed

Lines changed: 1264 additions & 2 deletions

File tree

docs/ref/modules/logcollector/README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,80 @@ This collector gets logs from Journald on Linux. It needs a field and a value to
8383
| | journald.ignore_if_missing | Boolean to ignore the filtering condition for logs without the specified field | false |
8484
| | journald.conditions | Vector of journald fields to filter to be applied simultaneously | |
8585

86+
### Agent-side Syslog listener
87+
88+
The Wazuh agent can optionally receive basic Syslog messages directly over UDP or
89+
TCP through the Logcollector module. This is useful for lightweight remote-site
90+
deployments where nearby devices or applications need to forward Syslog to a local
91+
Wazuh agent, while preserving the agent-based collection model. Received messages
92+
enter the normal Logcollector processing path and are sent through the standard
93+
agent-to-manager pipeline, so they remain associated with the receiving agent.
94+
95+
No listener is started unless it is explicitly configured. Each `syslog` entry
96+
defines one listener (one protocol, bind address and port). Several entries may be
97+
combined to run multiple independent listeners on the same agent.
98+
99+
```yaml
100+
logcollector:
101+
enabled: true
102+
syslog:
103+
# UDP listener on localhost
104+
- protocol: udp
105+
bind_address: 127.0.0.1
106+
port: 5514
107+
# UDP listener on a specific interface
108+
- protocol: udp
109+
bind_address: 192.168.10.20
110+
port: 5515
111+
# TCP listener on localhost
112+
- protocol: tcp
113+
bind_address: 127.0.0.1
114+
port: 1514
115+
```
116+
117+
`bind_address` is optional and defaults to `127.0.0.1`. Binding to `0.0.0.0` (all
118+
interfaces) must be configured explicitly. Each message is forwarded with the
119+
`remote-syslog` collector type and the listener identity (`<protocol>:<address>:<port>`)
120+
as its provider:
121+
122+
```json
123+
{"module":"logcollector","collector":"remote-syslog"}
124+
{"event":{"created":"2025-01-17T17:58:26.212Z","original":"<13>Jun 25 10:00:00 testhost testapp: UDP listener test","provider":"udp:127.0.0.1:5514"}}
125+
```
126+
127+
| Mandatory | Option | Description | Default |
128+
| :-------: | ------------------- | ----------------------------------------------------------------------- | --------- |
129+
| ✔️ | syslog | Vector of agent-side syslog listeners | |
130+
| ✔️ | syslog.protocol | Listener transport protocol: `udp` or `tcp` | |
131+
| ✔️ | syslog.port | Listener port (1-65535) | |
132+
| | syslog.bind_address | Address the listener binds to | 127.0.0.1 |
133+
134+
A listener definition is rejected (and the listener is not started) when the
135+
protocol is not `udp`/`tcp`, the port is missing or out of range, the bind address
136+
is malformed, or another listener already uses the same protocol, bind address and
137+
port combination.
138+
139+
> **Note:** For high-volume Syslog ingestion, TLS Syslog, disk-assisted queues,
140+
> advanced filtering, transformations, routing, or complex parsing pipelines, use
141+
> rsyslog, syslog-ng, Logstash, or the Wazuh manager remote Syslog input as
142+
> appropriate. Source IP filtering for the agent-side listener should be handled
143+
> with host firewall rules.
144+
145+
#### Limitations and future work
146+
147+
This first version implements only the UDP/TCP IP-socket listeners from
148+
[wazuh/wazuh#15178](https://github.com/wazuh/wazuh/issues/15178). The following are
149+
intentionally **not** included yet and are tracked as future work:
150+
151+
| Not yet supported | Notes |
152+
| ----------------- | ----- |
153+
| UNIX domain sockets (`unix_stream`, `unix_dgram`, `unix_seq`) | Local socket ingress requested in #15178; reuses the Boost.Asio local-socket pattern. |
154+
| Named pipe / FIFO (and Windows named pipes) | Pipe ingress requested in #15178; equivalent to the legacy `syslog-pipe` format. |
155+
| TLS Syslog (TCP) | The TCP listener is plaintext; use rsyslog/syslog-ng for TLS. |
156+
| TCP octet-counting framing (RFC 6587) | Only newline-delimited ("non-transparent") framing is parsed. Octet-counted messages (`<len> <msg>`) are not auto-detected. |
157+
| Hostname `bind_address` | Only numeric IP literals (IPv4/IPv6) are accepted; DNS names are rejected. |
158+
| `allowed-ips` source filtering | Restrict senders with host firewall rules until implemented. |
159+
86160
### Windows Collector
87161

88162
```yaml

docs/ref/modules/logcollector/architecture.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,20 @@ classDiagram
6161
+ Run()
6262
+ Stop()
6363
}
64+
class SyslogReader {
65+
- protocol : SyslogProtocol
66+
- bindAddress : string
67+
- port : uint16
68+
+ SyslogReader(protocol, bindAddress, port)
69+
+ Run()
70+
+ Stop()
71+
}
6472
IModule <-- Logcollector
6573
Logcollector o-- IReader
6674
IReader <|-- FileReader
6775
IReader <|-- JournaldReader
6876
IReader <|-- WindowsEventTracerReader
6977
IReader <|-- MacosReader
78+
IReader <|-- SyslogReader
7079
FileReader o-- LocalFile
7180
```

etc/config/wazuh_agent_linux.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,12 @@ logcollector:
2929
value: 0|1|2|3|4|5|6|7
3030
exact_match: true
3131
ignore_if_missing: true
32+
# Agent-side syslog listeners (UDP/TCP). Disabled by default: no listener is
33+
# started unless explicitly configured here. bind_address defaults to 127.0.0.1.
34+
# syslog:
35+
# - protocol: udp
36+
# bind_address: 127.0.0.1
37+
# port: 5514
38+
# - protocol: tcp
39+
# bind_address: 127.0.0.1
40+
# port: 1514

etc/config/wazuh_agent_macos.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@ logcollector:
2424
read_interval: 500ms
2525
macos:
2626
- level: info
27+
# Agent-side syslog listeners (UDP/TCP). Disabled by default: no listener is
28+
# started unless explicitly configured here. bind_address defaults to 127.0.0.1.
29+
# syslog:
30+
# - protocol: udp
31+
# bind_address: 127.0.0.1
32+
# port: 5514
33+
# - protocol: tcp
34+
# bind_address: 127.0.0.1
35+
# port: 1514

etc/config/wazuh_agent_windows.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,14 @@ logcollector:
2727
- channel: System
2828
- channel: Application
2929
- channel: Security
30+
# Agent-side syslog listeners (UDP/TCP). Disabled by default: no listener is
31+
# started unless explicitly configured here. bind_address defaults to 127.0.0.1.
32+
# syslog:
33+
# - protocol: udp
34+
# bind_address: 127.0.0.1
35+
# port: 5514
36+
# - protocol: tcp
37+
# bind_address: 127.0.0.1
38+
# port: 1514
3039

3140

src/modules/logcollector/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ find_package(cJSON CONFIG REQUIRED)
1616
find_package(nlohmann_json CONFIG REQUIRED)
1717
find_package(Boost REQUIRED COMPONENTS asio)
1818

19-
file(GLOB LOGCOLLECTOR_SOURCES src/*.cpp src/file_reader/src/*.cpp)
19+
file(GLOB LOGCOLLECTOR_SOURCES src/*.cpp src/file_reader/src/*.cpp src/syslog_reader/src/*.cpp)
2020
file(GLOB JOURNALD_SOURCES src/journald_reader/src/*.cpp)
2121
file(GLOB MACOS_SOURCES src/macos_reader/src/*.cpp)
2222
file(GLOB WIN_SOURCES src/winevt_reader/src/*.cpp)
@@ -44,6 +44,7 @@ target_include_directories(
4444
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
4545
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
4646
${CMAKE_CURRENT_SOURCE_DIR}/src/file_reader/include
47+
${CMAKE_CURRENT_SOURCE_DIR}/src/syslog_reader/include
4748
$<$<PLATFORM_ID:Linux>:${CMAKE_CURRENT_SOURCE_DIR}/src/journald_reader/include>
4849
$<$<PLATFORM_ID:Linux>:${SYSTEMD_INCLUDE_DIRS}>
4950
$<$<PLATFORM_ID:Windows>:${CMAKE_CURRENT_SOURCE_DIR}/src/winevt_reader/include>

src/modules/logcollector/include/logcollector.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ namespace logcollector
8181
/// @param configurationParser Configuration parser
8282
void SetupFileReader(const std::shared_ptr<const configuration::ConfigurationParser> configurationParser);
8383

84+
/// @brief Sets up the agent-side syslog listeners (UDP/TCP)
85+
///
86+
/// Reads every listener definition from the configuration, validates it and
87+
/// creates one reader per valid definition. Invalid or duplicate definitions
88+
/// are reported and skipped so that no listener is silently started.
89+
///
90+
/// @param configurationParser Configuration parser
91+
void SetupSyslogReaders(const std::shared_ptr<const configuration::ConfigurationParser> configurationParser);
92+
8493
/// @brief Clean all readers
8594
void CleanAllReaders();
8695

src/modules/logcollector/src/logcollector.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22

33
#include <boost/asio/co_spawn.hpp>
44
#include <boost/asio/detached.hpp>
5+
#include <boost/asio/ip/address.hpp>
56
#include <boost/asio/redirect_error.hpp>
67
#include <config.h>
78
#include <logger.hpp>
89
#include <timeHelper.hpp>
910

11+
#include <algorithm>
12+
#include <cctype>
1013
#include <chrono>
14+
#include <cstdint>
1115
#include <iomanip>
1216
#include <map>
17+
#include <set>
1318
#include <sstream>
1419

1520
#include "file_reader.hpp"
21+
#include "syslog_reader.hpp"
1622

1723
using namespace logcollector;
1824

@@ -66,9 +72,106 @@ void Logcollector::Setup(std::shared_ptr<const configuration::ConfigurationParse
6672
configurationParser->GetConfigOrDefault(config::logcollector::DEFAULT_ENABLED, "logcollector", "enabled");
6773

6874
SetupFileReader(configurationParser);
75+
SetupSyslogReaders(configurationParser);
6976
AddPlatformSpecificReader(configurationParser);
7077
}
7178

79+
void Logcollector::SetupSyslogReaders(
80+
const std::shared_ptr<const configuration::ConfigurationParser> configurationParser)
81+
{
82+
const auto syslogConfigs = configurationParser->GetConfigOrDefault<YAML::Node>(
83+
YAML::Node(YAML::NodeType::Sequence), "logcollector", "syslog");
84+
85+
constexpr int MIN_PORT = 1;
86+
constexpr int MAX_PORT = 65535;
87+
88+
std::set<std::string> seenListeners;
89+
90+
for (const auto& config : syslogConfigs)
91+
{
92+
if (!config.IsMap())
93+
{
94+
LogWarn("Invalid agent-side syslog listener configuration: entry is not a mapping.");
95+
continue;
96+
}
97+
98+
auto protocolStr = config["protocol"].as<std::string>("");
99+
std::transform(protocolStr.begin(),
100+
protocolStr.end(),
101+
protocolStr.begin(),
102+
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
103+
104+
SyslogProtocol protocol = SyslogProtocol::Udp;
105+
if (protocolStr == "udp")
106+
{
107+
protocol = SyslogProtocol::Udp;
108+
}
109+
else if (protocolStr == "tcp")
110+
{
111+
protocol = SyslogProtocol::Tcp;
112+
}
113+
else
114+
{
115+
LogError("Invalid agent-side syslog listener configuration: unsupported protocol {}.",
116+
protocolStr.empty() ? "(missing)" : protocolStr);
117+
continue;
118+
}
119+
120+
if (!config["port"])
121+
{
122+
LogError("Invalid agent-side syslog listener configuration: missing port.");
123+
continue;
124+
}
125+
126+
int port = 0;
127+
try
128+
{
129+
port = config["port"].as<int>();
130+
}
131+
catch (const std::exception&)
132+
{
133+
LogError("Invalid agent-side syslog listener configuration: invalid port {}.",
134+
config["port"].as<std::string>(""));
135+
continue;
136+
}
137+
138+
if (port < MIN_PORT || port > MAX_PORT)
139+
{
140+
LogError("Invalid agent-side syslog listener configuration: invalid port {}.", port);
141+
continue;
142+
}
143+
144+
const auto bindAddress = config["bind_address"].as<std::string>("127.0.0.1");
145+
146+
boost::system::error_code ec;
147+
boost::asio::ip::make_address(bindAddress, ec);
148+
if (ec)
149+
{
150+
LogError("Invalid agent-side syslog listener configuration: invalid bind address {}.", bindAddress);
151+
continue;
152+
}
153+
154+
const auto listenerId =
155+
SyslogReader::ProtocolToString(protocol) + ":" + bindAddress + ":" + std::to_string(port);
156+
157+
if (!seenListeners.insert(listenerId).second)
158+
{
159+
LogError("Invalid agent-side syslog listener configuration: duplicate listener {}.", listenerId);
160+
continue;
161+
}
162+
163+
AddReader(std::make_shared<SyslogReader>(
164+
[this](const std::string& location, const std::string& log, const std::string& collectorType)
165+
{ PushMessage(location, log, collectorType); },
166+
// NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines)
167+
[this](std::chrono::milliseconds duration) -> Awaitable { co_await Wait(duration); },
168+
[this](Awaitable task) { EnqueueTask(std::move(task)); },
169+
protocol,
170+
bindAddress,
171+
static_cast<std::uint16_t>(port)));
172+
}
173+
}
174+
72175
void Logcollector::SetupFileReader(const std::shared_ptr<const configuration::ConfigurationParser> configurationParser)
73176
{
74177
const auto fileWait = configurationParser->GetTimeConfigOrDefault(

0 commit comments

Comments
 (0)