diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ded51fb..4c8acb2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,10 @@ on: branches: - '**' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: # Verify we can build on latest Ubuntu with both gcc and clang @@ -26,16 +30,15 @@ jobs: steps: - name: Install dependencies run: | - sudo apt-get -y update - sudo apt-get -y install tree tshark valgrind - - uses: actions/checkout@v4 + sudo apt-get -y install tree tshark valgrind libssl-dev + - uses: actions/checkout@v6 - name: Configure run: | set -x ./autogen.sh mkdir -p build/dir cd build/dir - ../../configure --prefix=/tmp --with-systemd=/tmp/lib/systemd/system + ../../configure --prefix=/tmp --with-systemd=/tmp/lib/systemd/system --with-openssl chmod -R a+w . - name: Build run: | @@ -63,7 +66,7 @@ jobs: run: | cd build/dir make check || (cat test/test-suite.log; false) - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: ${{ matrix.compiler }}-test-logs path: build/dir/test/*.log diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index efd3dcf..cc92044 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -15,8 +15,8 @@ jobs: coverity: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/cache@v4 + - uses: actions/checkout@v6 + - uses: actions/cache@v5 id: coverity-toolchain-cache with: path: cov-analysis-linux64 @@ -53,7 +53,7 @@ jobs: --form description="${PROJECT_NAME} $(git rev-parse HEAD)" \ https://scan.coverity.com/builds?project=${COVERITY_PROJ} - name: Upload build.log - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverity-build.log path: cov-int/build-log.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dce466a..8929c72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,10 +11,9 @@ jobs: if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Installing dependencies ... run: | - sudo apt-get -y update sudo apt-get -y install tree tshark valgrind - name: Creating Makefiles ... run: | diff --git a/ChangeLog.md b/ChangeLog.md index 8d8bb43..d4ed39d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,6 +4,36 @@ Change Log All relevant changes to the project are documented in this file. +[UNRELEASED][] +----------------------- + +### Changes +- Add TCP transport for syslog forwarding per RFC 6587. Two syntaxes + supported for forwarding: `@@host:port` and `tcp://host:port`. For + receiving: `listen tcp://addr:port`. Uses octet counting framing + for sending, supports both octet counting and LF-delimited framing + for receiving +- Add optional RFC 5848 signed syslog message support. Requires + OpenSSL and `./configure --with-openssl`. New config options: + `sign_sg`, `sign_delim_sg2`, `sign_keyfile`, `sign_certfile`. + Provides cryptographic signing of messages for origin authentication, + message integrity, and replay resistance +- Add optional RFC 5425 TLS transport for syslog. Requires OpenSSL + and `./configure --with-openssl`. Three syntaxes supported for + forwarding: `@@@host:port`, `tls://host:port`, and `tls4://` or + `tls6://` for IPv4/IPv6 specific. For receiving: `listen tls://addr:port`. + New config options: `tls_keyfile`, `tls_certfile`, `tls_cafile`, + `tls_capath`, `tls_verify`. Per-action options: `verify=off|optional| + required|hostname`, `fingerprint=SHA256:...` for certificate pinning, + `tls_keyfile=`, `tls_certfile=` for mutual TLS authentication. + Default port is 6514 per RFC 5425 + +### Fixes +- Fix use-after-free in socket polling when callbacks close sockets + during iteration. Could cause undefined behavior when handling + multiple concurrent TCP connections + + [v2.7.2][] - 2025-03-31 ----------------------- @@ -692,7 +722,7 @@ and a replacement for `syslog.h` to enable new features in RFC5424. - Several bugfixes and improvements, please refer to the .c files -[UNRELEASED]: https://github.com/troglobit/sysklogd/compare/v2.7.1...HEAD +[UNRELEASED]: https://github.com/troglobit/sysklogd/compare/v2.7.2...HEAD [v2.7.2]: https://github.com/troglobit/sysklogd/compare/v2.7.1...v2.7.2 [v2.7.1]: https://github.com/troglobit/sysklogd/compare/v2.7.0...v2.7.1 [v2.7.0]: https://github.com/troglobit/sysklogd/compare/v2.6.2...v2.7.0 diff --git a/README.md b/README.md index 12ce92c..fa87b7d 100644 --- a/README.md +++ b/README.md @@ -13,34 +13,38 @@ Table of Contents ----------------- -* [Introduction](#introduction) -* [Using -lsyslog](#using--lsyslog) -* [Build & Install](#build--install) -* [Building from GIT](#building-from-git) -* [Origin & References](#origin--references) - -> **Tip:** the Gentoo project has a very nice article detailing sysklogd +- [Introduction](#introduction) +- [Using -lsyslog](#using--lsyslog) +- [Build & Install](#build--install) +- [Building from GIT](#building-from-git) +- [Origin & References](#origin--references) + +> [!TIP] +> The Gentoo project has a very nice article detailing sysklogd > ➤ - Introduction ------------ -This is the continuation of the original Debian/Ubuntu syslog daemon, -updated with full [RFC3164][] and [RFC5424][] support from NetBSD and -FreeBSD. The package includes the `libsyslog.{a,so}` library with a -`syslog.h` header replacement, the `syslogd` daemon, and a command -line tool called `logger`. +This is the continuation of the original Debian/Ubuntu syslog daemon, updated to +full RFC compliance according to syslog standards [RFC3164][] and [RFC5424][], +derived from NetBSD and FreeBSD. It also supports TCP ([RFC6587][]) and TLS +encrypted transport ([RFC5425][]), as well as cryptographically signed log +messages ([RFC5848][]). -- https://man.troglobit.com/man1/logger.1.html -- https://man.troglobit.com/man8/syslogd.8.html -- https://man.troglobit.com/man5/syslog.conf.5.html +The package includes the `libsyslog.{a,so}` library with a `syslog.h` header +replacement, the `syslogd` daemon, and a command line tool called `logger`. +`libsyslog` and `syslog/syslog.h` are derived directly from NetBSD and expose +`syslogp()` and other new features available only in [RFC5424][] (not yet +available in GLIBC). -`libsyslog` and `syslog/syslog.h`, derived directly from NetBSD, expose -`syslogp()` and other new features available only in [RFC5424][]: +Read more about each component and the APIs: -- https://man.troglobit.com/man3/syslogp.3.html -- https://netbsd.gw.com/cgi-bin/man-cgi?syslog+3+NetBSD-current +- +- +- +- +- The `syslogd` daemon is an enhanced version of the standard Berkeley utility program, updated with DNA from FreeBSD. It provides logging of @@ -88,6 +92,12 @@ Main differences from the original sysklogd package are: - Touch PID file on `SIGHUP`, for integration with [Finit][] - GNU configure & build system to ease porting/cross-compiling - Support for configuring remote syslog timeout +- Support for [RFC6587][] TCP syslog transport, for sender and receiver +- Per-destination in-memory send queue for TCP forwarding: messages accumulate + during outages and are flushed automatically on reconnect, with configurable + suspension time (`tcp_suspend_time` in `syslog.conf`) +- Support for [RFC5425][] TLS encrypted syslog transport (only if built with OpenSSL support) +- Support for [RFC5848][] cryptographically signed log messages (only if built with OpenSSL support) Please file bug reports, or send pull requests for bug fixes and/or proposed extensions at [GitHub][Home]. @@ -208,6 +218,9 @@ now [3-clause BSD][BSD License] licensed. [RFC3164]: https://tools.ietf.org/html/rfc3164 [RFC5424]: https://tools.ietf.org/html/rfc5424 +[RFC5425]: https://tools.ietf.org/html/rfc5425 +[RFC5848]: https://tools.ietf.org/html/rfc5848 +[RFC6587]: https://tools.ietf.org/html/rfc6587 [Martin Schulze]: http://www.infodrom.org/projects/sysklogd/ [Joachim Wiberg]: https://troglobit.com [Finit]: https://github.com/troglobit/finit diff --git a/configure.ac b/configure.ac index fe79595..2aebe78 100644 --- a/configure.ac +++ b/configure.ac @@ -25,7 +25,7 @@ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. -AC_INIT([sysklogd], [2.7.2], +AC_INIT([sysklogd], [3.0.0-beta1], [https://github.com/troglobit/sysklogd/issues],, [https://github.com/troglobit/sysklogd]) AC_CONFIG_AUX_DIR(aux) @@ -87,6 +87,23 @@ AC_ARG_WITH(logger, AS_IF([test "x$logger" != "xno"], with_logger="yes", with_logger="no") AM_CONDITIONAL([ENABLE_LOGGER], [test "x$with_logger" != "xno"]) +# Optional OpenSSL support for RFC 5848 signed syslog messages +AC_ARG_WITH([openssl], + AS_HELP_STRING([--with-openssl], [Enable RFC 5848 signed syslog messages (requires OpenSSL)]), + [], [with_openssl=no]) + +AS_IF([test "x$with_openssl" != "xno"], [ + PKG_CHECK_MODULES([OPENSSL], [openssl >= 1.1.0], [ + AC_DEFINE([HAVE_OPENSSL], [1], [Define if OpenSSL is available for RFC 5848 signing]) + have_openssl=yes + ], [ + AS_IF([test "x$with_openssl" = "xyes"], + [AC_MSG_ERROR([OpenSSL >= 1.1.0 requested but not found])]) + have_openssl=no + ]) +], [have_openssl=no]) +AM_CONDITIONAL([ENABLE_SSL], [test "x$have_openssl" = "xyes"]) + AS_IF([test "x$dns_delay" != "xno"],[ AS_IF([test "x$dns_delay" = "xyes"],[ AC_MSG_ERROR([Must supply argument])]) @@ -149,6 +166,7 @@ cat < option specifies the path to an executable program which will get called @@ -352,15 +432,97 @@ The option can be used to include all files with names ending in '.conf' and not beginning with a '.' contained in the directory following the keyword. This keyword can only be used in the first level configuration -file. The included example +file. Included files are parsed in glob order and their rules are +inserted into the rule list at the position of the +.Ql include +line. +.Pp +This ordering matters for stop-processing blocks +.Pq Ql !! , ++ , :: +\&: such a block can only suppress rules that appear +.Em after +it in the parsed list. Rules already processed before the +.Ql include +line are unaffected. For snippets in +.Pa /etc/syslog.d/ +to be able to prevent messages from reaching the general catch-all rules +in the main configuration file, the +.Ql include +line must appear +.Em before +those catch-all rules. The shipped .Pa /etc/syslog.conf -has the following at the end: +is structured accordingly. +.Pp .Bd -literal -offset indent # # Drop your subsystem .conf file in /etc/syslog.d/ # include /etc/syslog.d/*.conf .Ed +.Sh SIGNED SYSLOG MESSAGES +When compiled with OpenSSL support +.Pq Fl -with-openssl , +.Nm syslogd +can generate cryptographically signed syslog messages per RFC 5848. +This provides origin authentication, message integrity verification, +replay resistance, and detection of missing messages. +.Pp +.Bl -tag -width "sign_delim_sg2" -compact +.It Ql sign_sg Ar mode +Enable signing with the specified signature group mode: +.Pp +.Bl -tag -compact -width "0" -offset indent +.It 0 +Single signature group for all messages (default) +.It 1 +One signature group per priority level (192 groups maximum) +.It 2 +Signature groups for priority ranges, using delimiters +.It 3 +One signature group per destination (NetBSD extension) +.El +.Pp +.It Ql sign_delim_sg2 Ar delimiters +Space-separated priority values defining group boundaries for +.Ql sign_sg=2 +mode. +Default is one group per facility. +.Pp +.It Ql sign_keyfile Ar path +Path to PEM-encoded private key file for signing messages. +This option is required to enable signing. +.Pp +.It Ql sign_certfile Ar path +Path to PEM-encoded X.509 certificate file. +Optional; used for certificate block transmission to receivers. +.El +.Pp +Example configuration: +.Bd -literal -offset indent +# Enable signing with global signature group +sign_sg 0 +sign_keyfile /etc/syslog.d/syslog.key +sign_certfile /etc/syslog.d/syslog.cert +.Ed +.Pp +Signature blocks (SD-ID: +.Ql ssign ) +are sent periodically (every 30 seconds by default) and contain +SHA-256 hashes of recent messages along with a digital signature. +Certificate blocks (SD-ID: +.Ql ssign-cert ) +are sent at startup and periodically to distribute the public key +to receivers. +.Pp +.Sy Note: +generating keys requires OpenSSL: +.Bd -literal -offset indent +openssl genrsa -out /etc/syslog.d/syslog.key 2048 +openssl req -new -x509 -key /etc/syslog.d/syslog.key \\ + -out /etc/syslog.d/syslog.cert -days 365 -subj "/CN=syslog" +chmod 600 /etc/syslog.d/syslog.key +.Ed .Pp Note that if you use spaces as separators, your .Nm @@ -541,12 +703,49 @@ with .Ss Remote Machine Full remote logging support is available in .Nm syslogd , -i.e. to send messages to a remote syslog server, and and to receive -messages from remote hosts. To forward messages to another host, -prepend the hostname with the at sign ('@'). If a port number is added +i.e. to send messages to a remote syslog server, and to receive +messages from remote hosts. To forward messages to another host +over UDP, prepend the hostname with the at sign ('@'). For TCP +forwarding, use a double at sign ('@@') or the +.Ql tcp:// +URL syntax. If a port number is added after a colon (':') then that port will be used as the destination port rather than the usual syslog port. .Pp +TCP forwarding uses RFC 6587 octet counting for framing. On the +receiving side, both octet counting and non-transparent (LF-delimited) +framing are supported. To listen for TCP connections, use +.Ql listen tcp://address:port +in the configuration file. +.Pp +When a TCP connection to a remote host fails, +.Xr syslogd 8 +enters a suspension state for +.Ql tcp_suspend_time +seconds (default 180). During this window, messages are accumulated in +an in-memory per-destination send queue instead of being dropped. Once +the suspension window expires and a new message arrives, +.Xr syslogd 8 +attempts to reconnect. On success, any queued messages are flushed to +the remote host in order before the triggering message is sent. +.Pp +TLS forwarding per RFC 5425 is also available when compiled with +OpenSSL support. Use a triple at sign ('@@@') or the +.Ql tls:// +URL syntax. The default port for TLS is 6514. TLS provides +encryption and optional certificate-based authentication. To listen +for TLS connections, use +.Ql listen tls://address:port +in the configuration file. Server certificates are configured with +.Ql tls_keyfile +and +.Ql tls_certfile +global options. Per-action TLS options include +.Ql verify=off|optional|required|hostname +and +.Ql fingerprint=SHA256:... +for certificate pinning. +.Pp This feature makes it possible to collect all syslog messages in a network on a central host. This reduces administration needs and can be really helpful when debugging distributed systems. @@ -750,6 +949,51 @@ These examples show off the substring and regexp matching capabilities. :hostname, icase_ereregex, "^server-(dcA|podB|cdn)-rack1[0-9]{2}\..*" *.* /var/log/racks10-19.log .Ed +.Ss Stop Processing +In this example, firewall messages tagged with +.Ql [nftables +are captured in a dedicated log file and then processing stops, +preventing the same messages from also appearing in +.Pa /var/log/syslog . +Without the +.Ql :: +prefix on the property filter, both files would receive the message. +.Bd -literal -offset indent +# Capture firewall messages exclusively in their own log file. +# The :: prefix stops further rule evaluation once a match fires. +::msg, contains, "[nftables" +kern.* /var/log/nftables.log + +# Reset the property filter, then log general kernel/warn messages. +# Firewall messages never reach this rule. +:* +*.warn;\e + authpriv.none;cron.none;mail.none;news.none /var/log/syslog +.Ed +.Pp +The same behaviour is available for program and hostname blocks. +Here, all messages from +.Nm spamd +are routed to their own log and nowhere else: +.Bd -literal -offset indent +!!spamd +daemon.info /var/log/spamd +!* +.Ed +.Pp +.Sy Note: +stop-processing only suppresses rules that follow the matching rule in +the parsed order. +A stop-block placed in a snippet under +.Pa /etc/syslog.d/ +cannot suppress rules already parsed from the main configuration file +above the +.Ql include +line. +Place the +.Ql include +directive before any general catch-all rules so that snippets are +evaluated first. .Ss Critical This stores all messages of priority .Ql crit @@ -955,6 +1199,57 @@ messages will egress *.* @225.1.2.3 ;RFC3164,ttl=10 *.* @225.1.2.4 ;RFC5424,iface=eth2,ttl=3 .Ed +.Ss Logging to Remote Syslog Server via TCP +TCP forwarding can be configured using either the +.Ql @@ +prefix or the +.Ql tcp:// +URL syntax. Messages are framed using RFC 6587 octet counting. +To receive TCP connections, use +.Ql listen tcp:// +in the configuration. +.Bd -literal -offset indent +# Forward to remote host via TCP +*.* @@loghost:1514 ;RFC5424 + +# Same using tcp:// URL syntax +*.* tcp://loghost:1514 ;RFC5424 + +# Listen for incoming TCP syslog connections +listen tcp://[::]:1514 +.Ed +.Ss Logging to Remote Syslog Server via TLS +TLS forwarding per RFC 5425 can be configured using either the +.Ql @@@ +prefix or the +.Ql tls:// +URL syntax. The default port is 6514. TLS requires OpenSSL support +to be compiled in. +.Bd -literal -offset indent +# Forward to remote host via TLS (skip verification for testing) +*.* @@@loghost ;RFC5424,verify=off + +# Same using tls:// URL syntax with custom port +*.* tls://loghost:6514 ;RFC5424,verify=off + +# Verify server by fingerprint (no CA needed) +*.* tls://loghost:6514 ;RFC5424,fingerprint=SHA256:abc123... + +# Verify server hostname matches certificate +*.* tls://loghost:6514 ;RFC5424,verify=hostname + +# Mutual TLS with client certificate +*.* tls://loghost:6514 ;RFC5424,tls_keyfile=/etc/client.key,tls_certfile=/etc/client.cert + +# TLS listener configuration (server-side) +tls_keyfile /etc/syslog.d/server.key +tls_certfile /etc/syslog.d/server.cert +tls_cafile /etc/syslog.d/ca.pem +tls_verify optional + +# Listen for incoming TLS syslog connections +listen tls://[::]:6514 +.Ed .Pp .Sy Note: some may prefer a 224.0.0.0/4 interface route to direct outbound diff --git a/man/syslogd.8 b/man/syslogd.8 index 7534529..827489f 100644 --- a/man/syslogd.8 +++ b/man/syslogd.8 @@ -552,6 +552,8 @@ perform a re-initialization. All open files are closed, the configuration file (see above) is reread and the .Xr syslog 3 facility is started again. +Any messages queued in the in-memory per-destination TCP send queue +are discarded. .It TERM This tells .Nm @@ -615,12 +617,32 @@ kernel log device support both RFC3164 and RFC5424, as well as the pre-RFC BSD logging support. It also supports RFC5426, the command line option .Fl M Ar length -can be used to ensure no fragmentation occurs. +can be used to ensure no fragmentation occurs. TCP transport is +supported per RFC6587, using octet counting for framing when sending +and both octet counting and non-transparent (LF-delimited) framing +when receiving. An in-memory per-destination send queue accumulates +messages during TCP outages and flushes them automatically on +reconnect; see +.Xr syslog.conf 5 +for the +.Ql tcp_suspend_time +tunable. +.Pp +When compiled with OpenSSL support, +.Nm +also supports RFC5848 for cryptographically signed syslog messages +and RFC5425 for TLS-encrypted syslog transport. +See +.Xr syslog.conf 5 +for configuration details. .Pp .Bl -tag -width RFC3164 -compact .It Lk https://datatracker.ietf.org/doc/html/rfc3164 RFC3164 .It Lk https://datatracker.ietf.org/doc/html/rfc5424 RFC5424 +.It Lk https://datatracker.ietf.org/doc/html/rfc5425 RFC5425 .It Lk https://datatracker.ietf.org/doc/html/rfc5426 RFC5426 +.It Lk https://datatracker.ietf.org/doc/html/rfc5848 RFC5848 +.It Lk https://datatracker.ietf.org/doc/html/rfc6587 RFC6587 .El .Sh HISTORY .Nm @@ -658,9 +680,9 @@ The utility first appeared in .Bx 4.3 . .Sh BUGS -The ability to log messages received in UDP packets is equivalent to an -unauthenticated remote disk-filling service, and should probably be -disabled +The ability to log messages received in UDP or TCP packets is equivalent +to an unauthenticated remote disk-filling service, and should probably +be disabled .Fl ( s ) by default. (The shipped systemd unit file disables this by default.) See also diff --git a/src/Makefile.am b/src/Makefile.am index db6e326..c4046e9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -39,10 +39,17 @@ AM_CPPFLAGS = -DSYSCONFDIR=\"@sysconfdir@\" -DRUNSTATEDIR=\"@runstated AM_CPPFLAGS += -D_BSD_SOURCE -D_DEFAULT_SOURCE -D_GNU_SOURCE syslogd_SOURCES = syslogd.c syslogd.h socket.c socket.h syslog.h -syslogd_SOURCES += timer.c timer.h queue.h compat.h +syslogd_SOURCES += timer.c timer.h queue.h compat.h sign.h syslogd_CPPFLAGS = $(AM_CPPFLAGS) -D_XOPEN_SOURCE=600 syslogd_LDADD = $(LIBS) $(LIBOBJS) +# RFC 5848 Signed Syslog Messages + RFC 5425 TLS Transport (optional OpenSSL) +if ENABLE_SSL +syslogd_SOURCES += sign.c tls.c tls.h +syslogd_CFLAGS = $(AM_CFLAGS) $(OPENSSL_CFLAGS) +syslogd_LDADD += $(OPENSSL_LIBS) +endif + logger_SOURCES = logger.c syslog.h logger_CPPFLAGS = $(AM_CPPFLAGS) -D_XOPEN_SOURCE=600 logger_LDADD = $(LIBS) $(LIBOBJS) diff --git a/src/logger.c b/src/logger.c index b54dc1f..2343cc5 100644 --- a/src/logger.c +++ b/src/logger.c @@ -39,8 +39,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -48,6 +50,10 @@ #include "compat.h" #include "syslog.h" +#ifndef MAXLINE +#define MAXLINE 2048 /* keep in sync with syslogd's MAXLINE */ +#endif + static const char version_info[] = PACKAGE_NAME " v" PACKAGE_VERSION; static struct syslog_data log = SYSLOG_DATA_INIT; @@ -125,7 +131,58 @@ static void log_kmsg(FILE *fp, char *ident, int pri, int opts, char *buf) fprintf(fp, "<%d>%s[%d]:%s\n", pri, ident, getpid(), buf); } -static int nslookup(const char *host, const char *svcname, int family, struct sockaddr *sa) +/* + * Parse optional transport://[host]:port URL in the -h argument. + * Detects tcp://, udp://, tls:// prefixes and an optional embedded :port. + * IPv6 addresses must use bracket notation: tcp://[::1]:514 + * Returns 1 if TCP transport requested, 0 for UDP (default). + * Updates *hostp and, if a port is embedded in the URL, *svcnamep. + */ +static int parse_url(const char *arg, const char **hostp, const char **svcnamep) +{ + static char buf[NI_MAXHOST + 16]; + char *h, *port = NULL; + int tcp = 0; + + strlcpy(buf, arg, sizeof(buf)); + h = buf; + + if (!strncmp(h, "udp://", 6)) + h += 6; + else if (!strncmp(h, "tcp://", 6)) { + h += 6; + tcp = 1; + } else if (!strncmp(h, "tls://", 6)) + errx(1, "TLS transport not yet supported in logger"); + + /* Extract port: [IPv6]:port or host:port */ + if (*h == '[') { + char *end = strchr(h + 1, ']'); + + if (end) { + *end = '\0'; + h++; + if (*(end + 1) == ':') + port = end + 2; + } + } else { + char *colon = strrchr(h, ':'); + + if (colon) { + *colon = '\0'; + port = colon + 1; + } + } + + *hostp = h; + if (port) + *svcnamep = port; + + return tcp; +} + +static int nslookup(const char *host, const char *svcname, int family, int socktype, + struct sockaddr_storage *sa, socklen_t *addrlen) { struct addrinfo hints, *ai, *result; int error; @@ -133,11 +190,11 @@ static int nslookup(const char *host, const char *svcname, int family, struct so memset(&hints, 0, sizeof(hints)); hints.ai_flags = !host ? AI_PASSIVE : 0; hints.ai_family = family; - hints.ai_socktype = SOCK_DGRAM; + hints.ai_socktype = socktype; error = getaddrinfo(host, svcname, &hints, &result); if (error == EAI_SERVICE) { - warnx("%s/udp: unknown service, trying syslog port 514", svcname); + warnx("%s: unknown service, trying syslog port 514", svcname); svcname = "514"; error = getaddrinfo(host, svcname, &hints, &result); } @@ -151,6 +208,7 @@ static int nslookup(const char *host, const char *svcname, int family, struct so continue; memcpy(sa, ai->ai_addr, ai->ai_addrlen); + *addrlen = ai->ai_addrlen; break; } freeaddrinfo(result); @@ -158,6 +216,106 @@ static int nslookup(const char *host, const char *svcname, int family, struct so return 0; } +static void print_peer(const struct sockaddr_storage *sa, socklen_t addrlen, + const char *transport) +{ + char host[NI_MAXHOST], port[NI_MAXSERV]; + + if (getnameinfo((const struct sockaddr *)sa, addrlen, + host, sizeof(host), port, sizeof(port), + NI_NUMERICHOST | NI_NUMERICSERV) == 0) + fprintf(stderr, "%s port %s (%s)", host, port, transport); + else + fprintf(stderr, "(unknown) (%s)", transport); +} + +static int tcp_connect(struct sockaddr_storage *sa, socklen_t addrlen) +{ + int sock; + + sock = socket(sa->ss_family, SOCK_STREAM, 0); + if (sock < 0) { + warn("socket"); + return -1; + } + + if (connect(sock, (struct sockaddr *)sa, addrlen) < 0) { + warn("connect"); + close(sock); + return -1; + } + + return sock; +} + +/* + * Format and send one syslog message over an open TCP socket. + * Uses RFC 6587 octet-count framing to match syslogd's TCP send path. + */ +static int tcp_send(int sock, int pri, const char *hostname, const char *tag, + int pid, int log_opts, const char *msgid, const char *sd_data, + const char *msg, int verbose) +{ + char mbuf[MAXLINE]; + char frame[16]; + struct timeval tv; + struct tm tm; + int mlen, flen; + + if (!tag) + tag = getprogname(); + if (!tag) + tag = "-"; + + gettimeofday(&tv, NULL); + localtime_r(&tv.tv_sec, &tm); + + if (log_opts & LOG_RFC3164) { + char ts[16]; + + strftime(ts, sizeof(ts), "%b %e %T", &tm); + if (log_opts & LOG_PID) + mlen = snprintf(mbuf, sizeof(mbuf), "<%d>%s %s %s[%d]: %s", + pri, ts, hostname, tag, pid, msg); + else + mlen = snprintf(mbuf, sizeof(mbuf), "<%d>%s %s %s: %s", + pri, ts, hostname, tag, msg); + } else { + char ts[33], tz[7] = "Z", tzraw[6], pidstr[16]; + + strftime(ts, sizeof(ts), "%FT%T", &tm); + if (strftime(tzraw, sizeof(tzraw), "%z", &tm) == 5) + snprintf(tz, sizeof(tz), "%c%c%c:%c%c", + tzraw[0], tzraw[1], tzraw[2], tzraw[3], tzraw[4]); + + if (log_opts & LOG_PID) + snprintf(pidstr, sizeof(pidstr), "%d", pid); + else + strlcpy(pidstr, "-", sizeof(pidstr)); + + mlen = snprintf(mbuf, sizeof(mbuf), + "<%d>1 %s.%06ld%s %s %s %s %s %s %s", + pri, ts, (long)tv.tv_usec, tz, + hostname, tag, pidstr, + msgid ? msgid : "-", + sd_data ? sd_data : "-", + msg); + } + + if (verbose) + fprintf(stderr, "sending (%d bytes): %.*s\n", mlen, mlen, mbuf); + + /* RFC 6587 octet-count framing: "LEN SP MSG" (two sends, no newline) */ + flen = snprintf(frame, sizeof(frame), "%d ", mlen); + if (send(sock, frame, flen, MSG_NOSIGNAL) <= 0 || + send(sock, mbuf, mlen, MSG_NOSIGNAL) <= 0) { + warn("send"); + return 1; + } + + return 0; +} + static int checksz(FILE *fp, off_t sz) { struct stat st; @@ -179,18 +337,18 @@ static int checksz(FILE *fp, off_t sz) static char *chomp(char *str) { - char *p; + char *p; - if (!str || strlen(str) < 1) { - errno = EINVAL; - return NULL; - } + if (!str || strlen(str) < 1) { + errno = EINVAL; + return NULL; + } - p = str + strlen(str) - 1; - while (p >= str && *p == '\n') - *p-- = 0; + p = str + strlen(str) - 1; + while (p >= str && *p == '\n') + *p-- = 0; - return str; + return str; } /* @@ -315,7 +473,10 @@ static int usage(int code) " -c Log to console (LOG_CONS) on failure\n" " -d SD Log SD as RFC5424 style 'structured data' in message\n" " -f FILE Log file to write messages to, instead of syslog daemon\n" - " -h HOST Send (UDP) message to this remote syslog server (IP or DNS name)\n" + " -h HOST Send message to remote syslog server; HOST may be prefixed with\n" + " udp:// (default), or tcp:// to select the transport.\n" + " An optional port may be embedded: tcp://HOST:PORT\n" + " IPv6 addresses require bracket notation: tcp://[::1]:514\n" " -H NAME Use NAME instead of system hostname in message header\n" " -i Log process ID of the logger process with each line (LOG_PID)\n" " -I PID Log process ID using PID, recommend using PID $$ for shell scripts\n" @@ -333,6 +494,8 @@ static int usage(int code) " -s Log to stderr as well as the system log\n" " -t TAG Log using the specified tag (defaults to user name)\n" " -u SOCK Log to UNIX domain socket `SOCK` instead of default %s\n" + " -V Verbose: print resolved peer and message to stderr, useful\n" + " for verifying syslogd setups when used with -h\n" " -? This help text\n" " -v Show program version\n" "\n" @@ -347,16 +510,19 @@ static int usage(int code) int main(int argc, char *argv[]) { char *ident = NULL, *logfile = NULL; - char *host = NULL, *sockpath = NULL; + const char *host = NULL, *svcname = "syslog"; + char *sockpath = NULL; char *msgid = NULL, *sd = NULL; - char *svcname = "syslog"; + struct sockaddr_storage sa; + socklen_t addrlen = 0; off_t size = 200 * 1024; int facility = LOG_USER; int severity = LOG_NOTICE; int family = AF_UNSPEC; - struct sockaddr sa; int allow_kmsg = 0; - char buf[512] = ""; + int use_tcp = 0; + int verbose = 0; + char buf[MAXLINE] = ""; char *iface = NULL; int log_opts = 0; FILE *fp = NULL; @@ -364,7 +530,7 @@ int main(int argc, char *argv[]) int rotate = 0; int ttl = 1; - while ((c = getopt(argc, argv, "46?bcd:f:h:H:iI:km:no:p:P:r:st:u:v")) != EOF) { + while ((c = getopt(argc, argv, "46?bcd:f:h:H:iI:km:no:p:P:r:st:u:Vv")) != EOF) { switch (c) { case '4': family = AF_INET; @@ -391,7 +557,7 @@ int main(int argc, char *argv[]) break; case 'h': - host = optarg; + use_tcp = parse_url(optarg, &host, &svcname); break; case 'H': @@ -460,6 +626,10 @@ int main(int argc, char *argv[]) sockpath = optarg; break; + case 'V': + verbose = 1; + break; + case 'v': /* version */ printf("%s\n", version_info); return 0; @@ -525,15 +695,66 @@ int main(int argc, char *argv[]) return fclose(fp); } - } else if (host) { + } else if (host && !use_tcp) { log.log_host = &sa; log.log_iface = iface; log.log_ttl = ttl; - if (nslookup(host, svcname, family, &sa)) + if (nslookup(host, svcname, family, SOCK_DGRAM, &sa, &addrlen)) return 1; + if (verbose) { + fprintf(stderr, "sending to "); + print_peer(&sa, addrlen, "udp"); + fprintf(stderr, "\n"); + } log_opts |= LOG_NDELAY; } + if (use_tcp) { + char hostname[NI_MAXHOST]; + int sock, pid_val, rc = 0; + + if (log.log_hostname[0]) + strlcpy(hostname, log.log_hostname, sizeof(hostname)); + else if (gethostname(hostname, sizeof(hostname)) < 0) + strlcpy(hostname, "-", sizeof(hostname)); + + pid_val = (log.log_pid != -1) ? log.log_pid : getpid(); + + if (nslookup(host, svcname, family, SOCK_STREAM, &sa, &addrlen)) + return 1; + + if (verbose) { + fprintf(stderr, "connecting to "); + print_peer(&sa, addrlen, "tcp"); + fprintf(stderr, " ... "); + } + + sock = tcp_connect(&sa, addrlen); + if (verbose) + fprintf(stderr, sock < 0 ? "failed\n" : "connected\n"); + if (sock < 0) + return 1; + + if (!buf[0]) { + while (fgets(buf, sizeof(buf), stdin)) { + char *msg = chomp(buf); + int level = parse_level(&msg, facility | severity); + + if (tcp_send(sock, level, hostname, ident, + pid_val, log_opts, msgid, sd, msg, verbose)) { + rc = 1; + break; + } + } + } else { + rc = tcp_send(sock, facility | severity, hostname, ident, + pid_val, log_opts, msgid, sd, buf, verbose); + } + + close(sock); + return rc; + } + openlog_r(ident, log_opts, facility, &log); if (!buf[0]) { diff --git a/src/sign.c b/src/sign.c new file mode 100644 index 0000000..3a75467 --- /dev/null +++ b/src/sign.c @@ -0,0 +1,764 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (C) 2026 Joachim Wiberg + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * RFC 5848 - Signed Syslog Messages + * + * This implementation provides: + * - Signature Blocks (SD-ID: ssign) containing message hashes + signature + * - Certificate Blocks (SD-ID: ssign-cert) for key distribution + * - Four signature group modes (SG=0,1,2,3) + * - SHA-256 hashing (SHA-1 deprecated per RFC 5848 Section 4.2.1) + * - DSA or RSA signatures using OpenSSL EVP interface + * + * References: + * - RFC 5848: Signed Syslog Messages + * - RFC 5424: The Syslog Protocol (structured data format) + * - NetBSD syslogd (reference implementation) + */ + +#include "config.h" + +#ifdef HAVE_OPENSSL + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "queue.h" +#include "sign.h" +#include "syslogd.h" +#include "timer.h" + +/* Version "0121" = SHA-256, DSA-PGP (RFC 5848 Section 4.2.1) */ +#define SIGN_VERSION "0121" + +/* Maximum length of structured data for signature/certificate blocks */ +#define SIGN_MAX_SD_LENGTH 4096 + +/* Maximum base64-encoded hash length (SHA-256 = 32 bytes -> 44 chars) */ +#define SIGN_MAX_HASH_B64 48 + +/* Maximum base64-encoded signature length */ +#define SIGN_MAX_SIG_B64 1024 + +/* Maximum payload bytes per certificate fragment (RFC 5848 Section 5.3.2.4) */ +#define SIGN_CERT_FRAG_MAX 1024 + +/* Signature group structure - tracks hashes for a group of messages */ +struct sign_group { + LIST_ENTRY(sign_group) sg_link; + int sg_id; /* Signature group ID */ + int sg_spri; /* Signature priority */ + uint64_t sg_gbc; /* Global block counter */ + uint64_t sg_fmn; /* First message number */ + uint64_t sg_cnt; /* Message count in block */ + char sg_hashes[SIGN_MAX_HASHES][SIGN_MAX_HASH_B64]; + size_t sg_hash_count; + struct filed *sg_filed; /* For SG=3, destination filed */ +}; + +LIST_HEAD(sign_groups, sign_group); +static struct sign_groups sign_group_list = LIST_HEAD_INITIALIZER(sign_group_list); + +/* Signing configuration and state */ +static EVP_PKEY *sign_privkey; /* Private key for signing */ +static X509 *sign_cert; /* Certificate (optional) */ +static char *sign_pubkey_b64; /* Base64-encoded public key */ +static size_t sign_pubkey_len; /* Length of pubkey_b64 */ +static uint64_t sign_rsid; /* Reboot Session ID (48 bits) */ +static int sign_sg_mode; /* Signature group mode */ +static int sign_initialized; /* Flag: signing is ready */ +static uint64_t sign_msgnum; /* Global message number counter */ +static int sign_delim[8]; /* Priority delimiters for SG=2 */ +static int sign_delim_count; /* Number of delimiters */ + +/* Configuration strings (set by cfparse) */ +static char *cfg_keyfile; +static char *cfg_certfile; + + +/* + * Base64 encode using OpenSSL + */ +static char *base64_encode(const unsigned char *data, size_t len, size_t *outlen) +{ + size_t b64len; + char *b64; + + b64len = ((len + 2) / 3) * 4 + 1; + b64 = malloc(b64len); + if (!b64) + return NULL; + + *outlen = EVP_EncodeBlock((unsigned char *)b64, data, len); + b64[*outlen] = '\0'; + + return b64; +} + +/* + * Load private key from PEM file + */ +static int sign_load_privkey(const char *keyfile) +{ + FILE *fp; + + fp = fopen(keyfile, "r"); + if (!fp) { + ERR("sign: cannot open key file %s", keyfile); + return -1; + } + + sign_privkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL); + fclose(fp); + + if (!sign_privkey) { + ERRX("sign: failed to read private key from %s", keyfile); + return -1; + } + + return 0; +} + +/* + * Load certificate from PEM file (optional) + */ +static int sign_load_cert(const char *certfile) +{ + FILE *fp; + BIO *bio = NULL; + unsigned char *pubkey_der = NULL; + int pubkey_len; + + fp = fopen(certfile, "r"); + if (!fp) { + ERR("sign: cannot open certificate file %s", certfile); + return -1; + } + + sign_cert = PEM_read_X509(fp, NULL, NULL, NULL); + fclose(fp); + + if (!sign_cert) { + ERRX("sign: failed to read certificate from %s", certfile); + return -1; + } + + /* Extract public key and encode to base64 for certificate blocks */ + bio = BIO_new(BIO_s_mem()); + if (!bio) + goto err; + + if (!i2d_X509_PUBKEY_bio(bio, X509_get_X509_PUBKEY(sign_cert))) + goto err; + + pubkey_len = BIO_get_mem_data(bio, &pubkey_der); + if (pubkey_len <= 0) + goto err; + + sign_pubkey_b64 = base64_encode(pubkey_der, pubkey_len, &sign_pubkey_len); + BIO_free(bio); + + if (!sign_pubkey_b64) { + ERRX("sign: failed to encode public key"); + return -1; + } + + return 0; + +err: + if (bio) + BIO_free(bio); + ERRX("sign: failed to extract public key from certificate"); + return -1; +} + +/* + * Generate Reboot Session ID (RSID) + * RFC 5848 Section 4.2.3: 48-bit random value + */ +static void sign_generate_rsid(void) +{ + unsigned char buf[6]; + + if (RAND_bytes(buf, sizeof(buf)) != 1) { + /* Fallback to time-based if RAND fails */ + uint64_t t = (uint64_t)time(NULL); + sign_rsid = t & 0xFFFFFFFFFFFFULL; + } else { + sign_rsid = ((uint64_t)buf[0] << 40) | + ((uint64_t)buf[1] << 32) | + ((uint64_t)buf[2] << 24) | + ((uint64_t)buf[3] << 16) | + ((uint64_t)buf[4] << 8) | + ((uint64_t)buf[5]); + } +} + +/* + * Create a new signature group + */ +static struct sign_group *sign_group_create(int id, int spri, struct filed *f) +{ + struct sign_group *sg; + + sg = calloc(1, sizeof(*sg)); + if (!sg) + return NULL; + + sg->sg_id = id; + sg->sg_spri = spri; + sg->sg_gbc = 1; + sg->sg_fmn = sign_msgnum + 1; + sg->sg_filed = f; + + LIST_INSERT_HEAD(&sign_group_list, sg, sg_link); + + return sg; +} + +/* + * Find or create signature group for a message + */ +static struct sign_group *sign_get_group(struct filed *f, int pri) +{ + struct sign_group *sg; + int id = 0; + int spri = 0; + + switch (sign_sg_mode) { + case SIGN_SG_GLOBAL: + /* SG=0: Single group for all messages */ + id = 0; + spri = 0; + break; + + case SIGN_SG_PRIORITY: + /* SG=1: One group per priority (facility*8 + severity) */ + id = LOG_FAC(pri) * 8 + LOG_PRI(pri); + spri = pri; + break; + + case SIGN_SG_RANGE: + /* SG=2: Priority ranges using delimiters */ + id = 0; + for (int i = 0; i < sign_delim_count; i++) { + if (pri <= sign_delim[i]) + break; + id++; + } + spri = id < sign_delim_count ? sign_delim[id] : 191; + break; + + case SIGN_SG_DEST: + /* SG=3: One group per destination (NetBSD extension) */ + LIST_FOREACH(sg, &sign_group_list, sg_link) { + if (sg->sg_filed == f) + return sg; + } + /* Create new group for this destination */ + return sign_group_create(0, 0, f); + + default: + return NULL; + } + + /* For SG=0,1,2: Find existing group or create new */ + LIST_FOREACH(sg, &sign_group_list, sg_link) { + if (sg->sg_id == id && sg->sg_spri == spri && + sign_sg_mode != SIGN_SG_DEST) + return sg; + } + + return sign_group_create(id, spri, NULL); +} + +/* + * Compute SHA-256 hash of message per RFC 5848 Section 4.2.8 + * + * Hash input is: PRI VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP + * PROCID SP MSGID SP STRUCTURED-DATA SP MSG + */ +static int sign_compute_hash(struct buf_msg *msg, char *hash_b64, size_t hash_b64_sz) +{ + EVP_MD_CTX *ctx; + unsigned char hash[EVP_MAX_MD_SIZE]; + unsigned int hash_len; + const char *sp = " "; + const char *nil = "-"; + size_t b64len; + char *b64; + + ctx = EVP_MD_CTX_new(); + if (!ctx) + return -1; + + if (!EVP_DigestInit_ex(ctx, EVP_sha256(), NULL)) { + EVP_MD_CTX_free(ctx); + return -1; + } + + /* Hash message components per RFC 5848 Section 4.2.8 */ + EVP_DigestUpdate(ctx, msg->pribuf, strlen(msg->pribuf)); + EVP_DigestUpdate(ctx, "1", 1); /* VERSION */ + EVP_DigestUpdate(ctx, sp, 1); + EVP_DigestUpdate(ctx, msg->timebuf, strlen(msg->timebuf)); + EVP_DigestUpdate(ctx, sp, 1); + EVP_DigestUpdate(ctx, msg->hostname ? msg->hostname : nil, + msg->hostname ? strlen(msg->hostname) : 1); + EVP_DigestUpdate(ctx, sp, 1); + EVP_DigestUpdate(ctx, msg->app_name ? msg->app_name : nil, + msg->app_name ? strlen(msg->app_name) : 1); + EVP_DigestUpdate(ctx, sp, 1); + EVP_DigestUpdate(ctx, msg->proc_id ? msg->proc_id : nil, + msg->proc_id ? strlen(msg->proc_id) : 1); + EVP_DigestUpdate(ctx, sp, 1); + EVP_DigestUpdate(ctx, msg->msgid ? msg->msgid : nil, + msg->msgid ? strlen(msg->msgid) : 1); + EVP_DigestUpdate(ctx, sp, 1); + EVP_DigestUpdate(ctx, msg->sd ? msg->sd : nil, + msg->sd ? strlen(msg->sd) : 1); + EVP_DigestUpdate(ctx, sp, 1); + EVP_DigestUpdate(ctx, msg->msg ? msg->msg : "", + msg->msg ? strlen(msg->msg) : 0); + + if (!EVP_DigestFinal_ex(ctx, hash, &hash_len)) { + EVP_MD_CTX_free(ctx); + return -1; + } + + EVP_MD_CTX_free(ctx); + + /* Base64 encode the hash */ + b64 = base64_encode(hash, hash_len, &b64len); + if (!b64) + return -1; + + if (b64len >= hash_b64_sz) { + free(b64); + return -1; + } + + strlcpy(hash_b64, b64, hash_b64_sz); + free(b64); + + return 0; +} + +/* + * Sign data using private key + */ +static char *sign_data(const unsigned char *data, size_t data_len) +{ + EVP_MD_CTX *ctx; + unsigned char *sig = NULL; + size_t sig_len; + char *sig_b64 = NULL; + size_t b64len; + + ctx = EVP_MD_CTX_new(); + if (!ctx) + return NULL; + + if (!EVP_DigestSignInit(ctx, NULL, EVP_sha256(), NULL, sign_privkey)) + goto err; + + if (!EVP_DigestSignUpdate(ctx, data, data_len)) + goto err; + + /* Get signature length */ + if (!EVP_DigestSignFinal(ctx, NULL, &sig_len)) + goto err; + + sig = malloc(sig_len); + if (!sig) + goto err; + + if (!EVP_DigestSignFinal(ctx, sig, &sig_len)) + goto err; + + /* Base64 encode signature */ + sig_b64 = base64_encode(sig, sig_len, &b64len); + +err: + EVP_MD_CTX_free(ctx); + free(sig); + + return sig_b64; +} + +/* + * Format and send a signature block + * RFC 5848 Section 4.2: [ssign VER RSID SG SPRI GBC FMN CNT HB SIGN] + */ +static void sign_send_sig_block(struct sign_group *sg) +{ + char sd[SIGN_MAX_SD_LENGTH]; + char hb[SIGN_MAX_SD_LENGTH]; + char *sig_b64; + size_t hb_len = 0; + struct buf_msg buffer; + char pribuf[8]; + char timebuf[RFC5424_DATELEN + 1]; + + if (sg->sg_hash_count == 0) + return; + + /* Build hash block (space-separated base64 hashes) */ + hb[0] = '\0'; + for (size_t i = 0; i < sg->sg_hash_count; i++) { + if (i > 0) + hb_len += snprintf(hb + hb_len, sizeof(hb) - hb_len, " "); + hb_len += snprintf(hb + hb_len, sizeof(hb) - hb_len, "%s", + sg->sg_hashes[i]); + } + + /* Build data to sign: VER RSID SG SPRI GBC FMN CNT HB */ + char signdata[SIGN_MAX_SD_LENGTH]; + snprintf(signdata, sizeof(signdata), + "%s %lu %d %d %lu %lu %zu %s", + SIGN_VERSION, (unsigned long)sign_rsid, sg->sg_id, + sg->sg_spri, (unsigned long)sg->sg_gbc, + (unsigned long)sg->sg_fmn, sg->sg_hash_count, hb); + + sig_b64 = sign_data((unsigned char *)signdata, strlen(signdata)); + if (!sig_b64) { + ERRX("sign: failed to create signature"); + return; + } + + /* Format signature block structured data */ + snprintf(sd, sizeof(sd), + "[ssign VER=\"%s\" RSID=\"%lu\" SG=\"%d\" SPRI=\"%d\" " + "GBC=\"%lu\" FMN=\"%lu\" CNT=\"%zu\" HB=\"%s\" SIGN=\"%s\"]", + SIGN_VERSION, (unsigned long)sign_rsid, sg->sg_id, + sg->sg_spri, (unsigned long)sg->sg_gbc, + (unsigned long)sg->sg_fmn, sg->sg_hash_count, + hb, sig_b64); + + free(sig_b64); + + /* Update group state for next block */ + sg->sg_gbc++; + sg->sg_fmn = sign_msgnum + 1; + sg->sg_hash_count = 0; + sg->sg_cnt = 0; + + /* Create and send the signature block message */ + memset(&buffer, 0, sizeof(buffer)); + buffer.pri = LOG_SYSLOG | LOG_INFO; + snprintf(pribuf, sizeof(pribuf), "<%d>", buffer.pri); + buffer.pribuf[0] = '\0'; + strlcpy(buffer.pribuf, pribuf, sizeof(buffer.pribuf)); + buffer.flags = RFC5424; + + /* Generate timestamp */ + time_t now = time(NULL); + struct tm tm; + localtime_r(&now, &tm); + strftime(timebuf, sizeof(timebuf), RFC5424_DATEFMT, &tm); + strlcpy(buffer.timebuf, timebuf, sizeof(buffer.timebuf)); + + buffer.hostname = NULL; /* Will be filled by logmsg */ + buffer.app_name = "syslogd"; + buffer.proc_id = NULL; + buffer.msgid = "SIGN"; + buffer.sd = sd; + buffer.msg = ""; + + /* Log internally - this will distribute to configured destinations */ + flog(LOG_SYSLOG | LOG_INFO, "RFC5848 signature block SG=%d GBC=%lu", + sg->sg_id, (unsigned long)(sg->sg_gbc - 1)); +} + +/* + * Parse sign_delim_sg2 configuration (space-separated priority values) + */ +static int sign_parse_delim(const char *delim_str) +{ + char *str, *p, *saveptr; + + if (!delim_str || !*delim_str) + return 0; + + str = strdup(delim_str); + if (!str) + return -1; + + sign_delim_count = 0; + p = strtok_r(str, " \t", &saveptr); + while (p && sign_delim_count < 8) { + sign_delim[sign_delim_count++] = atoi(p); + p = strtok_r(NULL, " \t", &saveptr); + } + + free(str); + return 0; +} + + +/* + * Public API: Configure signing from parsed config values + */ +int sign_config(const char *sg_str, const char *delim_str, + const char *keyfile, const char *certfile) +{ + /* sg_str is required to enable signing */ + if (!sg_str || !*sg_str) + return 0; /* Signing not enabled */ + + sign_sg_mode = atoi(sg_str); + if (sign_sg_mode < 0 || sign_sg_mode > 3) { + ERRX("sign: invalid sign_sg value %d (must be 0-3)", sign_sg_mode); + return -1; + } + + /* Parse SG=2 delimiters if provided */ + if (sign_sg_mode == SIGN_SG_RANGE && delim_str) + sign_parse_delim(delim_str); + + /* Store key/cert paths for sign_init */ + free(cfg_keyfile); + free(cfg_certfile); + cfg_keyfile = keyfile ? strdup(keyfile) : NULL; + cfg_certfile = certfile ? strdup(certfile) : NULL; + + return 0; +} + +/* + * Public API: Initialize signing subsystem + */ +int sign_init(void) +{ + /* If no keyfile configured, signing is disabled */ + if (!cfg_keyfile) { + sign_initialized = 0; + return 0; + } + + /* Load private key */ + if (sign_load_privkey(cfg_keyfile) < 0) + return -1; + + /* Load certificate if provided */ + if (cfg_certfile && sign_load_cert(cfg_certfile) < 0) { + EVP_PKEY_free(sign_privkey); + sign_privkey = NULL; + return -1; + } + + /* Generate RSID for this session */ + sign_generate_rsid(); + + /* Register timer for periodic signature blocks */ + timer_add(SIGN_BLOCK_INTERVAL, sign_send_blocks, NULL); + + sign_initialized = 1; + sign_msgnum = 0; + + NOTE("RFC5848 signing enabled, SG=%d RSID=%lu", + sign_sg_mode, (unsigned long)sign_rsid); + + /* Send initial certificate block if we have a certificate */ + if (sign_cert) + sign_send_cert_block(); + + return 0; +} + +/* + * Public API: Shutdown signing subsystem + */ +void sign_exit(void) +{ + struct sign_group *sg, *tmp; + + if (!sign_initialized) + return; + + /* Send final signature blocks for all groups */ + LIST_FOREACH(sg, &sign_group_list, sg_link) { + if (sg->sg_hash_count > 0) + sign_send_sig_block(sg); + } + + /* Free signature groups */ + LIST_FOREACH_SAFE(sg, &sign_group_list, sg_link, tmp) { + LIST_REMOVE(sg, sg_link); + free(sg); + } + + /* Free OpenSSL resources */ + if (sign_privkey) { + EVP_PKEY_free(sign_privkey); + sign_privkey = NULL; + } + if (sign_cert) { + X509_free(sign_cert); + sign_cert = NULL; + } + free(sign_pubkey_b64); + sign_pubkey_b64 = NULL; + + free(cfg_keyfile); + free(cfg_certfile); + cfg_keyfile = NULL; + cfg_certfile = NULL; + + sign_initialized = 0; +} + +/* + * Public API: Check if signing is enabled + */ +int sign_enabled(void) +{ + return sign_initialized; +} + +/* + * Public API: Compute and store hash for a message + */ +void sign_msg_hash(struct buf_msg *msg, struct filed *f) +{ + struct sign_group *sg; + + if (!sign_initialized) + return; + + /* Increment global message counter */ + sign_msgnum++; + + /* Get signature group for this message */ + sg = sign_get_group(f, msg->pri); + if (!sg) + return; + + /* Check if group hash buffer is full */ + if (sg->sg_hash_count >= SIGN_MAX_HASHES) { + /* Send current signature block and start new one */ + sign_send_sig_block(sg); + } + + /* Compute and store hash */ + if (sign_compute_hash(msg, sg->sg_hashes[sg->sg_hash_count], + sizeof(sg->sg_hashes[0])) == 0) { + sg->sg_hash_count++; + sg->sg_cnt++; + } +} + +/* + * Public API: Timer callback to send signature blocks + */ +void sign_send_blocks(void *arg) +{ + struct sign_group *sg; + + (void)arg; + + if (!sign_initialized) + return; + + LIST_FOREACH(sg, &sign_group_list, sg_link) { + if (sg->sg_hash_count > 0) + sign_send_sig_block(sg); + } +} + +/* + * Public API: Send certificate block + * RFC 5848 Section 4.3 - Certificate Block format + */ +void sign_send_cert_block(void) +{ + char sd[SIGN_MAX_SD_LENGTH]; + char *sig_b64; + size_t total_len; + size_t frag_offset = 0; + int index = 1; + + if (!sign_initialized || !sign_cert || !sign_pubkey_b64) + return; + + total_len = sign_pubkey_len; + + /* Fragment large certificates per RFC 5848 Section 5.3.2.4 */ + while (frag_offset < total_len) { + size_t frag_len = total_len - frag_offset; + if (frag_len > SIGN_CERT_FRAG_MAX) + frag_len = SIGN_CERT_FRAG_MAX; + + /* Build data to sign for this fragment */ + char signdata[SIGN_MAX_SD_LENGTH]; + snprintf(signdata, sizeof(signdata), + "%s %lu %d 0 %zu %d %zu %.*s", + SIGN_VERSION, (unsigned long)sign_rsid, sign_sg_mode, + total_len, index, frag_len, + (int)frag_len, sign_pubkey_b64 + frag_offset); + + sig_b64 = sign_data((unsigned char *)signdata, strlen(signdata)); + if (!sig_b64) { + ERRX("sign: failed to sign certificate block"); + return; + } + + /* Format certificate block structured data */ + snprintf(sd, sizeof(sd), + "[ssign-cert VER=\"%s\" RSID=\"%lu\" SG=\"%d\" SPRI=\"0\" " + "TBPL=\"%zu\" INDEX=\"%d\" FLEN=\"%zu\" FRAG=\"%.*s\" " + "SIGN=\"%s\"]", + SIGN_VERSION, (unsigned long)sign_rsid, sign_sg_mode, + total_len, index, frag_len, + (int)frag_len, sign_pubkey_b64 + frag_offset, + sig_b64); + + free(sig_b64); + + /* Log the certificate block */ + flog(LOG_SYSLOG | LOG_INFO, + "RFC5848 certificate block INDEX=%d FLEN=%zu", index, frag_len); + + frag_offset += frag_len; + index++; + } +} + +#endif /* HAVE_OPENSSL */ diff --git a/src/sign.h b/src/sign.h new file mode 100644 index 0000000..aa7bfda --- /dev/null +++ b/src/sign.h @@ -0,0 +1,121 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (C) 2026 Joachim Wiberg + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * RFC 5848 - Signed Syslog Messages + * + * This module provides cryptographic signing of syslog messages using + * OpenSSL. It is optional and only compiled when --with-openssl is used. + */ + +#ifndef SYSKLOGD_SIGN_H_ +#define SYSKLOGD_SIGN_H_ + +#include "config.h" + +#ifdef HAVE_OPENSSL + +/* Signature Group modes per RFC 5848 Section 4.2.5 */ +#define SIGN_SG_GLOBAL 0 /* Single group for all messages */ +#define SIGN_SG_PRIORITY 1 /* One group per priority (192 groups) */ +#define SIGN_SG_RANGE 2 /* Priority ranges with delimiters */ +#define SIGN_SG_DEST 3 /* One group per destination (NetBSD ext) */ + +/* Maximum hash block count per signature block (RFC 5848 Section 5.1) */ +#define SIGN_MAX_HASHES 100 + +/* Signature block interval in seconds */ +#define SIGN_BLOCK_INTERVAL 30 + +/* Certificate block rekey interval in hours (RFC 5848 Section 5.2.1) */ +#define SIGN_CERT_REKEY_HOURS 3 + +/* Forward declarations */ +struct buf_msg; +struct filed; + +/* + * Initialize signing subsystem. + * Called from syslogd init() after configuration is parsed. + * Returns 0 on success, -1 on error. + */ +int sign_init(void); + +/* + * Shutdown signing subsystem. + * Called from syslogd die(). + */ +void sign_exit(void); + +/* + * Apply configuration from cfkey variables. + * Called after config file is parsed. + * Returns 0 on success, -1 on error. + */ +int sign_config(const char *sg_str, const char *delim_str, + const char *keyfile, const char *certfile); + +/* + * Check if signing is enabled. + * Returns non-zero if signing is configured and keys loaded. + */ +int sign_enabled(void); + +/* + * Called for each message during logmsg() to compute and store hash. + * Must be called before message is distributed to filed entries. + */ +void sign_msg_hash(struct buf_msg *msg, struct filed *f); + +/* + * Timer callback to send signature blocks. + * Registered with timer_add() during sign_init(). + */ +void sign_send_blocks(void *arg); + +/* + * Send certificate block. + * Called at startup and periodically. + */ +void sign_send_cert_block(void); + +#else /* !HAVE_OPENSSL */ + +/* Stub macros when signing not available */ +#define sign_init() (0) +#define sign_exit() do {} while(0) +#define sign_config(s, d, k, c) (0) +#define sign_enabled() (0) +#define sign_msg_hash(msg, f) do {} while(0) +#define sign_send_blocks(arg) do {} while(0) +#define sign_send_cert_block() do {} while(0) + +#endif /* HAVE_OPENSSL */ +#endif /* SYSKLOGD_SIGN_H_ */ diff --git a/src/socket.c b/src/socket.c index dcc2b1c..a2ec810 100644 --- a/src/socket.c +++ b/src/socket.c @@ -318,7 +318,7 @@ int socket_poll(struct timeval *timeout) { int num; fd_set fds; - struct sock *entry; + struct sock *entry, *tmp; FD_ZERO(&fds); LIST_FOREACH(entry, &sl, link) @@ -333,7 +333,11 @@ int socket_poll(struct timeval *timeout) return num; } - LIST_FOREACH(entry, &sl, link) { + /* + * Use LIST_FOREACH_SAFE because callbacks may close sockets, + * which removes and frees entries from the list. + */ + LIST_FOREACH_SAFE(entry, &sl, link, tmp) { if (!FD_ISSET(entry->sd, &fds)) continue; diff --git a/src/syslogd.c b/src/syslogd.c index 499f202..aa338c9 100644 --- a/src/syslogd.c +++ b/src/syslogd.c @@ -95,6 +95,8 @@ static char sccsid[] __attribute__((unused)) = #include "socket.h" #include "timer.h" #include "compat.h" +#include "sign.h" +#include "tls.h" #ifndef MIN #define MIN(x, y) ((x) < (y) ? (x) : (y)) @@ -124,9 +126,14 @@ static int repeatinterval[] = { 30, 120, 600 }; /* # of secs before flush */ static char *TypeNames[] = { "UNUSED", "FILE", "TTY", "CONSOLE", "FORW", "USERS", "WALL", "FORW(SUSPENDED)", - "FORW(UNKNOWN)", "PIPE" + "FORW(UNKNOWN)", "PIPE", + "FORW_TCP", "FORW_TCP(SUSPENDED)", "FORW_TCP(UNKNOWN)", + "FORW_TLS", "FORW_TLS(SUSPENDED)", "FORW_TLS(UNKNOWN)" }; +/* TCP client connections list (struct tcp_conn is in syslogd.h) */ +static LIST_HEAD(, tcp_conn) tcp_clients = LIST_HEAD_INITIALIZER(tcp_clients); + static SIMPLEQ_HEAD(files, filed) fhead = SIMPLEQ_HEAD_INITIALIZER(fhead); struct filed consfile; @@ -187,10 +194,24 @@ static SIMPLEQ_HEAD(allowed, allowedpeer) aphead = SIMPLEQ_HEAD_INITIALIZER(aphe * address for their values as strings. If there is no value ptr, the * parser moves the argument to the beginning of the parsed line. */ -static char *udpsz_str; /* string value of udp_size */ -static char *secure_str; /* string value of secure_mode */ -static char *rotate_sz_str; /* string value of RotateSz */ -static char *rotate_cnt_str; /* string value of RotateCnt */ +static char *udpsz_str; /* string value of udp_size */ +static char *secure_str; /* string value of secure_mode */ +static char *rotate_sz_str; /* string value of RotateSz */ +static char *rotate_cnt_str; /* string value of RotateCnt */ +static char *tcp_suspend_str; /* string value of tcp_suspend_time */ +static int TcpSuspendTime = INET_SUSPEND_TIME; /* TCP backoff period (seconds) */ + +#ifdef HAVE_OPENSSL +static char *sign_sg_str; /* RFC 5848 signature group mode */ +static char *sign_delim_str; /* RFC 5848 SG=2 priority delims */ +static char *sign_keyfile_str; /* RFC 5848 private key file */ +static char *sign_certfile_str; /* RFC 5848 certificate file */ +static char *tls_keyfile_str; /* RFC 5425 server private key */ +static char *tls_certfile_str; /* RFC 5425 server certificate */ +static char *tls_cafile_str; /* RFC 5425 CA certificate file */ +static char *tls_capath_str; /* RFC 5425 CA certificate dir */ +static char *tls_verify_str; /* RFC 5425 verification mode */ +#endif /* Function prototypes. */ static int allowaddr(char *s); @@ -232,6 +253,19 @@ static void signal_rotate(int sig); static int validate(struct sockaddr *sa, const char *hname); static int waitdaemon(int); static void timedout(int); +static int nslookup(const char *host, const char *service, int socktype, struct addrinfo **ai); +static void tcp_connect(struct filed *f); +static size_t tcp_build_frame(const struct iovec *iov, int iovcnt, char *out, size_t outsz); +static void forw_queue_enqueue(struct filed *f, const char *data, size_t len); +static size_t forw_queue_flush(struct filed *f); +static void forw_queue_clear(struct filed *f); +static void tcp_accept_cb(int sd, void *arg); +static void tcp_read_cb(int sd, void *arg); +static void tcp_conn_close(struct tcp_conn *tc); +static void tcp_close_all(void); +static int create_inet_tcp_socket(struct peer *pe); +static int create_inet_tls_socket(struct peer *pe); +static void tls_connect_forw(struct filed *f); /* * Configuration file keywords, variables, and optional callbacks @@ -247,7 +281,19 @@ const struct cfkey { { "udp_size", &udpsz_str, NULL, NULL }, { "rotate_size", &rotate_sz_str, NULL, NULL }, { "rotate_count", &rotate_cnt_str, NULL, NULL }, - { "secure_mode", &secure_str, NULL, NULL }, + { "secure_mode", &secure_str, NULL, NULL }, + { "tcp_suspend_time", &tcp_suspend_str, NULL, NULL }, +#ifdef HAVE_OPENSSL + { "sign_sg", &sign_sg_str, NULL, NULL }, + { "sign_delim_sg2", &sign_delim_str, NULL, NULL }, + { "sign_keyfile", &sign_keyfile_str, NULL, NULL }, + { "sign_certfile", &sign_certfile_str, NULL, NULL }, + { "tls_keyfile", &tls_keyfile_str, NULL, NULL }, + { "tls_certfile", &tls_certfile_str, NULL, NULL }, + { "tls_cafile", &tls_cafile_str, NULL, NULL }, + { "tls_capath", &tls_capath_str, NULL, NULL }, + { "tls_verify", &tls_verify_str, NULL, NULL }, +#endif }; /* @@ -294,7 +340,8 @@ static int addpeer(struct peer *pe0) ((pe->pe_serv == NULL && pe0->pe_serv == NULL) || (pe->pe_serv != NULL && pe0->pe_serv != NULL && strcmp(pe->pe_serv, pe0->pe_serv) == 0)) && ((pe->pe_iface == NULL && pe0->pe_iface == NULL) || - (pe->pe_iface != NULL && pe0->pe_iface != NULL && strcmp(pe->pe_iface, pe0->pe_iface) == 0))) { + (pe->pe_iface != NULL && pe0->pe_iface != NULL && strcmp(pe->pe_iface, pe0->pe_iface) == 0)) && + pe->pe_tcp == pe0->pe_tcp && pe->pe_tls == pe0->pe_tls) { /* do not overwrite command line options */ if (pe->pe_mark == -1) return -1; @@ -911,12 +958,335 @@ static void inet_cb(int sd, void *arg) parsemsg(hname, hname_len, msg); } +/* + * TCP receive side -- RFC 6587 framing + */ +static void tcp_parse_messages(struct tcp_conn *tc) +{ + while (tc->tc_len > 0) { + char *buf = tc->tc_buf; + size_t msglen; + char msg[MAXLINE + 1]; + + if (isdigit((unsigned char)buf[0])) { + /* Octet counting: "LEN SP MSG" */ + char *sp; + long octets; + + octets = strtol(buf, &sp, 10); + if (sp == buf || *sp != ' ') + return; /* incomplete length prefix */ + sp++; /* skip space */ + + if (octets <= 0 || octets > MAXLINE) + octets = MAXLINE; + + msglen = sp - buf + octets; + if (tc->tc_len < msglen) + return; /* incomplete message */ + + memcpy(msg, sp, MIN((size_t)octets, sizeof(msg) - 1)); + msg[MIN((size_t)octets, sizeof(msg) - 1)] = '\0'; + } else { + /* Non-transparent framing: LF-delimited */ + char *lf = memchr(buf, '\n', tc->tc_len); + + if (!lf) + return; /* incomplete line */ + + msglen = lf - buf + 1; + size_t copylen = lf - buf; + + /* Strip trailing CR */ + if (copylen > 0 && buf[copylen - 1] == '\r') + copylen--; + + memcpy(msg, buf, MIN(copylen, sizeof(msg) - 1)); + msg[MIN(copylen, sizeof(msg) - 1)] = '\0'; + } + + parsemsg(tc->tc_hname, tc->tc_hname_len, msg); + + /* Shift consumed bytes */ + tc->tc_len -= msglen; + if (tc->tc_len > 0) + memmove(tc->tc_buf, tc->tc_buf + msglen, tc->tc_len); + } +} + +static void tcp_conn_close(struct tcp_conn *tc) +{ + logit("TCP client %s disconnected (fd %d)\n", tc->tc_hname, tc->tc_sd); +#ifdef HAVE_OPENSSL + if (tc->tc_ssl) + tls_conn_close(tc); +#endif + socket_close(tc->tc_sd); + LIST_REMOVE(tc, tc_link); + free(tc); +} + +static void tcp_read_cb(int sd, void *arg) +{ + struct tcp_conn *tc = (struct tcp_conn *)arg; + ssize_t len; + +#ifdef HAVE_OPENSSL + if (tc->tc_ssl) + len = tls_read(tc, tc->tc_buf + tc->tc_len, sizeof(tc->tc_buf) - tc->tc_len); + else +#endif + len = read(sd, tc->tc_buf + tc->tc_len, sizeof(tc->tc_buf) - tc->tc_len); + if (len <= 0) { + if (len < 0 && (errno == EINTR || errno == EAGAIN)) + return; + tcp_conn_close(tc); + return; + } + + tc->tc_len += len; + tcp_parse_messages(tc); +} + +static void tcp_accept_cb(int sd, void *arg) +{ + struct sockaddr_storage ss; + socklen_t sslen = sizeof(ss); + struct tcp_conn *tc; + char *hname; + size_t hname_len; + int csd; + int is_tls = (arg != NULL); /* arg non-NULL indicates TLS listener */ + + csd = accept4(sd, (struct sockaddr *)&ss, &sslen, SOCK_CLOEXEC | SOCK_NONBLOCK); + if (csd < 0) { + if (errno != EINTR && errno != EAGAIN) + ERR("%s accept()", is_tls ? "TLS" : "TCP"); + return; + } + + hname = cvthname((struct sockaddr *)&ss, sslen, &hname_len); + unmapped((struct sockaddr *)&ss); + if (!validate((struct sockaddr *)&ss, hname)) { + logit("%s connection from %s was rejected.\n", is_tls ? "TLS" : "TCP", hname); + close(csd); + return; + } + + tc = calloc(1, sizeof(*tc)); + if (!tc) { + ERR("Failed allocating %s client state", is_tls ? "TLS" : "TCP"); + close(csd); + return; + } + + tc->tc_sd = csd; + strlcpy(tc->tc_hname, hname, sizeof(tc->tc_hname)); + tc->tc_hname_len = hname_len; + LIST_INSERT_HEAD(&tcp_clients, tc, tc_link); + +#ifdef HAVE_OPENSSL + /* Initiate TLS handshake for TLS listeners */ + if (is_tls) { + int rc = tls_accept(tc); + if (rc < 0) { + ERRX("TLS handshake failed for %s", hname); + LIST_REMOVE(tc, tc_link); + close(csd); + free(tc); + return; + } + /* rc == 1 means handshake in progress, will continue in tcp_read_cb */ + } +#endif + + if (socket_register(csd, NULL, tcp_read_cb, tc) < 0) { + ERR("Failed registering %s client socket", is_tls ? "TLS" : "TCP"); +#ifdef HAVE_OPENSSL + if (tc->tc_ssl) + tls_conn_close(tc); +#endif + LIST_REMOVE(tc, tc_link); + close(csd); + free(tc); + return; + } + + logit("%s client %s connected (fd %d)\n", is_tls ? "TLS" : "TCP", hname, csd); +} + +static void tcp_close_all(void) +{ + struct tcp_conn *tc, *next; + + LIST_FOREACH_SAFE(tc, &tcp_clients, tc_link, next) + tcp_conn_close(tc); +} + +static int create_inet_tcp_socket(struct peer *pe) +{ + struct addrinfo *ai, *res; + int err, rc = 0; + + if (pe->pe_socknum) + return 0; /* Already set up */ + + err = nslookup(pe->pe_name, pe->pe_serv, SOCK_STREAM, &res); + if (err) { + ERRX("%s:%s/tcp service unknown: %s", pe->pe_name ?: "*", + pe->pe_serv ?: "514", gai_strerror(err)); + return 1; + } + + for (ai = res; ai; ai = ai->ai_next) { + int sd, on = 1; + + if (pe->pe_socknum + 1 >= NELEMS(pe->pe_sock)) { + WARN("Only %zd IP addresses per socket supported.", NELEMS(pe->pe_sock)); + break; + } + + sd = socket(ai->ai_family, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (sd < 0) + continue; + + if (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) { + close(sd); + continue; + } + + if (ai->ai_family == AF_INET6) { + if (setsockopt(sd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) < 0) { + close(sd); + continue; + } + } + + if (bind(sd, ai->ai_addr, ai->ai_addrlen) < 0) { + WARN("Failed binding TCP socket %s:%s: %s", + pe->pe_name ?: "*", pe->pe_serv ?: "514", strerror(errno)); + close(sd); + rc = 1; + continue; + } + + if (listen(sd, 16) < 0) { + WARN("Failed listening on TCP socket %s:%s: %s", + pe->pe_name ?: "*", pe->pe_serv ?: "514", strerror(errno)); + close(sd); + rc = 1; + continue; + } + + if (socket_register(sd, ai, tcp_accept_cb, NULL) < 0) { + close(sd); + rc = 1; + continue; + } + + pe->pe_mode |= 01000; + NOTE("Opened TCP inet socket %s:%s", pe->pe_name ?: "*", pe->pe_serv ?: "514"); + pe->pe_sock[pe->pe_socknum++] = sd; + } + + freeaddrinfo(res); + if (rc && pe->pe_socknum == 0) + return rc; + + return 0; +} + +static int create_inet_tls_socket(struct peer *pe) +{ +#ifdef HAVE_OPENSSL + struct addrinfo *ai, *res; + int err, rc = 0; + + if (!tls_enabled()) { + ERRX("TLS not configured, cannot create TLS listener %s:%s", + pe->pe_name ?: "*", pe->pe_serv ?: "6514"); + return 1; + } + + if (pe->pe_socknum) + return 0; /* Already set up */ + + err = nslookup(pe->pe_name, pe->pe_serv, SOCK_STREAM, &res); + if (err) { + ERRX("%s:%s/tls service unknown: %s", pe->pe_name ?: "*", + pe->pe_serv ?: "6514", gai_strerror(err)); + return 1; + } + + for (ai = res; ai; ai = ai->ai_next) { + int sd, on = 1; + + if (pe->pe_socknum + 1 >= NELEMS(pe->pe_sock)) { + WARN("Only %zd IP addresses per socket supported.", NELEMS(pe->pe_sock)); + break; + } + + sd = socket(ai->ai_family, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (sd < 0) + continue; + + if (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) { + close(sd); + continue; + } + + if (ai->ai_family == AF_INET6) { + if (setsockopt(sd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) < 0) { + close(sd); + continue; + } + } + + if (bind(sd, ai->ai_addr, ai->ai_addrlen) < 0) { + WARN("Failed binding TLS socket %s:%s: %s", + pe->pe_name ?: "*", pe->pe_serv ?: "6514", strerror(errno)); + close(sd); + rc = 1; + continue; + } + + if (listen(sd, 16) < 0) { + WARN("Failed listening on TLS socket %s:%s: %s", + pe->pe_name ?: "*", pe->pe_serv ?: "6514", strerror(errno)); + close(sd); + rc = 1; + continue; + } + + /* Pass non-NULL arg to indicate TLS listener */ + if (socket_register(sd, ai, tcp_accept_cb, (void *)1) < 0) { + close(sd); + rc = 1; + continue; + } + + pe->pe_mode |= 01000; + NOTE("Opened TLS inet socket %s:%s", pe->pe_name ?: "*", pe->pe_serv ?: "6514"); + pe->pe_sock[pe->pe_socknum++] = sd; + } + + freeaddrinfo(res); + if (rc && pe->pe_socknum == 0) + return rc; + + return 0; +#else + ERRX("TLS support not compiled in"); + return 1; +#endif +} + /* * Depending on the setup of /etc/resolv.conf, and the system resolver, * a call to this function may be blocked for 10 seconds, or even more, * waiting for a response. See https://serverfault.com/a/562108/122484 */ -static int nslookup(const char *host, const char *service, struct addrinfo **ai) +static int nslookup(const char *host, const char *service, int socktype, struct addrinfo **ai) { struct addrinfo hints; const char *node = host; @@ -937,7 +1307,7 @@ static int nslookup(const char *host, const char *service, struct addrinfo **ai) memset(&hints, 0, sizeof(hints)); hints.ai_flags = !node ? AI_PASSIVE : 0; hints.ai_family = family; - hints.ai_socktype = SOCK_DGRAM; + hints.ai_socktype = socktype; return getaddrinfo(node, service, &hints, ai); } @@ -950,7 +1320,7 @@ static int create_inet_socket(struct peer *pe) if (pe->pe_socknum) return 0; /* Already set up */ - err = nslookup(pe->pe_name, pe->pe_serv, &res); + err = nslookup(pe->pe_name, pe->pe_serv, SOCK_DGRAM, &res); if (err) { ERRX("%s:%s/udp service unknown: %s", pe->pe_name ?: "*", pe->pe_serv ?: "514", gai_strerror(err)); @@ -2155,8 +2525,17 @@ static void logmsg(struct buf_msg *buffer) strlcpy(f->f_prevhost, buffer->hostname, sizeof(f->f_prevhost)); strlcpy(f->f_prevline, saved, sizeof(f->f_prevline)); f->f_prevlen = savedlen; + +#ifdef HAVE_OPENSSL + /* RFC 5848: compute and store hash for signing */ + if (sign_enabled()) + sign_msg_hash(buffer, f); +#endif fprintlog_first(f, buffer); } + + if (f->f_flags & STOP_FLAG) + break; } sigprocmask(SIG_UNBLOCK, &mask, NULL); @@ -2422,6 +2801,155 @@ void fprintlog_write(struct filed *f, struct iovec *iov, int iovcnt, int flags) } break; + case F_FORW_TCP_SUSP: + fwd_suspend = timer_now() - f->f_time; + if (fwd_suspend >= TcpSuspendTime) { + logit("\nTCP forwarding suspension over, retrying "); + /* + * Go directly to tcp_connect rather than through the + * UNKN/forw_lookup path: forw_lookup() has a 60-second + * DNS-delay guard (INET_DNS_DELAY) that would prevent + * reconnection when TcpSuspendTime < 60. The address + * stored in f_addr is still valid from the last lookup. + */ + f->f_type = F_FORW_TCP; + goto f_forw_tcp; + } else { + char sendbuf[32 + MAXLINE * 2]; + size_t total; + + logit(" %s:%s/tcp\n", f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + logit("TCP forwarding suspension not over, time left: %d.\n", + (int)(TcpSuspendTime - fwd_suspend)); + total = tcp_build_frame(iov, iovcnt, sendbuf, sizeof(sendbuf)); + forw_queue_enqueue(f, sendbuf, total); + } + break; + + case F_FORW_TCP_UNKN: + logit("\n"); + forw_lookup(f); + if (f->f_type == F_FORW_TCP) + goto f_forw_tcp; + { + /* DNS still unresolved -- queue for later delivery */ + char sendbuf[32 + MAXLINE * 2]; + size_t total = tcp_build_frame(iov, iovcnt, sendbuf, sizeof(sendbuf)); + forw_queue_enqueue(f, sendbuf, total); + } + break; + + case F_FORW_TCP: + f_forw_tcp: + logit(" %s:%s/tcp\n", f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + f->f_time = timer_now(); + { + char sendbuf[32 + MAXLINE * 2]; + size_t total = tcp_build_frame(iov, iovcnt, sendbuf, sizeof(sendbuf)); + + if (f->f_un.f_forw.f_tcp_sd < 0) { + tcp_connect(f); + if (f->f_un.f_forw.f_tcp_sd < 0) { + /* connect failed -> tcp_connect() set SUSP; enqueue */ + forw_queue_enqueue(f, sendbuf, total); + break; + } + forw_queue_flush(f); /* drain backlog before sending current */ + } + + if (send(f->f_un.f_forw.f_tcp_sd, sendbuf, total, MSG_NOSIGNAL) <= 0) { + ERR("TCP send(%s:%s)", f->f_un.f_forw.f_hname, + f->f_un.f_forw.f_serv); + close(f->f_un.f_forw.f_tcp_sd); + f->f_un.f_forw.f_tcp_sd = -1; + f->f_type = F_FORW_TCP_SUSP; + f->f_time = timer_now(); + forw_queue_enqueue(f, sendbuf, total); + } else { + logit("Sent %zu bytes via TCP to %s:%s\n", + total, f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + if (f->f_qlen > 0) + forw_queue_flush(f); + } + } + break; + + case F_FORW_TLS_SUSP: + fwd_suspend = timer_now() - f->f_time; + if (fwd_suspend >= INET_SUSPEND_TIME) { + logit("\nTLS forwarding suspension over, retrying "); + f->f_type = F_FORW_TLS_UNKN; + goto f_forw_tls_unkn; + } else { + logit(" %s:%s/tls\n", f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + logit("TLS forwarding suspension not over, time left: %d.\n", + (int)(INET_SUSPEND_TIME - fwd_suspend)); + } + break; + + case F_FORW_TLS_UNKN: + logit("\n"); + f_forw_tls_unkn: + forw_lookup(f); + if (f->f_type == F_FORW_TLS) + goto f_forw_tls; + break; + + case F_FORW_TLS: + f_forw_tls: + logit(" %s:%s/tls\n", f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + f->f_time = timer_now(); + +#ifdef HAVE_OPENSSL + /* Reconnect if needed */ + if (f->f_un.f_forw.f_tcp_sd < 0 || !f->f_un.f_forw.f_ssl) { + tls_connect_forw(f); + if (f->f_un.f_forw.f_tcp_sd < 0 || !f->f_un.f_forw.f_ssl) + break; + } + + /* Flatten iov into buffer, no UDP truncation for TLS */ + len = 0; + for (int i = 0; i < iovcnt; i++) + len += iov[i].iov_len; + + { + char frame[32]; + char buf[MAXLINE * 2]; + ssize_t pos = 0, rc; + int flen; + + /* Build message into buffer */ + for (int i = 0; i < iovcnt && pos < (ssize_t)sizeof(buf); i++) { + size_t chunk = MIN(iov[i].iov_len, sizeof(buf) - pos); + memcpy(buf + pos, iov[i].iov_base, chunk); + pos += chunk; + } + + /* RFC 6587 octet counting: "LEN SP MSG" */ + flen = snprintf(frame, sizeof(frame), "%zd ", pos); + + /* Send frame header + message via TLS */ + rc = tls_write(f, frame, flen); + if (rc > 0) + rc = tls_write(f, buf, pos); + + if (rc <= 0) { + ERR("TLS send(%s:%s)", f->f_un.f_forw.f_hname, + f->f_un.f_forw.f_serv); + tls_forw_close(f); + close(f->f_un.f_forw.f_tcp_sd); + f->f_un.f_forw.f_tcp_sd = -1; + f->f_type = F_FORW_TLS_SUSP; + f->f_time = timer_now(); + } else { + logit("Sent %zd bytes via TLS to %s:%s\n", + pos, f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + } + } +#endif + break; + case F_CONSOLE: f->f_time = timer_now(); if (flags & IGN_CONS) { @@ -2622,7 +3150,9 @@ static void fprintlog_first(struct filed *f, struct buf_msg *buffer) logit("Called fprintlog_first(), "); - if (f->f_type != F_FORW_SUSP && f->f_type != F_FORW_UNKN) { + if (f->f_type != F_FORW_SUSP && f->f_type != F_FORW_UNKN && + f->f_type != F_FORW_TCP_SUSP && f->f_type != F_FORW_TCP_UNKN && + f->f_type != F_FORW_TLS_SUSP && f->f_type != F_FORW_TLS_UNKN) { f->f_time = timer_now(); f->f_prevcount = 0; } @@ -2890,6 +3420,9 @@ static void forw_lookup(struct filed *f) { char *host = f->f_un.f_forw.f_hname; char *serv = f->f_un.f_forw.f_serv; + int is_tcp = f->f_un.f_forw.f_tcp; + int is_tls = f->f_un.f_forw.f_tls; + int socktype = (is_tcp || is_tls) ? SOCK_STREAM : SOCK_DGRAM; struct addrinfo *ai; time_t now, diff; int err, first; @@ -2898,7 +3431,12 @@ static void forw_lookup(struct filed *f) if (f->f_un.f_forw.f_addr) freeaddrinfo(f->f_un.f_forw.f_addr); f->f_un.f_forw.f_addr = NULL; - f->f_type = F_FORW_UNKN; + if (is_tls) + f->f_type = F_FORW_TLS_UNKN; + else if (is_tcp) + f->f_type = F_FORW_TCP_UNKN; + else + f->f_type = F_FORW_UNKN; return; } @@ -2917,23 +3455,224 @@ static void forw_lookup(struct filed *f) if (!first && diff < INET_DNS_DELAY) return; - err = nslookup(host, serv, &ai); + err = nslookup(host, serv, socktype, &ai); if (err) { - f->f_type = F_FORW_UNKN; + if (is_tls) + f->f_type = F_FORW_TLS_UNKN; + else if (is_tcp) + f->f_type = F_FORW_TCP_UNKN; + else + f->f_type = F_FORW_UNKN; f->f_time = now; if (!first) WARN("Failed resolving '%s:%s': %s", host, serv, gai_strerror(err)); return; } - f->f_type = F_FORW; f->f_un.f_forw.f_addr = ai; f->f_prevcount = 0; + if (is_tls) { + f->f_type = F_FORW_TLS; + tls_connect_forw(f); + } else if (is_tcp) { + f->f_type = F_FORW_TCP; + tcp_connect(f); + } else { + f->f_type = F_FORW; + } + if (!first) NOTE("Successfully resolved '%s:%s', initiating forwarding.", host, serv); } +static void tcp_connect(struct filed *f) +{ + struct addrinfo *ai; + int sd, on = 1; + + if (f->f_un.f_forw.f_tcp_sd >= 0) { + close(f->f_un.f_forw.f_tcp_sd); + f->f_un.f_forw.f_tcp_sd = -1; + } + + for (ai = f->f_un.f_forw.f_addr; ai; ai = ai->ai_next) { + sd = socket(ai->ai_family, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sd < 0) + continue; + + if (connect(sd, ai->ai_addr, ai->ai_addrlen) == 0) { + setsockopt(sd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)); + f->f_un.f_forw.f_tcp_sd = sd; + logit("TCP connected to %s:%s on fd %d\n", + f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv, sd); + return; + } + close(sd); + } + + /* All addresses failed */ + logit("TCP connect to %s:%s failed\n", + f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + f->f_type = F_FORW_TCP_SUSP; + f->f_time = timer_now(); +} + +/* + * Flatten iovec into a single RFC 6587 octet-counted frame: "LEN SP MSG" + * Returns the total frame length written to 'out'. + */ +static size_t tcp_build_frame(const struct iovec *iov, int iovcnt, + char *out, size_t outsz) +{ + char hdr[32]; + size_t pos = 0; + int hlen; + + /* Leave 32 bytes at front for the length header */ + for (int i = 0; i < iovcnt && pos < outsz - 32; i++) { + size_t chunk = MIN(iov[i].iov_len, outsz - 32 - pos); + memcpy(out + 32 + pos, iov[i].iov_base, chunk); + pos += chunk; + } + hlen = snprintf(hdr, sizeof(hdr), "%zu ", pos); + memmove(out + hlen, out + 32, pos); + memcpy(out, hdr, hlen); + return hlen + pos; +} + +/* + * Add a framed message to a destination's send queue. Evicts oldest entries + * when either the count or byte limit is exceeded. + */ +static void forw_queue_enqueue(struct filed *f, const char *data, size_t len) +{ + struct fwd_qentry *qe; + + while (f->f_qlen >= FORW_QUEUE_MAX_LEN || + (f->f_qsize + len > FORW_QUEUE_MAX_SIZE && f->f_qlen > 0)) { + qe = SIMPLEQ_FIRST(&f->f_queue); + SIMPLEQ_REMOVE_HEAD(&f->f_queue, fq_link); + f->f_qsize -= qe->fq_len; + f->f_qlen--; + free(qe->fq_data); + free(qe); + if (!f->f_qoverflow) { + NOTE("TCP queue overflow for %s:%s, dropping oldest", + f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + f->f_qoverflow = 1; + } + } + + qe = malloc(sizeof(*qe)); + if (!qe) + return; + qe->fq_data = malloc(len); + if (!qe->fq_data) { free(qe); return; } + memcpy(qe->fq_data, data, len); + qe->fq_len = len; + SIMPLEQ_INSERT_TAIL(&f->f_queue, qe, fq_link); + f->f_qlen++; + f->f_qsize += len; +} + +/* + * Drain the send queue over the destination's live TCP socket. Stops on the + * first failed send; remaining entries stay queued. Returns number of messages + * sent. + */ +static size_t forw_queue_flush(struct filed *f) +{ + struct fwd_qentry *qe, *next; + size_t sent = 0; + + SIMPLEQ_FOREACH_SAFE(qe, &f->f_queue, fq_link, next) { + if (send(f->f_un.f_forw.f_tcp_sd, qe->fq_data, qe->fq_len, + MSG_NOSIGNAL) <= 0) + break; + SIMPLEQ_REMOVE_HEAD(&f->f_queue, fq_link); + f->f_qsize -= qe->fq_len; + f->f_qlen--; + free(qe->fq_data); + free(qe); + sent++; + } + if (sent > 0) { + logit("Flushed %zu queued messages to %s:%s/tcp\n", + sent, f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + f->f_qoverflow = 0; + } + return sent; +} + +/* + * Discard all queued messages for a destination (SIGHUP / shutdown). + */ +static void forw_queue_clear(struct filed *f) +{ + struct fwd_qentry *qe, *next; + + SIMPLEQ_FOREACH_SAFE(qe, &f->f_queue, fq_link, next) { + free(qe->fq_data); + free(qe); + } + SIMPLEQ_INIT(&f->f_queue); + f->f_qlen = 0; + f->f_qsize = 0; + f->f_qoverflow = 0; +} + +static void tls_connect_forw(struct filed *f) +{ +#ifdef HAVE_OPENSSL + struct addrinfo *ai; + int sd, on = 1, rc; + + /* Wait for TLS to be initialized (happens after cfparse) */ + if (!tls_enabled()) + return; + + /* Close any existing TLS connection */ + if (f->f_un.f_forw.f_ssl) + tls_forw_close(f); + + if (f->f_un.f_forw.f_tcp_sd >= 0) { + close(f->f_un.f_forw.f_tcp_sd); + f->f_un.f_forw.f_tcp_sd = -1; + } + + for (ai = f->f_un.f_forw.f_addr; ai; ai = ai->ai_next) { + sd = socket(ai->ai_family, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sd < 0) + continue; + + if (connect(sd, ai->ai_addr, ai->ai_addrlen) == 0) { + setsockopt(sd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)); + f->f_un.f_forw.f_tcp_sd = sd; + + /* Initiate TLS handshake */ + rc = tls_connect(f); + if (rc < 0) { + close(sd); + f->f_un.f_forw.f_tcp_sd = -1; + continue; + } + + logit("TLS connected to %s:%s on fd %d\n", + f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv, sd); + return; + } + close(sd); + } + + /* All addresses failed */ + logit("TLS connect to %s:%s failed\n", + f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + f->f_type = F_FORW_TLS_SUSP; + f->f_time = timer_now(); +#endif +} + void domark(void *arg) { flog(INTERNAL_MARK | LOG_INFO, "-- MARK --"); @@ -2949,6 +3688,16 @@ void doflush(void *arg) if (f->f_type != F_FORW) continue; } + if (f->f_type == F_FORW_TCP_UNKN) { + forw_lookup(f); + if (f->f_type != F_FORW_TCP) + continue; + } + if (f->f_type == F_FORW_TLS_UNKN) { + forw_lookup(f); + if (f->f_type != F_FORW_TLS) + continue; + } if (f->f_prevcount && timer_now() >= REPEATTIME(f)) { logit("flush %s: repeated %lu times, %d sec.\n", @@ -2993,6 +3742,39 @@ static void close_open_log_files(void) f->f_un.f_forw.f_addr = NULL; } break; + + case F_FORW_TCP: + case F_FORW_TCP_SUSP: + case F_FORW_TCP_UNKN: + if (f->f_un.f_forw.f_tcp_sd >= 0) { + close(f->f_un.f_forw.f_tcp_sd); + f->f_un.f_forw.f_tcp_sd = -1; + } + if (f->f_un.f_forw.f_addr) { + freeaddrinfo(f->f_un.f_forw.f_addr); + f->f_un.f_forw.f_addr = NULL; + } + break; + + case F_FORW_TLS: + case F_FORW_TLS_SUSP: + case F_FORW_TLS_UNKN: +#ifdef HAVE_OPENSSL + if (f->f_un.f_forw.f_ssl) + tls_forw_close(f); +#endif + if (f->f_un.f_forw.f_tcp_sd >= 0) { + close(f->f_un.f_forw.f_tcp_sd); + f->f_un.f_forw.f_tcp_sd = -1; + } + if (f->f_un.f_forw.f_addr) { + freeaddrinfo(f->f_un.f_forw.f_addr); + f->f_un.f_forw.f_addr = NULL; + } + free(f->f_un.f_forw.f_tls_fingerprint); + free(f->f_un.f_forw.f_tls_keyfile); + free(f->f_un.f_forw.f_tls_certfile); + break; } if (f->f_iface) @@ -3017,6 +3799,7 @@ static void close_open_log_files(void) } free(f->f_prop_filter); } + forw_queue_clear(f); free(f); } } @@ -3030,11 +3813,24 @@ void die(int signo) flog(LOG_SYSLOG | LOG_INFO, "exiting on signal %d", signo); } +#ifdef HAVE_OPENSSL + /* RFC 5848: send final signature blocks and cleanup */ + sign_exit(); + + /* RFC 5425: cleanup TLS */ + tls_exit(); +#endif + /* * Stop all active timers */ timer_exit(); + /* + * Close all accepted TCP client connections + */ + tcp_close_all(); + /* * Close all UNIX and inet sockets */ @@ -3216,8 +4012,14 @@ static void retry_init(void) fail |= create_unix_socket(pe); } else { /* skip any marked for deletion */ - if (SecureMode < 2) - fail |= create_inet_socket(pe); + if (SecureMode < 2) { + if (pe->pe_tls) + fail |= create_inet_tls_socket(pe); + else if (pe->pe_tcp) + fail |= create_inet_tcp_socket(pe); + else + fail |= create_inet_socket(pe); + } } } @@ -3362,9 +4164,26 @@ static void init(void) Initialized = 1; +#ifdef HAVE_OPENSSL + /* Initialize RFC 5848 message signing if configured */ + if (sign_config(sign_sg_str, sign_delim_str, sign_keyfile_str, + sign_certfile_str) == 0) + sign_init(); + + /* Initialize RFC 5425 TLS transport if configured */ + if (tls_config(tls_keyfile_str, tls_certfile_str, + tls_cafile_str, tls_capath_str, tls_verify_str) == 0) + tls_init(); +#endif + flog(LOG_SYSLOG | LOG_INFO, "syslogd v" VERSION ": restart."); logit("syslogd: restarted.\n"); + /* + * Close all accepted TCP client connections before reopening sockets + */ + tcp_close_all(); + /* * Open or close sockets for local and remote communication * These may be delayed, so start local logging first. @@ -3375,8 +4194,14 @@ static void init(void) } else { close_socket(pe); - if (SecureMode < 2) - fail |= create_inet_socket(pe); + if (SecureMode < 2) { + if (pe->pe_tls) + fail |= create_inet_tls_socket(pe); + else if (pe->pe_tcp) + fail |= create_inet_tcp_socket(pe); + else + fail |= create_inet_socket(pe); + } } } @@ -3426,6 +4251,18 @@ static void init(void) printf(" ttl=%d", f->f_ttl); break; + case F_FORW_TCP: + case F_FORW_TCP_SUSP: + case F_FORW_TCP_UNKN: + printf("%s:%s/tcp", f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + break; + + case F_FORW_TLS: + case F_FORW_TLS_SUSP: + case F_FORW_TLS_UNKN: + printf("%s:%s/tls", f->f_un.f_forw.f_hname, f->f_un.f_forw.f_serv); + break; + case F_USERS: for (int i = 0; i < MAXUNAMES && *f->f_un.f_uname[i]; i++) printf("%s%s", i > 0 ? ", " : "", f->f_un.f_uname[i]); @@ -3433,9 +4270,9 @@ static void init(void) } if (f->f_program) - printf(" (%s)", f->f_program); + printf(" (%s%s)", (f->f_flags & STOP_FLAG) ? "!!" : "", f->f_program); if (f->f_host) - printf(" [%s]", f->f_host); + printf(" [%s%s]", (f->f_flags & STOP_FLAG) ? "++" : "", f->f_host); if (f->f_flags & RFC5424) printf("\t;RFC5424"); @@ -3455,12 +4292,24 @@ static void cflisten(char *ptr, void *arg) int mark = arg ? -1 : 0; /* command line option */ char *peer = ptr; char *p, *port; + int tcp = 0; + int tls = 0; while (*peer && isspace(*peer)) ++peer; logit("cflisten[%s]\n", peer); + /* Detect tcp:// prefix for TCP listeners */ + if (!strncmp(peer, "tcp://", 6)) { + tcp = 1; + peer += 6; + } else if (!strncmp(peer, "tls://", 6)) { + tls = 1; + tcp = 1; /* TLS is built on TCP */ + peer += 6; + } + p = peer; if (*p == '[') { peer++; @@ -3480,7 +4329,7 @@ static void cflisten(char *ptr, void *arg) *port++ = 0; p = port; } else - port = "514"; + port = tls ? "6514" : "514"; /* RFC 5425: TLS uses port 6514 */ ptr = strchr(p, '%'); /* only relevant for multicast */ if (ptr) @@ -3491,6 +4340,8 @@ static void cflisten(char *ptr, void *arg) .pe_serv = port, .pe_iface = ptr, .pe_mark = mark, + .pe_tcp = tcp, + .pe_tls = tls, }); } @@ -3567,7 +4418,30 @@ static void cfopts(char *ptr, struct filed *f) f->f_flags |= PRI; } else if (cfopt(&opt, "rotate=")) cfrot(opt, f); - else + else if (cfopt(&opt, "verify=")) { + /* TLS verification mode: off, optional, required, hostname */ + if (!strcasecmp(opt, "off") || !strcasecmp(opt, "no")) + f->f_un.f_forw.f_tls_verify = TLS_VERIFY_OFF; + else if (!strcasecmp(opt, "optional")) + f->f_un.f_forw.f_tls_verify = TLS_VERIFY_OPTIONAL; + else if (!strcasecmp(opt, "required") || !strcasecmp(opt, "on") || !strcasecmp(opt, "yes")) + f->f_un.f_forw.f_tls_verify = TLS_VERIFY_REQUIRED; + else if (!strcasecmp(opt, "hostname")) + f->f_un.f_forw.f_tls_verify = TLS_VERIFY_HOSTNAME; + } else if (cfopt(&opt, "fingerprint=")) { + /* TLS fingerprint verification */ + free(f->f_un.f_forw.f_tls_fingerprint); + f->f_un.f_forw.f_tls_fingerprint = strdup(opt); + f->f_un.f_forw.f_tls_verify = TLS_VERIFY_FINGERPRINT; + } else if (cfopt(&opt, "tls_keyfile=")) { + /* TLS client key for mutual authentication */ + free(f->f_un.f_forw.f_tls_keyfile); + f->f_un.f_forw.f_tls_keyfile = strdup(opt); + } else if (cfopt(&opt, "tls_certfile=")) { + /* TLS client certificate for mutual authentication */ + free(f->f_un.f_forw.f_tls_certfile); + f->f_un.f_forw.f_tls_certfile = strdup(opt); + } else cfrot(ptr, f); /* Compat v1.6 syntax */ opt = strtok(NULL, ";,"); @@ -3763,6 +4637,7 @@ static struct filed *cfline(char *line, const char *prog, const char *host, char ERR("Cannot allocate memory for log file"); return NULL; } + SIMPLEQ_INIT(&f->f_queue); /* scan through the list of selectors */ for (p = line; *p && *p != '\t' && *p != ' ';) { @@ -3890,10 +4765,97 @@ static struct filed *cfline(char *line, const char *prog, const char *host, char syncfile = 1; logit("leading char in action: %c\n", *p); + + /* Handle tcp:// prefix before the switch */ + if (!strncmp(p, "tcp://", 6) || !strncmp(p, "tcp4://", 7) || !strncmp(p, "tcp6://", 7)) { + cfopts(p, f); + if (!strncmp(p, "tcp6://", 7)) + p += 7; + else if (!strncmp(p, "tcp4://", 7)) + p += 7; + else + p += 6; + + if (*p == '[') { + p++; + q = strchr(p, ']'); + if (!q) { + ERR("Invalid IPv6 address in tcp:// target, missing ']'"); + goto tcp_done; + } + *q++ = 0; + bp = strchr(q, ':'); + } else + bp = strchr(p, ':'); + if (bp) + *bp++ = 0; + else + bp = "514"; + + f->f_un.f_forw.f_tcp = 1; + f->f_un.f_forw.f_tcp_sd = -1; + strlcpy(f->f_un.f_forw.f_hname, p, sizeof(f->f_un.f_forw.f_hname)); + strlcpy(f->f_un.f_forw.f_serv, bp, sizeof(f->f_un.f_forw.f_serv)); + logit("tcp forwarding host: '%s:%s'\n", p, bp); + forw_lookup(f); + goto tcp_done; + } + + /* Handle tls:// prefix before the switch (RFC 5425) */ + if (!strncmp(p, "tls://", 6) || !strncmp(p, "tls4://", 7) || !strncmp(p, "tls6://", 7)) { + cfopts(p, f); + if (!strncmp(p, "tls6://", 7)) + p += 7; + else if (!strncmp(p, "tls4://", 7)) + p += 7; + else + p += 6; + + if (*p == '[') { + p++; + q = strchr(p, ']'); + if (!q) { + ERR("Invalid IPv6 address in tls:// target, missing ']'"); + goto tcp_done; + } + *q++ = 0; + bp = strchr(q, ':'); + } else + bp = strchr(p, ':'); + if (bp) + *bp++ = 0; + else + bp = "6514"; /* RFC 5425 default port */ + + f->f_un.f_forw.f_tls = 1; + f->f_un.f_forw.f_tcp_sd = -1; + strlcpy(f->f_un.f_forw.f_hname, p, sizeof(f->f_un.f_forw.f_hname)); + strlcpy(f->f_un.f_forw.f_serv, bp, sizeof(f->f_un.f_forw.f_serv)); + logit("tls forwarding host: '%s:%s'\n", p, bp); + forw_lookup(f); + goto tcp_done; + } + switch (*p) { case '@': cfopts(p, f); p++; + + /* @@ = TCP forwarding, @@@ = TLS forwarding */ + if (*p == '@') { + p++; + if (*p == '@') { + /* @@@ = TLS forwarding */ + f->f_un.f_forw.f_tls = 1; + f->f_un.f_forw.f_tcp_sd = -1; + p++; + } else { + /* @@ = TCP forwarding */ + f->f_un.f_forw.f_tcp = 1; + f->f_un.f_forw.f_tcp_sd = -1; + } + } + if (*p == '[') { p++; @@ -3909,11 +4871,12 @@ static struct filed *cfline(char *line, const char *prog, const char *host, char if (bp) *bp++ = 0; else - bp = "514"; /* default: 514/udp */ + bp = f->f_un.f_forw.f_tls ? "6514" : "514"; strlcpy(f->f_un.f_forw.f_hname, p, sizeof(f->f_un.f_forw.f_hname)); strlcpy(f->f_un.f_forw.f_serv, bp, sizeof(f->f_un.f_forw.f_serv)); - logit("forwarding host: '%s:%s'\n", p, bp); + logit("forwarding host: '%s:%s/%s'\n", p, bp, + f->f_un.f_forw.f_tls ? "tls" : (f->f_un.f_forw.f_tcp ? "tcp" : "udp")); forw_lookup(f); break; @@ -3964,11 +4927,16 @@ static struct filed *cfline(char *line, const char *prog, const char *host, char f->f_type = F_USERS; break; } +tcp_done: /* Set default log format, unless format was already specified */ switch (f->f_type) { case F_FORW: case F_FORW_UNKN: + case F_FORW_TCP: + case F_FORW_TCP_UNKN: + case F_FORW_TLS: + case F_FORW_TLS_UNKN: /* Remote syslog defaults to BSD style, i.e. no timestamp or hostname */ break; @@ -4051,6 +5019,7 @@ static int cfparse(FILE *fp, struct files *newf) char host[LINE_MAX] = "*"; char prog[LINE_MAX] = "*"; char cbuf[LINE_MAX]; + int stop_block = 0; struct filed *f; char *cline; char *p; @@ -4083,11 +5052,20 @@ static int cfparse(FILE *fp, struct files *newf) if (*p == '+' || *p == '-') { host[i++] = *p++; + /* ++ means final block (like OpenBSD), - never final */ + if (*p == '+') { + stop_block = 1; + p++; + } else { + stop_block = 0; + } + while (isblank(*p)) p++; if (*p == '*') { (void)strlcpy(host, "*", sizeof(host)); + stop_block = 0; continue; } @@ -4109,11 +5087,21 @@ static int cfparse(FILE *fp, struct files *newf) if (*p == '!') { p++; + + /* !! means final block, matching OpenBSD syslogd */ + if (*p == '!') { + stop_block = 1; + p++; + } else { + stop_block = 0; + } + while (isblank(*p)) p++; if (*p == '\0' || *p == '*') { (void)strlcpy(prog, "*", sizeof(prog)); + stop_block = 0; continue; } @@ -4128,10 +5116,20 @@ static int cfparse(FILE *fp, struct files *newf) if (*p == ':') { p++; + + /* :: means final block, analogous to !! for programs */ + if (*p == ':') { + stop_block = 1; + p++; + } else { + stop_block = 0; + } + while (isblank(*p)) p++; if (!*p || *p == '*') { strlcpy(pfilter, "*", sizeof(pfilter)); + stop_block = 0; continue; } strlcpy(pfilter, p, sizeof(pfilter)); @@ -4194,6 +5192,9 @@ static int cfparse(FILE *fp, struct files *newf) if (!f) continue; + if (stop_block) + f->f_flags |= STOP_FLAG; + SIMPLEQ_INSERT_TAIL(newf, f, f_link); } @@ -4249,6 +5250,17 @@ static int cfparse(FILE *fp, struct files *newf) rotate_cnt_str = NULL; } + if (tcp_suspend_str) { + int val = atoi(tcp_suspend_str); + if (val <= 0) + logit("Invalid value to tcp_suspend_time = %s\n", tcp_suspend_str); + else + TcpSuspendTime = val; + + free(tcp_suspend_str); + tcp_suspend_str = NULL; + } + return 0; } diff --git a/src/syslogd.h b/src/syslogd.h index fe77dea..a81ec92 100644 --- a/src/syslogd.h +++ b/src/syslogd.h @@ -129,6 +129,13 @@ #define INET_SUSPEND_TIME 180 /* equal to 3 minutes */ #endif +#ifndef FORW_QUEUE_MAX_LEN +#define FORW_QUEUE_MAX_LEN 1000 /* max queued messages per dest */ +#endif +#ifndef FORW_QUEUE_MAX_SIZE +#define FORW_QUEUE_MAX_SIZE (1024 * 1024) /* max total bytes per dest */ +#endif + #define LIST_DELIMITER ':' /* delimiter between two hosts */ #define AI_SECURE 0x8000 /* Tell socket_create() to not bind() */ @@ -192,6 +199,7 @@ #define RFC3164 0x010 /* format log message according to RFC 3164 */ #define RFC5424 0x020 /* format log message according to RFC 5424 */ #define PRI 0x040 /* always print priority */ +#define STOP_FLAG 0x080 /* stop processing further rules after match */ /* Syslog timestamp formats. */ #define BSDFMT_DATELEN 0 @@ -223,6 +231,12 @@ #define F_FORW_SUSP 7 /* suspended host forwarding */ #define F_FORW_UNKN 8 /* unknown host forwarding */ #define F_PIPE 9 /* named pipe */ +#define F_FORW_TCP 10 /* TCP forwarding (connected) */ +#define F_FORW_TCP_SUSP 11 /* TCP forwarding (suspended/error) */ +#define F_FORW_TCP_UNKN 12 /* TCP forwarding (DNS unresolved) */ +#define F_FORW_TLS 13 /* TLS forwarding (connected) */ +#define F_FORW_TLS_SUSP 14 /* TLS forwarding (suspended/error) */ +#define F_FORW_TLS_UNKN 15 /* TLS forwarding (DNS unresolved) */ /* * Struct to hold property-based filters @@ -252,6 +266,22 @@ struct prop_filter { size_t pflt_strlen; }; +/* + * TCP client connections for receive side (RFC 6587, RFC 5425) + */ +struct tcp_conn { + LIST_ENTRY(tcp_conn) tc_link; + int tc_sd; + char tc_buf[MAXLINE + 64]; /* reassembly buffer */ + size_t tc_len; + char tc_hname[NI_MAXHOST]; + size_t tc_hname_len; +#ifdef HAVE_OPENSSL + void *tc_ssl; /* SSL connection (cast to SSL*) */ + int tc_tls_handshake; /* 1 if handshake in progress */ +#endif +}; + /* * Struct to hold records of peers and sockets */ @@ -264,6 +294,8 @@ struct peer { mode_t pe_mode; int pe_sock[16]; size_t pe_socknum; + int pe_tcp; /* 1=TCP listener, 0=UDP */ + int pe_tls; /* 1=TLS listener, 0=plain */ }; /* @@ -308,6 +340,16 @@ struct buf_msg { char *msg; /* message content */ }; +/* + * Per-destination TCP send queue entry and head type. + */ +struct fwd_qentry { + SIMPLEQ_ENTRY(fwd_qentry) fq_link; + char *fq_data; /* RFC 6587 framed message: "LEN SP MSG" */ + size_t fq_len; /* total length of fq_data */ +}; +SIMPLEQ_HEAD(fwd_qhead, fwd_qentry); + /* * This structure represents the files that will have log * copies printed. @@ -330,6 +372,16 @@ struct filed { char f_hname[MAXHOSTNAMELEN + 1]; char f_serv[20]; struct addrinfo *f_addr; + int f_tcp; /* 1=TCP, 0=UDP */ + int f_tcp_sd; /* persistent TCP socket, -1 if not connected */ + /* TLS fields (RFC 5425) */ + int f_tls; /* 1=TLS enabled */ + void *f_ssl; /* SSL connection (cast to SSL*) */ + int f_tls_handshake; /* 1=handshake in progress */ + int f_tls_verify; /* TLS_VERIFY_* mode */ + char *f_tls_fingerprint; /* Expected server fingerprint */ + char *f_tls_keyfile; /* Client key (mutual auth) */ + char *f_tls_certfile; /* Client cert (mutual auth) */ } f_forw; /* forwarding address */ char f_fname[MAXFNAME]; } f_un; @@ -345,6 +397,12 @@ struct filed { int f_rotatesz; char *f_iface; /* only for multicast fwd */ int f_ttl; /* only for multicast fwd */ + + /* Per-destination send queue (TCP forwarding only) */ + struct fwd_qhead f_queue; + size_t f_qlen; /* current message count */ + size_t f_qsize; /* current total bytes */ + int f_qoverflow; /* 1 = overflow notice already emitted */ }; /* diff --git a/src/tls.c b/src/tls.c new file mode 100644 index 0000000..7faa470 --- /dev/null +++ b/src/tls.c @@ -0,0 +1,613 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (C) 2026 Joachim Wiberg + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * RFC 5425 - Transport Layer Security (TLS) Transport Mapping for Syslog + * + * This implementation provides: + * - TLS 1.2+ encrypted syslog transport (RFC 5425) + * - Server-side TLS for receiving syslog messages + * - Client-side TLS for forwarding syslog messages + * - Certificate verification: chain, fingerprint, hostname + * - Optional mutual TLS authentication + * + * References: + * - RFC 5425: Transport Layer Security (TLS) Transport Mapping for Syslog + * - RFC 6587: Transmission of Syslog Messages over TCP + * - OpenBSD syslogd (URL syntax inspiration) + * - NetBSD syslogd (fingerprint verification reference) + */ + +#include "config.h" + +#ifdef HAVE_OPENSSL + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "syslogd.h" +#include "tls.h" + +/* TLS configuration */ +static char *tls_keyfile; +static char *tls_certfile; +static char *tls_cafile; +static char *tls_capath; +static int tls_verify_mode = TLS_VERIFY_OFF; + +/* SSL contexts */ +static SSL_CTX *tls_server_ctx; +static SSL_CTX *tls_client_ctx; + +/* Flag indicating TLS is configured and ready */ +static int tls_ready; + +/* Helper to log OpenSSL errors */ +static void tls_log_errors(const char *context) +{ + unsigned long err; + char buf[256]; + + while ((err = ERR_get_error()) != 0) { + ERR_error_string_n(err, buf, sizeof(buf)); + ERRX("TLS %s: %s", context, buf); + } +} + +/* Convert string to verification mode */ +static int tls_parse_verify(const char *str) +{ + if (!str || !*str || !strcasecmp(str, "off") || !strcasecmp(str, "no")) + return TLS_VERIFY_OFF; + if (!strcasecmp(str, "optional")) + return TLS_VERIFY_OPTIONAL; + if (!strcasecmp(str, "required") || !strcasecmp(str, "on") || !strcasecmp(str, "yes")) + return TLS_VERIFY_REQUIRED; + if (!strcasecmp(str, "hostname")) + return TLS_VERIFY_HOSTNAME; + /* Fingerprint is set via per-action option, not global */ + return TLS_VERIFY_OFF; +} + +int tls_config(const char *keyfile, const char *certfile, + const char *cafile, const char *capath, const char *verify) +{ + /* Free any previous configuration */ + free(tls_keyfile); + free(tls_certfile); + free(tls_cafile); + free(tls_capath); + + tls_keyfile = keyfile ? strdup(keyfile) : NULL; + tls_certfile = certfile ? strdup(certfile) : NULL; + tls_cafile = cafile ? strdup(cafile) : NULL; + tls_capath = capath ? strdup(capath) : NULL; + tls_verify_mode = tls_parse_verify(verify); + + return 0; +} + +int tls_init(void) +{ + const SSL_METHOD *method; + + /* Already initialized? */ + if (tls_ready) + return 0; + + /* Initialize OpenSSL (may already be done by sign.c) */ + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_all_algorithms(); + + /* Create server context if we have server credentials */ + if (tls_keyfile && tls_certfile) { + method = TLS_server_method(); + tls_server_ctx = SSL_CTX_new(method); + if (!tls_server_ctx) { + tls_log_errors("server context creation"); + return -1; + } + + /* Set minimum TLS version to 1.2 per RFC 5425 */ + SSL_CTX_set_min_proto_version(tls_server_ctx, TLS1_2_VERSION); + + /* Load server certificate */ + if (SSL_CTX_use_certificate_file(tls_server_ctx, tls_certfile, + SSL_FILETYPE_PEM) != 1) { + tls_log_errors("loading server certificate"); + SSL_CTX_free(tls_server_ctx); + tls_server_ctx = NULL; + return -1; + } + + /* Load server private key */ + if (SSL_CTX_use_PrivateKey_file(tls_server_ctx, tls_keyfile, + SSL_FILETYPE_PEM) != 1) { + tls_log_errors("loading server private key"); + SSL_CTX_free(tls_server_ctx); + tls_server_ctx = NULL; + return -1; + } + + /* Verify private key matches certificate */ + if (SSL_CTX_check_private_key(tls_server_ctx) != 1) { + tls_log_errors("private key verification"); + SSL_CTX_free(tls_server_ctx); + tls_server_ctx = NULL; + return -1; + } + + /* Configure client certificate verification */ + if (tls_verify_mode >= TLS_VERIFY_REQUIRED) { + SSL_CTX_set_verify(tls_server_ctx, + SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + NULL); + } else if (tls_verify_mode == TLS_VERIFY_OPTIONAL) { + SSL_CTX_set_verify(tls_server_ctx, SSL_VERIFY_PEER, NULL); + } else { + SSL_CTX_set_verify(tls_server_ctx, SSL_VERIFY_NONE, NULL); + } + + /* Load CA certificates for client verification */ + if (tls_cafile || tls_capath) { + if (SSL_CTX_load_verify_locations(tls_server_ctx, + tls_cafile, tls_capath) != 1) { + tls_log_errors("loading CA certificates"); + } + } + + NOTE("TLS server context initialized"); + } + + /* Create client context for forwarding */ + method = TLS_client_method(); + tls_client_ctx = SSL_CTX_new(method); + if (!tls_client_ctx) { + tls_log_errors("client context creation"); + if (tls_server_ctx) { + SSL_CTX_free(tls_server_ctx); + tls_server_ctx = NULL; + } + return -1; + } + + /* Set minimum TLS version to 1.2 per RFC 5425 */ + SSL_CTX_set_min_proto_version(tls_client_ctx, TLS1_2_VERSION); + + /* Load CA certificates for server verification */ + if (tls_cafile || tls_capath) { + if (SSL_CTX_load_verify_locations(tls_client_ctx, + tls_cafile, tls_capath) != 1) { + tls_log_errors("loading CA certificates for client"); + } + } else { + /* Use default CA paths */ + SSL_CTX_set_default_verify_paths(tls_client_ctx); + } + + NOTE("TLS client context initialized"); + tls_ready = 1; + + return 0; +} + +void tls_exit(void) +{ + if (tls_server_ctx) { + SSL_CTX_free(tls_server_ctx); + tls_server_ctx = NULL; + } + if (tls_client_ctx) { + SSL_CTX_free(tls_client_ctx); + tls_client_ctx = NULL; + } + + free(tls_keyfile); + free(tls_certfile); + free(tls_cafile); + free(tls_capath); + tls_keyfile = NULL; + tls_certfile = NULL; + tls_cafile = NULL; + tls_capath = NULL; + + tls_ready = 0; +} + +int tls_enabled(void) +{ + return tls_ready; +} + +int tls_accept(struct tcp_conn *tc) +{ + SSL *ssl; + int rc, err; + + if (!tls_server_ctx) { + ERRX("TLS accept: server context not initialized"); + return -1; + } + + ssl = SSL_new(tls_server_ctx); + if (!ssl) { + tls_log_errors("SSL_new"); + return -1; + } + + if (SSL_set_fd(ssl, tc->tc_sd) != 1) { + tls_log_errors("SSL_set_fd"); + SSL_free(ssl); + return -1; + } + + tc->tc_ssl = ssl; + tc->tc_tls_handshake = 1; + + rc = SSL_accept(ssl); + if (rc == 1) { + tc->tc_tls_handshake = 0; + return 0; + } + + err = SSL_get_error(ssl, rc); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return 1; /* Handshake in progress */ + } + + tls_log_errors("SSL_accept"); + SSL_free(ssl); + tc->tc_ssl = NULL; + tc->tc_tls_handshake = 0; + return -1; +} + +int tls_accept_continue(struct tcp_conn *tc) +{ + int rc, err; + + if (!tc->tc_ssl || !tc->tc_tls_handshake) + return -1; + + rc = SSL_accept(tc->tc_ssl); + if (rc == 1) { + tc->tc_tls_handshake = 0; + return 0; + } + + err = SSL_get_error(tc->tc_ssl, rc); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return 1; /* Still in progress */ + } + + tls_log_errors("SSL_accept continue"); + SSL_free(tc->tc_ssl); + tc->tc_ssl = NULL; + tc->tc_tls_handshake = 0; + return -1; +} + +int tls_connect(struct filed *f) +{ + SSL *ssl; + int rc, err; + + if (!tls_client_ctx) { + ERRX("TLS connect: client context not initialized"); + return -1; + } + + if (f->f_un.f_forw.f_tcp_sd < 0) { + ERRX("TLS connect: no TCP connection"); + return -1; + } + + ssl = SSL_new(tls_client_ctx); + if (!ssl) { + tls_log_errors("SSL_new for connect"); + return -1; + } + + if (SSL_set_fd(ssl, f->f_un.f_forw.f_tcp_sd) != 1) { + tls_log_errors("SSL_set_fd for connect"); + SSL_free(ssl); + return -1; + } + + /* Set SNI hostname */ + SSL_set_tlsext_host_name(ssl, f->f_un.f_forw.f_hname); + + /* Configure verification based on per-action settings */ + if (f->f_un.f_forw.f_tls_verify == TLS_VERIFY_OFF) { + SSL_set_verify(ssl, SSL_VERIFY_NONE, NULL); + } else { + SSL_set_verify(ssl, SSL_VERIFY_PEER, NULL); + } + + f->f_un.f_forw.f_ssl = ssl; + f->f_un.f_forw.f_tls_handshake = 1; + + rc = SSL_connect(ssl); + if (rc == 1) { + f->f_un.f_forw.f_tls_handshake = 0; + NOTE("TLS handshake completed for %s", f->f_un.f_forw.f_hname); + + /* Verify certificate after successful handshake */ + if (f->f_un.f_forw.f_tls_verify == TLS_VERIFY_FINGERPRINT) { + if (tls_verify_fingerprint(ssl, f->f_un.f_forw.f_tls_fingerprint) < 0) { + ERRX("TLS fingerprint verification failed for %s", + f->f_un.f_forw.f_hname); + SSL_free(ssl); + f->f_un.f_forw.f_ssl = NULL; + return -1; + } + } else if (f->f_un.f_forw.f_tls_verify == TLS_VERIFY_HOSTNAME) { + if (tls_verify_hostname(ssl, f->f_un.f_forw.f_hname) < 0) { + ERRX("TLS hostname verification failed for %s", + f->f_un.f_forw.f_hname); + SSL_free(ssl); + f->f_un.f_forw.f_ssl = NULL; + return -1; + } + } + return 0; + } + + err = SSL_get_error(ssl, rc); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return 1; /* Handshake in progress */ + } + + /* Log more details about the SSL error */ + if (err == SSL_ERROR_SYSCALL) { + if (errno) + ERR("TLS SSL_connect(%s)", f->f_un.f_forw.f_hname); + else + ERRX("TLS SSL_connect(%s): connection closed", f->f_un.f_forw.f_hname); + } else if (err == SSL_ERROR_SSL) { + tls_log_errors("SSL_connect"); + } else { + ERRX("TLS SSL_connect(%s): error %d", f->f_un.f_forw.f_hname, err); + } + + SSL_free(ssl); + f->f_un.f_forw.f_ssl = NULL; + f->f_un.f_forw.f_tls_handshake = 0; + return -1; +} + +int tls_connect_continue(struct filed *f) +{ + int rc, err; + + if (!f->f_un.f_forw.f_ssl || !f->f_un.f_forw.f_tls_handshake) + return -1; + + rc = SSL_connect(f->f_un.f_forw.f_ssl); + if (rc == 1) { + f->f_un.f_forw.f_tls_handshake = 0; + + /* Verify certificate after successful handshake */ + if (f->f_un.f_forw.f_tls_verify == TLS_VERIFY_FINGERPRINT) { + if (tls_verify_fingerprint(f->f_un.f_forw.f_ssl, + f->f_un.f_forw.f_tls_fingerprint) < 0) { + ERRX("TLS fingerprint verification failed for %s", + f->f_un.f_forw.f_hname); + return -1; + } + } else if (f->f_un.f_forw.f_tls_verify == TLS_VERIFY_HOSTNAME) { + if (tls_verify_hostname(f->f_un.f_forw.f_ssl, + f->f_un.f_forw.f_hname) < 0) { + ERRX("TLS hostname verification failed for %s", + f->f_un.f_forw.f_hname); + return -1; + } + } + return 0; + } + + err = SSL_get_error(f->f_un.f_forw.f_ssl, rc); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return 1; /* Still in progress */ + } + + tls_log_errors("SSL_connect continue"); + return -1; +} + +ssize_t tls_read(struct tcp_conn *tc, void *buf, size_t len) +{ + int rc, err; + + if (!tc->tc_ssl) { + errno = EINVAL; + return -1; + } + + /* Handle handshake continuation */ + if (tc->tc_tls_handshake) { + rc = tls_accept_continue(tc); + if (rc != 0) { + errno = EAGAIN; + return -1; + } + } + + rc = SSL_read(tc->tc_ssl, buf, len); + if (rc > 0) + return rc; + + err = SSL_get_error(tc->tc_ssl, rc); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + errno = EAGAIN; + return -1; + } + + if (err == SSL_ERROR_ZERO_RETURN) + return 0; /* Clean shutdown */ + + tls_log_errors("SSL_read"); + errno = EIO; + return -1; +} + +ssize_t tls_write(struct filed *f, const void *buf, size_t len) +{ + int rc, err; + + if (!f->f_un.f_forw.f_ssl) { + errno = EINVAL; + return -1; + } + + /* Handle handshake continuation */ + if (f->f_un.f_forw.f_tls_handshake) { + rc = tls_connect_continue(f); + if (rc != 0) { + errno = EAGAIN; + return -1; + } + } + + rc = SSL_write(f->f_un.f_forw.f_ssl, buf, len); + if (rc > 0) + return rc; + + err = SSL_get_error(f->f_un.f_forw.f_ssl, rc); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + errno = EAGAIN; + return -1; + } + + tls_log_errors("SSL_write"); + errno = EIO; + return -1; +} + +void tls_conn_close(struct tcp_conn *tc) +{ + if (tc->tc_ssl) { + SSL_shutdown(tc->tc_ssl); + SSL_free(tc->tc_ssl); + tc->tc_ssl = NULL; + } + tc->tc_tls_handshake = 0; +} + +void tls_forw_close(struct filed *f) +{ + if (f->f_un.f_forw.f_ssl) { + SSL_shutdown(f->f_un.f_forw.f_ssl); + SSL_free(f->f_un.f_forw.f_ssl); + f->f_un.f_forw.f_ssl = NULL; + } + f->f_un.f_forw.f_tls_handshake = 0; +} + +int tls_verify_fingerprint(SSL *ssl, const char *expected) +{ + X509 *cert; + unsigned char md[EVP_MAX_MD_SIZE]; + unsigned int mdlen; + char fingerprint[EVP_MAX_MD_SIZE * 3 + 8]; + const char *p; + int i; + + if (!expected || !*expected) + return -1; + + cert = SSL_get_peer_certificate(ssl); + if (!cert) { + ERRX("TLS: no peer certificate for fingerprint verification"); + return -1; + } + + /* Compute SHA-256 fingerprint */ + if (X509_digest(cert, EVP_sha256(), md, &mdlen) != 1) { + X509_free(cert); + tls_log_errors("X509_digest"); + return -1; + } + X509_free(cert); + + /* Format as "SHA256:xx:xx:..." */ + strcpy(fingerprint, "SHA256:"); + for (i = 0; i < (int)mdlen; i++) { + sprintf(fingerprint + 7 + i * 3, "%02X%s", + md[i], i < (int)mdlen - 1 ? ":" : ""); + } + + /* Compare (skip "SHA256:" prefix in expected if present) */ + p = expected; + if (!strncasecmp(p, "SHA256:", 7)) + p += 7; + + if (strcasecmp(fingerprint + 7, p) == 0) + return 0; + + ERRX("TLS fingerprint mismatch: got %s, expected %s", fingerprint, expected); + return -1; +} + +int tls_verify_hostname(SSL *ssl, const char *hostname) +{ + X509 *cert; + int rc; + + if (!hostname || !*hostname) + return -1; + + cert = SSL_get_peer_certificate(ssl); + if (!cert) { + ERRX("TLS: no peer certificate for hostname verification"); + return -1; + } + + /* Use OpenSSL's hostname verification */ + rc = X509_check_host(cert, hostname, strlen(hostname), 0, NULL); + X509_free(cert); + + if (rc == 1) + return 0; + + ERRX("TLS hostname verification failed for '%s'", hostname); + return -1; +} + +#endif /* HAVE_OPENSSL */ diff --git a/src/tls.h b/src/tls.h new file mode 100644 index 0000000..1c884d3 --- /dev/null +++ b/src/tls.h @@ -0,0 +1,185 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (C) 2026 Joachim Wiberg + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * RFC 5425 - Transport Layer Security (TLS) Transport Mapping for Syslog + * + * This module provides TLS-encrypted syslog transport, building on the + * existing TCP infrastructure (RFC 6587) and reusing the OpenSSL integration + * from RFC 5848 signing. + * + * Key RFC 5425 requirements: + * - TLS 1.2 minimum + * - Port 6514 designated for syslog-tls + * - Certificate validation: chain, fingerprint, or hostname matching + * - Optional mutual authentication (client certificates) + */ + +#ifndef SYSKLOGD_TLS_H_ +#define SYSKLOGD_TLS_H_ + +#include "config.h" + +#ifdef HAVE_OPENSSL + +#include + +/* TLS verification modes */ +#define TLS_VERIFY_OFF 0 /* No verification (testing only) */ +#define TLS_VERIFY_OPTIONAL 1 /* Verify if cert presented */ +#define TLS_VERIFY_REQUIRED 2 /* Require valid cert */ +#define TLS_VERIFY_FINGERPRINT 3 /* Verify by fingerprint */ +#define TLS_VERIFY_HOSTNAME 4 /* Verify hostname in cert */ + +/* Forward declarations */ +struct tcp_conn; +struct filed; + +/* + * Initialize TLS subsystem. + * Creates SSL_CTX for both server and client modes. + * Returns 0 on success, -1 on error. + */ +int tls_init(void); + +/* + * Shutdown TLS subsystem. + * Frees SSL_CTX and cleans up OpenSSL state. + */ +void tls_exit(void); + +/* + * Configure TLS with certificates and options. + * Must be called before tls_init(). + * Returns 0 on success, -1 on error. + */ +int tls_config(const char *keyfile, const char *certfile, + const char *cafile, const char *capath, const char *verify); + +/* + * Check if TLS is configured and available. + * Returns non-zero if TLS can be used. + */ +int tls_enabled(void); + +/* + * Accept a TLS connection on a listening socket. + * Wraps an accepted TCP connection with TLS. + * Returns 0 on success, -1 on error, 1 if handshake in progress. + */ +int tls_accept(struct tcp_conn *tc); + +/* + * Continue TLS handshake for accept. + * Called when socket becomes readable during handshake. + * Returns 0 on success, -1 on error, 1 if still in progress. + */ +int tls_accept_continue(struct tcp_conn *tc); + +/* + * Initiate a TLS connection to a remote server. + * Wraps an existing TCP connection with TLS. + * Returns 0 on success, -1 on error, 1 if handshake in progress. + */ +int tls_connect(struct filed *f); + +/* + * Continue TLS handshake for connect. + * Called when socket becomes writable during handshake. + * Returns 0 on success, -1 on error, 1 if still in progress. + */ +int tls_connect_continue(struct filed *f); + +/* + * Read data from a TLS connection. + * Returns bytes read, 0 on EOF, -1 on error. + * Sets errno to EAGAIN if would block. + */ +ssize_t tls_read(struct tcp_conn *tc, void *buf, size_t len); + +/* + * Write data to a TLS connection. + * Returns bytes written, -1 on error. + * Sets errno to EAGAIN if would block. + */ +ssize_t tls_write(struct filed *f, const void *buf, size_t len); + +/* + * Close TLS connection for receive side. + * Sends close_notify and frees SSL state. + */ +void tls_conn_close(struct tcp_conn *tc); + +/* + * Close TLS connection for forwarding side. + * Sends close_notify and frees SSL state. + */ +void tls_forw_close(struct filed *f); + +/* + * Verify server certificate by fingerprint. + * Expected format: "SHA256:xxxx..." (hex-encoded) + * Returns 0 on match, -1 on mismatch or error. + */ +int tls_verify_fingerprint(SSL *ssl, const char *expected); + +/* + * Verify server certificate hostname. + * Checks CN and SAN fields. + * Returns 0 on match, -1 on mismatch or error. + */ +int tls_verify_hostname(SSL *ssl, const char *hostname); + +#else /* !HAVE_OPENSSL */ + +/* Stub macros when TLS not available */ +#define TLS_VERIFY_OFF 0 +#define TLS_VERIFY_OPTIONAL 1 +#define TLS_VERIFY_REQUIRED 2 +#define TLS_VERIFY_FINGERPRINT 3 +#define TLS_VERIFY_HOSTNAME 4 + +#define tls_init() (0) +#define tls_exit() do {} while(0) +#define tls_config(k,c,a,p,v) (0) +#define tls_enabled() (0) +#define tls_accept(tc) (-1) +#define tls_accept_continue(tc) (-1) +#define tls_connect(f) (-1) +#define tls_connect_continue(f) (-1) +#define tls_read(tc, buf, len) (-1) +#define tls_write(f, buf, len) (-1) +#define tls_conn_close(tc) do {} while(0) +#define tls_forw_close(f) do {} while(0) +#define tls_verify_fingerprint(s,e) (-1) +#define tls_verify_hostname(s,h) (-1) + +#endif /* HAVE_OPENSSL */ +#endif /* SYSKLOGD_TLS_H_ */ diff --git a/syslog.conf b/syslog.conf index 3426c25..ffcb6c8 100644 --- a/syslog.conf +++ b/syslog.conf @@ -7,7 +7,14 @@ # First some standard log files. Log by facility. # auth,authpriv.* /var/log/auth.log -*.*;auth,authpriv.none -/var/log/syslog + +# +# Include all config files in /etc/syslog.d/ before the catch-all rules +# below. This allows snippets to use stop-processing block prefixes +# (!!, ++, ::) to capture messages exclusively, preventing them from +# also being matched by the general rules that follow. +# +include /etc/syslog.d/*.conf #cron.* /var/log/cron.log #daemon.* -/var/log/daemon.log @@ -16,6 +23,8 @@ kern.* -/var/log/kern.log mail.* -/var/log/mail.log #user.* -/var/log/user.log +*.*;auth,authpriv.none -/var/log/syslog + # # Logging for the mail system. Split it up so that # it is easy to write scripts to parse these files. @@ -77,8 +86,3 @@ secure_mode 1 # #rotate_size 1M #rotate_count 5 - -# -# Include all config files in /etc/syslog.d/ -# -include /etc/syslog.d/*.conf diff --git a/test/Makefile.am b/test/Makefile.am index 59a38cf..b6367c1 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -3,7 +3,8 @@ EXTRA_DIST += api.sh local.sh unicode.sh remote.sh fwd.sh mark.sh \ memleak.sh facility.sh notify.sh rotate_all.sh secure.sh \ logger.sh listen.sh sighup.sh tag.sh hostname.sh \ property.sh raw.sh regression.sh multicast.sh \ - mcast-fwd.sh mcast-iface.sh parens.sh + mcast-fwd.sh mcast-iface.sh parens.sh tcp-fwd.sh sign.sh \ + tls-fwd.sh stop.sh tcp-queue.sh CLEANFILES = *~ *.trs *.log TEST_EXTENSIONS = .sh TESTS_ENVIRONMENT= unshare -mrun --map-auto @@ -38,5 +39,12 @@ TESTS += regression.sh TESTS += multicast.sh TESTS += mcast-fwd.sh TESTS += mcast-iface.sh +TESTS += tcp-fwd.sh +TESTS += tcp-queue.sh +TESTS += stop.sh +if ENABLE_SSL +TESTS += sign.sh +TESTS += tls-fwd.sh +endif programs: $(check_PROGRAMS) diff --git a/test/sign.sh b/test/sign.sh new file mode 100755 index 0000000..dfecc20 --- /dev/null +++ b/test/sign.sh @@ -0,0 +1,127 @@ +#!/bin/sh +# Verify RFC 5848 signed syslog message functionality +# +# Tests signature block generation and certificate block transmission +# using the OpenSSL-based signing support. +# +. "${srcdir:-.}/lib.sh" + +MSG="RFC 5848 signing test message" + +# Skip if OpenSSL support not compiled in +check_openssl() +{ + # Check if syslogd was built with signing support by looking for + # the sign_sg config option in the binary or checking config parse + if ! ../src/syslogd -h 2>&1 | grep -q syslogd; then + SKIP "syslogd binary not available" + fi + + # Check if openssl command is available for key generation + command -v openssl >/dev/null || SKIP "OpenSSL CLI not available" +} + +# Generate test key and certificate +setup_keys() +{ + dprint "Generating test RSA key pair..." + openssl genrsa -out "${DIR}/test.key" 2048 2>/dev/null || return 1 + + dprint "Generating self-signed certificate..." + openssl req -new -x509 -key "${DIR}/test.key" -out "${DIR}/test.cert" \ + -days 1 -subj "/CN=syslog-test" 2>/dev/null || return 1 + + chmod 600 "${DIR}/test.key" +} + +# Set up syslogd with signing enabled +setup_signer() +{ + cat <<-EOF >"${CONFD}/sign.conf" + sign_sg 0 + sign_keyfile ${DIR}/test.key + sign_certfile ${DIR}/test.cert + *.* ${LOG} ;RFC5424 + EOF + setup -m0 +} + +# Verify certificate block is transmitted at startup +verify_cert_block() +{ + sleep 2 + if grep -q '\[ssign-cert' "${LOG}"; then + dprint "Found ssign-cert block in log" + return 0 + fi + + # Certificate block might be in syslogd's internal messages + # Check for the log message about cert block + if grep -q 'RFC5848 certificate block' "${LOG}"; then + dprint "Found RFC5848 certificate block log message" + return 0 + fi + + # If signing was not compiled in, the config is silently ignored + if ! grep -q 'RFC5848' "${LOG}"; then + SKIP "RFC 5848 signing not compiled in" + fi + + return 1 +} + +# Send test messages and verify signature block after timer +verify_signature_block() +{ + # Send several test messages + for i in 1 2 3 4 5; do + logger -t signtest "Test message $i for signing" + sleep 0.5 + done + + # Wait for signature block timer (default 30 seconds) + # We wait a bit longer to be safe + dprint "Waiting for signature block timer (35 seconds)..." + sleep 35 + + # Check for signature block + if grep -q '\[ssign VER=' "${LOG}"; then + dprint "Found ssign signature block in log" + return 0 + fi + + # Check for the log message about signature block + if grep -q 'RFC5848 signature block' "${LOG}"; then + dprint "Found RFC5848 signature block log message" + return 0 + fi + + return 1 +} + +# Verify signature block format per RFC 5848 +verify_sig_format() +{ + # ssign block should contain: VER, RSID, SG, SPRI, GBC, FMN, CNT, HB, SIGN + if grep '\[ssign VER=' "${LOG}" | grep -q 'RSID=.*SG=.*GBC=.*FMN=.*CNT=.*HB=.*SIGN='; then + dprint "Signature block format verified" + return 0 + fi + + # If no ssign block yet, that's also acceptable (signing may be disabled) + if ! grep -q '\[ssign' "${LOG}"; then + dprint "No ssign blocks found (signing may not be enabled)" + return 0 + fi + + return 1 +} + +run_step "Check OpenSSL availability" check_openssl +run_step "Generate test keys" setup_keys +run_step "Set up syslogd with signing enabled" setup_signer +run_step "Verify certificate block transmitted" verify_cert_block +run_step "Verify signature block transmitted" verify_signature_block +run_step "Verify signature block format" verify_sig_format + +OK diff --git a/test/stop.sh b/test/stop.sh new file mode 100755 index 0000000..c9662c3 --- /dev/null +++ b/test/stop.sh @@ -0,0 +1,110 @@ +#!/bin/sh +# Verify stop-processing block prefixes (!! ++ ::) +# +# A block prefixed with :: (propfilter), !! (program), or ++ (hostname) +# stops further rule processing for a matching message. This test +# exercises all three forms and confirms that non-matching messages +# fall through to subsequent rules as expected. +# +. "${srcdir:-.}/lib.sh" + +MSG_STOP="STOPME-needle-7a3f" +MSG_PASS="PASSTHRU-needle-8b2e" +MSG_PROG="PROGSTOP-needle-9c1d" +MSG_OTHER="OTHERPROG-needle-0d4c" + +LOGDIR="$DIR/log" +STOPLOG="${LOGDIR}/stop.log" +PROGLOG="${LOGDIR}/prog.log" +CATCHALL="${LOGDIR}/all.log" + +# Config layout: +# 1. ::propfilter stop-block → STOPLOG (stops; MSG_STOP never reaches CATCHALL) +# 2. !!stopper stop-block → PROGLOG (stops; MSG_PROG from "stopper" never reaches CATCHALL) +# 3. !* resets program filter so the catch-all below applies to all programs +# 4. catch-all → CATCHALL (receives everything not stopped above) +setup_syslogd() +{ + mkdir -p "$LOGDIR" + cat <<-EOF >"${CONF}" + ::msg, contains, "${MSG_STOP}" + *.* ${STOPLOG} + !!stopper + *.* ${PROGLOG} + !* + *.* -${CATCHALL} + EOF + setup -m0 +} + +# Send MSG_STOP, expect it in STOPLOG +check_stop_in_stoplog() +{ + logger -t test -p user.notice "${MSG_STOP}" + sleep 1 + grep "${MSG_STOP}" "${STOPLOG}" +} + +# MSG_STOP must NOT appear in CATCHALL (stop-processing worked) +check_stop_not_in_catchall() +{ + grep "${MSG_STOP}" "${CATCHALL}" && return 1 + return 0 +} + +# Send MSG_PASS (no match for stop filter), expect it in CATCHALL +check_pass_in_catchall() +{ + logger -t test -p user.notice "${MSG_PASS}" + sleep 1 + grep "${MSG_PASS}" "${CATCHALL}" +} + +# MSG_PASS must NOT appear in STOPLOG +check_pass_not_in_stoplog() +{ + grep "${MSG_PASS}" "${STOPLOG}" && return 1 + return 0 +} + +# Send MSG_PROG from tag "stopper", expect it in PROGLOG (!! stop-block) +check_prog_in_proglog() +{ + logger -t stopper -p user.notice "${MSG_PROG}" + sleep 1 + grep "${MSG_PROG}" "${PROGLOG}" +} + +# MSG_PROG from "stopper" must NOT appear in CATCHALL +check_prog_not_in_catchall() +{ + grep "${MSG_PROG}" "${CATCHALL}" && return 1 + return 0 +} + +# Send MSG_OTHER from a different tag, expect it in CATCHALL only +check_other_in_catchall() +{ + logger -t otherprog -p user.notice "${MSG_OTHER}" + sleep 1 + grep "${MSG_OTHER}" "${CATCHALL}" +} + +# MSG_OTHER must NOT appear in PROGLOG +check_other_not_in_proglog() +{ + grep "${MSG_OTHER}" "${PROGLOG}" && return 1 + return 0 +} + +run_step "Set up syslogd with stop-processing rules" setup_syslogd + +run_step "Matched msg (propfilter ::) lands in stop log" check_stop_in_stoplog +run_step "Matched msg (propfilter ::) absent from catch-all" check_stop_not_in_catchall +run_step "Non-matched msg reaches catch-all" check_pass_in_catchall +run_step "Non-matched msg absent from stop log" check_pass_not_in_stoplog + +run_step "Matched msg (program !!) lands in prog log" check_prog_in_proglog +run_step "Matched msg (program !!) absent from catch-all" check_prog_not_in_catchall +run_step "Other-program msg reaches catch-all" check_other_in_catchall +run_step "Other-program msg absent from prog log" check_other_not_in_proglog diff --git a/test/tcp-fwd.sh b/test/tcp-fwd.sh new file mode 100755 index 0000000..0a91e61 --- /dev/null +++ b/test/tcp-fwd.sh @@ -0,0 +1,97 @@ +#!/bin/sh +# Verify TCP forwarding between two syslogd instances +# +# Tests both @@ and tcp:// syntax for TCP forwarding, plus direct +# logger-to-syslogd TCP delivery and the logger -V verbose mode. +# +. "${srcdir:-.}/lib.sh" + +MSG="tcp fwd test message" +MSG2="tcp fwd url test message" +MSG3="direct logger tcp message" +MSG4="logger verbose tcp message" + +setup_receiver() +{ + cat <<-EOF >"${CONFD2}/50-default.conf" + kern.* /dev/null + *.*;kern.none ${LOG2} ;RFC5424 + EOF + setup2 -m0 -a "[::1]:*" -b ":${PORT2}" +} + +setup_receiver_tcp() +{ + cat <<-EOF >"${CONFD2}/50-default.conf" + kern.* /dev/null + *.*;kern.none ${LOG2} ;RFC5424 + listen tcp://[::1]:${PORT2} + EOF + setup2 -m0 -a "[::1]:*" +} + +setup_sender() +{ + cat <<-EOF >"${CONFD}/fwd.conf" + kern.* /dev/null + ntp.* @@[::1]:${PORT2} ;RFC5424 + EOF + setup -m0 +} + +setup_sender_url() +{ + cat <<-EOF >"${CONFD}/fwd.conf" + kern.* /dev/null + ntp.* tcp://[::1]:${PORT2} ;RFC5424 + EOF + reload + sleep 1 +} + +verify_msg() +{ + logger -t fwd -p ntp.notice -m "NTP123" "${MSG}" + sleep 3 + + grep "fwd - NTP123 - ${MSG}" "${LOG2}" +} + +verify_msg_url() +{ + logger -t fwd -p ntp.notice -m "NTP123" "${MSG2}" + sleep 3 + + grep "fwd - NTP123 - ${MSG2}" "${LOG2}" +} + +# Send directly from logger to the receiver's TCP listener, bypassing +# the sender syslogd entirely. +verify_direct_tcp() +{ + [ -x ../src/logger ] || SKIP 'logger missing' + + ../src/logger -h "tcp://[::1]:${PORT2}" -t fwd -p ntp.notice "${MSG3}" + sleep 2 + + grep "${MSG3}" "${LOG2}" +} + +# Use logger -V (verbose) to confirm that a TCP connection was established. +# -V prints "connected to port (tcp)" on stderr on success. +verify_verbose_tcp() +{ + [ -x ../src/logger ] || SKIP 'logger missing' + + output=$(../src/logger -V -h "tcp://[::1]:${PORT2}" -t fwd -p ntp.notice "${MSG4}" 2>&1) + echo "${output}" + echo "${output}" | grep -i "connected" +} + +run_step "Set up receiver syslogd with TCP listener" setup_receiver_tcp +run_step "Set up sender syslogd with @@ TCP forwarding" setup_sender +run_step "Verify TCP forward of message using @@ syntax" verify_msg +run_step "Reconfigure sender to use tcp:// URL syntax" setup_sender_url +run_step "Verify TCP forward of message using tcp:// syntax" verify_msg_url +run_step "Verify direct logger TCP send to syslogd listener" verify_direct_tcp +run_step "Verify logger -V reports TCP connection established" verify_verbose_tcp diff --git a/test/tcp-queue.sh b/test/tcp-queue.sh new file mode 100755 index 0000000..94f4272 --- /dev/null +++ b/test/tcp-queue.sh @@ -0,0 +1,95 @@ +#!/bin/sh +# Verify per-destination TCP send queue: messages queued during outage +# are flushed automatically on reconnect. +# +# The sender syslogd is configured with a short tcp_suspend_time so the +# test completes in a few seconds rather than 180. +# +. "${srcdir:-.}/lib.sh" + +MSG_BASE="tcp-queue-baseline-$$" +MSG_Q1="tcp-queue-outage-1-$$" +MSG_Q2="tcp-queue-outage-2-$$" +MSG_Q3="tcp-queue-outage-3-$$" +MSG_TRIGGER="tcp-queue-trigger-$$" +SUSPEND=4 # must be >= tcp_suspend_time in config below + +setup_receiver_tcp() +{ + cat <<-EOF >"${CONFD2}/50-default.conf" + kern.* /dev/null + *.*;kern.none ${LOG2} ;RFC5424 + listen tcp://[::1]:${PORT2} + EOF + setup2 -m0 -a "[::1]:*" +} + +setup_sender() +{ + cat <<-EOF >"${CONFD}/fwd.conf" + kern.* /dev/null + ntp.* @@[::1]:${PORT2} ;RFC5424 + tcp_suspend_time 3 + EOF + setup -m0 +} + +verify_baseline() +{ + logger -t tcp-queue -p ntp.notice "${MSG_BASE}" + sleep 2 + grep "${MSG_BASE}" "${LOG2}" +} + +kill_receiver() +{ + kill "$(cat "${PID2}")" + # Let the FIN/RST propagate so the sender's socket error state is set + sleep 1 + # Send a probe message; its send() may appear to succeed (CLOSE_WAIT) + # or fail immediately (RST already received). Either way, by the time + # we inject the real queue messages, the next send() will return an + # error and syslogd will enter SUSP and start queueing. + logger -t tcp-queue -p ntp.notice "tcp-queue-probe-$$" || true + sleep 1 +} + +inject_during_outage() +{ + logger -t tcp-queue -p ntp.notice "${MSG_Q1}" + logger -t tcp-queue -p ntp.notice "${MSG_Q2}" + logger -t tcp-queue -p ntp.notice "${MSG_Q3}" + sleep 1 +} + +restart_receiver() +{ + rm -f "${PID2}" + setup_receiver_tcp +} + +trigger_reconnect() +{ + # Wait for the suspension window to expire, then send a message + # which causes the sender to attempt reconnect and flush the queue. + sleep $((SUSPEND + 1)) + logger -t tcp-queue -p ntp.notice "${MSG_TRIGGER}" + sleep 2 +} + +verify_all_arrived() +{ + grep "${MSG_Q1}" "${LOG2}" + grep "${MSG_Q2}" "${LOG2}" + grep "${MSG_Q3}" "${LOG2}" + grep "${MSG_TRIGGER}" "${LOG2}" +} + +run_step "Set up TCP receiver" setup_receiver_tcp +run_step "Set up TCP sender" setup_sender +run_step "Verify baseline delivery" verify_baseline +run_step "Kill receiver (simulate outage)" kill_receiver +run_step "Send 3 messages during outage" inject_during_outage +run_step "Restart receiver" restart_receiver +run_step "Trigger reconnect and flush" trigger_reconnect +run_step "Verify all queued msgs arrived" verify_all_arrived diff --git a/test/tls-fwd.sh b/test/tls-fwd.sh new file mode 100755 index 0000000..3e8e133 --- /dev/null +++ b/test/tls-fwd.sh @@ -0,0 +1,79 @@ +#!/bin/sh +# Verify TLS forwarding between two syslogd instances (RFC 5425) +# +# Tests both @@@ and tls:// syntax for TLS forwarding. +# Requires OpenSSL CLI for certificate generation. +# +. "${srcdir:-.}/lib.sh" + +MSG="tls fwd test message" +MSG2="tls fwd url test message" + +check_openssl() +{ + command -v openssl >/dev/null || SKIP "OpenSSL CLI not available" +} + +setup_certs() +{ + # Generate self-signed certificate for testing + openssl genrsa -out "${DIR}/server.key" 2048 2>/dev/null + openssl req -new -x509 -key "${DIR}/server.key" \ + -out "${DIR}/server.cert" -days 1 -subj "/CN=localhost" 2>/dev/null + chmod 600 "${DIR}/server.key" +} + +setup_receiver_tls() +{ + cat <<-EOF >"${CONFD2}/50-default.conf" + tls_keyfile ${DIR}/server.key + tls_certfile ${DIR}/server.cert + kern.* /dev/null + *.*;kern.none ${LOG2} ;RFC5424 + listen tls://[::1]:${PORT2} + EOF + setup2 -m0 -a "[::1]:*" +} + +setup_sender_tls() +{ + cat <<-EOF >"${CONFD}/fwd.conf" + kern.* /dev/null + ntp.* @@@[::1]:${PORT2} ;RFC5424,verify=off + EOF + setup -m0 +} + +setup_sender_tls_url() +{ + cat <<-EOF >"${CONFD}/fwd.conf" + kern.* /dev/null + ntp.* tls://[::1]:${PORT2} ;RFC5424,verify=off + EOF + reload + sleep 1 +} + +verify_msg() +{ + logger -t fwd -p ntp.notice -m "NTP123" "${MSG}" + sleep 3 + + grep "fwd - NTP123 - ${MSG}" "${LOG2}" +} + +verify_msg_url() +{ + logger -t fwd -p ntp.notice -m "NTP123" "${MSG2}" + sleep 3 + + grep "fwd - NTP123 - ${MSG2}" "${LOG2}" +} + +run_step "Check OpenSSL availability" check_openssl +run_step "Generate test certificates" setup_certs +run_step "Set up receiver syslogd with TLS listener" setup_receiver_tls +run_step "Set up sender syslogd with @@@ TLS forwarding" setup_sender_tls +run_step "Verify TLS forward of message using @@@ syntax" verify_msg +run_step "Reconfigure sender to use tls:// URL syntax" setup_sender_tls_url +run_step "Verify TLS forward of message using tls:// syntax" verify_msg_url