]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
journald: implement socket forwarding
authorSam Leonard <sam.leonard@codethink.co.uk>
Thu, 21 Dec 2023 15:32:15 +0000 (15:32 +0000)
committerSam Leonard <sam.leonard@codethink.co.uk>
Thu, 15 Feb 2024 14:08:20 +0000 (14:08 +0000)
This commit adds a new way of forwarding journal messages - forwarding
over a socket.

The socket can be any of AF_INET, AF_INET6, AF_UNIUX or AF_VSOCK.

The address to connect to is retrieved from the "journald.forward_address" credential.

It can also be specified in systemd-journald's unit file with ForwardAddress=

13 files changed:
man/journald.conf.xml
man/systemd.system-credentials.xml
src/journal/journald-gperf.gperf
src/journal/journald-server.c
src/journal/journald-server.h
src/journal/journald-socket.c [new file with mode: 0644]
src/journal/journald-socket.h [new file with mode: 0644]
src/journal/journald.conf
src/journal/meson.build
src/journal/test-journald-config.c
test/fuzz/fuzz-unit-file/directives-all.service
test/units/testsuite-26.sh
units/systemd-journald.service.in

index 5ecf004587975be59e3822a4b29b9629d1cd9aa3..4363702b10ca34b643ff8ad0aa67f25f975a6ed7 100644 (file)
         large individual journal files may grow at most. This influences the granularity in which disk space
         is made available through rotation, i.e. deletion of historic data. Defaults to one eighth of the
         values configured with <varname>SystemMaxUse=</varname> and <varname>RuntimeMaxUse=</varname> capped
-        to 128M, so that usually seven rotated journal files are kept as history. If the journal compact 
+        to 128M, so that usually seven rotated journal files are kept as history. If the journal compact
         mode is enabled (enabled by default), the maximum file size is capped to 4G.</para>
 
         <para>Specify values in bytes or use K, M, G, T, P, E as units for the specified sizes (equal to
         <term><varname>ForwardToKMsg=</varname></term>
         <term><varname>ForwardToConsole=</varname></term>
         <term><varname>ForwardToWall=</varname></term>
+        <term><varname>ForwardToSocket=</varname></term>
 
         <listitem><para>Control whether log messages received by the journal daemon shall be forwarded to a
-        traditional syslog daemon, to the kernel log buffer (kmsg), to the system console, or sent as wall
-        messages to all logged-in users.  These options take boolean arguments. If forwarding to syslog is
-        enabled but nothing reads messages from the socket, forwarding to syslog has no effect. By default,
+        traditional syslog daemon, to the kernel log buffer (kmsg), to the system console, sent as wall
+        messages to all logged-in users or sent over a socket. These options take boolean arguments except
+        for <literal>ForwardToSocket=</literal> which takes an an address instead. If forwarding
+        to syslog is enabled but nothing reads messages from the socket, forwarding to syslog has no effect. By default,
         only forwarding to wall is enabled. These settings may be overridden at boot time with the kernel
         command line options <literal>systemd.journald.forward_to_syslog</literal>,
         <literal>systemd.journald.forward_to_kmsg</literal>,
         <literal>=</literal> and the following argument, true is assumed. Otherwise, the argument is parsed
         as a boolean.</para>
 
+        <para>The socket forwarding address can be specified with the credential
+        <literal>journal.forward_to_socket</literal>. The following socket types are supported:</para>
+
+        <para><simplelist type="inline">
+          <member><constant>AF_INET</constant> (e.g. <literal>192.168.0.11:4444</literal>)</member>
+          <member><constant>AF_INET6</constant> (e.g. <literal>[2001:db8::ff00:42:8329]:4444</literal>)</member>
+          <member><constant>AF_UNIX</constant> (e.g. <literal>/run/host/journal/socket</literal>)</member>
+          <member><constant>AF_VSOCK</constant> (e.g. <literal>vsock:2:1234</literal>)</member>
+        </simplelist></para>
+
         <para>When forwarding to the console, the TTY to log to can be changed with
         <varname>TTYPath=</varname>, described below.</para>
 
         <command>systemd</command> will automatically disable kernel's rate-limiting applied to userspace
         processes (equivalent to setting <literal>printk.devkmsg=on</literal>).</para>
 
+        <para>When forwarding over a socket the <ulink url="https://systemd.io/JOURNAL_EXPORT_FORMATS/#journal-export-format">
+        Journal Export Format</ulink> is used when sending over the wire.  Notably this includes the metadata
+        field <varname>__REALTIME_TIMESTAMP</varname> so that
+        <command>systemd-journal-remote</command> (see
+        <citerefentry><refentrytitle>systemd-journal-remote.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>)
+        can be used to receive the forwarded journal entries.</para>
+
         <para>Note: Forwarding is performed synchronously within journald, and may significantly affect its
         performance.  This is particularly relevant when using ForwardToConsole=yes in cloud environments,
-        where the console is often a slow, virtual serial port.  Since journald is implemented as a
-        conventional single-process daemon, forwarding to a completely hung console will block journald.
-        This can have a cascading effect resulting in any services synchronously logging to the blocked
-        journal also becoming blocked.  Unless actively debugging/developing something, it's generally
-        preferable to setup a <command>journalctl --follow</command> style service redirected to the
+        where the console is often a slow, virtual serial port.
+        Since journald is implemented as a conventional single-process daemon, forwarding to a completely
+        hung console will block journald. This can have a cascading effect resulting in any services synchronously
+        logging to the blocked journal also becoming blocked.  Unless actively debugging/developing something, it's
+        generally preferable to setup a <command>journalctl --follow</command> style service redirected to the
         console, instead of ForwardToConsole=yes, for production use.</para>
         </listitem>
+
+        <para>Note: Using <varname>ForwardToSocket=</varname> over IPv4/IPv6 links can be very slow due to the synchronous nature of the sockets.
+        Take care to ensure your link is a low-latency local link if possible. Typically IP networking is not available everywhere
+        journald runs, e.g. in the initrd during boot. Consider using <constant>AF_VSOCK</constant>/<constant>AF_UNIX</constant> sockets for this if possible.
+        </para>
       </varlistentry>
 
       <varlistentry>
         <term><varname>MaxLevelKMsg=</varname></term>
         <term><varname>MaxLevelConsole=</varname></term>
         <term><varname>MaxLevelWall=</varname></term>
+        <term><varname>MaxLevelSocket=</varname></term>
 
         <listitem><para>Controls the maximum log level of messages
         that are stored in the journal, forwarded to syslog, kmsg, the
-        console or wall (if that is enabled, see above). As argument,
-        takes one of
+        console, a socket, or wall (if that is enabled, see above).
+        As argument, takes one of
         <literal>emerg</literal>,
         <literal>alert</literal>,
         <literal>crit</literal>,
         or integer values in the range of 0–7 (corresponding to the
         same levels). Messages equal or below the log level specified
         are stored/forwarded, messages above are dropped. Defaults to
-        <literal>debug</literal> for <varname>MaxLevelStore=</varname>
-        and <varname>MaxLevelSyslog=</varname>, to ensure that the all
-        messages are stored in the journal and forwarded to syslog.
+        <literal>debug</literal> for <varname>MaxLevelStore=</varname>,
+        <varname>MaxLevelSyslog=</varname> and
+        <varname>MaxLevelSocket=</varname>, to ensure that the all
+        messages are stored in the journal, forwarded to syslog and
+        the socket if one exists.
         Defaults to
         <literal>notice</literal> for <varname>MaxLevelKMsg=</varname>,
         <literal>info</literal> for <varname>MaxLevelConsole=</varname>,
         <literal>systemd.journald.max_level_syslog=</literal>,
         <literal>systemd.journald.max_level_kmsg=</literal>,
         <literal>systemd.journald.max_level_console=</literal>,
-        <literal>systemd.journald.max_level_wall=</literal>.</para>
+        <literal>systemd.journald.max_level_wall=</literal>,
+        <literal>systemd.journald.max_level_socket=</literal>.</para>
 
         <xi:include href="version-info.xml" xpointer="v185"/>
         </listitem>
index adc0052456e2511b1378b4d775065e50b7670126..749d3bfb1574b050775ef72d4519c4c6a295f6e0 100644 (file)
         <xi:include href="version-info.xml" xpointer="v254"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>journal.forward_to_socket</varname></term>
+        <listitem>
+          <para>Used by
+          <citerefentry><refentrytitle>systemd-journald</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+          to determine where to forward log messages for socket forwarding, see
+          <citerefentry><refentrytitle>journald.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry> for details.</para>
+
+          <xi:include href="version-info.xml" xpointer="v256"/>
+        </listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>vmm.notify_socket</varname></term>
         <listitem>
index 90765976288350413a08d6508d643d3452c70bfe..49987f5fae3aed89b46debf3a07739deed1bc0af 100644 (file)
@@ -19,35 +19,37 @@ struct ConfigPerfItem;
 %struct-type
 %includes
 %%
-Journal.Storage,            config_parse_storage,    0, offsetof(Server, storage)
-Journal.Compress,           config_parse_compress,   0, offsetof(Server, compress)
-Journal.Seal,               config_parse_bool,       0, offsetof(Server, seal)
-Journal.ReadKMsg,           config_parse_bool,       0, offsetof(Server, read_kmsg)
-Journal.Audit,              config_parse_tristate,   0, offsetof(Server, set_audit)
-Journal.SyncIntervalSec,    config_parse_sec,        0, offsetof(Server, sync_interval_usec)
+Journal.Storage,            config_parse_storage,           0, offsetof(Server, storage)
+Journal.Compress,           config_parse_compress,          0, offsetof(Server, compress)
+Journal.Seal,               config_parse_bool,              0, offsetof(Server, seal)
+Journal.ReadKMsg,           config_parse_bool,              0, offsetof(Server, read_kmsg)
+Journal.Audit,              config_parse_tristate,          0, offsetof(Server, set_audit)
+Journal.SyncIntervalSec,    config_parse_sec,               0, offsetof(Server, sync_interval_usec)
 # The following is a legacy name for compatibility
-Journal.RateLimitInterval,  config_parse_sec,        0, offsetof(Server, ratelimit_interval)
-Journal.RateLimitIntervalSec,config_parse_sec,       0, offsetof(Server, ratelimit_interval)
-Journal.RateLimitBurst,     config_parse_unsigned,   0, offsetof(Server, ratelimit_burst)
-Journal.SystemMaxUse,       config_parse_iec_uint64, 0, offsetof(Server, system_storage.metrics.max_use)
-Journal.SystemMaxFileSize,  config_parse_iec_uint64, 0, offsetof(Server, system_storage.metrics.max_size)
-Journal.SystemKeepFree,     config_parse_iec_uint64, 0, offsetof(Server, system_storage.metrics.keep_free)
-Journal.SystemMaxFiles,     config_parse_uint64,     0, offsetof(Server, system_storage.metrics.n_max_files)
-Journal.RuntimeMaxUse,      config_parse_iec_uint64, 0, offsetof(Server, runtime_storage.metrics.max_use)
-Journal.RuntimeMaxFileSize, config_parse_iec_uint64, 0, offsetof(Server, runtime_storage.metrics.max_size)
-Journal.RuntimeKeepFree,    config_parse_iec_uint64, 0, offsetof(Server, runtime_storage.metrics.keep_free)
-Journal.RuntimeMaxFiles,    config_parse_uint64,     0, offsetof(Server, runtime_storage.metrics.n_max_files)
-Journal.MaxRetentionSec,    config_parse_sec,        0, offsetof(Server, max_retention_usec)
-Journal.MaxFileSec,         config_parse_sec,        0, offsetof(Server, max_file_usec)
-Journal.ForwardToSyslog,    config_parse_bool,       0, offsetof(Server, forward_to_syslog)
-Journal.ForwardToKMsg,      config_parse_bool,       0, offsetof(Server, forward_to_kmsg)
-Journal.ForwardToConsole,   config_parse_bool,       0, offsetof(Server, forward_to_console)
-Journal.ForwardToWall,      config_parse_bool,       0, offsetof(Server, forward_to_wall)
-Journal.TTYPath,            config_parse_path,       0, offsetof(Server, tty_path)
-Journal.MaxLevelStore,      config_parse_log_level,  0, offsetof(Server, max_level_store)
-Journal.MaxLevelSyslog,     config_parse_log_level,  0, offsetof(Server, max_level_syslog)
-Journal.MaxLevelKMsg,       config_parse_log_level,  0, offsetof(Server, max_level_kmsg)
-Journal.MaxLevelConsole,    config_parse_log_level,  0, offsetof(Server, max_level_console)
-Journal.MaxLevelWall,       config_parse_log_level,  0, offsetof(Server, max_level_wall)
-Journal.SplitMode,          config_parse_split_mode, 0, offsetof(Server, split_mode)
-Journal.LineMax,            config_parse_line_max,   0, offsetof(Server, line_max)
+Journal.RateLimitInterval,  config_parse_sec,               0, offsetof(Server, ratelimit_interval)
+Journal.RateLimitIntervalSec,config_parse_sec,              0, offsetof(Server, ratelimit_interval)
+Journal.RateLimitBurst,     config_parse_unsigned,          0, offsetof(Server, ratelimit_burst)
+Journal.SystemMaxUse,       config_parse_iec_uint64,        0, offsetof(Server, system_storage.metrics.max_use)
+Journal.SystemMaxFileSize,  config_parse_iec_uint64,        0, offsetof(Server, system_storage.metrics.max_size)
+Journal.SystemKeepFree,     config_parse_iec_uint64,        0, offsetof(Server, system_storage.metrics.keep_free)
+Journal.SystemMaxFiles,     config_parse_uint64,            0, offsetof(Server, system_storage.metrics.n_max_files)
+Journal.RuntimeMaxUse,      config_parse_iec_uint64,        0, offsetof(Server, runtime_storage.metrics.max_use)
+Journal.RuntimeMaxFileSize, config_parse_iec_uint64,        0, offsetof(Server, runtime_storage.metrics.max_size)
+Journal.RuntimeKeepFree,    config_parse_iec_uint64,        0, offsetof(Server, runtime_storage.metrics.keep_free)
+Journal.RuntimeMaxFiles,    config_parse_uint64,            0, offsetof(Server, runtime_storage.metrics.n_max_files)
+Journal.MaxRetentionSec,    config_parse_sec,               0, offsetof(Server, max_retention_usec)
+Journal.MaxFileSec,         config_parse_sec,               0, offsetof(Server, max_file_usec)
+Journal.ForwardToSyslog,    config_parse_bool,              0, offsetof(Server, forward_to_syslog)
+Journal.ForwardToKMsg,      config_parse_bool,              0, offsetof(Server, forward_to_kmsg)
+Journal.ForwardToConsole,   config_parse_bool,              0, offsetof(Server, forward_to_console)
+Journal.ForwardToWall,      config_parse_bool,              0, offsetof(Server, forward_to_wall)
+Journal.ForwardToSocket,    config_parse_forward_to_socket, 0, offsetof(Server, forward_to_socket)
+Journal.TTYPath,            config_parse_path,              0, offsetof(Server, tty_path)
+Journal.MaxLevelStore,      config_parse_log_level,         0, offsetof(Server, max_level_store)
+Journal.MaxLevelSyslog,     config_parse_log_level,         0, offsetof(Server, max_level_syslog)
+Journal.MaxLevelKMsg,       config_parse_log_level,         0, offsetof(Server, max_level_kmsg)
+Journal.MaxLevelConsole,    config_parse_log_level,         0, offsetof(Server, max_level_console)
+Journal.MaxLevelWall,       config_parse_log_level,         0, offsetof(Server, max_level_wall)
+Journal.MaxLevelSocket,     config_parse_log_level,         0, offsetof(Server, max_level_socket)
+Journal.SplitMode,          config_parse_split_mode,        0, offsetof(Server, split_mode)
+Journal.LineMax,            config_parse_line_max,          0, offsetof(Server, line_max)
index e7f15e2ee7c027f3bfc732c0516ee459209e4fed..8d9d0957523f3a4cf3427afe95ac4dd35cd486f9 100644 (file)
@@ -18,6 +18,7 @@
 #include "audit-util.h"
 #include "cgroup-util.h"
 #include "conf-parser.h"
+#include "creds-util.h"
 #include "dirent-util.h"
 #include "extract-word.h"
 #include "fd-util.h"
@@ -39,6 +40,7 @@
 #include "journald-native.h"
 #include "journald-rate-limit.h"
 #include "journald-server.h"
+#include "journald-socket.h"
 #include "journald-stream.h"
 #include "journald-syslog.h"
 #include "log.h"
@@ -51,6 +53,7 @@
 #include "rm-rf.h"
 #include "selinux-util.h"
 #include "signal-util.h"
+#include "socket-netlink.h"
 #include "socket-util.h"
 #include "stdio-util.h"
 #include "string-table.h"
@@ -1151,6 +1154,8 @@ static void server_dispatch_message_real(
         else
                 journal_uid = 0;
 
+        server_forward_socket(s, iovec, n, priority);
+
         server_write_to_journal(s, journal_uid, iovec, n, priority);
 }
 
@@ -1838,6 +1843,17 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat
                 else
                         s->max_level_wall = r;
 
+        } else if (proc_cmdline_key_streq(key, "systemd.journald.max_level_socket")) {
+
+                if (proc_cmdline_value_missing(key, value))
+                        return 0;
+
+                r = log_level_from_string(value);
+                if (r < 0)
+                        log_warning("Failed to parse max level socket value \"%s\". Ignoring.", value);
+                else
+                        s->max_level_socket = r;
+
         } else if (startswith(key, "systemd.journald"))
                 log_warning("Unknown journald kernel command line option \"%s\". Ignoring.", key);
 
@@ -2443,6 +2459,29 @@ static int server_setup_memory_pressure(Server *s) {
         return 0;
 }
 
+static void server_load_credentials(Server *s) {
+        _cleanup_free_ void *data = NULL;
+        int r;
+
+        assert(s);
+
+        /* if we already have a forward address from config don't load the credential */
+        if (s->forward_to_socket.sockaddr.sa.sa_family != AF_UNSPEC) {
+                log_debug("Socket forward address already set not loading journal.forward_to_socket");
+                return;
+        }
+
+        r = read_credential("journal.forward_to_socket", &data, NULL);
+        if (r < 0) {
+                log_debug_errno(r, "Failed to read credential journal.forward_to_socket, ignoring: %m");
+                return;
+        }
+
+        r = socket_address_parse(&s->forward_to_socket, data);
+        if (r < 0)
+                log_debug_errno(r, "Failed to parse credential journal.forward_to_socket, ignoring: %m");
+}
+
 int server_new(Server **ret) {
         _cleanup_(server_freep) Server *s = NULL;
 
@@ -2460,6 +2499,7 @@ int server_new(Server **ret) {
                 .audit_fd = -EBADF,
                 .hostname_fd = -EBADF,
                 .notify_fd = -EBADF,
+                .forward_socket_fd = -EBADF,
 
                 .compress.enabled = true,
                 .compress.threshold_bytes = UINT64_MAX,
@@ -2476,6 +2516,7 @@ int server_new(Server **ret) {
                 .ratelimit_burst = DEFAULT_RATE_LIMIT_BURST,
 
                 .forward_to_wall = true,
+                .forward_to_socket = { .sockaddr.sa.sa_family = AF_UNSPEC },
 
                 .max_file_usec = DEFAULT_MAX_FILE_USEC,
 
@@ -2484,6 +2525,7 @@ int server_new(Server **ret) {
                 .max_level_kmsg = LOG_NOTICE,
                 .max_level_console = LOG_INFO,
                 .max_level_wall = LOG_EMERG,
+                .max_level_socket = LOG_DEBUG,
 
                 .line_max = DEFAULT_LINE_MAX,
 
@@ -2524,6 +2566,8 @@ int server_init(Server *s, const char *namespace) {
 
         server_parse_config_file(s);
 
+        server_load_credentials(s);
+
         if (!s->namespace) {
                 /* Parse kernel command line, but only if we are not a namespace instance */
                 r = proc_cmdline_parse(parse_proc_cmdline_item, s, PROC_CMDLINE_STRIP_RD_PREFIX);
@@ -2796,6 +2840,7 @@ Server* server_free(Server *s) {
         safe_close(s->audit_fd);
         safe_close(s->hostname_fd);
         safe_close(s->notify_fd);
+        safe_close(s->forward_socket_fd);
 
         if (s->ratelimit)
                 journal_ratelimit_free(s->ratelimit);
@@ -2931,3 +2976,34 @@ int config_parse_compress(
 
         return 0;
 }
+
+int config_parse_forward_to_socket(
+                const char* unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        SocketAddress* addr = ASSERT_PTR(data);
+        int r;
+
+        assert(unit);
+        assert(filename);
+        assert(rvalue);
+
+        if (isempty(rvalue))
+                *addr = (SocketAddress) { .sockaddr.sa.sa_family = AF_UNSPEC };
+        else {
+                r = socket_address_parse(addr, rvalue);
+                if (r < 0)
+                        log_syntax(unit, LOG_WARNING, filename, line, r,
+                                   "Failed to parse ForwardToSocket= value, ignoring: %s", rvalue);
+        }
+
+        return 0;
+}
index aeb85ab0a237762574c787340962edafc51a2b53..b63028e8c2c71f005f7631a4969a0bf1c6362763 100644 (file)
@@ -5,6 +5,7 @@
 #include <sys/types.h>
 
 #include "sd-event.h"
+#include "socket-util.h"
 
 typedef struct Server Server;
 
@@ -78,6 +79,7 @@ struct Server {
         int audit_fd;
         int hostname_fd;
         int notify_fd;
+        int forward_socket_fd;
 
         sd_event *event;
 
@@ -123,6 +125,7 @@ struct Server {
         bool forward_to_syslog;
         bool forward_to_console;
         bool forward_to_wall;
+        SocketAddress forward_to_socket;
 
         unsigned n_forward_syslog_missed;
         usec_t last_warn_forward_syslog_missed;
@@ -142,6 +145,7 @@ struct Server {
         int max_level_kmsg;
         int max_level_console;
         int max_level_wall;
+        int max_level_socket;
 
         Storage storage;
         SplitMode split_mode;
@@ -214,6 +218,7 @@ const struct ConfigPerfItem* journald_gperf_lookup(const char *key, GPERF_LEN_TY
 CONFIG_PARSER_PROTOTYPE(config_parse_storage);
 CONFIG_PARSER_PROTOTYPE(config_parse_line_max);
 CONFIG_PARSER_PROTOTYPE(config_parse_compress);
+CONFIG_PARSER_PROTOTYPE(config_parse_forward_to_socket);
 
 const char *storage_to_string(Storage s) _const_;
 Storage storage_from_string(const char *s) _pure_;
diff --git a/src/journal/journald-socket.c b/src/journal/journald-socket.c
new file mode 100644 (file)
index 0000000..6073cae
--- /dev/null
@@ -0,0 +1,162 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+
+#include "fd-util.h"
+#include "iovec-util.h"
+#include "journald-socket.h"
+#include "log.h"
+#include "macro.h"
+#include "process-util.h"
+#include "socket-util.h"
+#include "sparse-endian.h"
+
+void server_open_forward_socket(Server *s) {
+        _cleanup_close_ int socket_fd = -EBADF;
+        const SocketAddress *addr;
+        int family;
+
+        assert(s);
+
+        /* nop if there is nothing to do */
+        if (s->forward_to_socket.sockaddr.sa.sa_family == AF_UNSPEC || s->namespace || s->forward_socket_fd >= 0)
+                return;
+
+        addr = &s->forward_to_socket;
+
+        family = socket_address_family(addr);
+
+        if (!IN_SET(family, AF_UNIX, AF_INET, AF_INET6, AF_VSOCK)) {
+                log_debug("Unsupported socket type for forward socket: %d", family);
+                return;
+        }
+
+        socket_fd = socket(family, SOCK_STREAM|SOCK_CLOEXEC, 0);
+        if (socket_fd < 0) {
+                log_debug_errno(errno, "Failed to create forward socket, ignoring: %m");
+                return;
+        }
+
+        if (connect(socket_fd, &addr->sockaddr.sa, addr->size) < 0) {
+                log_debug_errno(errno, "Failed to connect to remote address for forwarding, ignoring: %m");
+                return;
+        }
+
+        s->forward_socket_fd = TAKE_FD(socket_fd);
+        log_debug("Successfully connected to remote address for forwarding");
+}
+
+static inline bool must_serialise(struct iovec iov) {
+        /* checks an iovec of the form FIELD=VALUE to see if VALUE needs binary safe serialisation:
+         * See https://systemd.io/JOURNAL_EXPORT_FORMATS/#journal-export-format for more information
+         * on binary safe serialisation for the journal export format */
+
+        assert(iov.iov_len == 0 || iov.iov_base);
+
+        const uint8_t *s = iov.iov_base;
+        bool before_value = true;
+
+        FOREACH_ARRAY(c, s, iov.iov_len) {
+                if (before_value)
+                        before_value = *c != (uint8_t)'=';
+                else if (*c < (uint8_t)' ' && *c != (uint8_t)'\t')
+                        return true;
+        }
+
+        return false;
+}
+
+void server_forward_socket(
+                Server *s,
+                const struct iovec *iovec,
+                size_t n_iovec,
+                int priority) {
+        _cleanup_free_ struct iovec *iov_alloc = NULL;
+        struct iovec *iov = NULL;
+
+        _cleanup_free_ le64_t *len_alloc = NULL;
+        le64_t *len = NULL;
+
+        assert(s);
+        assert(iovec);
+        assert(n_iovec > 0);
+
+        if (LOG_PRI(priority) > s->max_level_socket)
+                return;
+
+        server_open_forward_socket(s);
+
+        /* if we failed to open a socket just return */
+        if (s->forward_socket_fd < 0)
+                return;
+
+        /* we need a newline after each iovec + 4 for each we have to serialise in a binary safe way
+         * +1 for the final __REALTIME_TIMESTAMP metadata field */
+        size_t n = n_iovec * 5 + 1;
+
+        if (n < ALLOCA_MAX / (sizeof(struct iovec) + sizeof(le64_t)) / 2) {
+                iov = newa(struct iovec, n);
+                len = newa(le64_t, n_iovec);
+        } else {
+                iov_alloc = new(struct iovec, n);
+                if (!iov_alloc) {
+                        log_oom();
+                        return;
+                }
+
+                iov = iov_alloc;
+
+                len_alloc = new(le64_t, n_iovec);
+                if (!len_alloc) {
+                        log_oom();
+                        return;
+                }
+
+                len = len_alloc;
+        }
+
+        struct iovec nl = IOVEC_MAKE_STRING("\n");
+        size_t iov_idx = 0, len_idx = 0;
+        FOREACH_ARRAY(i, iovec, n_iovec) {
+                if (must_serialise(*i)) {
+                        const uint8_t *c;
+                        c = memchr(i->iov_base, '=', i->iov_len);
+
+                        /* this should never happen */
+                        if (_unlikely_(!c || c == i->iov_base)) {
+                                log_error("Found invalid journal field, refusing to forward.");
+                                return;
+                        }
+
+                        /* write the field name */
+                        iov[iov_idx++] = IOVEC_MAKE(i->iov_base, c - (uint8_t*) i->iov_base);
+                        iov[iov_idx++] = nl;
+
+                        /* write the length of the value */
+                        len[len_idx] = htole64(i->iov_len - (c - (uint8_t*) i->iov_base) - 1);
+                        iov[iov_idx++] = IOVEC_MAKE(&len[len_idx++], sizeof(le64_t));
+
+                        /* write the raw binary value */
+                        iov[iov_idx++] = IOVEC_MAKE(c + 1, i->iov_len - (c - (uint8_t*) i->iov_base) - 1);
+                } else
+                        /* if it doesn't need special treatment just write the value out */
+                        iov[iov_idx++] = *i;
+
+                iov[iov_idx++] = nl;
+        }
+
+        /* synthesise __REALTIME_TIMESTAMP as the last argument so systemd-journal-upload can receive these export messages */
+        char buf[sizeof("__REALTIME_TIMESTAMP=") + DECIMAL_STR_MAX(usec_t) + 2];
+        xsprintf(buf, "__REALTIME_TIMESTAMP="USEC_FMT"\n\n", now(CLOCK_REALTIME));
+        iov[iov_idx++] = IOVEC_MAKE_STRING(buf);
+
+        if (writev(s->forward_socket_fd, iov, iov_idx) < 0) {
+                log_debug_errno(errno, "Failed to forward log message over socket: %m");
+
+                /* if we failed to send once we will probably fail again so wait for a new connection to
+                 * establish before attempting to forward again */
+                s->forward_socket_fd = safe_close(s->forward_socket_fd);
+        }
+}
diff --git a/src/journal/journald-socket.h b/src/journal/journald-socket.h
new file mode 100644 (file)
index 0000000..cb156d7
--- /dev/null
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "journald-server.h"
+#include "socket-util.h"
+
+void server_forward_socket(Server *s, const struct iovec *iovec, size_t n, int priority);
+void server_open_forward_socket(Server *s);
index 7b9e23205e5bb7f9ece85a98d82594a29719dac9..13cdd6300fb305a5776b9df1ee8d3e45d4765739 100644 (file)
@@ -44,6 +44,7 @@
 #MaxLevelKMsg=notice
 #MaxLevelConsole=info
 #MaxLevelWall=emerg
+#MaxLevelSocket=debug
 #LineMax=48K
 #ReadKMsg=yes
 #Audit=yes
index 36600bf2c6e17c63ba5cf823ab292deb8153c4a6..a3c57106101407f26e7d9ae35264a5651ed22e04 100644 (file)
@@ -12,6 +12,7 @@ sources = files(
         'journald-stream.c',
         'journald-syslog.c',
         'journald-wall.c',
+        'journald-socket.c',
 )
 
 sources += custom_target(
index 1a6c531f4e1bb69c94310ccfc5e2f752f7ccf233..cd13ac197d3a5d7c66280cc4768afa0199f0fc0b 100644 (file)
@@ -1,8 +1,15 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include <netinet/in.h>
 #include <stdbool.h>
+#include <string.h>
+#include <sys/un.h>
 
 #include "journald-server.h"
+#include "log.h"
+#include "path-util.h"
+#include "socket-util.h"
+#include "sparse-endian.h"
 #include "tests.h"
 
 #define _COMPRESS_PARSE_CHECK(str, enab, thresh, varname)               \
@@ -47,4 +54,125 @@ TEST(config_compress) {
         COMPRESS_PARSE_CHECK("", true, UINT64_MAX);
 }
 
+#define _FORWARD_TO_SOCKET_PARSE_CHECK_FAILS(str, addr, varname)             \
+        do {                                                               \
+                SocketAddress varname = {};                                \
+                config_parse_forward_to_socket("", "", 0, "", 0, "", 0, str, \
+                                             &varname, NULL);              \
+                assert_se(socket_address_verify(&varname, true) < 0);      \
+        } while (0)
+
+#define FORWARD_TO_SOCKET_PARSE_CHECK_FAILS(str) \
+        _FORWARD_TO_SOCKET_PARSE_CHECK_FAILS(str, addr, conf##__COUNTER__)
+
+#define _FORWARD_TO_SOCKET_PARSE_CHECK(str, addr, varname)                   \
+        do {                                                               \
+                SocketAddress varname = {};                                \
+                config_parse_forward_to_socket("", "", 0, "", 0, "", 0, str, \
+                                      &varname, NULL);                     \
+                buf = mfree(buf); \
+                buf2 = mfree(buf2); \
+                socket_address_print(&varname, &buf);\
+                socket_address_print(&addr, &buf2);\
+                log_info("\"%s\" parsed as \"%s\", should be \"%s\"", str, buf, buf2); \
+                log_info("socket_address_verify(&addr, false) = %d", socket_address_verify(&addr, false)); \
+                log_info("socket_address_verify(&varname, false) = %d", socket_address_verify(&varname, false)); \
+                log_info("socket_address_family(&addr) = %d", socket_address_family(&addr)); \
+                log_info("socket_address_family(&varname) = %d", socket_address_family(&varname)); \
+                log_info("addr.size = %u", addr.size); \
+                log_info("varname.size = %u", varname.size); \
+                assert_se(socket_address_equal(&varname, &addr));          \
+        } while (0)
+
+#define FORWARD_TO_SOCKET_PARSE_CHECK(str, addr)                     \
+        _FORWARD_TO_SOCKET_PARSE_CHECK(str, addr, conf##__COUNTER__)
+
+TEST(config_forward_to_socket) {
+        SocketAddress addr;
+        _cleanup_free_ char *buf = NULL, *buf2 = NULL;
+
+        /* Valid AF_UNIX */
+        addr = (SocketAddress) {
+                .sockaddr.un = (struct sockaddr_un) {
+                        .sun_family = AF_UNIX,
+                        .sun_path = "/run/host/journal/socket",
+                },
+                .size = offsetof(struct sockaddr_un, sun_path) + strlen("/run/host/journal/socket") + 1,
+        };
+        FORWARD_TO_SOCKET_PARSE_CHECK("/run/host/journal/socket", addr);
+
+        addr.size -= 1;
+        memcpy(addr.sockaddr.un.sun_path, "\0run/host/journal/socket", sizeof("\0run/host/journal/socket"));
+        FORWARD_TO_SOCKET_PARSE_CHECK("@run/host/journal/socket", addr);
+
+        /* Valid AF_INET */
+        addr = (SocketAddress) {
+                .sockaddr.in = (struct sockaddr_in) {
+                        .sin_family = AF_INET,
+                        .sin_addr = { htobe32(0xC0A80001) },
+                        .sin_port = htobe16(1234),
+                },
+                .size = sizeof(struct sockaddr_in),
+        };
+        FORWARD_TO_SOCKET_PARSE_CHECK("192.168.0.1:1234", addr);
+
+        /* Valid AF_INET6 */
+        addr = (SocketAddress) {
+                .sockaddr.in6 = (struct sockaddr_in6) {
+                        .sin6_family = AF_INET6,
+                        .sin6_addr = (struct in6_addr) {
+                                .s6_addr16 = {
+                                        htobe16(0x2001),
+                                        htobe16(0xdb8),
+                                        htobe16(0x4006),
+                                        htobe16(0x812),
+                                        0, 0, 0,
+                                        htobe16(0x200e)
+                                }
+                        },
+                        .sin6_port = htobe16(8080),
+                },
+                .size = sizeof(struct sockaddr_in6),
+        };
+        FORWARD_TO_SOCKET_PARSE_CHECK("[2001:db8:4006:812::200e]:8080", addr);
+
+        /* Valid AF_VSOCK */
+        addr = (SocketAddress) {
+                .sockaddr.vm = (struct sockaddr_vm) {
+                        .svm_family = AF_VSOCK,
+                        .svm_cid = 123456,
+                        .svm_port = 654321,
+                },
+                .size = sizeof(struct sockaddr_vm),
+        };
+        FORWARD_TO_SOCKET_PARSE_CHECK("vsock:123456:654321", addr);
+
+        /* Invalid IPv4 */
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("256.123.45.12:1235");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("252.123.45.12:123500");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("252.123.45.12:0");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("252.123.45.12:-1");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("-1.123.45.12:22");
+
+        /* Invalid IPv6 */
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("[2001:db8:4006:812::200e]:80800");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("[1ffff:db8:4006:812::200e]:8080");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("[-1:db8:4006:812::200e]:8080");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("[2001:db8:4006:812::200e]:-1");
+
+        /* Invalid UNIX */
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("a/b/c");
+
+        /* Invalid VSock */
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("vsock:4294967296:1234");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("vsock:1234:4294967296");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("vsock:abcd:1234");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("vsock:1234:abcd");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("vsock:1234");
+
+        /* Invalid Case */
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("");
+        FORWARD_TO_SOCKET_PARSE_CHECK_FAILS("ahh yes sockets, mmh");
+}
+
 DEFINE_TEST_MAIN(LOG_INFO);
index 18d9c3b30f651553222837b7884be7ca9d7e5ac3..b05b0a49731675aee3385c65075d13c09aec365b 100644 (file)
@@ -865,6 +865,7 @@ MaxLevelKMsg=
 MaxLevelStore=
 MaxLevelSyslog=
 MaxLevelWall=
+MaxLevelSocket=
 MaxRetentionSec=
 MaxUse=
 MemoryDenyWriteExecute=
index 910e7531b1e887b10a901b85a33e0687597822cd..2dd62a4f67412a423e60abcf905088ee0bc542df 100755 (executable)
@@ -343,7 +343,7 @@ done
 
 # Aux verbs & assorted checks
 systemctl is-active "*-journald.service"
-systemctl cat "*journal*"
+systemctl cat "*udevd*"
 systemctl cat "$UNIT_NAME"
 systemctl help "$UNIT_NAME"
 systemctl service-watchdogs
index 37eeabc5103776c5b49f3ac5aea8279af5f463dd..2b340f4ea7128bedc026cd2c3a4de383d796dac3 100644 (file)
@@ -29,6 +29,7 @@ IgnoreOnIsolate=yes
 DeviceAllow=char-* rw
 ExecStart={{LIBEXECDIR}}/systemd-journald
 FileDescriptorStoreMax=4224
+ImportCredential=journal.*
 IPAddressDeny=any
 LockPersonality=yes
 MemoryDenyWriteExecute=yes
@@ -37,7 +38,7 @@ OOMScoreAdjust=-250
 ProtectClock=yes
 Restart=always
 RestartSec=0
-RestrictAddressFamilies=AF_UNIX AF_NETLINK
+RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_VSOCK AF_INET AF_INET6
 RestrictNamespaces=yes
 RestrictRealtime=yes
 RestrictSUIDSGID=yes