summaryrefslogtreecommitdiffstats
path: root/logsrvd/logsrvd_journal.c
diff options
context:
space:
mode:
Diffstat (limited to 'logsrvd/logsrvd_journal.c')
-rw-r--r--logsrvd/logsrvd_journal.c616
1 files changed, 616 insertions, 0 deletions
diff --git a/logsrvd/logsrvd_journal.c b/logsrvd/logsrvd_journal.c
new file mode 100644
index 0000000..354d71c
--- /dev/null
+++ b/logsrvd/logsrvd_journal.c
@@ -0,0 +1,616 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2021-2022 Todd C. Miller <Todd.Miller@sudo.ws>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is an open source non-commercial project. Dear PVS-Studio, please check it.
+ * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
+ */
+
+#include <config.h>
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <arpa/inet.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#ifdef HAVE_STDBOOL_H
+# include <stdbool.h>
+#else
+# include "compat/stdbool.h"
+#endif /* HAVE_STDBOOL_H */
+#if defined(HAVE_STDINT_H)
+# include <stdint.h>
+#elif defined(HAVE_INTTYPES_H)
+# include <inttypes.h>
+#endif
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "sudo_compat.h"
+#include "sudo_conf.h"
+#include "sudo_debug.h"
+#include "sudo_event.h"
+#include "sudo_eventlog.h"
+#include "sudo_fatal.h"
+#include "sudo_gettext.h"
+#include "sudo_iolog.h"
+#include "sudo_util.h"
+
+#include "logsrvd.h"
+
+/*
+ * Helper function to set closure->journal and closure->journal_path.
+ */
+static bool
+journal_fdopen(int fd, const char *journal_path,
+ struct connection_closure *closure)
+{
+ debug_decl(journal_fdopen, SUDO_DEBUG_UTIL);
+
+ free(closure->journal_path);
+ closure->journal_path = strdup(journal_path);
+ if (closure->journal_path == NULL) {
+ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ debug_return_bool(false);
+ }
+
+ /* Defer fdopen() until last--it cannot be undone. */
+ if (closure->journal != NULL)
+ fclose(closure->journal);
+ if ((closure->journal = fdopen(fd, "r+")) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to fdopen journal file %s", journal_path);
+ debug_return_bool(false);
+ }
+
+ debug_return_bool(true);
+}
+
+static int
+journal_mkstemp(const char *parent_dir, char *pathbuf, int pathlen)
+{
+ int len, dfd = -1, fd = -1;
+ mode_t dirmode, oldmask;
+ char *template;
+ debug_decl(journal_mkstemp, SUDO_DEBUG_UTIL);
+
+ /* umask must not be more restrictive than the file modes. */
+ dirmode = logsrvd_conf_iolog_mode() | S_IXUSR;
+ if (dirmode & (S_IRGRP|S_IWGRP))
+ dirmode |= S_IXGRP;
+ if (dirmode & (S_IROTH|S_IWOTH))
+ dirmode |= S_IXOTH;
+ oldmask = umask(ACCESSPERMS & ~dirmode);
+
+ len = snprintf(pathbuf, pathlen, "%s/%s/%s",
+ logsrvd_conf_relay_dir(), parent_dir, RELAY_TEMPLATE);
+ if (len >= pathlen) {
+ errno = ENAMETOOLONG;
+ sudo_warn("%s/%s/%s", logsrvd_conf_relay_dir(), parent_dir,
+ RELAY_TEMPLATE);
+ goto done;
+ }
+ dfd = sudo_open_parent_dir(pathbuf, logsrvd_conf_iolog_uid(),
+ logsrvd_conf_iolog_gid(), S_IRWXU|S_IXGRP|S_IXOTH, false);
+ if (dfd == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to create parent dir for %s", pathbuf);
+ goto done;
+ }
+ template = pathbuf + (len - strlen(RELAY_TEMPLATE));
+ if ((fd = mkostempsat(dfd, template, 0, 0)) == -1) {
+ sudo_warn(U_("%s: %s"), "mkstemp", pathbuf);
+ goto done;
+ }
+
+done:
+ umask(oldmask);
+ if (dfd != -1)
+ close(dfd);
+
+ debug_return_int(fd);
+}
+
+/*
+ * Create a temporary file in the relay dir and store it in the closure.
+ */
+static bool
+journal_create(struct connection_closure *closure)
+{
+ char journal_path[PATH_MAX];
+ int fd;
+ debug_decl(journal_create, SUDO_DEBUG_UTIL);
+
+ fd = journal_mkstemp("incoming", journal_path, sizeof(journal_path));
+ if (fd == -1) {
+ closure->errstr = _("unable to create journal file");
+ debug_return_bool(false);
+ }
+ if (!sudo_lock_file(fd, SUDO_TLOCK)) {
+ sudo_warn(U_("unable to lock %s"), journal_path);
+ unlink(journal_path);
+ close(fd);
+ closure->errstr = _("unable to lock journal file");
+ debug_return_bool(false);
+ }
+ if (!journal_fdopen(fd, journal_path, closure)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to fdopen journal file %s", journal_path);
+ unlink(journal_path);
+ close(fd);
+ closure->errstr = _("unable to open journal file");
+ debug_return_bool(false);
+ }
+
+ debug_return_bool(true);
+}
+
+/*
+ * Flush any buffered data, rewind journal to the beginning and
+ * move to the outgoing directory.
+ * The actual open file is closed in connection_closure_free().
+ */
+static bool
+journal_finish(struct connection_closure *closure)
+{
+ char outgoing_path[PATH_MAX];
+ size_t len;
+ int fd;
+ debug_decl(journal_finish, SUDO_DEBUG_UTIL);
+
+ if (fflush(closure->journal) != 0) {
+ closure->errstr = _("unable to write journal file");
+ debug_return_bool(false);
+ }
+ rewind(closure->journal);
+
+ /* Move journal to the outgoing directory. */
+ fd = journal_mkstemp("outgoing", outgoing_path, sizeof(outgoing_path));
+ if (fd == -1) {
+ closure->errstr = _("unable to rename journal file");
+ debug_return_bool(false);
+ }
+ close(fd);
+ if (rename(closure->journal_path, outgoing_path) == -1) {
+ sudo_warn(U_("unable to rename %s to %s"), closure->journal_path,
+ outgoing_path);
+ closure->errstr = _("unable to rename journal file");
+ unlink(outgoing_path);
+ debug_return_bool(false);
+ }
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "renamed %s -> %s", closure->journal_path, outgoing_path);
+ len = strlen(outgoing_path);
+ if (strlen(closure->journal_path) == len) {
+ /* This should always be true. */
+ memcpy(closure->journal_path, outgoing_path, len);
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
+ "length mismatch %zu != %zu", strlen(closure->journal_path), len);
+ free(closure->journal_path);
+ closure->journal_path = strdup(outgoing_path);
+ if (closure->journal_path == NULL) {
+ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ closure->errstr = _("unable to allocate memory");
+ debug_return_bool(false);
+ }
+ }
+
+ debug_return_bool(true);
+}
+
+/*
+ * Seek ahead in the journal to the specified target time.
+ * Returns true if we reached the target time exactly, else false.
+ */
+static bool
+journal_seek(struct timespec *target, struct connection_closure *closure)
+{
+ ClientMessage *msg = NULL;
+ size_t nread, bufsize = 0;
+ uint8_t *buf = NULL;
+ uint32_t msg_len;
+ bool ret = false;
+ debug_decl(journal_seek, SUDO_DEBUG_UTIL);
+
+ for (;;) {
+ TimeSpec *delay = NULL;
+
+ /* Read message size (uint32_t in network byte order). */
+ nread = fread(&msg_len, sizeof(msg_len), 1, closure->journal);
+ if (nread != 1) {
+ if (feof(closure->journal)) {
+ sudo_warnx(U_("%s: %s"), closure->journal_path,
+ U_("unexpected EOF reading journal file"));
+ closure->errstr = _("unexpected EOF reading journal file");
+ } else {
+ sudo_warn(U_("%s: %s"), closure->journal_path,
+ U_("error reading journal file"));
+ closure->errstr = _("error reading journal file");
+ }
+ break;
+ }
+ msg_len = ntohl(msg_len);
+ if (msg_len > MESSAGE_SIZE_MAX) {
+ sudo_warnx(U_("%s: %s"), closure->journal_path,
+ U_("client message too large"));
+ closure->errstr = _("client message too large");
+ break;
+ }
+
+ /* Read actual message now that we know the size. */
+ if (msg_len != 0) {
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "%s: reading message %u bytes", closure->journal_path, msg_len);
+
+ if (msg_len > bufsize) {
+ bufsize = sudo_pow2_roundup(msg_len);
+ free(buf);
+ if ((buf = malloc(bufsize)) == NULL) {
+ closure->errstr = _("unable to allocate memory");
+ break;
+ }
+ }
+
+ nread = fread(buf, msg_len, 1, closure->journal);
+ if (nread != 1) {
+ if (feof(closure->journal)) {
+ sudo_warnx(U_("%s: %s"), closure->journal_path,
+ U_("unexpected EOF reading journal file"));
+ closure->errstr = _("unexpected EOF reading journal file");
+ } else {
+ sudo_warn(U_("%s: %s"), closure->journal_path,
+ U_("error reading journal file"));
+ closure->errstr = _("error reading journal file");
+ }
+ break;
+ }
+ }
+
+ client_message__free_unpacked(msg, NULL);
+ msg = client_message__unpack(NULL, msg_len, buf);
+ if (msg == NULL) {
+ sudo_warnx(U_("unable to unpack %s size %zu"), "ClientMessage",
+ (size_t)msg_len);
+ closure->errstr = _("invalid journal file, unable to restart");
+ break;
+ }
+
+ switch (msg->type_case) {
+ case CLIENT_MESSAGE__TYPE_HELLO_MSG:
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "seeking past ClientHello (%d)", msg->type_case);
+ break;
+ case CLIENT_MESSAGE__TYPE_ACCEPT_MSG:
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "seeking past AcceptMessage (%d)", msg->type_case);
+ break;
+ case CLIENT_MESSAGE__TYPE_REJECT_MSG:
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "seeking past RejectMessage (%d)", msg->type_case);
+ break;
+ case CLIENT_MESSAGE__TYPE_EXIT_MSG:
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "seeking past ExitMessage (%d)", msg->type_case);
+ break;
+ case CLIENT_MESSAGE__TYPE_RESTART_MSG:
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "seeking past RestartMessage (%d)", msg->type_case);
+ break;
+ case CLIENT_MESSAGE__TYPE_ALERT_MSG:
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "seeking past AlertMessage (%d)", msg->type_case);
+ break;
+ case CLIENT_MESSAGE__TYPE_TTYIN_BUF:
+ delay = msg->u.ttyin_buf->delay;
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "read IoBuffer (%d), delay [%lld, %ld]", msg->type_case,
+ (long long)delay->tv_sec, (long)delay->tv_nsec);
+ break;
+ case CLIENT_MESSAGE__TYPE_TTYOUT_BUF:
+ delay = msg->u.ttyout_buf->delay;
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "read IoBuffer (%d), delay [%lld, %ld]", msg->type_case,
+ (long long)delay->tv_sec, (long)delay->tv_nsec);
+ break;
+ case CLIENT_MESSAGE__TYPE_STDIN_BUF:
+ delay = msg->u.stdin_buf->delay;
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "read IoBuffer (%d), delay [%lld, %ld]", msg->type_case,
+ (long long)delay->tv_sec, (long)delay->tv_nsec);
+ break;
+ case CLIENT_MESSAGE__TYPE_STDOUT_BUF:
+ delay = msg->u.stdout_buf->delay;
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "read stdout_buf (%d), delay [%lld, %ld]", msg->type_case,
+ (long long)delay->tv_sec, (long)delay->tv_nsec);
+ break;
+ case CLIENT_MESSAGE__TYPE_STDERR_BUF:
+ delay = msg->u.stderr_buf->delay;
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "read stderr_buf (%d), delay [%lld, %ld]", msg->type_case,
+ (long long)delay->tv_sec, (long)delay->tv_nsec);
+ break;
+ case CLIENT_MESSAGE__TYPE_WINSIZE_EVENT:
+ delay = msg->u.winsize_event->delay;
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "read ChangeWindowSize (%d), delay [%lld, %ld]", msg->type_case,
+ (long long)delay->tv_sec, (long)delay->tv_nsec);
+ break;
+ case CLIENT_MESSAGE__TYPE_SUSPEND_EVENT:
+ delay = msg->u.suspend_event->delay;
+ sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
+ "read CommandSuspend (%d), delay [%lld, %ld]", msg->type_case,
+ (long long)delay->tv_sec, (long)delay->tv_nsec);
+ break;
+ default:
+ sudo_warnx(U_("unexpected type_case value %d in %s from %s"),
+ msg->type_case, "ClientMessage", closure->journal_path);
+ break;
+ }
+ if (delay != NULL)
+ update_elapsed_time(delay, &closure->elapsed_time);
+
+ if (sudo_timespeccmp(&closure->elapsed_time, target, >=)) {
+ if (sudo_timespeccmp(&closure->elapsed_time, target, ==)) {
+ ret = true;
+ break;
+ }
+
+ /* Mismatch between resume point and stored log. */
+ closure->errstr = _("invalid journal file, unable to restart");
+ sudo_warnx(U_("%s: unable to find resume point [%lld, %ld]"),
+ closure->journal_path, (long long)target->tv_sec,
+ target->tv_nsec);
+ break;
+ }
+ }
+
+ client_message__free_unpacked(msg, NULL);
+ free(buf);
+
+ debug_return_bool(ret);
+}
+
+/*
+ * Restart an existing journal.
+ * Seeks to the resume_point in RestartMessage before continuing.
+ * Returns true if we reached the target time exactly, else false.
+ */
+static bool
+journal_restart(RestartMessage *msg, uint8_t *buf, size_t buflen,
+ struct connection_closure *closure)
+{
+ struct timespec target;
+ int fd, len;
+ char *cp, journal_path[PATH_MAX];
+ debug_decl(journal_restart, SUDO_DEBUG_UTIL);
+
+ /* Strip off leading hostname from log_id. */
+ if ((cp = strchr(msg->log_id, '/')) != NULL) {
+ if (cp != msg->log_id)
+ cp++;
+ } else {
+ cp = msg->log_id;
+ }
+ len = snprintf(journal_path, sizeof(journal_path), "%s/incoming/%s",
+ logsrvd_conf_relay_dir(), cp);
+ if (len >= ssizeof(journal_path)) {
+ errno = ENAMETOOLONG;
+ sudo_warn("%s/incoming/%s", logsrvd_conf_relay_dir(), cp);
+ closure->errstr = _("unable to create journal file");
+ debug_return_bool(false);
+ }
+ if ((fd = open(journal_path, O_RDWR)) == -1) {
+ sudo_warn(U_("unable to open %s"), journal_path);
+ closure->errstr = _("unable to create journal file");
+ debug_return_bool(false);
+ }
+ if (!journal_fdopen(fd, journal_path, closure)) {
+ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ close(fd);
+ closure->errstr = _("unable to allocate memory");
+ debug_return_bool(false);
+ }
+
+ /* Seek forward to resume point. */
+ target.tv_sec = msg->resume_point->tv_sec;
+ target.tv_nsec = msg->resume_point->tv_nsec;
+ if (!journal_seek(&target, closure)) {
+ sudo_warn(U_("unable to seek to [%lld, %ld] in journal file %s"),
+ (long long)target.tv_sec, target.tv_nsec, journal_path);
+ debug_return_bool(false);
+ }
+
+ debug_return_bool(true);
+}
+
+static bool
+journal_write(uint8_t *buf, size_t len, struct connection_closure *closure)
+{
+ uint32_t msg_len;
+ debug_decl(journal_write, SUDO_DEBUG_UTIL);
+
+ /* 32-bit message length in network byte order. */
+ msg_len = htonl((uint32_t)len);
+ if (fwrite(&msg_len, 1, sizeof(msg_len), closure->journal) != sizeof(msg_len)) {
+ closure->errstr = _("unable to write journal file");
+ debug_return_bool(false);
+ }
+ /* message payload */
+ if (fwrite(buf, 1, len, closure->journal) != len) {
+ closure->errstr = _("unable to write journal file");
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+/*
+ * Store an AcceptMessage from the client in the journal.
+ */
+static bool
+journal_accept(AcceptMessage *msg, uint8_t *buf, size_t len,
+ struct connection_closure *closure)
+{
+ debug_decl(journal_accept, SUDO_DEBUG_UTIL);
+
+ if (closure->journal_path != NULL) {
+ /* Re-use existing journal file. */
+ debug_return_bool(journal_write(buf, len, closure));
+ }
+
+ /* Store message in a journal for later relaying. */
+ if (!journal_create(closure))
+ debug_return_bool(false);
+ if (!journal_write(buf, len, closure))
+ debug_return_bool(false);
+
+ if (msg->expect_iobufs) {
+ /* Send log ID to client for restarting connections. */
+ if (!fmt_log_id_message(closure->journal_path, closure))
+ debug_return_bool(false);
+ if (sudo_ev_add(closure->evbase, closure->write_ev,
+ logsrvd_conf_server_timeout(), false) == -1) {
+ sudo_warnx("%s", U_("unable to add event to queue"));
+ debug_return_bool(false);
+ }
+ }
+
+ debug_return_bool(true);
+}
+
+/*
+ * Store a RejectMessage from the client in the journal.
+ */
+static bool
+journal_reject(RejectMessage *msg, uint8_t *buf, size_t len,
+ struct connection_closure *closure)
+{
+ debug_decl(journal_reject, SUDO_DEBUG_UTIL);
+
+ /* Store message in a journal for later relaying. */
+ if (closure->journal_path == NULL) {
+ if (!journal_create(closure))
+ debug_return_bool(false);
+ }
+ if (!journal_write(buf, len, closure))
+ debug_return_bool(false);
+
+ debug_return_bool(true);
+}
+
+/*
+ * Store an ExitMessage from the client in the journal.
+ */
+static bool
+journal_exit(ExitMessage *msg, uint8_t *buf, size_t len,
+ struct connection_closure *closure)
+{
+ debug_decl(journal_exit, SUDO_DEBUG_UTIL);
+
+ /* Store exit message in journal. */
+ if (!journal_write(buf, len, closure))
+ debug_return_bool(false);
+ if (!journal_finish(closure))
+ debug_return_bool(false);
+
+ debug_return_bool(true);
+}
+
+/*
+ * Store an AlertMessage from the client in the journal.
+ */
+static bool
+journal_alert(AlertMessage *msg, uint8_t *buf, size_t len,
+ struct connection_closure *closure)
+{
+ debug_decl(journal_alert, SUDO_DEBUG_UTIL);
+
+ /* Store message in a journal for later relaying. */
+ if (closure->journal_path == NULL) {
+ if (!journal_create(closure))
+ debug_return_bool(false);
+ }
+ if (!journal_write(buf, len, closure))
+ debug_return_bool(false);
+
+ debug_return_bool(true);
+}
+
+/*
+ * Store an IoBuffer from the client in the journal.
+ */
+static bool
+journal_iobuf(int iofd, IoBuffer *iobuf, uint8_t *buf, size_t len,
+ struct connection_closure *closure)
+{
+ debug_decl(journal_iobuf, SUDO_DEBUG_UTIL);
+
+ if (!journal_write(buf, len, closure))
+ debug_return_bool(false);
+ update_elapsed_time(iobuf->delay, &closure->elapsed_time);
+
+ debug_return_bool(true);
+}
+
+/*
+ * Store a CommandSuspend message from the client in the journal.
+ */
+static bool
+journal_suspend(CommandSuspend *msg, uint8_t *buf, size_t len,
+ struct connection_closure *closure)
+{
+ debug_decl(journal_suspend, SUDO_DEBUG_UTIL);
+
+ update_elapsed_time(msg->delay, &closure->elapsed_time);
+
+ debug_return_bool(journal_write(buf, len, closure));
+}
+
+/*
+ * Store a ChangeWindowSize message from the client in the journal.
+ */
+static bool
+journal_winsize(ChangeWindowSize *msg, uint8_t *buf, size_t len,
+ struct connection_closure *closure)
+{
+ debug_decl(journal_winsize, SUDO_DEBUG_UTIL);
+
+ update_elapsed_time(msg->delay, &closure->elapsed_time);
+
+ debug_return_bool(journal_write(buf, len, closure));
+}
+
+struct client_message_switch cms_journal = {
+ journal_accept,
+ journal_reject,
+ journal_exit,
+ journal_restart,
+ journal_alert,
+ journal_iobuf,
+ journal_suspend,
+ journal_winsize
+};