From f31cff849d3db074d0d8157e4117d9b8372c03e8 Mon Sep 17 00:00:00 2001 From: Sam Leonard Date: Thu, 21 Dec 2023 15:32:15 +0000 Subject: [PATCH] journald: implement socket forwarding 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= --- man/journald.conf.xml | 58 +++++-- man/systemd.system-credentials.xml | 12 ++ src/journal/journald-gperf.gperf | 64 +++---- src/journal/journald-server.c | 76 ++++++++ src/journal/journald-server.h | 5 + src/journal/journald-socket.c | 162 ++++++++++++++++++ src/journal/journald-socket.h | 8 + src/journal/journald.conf | 1 + src/journal/meson.build | 1 + src/journal/test-journald-config.c | 128 ++++++++++++++ .../fuzz-unit-file/directives-all.service | 1 + test/units/testsuite-26.sh | 2 +- units/systemd-journald.service.in | 3 +- 13 files changed, 473 insertions(+), 48 deletions(-) create mode 100644 src/journal/journald-socket.c create mode 100644 src/journal/journald-socket.h diff --git a/man/journald.conf.xml b/man/journald.conf.xml index 5ecf0045879..4363702b10c 100644 --- a/man/journald.conf.xml +++ b/man/journald.conf.xml @@ -288,7 +288,7 @@ 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 SystemMaxUse= and RuntimeMaxUse= 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. Specify values in bytes or use K, M, G, T, P, E as units for the specified sizes (equal to @@ -368,11 +368,13 @@ ForwardToKMsg= ForwardToConsole= ForwardToWall= + ForwardToSocket= 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 ForwardToSocket= 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 systemd.journald.forward_to_syslog, systemd.journald.forward_to_kmsg, @@ -381,6 +383,16 @@ = and the following argument, true is assumed. Otherwise, the argument is parsed as a boolean. + The socket forwarding address can be specified with the credential + journal.forward_to_socket. The following socket types are supported: + + + AF_INET (e.g. 192.168.0.11:4444) + AF_INET6 (e.g. [2001:db8::ff00:42:8329]:4444) + AF_UNIX (e.g. /run/host/journal/socket) + AF_VSOCK (e.g. vsock:2:1234) + + When forwarding to the console, the TTY to log to can be changed with TTYPath=, described below. @@ -389,15 +401,27 @@ systemd will automatically disable kernel's rate-limiting applied to userspace processes (equivalent to setting printk.devkmsg=on). + When forwarding over a socket the + Journal Export Format is used when sending over the wire. Notably this includes the metadata + field __REALTIME_TIMESTAMP so that + systemd-journal-remote (see + systemd-journal-remote.service8) + can be used to receive the forwarded journal entries. + 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 journalctl --follow 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 journalctl --follow style service redirected to the console, instead of ForwardToConsole=yes, for production use. + + Note: Using ForwardToSocket= 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 AF_VSOCK/AF_UNIX sockets for this if possible. + @@ -406,11 +430,12 @@ MaxLevelKMsg= MaxLevelConsole= MaxLevelWall= + MaxLevelSocket= 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 emerg, alert, crit, @@ -422,9 +447,11 @@ 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 - debug for MaxLevelStore= - and MaxLevelSyslog=, to ensure that the all - messages are stored in the journal and forwarded to syslog. + debug for MaxLevelStore=, + MaxLevelSyslog= and + MaxLevelSocket=, to ensure that the all + messages are stored in the journal, forwarded to syslog and + the socket if one exists. Defaults to notice for MaxLevelKMsg=, info for MaxLevelConsole=, @@ -435,7 +462,8 @@ systemd.journald.max_level_syslog=, systemd.journald.max_level_kmsg=, systemd.journald.max_level_console=, - systemd.journald.max_level_wall=. + systemd.journald.max_level_wall=, + systemd.journald.max_level_socket=. diff --git a/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml index adc0052456e..749d3bfb157 100644 --- a/man/systemd.system-credentials.xml +++ b/man/systemd.system-credentials.xml @@ -296,6 +296,18 @@ + + journal.forward_to_socket + + Used by + systemd-journald8 + to determine where to forward log messages for socket forwarding, see + journald.conf5 for details. + + + + + vmm.notify_socket diff --git a/src/journal/journald-gperf.gperf b/src/journal/journald-gperf.gperf index 90765976288..49987f5fae3 100644 --- a/src/journal/journald-gperf.gperf +++ b/src/journal/journald-gperf.gperf @@ -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) diff --git a/src/journal/journald-server.c b/src/journal/journald-server.c index e7f15e2ee7c..8d9d0957523 100644 --- a/src/journal/journald-server.c +++ b/src/journal/journald-server.c @@ -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; +} diff --git a/src/journal/journald-server.h b/src/journal/journald-server.h index aeb85ab0a23..b63028e8c2c 100644 --- a/src/journal/journald-server.h +++ b/src/journal/journald-server.h @@ -5,6 +5,7 @@ #include #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 index 00000000000..6073caea021 --- /dev/null +++ b/src/journal/journald-socket.c @@ -0,0 +1,162 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#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 index 00000000000..cb156d73fab --- /dev/null +++ b/src/journal/journald-socket.h @@ -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); diff --git a/src/journal/journald.conf b/src/journal/journald.conf index 7b9e23205e5..13cdd6300fb 100644 --- a/src/journal/journald.conf +++ b/src/journal/journald.conf @@ -44,6 +44,7 @@ #MaxLevelKMsg=notice #MaxLevelConsole=info #MaxLevelWall=emerg +#MaxLevelSocket=debug #LineMax=48K #ReadKMsg=yes #Audit=yes diff --git a/src/journal/meson.build b/src/journal/meson.build index 36600bf2c6e..a3c57106101 100644 --- a/src/journal/meson.build +++ b/src/journal/meson.build @@ -12,6 +12,7 @@ sources = files( 'journald-stream.c', 'journald-syslog.c', 'journald-wall.c', + 'journald-socket.c', ) sources += custom_target( diff --git a/src/journal/test-journald-config.c b/src/journal/test-journald-config.c index 1a6c531f4e1..cd13ac197d3 100644 --- a/src/journal/test-journald-config.c +++ b/src/journal/test-journald-config.c @@ -1,8 +1,15 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include +#include +#include #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); diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service index 18d9c3b30f6..b05b0a49731 100644 --- a/test/fuzz/fuzz-unit-file/directives-all.service +++ b/test/fuzz/fuzz-unit-file/directives-all.service @@ -865,6 +865,7 @@ MaxLevelKMsg= MaxLevelStore= MaxLevelSyslog= MaxLevelWall= +MaxLevelSocket= MaxRetentionSec= MaxUse= MemoryDenyWriteExecute= diff --git a/test/units/testsuite-26.sh b/test/units/testsuite-26.sh index 910e7531b1e..2dd62a4f674 100755 --- a/test/units/testsuite-26.sh +++ b/test/units/testsuite-26.sh @@ -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 diff --git a/units/systemd-journald.service.in b/units/systemd-journald.service.in index 37eeabc5103..2b340f4ea71 100644 --- a/units/systemd-journald.service.in +++ b/units/systemd-journald.service.in @@ -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