From 5ea543188dd6180fb403e88babb972bbf34dfbe4 Mon Sep 17 00:00:00 2001 From: reverse Date: Tue, 11 Aug 2026 11:21:09 +0000 Subject: [PATCH] syslog: tee the server-notice/log stream to syslog (native /dev/log unix datagram or udp host:port) --- echoircd.conf.example | 6 +++ src/modules/mod.rs | 1 + src/modules/syslog.rs | 107 ++++++++++++++++++++++++++++++++++++++++++ src/server.rs | 1 + 4 files changed, 115 insertions(+) create mode 100644 src/modules/syslog.rs diff --git a/echoircd.conf.example b/echoircd.conf.example index 4e8b5b8..ac579b0 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -130,6 +130,12 @@ amu_target = both # "nick!user@host realname" matches a regex (native engine; no config to enable). # rline_matchonnickchange = yes # also re-check the R-lines when a user changes nick +# --- syslog: mirror the server-notice / log stream to the system logger --- +# syslog = yes +# syslog_target = /dev/log # a Unix socket path, or host:port for UDP +# syslog_facility = daemon # kern user mail daemon auth ... local0..local7 +# syslog_tag = echoircd + # --- security groups: securitygroup = [criteria...] # criteria: public tls insecure account unregistered oper exclude-oper # bot exclude-bot webirc exclude-webirc mask= exclude= diff --git a/src/modules/mod.rs b/src/modules/mod.rs index b55c5bd..5a2910b 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -66,6 +66,7 @@ pub mod serverban; pub mod showfile; pub mod solvemsg; pub mod snoop; +pub mod syslog; pub mod tline; pub mod whoisport; diff --git a/src/modules/syslog.rs b/src/modules/syslog.rs new file mode 100644 index 0000000..08023a5 --- /dev/null +++ b/src/modules/syslog.rs @@ -0,0 +1,107 @@ +//! syslog — mirror the server-notice / log stream to the system logger. Off unless +//! `syslog = yes`. Sends to a local Unix datagram socket (`syslog_target`, default +//! `/dev/log`) or, when the target looks like `host:port`, over UDP. Messages are +//! RFC 3164 `TAG[pid]: msg`; the receiving daemon stamps the time. +//! +//! ```text +//! syslog = yes +//! syslog_target = /dev/log # or e.g. 10.0.0.5:514 for a remote collector +//! syslog_facility = daemon # kern user mail daemon auth ... local0..local7 +//! syslog_tag = echoircd +//! ``` +//! +//! The socket is cached per target on the core thread (snotice is single-threaded), +//! reopened only when the configured target changes. + +use std::cell::RefCell; +use std::net::UdpSocket; +use std::os::unix::net::UnixDatagram; + +use crate::server::Server; + +/// An opened syslog transport. +enum Sink { + Unix(UnixDatagram), + Udp(UdpSocket, String), // socket + "host:port" destination +} + +thread_local! { + /// (target-string, sink) cached for the current config; reopened on change. + static SINK: RefCell> = const { RefCell::new(None) }; +} + +/// Syslog facility name → numeric code (RFC 3164 §4.1.1). +fn facility(name: &str) -> u8 { + match name.to_ascii_lowercase().as_str() { + "kern" => 0, + "user" => 1, + "mail" => 2, + "auth" => 4, + "syslog" => 5, + "lpr" => 6, + "news" => 7, + "uucp" => 8, + "cron" => 9, + "authpriv" => 10, + "ftp" => 11, + "local0" => 16, + "local1" => 17, + "local2" => 18, + "local3" => 19, + "local4" => 20, + "local5" => 21, + "local6" => 22, + "local7" => 23, + _ => 3, // daemon + } +} + +/// Open the transport for `target` (`host:port` ⇒ UDP, else a Unix datagram path). +fn open(target: &str) -> Option { + if target.contains(':') && !target.starts_with('/') { + let sock = UdpSocket::bind("0.0.0.0:0") + .or_else(|_| UdpSocket::bind("[::]:0")) + .ok()?; + Some(Sink::Udp(sock, target.to_string())) + } else { + let sock = UnixDatagram::unbound().ok()?; + sock.connect(target).ok()?; + Some(Sink::Unix(sock)) + } +} + +/// Tee `msg` to syslog when enabled. Called at the tail of [`Server::snotice`]. +pub fn tee(s: &Server, msg: &str) { + if !s.conf_bool("syslog", false) { + SINK.with(|c| *c.borrow_mut() = None); // dropped/disabled: forget any socket + return; + } + let target = s + .conf("syslog_target") + .map(str::to_string) + .unwrap_or_else(|| "/dev/log".to_string()); + let tag = s.conf("syslog_tag").unwrap_or("echoircd"); + // severity "notice" (5); PRI = facility*8 + severity + let pri = facility(s.conf("syslog_facility").unwrap_or("daemon")) as u16 * 8 + 5; + let line = format!("<{pri}>{tag}[{}]: {msg}", std::process::id()); + SINK.with(|cell| { + let mut slot = cell.borrow_mut(); + // (re)open if the target changed or nothing is open yet + let need_open = match slot.as_ref() { + Some((t, _)) => t != &target, + None => true, + }; + if need_open { + *slot = open(&target).map(|sink| (target.clone(), sink)); + } + if let Some((_, sink)) = slot.as_ref() { + let ok = match sink { + Sink::Unix(sock) => sock.send(line.as_bytes()).is_ok(), + Sink::Udp(sock, dst) => sock.send_to(line.as_bytes(), dst).is_ok(), + }; + if !ok { + *slot = None; // transient failure: drop so we reopen next time + } + } + }); +} diff --git a/src/server.rs b/src/server.rs index 971b41c..ca9cc7a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -757,6 +757,7 @@ impl Server { self.deliver_server_notice(o, msg, &jval); } crate::modules::chanlog::tee(self, msg); + crate::modules::syslog::tee(self, msg); } /// Broadcast a `*** msg` server NOTICE to *every* registered local user — for