summaryrefslogtreecommitdiffstats
path: root/logsrvd
diff options
context:
space:
mode:
Diffstat (limited to 'logsrvd')
-rw-r--r--logsrvd/Makefile.in314
-rw-r--r--logsrvd/iolog_writer.c1088
-rw-r--r--logsrvd/logsrv_util.c159
-rw-r--r--logsrvd/logsrv_util.h43
-rw-r--r--logsrvd/logsrvd.c2042
-rw-r--r--logsrvd/logsrvd.h162
-rw-r--r--logsrvd/logsrvd_conf.c1143
-rw-r--r--logsrvd/sendlog.c1893
-rw-r--r--logsrvd/sendlog.h76
9 files changed, 6920 insertions, 0 deletions
diff --git a/logsrvd/Makefile.in b/logsrvd/Makefile.in
new file mode 100644
index 0000000..2dcde18
--- /dev/null
+++ b/logsrvd/Makefile.in
@@ -0,0 +1,314 @@
+#
+# SPDX-License-Identifier: ISC
+#
+# Copyright (c) 2019-2020 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.
+#
+# @configure_input@
+#
+
+#### Start of system configuration section. ####
+
+srcdir = @srcdir@
+abs_srcdir = @abs_srcdir@
+top_srcdir = @top_srcdir@
+abs_top_srcdir = @abs_top_srcdir@
+top_builddir = @top_builddir@
+abs_top_builddir = @abs_top_builddir@
+devdir = @devdir@
+scriptdir = $(top_srcdir)/scripts
+incdir = $(top_srcdir)/include
+rundir = @rundir@
+cross_compiling = @CROSS_COMPILING@
+
+# Compiler & tools to use
+CC = @CC@
+LIBTOOL = @LIBTOOL@
+SED = @SED@
+
+# Our install program supports extra flags...
+INSTALL = $(SHELL) $(top_srcdir)/install-sh -c
+INSTALL_OWNER = -o $(install_uid) -g $(install_gid)
+INSTALL_BACKUP = @INSTALL_BACKUP@
+
+# Libraries
+LT_LIBS = $(top_builddir)/lib/iolog/libsudo_iolog.la \
+ $(top_builddir)/lib/eventlog/libsudo_eventlog.la \
+ $(top_builddir)/lib/logsrv/liblogsrv.la
+LIBS = $(LT_LIBS) @LIBTLS@
+
+# C preprocessor defines
+CPPDEFS = -D_PATH_SUDO_LOGSRVD_CONF=\"$(sysconfdir)/sudo_logsrvd.conf\" \
+ -DLOCALEDIR=\"$(localedir)\"
+
+# C preprocessor flags
+CPPFLAGS = -I$(incdir) -I$(top_builddir) -I$(devdir) -I$(srcdir) \
+ $(CPPDEFS) @CPPFLAGS@
+
+# Usually -O and/or -g
+CFLAGS = @CFLAGS@
+
+# Flags to pass to the link stage
+LDFLAGS = @LDFLAGS@
+LT_LDFLAGS = @LT_LDFLAGS@
+
+# Flags to pass to libtool
+LTFLAGS = --tag=disable-static
+
+# Address sanitizer flags
+ASAN_CFLAGS = @ASAN_CFLAGS@
+ASAN_LDFLAGS = @ASAN_LDFLAGS@
+
+# PIE flags
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+
+# Stack smashing protection flags
+SSP_CFLAGS = @SSP_CFLAGS@
+SSP_LDFLAGS = @SSP_LDFLAGS@
+
+# cppcheck options, usually set in the top-level Makefile
+CPPCHECK_OPTS = -q --enable=warning,performance,portability --suppress=constStatement --suppress=compareBoolExpressionWithInt --error-exitcode=1 --inline-suppr -Dva_copy=va_copy -U__cplusplus -UQUAD_MAX -UQUAD_MIN -UUQUAD_MAX -U_POSIX_HOST_NAME_MAX -U_POSIX_PATH_MAX -U__NBBY -DNSIG=64
+
+# splint options, usually set in the top-level Makefile
+SPLINT_OPTS = -D__restrict= -checks
+
+# PVS-studio options
+PVS_CFG = $(top_srcdir)/PVS-Studio.cfg
+PVS_IGNORE = 'V707,V011,V002,V536'
+PVS_LOG_OPTS = -a 'GA:1,2' -e -t errorfile -d $(PVS_IGNORE)
+
+# Where to install things...
+prefix = @prefix@
+exec_prefix = @exec_prefix@
+bindir = @bindir@
+sbindir = @sbindir@
+sysconfdir = @sysconfdir@
+libexecdir = @libexecdir@
+datarootdir = @datarootdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+
+# User and group IDs the installed files should be "owned" by
+install_uid = 0
+install_gid = 0
+
+#### End of system configuration section. ####
+
+SHELL = @SHELL@
+
+PROGS = sudo_logsrvd sudo_sendlog
+
+LOGSRVD_OBJS = logsrv_util.o iolog_writer.o logsrvd.o logsrvd_conf.o
+
+SENDLOG_OBJS = logsrv_util.o sendlog.o
+
+IOBJS = $(LOGSRVD_OBJS:.o=.i) $(SENDLOG_OBJS:.o=.i)
+
+POBJS = $(IOBJS:.i=.plog)
+
+LIBOBJDIR = $(top_builddir)/@ac_config_libobj_dir@/
+
+VERSION = @PACKAGE_VERSION@
+
+all: $(PROGS)
+
+depend:
+ $(scriptdir)/mkdep.pl --srcdir=$(abs_top_srcdir) \
+ --builddir=$(abs_top_builddir) logsrvd/Makefile.in
+ cd $(top_builddir) && ./config.status --file logsrvd/Makefile
+
+Makefile: $(srcdir)/Makefile.in
+ cd $(top_builddir) && ./config.status --file logsrvd/Makefile
+
+.SUFFIXES: .c .h .i .lo .o .plog
+
+.c.o:
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $<
+
+.c.lo:
+ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $<
+
+.c.i:
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+
+.i.plog:
+ ifile=$<; rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $${ifile%i}c --i-file $< --output-file $@
+
+sudo_logsrvd: $(LOGSRVD_OBJS) $(LT_LIBS)
+ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(LOGSRVD_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(LIBS)
+
+sudo_sendlog: $(SENDLOG_OBJS) $(LT_LIBS)
+ $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(SENDLOG_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(SSP_LDFLAGS) $(LIBS)
+
+pre-install:
+
+install: install-binaries
+
+install-dirs:
+ $(SHELL) $(scriptdir)/mkinstalldirs $(DESTDIR)$(sbindir)
+
+install-binaries: install-dirs $(PROGS)
+ INSTALL_BACKUP='$(INSTALL_BACKUP)' $(LIBTOOL) $(LTFLAGS) --mode=install $(INSTALL) $(INSTALL_OWNER) -m 0755 sudo_logsrvd $(DESTDIR)$(sbindir)/sudo_logsrvd
+ INSTALL_BACKUP='$(INSTALL_BACKUP)' $(LIBTOOL) $(LTFLAGS) --mode=install $(INSTALL) $(INSTALL_OWNER) -m 0755 sudo_sendlog $(DESTDIR)$(sbindir)/sudo_sendlog
+
+install-doc:
+
+install-includes:
+
+install-plugin:
+
+uninstall:
+ -rm -f $(DESTDIR)$(sbindir)/sudo_logsrvd \
+ $(DESTDIR)$(sbindir)/sudo_sendlog
+ -test -z "$(INSTALL_BACKUP)" || \
+ rm -f $(DESTDIR)$(sbindir)/sudo_logsrvd$(INSTALL_BACKUP) \
+ $(DESTDIR)$(sbindir)/sudo_sendlog$(INSTALL_BACKUP)
+
+splint:
+ splint $(SPLINT_OPTS) -I$(incdir) -I$(top_builddir) -I. -I$(srcdir) $(srcdir)/*.c
+
+cppcheck:
+ cppcheck $(CPPCHECK_OPTS) -I$(incdir) -I$(top_builddir) -I. -I$(srcdir) $(srcdir)/*.c
+
+pvs-log-files: $(POBJS)
+
+pvs-studio: $(POBJS)
+ plog-converter $(PVS_LOG_OPTS) $(POBJS)
+
+check:
+
+clean:
+ -$(LIBTOOL) $(LTFLAGS) --mode=clean rm -f $(PROGS) *.lo *.o *.la
+ -rm -f *.i *.plog stamp-* core *.core core.*
+
+mostlyclean: clean
+
+distclean: clean
+ -rm -rf Makefile .libs
+
+clobber: distclean
+
+realclean: distclean
+ rm -f TAGS tags
+
+cleandir: realclean
+
+# Autogenerated dependencies, do not modify
+iolog_writer.o: $(srcdir)/iolog_writer.c $(incdir)/compat/stdbool.h \
+ $(incdir)/log_server.pb-c.h $(incdir)/protobuf-c/protobuf-c.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_eventlog.h $(incdir)/sudo_gettext.h \
+ $(incdir)/sudo_iolog.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(srcdir)/logsrv_util.h \
+ $(srcdir)/logsrvd.h $(top_builddir)/config.h
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/iolog_writer.c
+iolog_writer.i: $(srcdir)/iolog_writer.c $(incdir)/compat/stdbool.h \
+ $(incdir)/log_server.pb-c.h $(incdir)/protobuf-c/protobuf-c.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_eventlog.h $(incdir)/sudo_gettext.h \
+ $(incdir)/sudo_iolog.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(srcdir)/logsrv_util.h \
+ $(srcdir)/logsrvd.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+iolog_writer.plog: iolog_writer.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog_writer.c --i-file $< --output-file $@
+logsrv_util.o: $(srcdir)/logsrv_util.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+ $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
+ $(srcdir)/logsrv_util.h $(top_builddir)/config.h
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/logsrv_util.c
+logsrv_util.i: $(srcdir)/logsrv_util.c $(incdir)/compat/stdbool.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_debug.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+ $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
+ $(srcdir)/logsrv_util.h $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+logsrv_util.plog: logsrv_util.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/logsrv_util.c --i-file $< --output-file $@
+logsrvd.o: $(srcdir)/logsrvd.c $(incdir)/compat/getopt.h \
+ $(incdir)/compat/stdbool.h $(incdir)/hostcheck.h \
+ $(incdir)/log_server.pb-c.h $(incdir)/protobuf-c/protobuf-c.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \
+ $(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_json.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(incdir)/sudo_rand.h $(incdir)/sudo_util.h \
+ $(srcdir)/logsrv_util.h $(srcdir)/logsrvd.h \
+ $(top_builddir)/config.h $(top_builddir)/pathnames.h
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/logsrvd.c
+logsrvd.i: $(srcdir)/logsrvd.c $(incdir)/compat/getopt.h \
+ $(incdir)/compat/stdbool.h $(incdir)/hostcheck.h \
+ $(incdir)/log_server.pb-c.h $(incdir)/protobuf-c/protobuf-c.h \
+ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \
+ $(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_json.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(incdir)/sudo_rand.h $(incdir)/sudo_util.h \
+ $(srcdir)/logsrv_util.h $(srcdir)/logsrvd.h \
+ $(top_builddir)/config.h $(top_builddir)/pathnames.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+logsrvd.plog: logsrvd.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/logsrvd.c --i-file $< --output-file $@
+logsrvd_conf.o: $(srcdir)/logsrvd_conf.c $(incdir)/compat/getaddrinfo.h \
+ $(incdir)/compat/stdbool.h $(incdir)/log_server.pb-c.h \
+ $(incdir)/protobuf-c/protobuf-c.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_eventlog.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+ $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
+ $(srcdir)/logsrv_util.h $(srcdir)/logsrvd.h \
+ $(top_builddir)/config.h $(top_builddir)/pathnames.h
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/logsrvd_conf.c
+logsrvd_conf.i: $(srcdir)/logsrvd_conf.c $(incdir)/compat/getaddrinfo.h \
+ $(incdir)/compat/stdbool.h $(incdir)/log_server.pb-c.h \
+ $(incdir)/protobuf-c/protobuf-c.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_debug.h $(incdir)/sudo_eventlog.h \
+ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \
+ $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \
+ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \
+ $(srcdir)/logsrv_util.h $(srcdir)/logsrvd.h \
+ $(top_builddir)/config.h $(top_builddir)/pathnames.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+logsrvd_conf.plog: logsrvd_conf.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/logsrvd_conf.c --i-file $< --output-file $@
+sendlog.o: $(srcdir)/sendlog.c $(incdir)/compat/getaddrinfo.h \
+ $(incdir)/compat/getopt.h $(incdir)/compat/stdbool.h \
+ $(incdir)/hostcheck.h $(incdir)/log_server.pb-c.h \
+ $(incdir)/protobuf-c/protobuf-c.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \
+ $(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(srcdir)/logsrv_util.h $(srcdir)/sendlog.h \
+ $(top_builddir)/config.h
+ $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/sendlog.c
+sendlog.i: $(srcdir)/sendlog.c $(incdir)/compat/getaddrinfo.h \
+ $(incdir)/compat/getopt.h $(incdir)/compat/stdbool.h \
+ $(incdir)/hostcheck.h $(incdir)/log_server.pb-c.h \
+ $(incdir)/protobuf-c/protobuf-c.h $(incdir)/sudo_compat.h \
+ $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \
+ $(incdir)/sudo_eventlog.h $(incdir)/sudo_fatal.h \
+ $(incdir)/sudo_gettext.h $(incdir)/sudo_iolog.h \
+ $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \
+ $(incdir)/sudo_util.h $(srcdir)/logsrv_util.h $(srcdir)/sendlog.h \
+ $(top_builddir)/config.h
+ $(CC) -E -o $@ $(CPPFLAGS) $<
+sendlog.plog: sendlog.i
+ rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/sendlog.c --i-file $< --output-file $@
diff --git a/logsrvd/iolog_writer.c b/logsrvd/iolog_writer.c
new file mode 100644
index 0000000..6f620b4
--- /dev/null
+++ b/logsrvd/iolog_writer.c
@@ -0,0 +1,1088 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 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.
+ */
+
+#include "config.h"
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#ifdef HAVE_STDBOOL_H
+# include <stdbool.h>
+#else
+# include "compat/stdbool.h"
+#endif /* HAVE_STDBOOL_H */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "sudo_compat.h"
+#include "sudo_debug.h"
+#include "sudo_eventlog.h"
+#include "sudo_gettext.h"
+#include "sudo_iolog.h"
+#include "sudo_queue.h"
+#include "sudo_util.h"
+
+#include "log_server.pb-c.h"
+#include "logsrvd.h"
+
+static inline bool
+has_numval(InfoMessage *info)
+{
+ return info->value_case == INFO_MESSAGE__VALUE_NUMVAL;
+}
+
+static inline bool
+has_strval(InfoMessage *info)
+{
+ return info->value_case == INFO_MESSAGE__VALUE_STRVAL;
+}
+
+static inline bool
+has_strlistval(InfoMessage *info)
+{
+ return info->value_case == INFO_MESSAGE__VALUE_STRLISTVAL;
+}
+
+/*
+ * Copy the specified string list.
+ * The input string list need not be NULL-terminated.
+ * Returns a NULL-terminated string vector.
+ */
+static char **
+strlist_copy(InfoMessage__StringList *strlist)
+{
+ char **dst, **src = strlist->strings;
+ size_t i, len = strlist->n_strings;
+ debug_decl(strlist_copy, SUDO_DEBUG_UTIL);
+
+ dst = reallocarray(NULL, len + 1, sizeof(char *));
+ if (dst == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "reallocarray(NULL, %zu, %zu)", len + 1, sizeof(char *));
+ goto bad;
+ }
+ for (i = 0; i < len; i++) {
+ if ((dst[i] = strdup(src[i])) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, "strdup");
+ goto bad;
+ }
+ }
+ dst[i] = NULL;
+ debug_return_ptr(dst);
+
+bad:
+ if (dst != NULL) {
+ while (i--)
+ free(dst[i]);
+ free(dst);
+ }
+ debug_return_ptr(NULL);
+}
+
+/*
+ * Fill in eventlog details from an AcceptMessage
+ * Caller is responsible for freeing strings in struct eventlog.
+ * Returns true on success and false on failure.
+ */
+struct eventlog *
+evlog_new(TimeSpec *submit_time, InfoMessage **info_msgs, size_t infolen)
+{
+ struct eventlog *evlog;
+ size_t idx;
+ debug_decl(evlog_new, SUDO_DEBUG_UTIL);
+
+ evlog = calloc(1, sizeof(*evlog));
+ if (evlog == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "calloc(1, %zu)", sizeof(*evlog));
+ goto bad;
+ }
+ memset(evlog, 0, sizeof(*evlog));
+
+ /* Submit time. */
+ if (submit_time != NULL) {
+ evlog->submit_time.tv_sec = submit_time->tv_sec;
+ evlog->submit_time.tv_nsec = submit_time->tv_nsec;
+ }
+
+ /* Default values */
+ evlog->lines = 24;
+ evlog->columns = 80;
+ evlog->runuid = (uid_t)-1;
+ evlog->rungid = (gid_t)-1;
+
+ /* Pull out values by key from info array. */
+ for (idx = 0; idx < infolen; idx++) {
+ InfoMessage *info = info_msgs[idx];
+ const char *key = info->key;
+ switch (key[0]) {
+ case 'c':
+ if (strcmp(key, "columns") == 0) {
+ if (!has_numval(info)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "columns specified but not a number");
+ } else if (info->u.numval <= 0 || info->u.numval > INT_MAX) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "columns (%" PRId64 ") out of range", info->u.numval);
+ } else {
+ evlog->columns = info->u.numval;
+ }
+ continue;
+ }
+ if (strcmp(key, "command") == 0) {
+ if (has_strval(info)) {
+ if ((evlog->command = strdup(info->u.strval)) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "command specified but not a string");
+ }
+ continue;
+ }
+ break;
+ case 'l':
+ if (strcmp(key, "lines") == 0) {
+ if (!has_numval(info)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "lines specified but not a number");
+ } else if (info->u.numval <= 0 || info->u.numval > INT_MAX) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "lines (%" PRId64 ") out of range", info->u.numval);
+ } else {
+ evlog->lines = info->u.numval;
+ }
+ continue;
+ }
+ break;
+ case 'r':
+ if (strcmp(key, "runargv") == 0) {
+ if (has_strlistval(info)) {
+ evlog->argv = strlist_copy(info->u.strlistval);
+ if (evlog->argv == NULL)
+ goto bad;
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "runargv specified but not a string list");
+ }
+ continue;
+ }
+ if (strcmp(key, "runchroot") == 0) {
+ if (has_strval(info)) {
+ if ((evlog->runchroot = strdup(info->u.strval)) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "runchroot specified but not a string");
+ }
+ continue;
+ }
+ if (strcmp(key, "runcwd") == 0) {
+ if (has_strval(info)) {
+ if ((evlog->runcwd = strdup(info->u.strval)) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "runcwd specified but not a string");
+ }
+ continue;
+ }
+ if (strcmp(key, "runenv") == 0) {
+ if (has_strlistval(info)) {
+ evlog->envp = strlist_copy(info->u.strlistval);
+ if (evlog->envp == NULL)
+ goto bad;
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "runenv specified but not a string list");
+ }
+ continue;
+ }
+ if (strcmp(key, "rungid") == 0) {
+ if (!has_numval(info)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "rungid specified but not a number");
+ } else if (info->u.numval < 0 || info->u.numval > INT_MAX) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "rungid (%" PRId64 ") out of range", info->u.numval);
+ } else {
+ evlog->rungid = info->u.numval;
+ }
+ continue;
+ }
+ if (strcmp(key, "rungroup") == 0) {
+ if (has_strval(info)) {
+ if ((evlog->rungroup = strdup(info->u.strval)) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "rungroup specified but not a string");
+ }
+ continue;
+ }
+ if (strcmp(key, "runuid") == 0) {
+ if (!has_numval(info)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "runuid specified but not a number");
+ } else if (info->u.numval < 0 || info->u.numval > INT_MAX) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "runuid (%" PRId64 ") out of range", info->u.numval);
+ } else {
+ evlog->runuid = info->u.numval;
+ }
+ continue;
+ }
+ if (strcmp(key, "runuser") == 0) {
+ if (has_strval(info)) {
+ if ((evlog->runuser = strdup(info->u.strval)) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "runuser specified but not a string");
+ }
+ continue;
+ }
+ break;
+ case 's':
+ if (strcmp(key, "submitcwd") == 0) {
+ if (has_strval(info)) {
+ if ((evlog->cwd = strdup(info->u.strval)) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "submitcwd specified but not a string");
+ }
+ continue;
+ }
+ if (strcmp(key, "submitgroup") == 0) {
+ if (has_strval(info)) {
+ if ((evlog->submitgroup = strdup(info->u.strval)) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "submitgroup specified but not a string");
+ }
+ continue;
+ }
+ if (strcmp(key, "submithost") == 0) {
+ if (has_strval(info)) {
+ if ((evlog->submithost = strdup(info->u.strval)) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "submithost specified but not a string");
+ }
+ continue;
+ }
+ if (strcmp(key, "submituser") == 0) {
+ if (has_strval(info)) {
+ if ((evlog->submituser = strdup(info->u.strval)) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "submituser specified but not a string");
+ }
+ continue;
+ }
+ break;
+ case 't':
+ if (strcmp(key, "ttyname") == 0) {
+ if (has_strval(info)) {
+ if ((evlog->ttyname = strdup(info->u.strval)) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "ttyname specified but not a string");
+ }
+ continue;
+ }
+ break;
+ }
+ }
+
+ /* Check for required settings */
+ if (evlog->submituser == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "missing submituser in AcceptMessage");
+ goto bad;
+ }
+ if (evlog->submithost == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "missing submithost in AcceptMessage");
+ goto bad;
+ }
+ if (evlog->runuser == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "missing runuser in AcceptMessage");
+ goto bad;
+ }
+ if (evlog->command == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "missing command in AcceptMessage");
+ goto bad;
+ }
+
+ /* Other settings that must exist for event logging. */
+ if (evlog->cwd == NULL) {
+ if ((evlog->cwd = strdup("unknown")) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ }
+ if (evlog->runcwd == NULL) {
+ if ((evlog->runcwd = strdup(evlog->cwd)) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ }
+ if (evlog->submitgroup == NULL) {
+ /* TODO: make submitgroup required */
+ if ((evlog->submitgroup = strdup("unknown")) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ }
+ if (evlog->ttyname == NULL) {
+ if ((evlog->ttyname = strdup("unknown")) == NULL) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ }
+
+ debug_return_ptr(evlog);
+
+bad:
+ eventlog_free(evlog);
+ debug_return_ptr(NULL);
+}
+
+struct iolog_path_closure {
+ char *iolog_dir;
+ struct eventlog *evlog;
+};
+
+static size_t
+fill_seq(char *str, size_t strsize, void *v)
+{
+ struct iolog_path_closure *closure = v;
+ char *sessid = closure->evlog->sessid;
+ int len;
+ debug_decl(fill_seq, SUDO_DEBUG_UTIL);
+
+ if (sessid[0] == '\0') {
+ if (!iolog_nextid(closure->iolog_dir, sessid))
+ debug_return_size_t((size_t)-1);
+ }
+
+ /* Path is of the form /var/log/sudo-io/00/00/01. */
+ len = snprintf(str, strsize, "%c%c/%c%c/%c%c", sessid[0],
+ sessid[1], sessid[2], sessid[3], sessid[4], sessid[5]);
+ if (len < 0 || len >= (ssize_t)strsize) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to format session id");
+ debug_return_size_t(strsize); /* handle non-standard snprintf() */
+ }
+ debug_return_size_t(len);
+}
+
+static size_t
+fill_user(char *str, size_t strsize, void *v)
+{
+ struct iolog_path_closure *closure = v;
+ const struct eventlog *evlog = closure->evlog;
+ debug_decl(fill_user, SUDO_DEBUG_UTIL);
+
+ if (evlog->submituser == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "submituser not set");
+ debug_return_size_t(strsize);
+ }
+ debug_return_size_t(strlcpy(str, evlog->submituser, strsize));
+}
+
+static size_t
+fill_group(char *str, size_t strsize, void *v)
+{
+ struct iolog_path_closure *closure = v;
+ const struct eventlog *evlog = closure->evlog;
+ debug_decl(fill_group, SUDO_DEBUG_UTIL);
+
+ if (evlog->submitgroup == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "submitgroup not set");
+ debug_return_size_t(strsize);
+ }
+ debug_return_size_t(strlcpy(str, evlog->submitgroup, strsize));
+}
+
+static size_t
+fill_runas_user(char *str, size_t strsize, void *v)
+{
+ struct iolog_path_closure *closure = v;
+ const struct eventlog *evlog = closure->evlog;
+ debug_decl(fill_runas_user, SUDO_DEBUG_UTIL);
+
+ if (evlog->runuser == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "runuser not set");
+ debug_return_size_t(strsize);
+ }
+ debug_return_size_t(strlcpy(str, evlog->runuser, strsize));
+}
+
+static size_t
+fill_runas_group(char *str, size_t strsize, void *v)
+{
+ struct iolog_path_closure *closure = v;
+ const struct eventlog *evlog = closure->evlog;
+ debug_decl(fill_runas_group, SUDO_DEBUG_UTIL);
+
+ /* FIXME: rungroup not guaranteed to be set */
+ if (evlog->rungroup == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "rungroup not set");
+ debug_return_size_t(strsize);
+ }
+ debug_return_size_t(strlcpy(str, evlog->rungroup, strsize));
+}
+
+static size_t
+fill_hostname(char *str, size_t strsize, void *v)
+{
+ struct iolog_path_closure *closure = v;
+ const struct eventlog *evlog = closure->evlog;
+ debug_decl(fill_hostname, SUDO_DEBUG_UTIL);
+
+ if (evlog->submithost == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "submithost not set");
+ debug_return_size_t(strsize);
+ }
+ debug_return_size_t(strlcpy(str, evlog->submithost, strsize));
+}
+
+static size_t
+fill_command(char *str, size_t strsize, void *v)
+{
+ struct iolog_path_closure *closure = v;
+ const struct eventlog *evlog = closure->evlog;
+ debug_decl(fill_command, SUDO_DEBUG_UTIL);
+
+ if (evlog->command == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "command not set");
+ debug_return_size_t(strsize);
+ }
+ debug_return_size_t(strlcpy(str, evlog->command, strsize));
+}
+
+/* Note: "seq" must be first in the list. */
+static const struct iolog_path_escape path_escapes[] = {
+ { "seq", fill_seq },
+ { "user", fill_user },
+ { "group", fill_group },
+ { "runas_user", fill_runas_user },
+ { "runas_group", fill_runas_group },
+ { "hostname", fill_hostname },
+ { "command", fill_command },
+ { NULL, NULL }
+};
+
+/*
+ * Create I/O log path
+ * Sets iolog_path, iolog_file and iolog_dir_fd in the closure
+ */
+static bool
+create_iolog_path(struct connection_closure *closure)
+{
+ struct eventlog *evlog = closure->evlog;
+ struct iolog_path_closure path_closure;
+ char expanded_dir[PATH_MAX], expanded_file[PATH_MAX], pathbuf[PATH_MAX];
+ size_t len;
+ debug_decl(create_iolog_path, SUDO_DEBUG_UTIL);
+
+ path_closure.evlog = evlog;
+ path_closure.iolog_dir = expanded_dir;
+
+ if (!expand_iolog_path(logsrvd_conf_iolog_dir(), expanded_dir,
+ sizeof(expanded_dir), &path_escapes[1], &path_closure)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to expand iolog dir %s", logsrvd_conf_iolog_dir());
+ goto bad;
+ }
+
+ if (!expand_iolog_path(logsrvd_conf_iolog_file(), expanded_file,
+ sizeof(expanded_file), &path_escapes[0], &path_closure)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to expand iolog dir %s", logsrvd_conf_iolog_file());
+ goto bad;
+ }
+
+ len = snprintf(pathbuf, sizeof(pathbuf), "%s/%s", expanded_dir,
+ expanded_file);
+ if (len >= sizeof(pathbuf)) {
+ errno = ENAMETOOLONG;
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "%s/%s", expanded_dir, expanded_file);
+ goto bad;
+ }
+
+ /*
+ * Create log path, along with any intermediate subdirs.
+ * Calls mkdtemp() if pathbuf ends in XXXXXX.
+ */
+ if (!iolog_mkpath(pathbuf)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to mkdir iolog path %s", pathbuf);
+ goto bad;
+ }
+ if ((evlog->iolog_path = strdup(pathbuf)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+ evlog->iolog_file = evlog->iolog_path + strlen(expanded_dir) + 1;
+
+ /* We use iolog_dir_fd in calls to openat(2) */
+ closure->iolog_dir_fd =
+ iolog_openat(AT_FDCWD, evlog->iolog_path, O_RDONLY);
+ if (closure->iolog_dir_fd == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "%s", evlog->iolog_path);
+ goto bad;
+ }
+
+ debug_return_bool(true);
+bad:
+ free(evlog->iolog_path);
+ evlog->iolog_path = NULL;
+ debug_return_bool(false);
+}
+
+static bool
+iolog_create(int iofd, struct connection_closure *closure)
+{
+ debug_decl(iolog_create, SUDO_DEBUG_UTIL);
+
+ if (iofd < 0 || iofd >= IOFD_MAX) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "invalid iofd %d", iofd);
+ debug_return_bool(false);
+ }
+
+ closure->iolog_files[iofd].enabled = true;
+ debug_return_bool(iolog_open(&closure->iolog_files[iofd],
+ closure->iolog_dir_fd, iofd, "w"));
+}
+
+void
+iolog_close_all(struct connection_closure *closure)
+{
+ const char *errstr;
+ int i;
+ debug_decl(iolog_close, SUDO_DEBUG_UTIL);
+
+ for (i = 0; i < IOFD_MAX; i++) {
+ if (!closure->iolog_files[i].enabled)
+ continue;
+ if (!iolog_close(&closure->iolog_files[i], &errstr)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "error closing iofd %d: %s", i, errstr);
+ }
+ }
+ if (closure->iolog_dir_fd != -1)
+ close(closure->iolog_dir_fd);
+
+ debug_return;
+}
+
+bool
+iolog_init(AcceptMessage *msg, struct connection_closure *closure)
+{
+ struct eventlog *evlog = closure->evlog;
+ debug_decl(iolog_init, SUDO_DEBUG_UTIL);
+
+ /* Create I/O log path */
+ if (!create_iolog_path(closure))
+ debug_return_bool(false);
+
+ /* Write sudo I/O log info file */
+ if (!iolog_write_info_file(closure->iolog_dir_fd, evlog))
+ debug_return_bool(false);
+
+ /*
+ * Create timing, stdout, stderr and ttyout files for sudoreplay.
+ * Others will be created on demand.
+ */
+ if (!iolog_create(IOFD_TIMING, closure) ||
+ !iolog_create(IOFD_STDOUT, closure) ||
+ !iolog_create(IOFD_STDERR, closure) ||
+ !iolog_create(IOFD_TTYOUT, closure))
+ debug_return_bool(false);
+
+ /* Ready to log I/O buffers. */
+ debug_return_bool(true);
+}
+
+/*
+ * Copy len bytes from src to dst.
+ */
+static bool
+iolog_copy(struct iolog_file *src, struct iolog_file *dst, off_t remainder,
+ const char **errstr)
+{
+ char buf[64 * 1024];
+ ssize_t nread;
+ debug_decl(iolog_copy, SUDO_DEBUG_UTIL);
+
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "copying %lld bytes", (long long)remainder);
+ while (remainder > 0) {
+ const ssize_t toread = MIN(remainder, ssizeof(buf));
+ nread = iolog_read(src, buf, toread, errstr);
+ if (nread == -1)
+ debug_return_bool(false);
+ remainder -= nread;
+
+ do {
+ ssize_t nwritten = iolog_write(dst, buf, nread, errstr);
+ if (nwritten == -1)
+ debug_return_bool(false);
+ nread -= nwritten;
+ } while (nread > 0);
+ }
+
+ debug_return_bool(true);
+}
+
+/* Compressed logs don't support random access, need to rewrite them. */
+static bool
+iolog_rewrite(const struct timespec *target, struct connection_closure *closure)
+{
+ const struct eventlog *evlog = closure->evlog;
+ struct iolog_file new_iolog_files[IOFD_MAX];
+ off_t iolog_file_sizes[IOFD_MAX] = { 0 };
+ struct timing_closure timing;
+ int iofd, len, tmpdir_fd = -1;
+ const char *name, *errstr;
+ char tmpdir[PATH_MAX];
+ bool ret = false;
+ debug_decl(iolog_rewrite, SUDO_DEBUG_UTIL);
+
+ /* Parse timing file until we reach the target point. */
+ /* TODO: use iolog_seekto with a callback? */
+ for (;;) {
+ /* Read next record from timing file. */
+ if (iolog_read_timing_record(&closure->iolog_files[IOFD_TIMING], &timing) != 0)
+ goto done;
+ sudo_timespecadd(&timing.delay, &closure->elapsed_time,
+ &closure->elapsed_time);
+ if (timing.event < IOFD_TIMING) {
+ if (!closure->iolog_files[timing.event].enabled) {
+ /* Missing log file. */
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "iofd %d referenced but not open", timing.event);
+ goto done;
+ }
+ iolog_file_sizes[timing.event] += timing.u.nbytes;
+ }
+
+ if (sudo_timespeccmp(&closure->elapsed_time, target, >=)) {
+ if (sudo_timespeccmp(&closure->elapsed_time, target, ==))
+ break;
+
+ /* Mismatch between resume point and stored log. */
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "resume point mismatch, target [%lld, %ld], have [%lld, %ld]",
+ (long long)target->tv_sec, target->tv_nsec,
+ (long long)closure->elapsed_time.tv_sec,
+ closure->elapsed_time.tv_nsec);
+ goto done;
+ }
+ }
+ iolog_file_sizes[IOFD_TIMING] =
+ iolog_seek(&closure->iolog_files[IOFD_TIMING], 0, SEEK_CUR);
+ iolog_rewind(&closure->iolog_files[IOFD_TIMING]);
+
+ /* Create new I/O log files in a temporary directory. */
+ len = snprintf(tmpdir, sizeof(tmpdir), "%s/restart.XXXXXX",
+ evlog->iolog_path);
+ if (len < 0 || len >= ssizeof(tmpdir)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to format %s/restart.XXXXXX", evlog->iolog_path);
+ goto done;
+ }
+ if (!iolog_mkdtemp(tmpdir)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to mkdtemp %s", tmpdir);
+ goto done;
+ }
+ if ((tmpdir_fd = iolog_openat(AT_FDCWD, tmpdir, O_RDONLY)) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to open %s", tmpdir);
+ goto done;
+ }
+
+ /* Create new copies of the existing iologs */
+ memset(new_iolog_files, 0, sizeof(new_iolog_files));
+ for (iofd = 0; iofd < IOFD_MAX; iofd++) {
+ if (!closure->iolog_files[iofd].enabled)
+ continue;
+ new_iolog_files[iofd].enabled = true;
+ if (!iolog_open(&new_iolog_files[iofd], tmpdir_fd, iofd, "w")) {
+ if (errno != ENOENT) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to open %s/%s", tmpdir, iolog_fd_to_name(iofd));
+ goto done;
+ }
+ }
+ }
+
+ for (iofd = 0; iofd < IOFD_MAX; iofd++) {
+ if (!closure->iolog_files[iofd].enabled)
+ continue;
+ if (!iolog_copy(&closure->iolog_files[iofd], &new_iolog_files[iofd],
+ iolog_file_sizes[iofd], &errstr)) {
+ name = iolog_fd_to_name(iofd);
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to copy %s/%s to %s/%s: %s",
+ evlog->iolog_path, name, tmpdir, name, errstr);
+ goto done;
+ }
+ }
+
+ /* Move copied log files into place. */
+ for (iofd = 0; iofd < IOFD_MAX; iofd++) {
+ char from[PATH_MAX], to[PATH_MAX];
+
+ if (!closure->iolog_files[iofd].enabled)
+ continue;
+
+ /* This would be easier with renameat(2), old systems are annoying. */
+ name = iolog_fd_to_name(iofd);
+ len = snprintf(from, sizeof(from), "%s/%s", tmpdir, name);
+ if (len < 0 || len >= ssizeof(from)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to format %s/%s", tmpdir, name);
+ goto done;
+ }
+ len = snprintf(to, sizeof(to), "%s/%s", evlog->iolog_path,
+ name);
+ if (len < 0 || len >= ssizeof(from)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to format %s/%s", evlog->iolog_path, name);
+ goto done;
+ }
+ if (!iolog_rename(from, to)) {
+ sudo_debug_printf(
+ SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to rename %s to %s", from, to);
+ goto done;
+ }
+ }
+
+ for (iofd = 0; iofd < IOFD_MAX; iofd++) {
+ if (!closure->iolog_files[iofd].enabled)
+ continue;
+ (void)iolog_close(&closure->iolog_files[iofd], &errstr);
+ closure->iolog_files[iofd] = new_iolog_files[iofd];
+ new_iolog_files[iofd].enabled = false;
+ }
+
+ /* Ready to log I/O buffers. */
+ ret = true;
+done:
+ if (tmpdir_fd != -1) {
+ if (!ret) {
+ for (iofd = 0; iofd < IOFD_MAX; iofd++) {
+ if (!new_iolog_files[iofd].enabled)
+ continue;
+ (void)iolog_close(&new_iolog_files[iofd], &errstr);
+ (void)unlinkat(tmpdir_fd, iolog_fd_to_name(iofd), 0);
+ }
+ }
+ close(tmpdir_fd);
+ (void)rmdir(tmpdir);
+ }
+ debug_return_bool(ret);
+}
+
+bool
+iolog_restart(RestartMessage *msg, struct connection_closure *closure)
+{
+ struct eventlog *evlog = closure->evlog;
+ struct timespec target;
+ struct stat sb;
+ int iofd;
+ debug_decl(iolog_restart, SUDO_DEBUG_UTIL);
+
+ target.tv_sec = msg->resume_point->tv_sec;
+ target.tv_nsec = msg->resume_point->tv_nsec;
+
+ if ((evlog->iolog_path = strdup(msg->log_id)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "strdup");
+ goto bad;
+ }
+
+ /* We use iolog_dir_fd in calls to openat(2) */
+ closure->iolog_dir_fd =
+ iolog_openat(AT_FDCWD, evlog->iolog_path, O_RDONLY);
+ if (closure->iolog_dir_fd == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "%s", evlog->iolog_path);
+ goto bad;
+ }
+
+ /* If the timing file write bit is clear, log is already complete. */
+ if (fstatat(closure->iolog_dir_fd, "timing", &sb, 0) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to stat %s/timing", evlog->iolog_path);
+ goto bad;
+ }
+ if (!ISSET(sb.st_mode, S_IWUSR)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "%s already complete", evlog->iolog_path);
+ closure->errstr = _("log is already complete, cannot be restarted");
+ goto bad;
+ }
+
+ /* Open existing I/O log files. */
+ if (!iolog_open_all(closure->iolog_dir_fd, evlog->iolog_path,
+ closure->iolog_files, "r+"))
+ goto bad;
+
+ /* Compressed logs don't support random access, so rewrite them. */
+ for (iofd = 0; iofd < IOFD_MAX; iofd++) {
+ if (closure->iolog_files[iofd].compressed)
+ debug_return_bool(iolog_rewrite(&target, closure));
+ }
+
+ /* Parse timing file until we reach the target point. */
+ if (!iolog_seekto(closure->iolog_dir_fd, evlog->iolog_path,
+ closure->iolog_files, &closure->elapsed_time, &target))
+ goto bad;
+
+ /* Must seek or flush before switching from read -> write. */
+ if (iolog_seek(&closure->iolog_files[IOFD_TIMING], 0, SEEK_CUR) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "lseek(IOFD_TIMING, 0, SEEK_CUR)");
+ goto bad;
+ }
+
+ /* Ready to log I/O buffers. */
+ debug_return_bool(true);
+bad:
+ if (closure->errstr == NULL)
+ closure->errstr = _("unable to restart log");
+ debug_return_bool(false);
+}
+
+/*
+ * Add given delta to elapsed time.
+ * We cannot use timespecadd here since delta is not struct timespec.
+ */
+static void
+update_elapsed_time(TimeSpec *delta, struct timespec *elapsed)
+{
+ debug_decl(update_elapsed_time, SUDO_DEBUG_UTIL);
+
+ /* Cannot use timespecadd since msg doesn't use struct timespec. */
+ elapsed->tv_sec += delta->tv_sec;
+ elapsed->tv_nsec += delta->tv_nsec;
+ while (elapsed->tv_nsec >= 1000000000) {
+ elapsed->tv_sec++;
+ elapsed->tv_nsec -= 1000000000;
+ }
+
+ debug_return;
+}
+
+int
+store_iobuf(int iofd, IoBuffer *msg, struct connection_closure *closure)
+{
+ const struct eventlog *evlog = closure->evlog;
+ const char *errstr;
+ char tbuf[1024];
+ int len;
+ debug_decl(store_iobuf, SUDO_DEBUG_UTIL);
+
+ /* Open log file as needed. */
+ if (!closure->iolog_files[iofd].enabled) {
+ if (!iolog_create(iofd, closure))
+ debug_return_int(-1);
+ }
+
+ /* Format timing data. */
+ /* FIXME - assumes IOFD_* matches IO_EVENT_* */
+ len = snprintf(tbuf, sizeof(tbuf), "%d %lld.%09d %zu\n",
+ iofd, (long long)msg->delay->tv_sec, (int)msg->delay->tv_nsec,
+ msg->data.len);
+ if (len < 0 || len >= ssizeof(tbuf)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to format timing buffer, len %d", len);
+ debug_return_int(-1);
+ }
+
+ /* Write to specified I/O log file. */
+ if (!iolog_write(&closure->iolog_files[iofd], msg->data.data,
+ msg->data.len, &errstr)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to write to %s/%s: %s", evlog->iolog_path,
+ iolog_fd_to_name(iofd), errstr);
+ debug_return_int(-1);
+ }
+
+ /* Write timing data. */
+ if (!iolog_write(&closure->iolog_files[IOFD_TIMING], tbuf,
+ len, &errstr)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to write to %s/%s: %s", evlog->iolog_path,
+ iolog_fd_to_name(IOFD_TIMING), errstr);
+ debug_return_int(-1);
+ }
+
+ update_elapsed_time(msg->delay, &closure->elapsed_time);
+
+ debug_return_int(0);
+}
+
+int
+store_suspend(CommandSuspend *msg, struct connection_closure *closure)
+{
+ const struct eventlog *evlog = closure->evlog;
+ const char *errstr;
+ char tbuf[1024];
+ int len;
+ debug_decl(store_suspend, SUDO_DEBUG_UTIL);
+
+ /* Format timing data including suspend signal. */
+ len = snprintf(tbuf, sizeof(tbuf), "%d %lld.%09d %s\n", IO_EVENT_SUSPEND,
+ (long long)msg->delay->tv_sec, (int)msg->delay->tv_nsec,
+ msg->signal);
+ if (len < 0 || len >= ssizeof(tbuf)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to format timing buffer, len %d, signal %s",
+ len, msg->signal);
+ debug_return_int(-1);
+ }
+
+ /* Write timing data. */
+ if (!iolog_write(&closure->iolog_files[IOFD_TIMING], tbuf,
+ len, &errstr)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to write to %s/%s: %s", evlog->iolog_path,
+ iolog_fd_to_name(IOFD_TIMING), errstr);
+ debug_return_int(-1);
+ }
+
+ update_elapsed_time(msg->delay, &closure->elapsed_time);
+
+ debug_return_int(0);
+}
+
+int
+store_winsize(ChangeWindowSize *msg, struct connection_closure *closure)
+{
+ const struct eventlog *evlog = closure->evlog;
+ const char *errstr;
+ char tbuf[1024];
+ int len;
+ debug_decl(store_winsize, SUDO_DEBUG_UTIL);
+
+ /* Format timing data including new window size. */
+ len = snprintf(tbuf, sizeof(tbuf), "%d %lld.%09d %d %d\n", IO_EVENT_WINSIZE,
+ (long long)msg->delay->tv_sec, (int)msg->delay->tv_nsec,
+ msg->rows, msg->cols);
+ if (len < 0 || len >= ssizeof(tbuf)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to format timing buffer, len %d", len);
+ debug_return_int(-1);
+ }
+
+ /* Write timing data. */
+ if (!iolog_write(&closure->iolog_files[IOFD_TIMING], tbuf,
+ len, &errstr)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to write to %s/%s: %s", evlog->iolog_path,
+ iolog_fd_to_name(IOFD_TIMING), errstr);
+ debug_return_int(-1);
+ }
+
+ update_elapsed_time(msg->delay, &closure->elapsed_time);
+
+ debug_return_int(0);
+}
diff --git a/logsrvd/logsrv_util.c b/logsrvd/logsrv_util.c
new file mode 100644
index 0000000..e5d54f1
--- /dev/null
+++ b/logsrvd/logsrv_util.c
@@ -0,0 +1,159 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 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.
+ */
+
+#include "config.h"
+
+#include <sys/types.h>
+
+#include <errno.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_debug.h"
+#include "sudo_fatal.h"
+#include "sudo_gettext.h"
+#include "sudo_iolog.h"
+#include "sudo_util.h"
+
+#include "logsrv_util.h"
+
+/*
+ * Expand buf as needed or just reset it.
+ */
+bool
+expand_buf(struct connection_buffer *buf, unsigned int needed)
+{
+ void *newdata;
+ debug_decl(expand_buf, SUDO_DEBUG_UTIL);
+
+ if (buf->size < needed) {
+ /* Expand buffer. */
+ needed = sudo_pow2_roundup(needed);
+ if ((newdata = malloc(needed)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
+ "%s: unable to malloc %u", __func__, needed);
+ debug_return_bool(false);
+ }
+ if (buf->off > 0)
+ memcpy(newdata, buf->data + buf->off, buf->len - buf->off);
+ free(buf->data);
+ buf->data = newdata;
+ buf->size = needed;
+ } else {
+ /* Just reset existing buffer. */
+ if (buf->off > 0) {
+ memmove(buf->data, buf->data + buf->off,
+ buf->len - buf->off);
+ }
+ }
+ buf->len -= buf->off;
+ buf->off = 0;
+
+ debug_return_bool(true);
+}
+
+/*
+ * Open any I/O log files that are present.
+ * The timing file must always exist.
+ */
+bool
+iolog_open_all(int dfd, const char *iolog_dir, struct iolog_file *iolog_files,
+ const char *mode)
+{
+ int iofd;
+ debug_decl(iolog_open_all, SUDO_DEBUG_UTIL);
+
+ for (iofd = 0; iofd < IOFD_MAX; iofd++) {
+ iolog_files[iofd].enabled = true;
+ if (!iolog_open(&iolog_files[iofd], dfd, iofd, mode)) {
+ if (errno != ENOENT) {
+ sudo_warn(U_("unable to open %s/%s"), iolog_dir,
+ iolog_fd_to_name(iofd));
+ debug_return_bool(false);
+ }
+ }
+ }
+ if (!iolog_files[IOFD_TIMING].enabled) {
+ sudo_warn(U_("unable to open %s/%s"), iolog_dir,
+ iolog_fd_to_name(IOFD_TIMING));
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+/*
+ * Seek to the specified point in time in the I/O logs.
+ */
+bool
+iolog_seekto(int iolog_dir_fd, const char *iolog_path,
+ struct iolog_file *iolog_files, struct timespec *elapsed_time,
+ const struct timespec *target)
+{
+ struct timing_closure timing;
+ off_t pos;
+ debug_decl(iolog_seekto, SUDO_DEBUG_UTIL);
+
+ /* Parse timing file until we reach the target point. */
+ for (;;) {
+ if (iolog_read_timing_record(&iolog_files[IOFD_TIMING], &timing) != 0)
+ goto bad;
+ sudo_timespecadd(&timing.delay, elapsed_time, elapsed_time);
+ if (timing.event < IOFD_TIMING) {
+ if (!iolog_files[timing.event].enabled) {
+ /* Missing log file. */
+ sudo_warn(U_("missing I/O log file %s/%s"), iolog_path,
+ iolog_fd_to_name(timing.event));
+ goto bad;
+ }
+ pos = iolog_seek(&iolog_files[timing.event], timing.u.nbytes,
+ SEEK_CUR);
+ if (pos == -1) {
+ sudo_warn(U_("%s/%s: unable to seek forward %zu"), iolog_path,
+ iolog_fd_to_name(timing.event), timing.u.nbytes);
+ goto bad;
+ }
+ }
+ if (sudo_timespeccmp(elapsed_time, target, >=)) {
+ if (sudo_timespeccmp(elapsed_time, target, ==))
+ break;
+
+ /* Mismatch between resume point and stored log. */
+ sudo_warnx(U_("unable to find resume point [%lld, %ld] in %s/%s"),
+ (long long)target->tv_sec, target->tv_nsec, iolog_path,
+ "timing");
+ goto bad;
+ }
+ }
+ debug_return_bool(true);
+bad:
+ debug_return_bool(false);
+}
diff --git a/logsrvd/logsrv_util.h b/logsrvd/logsrv_util.h
new file mode 100644
index 0000000..2f4cb06
--- /dev/null
+++ b/logsrvd/logsrv_util.h
@@ -0,0 +1,43 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 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.
+ */
+
+#ifndef SUDO_LOGSRV_UTIL_H
+#define SUDO_LOGSRV_UTIL_H
+
+/* Default ports to listen on */
+#define DEFAULT_PORT "30343"
+#define DEFAULT_PORT_TLS "30344"
+
+/* Maximum message size (2Mb) */
+#define MESSAGE_SIZE_MAX (2 * 1024 * 1024)
+
+struct connection_buffer {
+ uint8_t *data;
+ unsigned int size;
+ unsigned int len;
+ unsigned int off;
+};
+
+/* logsrv_util.c */
+struct iolog_file;
+bool expand_buf(struct connection_buffer *buf, unsigned int needed);
+bool iolog_open_all(int dfd, const char *iolog_dir, struct iolog_file *iolog_files, const char *mode);
+bool iolog_seekto(int iolog_dir_fd, const char *iolog_path, struct iolog_file *iolog_files, struct timespec *elapsed_time, const struct timespec *target);
+
+
+#endif /* SUDO_LOGSRV_UTIL_H */
diff --git a/logsrvd/logsrvd.c b/logsrvd/logsrvd.c
new file mode 100644
index 0000000..66b84c2
--- /dev/null
+++ b/logsrvd/logsrvd.c
@@ -0,0 +1,2042 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 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.
+ */
+
+#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>
+#ifdef HAVE_GETOPT_LONG
+# include <getopt.h>
+# else
+# include "compat/getopt.h"
+#endif /* HAVE_GETOPT_LONG */
+
+#if defined(HAVE_OPENSSL)
+# include <openssl/ssl.h>
+# include <openssl/err.h>
+#endif
+
+#define NEED_INET_NTOP /* to expose sudo_inet_ntop in sudo_compat.h */
+
+#include "pathnames.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_json.h"
+#include "sudo_iolog.h"
+#include "sudo_queue.h"
+#include "sudo_rand.h"
+#include "sudo_util.h"
+
+#include "log_server.pb-c.h"
+#include "hostcheck.h"
+#include "logsrvd.h"
+
+#if defined(HAVE_OPENSSL)
+# define LOGSRVD_DEFAULT_CIPHER_LST12 "HIGH:!aNULL"
+# define LOGSRVD_DEFAULT_CIPHER_LST13 "TLS_AES_256_GCM_SHA384"
+#endif
+
+#ifndef O_NOFOLLOW
+# define O_NOFOLLOW 0
+#endif
+
+/*
+ * Sudo I/O audit server.
+ */
+static int logsrvd_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER;
+TAILQ_HEAD(connection_list, connection_closure);
+static struct connection_list connections = TAILQ_HEAD_INITIALIZER(connections);
+static struct listener_list listeners = TAILQ_HEAD_INITIALIZER(listeners);
+static const char server_id[] = "Sudo Audit Server " PACKAGE_VERSION;
+static const char *conf_file = _PATH_SUDO_LOGSRVD_CONF;
+static double random_drop;
+
+/* Server callback may redirect to client callback for TLS. */
+static void client_msg_cb(int fd, int what, void *v);
+
+/*
+ * Free a struct connection_closure container and its contents.
+ */
+static void
+connection_closure_free(struct connection_closure *closure)
+{
+ debug_decl(connection_closure_free, SUDO_DEBUG_UTIL);
+
+ if (closure != NULL) {
+ bool shutting_down = closure->state == SHUTDOWN;
+ struct sudo_event_base *evbase = closure->evbase;
+
+ TAILQ_REMOVE(&connections, closure, entries);
+#if defined(HAVE_OPENSSL)
+ if (closure->tls) {
+ SSL_shutdown(closure->ssl);
+ SSL_free(closure->ssl);
+ }
+#endif
+ close(closure->sock);
+ iolog_close_all(closure);
+ sudo_ev_free(closure->commit_ev);
+ sudo_ev_free(closure->read_ev);
+ sudo_ev_free(closure->write_ev);
+#if defined(HAVE_OPENSSL)
+ sudo_ev_free(closure->ssl_accept_ev);
+#endif
+ eventlog_free(closure->evlog);
+ free(closure->read_buf.data);
+ free(closure->write_buf.data);
+ free(closure);
+
+ if (shutting_down && TAILQ_EMPTY(&connections))
+ sudo_ev_loopbreak(evbase);
+ }
+
+ debug_return;
+}
+
+static bool
+fmt_server_message(struct connection_buffer *buf, ServerMessage *msg)
+{
+ uint32_t msg_len;
+ bool ret = false;
+ size_t len;
+ debug_decl(fmt_server_message, SUDO_DEBUG_UTIL);
+
+ if (buf->len != 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "pending write, unable to format ServerMessage");
+ debug_return_bool(false);
+ }
+
+ len = server_message__get_packed_size(msg);
+ if (len > MESSAGE_SIZE_MAX) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "server message too large: %zu", len);
+ goto done;
+ }
+
+ /* Wire message size is used for length encoding, precedes message. */
+ msg_len = htonl((uint32_t)len);
+ len += sizeof(msg_len);
+
+ /* Resize buffer as needed. */
+ if (len > buf->size) {
+ free(buf->data);
+ buf->size = sudo_pow2_roundup(len);
+ if ((buf->data = malloc(buf->size)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to malloc %u", buf->size);
+ buf->size = 0;
+ goto done;
+ }
+ }
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "size + server message %zu bytes", len);
+
+ memcpy(buf->data, &msg_len, sizeof(msg_len));
+ server_message__pack(msg, buf->data + sizeof(msg_len));
+ buf->len = len;
+ ret = true;
+
+done:
+ debug_return_bool(ret);
+}
+
+static bool
+fmt_hello_message(struct connection_buffer *buf, bool tls)
+{
+ ServerMessage msg = SERVER_MESSAGE__INIT;
+ ServerHello hello = SERVER_HELLO__INIT;
+ debug_decl(fmt_hello_message, SUDO_DEBUG_UTIL);
+
+ /* TODO: implement redirect and servers array. */
+ hello.server_id = (char *)server_id;
+ msg.u.hello = &hello;
+ msg.type_case = SERVER_MESSAGE__TYPE_HELLO;
+
+ debug_return_bool(fmt_server_message(buf, &msg));
+}
+
+static bool
+fmt_log_id_message(const char *id, struct connection_buffer *buf)
+{
+ ServerMessage msg = SERVER_MESSAGE__INIT;
+ debug_decl(fmt_log_id_message, SUDO_DEBUG_UTIL);
+
+ msg.u.log_id = (char *)id;
+ msg.type_case = SERVER_MESSAGE__TYPE_LOG_ID;
+
+ debug_return_bool(fmt_server_message(buf, &msg));
+}
+
+static bool
+fmt_error_message(const char *errstr, struct connection_buffer *buf)
+{
+ ServerMessage msg = SERVER_MESSAGE__INIT;
+ debug_decl(fmt_error_message, SUDO_DEBUG_UTIL);
+
+ msg.u.error = (char *)errstr;
+ msg.type_case = SERVER_MESSAGE__TYPE_ERROR;
+
+ debug_return_bool(fmt_server_message(buf, &msg));
+}
+
+struct logsrvd_info_closure {
+ InfoMessage **info_msgs;
+ size_t infolen;
+};
+
+static bool
+logsrvd_json_log_cb(struct json_container *json, void *v)
+{
+ struct logsrvd_info_closure *closure = v;
+ struct json_value json_value;
+ size_t idx;
+ debug_decl(logsrvd_json_log_cb, SUDO_DEBUG_UTIL);
+
+ for (idx = 0; idx < closure->infolen; idx++) {
+ InfoMessage *info = closure->info_msgs[idx];
+
+ switch (info->value_case) {
+ case INFO_MESSAGE__VALUE_NUMVAL:
+ json_value.type = JSON_NUMBER;
+ json_value.u.number = info->u.numval;
+ if (!sudo_json_add_value(json, info->key, &json_value))
+ goto bad;
+ break;
+ case INFO_MESSAGE__VALUE_STRVAL:
+ json_value.type = JSON_STRING;
+ json_value.u.string = info->u.strval;
+ if (!sudo_json_add_value(json, info->key, &json_value))
+ goto bad;
+ break;
+ case INFO_MESSAGE__VALUE_STRLISTVAL: {
+ InfoMessage__StringList *strlist = info->u.strlistval;
+ size_t n;
+
+ if (!sudo_json_open_array(json, info->key))
+ goto bad;
+ for (n = 0; n < strlist->n_strings; n++) {
+ json_value.type = JSON_STRING;
+ json_value.u.string = strlist->strings[n];
+ if (!sudo_json_add_value(json, NULL, &json_value))
+ goto bad;
+ }
+ if (!sudo_json_close_array(json))
+ goto bad;
+ break;
+ }
+ default:
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected value case %d", info->value_case);
+ goto bad;
+ }
+ }
+ debug_return_bool(true);
+bad:
+ debug_return_bool(false);
+}
+
+/*
+ * Parse an AcceptMessage
+ */
+static bool
+handle_accept(AcceptMessage *msg, struct connection_closure *closure)
+{
+ struct logsrvd_info_closure info = { msg->info_msgs, msg->n_info_msgs };
+ debug_decl(handle_accept, SUDO_DEBUG_UTIL);
+
+ if (closure->state != INITIAL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected state %d", closure->state);
+ closure->errstr = _("state machine error");
+ debug_return_bool(false);
+ }
+
+ /* Check that message is valid. */
+ if (msg->submit_time == NULL || msg->n_info_msgs == 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "invalid AcceptMessage, submit_time: %p, n_info_msgs: %zu",
+ msg->submit_time, msg->n_info_msgs);
+ closure->errstr = _("invalid AcceptMessage");
+ debug_return_bool(false);
+ }
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received AcceptMessage", __func__);
+
+ closure->evlog = evlog_new(msg->submit_time, msg->info_msgs,
+ msg->n_info_msgs);
+ if (closure->evlog == NULL) {
+ closure->errstr = _("error parsing AcceptMessage");
+ debug_return_bool(false);
+ }
+
+ /* Create I/O log info file and parent directories. */
+ if (msg->expect_iobufs) {
+ if (!iolog_init(msg, closure)) {
+ closure->errstr = _("error creating I/O log");
+ debug_return_bool(false);
+ }
+ closure->log_io = true;
+ }
+
+ if (!eventlog_accept(closure->evlog, 0, logsrvd_json_log_cb, &info)) {
+ closure->errstr = _("error logging accept event");
+ debug_return_bool(false);
+ }
+
+ if (msg->expect_iobufs) {
+ /* Send log ID to client for restarting connections. */
+ if (!fmt_log_id_message(closure->evlog->iolog_path, &closure->write_buf))
+ debug_return_bool(false);
+ if (sudo_ev_add(closure->evbase, closure->write_ev,
+ logsrvd_conf_get_sock_timeout(), false) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to add server write event");
+ debug_return_bool(false);
+ }
+ }
+
+ closure->state = RUNNING;
+ debug_return_bool(true);
+}
+
+/*
+ * Parse a RejectMessage
+ */
+static bool
+handle_reject(RejectMessage *msg, struct connection_closure *closure)
+{
+ struct logsrvd_info_closure info = { msg->info_msgs, msg->n_info_msgs };
+ debug_decl(handle_reject, SUDO_DEBUG_UTIL);
+
+ if (closure->state != INITIAL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected state %d", closure->state);
+ closure->errstr = _("state machine error");
+ debug_return_bool(false);
+ }
+
+ /* Check that message is valid. */
+ if (msg->submit_time == NULL || msg->n_info_msgs == 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "invalid RejectMessage, submit_time: %p, n_info_msgs: %zu",
+ msg->submit_time, msg->n_info_msgs);
+ closure->errstr = _("invalid RejectMessage");
+ debug_return_bool(false);
+ }
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received RejectMessage", __func__);
+
+ closure->evlog = evlog_new(msg->submit_time, msg->info_msgs,
+ msg->n_info_msgs);
+ if (closure->evlog == NULL) {
+ closure->errstr = _("error parsing RejectMessage");
+ debug_return_bool(false);
+ }
+
+ if (!eventlog_reject(closure->evlog, 0, msg->reason,
+ logsrvd_json_log_cb, &info)) {
+ closure->errstr = _("error logging reject event");
+ debug_return_bool(false);
+ }
+
+ closure->state = FINISHED;
+ debug_return_bool(true);
+}
+
+static bool
+handle_exit(ExitMessage *msg, struct connection_closure *closure)
+{
+ struct timespec tv = { 0, 0 };
+ mode_t mode;
+ debug_decl(handle_exit, SUDO_DEBUG_UTIL);
+
+ if (closure->state != RUNNING) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected state %d", closure->state);
+ closure->errstr = _("state machine error");
+ debug_return_bool(false);
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ExitMessage", __func__);
+
+ /* Sudo I/O logs don't store this info. */
+ if (msg->signal != NULL && msg->signal[0] != '\0') {
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "command was killed by SIG%s%s", msg->signal,
+ msg->dumped_core ? " (core dumped)" : "");
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "command exited with %d", msg->exit_value);
+ }
+
+ if (closure->log_io) {
+ /* No more data, command exited. */
+ closure->state = EXITED;
+ sudo_ev_del(closure->evbase, closure->read_ev);
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: elapsed time: %lld, %ld",
+ __func__, (long long)closure->elapsed_time.tv_sec,
+ closure->elapsed_time.tv_nsec);
+
+ /* Clear write bits from I/O timing file to indicate completion. */
+ mode = logsrvd_conf_iolog_mode();
+ CLR(mode, S_IWUSR|S_IWGRP|S_IWOTH);
+ if (fchmodat(closure->iolog_dir_fd, "timing", mode, 0) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to fchmodat timing file");
+ }
+
+ /* Schedule the final commit point event immediately. */
+ if (sudo_ev_add(closure->evbase, closure->commit_ev, &tv, false) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to add commit point event");
+ debug_return_bool(false);
+ }
+ } else {
+ /* Command exited, no I/O logs to flush. */
+ closure->state = FINISHED;
+ }
+
+ debug_return_bool(true);
+}
+
+static bool
+handle_restart(RestartMessage *msg, struct connection_closure *closure)
+{
+ debug_decl(handle_restart, SUDO_DEBUG_UTIL);
+
+ if (closure->state != INITIAL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected state %d", closure->state);
+ closure->errstr = _("state machine error");
+ debug_return_bool(false);
+ }
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received RestartMessage for %s",
+ __func__, msg->log_id);
+
+ if (!iolog_restart(msg, closure)) {
+ sudo_debug_printf(SUDO_DEBUG_WARN, "%s: unable to restart I/O log", __func__);
+ /* XXX - structured error message so client can send from beginning */
+ if (!fmt_error_message(closure->errstr, &closure->write_buf))
+ debug_return_bool(false);
+ sudo_ev_del(closure->evbase, closure->read_ev);
+ if (sudo_ev_add(closure->evbase, closure->write_ev,
+ logsrvd_conf_get_sock_timeout(), false) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to add server write event");
+ debug_return_bool(false);
+ }
+ closure->state = ERROR;
+ debug_return_bool(true);
+ }
+
+ closure->state = RUNNING;
+ debug_return_bool(true);
+}
+
+static bool
+handle_alert(AlertMessage *msg, struct connection_closure *closure)
+{
+ struct timespec alert_time;
+ debug_decl(handle_alert, SUDO_DEBUG_UTIL);
+
+ /* Check that message is valid. */
+ if (msg->alert_time == NULL || msg->reason == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "invalid AlertMessage, alert_time: %p, reason: %p",
+ msg->alert_time, msg->reason);
+ closure->errstr = _("invalid AlertMessage");
+ debug_return_bool(false);
+ }
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received AlertMessage", __func__);
+
+ if (msg->info_msgs != NULL && msg->n_info_msgs != 0) {
+ closure->evlog = evlog_new(NULL, msg->info_msgs, msg->n_info_msgs);
+ if (closure->evlog == NULL) {
+ closure->errstr = _("error parsing AlertMessage");
+ debug_return_bool(false);
+ }
+ }
+
+ alert_time.tv_sec = msg->alert_time->tv_sec;
+ alert_time.tv_nsec = msg->alert_time->tv_nsec;
+ if (!eventlog_alert(closure->evlog, 0, &alert_time, msg->reason, NULL)) {
+ closure->errstr = _("error logging alert event");
+ debug_return_bool(false);
+ }
+
+ debug_return_bool(true);
+}
+
+static bool
+handle_iobuf(int iofd, IoBuffer *msg, struct connection_closure *closure)
+{
+ debug_decl(handle_iobuf, SUDO_DEBUG_UTIL);
+
+ if (closure->state != RUNNING) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected state %d", closure->state);
+ closure->errstr = _("state machine error");
+ debug_return_bool(false);
+ }
+ if (!closure->log_io) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "not logging I/O");
+ closure->errstr = _("protocol error");
+ debug_return_bool(false);
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received IoBuffer", __func__);
+
+ /* Store IoBuffer in log. */
+ if (store_iobuf(iofd, msg, closure) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "failed to store IoBuffer");
+ closure->errstr = _("error writing IoBuffer");
+ debug_return_bool(false);
+ }
+
+ /* Random drop is a debugging tool to test client restart. */
+ if (random_drop > 0.0) {
+ double randval = arc4random() / (double)UINT32_MAX;
+ if (randval < random_drop) {
+ sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
+ "randomly dropping connection (%f < %f)", randval, random_drop);
+ debug_return_bool(false);
+ }
+ }
+
+ /* Schedule a commit point in 10 sec if one is not already pending. */
+ if (!ISSET(closure->commit_ev->flags, SUDO_EVQ_INSERTED)) {
+ struct timespec tv = { ACK_FREQUENCY, 0 };
+ if (sudo_ev_add(closure->evbase, closure->commit_ev, &tv, false) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to add commit point event");
+ debug_return_bool(false);
+ }
+ }
+
+ debug_return_bool(true);
+}
+
+static bool
+handle_winsize(ChangeWindowSize *msg, struct connection_closure *closure)
+{
+ debug_decl(handle_winsize, SUDO_DEBUG_UTIL);
+
+ if (closure->state != RUNNING) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected state %d", closure->state);
+ closure->errstr = _("state machine error");
+ debug_return_bool(false);
+ }
+ if (!closure->log_io) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "not logging I/O");
+ closure->errstr = _("protocol error");
+ debug_return_bool(false);
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ChangeWindowSize",
+ __func__);
+
+ /* Store new window size in log. */
+ if (store_winsize(msg, closure) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "failed to store ChangeWindowSize");
+ closure->errstr = _("error writing ChangeWindowSize");
+ debug_return_bool(false);
+ }
+
+ debug_return_bool(true);
+}
+
+static bool
+handle_suspend(CommandSuspend *msg, struct connection_closure *closure)
+{
+ debug_decl(handle_suspend, SUDO_DEBUG_UTIL);
+
+ if (closure->state != RUNNING) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected state %d", closure->state);
+ closure->errstr = _("state machine error");
+ debug_return_bool(false);
+ }
+ if (!closure->log_io) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "not logging I/O");
+ closure->errstr = _("protocol error");
+ debug_return_bool(false);
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received CommandSuspend",
+ __func__);
+
+ /* Store suspend signal in log. */
+ if (store_suspend(msg, closure) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "failed to store CommandSuspend");
+ closure->errstr = _("error writing CommandSuspend");
+ debug_return_bool(false);
+ }
+
+ debug_return_bool(true);
+}
+
+static bool
+handle_client_hello(ClientHello *msg, struct connection_closure *closure)
+{
+ debug_decl(handle_client_hello, SUDO_DEBUG_UTIL);
+
+ if (closure->state != INITIAL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected state %d", closure->state);
+ closure->errstr = _("state machine error");
+ debug_return_bool(false);
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ClientHello",
+ __func__);
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: client ID %s",
+ __func__, msg->client_id);
+
+ debug_return_bool(true);
+}
+
+static bool
+handle_client_message(uint8_t *buf, size_t len,
+ struct connection_closure *closure)
+{
+ ClientMessage *msg;
+ bool ret = false;
+ debug_decl(handle_client_message, SUDO_DEBUG_UTIL);
+
+ msg = client_message__unpack(NULL, len, buf);
+ if (msg == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to unpack ClientMessage size %zu", len);
+ debug_return_bool(false);
+ }
+
+ switch (msg->type_case) {
+ case CLIENT_MESSAGE__TYPE_ACCEPT_MSG:
+ ret = handle_accept(msg->u.accept_msg, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_REJECT_MSG:
+ ret = handle_reject(msg->u.reject_msg, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_EXIT_MSG:
+ ret = handle_exit(msg->u.exit_msg, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_RESTART_MSG:
+ ret = handle_restart(msg->u.restart_msg, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_ALERT_MSG:
+ ret = handle_alert(msg->u.alert_msg, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_TTYIN_BUF:
+ ret = handle_iobuf(IOFD_TTYIN, msg->u.ttyin_buf, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_TTYOUT_BUF:
+ ret = handle_iobuf(IOFD_TTYOUT, msg->u.ttyout_buf, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_STDIN_BUF:
+ ret = handle_iobuf(IOFD_STDIN, msg->u.stdin_buf, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_STDOUT_BUF:
+ ret = handle_iobuf(IOFD_STDOUT, msg->u.stdout_buf, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_STDERR_BUF:
+ ret = handle_iobuf(IOFD_STDERR, msg->u.stderr_buf, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_WINSIZE_EVENT:
+ ret = handle_winsize(msg->u.winsize_event, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_SUSPEND_EVENT:
+ ret = handle_suspend(msg->u.suspend_event, closure);
+ break;
+ case CLIENT_MESSAGE__TYPE_HELLO_MSG:
+ ret = handle_client_hello(msg->u.hello_msg, closure);
+ break;
+ default:
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected type_case value %d", msg->type_case);
+ closure->errstr = _("unrecognized ClientMessage type");
+ break;
+ }
+ client_message__free_unpacked(msg, NULL);
+
+ debug_return_bool(ret);
+}
+
+static void
+shutdown_cb(int unused, int what, void *v)
+{
+ struct sudo_event_base *base = v;
+ debug_decl(shutdown_cb, SUDO_DEBUG_UTIL);
+
+#if defined(HAVE_OPENSSL)
+ /* deallocate server's SSL context object */
+ struct logsrvd_tls_runtime *tls_runtime = logsrvd_get_tls_runtime();
+ if (tls_runtime != NULL) {
+ SSL_CTX_free(tls_runtime->ssl_ctx);
+ }
+#endif
+ sudo_ev_loopbreak(base);
+
+ debug_return;
+}
+
+/*
+ * Shut down active client connections if any, or exit immediately.
+ */
+static void
+server_shutdown(struct sudo_event_base *base)
+{
+ struct connection_closure *closure, *next;
+ struct sudo_event *ev;
+ struct timespec tv = { 0, 0 };
+ debug_decl(server_shutdown, SUDO_DEBUG_UTIL);
+
+ if (TAILQ_EMPTY(&connections)) {
+ sudo_ev_loopbreak(base);
+ debug_return;
+ }
+
+ TAILQ_FOREACH_SAFE(closure, &connections, entries, next) {
+ closure->state = SHUTDOWN;
+ sudo_ev_del(base, closure->read_ev);
+ if (closure->log_io) {
+ /* Schedule final commit point for the connection. */
+ if (sudo_ev_add(base, closure->commit_ev, &tv, false) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to add commit point event");
+ }
+ } else {
+ /* No commit point, close connection immediately. */
+ sudo_ev_del(closure->evbase, closure->write_ev);
+ connection_closure_free(closure);
+ }
+ }
+
+ if (!TAILQ_EMPTY(&connections)) {
+ /* We need a timed event to exit even if clients time out. */
+ ev = sudo_ev_alloc(-1, SUDO_EV_TIMEOUT, shutdown_cb, base);
+ if (ev != NULL) {
+ tv.tv_sec = SHUTDOWN_TIMEO;
+ if (sudo_ev_add(base, ev, &tv, false) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to add shutdown event");
+ }
+ }
+ }
+
+ debug_return;
+}
+
+/*
+ * Send a server message to the client.
+ */
+static void
+server_msg_cb(int fd, int what, void *v)
+{
+ struct connection_closure *closure = v;
+ struct connection_buffer *buf = &closure->write_buf;
+ ssize_t nwritten;
+ debug_decl(server_msg_cb, SUDO_DEBUG_UTIL);
+
+ /* For TLS we may need to write as part of SSL_read(). */
+ if (closure->read_instead_of_write) {
+ closure->read_instead_of_write = false;
+ /* Delete write event if it was only due to SSL_read(). */
+ if (closure->temporary_write_event) {
+ closure->temporary_write_event = false;
+ sudo_ev_del(closure->evbase, closure->write_ev);
+ }
+ client_msg_cb(fd, what, v);
+ debug_return;
+ }
+
+ if (what == SUDO_EV_TIMEOUT) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "Writing to client timed out");
+ goto finished;
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending %u bytes to client",
+ __func__, buf->len - buf->off);
+
+#if defined(HAVE_OPENSSL)
+ if (closure->tls) {
+ nwritten = SSL_write(closure->ssl, buf->data + buf->off,
+ buf->len - buf->off);
+ if (nwritten <= 0) {
+ int err = SSL_get_error(closure->ssl, nwritten);
+ switch (err) {
+ case SSL_ERROR_WANT_READ:
+ /* ssl wants to read, read event always active */
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_write returns SSL_ERROR_WANT_READ");
+ /* Redirect persistent read event to finish SSL_write() */
+ closure->write_instead_of_read = true;
+ debug_return;
+ case SSL_ERROR_WANT_WRITE:
+ /* ssl wants to write more, write event remains active */
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_write returns SSL_ERROR_WANT_WRITE");
+ debug_return;
+ case SSL_ERROR_SYSCALL:
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected error during SSL_write(): %d (%s)",
+ err, strerror(errno));
+ goto finished;
+ default:
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected error during SSL_write(): %d (%s)",
+ err, ERR_error_string(ERR_get_error(), NULL));
+ goto finished;
+ }
+ }
+ } else
+#endif
+ {
+ nwritten = send(fd, buf->data + buf->off, buf->len - buf->off, 0);
+ }
+
+ if (nwritten == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to send %u bytes", buf->len - buf->off);
+ goto finished;
+ }
+ buf->off += nwritten;
+
+ if (buf->off == buf->len) {
+ /* sent entire message */
+ sudo_debug_printf(SUDO_DEBUG_INFO,
+ "%s: finished sending %u bytes to client", __func__, buf->len);
+ buf->off = 0;
+ buf->len = 0;
+ sudo_ev_del(closure->evbase, closure->write_ev);
+ if (closure->state == FINISHED || closure->state == SHUTDOWN ||
+ closure->state == ERROR)
+ goto finished;
+ }
+ debug_return;
+
+finished:
+ connection_closure_free(closure);
+ debug_return;
+}
+
+/*
+ * Receive client message(s).
+ */
+static void
+client_msg_cb(int fd, int what, void *v)
+{
+ struct connection_closure *closure = v;
+ struct connection_buffer *buf = &closure->read_buf;
+ uint32_t msg_len;
+ ssize_t nread;
+ debug_decl(client_msg_cb, SUDO_DEBUG_UTIL);
+
+ /* For TLS we may need to read as part of SSL_write(). */
+ if (closure->write_instead_of_read) {
+ closure->write_instead_of_read = false;
+ server_msg_cb(fd, what, v);
+ debug_return;
+ }
+
+ if (what == SUDO_EV_TIMEOUT) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "Reading from client timed out");
+ goto finished;
+ }
+
+#if defined(HAVE_OPENSSL)
+ if (closure->tls) {
+ nread = SSL_read(closure->ssl, buf->data + buf->len, buf->size);
+ if (nread <= 0) {
+ int err = SSL_get_error(closure->ssl, nread);
+ switch (err) {
+ case SSL_ERROR_ZERO_RETURN:
+ /* ssl connection shutdown cleanly */
+ nread = 0;
+ break;
+ case SSL_ERROR_WANT_READ:
+ /* ssl wants to read more, read event is always active */
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_read returns SSL_ERROR_WANT_READ");
+ /* Read event is always active. */
+ debug_return;
+ case SSL_ERROR_WANT_WRITE:
+ /* ssl wants to write, schedule a write if not pending */
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_read returns SSL_ERROR_WANT_WRITE");
+ if (!sudo_ev_pending(closure->write_ev, SUDO_EV_WRITE, NULL)) {
+ /* Enable a temporary write event. */
+ if (sudo_ev_add(closure->evbase, closure->write_ev,
+ logsrvd_conf_get_sock_timeout(), false) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to add event to queue");
+ goto finished;
+ }
+ closure->temporary_write_event = true;
+ }
+ /* Redirect write event to finish SSL_read() */
+ closure->read_instead_of_write = true;
+ debug_return;
+ case SSL_ERROR_SYSCALL:
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected error during SSL_read(): %d (%s)",
+ err, strerror(errno));
+ goto finished;
+ default:
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected error during SSL_read(): %d (%s)",
+ err, ERR_error_string(ERR_get_error(), NULL));
+ goto finished;
+ }
+ }
+ } else
+#endif
+ {
+ nread = recv(fd, buf->data + buf->len, buf->size - buf->len, 0);
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received %zd bytes from client",
+ __func__, nread);
+ switch (nread) {
+ case -1:
+ if (errno == EAGAIN)
+ debug_return;
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to receive %u bytes", buf->size - buf->len);
+ goto finished;
+ case 0:
+ if (closure->state != FINISHED) {
+ sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
+ "unexpected EOF");
+ }
+ goto finished;
+ default:
+ break;
+ }
+ buf->len += nread;
+
+ while (buf->len - buf->off >= sizeof(msg_len)) {
+ /* Read wire message size (uint32_t in network byte order). */
+ memcpy(&msg_len, buf->data + buf->off, sizeof(msg_len));
+ msg_len = ntohl(msg_len);
+
+ if (msg_len > MESSAGE_SIZE_MAX) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "client message too large: %u", msg_len);
+ closure->errstr = _("client message too large");
+ goto send_error;
+ }
+
+ if (msg_len + sizeof(msg_len) > buf->len - buf->off) {
+ /* Incomplete message, we'll read the rest next time. */
+ if (!expand_buf(buf, msg_len + sizeof(msg_len)))
+ goto finished;
+ debug_return;
+ }
+
+ /* Parse ClientMessage, could be zero bytes. */
+ sudo_debug_printf(SUDO_DEBUG_INFO,
+ "%s: parsing ClientMessage, size %u", __func__, msg_len);
+ buf->off += sizeof(msg_len);
+ if (!handle_client_message(buf->data + buf->off, msg_len, closure)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to parse ClientMessage, size %u", msg_len);
+ goto send_error;
+ }
+ buf->off += msg_len;
+ }
+ buf->len -= buf->off;
+ buf->off = 0;
+
+ if (closure->state == FINISHED)
+ goto finished;
+
+ debug_return;
+send_error:
+ if (closure->errstr == NULL)
+ goto finished;
+ if (fmt_error_message(closure->errstr, &closure->write_buf)) {
+ sudo_ev_del(closure->evbase, closure->read_ev);
+ if (sudo_ev_add(closure->evbase, closure->write_ev,
+ logsrvd_conf_get_sock_timeout(), false) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to add server write event");
+ }
+ }
+finished:
+ connection_closure_free(closure);
+ debug_return;
+}
+
+/*
+ * Format and schedule a commit_point message.
+ */
+static void
+server_commit_cb(int unused, int what, void *v)
+{
+ ServerMessage msg = SERVER_MESSAGE__INIT;
+ TimeSpec commit_point = TIME_SPEC__INIT;
+ struct connection_closure *closure = v;
+
+ debug_decl(server_commit_cb, SUDO_DEBUG_UTIL);
+
+ /* Send the client an acknowledgement of what has been committed to disk. */
+ commit_point.tv_sec = closure->elapsed_time.tv_sec;
+ commit_point.tv_nsec = closure->elapsed_time.tv_nsec;
+ msg.u.commit_point = &commit_point;
+ msg.type_case = SERVER_MESSAGE__TYPE_COMMIT_POINT;
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending commit point [%lld, %ld]",
+ __func__, (long long)closure->elapsed_time.tv_sec,
+ closure->elapsed_time.tv_nsec);
+
+ /* XXX - assumes no other server message pending, use a queue instead? */
+ if (!fmt_server_message(&closure->write_buf, &msg)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to format ServerMessage (commit point)");
+ goto bad;
+ }
+ if (sudo_ev_add(closure->evbase, closure->write_ev,
+ logsrvd_conf_get_sock_timeout(), false) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to add server write event");
+ goto bad;
+ }
+
+ if (closure->state == EXITED)
+ closure->state = FINISHED;
+ debug_return;
+bad:
+ connection_closure_free(closure);
+ debug_return;
+}
+
+/*
+ * Begin the sudo logserver protocol.
+ * When we enter the event loop the ServerHello message will be written
+ * and any pending ClientMessage will be read.
+ */
+static bool
+start_protocol(struct connection_closure *closure)
+{
+ const struct timespec *timeout = logsrvd_conf_get_sock_timeout();
+ debug_decl(start_protocol, SUDO_DEBUG_UTIL);
+
+ if (!fmt_hello_message(&closure->write_buf, closure->tls))
+ debug_return_bool(false);
+
+ if (sudo_ev_add(closure->evbase, closure->write_ev, timeout, false) == -1)
+ debug_return_bool(false);
+
+ /* No read timeout, client messages may happen at arbitrary times. */
+ if (sudo_ev_add(closure->evbase, closure->read_ev, NULL, false) == -1)
+ debug_return_bool(false);
+
+ debug_return_bool(true);
+}
+
+#if defined(HAVE_OPENSSL)
+static int
+verify_peer_identity(int preverify_ok, X509_STORE_CTX *ctx)
+{
+ HostnameValidationResult result;
+ struct connection_closure *closure;
+ SSL *ssl;
+ X509 *current_cert;
+ X509 *peer_cert;
+ debug_decl(verify_peer_identity, SUDO_DEBUG_UTIL);
+
+ /* if pre-verification of the cert failed, just propagate that result back */
+ if (preverify_ok != 1) {
+ debug_return_int(0);
+ }
+
+ /* since this callback is called for each cert in the chain,
+ * check that current cert is the peer's certificate
+ */
+ current_cert = X509_STORE_CTX_get_current_cert(ctx);
+ peer_cert = X509_STORE_CTX_get0_cert(ctx);
+
+ if (current_cert != peer_cert) {
+ debug_return_int(1);
+ }
+
+ /* read out the attached object (closure) from the ssl connection object */
+ ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
+ closure = (struct connection_closure *)SSL_get_ex_data(ssl, 1);
+
+ result = validate_hostname(peer_cert, closure->ipaddr, closure->ipaddr, 1);
+
+ switch(result)
+ {
+ case MatchFound:
+ debug_return_int(1);
+ default:
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "hostname validation failed");
+ debug_return_int(0);
+ }
+}
+
+static bool
+verify_server_cert(SSL_CTX *ctx, const struct logsrvd_tls_config *tls_config)
+{
+#ifdef HAVE_SSL_CTX_GET0_CERTIFICATE
+ bool ret = false;
+ X509_STORE_CTX *store_ctx = NULL;
+ X509_STORE *ca_store;
+ STACK_OF(X509) *chain_certs;
+ X509 *x509;
+ debug_decl(verify_server_cert, SUDO_DEBUG_UTIL);
+
+ if ((x509 = SSL_CTX_get0_certificate(ctx)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to get X509 object from SSL_CTX: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto exit;
+ }
+
+ if ((store_ctx = X509_STORE_CTX_new()) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to allocate X509_STORE_CTX object: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto exit;
+ }
+
+ if (!SSL_CTX_get0_chain_certs(ctx, &chain_certs)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to get chain certs: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto exit;
+ }
+
+ if ((ca_store = SSL_CTX_get_cert_store(ctx)) != NULL)
+ X509_STORE_set_flags(ca_store, X509_V_FLAG_X509_STRICT);
+
+ if (!X509_STORE_CTX_init(store_ctx, ca_store, x509, chain_certs)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to initialize X509_STORE_CTX object: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto exit;
+ }
+
+ if (X509_verify_cert(store_ctx) <= 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to verify cert %s: %s", tls_config->cert_path,
+ ERR_error_string(ERR_get_error(), NULL));
+ goto exit;
+ }
+
+ ret = true;
+exit:
+ X509_STORE_CTX_free(store_ctx);
+
+ debug_return_bool(ret);
+#else
+ /* TODO: verify server cert with old OpenSSL */
+ return true;
+#endif /* HAVE_SSL_CTX_GET0_CERTIFICATE */
+}
+
+static bool
+init_tls_ciphersuites(SSL_CTX *ctx, const struct logsrvd_tls_config *tls_config)
+{
+ const char *errstr;
+ int success = 0;
+ debug_decl(init_tls_ciphersuites, SUDO_DEBUG_UTIL);
+
+ if (tls_config->ciphers_v12) {
+ /* try to set TLS v1.2 ciphersuite list from config if given */
+ success = SSL_CTX_set_cipher_list(ctx, tls_config->ciphers_v12);
+ if (success) {
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "TLS 1.2 ciphersuite list set to %s", tls_config->ciphers_v12);
+ } else {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("unable to set TLS 1.2 ciphersuite to %s: %s"),
+ tls_config->ciphers_v12, errstr);
+ }
+ }
+ if (!success) {
+ /* fallback to default ciphersuites for TLS v1.2 */
+ if (SSL_CTX_set_cipher_list(ctx, LOGSRVD_DEFAULT_CIPHER_LST12) <= 0) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("unable to set TLS 1.2 ciphersuite to %s: %s"),
+ LOGSRVD_DEFAULT_CIPHER_LST12, errstr);
+ debug_return_bool(false);
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "TLS v1.2 ciphersuite list set to %s (default)",
+ LOGSRVD_DEFAULT_CIPHER_LST12);
+ }
+ }
+
+# if defined(HAVE_SSL_CTX_SET_CIPHERSUITES)
+ success = 0;
+ if (tls_config->ciphers_v13) {
+ /* try to set TLSv1.3 ciphersuite list from config */
+ success = SSL_CTX_set_ciphersuites(ctx, tls_config->ciphers_v13);
+ if (success) {
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "TLS v1.3 ciphersuite list set to %s", tls_config->ciphers_v13);
+ } else {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("unable to set TLS 1.3 ciphersuite to %s: %s"),
+ tls_config->ciphers_v13, errstr);
+ }
+ }
+ if (!success) {
+ /* fallback to default ciphersuites for TLS v1.3 */
+ if (SSL_CTX_set_ciphersuites(ctx, LOGSRVD_DEFAULT_CIPHER_LST13) <= 0) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("unable to set TLS 1.3 ciphersuite to %s: %s"),
+ LOGSRVD_DEFAULT_CIPHER_LST13, errstr);
+ debug_return_bool(false);
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "TLS v1.3 ciphersuite list set to %s (default)",
+ LOGSRVD_DEFAULT_CIPHER_LST13);
+ }
+ }
+# endif
+
+ debug_return_bool(true);
+}
+
+/*
+ * Calls series of openssl initialization functions in order to
+ * be able to establish configured network connections over TLS
+ */
+static bool
+init_tls_server_context(void)
+{
+ const SSL_METHOD *method;
+ FILE *dhparam_file = NULL;
+ SSL_CTX *ctx = NULL;
+ struct logsrvd_tls_runtime *tls_runtime = logsrvd_get_tls_runtime();
+ const struct logsrvd_tls_config *tls_config = logsrvd_get_tls_config();
+ bool ca_bundle_required = tls_config->verify | tls_config->check_peer;
+ const char *errstr;
+ debug_decl(init_tls_server_context, SUDO_DEBUG_UTIL);
+
+ SSL_library_init();
+ OpenSSL_add_all_algorithms();
+ SSL_load_error_strings();
+
+ if ((method = TLS_server_method()) == NULL) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("unable to get TLS server method: %s"), errstr);
+ goto bad;
+ }
+ if ((ctx = SSL_CTX_new(method)) == NULL) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("unable to create TLS context: %s"), errstr);
+ goto bad;
+ }
+
+ if (SSL_CTX_use_certificate_chain_file(ctx, tls_config->cert_path) <= 0) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("%s: %s"), tls_config->cert_path, errstr);
+ sudo_warnx(U_("unable to load certificate %s"), tls_config->cert_path);
+ goto bad;
+ }
+
+ /* if server or client authentication is required, CA bundle file has to be prepared */
+ if (ca_bundle_required) {
+ if (tls_config->cacert_path != NULL) {
+ STACK_OF(X509_NAME) *cacerts =
+ SSL_load_client_CA_file(tls_config->cacert_path);
+
+ if (cacerts == NULL) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("%s: %s"), tls_config->cacert_path, errstr);
+ sudo_warnx(U_("unable to load certificate authority bundle %s"),
+ tls_config->cacert_path);
+ goto bad;
+ }
+ SSL_CTX_set_client_CA_list(ctx, cacerts);
+
+ /* set the location of the CA bundle file for verification */
+ if (SSL_CTX_load_verify_locations(ctx, tls_config->cacert_path, NULL) <= 0) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx("SSL_CTX_load_verify_locations: %s", errstr);
+ goto bad;
+ }
+ } else {
+ if (!SSL_CTX_set_default_verify_paths(ctx)) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx("SSL_CTX_set_default_verify_paths: %s", errstr);
+ goto bad;
+ }
+ }
+
+ /* only verify server cert if it is set in the configuration */
+ if (tls_config->verify) {
+ if (!verify_server_cert(ctx, tls_config))
+ goto bad;
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "skipping server cert check");
+ }
+ }
+
+ /* if peer authentication is enabled, verify client cert during TLS handshake
+ * The last parameter is a callback, where identity validation (hostname/ip)
+ * will be performed, because it is not automatically done by openssl.
+ */
+ if (tls_config->check_peer) {
+ SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_peer_identity);
+ }
+
+ /* if private key file was not set, assume that the cert file contains the private key */
+ char* pkey = (tls_config->pkey_path == NULL ? tls_config->cert_path : tls_config->pkey_path);
+
+ if (!SSL_CTX_use_PrivateKey_file(ctx, pkey, SSL_FILETYPE_PEM) ||
+ !SSL_CTX_check_private_key(ctx)) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("%s: %s"), pkey, errstr);
+ sudo_warnx(U_("unable to load private key %s"), pkey);
+ goto bad;
+ }
+
+ /* initialize TLSv1.2 and TLSv1.3 ciphersuites */
+ if (!init_tls_ciphersuites(ctx, tls_config)) {
+ goto bad;
+ }
+
+ /* try to load and set diffie-hellman parameters */
+ if (tls_config->dhparams_path != NULL)
+ dhparam_file = fopen(tls_config->dhparams_path, "r");
+ if (dhparam_file != NULL) {
+ DH *dhparams = PEM_read_DHparams(dhparam_file, NULL, NULL, NULL);
+ if (dhparams != NULL) {
+ if (!SSL_CTX_set_tmp_dh(ctx, dhparams)) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("unable to set diffie-hellman parameters: %s"),
+ errstr);
+ DH_free(dhparams);
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "diffie-hellman parameters are loaded");
+ }
+ } else {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("unable to set diffie-hellman parameters: %s"),
+ errstr);
+ }
+ fclose(dhparam_file);
+ } else {
+ sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
+ "dhparam file not found, will use default parameters");
+ }
+
+ /* audit server supports TLS version 1.2 or higher */
+#ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION
+ if (!SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION)) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("unable to set minimum protocol version to TLS 1.2: %s"),
+ errstr);
+ goto bad;
+ }
+#else
+ SSL_CTX_set_options(ctx,
+ SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3|SSL_OP_NO_TLSv1|SSL_OP_NO_TLSv1_1);
+#endif
+
+ tls_runtime->ssl_ctx = ctx;
+
+ debug_return_bool(true);
+
+bad:
+ SSL_CTX_free(ctx);
+
+ debug_return_bool(false);
+}
+
+static void
+tls_handshake_cb(int fd, int what, void *v)
+{
+ struct connection_closure *closure = v;
+ debug_decl(tls_handshake_cb, SUDO_DEBUG_UTIL);
+
+ if (what == SUDO_EV_TIMEOUT) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "TLS handshake timed out");
+ goto bad;
+ }
+
+ int handshake_status = SSL_accept(closure->ssl);
+ int err = SSL_ERROR_NONE;
+ switch (err = SSL_get_error(closure->ssl, handshake_status)) {
+ case SSL_ERROR_NONE:
+ /* ssl handshake was successful */
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "TLS handshake successful");
+ break;
+ case SSL_ERROR_WANT_READ:
+ /* ssl handshake is ongoing, re-schedule the SSL_accept() call */
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_accept returns SSL_ERROR_WANT_READ");
+ if (what != SUDO_EV_READ) {
+ if (sudo_ev_set(closure->ssl_accept_ev, closure->sock,
+ SUDO_EV_READ, tls_handshake_cb, closure) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to set ssl_accept_ev to SUDO_EV_READ");
+ goto bad;
+ }
+ }
+ if (sudo_ev_add(closure->evbase, closure->ssl_accept_ev,
+ logsrvd_conf_get_sock_timeout(), false) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to add ssl_accept_ev to queue");
+ goto bad;
+ }
+ debug_return;
+ case SSL_ERROR_WANT_WRITE:
+ /* ssl handshake is ongoing, re-schedule the SSL_accept() call */
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_accept returns SSL_ERROR_WANT_WRITE");
+ if (what != SUDO_EV_WRITE) {
+ if (sudo_ev_set(closure->ssl_accept_ev, closure->sock,
+ SUDO_EV_WRITE, tls_handshake_cb, closure) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to set ssl_accept_ev to SUDO_EV_WRITE");
+ goto bad;
+ }
+ }
+ if (sudo_ev_add(closure->evbase, closure->ssl_accept_ev,
+ logsrvd_conf_get_sock_timeout(), false) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to add ssl_accept_ev to queue");
+ goto bad;
+ }
+ debug_return;
+ case SSL_ERROR_SYSCALL:
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected error during TLS handshake: %d (%s)",
+ err, strerror(errno));
+ goto bad;
+ default:
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected error during TLS handshake: %d (%s)",
+ err, ERR_error_string(ERR_get_error(), NULL));
+ goto bad;
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "TLS version: %s, negotiated cipher suite: %s",
+ SSL_get_version(closure->ssl),
+ SSL_get_cipher(closure->ssl));
+
+ /* Start the actual protocol now that the TLS handshake is complete. */
+ if (!start_protocol(closure))
+ goto bad;
+
+ debug_return;
+bad:
+ connection_closure_free(closure);
+ debug_return;
+}
+#endif /* HAVE_OPENSSL */
+
+/*
+ * Allocate a new connection closure.
+ */
+static struct connection_closure *
+connection_closure_alloc(int sock, bool tls, struct sudo_event_base *base)
+{
+ struct connection_closure *closure;
+ debug_decl(connection_closure_alloc, SUDO_DEBUG_UTIL);
+
+ if ((closure = calloc(1, sizeof(*closure))) == NULL)
+ debug_return_ptr(NULL);
+
+ closure->iolog_dir_fd = -1;
+ closure->sock = sock;
+ closure->tls = tls;
+ closure->evbase = base;
+
+ TAILQ_INSERT_TAIL(&connections, closure, entries);
+
+ closure->read_buf.size = 64 * 1024;
+ closure->read_buf.data = malloc(closure->read_buf.size);
+ if (closure->read_buf.data == NULL)
+ goto bad;
+
+ closure->commit_ev = sudo_ev_alloc(-1, SUDO_EV_TIMEOUT,
+ server_commit_cb, closure);
+ if (closure->commit_ev == NULL)
+ goto bad;
+
+ closure->read_ev = sudo_ev_alloc(sock, SUDO_EV_READ|SUDO_EV_PERSIST,
+ client_msg_cb, closure);
+ if (closure->read_ev == NULL)
+ goto bad;
+
+ closure->write_ev = sudo_ev_alloc(sock, SUDO_EV_WRITE|SUDO_EV_PERSIST,
+ server_msg_cb, closure);
+ if (closure->write_ev == NULL)
+ goto bad;
+
+#if defined(HAVE_OPENSSL)
+ if (tls) {
+ closure->ssl_accept_ev = sudo_ev_alloc(sock, SUDO_EV_READ,
+ tls_handshake_cb, closure);
+ if (closure->ssl_accept_ev == NULL)
+ goto bad;
+ }
+#endif
+
+ debug_return_ptr(closure);
+bad:
+ connection_closure_free(closure);
+ debug_return_ptr(NULL);
+}
+
+/*
+ * New connection.
+ * Allocate a connection closure and optionally perform TLS handshake.
+ */
+static bool
+new_connection(int sock, bool tls, const struct sockaddr *sa,
+ struct sudo_event_base *evbase)
+{
+ struct connection_closure *closure;
+ debug_decl(new_connection, SUDO_DEBUG_UTIL);
+
+ if ((closure = connection_closure_alloc(sock, tls, evbase)) == NULL)
+ goto bad;
+
+ /* store the peer's IP address in the closure object */
+ if (sa->sa_family == AF_INET) {
+ struct sockaddr_in *sin = (struct sockaddr_in *)sa;
+ inet_ntop(AF_INET, &sin->sin_addr, closure->ipaddr,
+ sizeof(closure->ipaddr));
+#if defined(HAVE_STRUCT_IN6_ADDR)
+ } else if (sa->sa_family == AF_INET6) {
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)sa;
+ inet_ntop(AF_INET6, &sin6->sin6_addr, closure->ipaddr,
+ sizeof(closure->ipaddr));
+#endif /* HAVE_STRUCT_IN6_ADDR */
+ } else {
+ sudo_fatal("%s", U_("unable to get remote IP addr"));
+ goto bad;
+ }
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "connection from %s", closure->ipaddr);
+
+#if defined(HAVE_OPENSSL)
+ /* If TLS is enabled, perform the TLS handshake first. */
+ if (tls) {
+ /* Create the SSL object for the closure and attach it to the socket */
+ if ((closure->ssl = SSL_new(logsrvd_get_tls_runtime()->ssl_ctx)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to create new ssl object: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto bad;
+ }
+
+ if (SSL_set_fd(closure->ssl, closure->sock) != 1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to set fd for TLS: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto bad;
+ }
+
+ /* attach the closure object to the ssl connection object to make it
+ available during hostname matching
+ */
+ if (SSL_set_ex_data(closure->ssl, 1, closure) <= 0) {
+ sudo_warnx(U_("Unable to attach user data to the ssl object: %s"),
+ ERR_error_string(ERR_get_error(), NULL));
+ goto bad;
+ }
+
+ /* Enable SSL_accept to begin handshake with client. */
+ if (sudo_ev_add(evbase, closure->ssl_accept_ev,
+ logsrvd_conf_get_sock_timeout(), false) == -1) {
+ sudo_fatal("%s", U_("unable to add event to queue"));
+ goto bad;
+ }
+ }
+#endif
+ /* If no TLS handshake, start the protocol immediately. */
+ if (!tls) {
+ if (!start_protocol(closure))
+ goto bad;
+ }
+
+ debug_return_bool(true);
+bad:
+ connection_closure_free(closure);
+ debug_return_bool(false);
+}
+
+static int
+create_listener(struct listen_address *addr)
+{
+ int flags, on, sock;
+ const char *family = "inet4";
+ debug_decl(create_listener, SUDO_DEBUG_UTIL);
+
+ if ((sock = socket(addr->sa_un.sa.sa_family, SOCK_STREAM, 0)) == -1) {
+ sudo_warn("socket");
+ goto bad;
+ }
+ on = 1;
+#ifdef HAVE_STRUCT_IN6_ADDR
+ if (addr->sa_un.sa.sa_family == AF_INET6) {
+ family = "inet6";
+# ifdef IPV6_V6ONLY
+ /* Disable IPv4-mapped IPv6 addresses. */
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) == -1)
+ sudo_warn("IPV6_V6ONLY");
+# endif
+ }
+#endif
+ if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
+ sudo_warn("SO_REUSEADDR");
+ if (bind(sock, &addr->sa_un.sa, addr->sa_size) == -1) {
+ /* TODO: only warn once for IPv4 and IPv6 or disambiguate */
+ sudo_warn("%s (%s)", addr->sa_str, family);
+ goto bad;
+ }
+ if (listen(sock, SOMAXCONN) == -1) {
+ sudo_warn("listen");
+ goto bad;
+ }
+ flags = fcntl(sock, F_GETFL, 0);
+ if (flags == -1 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) {
+ sudo_warn("fcntl(O_NONBLOCK)");
+ goto bad;
+ }
+ sudo_debug_printf(SUDO_DEBUG_INFO, "listening on %s (%s)", addr->sa_str,
+ family);
+
+ debug_return_int(sock);
+bad:
+ if (sock != -1)
+ close(sock);
+ debug_return_int(-1);
+}
+
+static void
+listener_cb(int fd, int what, void *v)
+{
+ struct listener *l = v;
+ struct sudo_event_base *evbase = sudo_ev_get_base(l->ev);
+ union sockaddr_union s_un;
+ socklen_t salen = sizeof(s_un);
+ int sock;
+ debug_decl(listener_cb, SUDO_DEBUG_UTIL);
+
+ sock = accept(fd, &s_un.sa, &salen);
+ if (sock != -1) {
+ /* set keepalive socket option on socket returned by accept */
+ if (logsrvd_conf_tcp_keepalive()) {
+ int keepalive = 1;
+ if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive,
+ sizeof(keepalive)) == -1) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to set SO_KEEPALIVE option");
+ }
+ }
+ if (!new_connection(sock, l->tls, &s_un.sa, evbase)) {
+ /* TODO: pause accepting on ENOMEM */
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to start new connection");
+ }
+ } else {
+ if (errno != EAGAIN) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to accept new connection");
+ }
+ /* TODO: pause accepting on ENFILE and EMFILE */
+ }
+
+ debug_return;
+}
+
+static bool
+register_listener(struct listen_address *addr, struct sudo_event_base *evbase)
+{
+ struct listener *l;
+ int sock;
+ debug_decl(register_listener, SUDO_DEBUG_UTIL);
+
+ sock = create_listener(addr);
+ if (sock == -1)
+ debug_return_bool(false);
+
+ /* TODO: make non-fatal */
+ if ((l = malloc(sizeof(*l))) == NULL)
+ sudo_fatal(NULL);
+ l->sock = sock;
+ l->tls = addr->tls;
+ l->ev = sudo_ev_alloc(sock, SUDO_EV_READ|SUDO_EV_PERSIST, listener_cb, l);
+ if (l->ev == NULL)
+ sudo_fatal(NULL);
+ if (sudo_ev_add(evbase, l->ev, NULL, false) == -1)
+ sudo_fatal("%s", U_("unable to add event to queue"));
+ TAILQ_INSERT_TAIL(&listeners, l, entries);
+
+ debug_return_bool(true);
+}
+
+/*
+ * Register listeners and init the TLS context.
+ */
+static bool
+server_setup(struct sudo_event_base *base)
+{
+ struct listen_address *addr;
+ struct listener *l;
+ int nlisteners = 0;
+ bool ret, config_tls = false;
+ debug_decl(server_setup, SUDO_DEBUG_UTIL);
+
+ /* Free old listeners (if any) and register new ones. */
+ while ((l = TAILQ_FIRST(&listeners)) != NULL) {
+ TAILQ_REMOVE(&listeners, l, entries);
+ sudo_ev_free(l->ev);
+ close(l->sock);
+ free(l);
+ }
+ TAILQ_FOREACH(addr, logsrvd_conf_listen_address(), entries) {
+ nlisteners += register_listener(addr, base);
+ if (addr->tls)
+ config_tls = true;
+ }
+ ret = nlisteners > 0;
+
+ if (ret && config_tls) {
+#if defined(HAVE_OPENSSL)
+ if (!init_tls_server_context())
+ ret = false;
+#endif
+ }
+
+ debug_return_bool(ret);
+}
+
+/*
+ * Reload config and re-initialize listeners and TLS context.
+ */
+static void
+server_reload(struct sudo_event_base *base)
+{
+ debug_decl(server_reload, SUDO_DEBUG_UTIL);
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "reloading server config");
+ if (logsrvd_conf_read(conf_file)) {
+ /* Re-initialize listeners and TLS context. */
+ if (!server_setup(base))
+ sudo_fatalx("%s", U_("unable setup listen socket"));
+
+ /* Re-read sudo.conf and re-initialize debugging. */
+ sudo_debug_deregister(logsrvd_debug_instance);
+ logsrvd_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER;
+ if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) != -1) {
+ logsrvd_debug_instance = sudo_debug_register(getprogname(),
+ NULL, NULL, sudo_conf_debug_files(getprogname()));
+ }
+ }
+
+ debug_return;
+}
+
+static void
+signal_cb(int signo, int what, void *v)
+{
+ struct sudo_event_base *base = v;
+ debug_decl(signal_cb, SUDO_DEBUG_UTIL);
+
+ switch (signo) {
+ case SIGHUP:
+ server_reload(base);
+ break;
+ case SIGINT:
+ case SIGTERM:
+ /* Shut down active connections. */
+ server_shutdown(base);
+ break;
+ default:
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unexpected signal %d", signo);
+ break;
+ }
+
+ debug_return;
+}
+
+static void
+register_signal(int signo, struct sudo_event_base *base)
+{
+ struct sudo_event *ev;
+ debug_decl(register_signal, SUDO_DEBUG_UTIL);
+
+ ev = sudo_ev_alloc(signo, SUDO_EV_SIGNAL, signal_cb, base);
+ if (ev == NULL)
+ sudo_fatal(NULL);
+ if (sudo_ev_add(base, ev, NULL, false) == -1)
+ sudo_fatal("%s", U_("unable to add event to queue"));
+
+ debug_return;
+}
+
+static void
+logsrvd_cleanup(void)
+{
+ /* TODO: cleanup like on signal */
+ return;
+}
+
+/*
+ * Write the process ID into a file, typically /var/run/sudo/sudo_logsrvd.pid.
+ * If the parent directory doesn't exist, it will be created.
+ */
+static void
+write_pidfile(void)
+{
+ FILE *fp;
+ int fd;
+ bool success;
+ char *pid_file = (char *)logsrvd_conf_pid_file();
+ debug_decl(write_pidfile, SUDO_DEBUG_UTIL);
+
+ if (pid_file == NULL)
+ debug_return;
+
+ /* sudo_mkdir_parents() modifies the path but restores it before return. */
+ success = sudo_mkdir_parents(pid_file, ROOT_UID, ROOT_GID,
+ S_IRWXU|S_IXGRP|S_IXOTH, false);
+ if (success) {
+ fd = open(pid_file, O_WRONLY|O_CREAT|O_NOFOLLOW, 0644);
+ if (fd == -1 || (fp = fdopen(fd, "w")) == NULL) {
+ sudo_warn("%s", pid_file);
+ if (fd != -1)
+ close(fd);
+ } else {
+ fprintf(fp, "%u\n", (unsigned int)getpid());
+ fflush(fp);
+ if (ferror(fp))
+ sudo_warn("%s", pid_file);
+ fclose(fp);
+ }
+ }
+ debug_return;
+}
+
+/*
+ * Fork, detach from the terminal and write pid file unless nofork set.
+ */
+static void
+daemonize(bool nofork)
+{
+ int fd;
+ debug_decl(daemonize, SUDO_DEBUG_UTIL);
+
+ if (!nofork) {
+ switch (fork()) {
+ case -1:
+ sudo_fatal("fork");
+ case 0:
+ /* child */
+ break;
+ default:
+ /* parent, exit */
+ _exit(EXIT_SUCCESS);
+ }
+
+ /* detach from terminal and write pid file. */
+ if (setsid() == -1)
+ sudo_fatal("setsid");
+ write_pidfile();
+ }
+
+ if (chdir("/") == -1)
+ sudo_warn("chdir(\"/\")");
+ if ((fd = open(_PATH_DEVNULL, O_RDWR)) != -1) {
+ (void) dup2(fd, STDIN_FILENO);
+ (void) dup2(fd, STDOUT_FILENO);
+ (void) dup2(fd, STDERR_FILENO);
+ if (fd > STDERR_FILENO)
+ (void) close(fd);
+ }
+
+ debug_return;
+}
+
+static void
+usage(bool fatal)
+{
+ fprintf(stderr, "usage: %s [-n] [-f conf_file] [-R percentage]\n",
+ getprogname());
+ if (fatal)
+ exit(EXIT_FAILURE);
+}
+
+static void
+help(void)
+{
+ (void)printf(_("%s - send sudo I/O log to remote server\n\n"),
+ getprogname());
+ usage(false);
+ (void)puts(_("\nOptions:\n"
+ " -f, --file path to configuration file\n"
+ " -h --help display help message and exit\n"
+ " -n, --no-fork do not fork, run in the foreground\n"
+ " -R, --random-drop percent chance connections will drop\n"
+ " -V, --version display version information and exit\n"));
+ exit(EXIT_SUCCESS);
+}
+
+static const char short_opts[] = "f:hnR:V";
+static struct option long_opts[] = {
+ { "file", required_argument, NULL, 'f' },
+ { "help", no_argument, NULL, 'h' },
+ { "no-fork", no_argument, NULL, 'n' },
+ { "random-drop", required_argument, NULL, 'R' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL, no_argument, NULL, 0 },
+};
+
+sudo_dso_public int main(int argc, char *argv[]);
+
+int
+main(int argc, char *argv[])
+{
+ struct sudo_event_base *evbase;
+ bool nofork = false;
+ char *ep;
+ int ch;
+ debug_decl_vars(main, SUDO_DEBUG_MAIN);
+
+#if defined(SUDO_DEVEL) && defined(__OpenBSD__)
+ {
+ extern char *malloc_options;
+ malloc_options = "S";
+ }
+#endif
+
+ initprogname(argc > 0 ? argv[0] : "sudo_logsrvd");
+ setlocale(LC_ALL, "");
+ bindtextdomain("sudo", LOCALEDIR); /* XXX - add logsrvd domain */
+ textdomain("sudo");
+
+ /* Register fatal/fatalx callback. */
+ sudo_fatal_callback_register(logsrvd_cleanup);
+
+ /* Read sudo.conf and initialize the debug subsystem. */
+ if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1)
+ exit(EXIT_FAILURE);
+ logsrvd_debug_instance = sudo_debug_register(getprogname(), NULL, NULL,
+ sudo_conf_debug_files(getprogname()));
+
+ if (protobuf_c_version_number() < 1003000)
+ sudo_fatalx("%s", U_("Protobuf-C version 1.3 or higher required"));
+
+ while ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) {
+ switch (ch) {
+ case 'f':
+ conf_file = optarg;
+ break;
+ case 'h':
+ help();
+ break;
+ case 'n':
+ nofork = true;
+ break;
+ case 'R':
+ /* random connection drop probability as a percentage (debug) */
+ errno = 0;
+ random_drop = strtod(optarg, &ep);
+ if (*ep != '\0' || errno != 0)
+ sudo_fatalx(U_("invalid random drop value: %s"), optarg);
+ random_drop /= 100.0; /* convert from percentage */
+ break;
+ case 'V':
+ (void)printf(_("%s version %s\n"), getprogname(),
+ PACKAGE_VERSION);
+ return 0;
+ default:
+ usage(true);
+ }
+ }
+
+ /* Read sudo_logsrvd.conf */
+ if (!logsrvd_conf_read(conf_file))
+ exit(EXIT_FAILURE);
+
+ if ((evbase = sudo_ev_base_alloc()) == NULL)
+ sudo_fatal(NULL);
+
+ /* Initialize listeners and TLS context. */
+ if (!server_setup(evbase))
+ sudo_fatalx("%s", U_("unable setup listen socket"));
+
+ register_signal(SIGHUP, evbase);
+ register_signal(SIGINT, evbase);
+ register_signal(SIGTERM, evbase);
+
+ /* Point of no return. */
+ daemonize(nofork);
+ signal(SIGPIPE, SIG_IGN);
+
+ sudo_ev_dispatch(evbase);
+ if (!nofork && logsrvd_conf_pid_file() != NULL)
+ unlink(logsrvd_conf_pid_file());
+
+ debug_return_int(1);
+}
diff --git a/logsrvd/logsrvd.h b/logsrvd/logsrvd.h
new file mode 100644
index 0000000..5c8a8b1
--- /dev/null
+++ b/logsrvd/logsrvd.h
@@ -0,0 +1,162 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 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.
+ */
+
+#ifndef SUDO_LOGSRVD_H
+#define SUDO_LOGSRVD_H
+
+#if PROTOBUF_C_VERSION_NUMBER < 1003000
+# error protobuf-c version 1.30 or higher required
+#endif
+
+#include "config.h"
+
+#if defined(HAVE_OPENSSL)
+# include <openssl/ssl.h>
+#endif
+
+#include "logsrv_util.h"
+
+/* Default timeout value for server socket */
+#define DEFAULT_SOCKET_TIMEOUT_SEC 30
+
+/* How often to send an ACK to the client (commit point) in seconds */
+#define ACK_FREQUENCY 10
+
+/* Shutdown timeout (in seconds) in case client connections time out. */
+#define SHUTDOWN_TIMEO 10
+
+/*
+ * Connection status.
+ * In the RUNNING state we expect I/O log buffers.
+ */
+enum connection_status {
+ INITIAL,
+ RUNNING,
+ EXITED,
+ SHUTDOWN,
+ FINISHED,
+ ERROR
+};
+
+/*
+ * Per-connection state.
+ */
+struct connection_closure {
+ TAILQ_ENTRY(connection_closure) entries;
+ struct eventlog *evlog;
+ struct timespec elapsed_time;
+ struct connection_buffer read_buf;
+ struct connection_buffer write_buf;
+ struct sudo_event_base *evbase;
+ struct sudo_event *commit_ev;
+ struct sudo_event *read_ev;
+ struct sudo_event *write_ev;
+#if defined(HAVE_OPENSSL)
+ struct sudo_event *ssl_accept_ev;
+ SSL *ssl;
+#endif
+ const char *errstr;
+ struct iolog_file iolog_files[IOFD_MAX];
+ bool tls;
+ bool log_io;
+ bool read_instead_of_write;
+ bool write_instead_of_read;
+ bool temporary_write_event;
+ int iolog_dir_fd;
+ int sock;
+#ifdef HAVE_STRUCT_IN6_ADDR
+ char ipaddr[INET6_ADDRSTRLEN];
+#else
+ char ipaddr[INET_ADDRSTRLEN];
+#endif
+ enum connection_status state;
+};
+
+union sockaddr_union {
+ struct sockaddr sa;
+ struct sockaddr_in sin;
+#ifdef HAVE_STRUCT_IN6_ADDR
+ struct sockaddr_in6 sin6;
+#endif
+};
+
+/*
+ * List of listen addresses.
+ */
+struct listen_address {
+ TAILQ_ENTRY(listen_address) entries;
+ char *sa_str;
+ union sockaddr_union sa_un;
+ socklen_t sa_size;
+ bool tls;
+};
+TAILQ_HEAD(listen_address_list, listen_address);
+
+/*
+ * List of active network listeners.
+ */
+struct listener {
+ TAILQ_ENTRY(listener) entries;
+ struct sudo_event *ev;
+ int sock;
+ bool tls;
+};
+TAILQ_HEAD(listener_list, listener);
+
+#if defined(HAVE_OPENSSL)
+/* parameters to configure tls */
+struct logsrvd_tls_config {
+ char *pkey_path;
+ char *cert_path;
+ char *cacert_path;
+ char *dhparams_path;
+ char *ciphers_v12;
+ char *ciphers_v13;
+ bool verify;
+ bool check_peer;
+};
+
+struct logsrvd_tls_runtime {
+ SSL_CTX *ssl_ctx;
+};
+#endif
+
+/* iolog_writer.c */
+struct eventlog *evlog_new(TimeSpec *submit_time, InfoMessage **info_msgs, size_t infolen);
+bool iolog_init(AcceptMessage *msg, struct connection_closure *closure);
+bool iolog_restart(RestartMessage *msg, struct connection_closure *closure);
+int store_iobuf(int iofd, IoBuffer *msg, struct connection_closure *closure);
+int store_suspend(CommandSuspend *msg, struct connection_closure *closure);
+int store_winsize(ChangeWindowSize *msg, struct connection_closure *closure);
+void iolog_close_all(struct connection_closure *closure);
+
+/* logsrvd_conf.c */
+bool logsrvd_conf_read(const char *path);
+const char *logsrvd_conf_iolog_dir(void);
+const char *logsrvd_conf_iolog_file(void);
+struct listen_address_list *logsrvd_conf_listen_address(void);
+bool logsrvd_conf_tcp_keepalive(void);
+const char *logsrvd_conf_pid_file(void);
+struct timespec *logsrvd_conf_get_sock_timeout(void);
+#if defined(HAVE_OPENSSL)
+const struct logsrvd_tls_config *logsrvd_get_tls_config(void);
+struct logsrvd_tls_runtime *logsrvd_get_tls_runtime(void);
+#endif
+mode_t logsrvd_conf_iolog_mode(void);
+
+#endif /* SUDO_LOGSRVD_H */
diff --git a/logsrvd/logsrvd_conf.c b/logsrvd/logsrvd_conf.c
new file mode 100644
index 0000000..d686ea6
--- /dev/null
+++ b/logsrvd/logsrvd_conf.c
@@ -0,0 +1,1143 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 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.
+ */
+
+#include "config.h"
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+
+#include <errno.h>
+#include <ctype.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <netdb.h>
+#ifdef HAVE_STDBOOL_H
+# include <stdbool.h>
+#else
+# include "compat/stdbool.h"
+#endif
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+#include <grp.h>
+#include <pwd.h>
+#ifndef HAVE_GETADDRINFO
+# include "compat/getaddrinfo.h"
+#endif
+
+#include "pathnames.h"
+#include "sudo_compat.h"
+#include "sudo_debug.h"
+#include "sudo_eventlog.h"
+#include "sudo_fatal.h"
+#include "sudo_gettext.h"
+#include "sudo_iolog.h"
+#include "sudo_util.h"
+
+#include "log_server.pb-c.h"
+#include "logsrvd.h"
+
+#if defined(HAVE_OPENSSL)
+# define DEFAULT_CA_CERT_PATH "/etc/ssl/sudo/cacert.pem"
+# define DEFAULT_SERVER_CERT_PATH "/etc/ssl/sudo/certs/logsrvd_cert.pem"
+# define DEFAULT_SERVER_KEY_PATH "/etc/ssl/sudo/private/logsrvd_key.pem"
+#endif
+
+struct logsrvd_config;
+typedef bool (*logsrvd_conf_cb_t)(struct logsrvd_config *config, const char *);
+
+struct logsrvd_config_entry {
+ char *conf_str;
+ logsrvd_conf_cb_t setter;
+};
+
+struct logsrvd_config_section {
+ char *name;
+ struct logsrvd_config_entry *entries;
+};
+
+static struct logsrvd_config {
+ struct logsrvd_config_server {
+ struct listen_address_list addresses;
+ struct timespec timeout;
+ bool tcp_keepalive;
+ char *pid_file;
+#if defined(HAVE_OPENSSL)
+ bool tls;
+ struct logsrvd_tls_config tls_config;
+ struct logsrvd_tls_runtime tls_runtime;
+#endif
+ } server;
+ struct logsrvd_config_iolog {
+ bool compress;
+ bool flush;
+ bool gid_set;
+ uid_t uid;
+ gid_t gid;
+ mode_t mode;
+ unsigned int maxseq;
+ char *iolog_dir;
+ char *iolog_file;
+ } iolog;
+ struct logsrvd_config_eventlog {
+ int log_type;
+ enum eventlog_format log_format;
+ } eventlog;
+ struct logsrvd_config_syslog {
+ unsigned int maxlen;
+ int facility;
+ int acceptpri;
+ int rejectpri;
+ int alertpri;
+ } syslog;
+ struct logsrvd_config_logfile {
+ char *path;
+ char *time_format;
+ FILE *stream;
+ } logfile;
+} *logsrvd_config;
+
+/* iolog getters */
+mode_t
+logsrvd_conf_iolog_mode(void)
+{
+ return logsrvd_config->iolog.mode;
+}
+
+const char *
+logsrvd_conf_iolog_dir(void)
+{
+ return logsrvd_config->iolog.iolog_dir;
+}
+
+const char *
+logsrvd_conf_iolog_file(void)
+{
+ return logsrvd_config->iolog.iolog_file;
+}
+
+/* server getters */
+struct listen_address_list *
+logsrvd_conf_listen_address(void)
+{
+ return &logsrvd_config->server.addresses;
+}
+
+bool
+logsrvd_conf_tcp_keepalive(void)
+{
+ return logsrvd_config->server.tcp_keepalive;
+}
+
+const char *
+logsrvd_conf_pid_file(void)
+{
+ return logsrvd_config->server.pid_file;
+}
+
+struct timespec *
+logsrvd_conf_get_sock_timeout(void)
+{
+ if (sudo_timespecisset(&logsrvd_config->server.timeout)) {
+ return &(logsrvd_config->server.timeout);
+ }
+
+ return NULL;
+}
+
+#if defined(HAVE_OPENSSL)
+const struct logsrvd_tls_config *
+logsrvd_get_tls_config(void)
+{
+ return &logsrvd_config->server.tls_config;
+}
+
+struct logsrvd_tls_runtime *
+logsrvd_get_tls_runtime(void)
+{
+ return &logsrvd_config->server.tls_runtime;
+}
+#endif
+
+/* I/O log callbacks */
+static bool
+cb_iolog_dir(struct logsrvd_config *config, const char *path)
+{
+ debug_decl(cb_iolog_dir, SUDO_DEBUG_UTIL);
+
+ free(config->iolog.iolog_dir);
+ if ((config->iolog.iolog_dir = strdup(path)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+static bool
+cb_iolog_file(struct logsrvd_config *config, const char *path)
+{
+ debug_decl(cb_iolog_file, SUDO_DEBUG_UTIL);
+
+ free(config->iolog.iolog_file);
+ if ((config->iolog.iolog_file = strdup(path)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+static bool
+cb_iolog_compress(struct logsrvd_config *config, const char *str)
+{
+ int val;
+ debug_decl(cb_iolog_compress, SUDO_DEBUG_UTIL);
+
+ if ((val = sudo_strtobool(str)) == -1)
+ debug_return_bool(false);
+
+ config->iolog.compress = val;
+ debug_return_bool(true);
+}
+
+static bool
+cb_iolog_flush(struct logsrvd_config *config, const char *str)
+{
+ int val;
+ debug_decl(cb_iolog_flush, SUDO_DEBUG_UTIL);
+
+ if ((val = sudo_strtobool(str)) == -1)
+ debug_return_bool(false);
+
+ config->iolog.flush = val;
+ debug_return_bool(true);
+}
+
+static bool
+cb_iolog_user(struct logsrvd_config *config, const char *user)
+{
+ struct passwd *pw;
+ debug_decl(cb_iolog_user, SUDO_DEBUG_UTIL);
+
+ if ((pw = getpwnam(user)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unknown user %s", user);
+ debug_return_bool(false);
+ }
+ config->iolog.uid = pw->pw_uid;
+ if (!config->iolog.gid_set)
+ config->iolog.gid = pw->pw_gid;
+
+ debug_return_bool(true);
+}
+
+static bool
+cb_iolog_group(struct logsrvd_config *config, const char *group)
+{
+ struct group *gr;
+ debug_decl(cb_iolog_group, SUDO_DEBUG_UTIL);
+
+ if ((gr = getgrnam(group)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unknown group %s", group);
+ debug_return_bool(false);
+ }
+ config->iolog.gid = gr->gr_gid;
+ config->iolog.gid_set = true;
+
+ debug_return_bool(true);
+}
+
+static bool
+cb_iolog_mode(struct logsrvd_config *config, const char *str)
+{
+ const char *errstr;
+ mode_t mode;
+ debug_decl(cb_iolog_mode, SUDO_DEBUG_UTIL);
+
+ mode = sudo_strtomode(str, &errstr);
+ if (errstr != NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to parse iolog mode %s", str);
+ debug_return_bool(false);
+ }
+ config->iolog.mode = mode;
+ debug_return_bool(true);
+}
+
+static bool
+cb_iolog_maxseq(struct logsrvd_config *config, const char *str)
+{
+ const char *errstr;
+ unsigned int value;
+ debug_decl(cb_iolog_maxseq, SUDO_DEBUG_UTIL);
+
+ value = sudo_strtonum(str, 0, SESSID_MAX, &errstr);
+ if (errstr != NULL) {
+ if (errno != ERANGE) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "bad maxseq: %s: %s", str, errstr);
+ debug_return_bool(false);
+ }
+ /* Out of range, clamp to SESSID_MAX as documented. */
+ value = SESSID_MAX;
+ }
+ config->iolog.maxseq = value;
+ debug_return_bool(true);
+}
+
+/* Server callbacks */
+static bool
+cb_listen_address(struct logsrvd_config *config, const char *str)
+{
+ struct addrinfo hints, *res, *res0 = NULL;
+ char *copy, *host, *port;
+ bool tls, ret = false;
+ int error;
+ debug_decl(cb_iolog_mode, SUDO_DEBUG_UTIL);
+
+ if ((copy = strdup(str)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+
+ /* Parse host[:port] */
+ if (!iolog_parse_host_port(copy, &host, &port, &tls, DEFAULT_PORT,
+ DEFAULT_PORT_TLS))
+ goto done;
+ if (host[0] == '*' && host[1] == '\0')
+ host = NULL;
+
+#if !defined(HAVE_OPENSSL)
+ if (tls) {
+ sudo_warnx("%s", U_("TLS not supported"));
+ goto done;
+ }
+#endif
+
+ /* Resolve host (and port if it is a service). */
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_PASSIVE;
+ error = getaddrinfo(host, port, &hints, &res0);
+ if (error != 0) {
+ sudo_gai_warn(error, U_("%s:%s"), host ? host : "*", port);
+ goto done;
+ }
+ for (res = res0; res != NULL; res = res->ai_next) {
+ struct listen_address *addr;
+
+ if ((addr = malloc(sizeof(*addr))) == NULL) {
+ sudo_warn(NULL);
+ goto done;
+ }
+ if ((addr->sa_str = strdup(str)) == NULL) {
+ sudo_warn(NULL);
+ free(addr);
+ goto done;
+ }
+ memcpy(&addr->sa_un, res->ai_addr, res->ai_addrlen);
+ addr->sa_size = res->ai_addrlen;
+ addr->tls = tls;
+ TAILQ_INSERT_TAIL(&config->server.addresses, addr, entries);
+ }
+
+ ret = true;
+done:
+ if (res0 != NULL)
+ freeaddrinfo(res0);
+ free(copy);
+ debug_return_bool(ret);
+}
+
+static bool
+cb_timeout(struct logsrvd_config *config, const char *str)
+{
+ int timeout;
+ const char* errstr;
+ debug_decl(cb_timeout, SUDO_DEBUG_UTIL);
+
+ timeout = sudo_strtonum(str, 0, UINT_MAX, &errstr);
+ if (errstr != NULL)
+ debug_return_bool(false);
+
+ config->server.timeout.tv_sec = timeout;
+
+ debug_return_bool(true);
+}
+
+static bool
+cb_keepalive(struct logsrvd_config *config, const char *str)
+{
+ int val;
+ debug_decl(cb_keepalive, SUDO_DEBUG_UTIL);
+
+ if ((val = sudo_strtobool(str)) == -1)
+ debug_return_bool(false);
+
+ config->server.tcp_keepalive = val;
+ debug_return_bool(true);
+}
+
+static bool
+cb_pid_file(struct logsrvd_config *config, const char *str)
+{
+ char *copy = NULL;
+ debug_decl(cb_pid_file, SUDO_DEBUG_UTIL);
+
+ /* An empty value means to disable the pid file. */
+ if (*str != '\0') {
+ if (*str != '/') {
+ sudo_warnx(U_("%s: not a fully qualified path"), str);
+ debug_return_bool(false);
+ }
+ if ((copy = strdup(str)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+ }
+
+ free(config->server.pid_file);
+ config->server.pid_file = copy;
+
+ debug_return_bool(true);
+}
+
+#if defined(HAVE_OPENSSL)
+static bool
+cb_tls_key(struct logsrvd_config *config, const char *path)
+{
+ debug_decl(cb_tls_key, SUDO_DEBUG_UTIL);
+
+ free(config->server.tls_config.pkey_path);
+ if ((config->server.tls_config.pkey_path = strdup(path)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+static bool
+cb_tls_cacert(struct logsrvd_config *config, const char *path)
+{
+ debug_decl(cb_tls_cacert, SUDO_DEBUG_UTIL);
+
+ free(config->server.tls_config.cacert_path);
+ if ((config->server.tls_config.cacert_path = strdup(path)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+static bool
+cb_tls_cert(struct logsrvd_config *config, const char *path)
+{
+ debug_decl(cb_tls_cert, SUDO_DEBUG_UTIL);
+
+ free(config->server.tls_config.cert_path);
+ if ((config->server.tls_config.cert_path = strdup(path)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+static bool
+cb_tls_dhparam(struct logsrvd_config *config, const char *path)
+{
+ debug_decl(cb_tls_dhparam, SUDO_DEBUG_UTIL);
+
+ free(config->server.tls_config.dhparams_path);
+ if ((config->server.tls_config.dhparams_path = strdup(path)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+static bool
+cb_tls_ciphers12(struct logsrvd_config *config, const char *str)
+{
+ debug_decl(cb_tls_ciphers12, SUDO_DEBUG_UTIL);
+
+ free(config->server.tls_config.ciphers_v12);
+ if ((config->server.tls_config.ciphers_v12 = strdup(str)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+static bool
+cb_tls_ciphers13(struct logsrvd_config *config, const char *str)
+{
+ debug_decl(cb_tls_ciphers13, SUDO_DEBUG_UTIL);
+
+ free(config->server.tls_config.ciphers_v13);
+ if ((config->server.tls_config.ciphers_v13 = strdup(str)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+static bool
+cb_tls_verify(struct logsrvd_config *config, const char *str)
+{
+ int val;
+ debug_decl(cb_tls_verify, SUDO_DEBUG_UTIL);
+
+ if ((val = sudo_strtobool(str)) == -1)
+ debug_return_bool(false);
+
+ config->server.tls_config.verify = val;
+ debug_return_bool(true);
+}
+
+static bool
+cb_tls_checkpeer(struct logsrvd_config *config, const char *str)
+{
+ int val;
+ debug_decl(cb_tls_checkpeer, SUDO_DEBUG_UTIL);
+
+ if ((val = sudo_strtobool(str)) == -1)
+ debug_return_bool(false);
+
+ config->server.tls_config.check_peer = val;
+ debug_return_bool(true);
+}
+#endif
+
+/* eventlog callbacks */
+static bool
+cb_eventlog_type(struct logsrvd_config *config, const char *str)
+{
+ debug_decl(cb_eventlog_type, SUDO_DEBUG_UTIL);
+
+ if (strcmp(str, "none") == 0)
+ config->eventlog.log_type = EVLOG_NONE;
+ else if (strcmp(str, "syslog") == 0)
+ config->eventlog.log_type = EVLOG_SYSLOG;
+ else if (strcmp(str, "logfile") == 0)
+ config->eventlog.log_type = EVLOG_FILE;
+ else
+ debug_return_bool(false);
+
+ debug_return_bool(true);
+}
+
+static bool
+cb_eventlog_format(struct logsrvd_config *config, const char *str)
+{
+ debug_decl(cb_eventlog_format, SUDO_DEBUG_UTIL);
+
+ if (strcmp(str, "json") == 0)
+ config->eventlog.log_format = EVLOG_JSON;
+ else if (strcmp(str, "sudo") == 0)
+ config->eventlog.log_format = EVLOG_SUDO;
+ else
+ debug_return_bool(false);
+
+ debug_return_bool(true);
+}
+
+/* syslog callbacks */
+static bool
+cb_syslog_maxlen(struct logsrvd_config *config, const char *str)
+{
+ unsigned int maxlen;
+ const char *errstr;
+ debug_decl(cb_syslog_maxlen, SUDO_DEBUG_UTIL);
+
+ maxlen = sudo_strtonum(str, 1, UINT_MAX, &errstr);
+ if (errstr != NULL)
+ debug_return_bool(false);
+
+ config->syslog.maxlen = maxlen;
+
+ debug_return_bool(true);
+}
+
+static bool
+cb_syslog_facility(struct logsrvd_config *config, const char *str)
+{
+ int logfac;
+ debug_decl(cb_syslog_facility, SUDO_DEBUG_UTIL);
+
+ if (!sudo_str2logfac(str, &logfac)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "invalid syslog priority %s", str);
+ debug_return_bool(false);
+ }
+
+ config->syslog.facility = logfac;
+
+ debug_return_bool(true);
+}
+
+static bool
+cb_syslog_acceptpri(struct logsrvd_config *config, const char *str)
+{
+ int logpri;
+ debug_decl(cb_syslog_acceptpri, SUDO_DEBUG_UTIL);
+
+ if (!sudo_str2logpri(str, &logpri)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "invalid syslog priority %s", str);
+ debug_return_bool(false);
+ }
+
+ config->syslog.acceptpri = logpri;
+
+ debug_return_bool(true);
+}
+
+static bool
+cb_syslog_rejectpri(struct logsrvd_config *config, const char *str)
+{
+ int logpri;
+ debug_decl(cb_syslog_rejectpri, SUDO_DEBUG_UTIL);
+
+ if (!sudo_str2logpri(str, &logpri))
+ debug_return_bool(false);
+
+ config->syslog.rejectpri = logpri;
+
+ debug_return_bool(true);
+}
+
+static bool
+cb_syslog_alertpri(struct logsrvd_config *config, const char *str)
+{
+ int logpri;
+ debug_decl(cb_syslog_alertpri, SUDO_DEBUG_UTIL);
+
+ if (!sudo_str2logpri(str, &logpri)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "invalid syslog priority %s", str);
+ debug_return_bool(false);
+ }
+
+ config->syslog.alertpri = logpri;
+
+ debug_return_bool(true);
+}
+
+/* logfile callbacks */
+static bool
+cb_logfile_path(struct logsrvd_config *config, const char *str)
+{
+ char *copy = NULL;
+ debug_decl(cb_logfile_path, SUDO_DEBUG_UTIL);
+
+ if (*str != '/') {
+ debug_return_bool(false);
+ sudo_warnx(U_("%s: not a fully qualified path"), str);
+ debug_return_bool(false);
+ }
+ if ((copy = strdup(str)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+
+ free(config->logfile.path);
+ config->logfile.path = copy;
+
+ debug_return_bool(true);
+}
+
+static bool
+cb_logfile_time_format(struct logsrvd_config *config, const char *str)
+{
+ char *copy = NULL;
+ debug_decl(cb_logfile_time_format, SUDO_DEBUG_UTIL);
+
+ if ((copy = strdup(str)) == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+
+ free(config->logfile.time_format);
+ config->logfile.time_format = copy;
+
+ debug_return_bool(true);
+}
+
+static struct logsrvd_config_entry server_conf_entries[] = {
+ { "listen_address", cb_listen_address },
+ { "timeout", cb_timeout },
+ { "tcp_keepalive", cb_keepalive },
+ { "pid_file", cb_pid_file },
+#if defined(HAVE_OPENSSL)
+ { "tls_key", cb_tls_key },
+ { "tls_cacert", cb_tls_cacert },
+ { "tls_cert", cb_tls_cert },
+ { "tls_dhparams", cb_tls_dhparam },
+ { "tls_ciphers_v12", cb_tls_ciphers12 },
+ { "tls_ciphers_v13", cb_tls_ciphers13 },
+ { "tls_checkpeer", cb_tls_checkpeer },
+ { "tls_verify", cb_tls_verify },
+#endif
+ { NULL }
+};
+
+static struct logsrvd_config_entry iolog_conf_entries[] = {
+ { "iolog_dir", cb_iolog_dir },
+ { "iolog_file", cb_iolog_file },
+ { "iolog_flush", cb_iolog_flush },
+ { "iolog_compress", cb_iolog_compress },
+ { "iolog_user", cb_iolog_user },
+ { "iolog_group", cb_iolog_group },
+ { "iolog_mode", cb_iolog_mode },
+ { "maxseq", cb_iolog_maxseq },
+ { NULL }
+};
+
+static struct logsrvd_config_entry eventlog_conf_entries[] = {
+ { "log_type", cb_eventlog_type },
+ { "log_format", cb_eventlog_format },
+ { NULL }
+};
+
+static struct logsrvd_config_entry syslog_conf_entries[] = {
+ { "maxlen", cb_syslog_maxlen },
+ { "facility", cb_syslog_facility },
+ { "reject_priority", cb_syslog_rejectpri },
+ { "accept_priority", cb_syslog_acceptpri },
+ { "alert_priority", cb_syslog_alertpri },
+ { NULL }
+};
+
+static struct logsrvd_config_entry logfile_conf_entries[] = {
+ { "path", cb_logfile_path },
+ { "time_format", cb_logfile_time_format },
+ { NULL }
+};
+
+static struct logsrvd_config_section logsrvd_config_sections[] = {
+ { "server", server_conf_entries },
+ { "iolog", iolog_conf_entries },
+ { "eventlog", eventlog_conf_entries },
+ { "syslog", syslog_conf_entries },
+ { "logfile", logfile_conf_entries },
+ { NULL }
+};
+
+static bool
+logsrvd_conf_parse(struct logsrvd_config *config, FILE *fp, const char *path)
+{
+ struct logsrvd_config_section *conf_section = NULL;
+ unsigned int lineno = 0;
+ size_t linesize = 0;
+ char *line = NULL;
+ bool ret = false;
+ debug_decl(logsrvd_conf_parse, SUDO_DEBUG_UTIL);
+
+ while (sudo_parseln(&line, &linesize, &lineno, fp, 0) != -1) {
+ struct logsrvd_config_entry *entry;
+ char *ep, *val;
+
+ /* Skip blank, comment or invalid lines. */
+ if (*line == '\0' || *line == ';')
+ continue;
+
+ /* New section */
+ if (line[0] == '[') {
+ char *section_name = line + 1;
+ char *cp = strchr(section_name, ']');
+ if (cp == NULL) {
+ sudo_warnx(U_("%s:%d unmatched '[': %s"),
+ path, lineno, line);
+ goto done;
+ }
+ *cp = '\0';
+ for (conf_section = logsrvd_config_sections; conf_section->name != NULL;
+ conf_section++) {
+ if (strcasecmp(section_name, conf_section->name) == 0)
+ break;
+ }
+ if (conf_section->name == NULL) {
+ sudo_warnx(U_("%s:%d invalid config section: %s"),
+ path, lineno, section_name);
+ goto done;
+ }
+ continue;
+ }
+
+ if ((ep = strchr(line, '=')) == NULL) {
+ sudo_warnx(U_("%s:%d invalid configuration line: %s"),
+ path, lineno, line);
+ goto done;
+ }
+
+ if (conf_section == NULL) {
+ sudo_warnx(U_("%s:%d expected section name: %s"),
+ path, lineno, line);
+ goto done;
+ }
+
+ val = ep + 1;
+ while (isspace((unsigned char)*val))
+ val++;
+ while (ep > line && isspace((unsigned char)ep[-1]))
+ ep--;
+ *ep = '\0';
+ for (entry = conf_section->entries; entry->conf_str != NULL; entry++) {
+ if (strcasecmp(line, entry->conf_str) == 0) {
+ if (!entry->setter(config, val)) {
+ sudo_warnx(U_("invalid value for %s: %s"),
+ entry->conf_str, val);
+ goto done;
+ }
+ break;
+ }
+ }
+ if (entry->conf_str == NULL) {
+ sudo_warnx(U_("%s:%d unknown key: %s"), path, lineno, line);
+ goto done;
+ }
+ }
+ ret = true;
+
+done:
+ free(line);
+ debug_return_bool(ret);
+}
+
+static FILE *
+logsrvd_open_eventlog(struct logsrvd_config *config)
+{
+ mode_t oldmask;
+ FILE *fp = NULL;
+ const char *omode;
+ int fd, flags;
+ debug_decl(logsrvd_open_eventlog, SUDO_DEBUG_UTIL);
+
+ /* Cannot append to a JSON file. */
+ if (config->eventlog.log_format == EVLOG_JSON) {
+ flags = O_RDWR|O_CREAT;
+ omode = "w";
+ } else {
+ flags = O_WRONLY|O_APPEND|O_CREAT;
+ omode = "a";
+ }
+ oldmask = umask(S_IRWXG|S_IRWXO);
+ fd = open(config->logfile.path, flags, S_IRUSR|S_IWUSR);
+ (void)umask(oldmask);
+ if (fd == -1 || (fp = fdopen(fd, omode)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
+ "unable to open log file %s", config->logfile.path);
+ if (fd != -1)
+ close(fd);
+ }
+
+ debug_return_ptr(fp);
+}
+
+static FILE *
+logsrvd_stub_open_log(int type, const char *logfile)
+{
+ /* Actual open already done by logsrvd_open_eventlog() */
+ return logsrvd_config->logfile.stream;
+}
+
+static void
+logsrvd_stub_close_log(int type, FILE *fp)
+{
+ return;
+}
+
+/* Set eventlog configuration settings from on logsrvd config. */
+static void
+logsrvd_conf_eventlog_setconf(struct logsrvd_config *config)
+{
+ debug_decl(logsrvd_conf_eventlog_setconf, SUDO_DEBUG_UTIL);
+
+ eventlog_set_type(config->eventlog.log_type);
+ eventlog_set_format(config->eventlog.log_format);
+ eventlog_set_syslog_acceptpri(config->syslog.acceptpri);
+ eventlog_set_syslog_rejectpri(config->syslog.rejectpri);
+ eventlog_set_syslog_alertpri(config->syslog.alertpri);
+ eventlog_set_syslog_maxlen(config->syslog.maxlen);
+ eventlog_set_logpath(config->logfile.path);
+ eventlog_set_time_fmt(config->logfile.time_format);
+ eventlog_set_open_log(logsrvd_stub_open_log);
+ eventlog_set_close_log(logsrvd_stub_close_log);
+
+ debug_return;
+}
+
+/* Free the specified struct logsrvd_config and its contents. */
+void
+logsrvd_conf_free(struct logsrvd_config *config)
+{
+ struct listen_address *addr;
+ debug_decl(logsrvd_conf_free, SUDO_DEBUG_UTIL);
+
+ if (config == NULL)
+ debug_return;
+
+ /* struct logsrvd_config_server */
+ while ((addr = TAILQ_FIRST(&config->server.addresses))) {
+ TAILQ_REMOVE(&config->server.addresses, addr, entries);
+ free(addr->sa_str);
+ free(addr);
+ }
+ free(config->server.pid_file);
+
+ /* struct logsrvd_config_iolog */
+ free(config->iolog.iolog_dir);
+ free(config->iolog.iolog_file);
+
+ /* struct logsrvd_config_logfile */
+ free(config->logfile.path);
+ free(config->logfile.time_format);
+ if (config->logfile.stream != NULL)
+ fclose(config->logfile.stream);
+
+#if defined(HAVE_OPENSSL)
+ free(config->server.tls_config.pkey_path);
+ free(config->server.tls_config.cert_path);
+ free(config->server.tls_config.cacert_path);
+ free(config->server.tls_config.dhparams_path);
+ free(config->server.tls_config.ciphers_v12);
+ free(config->server.tls_config.ciphers_v13);
+
+ if (config->server.tls_runtime.ssl_ctx != NULL)
+ SSL_CTX_free(config->server.tls_runtime.ssl_ctx);
+#endif
+
+ free(config);
+
+ debug_return;
+}
+
+/* Allocate a new struct logsrvd_config and set default values. */
+struct logsrvd_config *
+logsrvd_conf_alloc(void)
+{
+ struct logsrvd_config *config;
+ debug_decl(logsrvd_conf_alloc, SUDO_DEBUG_UTIL);
+
+ if ((config = calloc(1, sizeof(*config))) == NULL) {
+ sudo_warn(NULL);
+ debug_return_ptr(NULL);
+ }
+
+ /* Server defaults */
+ TAILQ_INIT(&config->server.addresses);
+ config->server.timeout.tv_sec = DEFAULT_SOCKET_TIMEOUT_SEC;
+ config->server.tcp_keepalive = true;
+ config->server.pid_file = strdup(_PATH_SUDO_LOGSRVD_PID);
+ if (config->server.pid_file == NULL) {
+ sudo_warn(NULL);
+ goto bad;
+ }
+
+#if defined(HAVE_OPENSSL)
+ /*
+ * Only set default CA and cert paths if the files actually exist.
+ * This ensures we don't enable TLS by default when it is not configured.
+ */
+ if (access(DEFAULT_CA_CERT_PATH, R_OK) == 0) {
+ config->server.tls_config.cacert_path = strdup(DEFAULT_CA_CERT_PATH);
+ if (config->server.tls_config.cacert_path == NULL) {
+ sudo_warn(NULL);
+ goto bad;
+ }
+ }
+ if (access(DEFAULT_SERVER_CERT_PATH, R_OK) == 0) {
+ config->server.tls_config.cert_path = strdup(DEFAULT_SERVER_CERT_PATH);
+ if (config->server.tls_config.cert_path == NULL) {
+ sudo_warn(NULL);
+ goto bad;
+ }
+ }
+ config->server.tls_config.pkey_path = strdup(DEFAULT_SERVER_KEY_PATH);
+ if (config->server.tls_config.pkey_path == NULL) {
+ sudo_warn(NULL);
+ goto bad;
+ }
+ config->server.tls_config.verify = true;
+ config->server.tls_config.check_peer = false;
+#endif
+
+ /* I/O log defaults */
+ config->iolog.compress = false;
+ config->iolog.flush = true;
+ config->iolog.mode = S_IRUSR|S_IWUSR;
+ config->iolog.maxseq = SESSID_MAX;
+ if (!cb_iolog_dir(config, _PATH_SUDO_IO_LOGDIR))
+ goto bad;
+ if (!cb_iolog_file(config, "%{seq}"))
+ goto bad;
+ config->iolog.uid = ROOT_UID;
+ config->iolog.gid = ROOT_GID;
+ config->iolog.gid_set = false;
+
+ /* Event log defaults */
+ config->eventlog.log_type = EVLOG_SYSLOG;
+ config->eventlog.log_format = EVLOG_SUDO;
+
+ /* Syslog defaults */
+ config->syslog.maxlen = 960;
+ if (!cb_syslog_facility(config, LOGFAC)) {
+ sudo_warnx(U_("unknown syslog facility %s"), LOGFAC);
+ goto bad;
+ }
+ if (!cb_syslog_acceptpri(config, PRI_SUCCESS)) {
+ sudo_warnx(U_("unknown syslog priority %s"), PRI_SUCCESS);
+ goto bad;
+ }
+ if (!cb_syslog_rejectpri(config, PRI_FAILURE)) {
+ sudo_warnx(U_("unknown syslog priority %s"), PRI_FAILURE);
+ goto bad;
+ }
+ if (!cb_syslog_alertpri(config, PRI_FAILURE)) {
+ sudo_warnx(U_("unknown syslog priority %s"), PRI_FAILURE);
+ goto bad;
+ }
+
+ /* Log file defaults */
+ if (!cb_logfile_time_format(config, "%h %e %T"))
+ goto bad;
+ if (!cb_logfile_path(config, _PATH_SUDO_LOGFILE))
+ goto bad;
+
+ debug_return_ptr(config);
+bad:
+ logsrvd_conf_free(config);
+ debug_return_ptr(NULL);
+}
+
+bool
+logsrvd_conf_apply(struct logsrvd_config *config)
+{
+ debug_decl(logsrvd_conf_apply, SUDO_DEBUG_UTIL);
+
+ /* There can be multiple addresses so we can't set a default earlier. */
+ if (TAILQ_EMPTY(&config->server.addresses)) {
+ /* Enable plaintext listender. */
+ if (!cb_listen_address(config, "*:" DEFAULT_PORT))
+ debug_return_bool(false);
+#if defined(HAVE_OPENSSL)
+ /* If a certificate was specified, enable the TLS listener too. */
+ if (config->server.tls_config.cert_path != NULL) {
+ if (!cb_listen_address(config, "*:" DEFAULT_PORT_TLS "(tls)"))
+ debug_return_bool(false);
+ }
+ } else {
+ struct listen_address *addr;
+
+ /* Check that TLS configuration is valid. */
+ TAILQ_FOREACH(addr, &config->server.addresses, entries) {
+ if (!addr->tls)
+ continue;
+ /*
+ * If a TLS listener was explicitly enabled but the cert path
+ * was not, use the default.
+ */
+ if (config->server.tls_config.cert_path == NULL) {
+ config->server.tls_config.cert_path =
+ strdup(DEFAULT_SERVER_CERT_PATH);
+ if (config->server.tls_config.cert_path == NULL) {
+ sudo_warn(NULL);
+ debug_return_bool(false);
+ }
+ }
+ break;
+ }
+#endif
+ }
+
+ /* Open event log if specified. */
+ switch (config->eventlog.log_type) {
+ case EVLOG_SYSLOG:
+ openlog("sudo", 0, config->syslog.facility);
+ break;
+ case EVLOG_FILE:
+ config->logfile.stream = logsrvd_open_eventlog(config);
+ if (config->logfile.stream == NULL)
+ debug_return_bool(false);
+ break;
+ case EVLOG_NONE:
+ break;
+ default:
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "cannot open unknown log type %d", config->eventlog.log_type);
+ break;
+ }
+
+ /* Set I/O log library settings */
+ iolog_set_defaults();
+ iolog_set_compress(config->iolog.compress);
+ iolog_set_flush(config->iolog.flush);
+ iolog_set_owner(config->iolog.uid, config->iolog.gid);
+ iolog_set_mode(config->iolog.mode);
+ iolog_set_maxseq(config->iolog.maxseq);
+
+ /* Set event log config */
+ logsrvd_conf_eventlog_setconf(config);
+
+ logsrvd_conf_free(logsrvd_config);
+ logsrvd_config = config;
+
+ debug_return_bool(true);
+}
+
+/*
+ * Read .ini style logsrvd.conf file.
+ * Note that we use '#' not ';' for the comment character.
+ */
+bool
+logsrvd_conf_read(const char *path)
+{
+ struct logsrvd_config *config;
+ bool ret = false;
+ FILE *fp = NULL;
+ debug_decl(logsrvd_conf_read, SUDO_DEBUG_UTIL);
+
+ config = logsrvd_conf_alloc();
+
+ if ((fp = fopen(path, "r")) == NULL) {
+ if (errno != ENOENT) {
+ sudo_warn("%s", path);
+ goto done;
+ }
+ } else {
+ if (!logsrvd_conf_parse(config, fp, path))
+ goto done;
+ }
+
+ /* Install new config */
+ if (logsrvd_conf_apply(config)) {
+ config = NULL;
+ ret = true;
+ }
+
+done:
+ logsrvd_conf_free(config);
+ if (fp != NULL)
+ fclose(fp);
+ debug_return_bool(ret);
+}
diff --git a/logsrvd/sendlog.c b/logsrvd/sendlog.c
new file mode 100644
index 0000000..2912d9e
--- /dev/null
+++ b/logsrvd/sendlog.c
@@ -0,0 +1,1893 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 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.
+ */
+
+#include "config.h"
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <arpa/inet.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <netdb.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>
+#ifndef HAVE_GETADDRINFO
+# include "compat/getaddrinfo.h"
+#endif
+#ifdef HAVE_GETOPT_LONG
+# include <getopt.h>
+# else
+# include "compat/getopt.h"
+#endif /* HAVE_GETOPT_LONG */
+
+#if defined(HAVE_OPENSSL)
+# include <openssl/ssl.h>
+# include <openssl/err.h>
+#endif
+
+#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 "hostcheck.h"
+#include "log_server.pb-c.h"
+#include "sendlog.h"
+
+#if defined(HAVE_OPENSSL)
+# define TLS_HANDSHAKE_TIMEO_SEC 10
+#endif
+
+TAILQ_HEAD(connection_list, client_closure);
+static struct connection_list connections = TAILQ_HEAD_INITIALIZER(connections);
+
+static const char *server_name = "localhost";
+#if defined(HAVE_STRUCT_IN6_ADDR)
+static char server_ip[INET6_ADDRSTRLEN];
+#else
+static char server_ip[INET_ADDRSTRLEN];
+#endif
+static char *iolog_dir;
+static bool testrun = false;
+static int nr_of_conns = 1;
+static int finished_transmissions = 0;
+
+#if defined(HAVE_OPENSSL)
+static SSL_CTX *ssl_ctx = NULL;
+static const char *ca_bundle = NULL;
+static const char *cert = NULL;
+static const char *key = NULL;
+static bool verify_server = true;
+#endif
+
+/* Server callback may redirect to client callback for TLS. */
+static void client_msg_cb(int fd, int what, void *v);
+static void server_msg_cb(int fd, int what, void *v);
+
+static void
+usage(bool fatal)
+{
+#if defined(HAVE_OPENSSL)
+ fprintf(stderr, "usage: %s [-AnV] [-b ca_bundle] [-c cert_file] [-h host] "
+ "[-i iolog-id] [-k key_file] [-p port] "
+#else
+ fprintf(stderr, "usage: %s [-AnV] [-h host] [-i iolog-id] [-p port] "
+#endif
+ "[-r restart-point] [-R reject-reason] [-t number] /path/to/iolog\n",
+ getprogname());
+ if (fatal)
+ exit(EXIT_FAILURE);
+}
+
+static void
+help(void)
+{
+ (void)printf(_("%s - send sudo I/O log to remote server\n\n"),
+ getprogname());
+ usage(false);
+ (void)puts(_("\nOptions:\n"
+ " --help display help message and exit\n"
+ " -A, --accept only send an accept event (no I/O)\n"
+ " -h, --host host to send logs to\n"
+ " -i, --iolog_id remote ID of I/O log to be resumed\n"
+ " -p, --port port to use when connecting to host\n"
+ " -r, --restart restart previous I/O log transfer\n"
+ " -R, --reject reject the command with the given reason\n"
+#if defined(HAVE_OPENSSL)
+ " -b, --ca-bundle certificate bundle file to verify server's cert against\n"
+ " -c, --cert certificate file for TLS handshake\n"
+ " -k, --key private key file\n"
+ " -n, --no-verify do not verify server certificate\n"
+#endif
+ " -t, --test test audit server by sending selected I/O log n times in parallel\n"
+ " -V, --version display version information and exit\n"));
+ exit(EXIT_SUCCESS);
+}
+
+/*
+ * Connect to specified host:port
+ * If host has multiple addresses, the first one that connects is used.
+ * Returns open socket or -1 on error.
+ */
+static int
+connect_server(const char *host, const char *port)
+{
+ struct addrinfo hints, *res, *res0;
+ const char *addr, *cause = "getaddrinfo";
+ int error, sock, save_errno;
+ debug_decl(connect_server, SUDO_DEBUG_UTIL);
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ error = getaddrinfo(host, port, &hints, &res0);
+ if (error != 0) {
+ sudo_warnx(U_("unable to look up %s:%s: %s"), host, port,
+ gai_strerror(error));
+ debug_return_int(-1);
+ }
+
+ sock = -1;
+ for (res = res0; res; res = res->ai_next) {
+ sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+ if (sock == -1) {
+ cause = "socket";
+ continue;
+ }
+ if (connect(sock, res->ai_addr, res->ai_addrlen) == -1) {
+ cause = "connect";
+ save_errno = errno;
+ close(sock);
+ errno = save_errno;
+ sock = -1;
+ continue;
+ }
+ if (*server_ip == '\0') {
+ switch (res->ai_family) {
+ case AF_INET:
+ addr = (char *)&((struct sockaddr_in *)res->ai_addr)->sin_addr;
+ break;
+ case AF_INET6:
+ addr = (char *)&((struct sockaddr_in6 *)res->ai_addr)->sin6_addr;
+ break;
+ default:
+ cause = "ai_family";
+ save_errno = EAFNOSUPPORT;
+ close(sock);
+ errno = save_errno;
+ sock = -1;
+ continue;
+ }
+ if (inet_ntop(res->ai_family, addr, server_ip,
+ sizeof(server_ip)) == NULL) {
+ sudo_warnx("%s", U_("unable to get server IP addr"));
+ }
+ }
+ break; /* success */
+ }
+ freeaddrinfo(res0);
+
+ if (sock != -1) {
+ int flags = fcntl(sock, F_GETFL, 0);
+ if (flags == -1 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) {
+ cause = "fcntl(O_NONBLOCK)";
+ save_errno = errno;
+ close(sock);
+ errno = save_errno;
+ sock = -1;
+ }
+ }
+ if (sock == -1)
+ sudo_warn("%s", cause);
+
+ debug_return_int(sock);
+}
+
+/*
+ * Read the next I/O buffer as described by closure->timing.
+ */
+static bool
+read_io_buf(struct client_closure *closure)
+{
+ struct timing_closure *timing = &closure->timing;
+ const char *errstr = NULL;
+ size_t nread;
+ debug_decl(read_io_buf, SUDO_DEBUG_UTIL);
+
+ if (!closure->iolog_files[timing->event].enabled) {
+ errno = ENOENT;
+ sudo_warn("%s/%s", iolog_dir, iolog_fd_to_name(timing->event));
+ debug_return_bool(false);
+ }
+
+ /* Expand buf as needed. */
+ if (timing->u.nbytes > closure->bufsize) {
+ free(closure->buf);
+ closure->bufsize = sudo_pow2_roundup(timing->u.nbytes);
+ if ((closure->buf = malloc(closure->bufsize)) == NULL) {
+ sudo_warn(NULL);
+ timing->u.nbytes = 0;
+ debug_return_bool(false);
+ }
+ }
+
+ nread = iolog_read(&closure->iolog_files[timing->event], closure->buf,
+ timing->u.nbytes, &errstr);
+ if (nread != timing->u.nbytes) {
+ sudo_warnx(U_("unable to read %s/%s: %s"), iolog_dir,
+ iolog_fd_to_name(timing->event), errstr);
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+/*
+ * Format a ClientMessage and store the wire format message in buf.
+ * Returns true on success, false on failure.
+ */
+static bool
+fmt_client_message(struct connection_buffer *buf, ClientMessage *msg)
+{
+ uint32_t msg_len;
+ bool ret = false;
+ size_t len;
+ debug_decl(fmt_client_message, SUDO_DEBUG_UTIL);
+
+ len = client_message__get_packed_size(msg);
+ if (len > MESSAGE_SIZE_MAX) {
+ sudo_warnx(U_("client message too large: %zu"), len);
+ goto done;
+ }
+ /* Wire message size is used for length encoding, precedes message. */
+ msg_len = htonl((uint32_t)len);
+ len += sizeof(msg_len);
+
+ /* Resize buffer as needed. */
+ if (len > buf->size) {
+ free(buf->data);
+ buf->size = sudo_pow2_roundup(len);
+ if ((buf->data = malloc(buf->size)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to malloc %u", buf->size);
+ buf->size = 0;
+ goto done;
+ }
+ }
+
+ memcpy(buf->data, &msg_len, sizeof(msg_len));
+ client_message__pack(msg, buf->data + sizeof(msg_len));
+ buf->len = len;
+ ret = true;
+
+done:
+ debug_return_bool(ret);
+}
+
+/*
+ * Split command + args into an array of strings.
+ * Returns an array containing command and args, reusing space in "command".
+ * Note that the returned array does not end with a terminating NULL.
+ */
+static char **
+split_command(char *command, size_t *lenp)
+{
+ char *cp;
+ char **args;
+ size_t len;
+ debug_decl(split_command, SUDO_DEBUG_UTIL);
+
+ for (cp = command, len = 0;;) {
+ len++;
+ if ((cp = strchr(cp, ' ')) == NULL)
+ break;
+ cp++;
+ }
+ args = reallocarray(NULL, len, sizeof(char *));
+ if (args == NULL)
+ debug_return_ptr(NULL);
+
+ for (cp = command, len = 0;;) {
+ args[len++] = cp;
+ if ((cp = strchr(cp, ' ')) == NULL)
+ break;
+ *cp++ = '\0';
+ }
+
+ *lenp = len;
+ debug_return_ptr(args);
+}
+
+static bool
+fmt_client_hello(struct client_closure *closure)
+{
+ ClientMessage client_msg = CLIENT_MESSAGE__INIT;
+ ClientHello hello_msg = CLIENT_HELLO__INIT;
+ bool ret = false;
+ debug_decl(fmt_client_hello, SUDO_DEBUG_UTIL);
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending ClientHello", __func__);
+ hello_msg.client_id = "Sudo Sendlog " PACKAGE_VERSION;
+
+ /* Schedule ClientMessage */
+ client_msg.u.hello_msg = &hello_msg;
+ client_msg.type_case = CLIENT_MESSAGE__TYPE_HELLO_MSG;
+ ret = fmt_client_message(&closure->write_buf, &client_msg);
+ if (ret) {
+ if (sudo_ev_add(closure->evbase, closure->read_ev, NULL, false) == -1)
+ ret = false;
+ if (sudo_ev_add(closure->evbase, closure->write_ev, NULL, false) == -1)
+ ret = false;
+ }
+
+ debug_return_bool(ret);
+}
+
+static void
+free_info_messages(InfoMessage **info_msgs, size_t n_info_msgs)
+{
+ debug_decl(free_info_messages, SUDO_DEBUG_UTIL);
+
+ if (info_msgs != NULL) {
+ while (n_info_msgs-- > 0) {
+ if (info_msgs[n_info_msgs]->value_case == INFO_MESSAGE__VALUE_STRLISTVAL) {
+ /* Only strlistval was dynamically allocated */
+ free(info_msgs[n_info_msgs]->u.strlistval->strings);
+ free(info_msgs[n_info_msgs]->u.strlistval);
+ }
+ free(info_msgs[n_info_msgs]);
+ }
+ free(info_msgs);
+ }
+
+ debug_return;
+}
+
+static InfoMessage **
+fmt_info_messages(const struct eventlog *evlog, char *hostname,
+ size_t *n_info_msgs)
+{
+ InfoMessage **info_msgs = NULL;
+ InfoMessage__StringList *runargv = NULL;
+ size_t info_msgs_size, n = 0;
+ debug_decl(fmt_info_messages, SUDO_DEBUG_UTIL);
+
+ /* Split command into a StringList. */
+ runargv = malloc(sizeof(*runargv));
+ if (runargv == NULL)
+ goto oom;
+ info_message__string_list__init(runargv);
+ runargv->strings = split_command(evlog->command, &runargv->n_strings);
+ if (runargv->strings == NULL)
+ goto oom;
+
+ /* The sudo I/O log info file has limited info. */
+ info_msgs_size = 10;
+ info_msgs = calloc(info_msgs_size, sizeof(InfoMessage *));
+ if (info_msgs == NULL)
+ goto oom;
+ for (n = 0; n < info_msgs_size; n++) {
+ info_msgs[n] = malloc(sizeof(InfoMessage));
+ if (info_msgs[n] == NULL)
+ goto oom;
+ info_message__init(info_msgs[n]);
+ }
+
+ /* Fill in info_msgs */
+ n = 0;
+ info_msgs[n]->key = "command";
+ info_msgs[n]->u.strval = evlog->command;
+ info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL;
+ n++;
+
+ info_msgs[n]->key = "columns";
+ info_msgs[n]->u.numval = evlog->columns;
+ info_msgs[n]->value_case = INFO_MESSAGE__VALUE_NUMVAL;
+ n++;
+
+ info_msgs[n]->key = "lines";
+ info_msgs[n]->u.numval = evlog->lines;
+ info_msgs[n]->value_case = INFO_MESSAGE__VALUE_NUMVAL;
+ n++;
+
+ info_msgs[n]->key = "runargv";
+ info_msgs[n]->u.strlistval = runargv;
+ info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRLISTVAL;
+ runargv = NULL;
+ n++;
+
+ if (evlog->rungroup != NULL) {
+ info_msgs[n]->key = "rungroup";
+ info_msgs[n]->u.strval = evlog->rungroup;
+ info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL;
+ n++;
+ }
+
+ info_msgs[n]->key = "runuser";
+ info_msgs[n]->u.strval = evlog->runuser;
+ info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL;
+ n++;
+
+ info_msgs[n]->key = "submitcwd";
+ info_msgs[n]->u.strval = evlog->cwd;
+ info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL;
+ n++;
+
+ info_msgs[n]->key = "submithost";
+ info_msgs[n]->u.strval = hostname;
+ info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL;
+ n++;
+
+ info_msgs[n]->key = "submituser";
+ info_msgs[n]->u.strval = evlog->submituser;
+ info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL;
+ n++;
+
+ info_msgs[n]->key = "ttyname";
+ info_msgs[n]->u.strval = evlog->ttyname;
+ info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL;
+ n++;
+
+ /* Update n_info_msgs. */
+ *n_info_msgs = n;
+
+ /* Avoid leaking unused info_msg structs. */
+ while (n < info_msgs_size) {
+ free(info_msgs[n++]);
+ }
+
+ debug_return_ptr(info_msgs);
+
+oom:
+ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+ free_info_messages(info_msgs, n);
+ if (runargv != NULL) {
+ free(runargv->strings);
+ free(runargv);
+ }
+ *n_info_msgs = 0;
+ debug_return_ptr(NULL);
+}
+
+/*
+ * Build and format a RejectMessage wrapped in a ClientMessage.
+ * Stores the wire format message in the closure's write buffer.
+ * Returns true on success, false on failure.
+ */
+static bool
+fmt_reject_message(struct client_closure *closure)
+{
+ ClientMessage client_msg = CLIENT_MESSAGE__INIT;
+ RejectMessage reject_msg = REJECT_MESSAGE__INIT;
+ TimeSpec tv = TIME_SPEC__INIT;
+ size_t n_info_msgs;
+ bool ret = false;
+ char *hostname;
+ debug_decl(fmt_reject_message, SUDO_DEBUG_UTIL);
+
+ /*
+ * Fill in RejectMessage and add it to ClientMessage.
+ */
+ if ((hostname = sudo_gethostname()) == NULL) {
+ sudo_warn("gethostname");
+ debug_return_bool(false);
+ }
+
+ /* Sudo I/O logs only store start time in seconds. */
+ tv.tv_sec = closure->evlog->submit_time.tv_sec;
+ tv.tv_nsec = closure->evlog->submit_time.tv_nsec;
+ reject_msg.submit_time = &tv;
+
+ /* Why the command was rejected. */
+ reject_msg.reason = closure->reject_reason;
+
+ reject_msg.info_msgs = fmt_info_messages(closure->evlog, hostname,
+ &n_info_msgs);
+ if (reject_msg.info_msgs == NULL)
+ goto done;
+
+ /* Update n_info_msgs. */
+ reject_msg.n_info_msgs = n_info_msgs;
+
+ sudo_debug_printf(SUDO_DEBUG_INFO,
+ "%s: sending RejectMessage, array length %zu", __func__, n_info_msgs);
+
+ /* Schedule ClientMessage */
+ client_msg.u.reject_msg = &reject_msg;
+ client_msg.type_case = CLIENT_MESSAGE__TYPE_REJECT_MSG;
+ ret = fmt_client_message(&closure->write_buf, &client_msg);
+ if (ret) {
+ if (sudo_ev_add(closure->evbase, closure->write_ev, NULL, false) == -1)
+ ret = false;
+ }
+
+done:
+ free_info_messages(reject_msg.info_msgs, n_info_msgs);
+ free(hostname);
+
+ debug_return_bool(ret);
+}
+
+/*
+ * Build and format an AcceptMessage wrapped in a ClientMessage.
+ * Stores the wire format message in the closure's write buffer.
+ * Returns true on success, false on failure.
+ */
+static bool
+fmt_accept_message(struct client_closure *closure)
+{
+ ClientMessage client_msg = CLIENT_MESSAGE__INIT;
+ AcceptMessage accept_msg = ACCEPT_MESSAGE__INIT;
+ TimeSpec tv = TIME_SPEC__INIT;
+ size_t n_info_msgs;
+ bool ret = false;
+ char *hostname;
+ debug_decl(fmt_accept_message, SUDO_DEBUG_UTIL);
+
+ /*
+ * Fill in AcceptMessage and add it to ClientMessage.
+ */
+ if ((hostname = sudo_gethostname()) == NULL) {
+ sudo_warn("gethostname");
+ debug_return_bool(false);
+ }
+
+ /* Sudo I/O logs only store start time in seconds. */
+ tv.tv_sec = closure->evlog->submit_time.tv_sec;
+ tv.tv_nsec = closure->evlog->submit_time.tv_nsec;
+ accept_msg.submit_time = &tv;
+
+ /* Client will send IoBuffer messages. */
+ accept_msg.expect_iobufs = !closure->accept_only;
+
+ accept_msg.info_msgs = fmt_info_messages(closure->evlog, hostname,
+ &n_info_msgs);
+ if (accept_msg.info_msgs == NULL)
+ goto done;
+
+ /* Update n_info_msgs. */
+ accept_msg.n_info_msgs = n_info_msgs;
+
+ sudo_debug_printf(SUDO_DEBUG_INFO,
+ "%s: sending AcceptMessage, array length %zu", __func__, n_info_msgs);
+
+ /* Schedule ClientMessage */
+ client_msg.u.accept_msg = &accept_msg;
+ client_msg.type_case = CLIENT_MESSAGE__TYPE_ACCEPT_MSG;
+ ret = fmt_client_message(&closure->write_buf, &client_msg);
+ if (ret) {
+ if (sudo_ev_add(closure->evbase, closure->write_ev, NULL, false) == -1)
+ ret = false;
+ }
+
+done:
+ free_info_messages(accept_msg.info_msgs, n_info_msgs);
+ free(hostname);
+
+ debug_return_bool(ret);
+}
+
+/*
+ * Build and format a RestartMessage wrapped in a ClientMessage.
+ * Stores the wire format message in the closure's write buffer.
+ * Returns true on success, false on failure.
+ */
+static bool
+fmt_restart_message(struct client_closure *closure)
+{
+ ClientMessage client_msg = CLIENT_MESSAGE__INIT;
+ RestartMessage restart_msg = RESTART_MESSAGE__INIT;
+ TimeSpec tv = TIME_SPEC__INIT;
+ bool ret = false;
+ debug_decl(fmt_restart_message, SUDO_DEBUG_UTIL);
+
+ sudo_debug_printf(SUDO_DEBUG_INFO,
+ "%s: sending RestartMessage, [%lld, %ld]", __func__,
+ (long long)closure->restart.tv_sec, closure->restart.tv_nsec);
+
+ tv.tv_sec = closure->restart.tv_sec;
+ tv.tv_nsec = closure->restart.tv_nsec;
+ restart_msg.resume_point = &tv;
+ restart_msg.log_id = (char *)closure->iolog_id;
+
+ /* Schedule ClientMessage */
+ client_msg.u.restart_msg = &restart_msg;
+ client_msg.type_case = CLIENT_MESSAGE__TYPE_RESTART_MSG;
+ ret = fmt_client_message(&closure->write_buf, &client_msg);
+ if (ret) {
+ if (sudo_ev_add(closure->evbase, closure->write_ev, NULL, false) == -1)
+ ret = false;
+ }
+
+ debug_return_bool(ret);
+}
+
+/*
+ * Build and format an ExitMessage wrapped in a ClientMessage.
+ * Stores the wire format message in the closure's write buffer.
+ * Returns true on success, false on failure.
+ */
+static bool
+fmt_exit_message(struct client_closure *closure)
+{
+ ClientMessage client_msg = CLIENT_MESSAGE__INIT;
+ ExitMessage exit_msg = EXIT_MESSAGE__INIT;
+ bool ret = false;
+ debug_decl(fmt_exit_message, SUDO_DEBUG_UTIL);
+
+ /*
+ * We don't have enough data in a sudo I/O log to create a real
+ * exit message. For example, the exit value and run time are
+ * not known. This results in a zero-sized message.
+ */
+ exit_msg.exit_value = 0;
+
+ sudo_debug_printf(SUDO_DEBUG_INFO,
+ "%s: sending ExitMessage, exit value %d",
+ __func__, exit_msg.exit_value);
+
+ /* Send ClientMessage */
+ client_msg.u.exit_msg = &exit_msg;
+ client_msg.type_case = CLIENT_MESSAGE__TYPE_EXIT_MSG;
+ if (!fmt_client_message(&closure->write_buf, &client_msg))
+ goto done;
+
+ ret = true;
+
+done:
+ debug_return_bool(ret);
+}
+
+/*
+ * Build and format an IoBuffer wrapped in a ClientMessage.
+ * Stores the wire format message in buf.
+ * Returns true on success, false on failure.
+ */
+static bool
+fmt_io_buf(int type, struct client_closure *closure,
+ struct connection_buffer *buf)
+{
+ ClientMessage client_msg = CLIENT_MESSAGE__INIT;
+ IoBuffer iobuf_msg = IO_BUFFER__INIT;
+ TimeSpec delay = TIME_SPEC__INIT;
+ bool ret = false;
+ debug_decl(fmt_io_buf, SUDO_DEBUG_UTIL);
+
+ if (!read_io_buf(closure))
+ goto done;
+
+ /* Fill in IoBuffer. */
+ /* TODO: split buffer if it is too large */
+ delay.tv_sec = closure->timing.delay.tv_sec;
+ delay.tv_nsec = closure->timing.delay.tv_nsec;
+ iobuf_msg.delay = &delay;
+ iobuf_msg.data.data = (void *)closure->buf;
+ iobuf_msg.data.len = closure->timing.u.nbytes;
+
+ sudo_debug_printf(SUDO_DEBUG_INFO,
+ "%s: sending IoBuffer length %zu, type %d, size %zu", __func__,
+ iobuf_msg.data.len, type, io_buffer__get_packed_size(&iobuf_msg));
+
+ /* Send ClientMessage, it doesn't matter which IoBuffer we set. */
+ client_msg.u.ttyout_buf = &iobuf_msg;
+ client_msg.type_case = type;
+ if (!fmt_client_message(buf, &client_msg))
+ goto done;
+
+ ret = true;
+
+done:
+ debug_return_bool(ret);
+}
+
+/*
+ * Build and format a ChangeWindowSize message wrapped in a ClientMessage.
+ * Stores the wire format message in buf.
+ * Returns true on success, false on failure.
+ */
+static bool
+fmt_winsize(struct client_closure *closure, struct connection_buffer *buf)
+{
+ ClientMessage client_msg = CLIENT_MESSAGE__INIT;
+ ChangeWindowSize winsize_msg = CHANGE_WINDOW_SIZE__INIT;
+ TimeSpec delay = TIME_SPEC__INIT;
+ struct timing_closure *timing = &closure->timing;
+ bool ret = false;
+ debug_decl(fmt_winsize, SUDO_DEBUG_UTIL);
+
+ /* Fill in ChangeWindowSize message. */
+ delay.tv_sec = timing->delay.tv_sec;
+ delay.tv_nsec = timing->delay.tv_nsec;
+ winsize_msg.delay = &delay;
+ winsize_msg.rows = timing->u.winsize.lines;
+ winsize_msg.cols = timing->u.winsize.cols;
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending ChangeWindowSize, %dx%d",
+ __func__, winsize_msg.rows, winsize_msg.cols);
+
+ /* Send ClientMessage */
+ client_msg.u.winsize_event = &winsize_msg;
+ client_msg.type_case = CLIENT_MESSAGE__TYPE_WINSIZE_EVENT;
+ if (!fmt_client_message(buf, &client_msg))
+ goto done;
+
+ ret = true;
+
+done:
+ debug_return_bool(ret);
+}
+
+/*
+ * Build and format a CommandSuspend message wrapped in a ClientMessage.
+ * Stores the wire format message in buf.
+ * Returns true on success, false on failure.
+ */
+static bool
+fmt_suspend(struct client_closure *closure, struct connection_buffer *buf)
+{
+ ClientMessage client_msg = CLIENT_MESSAGE__INIT;
+ CommandSuspend suspend_msg = COMMAND_SUSPEND__INIT;
+ TimeSpec delay = TIME_SPEC__INIT;
+ struct timing_closure *timing = &closure->timing;
+ bool ret = false;
+ debug_decl(fmt_suspend, SUDO_DEBUG_UTIL);
+
+ /* Fill in CommandSuspend message. */
+ delay.tv_sec = timing->delay.tv_sec;
+ delay.tv_nsec = timing->delay.tv_nsec;
+ suspend_msg.delay = &delay;
+ if (sig2str(timing->u.signo, closure->buf) == -1)
+ goto done;
+ suspend_msg.signal = closure->buf;
+
+ sudo_debug_printf(SUDO_DEBUG_INFO,
+ "%s: sending CommandSuspend, SIG%s", __func__, suspend_msg.signal);
+
+ /* Send ClientMessage */
+ client_msg.u.suspend_event = &suspend_msg;
+ client_msg.type_case = CLIENT_MESSAGE__TYPE_SUSPEND_EVENT;
+ if (!fmt_client_message(buf, &client_msg))
+ goto done;
+
+ ret = true;
+
+done:
+ debug_return_bool(ret);
+}
+
+/*
+ * Read the next entry for the I/O log timing file and format a ClientMessage.
+ * Stores the wire format message in the closure's write buffer.
+ * Returns true on success, false on failure.
+ */
+static bool
+fmt_next_iolog(struct client_closure *closure)
+{
+ struct timing_closure *timing = &closure->timing;
+ struct connection_buffer *buf = &closure->write_buf;
+ bool ret = false;
+ debug_decl(fmt_next_iolog, SUDO_DEBUG_UTIL);
+
+ if (buf->len != 0) {
+ sudo_warnx(U_("%s: write buffer already in use"), __func__);
+ debug_return_bool(false);
+ }
+
+ /* TODO: fill write buffer with multiple messages */
+again:
+ switch (iolog_read_timing_record(&closure->iolog_files[IOFD_TIMING], timing)) {
+ case 0:
+ /* OK */
+ break;
+ case 1:
+ /* no more IO buffers */
+ closure->state = SEND_EXIT;
+ debug_return_bool(fmt_exit_message(closure));
+ case -1:
+ default:
+ debug_return_bool(false);
+ }
+
+ /* Track elapsed time for comparison with commit points. */
+ sudo_timespecadd(&timing->delay, &closure->elapsed, &closure->elapsed);
+
+ /* If we have a restart point, ignore records until we hit it. */
+ if (sudo_timespecisset(&closure->restart)) {
+ if (sudo_timespeccmp(&closure->restart, &closure->elapsed, >=))
+ goto again;
+ sudo_timespecclear(&closure->restart); /* caught up */
+ }
+
+ switch (timing->event) {
+ case IO_EVENT_STDIN:
+ ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDIN_BUF, closure, buf);
+ break;
+ case IO_EVENT_STDOUT:
+ ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDOUT_BUF, closure, buf);
+ break;
+ case IO_EVENT_STDERR:
+ ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDERR_BUF, closure, buf);
+ break;
+ case IO_EVENT_TTYIN:
+ ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_TTYIN_BUF, closure, buf);
+ break;
+ case IO_EVENT_TTYOUT:
+ ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_TTYOUT_BUF, closure, buf);
+ break;
+ case IO_EVENT_WINSIZE:
+ ret = fmt_winsize(closure, buf);
+ break;
+ case IO_EVENT_SUSPEND:
+ ret = fmt_suspend(closure, buf);
+ break;
+ default:
+ sudo_warnx(U_("unexpected I/O event %d"), timing->event);
+ break;
+ }
+
+ debug_return_bool(ret);
+}
+
+/*
+ * Additional work to do after a ClientMessage was sent to the server.
+ * Advances state and formats the next ClientMessage (if any).
+ */
+static bool
+client_message_completion(struct client_closure *closure)
+{
+ debug_decl(client_message_completion, SUDO_DEBUG_UTIL);
+
+ switch (closure->state) {
+ case RECV_HELLO:
+ /* Wait for ServerHello, nothing to write until then. */
+ sudo_ev_del(closure->evbase, closure->write_ev);
+ break;
+ case SEND_ACCEPT:
+ if (closure->accept_only) {
+ closure->state = SEND_EXIT;
+ debug_return_bool(fmt_exit_message(closure));
+ }
+ FALLTHROUGH;
+ case SEND_RESTART:
+ closure->state = SEND_IO;
+ FALLTHROUGH;
+ case SEND_IO:
+ /* fmt_next_iolog() will advance state on EOF. */
+ if (!fmt_next_iolog(closure))
+ debug_return_bool(false);
+ break;
+ case SEND_REJECT:
+ /* Done writing, wait for server to close connection. */
+ sudo_ev_del(closure->evbase, closure->write_ev);
+ closure->state = FINISHED;
+ break;
+ case SEND_EXIT:
+ /* Done writing, wait for final commit point if sending I/O. */
+ sudo_ev_del(closure->evbase, closure->write_ev);
+ closure->state = closure->accept_only ? FINISHED : CLOSING;
+ break;
+ default:
+ sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state);
+ debug_return_bool(false);
+ }
+ debug_return_bool(true);
+}
+
+/*
+ * Respond to a ServerHello message from the server.
+ * Returns true on success, false on error.
+ */
+static bool
+handle_server_hello(ServerHello *msg, struct client_closure *closure)
+{
+ size_t n;
+ debug_decl(handle_server_hello, SUDO_DEBUG_UTIL);
+
+ if (closure->state != RECV_HELLO) {
+ sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state);
+ debug_return_bool(false);
+ }
+
+ /* Check that ServerHello is valid. */
+ if (msg->server_id == NULL || msg->server_id[0] == '\0') {
+ sudo_warnx("%s", U_("invalid ServerHello"));
+ debug_return_bool(false);
+ }
+
+ if (!testrun) {
+ printf("Server ID: %s\n", msg->server_id);
+ /* TODO: handle redirect */
+ if (msg->redirect != NULL && msg->redirect[0] != '\0')
+ printf("Redirect: %s\n", msg->redirect);
+ for (n = 0; n < msg->n_servers; n++) {
+ printf("Server %zu: %s\n", n + 1, msg->servers[n]);
+ }
+ }
+
+ debug_return_bool(true);
+}
+
+/*
+ * Respond to a CommitPoint message from the server.
+ * Returns true on success, false on error.
+ */
+static bool
+handle_commit_point(TimeSpec *commit_point, struct client_closure *closure)
+{
+ debug_decl(handle_commit_point, SUDO_DEBUG_UTIL);
+
+ /* Only valid after we have sent an IO buffer. */
+ if (closure->state < SEND_IO) {
+ sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state);
+ debug_return_bool(false);
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: commit point: [%lld, %d]",
+ __func__, (long long)commit_point->tv_sec, commit_point->tv_nsec);
+ closure->committed.tv_sec = commit_point->tv_sec;
+ closure->committed.tv_nsec = commit_point->tv_nsec;
+
+ debug_return_bool(true);
+}
+
+/*
+ * Respond to a LogId message from the server.
+ * Always returns true.
+ */
+static bool
+handle_log_id(char *id, struct client_closure *closure)
+{
+ debug_decl(handle_log_id, SUDO_DEBUG_UTIL);
+
+ if (!testrun)
+ printf("Remote log ID: %s\n", id);
+
+ debug_return_bool(true);
+}
+
+/*
+ * Respond to a ServerError message from the server.
+ * Always returns false.
+ */
+static bool
+handle_server_error(char *errmsg, struct client_closure *closure)
+{
+ debug_decl(handle_server_error, SUDO_DEBUG_UTIL);
+
+ sudo_warnx(U_("error message received from server: %s"), errmsg);
+ debug_return_bool(false);
+}
+
+/*
+ * Respond to a ServerAbort message from the server.
+ * Always returns false.
+ */
+static bool
+handle_server_abort(char *errmsg, struct client_closure *closure)
+{
+ debug_decl(handle_server_abort, SUDO_DEBUG_UTIL);
+
+ sudo_warnx(U_("abort message received from server: %s"), errmsg);
+ debug_return_bool(false);
+}
+
+/*
+ * Respond to a ServerMessage from the server.
+ * Returns true on success, false on error.
+ */
+static bool
+handle_server_message(uint8_t *buf, size_t len,
+ struct client_closure *closure)
+{
+ ServerMessage *msg;
+ bool ret = false;
+ debug_decl(handle_server_message, SUDO_DEBUG_UTIL);
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: unpacking ServerMessage", __func__);
+ msg = server_message__unpack(NULL, len, buf);
+ if (msg == NULL) {
+ sudo_warnx("%s", U_("unable to unpack ServerMessage"));
+ debug_return_bool(false);
+ }
+
+ switch (msg->type_case) {
+ case SERVER_MESSAGE__TYPE_HELLO:
+ if ((ret = handle_server_hello(msg->u.hello, closure))) {
+ if (sudo_timespecisset(&closure->restart)) {
+ closure->state = SEND_RESTART;
+ ret = fmt_restart_message(closure);
+ } else if (closure->reject_reason != NULL) {
+ closure->state = SEND_REJECT;
+ ret = fmt_reject_message(closure);
+ } else {
+ closure->state = SEND_ACCEPT;
+ ret = fmt_accept_message(closure);
+ }
+ }
+ break;
+ case SERVER_MESSAGE__TYPE_COMMIT_POINT:
+ ret = handle_commit_point(msg->u.commit_point, closure);
+ if (sudo_timespeccmp(&closure->elapsed, &closure->committed, ==)) {
+ sudo_ev_del(closure->evbase, closure->read_ev);
+ closure->state = FINISHED;
+ if (++finished_transmissions == nr_of_conns)
+ sudo_ev_loopexit(closure->evbase);
+ }
+ break;
+ case SERVER_MESSAGE__TYPE_LOG_ID:
+ ret = handle_log_id(msg->u.log_id, closure);
+ break;
+ case SERVER_MESSAGE__TYPE_ERROR:
+ ret = handle_server_error(msg->u.error, closure);
+ closure->state = ERROR;
+ break;
+ case SERVER_MESSAGE__TYPE_ABORT:
+ ret = handle_server_abort(msg->u.abort, closure);
+ closure->state = ERROR;
+ break;
+ default:
+ sudo_warnx(U_("%s: unexpected type_case value %d"),
+ __func__, msg->type_case);
+ break;
+ }
+
+ server_message__free_unpacked(msg, NULL);
+ debug_return_bool(ret);
+}
+
+/*
+ * Read and unpack a ServerMessage (read callback).
+ */
+static void
+server_msg_cb(int fd, int what, void *v)
+{
+ struct client_closure *closure = v;
+ struct connection_buffer *buf = &closure->read_buf;
+ ssize_t nread;
+ uint32_t msg_len;
+ debug_decl(server_msg_cb, SUDO_DEBUG_UTIL);
+
+ /* For TLS we may need to read as part of SSL_write(). */
+ if (closure->write_instead_of_read) {
+ closure->write_instead_of_read = false;
+ client_msg_cb(fd, what, v);
+ debug_return;
+ }
+
+ if (what == SUDO_EV_TIMEOUT) {
+ sudo_warnx("%s", U_("timeout reading from server"));
+ goto bad;
+ }
+
+#if defined(HAVE_OPENSSL)
+ if (cert != NULL) {
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: reading ServerMessage (TLS)", __func__);
+ nread = SSL_read(closure->ssl, buf->data + buf->len, buf->size - buf->len);
+ if (nread <= 0) {
+ const char *errstr;
+ int err;
+
+ switch (SSL_get_error(closure->ssl, nread)) {
+ case SSL_ERROR_ZERO_RETURN:
+ /* ssl connection shutdown cleanly */
+ nread = 0;
+ break;
+ case SSL_ERROR_WANT_READ:
+ /* ssl wants to read more, read event is always active */
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_read returns SSL_ERROR_WANT_READ");
+ debug_return;
+ case SSL_ERROR_WANT_WRITE:
+ /* ssl wants to write, schedule a write if not pending */
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_read returns SSL_ERROR_WANT_WRITE");
+ if (!sudo_ev_pending(closure->write_ev, SUDO_EV_WRITE, NULL)) {
+ /* Enable a temporary write event. */
+ if (sudo_ev_add(closure->evbase, closure->write_ev, NULL, false) == -1) {
+ sudo_warnx("%s", U_("unable to add event to queue"));
+ goto bad;
+ }
+ closure->temporary_write_event = true;
+ }
+ /* Redirect write event to finish SSL_read() */
+ closure->read_instead_of_write = true;
+ debug_return;
+ case SSL_ERROR_SSL:
+ /*
+ * For TLS 1.3, if the cert verify function on the server
+ * returns an error, OpenSSL will send an internal error
+ * alert when we read ServerHello. Convert to a more useful
+ * message and hope that no actual internal error occurs.
+ */
+ err = ERR_get_error();
+ if (closure->state == RECV_HELLO &&
+ ERR_GET_REASON(err) == SSL_R_TLSV1_ALERT_INTERNAL_ERROR) {
+ errstr = "host name does not match certificate";
+ } else {
+ errstr = ERR_reason_error_string(err);
+ }
+ sudo_warnx("%s", errstr);
+ goto bad;
+ case SSL_ERROR_SYSCALL:
+ sudo_warn("recv");
+ goto bad;
+ default:
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx("recv: %s", errstr);
+ goto bad;
+ }
+ }
+ } else
+#endif
+ {
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: reading ServerMessage", __func__);
+ nread = recv(fd, buf->data + buf->len, buf->size - buf->len, 0);
+ }
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received %zd bytes from server",
+ __func__, nread);
+ switch (nread) {
+ case -1:
+ if (errno == EAGAIN)
+ debug_return;
+ sudo_warn("recv");
+ goto bad;
+ case 0:
+ if (closure->state != FINISHED)
+ sudo_warnx("%s", U_("premature EOF"));
+ goto bad;
+ default:
+ break;
+ }
+ buf->len += nread;
+
+ while (buf->len - buf->off >= sizeof(msg_len)) {
+ /* Read wire message size (uint32_t in network byte order). */
+ memcpy(&msg_len, buf->data + buf->off, sizeof(msg_len));
+ msg_len = ntohl(msg_len);
+
+ if (msg_len > MESSAGE_SIZE_MAX) {
+ sudo_warnx(U_("server message too large: %u"), msg_len);
+ goto bad;
+ }
+
+ if (msg_len + sizeof(msg_len) > buf->len - buf->off) {
+ /* Incomplete message, we'll read the rest next time. */
+ if (!expand_buf(buf, msg_len + sizeof(msg_len)))
+ goto bad;
+ debug_return;
+ }
+
+ /* Parse ServerMessage, could be zero bytes. */
+ sudo_debug_printf(SUDO_DEBUG_INFO,
+ "%s: parsing ServerMessage, size %u", __func__, msg_len);
+ buf->off += sizeof(msg_len);
+ if (!handle_server_message(buf->data + buf->off, msg_len, closure))
+ goto bad;
+ buf->off += msg_len;
+ }
+ buf->len -= buf->off;
+ buf->off = 0;
+ debug_return;
+bad:
+ sudo_ev_del(closure->evbase, closure->read_ev);
+ debug_return;
+}
+
+/*
+ * Send a ClientMessage to the server (write callback).
+ */
+static void
+client_msg_cb(int fd, int what, void *v)
+{
+ struct client_closure *closure = v;
+ struct connection_buffer *buf = &closure->write_buf;
+ ssize_t nwritten;
+ debug_decl(client_msg_cb, SUDO_DEBUG_UTIL);
+
+ /* For TLS we may need to write as part of SSL_read(). */
+ if (closure->read_instead_of_write) {
+ closure->read_instead_of_write = false;
+ /* Delete write event if it was only due to SSL_read(). */
+ if (closure->temporary_write_event) {
+ closure->temporary_write_event = false;
+ sudo_ev_del(closure->evbase, closure->write_ev);
+ }
+ server_msg_cb(fd, what, v);
+ debug_return;
+ }
+
+ if (what == SUDO_EV_TIMEOUT) {
+ sudo_warnx("%s", U_("timeout writing to server"));
+ goto bad;
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO,
+ "%s: sending %u bytes to server", __func__, buf->len - buf->off);
+
+#if defined(HAVE_OPENSSL)
+ if (cert != NULL) {
+ nwritten = SSL_write(closure->ssl, buf->data + buf->off, buf->len - buf->off);
+ if (nwritten <= 0) {
+ const char *errstr;
+
+ switch (SSL_get_error(closure->ssl, nwritten)) {
+ case SSL_ERROR_ZERO_RETURN:
+ /* ssl connection shutdown */
+ goto bad;
+ case SSL_ERROR_WANT_READ:
+ /* ssl wants to read, read event always active */
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_write returns SSL_ERROR_WANT_READ");
+ /* Redirect read event to finish SSL_write() */
+ closure->write_instead_of_read = true;
+ debug_return;
+ case SSL_ERROR_WANT_WRITE:
+ /* ssl wants to write more, write event remains active */
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_write returns SSL_ERROR_WANT_WRITE");
+ debug_return;
+ case SSL_ERROR_SYSCALL:
+ sudo_warn("recv");
+ goto bad;
+ default:
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx("send: %s", errstr);
+ goto bad;
+ }
+ }
+ } else
+#endif
+ {
+ nwritten = send(fd, buf->data + buf->off, buf->len - buf->off, 0);
+ }
+ if (nwritten == -1) {
+ sudo_warn("send");
+ goto bad;
+ }
+ buf->off += nwritten;
+
+ if (buf->off == buf->len) {
+ /* sent entire message */
+ sudo_debug_printf(SUDO_DEBUG_INFO,
+ "%s: finished sending %u bytes to server", __func__, buf->len);
+ buf->off = 0;
+ buf->len = 0;
+ if (!client_message_completion(closure))
+ goto bad;
+ }
+ debug_return;
+
+bad:
+ sudo_ev_del(closure->evbase, closure->read_ev);
+ sudo_ev_del(closure->evbase, closure->write_ev);
+ debug_return;
+}
+
+/*
+ * Parse a timespec on the command line of the form
+ * seconds[,nanoseconds]
+ */
+static bool
+parse_timespec(struct timespec *ts, char *strval)
+{
+ const char *errstr;
+ char *nsecstr;
+ debug_decl(parse_timespec, SUDO_DEBUG_UTIL);
+
+ if ((nsecstr = strchr(strval, ',')) != NULL)
+ *nsecstr++ = '\0';
+
+ ts->tv_nsec = 0;
+ ts->tv_sec = sudo_strtonum(strval, 0, TIME_T_MAX, &errstr);
+ if (errstr != NULL) {
+ sudo_warnx(U_("%s: %s"), strval, U_(errstr));
+ debug_return_bool(false);
+ }
+
+ if (nsecstr != NULL) {
+ ts->tv_nsec = sudo_strtonum(nsecstr, 0, LONG_MAX, &errstr);
+ if (errstr != NULL) {
+ sudo_warnx(U_("%s: %s"), nsecstr, U_(errstr));
+ debug_return_bool(false);
+ }
+ }
+
+ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: parsed timespec [%lld, %ld]",
+ __func__, (long long)ts->tv_sec, ts->tv_nsec);
+ debug_return_bool(true);
+}
+
+#if defined(HAVE_OPENSSL)
+/*
+ * Check that the server's certificate is valid that it contains the
+ * server name or IP address.
+ * Returns 0 if the cert is invalid, else 1.
+ */
+static int
+verify_peer_identity(int preverify_ok, X509_STORE_CTX *ctx)
+{
+ X509 *current_cert;
+ X509 *peer_cert;
+ debug_decl(verify_peer_identity, SUDO_DEBUG_UTIL);
+
+ /* if pre-verification of the cert failed, just propagate that result back */
+ if (preverify_ok != 1) {
+ debug_return_int(0);
+ }
+
+ /* since this callback is called for each cert in the chain,
+ * check that current cert is the peer's certificate
+ */
+ current_cert = X509_STORE_CTX_get_current_cert(ctx);
+ peer_cert = X509_STORE_CTX_get0_cert(ctx);
+ if (current_cert != peer_cert) {
+ debug_return_int(1);
+ }
+
+ if (validate_hostname(peer_cert, server_name, server_ip, 0) == MatchFound) {
+ debug_return_int(1);
+ }
+
+ debug_return_int(0);
+}
+
+static SSL_CTX *
+init_tls_client_context(const char *ca_bundle_file, const char *cert_file, const char *key_file)
+{
+ const SSL_METHOD *method;
+ SSL_CTX *ctx = NULL;
+ debug_decl(init_tls_client_context, SUDO_DEBUG_UTIL);
+
+ SSL_library_init();
+ OpenSSL_add_all_algorithms();
+ SSL_load_error_strings();
+
+ if ((method = TLS_client_method()) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "creation of SSL_METHOD failed: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto bad;
+ }
+ if ((ctx = SSL_CTX_new(method)) == NULL) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "creation of new SSL_CTX object failed: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto bad;
+ }
+#ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION
+ if (!SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to restrict min. protocol version: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto bad;
+ }
+#else
+ SSL_CTX_set_options(ctx,
+ SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3|SSL_OP_NO_TLSv1|SSL_OP_NO_TLSv1_1);
+#endif
+
+ if (cert_file) {
+ if (!SSL_CTX_use_certificate_chain_file(ctx, cert_file)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to load cert to the ssl context: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto bad;
+ }
+ if (!SSL_CTX_use_PrivateKey_file(ctx, key_file, X509_FILETYPE_PEM)) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "unable to load key to the ssl context: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto bad;
+ }
+ }
+
+ if (ca_bundle_file != NULL) {
+ /* sets the location of the CA bundle file for verification purposes */
+ if (SSL_CTX_load_verify_locations(ctx, ca_bundle_file, NULL) <= 0) {
+ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+ "calling SSL_CTX_load_verify_locations() failed: %s",
+ ERR_error_string(ERR_get_error(), NULL));
+ goto bad;
+ }
+ }
+
+ if (verify_server) {
+ /* verify server cert during the handshake */
+ SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_peer_identity);
+ }
+
+ goto done;
+
+bad:
+ SSL_CTX_free(ctx);
+
+done:
+ debug_return_ptr(ctx);
+}
+
+static void
+tls_connect_cb(int sock, int what, void *v)
+{
+ struct client_closure *closure = v;
+ struct sudo_event_base *evbase = closure->evbase;
+ struct timespec timeo = { TLS_HANDSHAKE_TIMEO_SEC, 0 };
+ const char *errstr;
+ int con_stat;
+ debug_decl(tls_connect_cb, SUDO_DEBUG_UTIL);
+
+ if (what == SUDO_EV_TIMEOUT) {
+ sudo_warnx("%s", U_("TLS handshake timeout occurred"));
+ goto bad;
+ }
+
+ con_stat = SSL_connect(closure->ssl);
+
+ if (con_stat == 1) {
+ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
+ "SSL_connect successful");
+ closure->tls_connect_state = true;
+ } else {
+ switch (SSL_get_error(closure->ssl, con_stat)) {
+ /* TLS handshake is not finished, reschedule event */
+ case SSL_ERROR_WANT_READ:
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_connect returns SSL_ERROR_WANT_READ");
+ if (what != SUDO_EV_READ) {
+ if (sudo_ev_set(closure->tls_connect_ev, closure->sock,
+ SUDO_EV_READ, tls_connect_cb, closure) == -1) {
+ sudo_warnx("%s", U_("unable to set event"));
+ goto bad;
+ }
+ }
+ if (sudo_ev_add(evbase, closure->tls_connect_ev, &timeo, false) == -1) {
+ sudo_warnx("%s", U_("unable to add event to queue"));
+ goto bad;
+ }
+ break;
+ case SSL_ERROR_WANT_WRITE:
+ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
+ "SSL_connect returns SSL_ERROR_WANT_WRITE");
+ if (what != SUDO_EV_WRITE) {
+ if (sudo_ev_set(closure->tls_connect_ev, closure->sock,
+ SUDO_EV_WRITE, tls_connect_cb, closure) == -1) {
+ sudo_warnx("%s", U_("unable to set event"));
+ goto bad;
+ }
+ }
+ if (sudo_ev_add(evbase, closure->tls_connect_ev, &timeo, false) == -1) {
+ sudo_warnx("%s", U_("unable to add event to queue"));
+ goto bad;
+ }
+ break;
+ case SSL_ERROR_SYSCALL:
+ sudo_warnx(U_("TLS connection failed: %s"), strerror(errno));
+ goto bad;
+ default:
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("TLS connection failed: %s"), errstr);
+ goto bad;
+ }
+ }
+
+ if (closure->tls_connect_state) {
+ if (!testrun) {
+ printf("Negotiated protocol version: %s\n", SSL_get_version(closure->ssl));
+ printf("Negotiated ciphersuite: %s\n", SSL_get_cipher(closure->ssl));
+ }
+
+ /* Done with TLS connect, send ClientHello */
+ sudo_ev_free(closure->tls_connect_ev);
+ closure->tls_connect_ev = NULL;
+ if (!fmt_client_hello(closure))
+ goto bad;
+ }
+
+ debug_return;
+
+bad:
+ sudo_ev_loopbreak(evbase);
+ debug_return;
+}
+
+static bool
+tls_setup(struct client_closure *closure)
+{
+ const char *errstr;
+ debug_decl(tls_setup, SUDO_DEBUG_UTIL);
+
+ if ((ssl_ctx = init_tls_client_context(ca_bundle, cert, key)) == NULL) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("Unable to initialize ssl context: %s"), errstr);
+ goto bad;
+ }
+ if ((closure->ssl = SSL_new(ssl_ctx)) == NULL) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("Unable to allocate ssl object: %s"), errstr);
+ goto bad;
+ }
+ if (SSL_set_fd(closure->ssl, closure->sock) <= 0) {
+ errstr = ERR_reason_error_string(ERR_get_error());
+ sudo_warnx(U_("Unable to attach socket to the ssl object: %s"),
+ errstr);
+ goto bad;
+ }
+
+ if (sudo_ev_add(closure->evbase, closure->tls_connect_ev, NULL, false) == -1) {
+ sudo_warnx("%s", U_("unable to add event to queue"));
+ goto bad;
+ }
+
+ debug_return_bool(true);
+
+bad:
+ debug_return_bool(false);
+}
+#endif /* HAVE_OPENSSL */
+
+/*
+ * Free client closure contents.
+ */
+static void
+client_closure_free(struct client_closure *closure)
+{
+ debug_decl(connection_closure_free, SUDO_DEBUG_UTIL);
+
+ if (closure != NULL) {
+ TAILQ_REMOVE(&connections, closure, entries);
+#if defined(HAVE_OPENSSL)
+ if (closure->ssl != NULL) {
+ SSL_shutdown(closure->ssl);
+ SSL_free(closure->ssl);
+ }
+ sudo_ev_free(closure->tls_connect_ev);
+#endif
+ sudo_ev_free(closure->read_ev);
+ sudo_ev_free(closure->write_ev);
+ free(closure->read_buf.data);
+ free(closure->write_buf.data);
+ free(closure->buf);
+ close(closure->sock);
+ free(closure);
+ }
+
+ debug_return;
+}
+
+/*
+ * Initialize a new client closure
+ */
+static struct client_closure *
+client_closure_alloc(int sock, struct sudo_event_base *base,
+ struct timespec *elapsed, struct timespec *restart, const char *iolog_id,
+ char *reject_reason, bool accept_only, struct eventlog *evlog)
+{
+ struct client_closure *closure;
+ debug_decl(client_closure_alloc, SUDO_DEBUG_UTIL);
+
+ if ((closure = calloc(1, sizeof(*closure))) == NULL)
+ debug_return_ptr(NULL);
+
+ closure->sock = sock;
+ closure->evbase = base;
+
+ TAILQ_INSERT_TAIL(&connections, closure, entries);
+
+ closure->state = RECV_HELLO;
+ closure->accept_only = accept_only;
+ closure->reject_reason = reject_reason;
+ closure->evlog = evlog;
+
+ closure->elapsed.tv_sec = elapsed->tv_sec;
+ closure->elapsed.tv_nsec = elapsed->tv_nsec;
+ closure->restart.tv_sec = restart->tv_sec;
+ closure->restart.tv_nsec = restart->tv_nsec;
+
+ closure->iolog_id = iolog_id;
+
+ closure->read_buf.size = 8 * 1024;
+ closure->read_buf.data = malloc(closure->read_buf.size);
+ if (closure->read_buf.data == NULL)
+ goto bad;
+
+ closure->read_ev = sudo_ev_alloc(sock, SUDO_EV_READ|SUDO_EV_PERSIST,
+ server_msg_cb, closure);
+ if (closure->read_ev == NULL)
+ goto bad;
+
+ closure->write_ev = sudo_ev_alloc(sock, SUDO_EV_WRITE|SUDO_EV_PERSIST,
+ client_msg_cb, closure);
+ if (closure->write_ev == NULL)
+ goto bad;
+
+#if defined(HAVE_OPENSSL)
+ if (cert != NULL) {
+ closure->tls_connect_ev = sudo_ev_alloc(sock, SUDO_EV_WRITE,
+ tls_connect_cb, closure);
+ if (closure->tls_connect_ev == NULL)
+ goto bad;
+ }
+#endif
+
+ debug_return_ptr(closure);
+bad:
+ client_closure_free(closure);
+ debug_return_ptr(NULL);
+}
+
+#if defined(HAVE_OPENSSL)
+static const char short_opts[] = "Ah:i:np:r:R:t:b:c:k:V";
+#else
+static const char short_opts[] = "Ah:i:Ip:r:R:t:V";
+#endif
+static struct option long_opts[] = {
+ { "accept", no_argument, NULL, 'A' },
+ { "help", no_argument, NULL, 1 },
+ { "host", required_argument, NULL, 'h' },
+ { "iolog-id", required_argument, NULL, 'i' },
+ { "port", required_argument, NULL, 'p' },
+ { "restart", required_argument, NULL, 'r' },
+ { "reject", required_argument, NULL, 'R' },
+ { "test", optional_argument, NULL, 't' },
+#if defined(HAVE_OPENSSL)
+ { "ca-bundle", required_argument, NULL, 'b' },
+ { "cert", required_argument, NULL, 'c' },
+ { "key", required_argument, NULL, 'k' },
+ { "no-verify", no_argument, NULL, 'n' },
+#endif
+ { "version", no_argument, NULL, 'V' },
+ { NULL, no_argument, NULL, 0 },
+};
+
+sudo_dso_public int main(int argc, char *argv[]);
+
+int
+main(int argc, char *argv[])
+{
+ struct client_closure *closure = NULL;
+ struct sudo_event_base *evbase;
+ struct eventlog *evlog;
+ const char *port = NULL;
+ struct timespec restart = { 0, 0 };
+ struct timespec elapsed = { 0, 0 };
+ bool accept_only = false;
+ char *reject_reason = NULL;
+ const char *iolog_id = NULL;
+ const char *open_mode = "r";
+ const char *errstr;
+ int ch, sock, iolog_dir_fd, finished;
+ debug_decl_vars(main, SUDO_DEBUG_MAIN);
+
+#if defined(SUDO_DEVEL) && defined(__OpenBSD__)
+ {
+ extern char *malloc_options;
+ malloc_options = "S";
+ }
+#endif
+
+ signal(SIGPIPE, SIG_IGN);
+
+ initprogname(argc > 0 ? argv[0] : "sudo_sendlog");
+ setlocale(LC_ALL, "");
+ bindtextdomain("sudo", LOCALEDIR); /* XXX - add logsrvd domain */
+ textdomain("sudo");
+
+ /* Read sudo.conf and initialize the debug subsystem. */
+ if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1)
+ exit(EXIT_FAILURE);
+ sudo_debug_register(getprogname(), NULL, NULL,
+ sudo_conf_debug_files(getprogname()));
+
+ if (protobuf_c_version_number() < 1003000)
+ sudo_fatalx("%s", U_("Protobuf-C version 1.3 or higher required"));
+
+ while ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) {
+ switch (ch) {
+ case 'A':
+ accept_only = true;
+ break;
+ case 'h':
+ server_name = optarg;
+ break;
+ case 'i':
+ iolog_id = optarg;
+ break;
+ case 'R':
+ reject_reason = optarg;
+ break;
+ case 'p':
+ port = optarg;
+ break;
+ case 'r':
+ if (!parse_timespec(&restart, optarg))
+ goto bad;
+ open_mode = "r+";
+ break;
+ case 't':
+ nr_of_conns = sudo_strtonum(optarg, 1, INT_MAX, &errstr);
+ if (errstr != NULL) {
+ sudo_warnx(U_("%s: %s"), optarg, U_(errstr));
+ goto bad;
+ }
+ testrun = true;
+ break;
+ case 1:
+ help();
+ break;
+#if defined(HAVE_OPENSSL)
+ case 'b':
+ ca_bundle = optarg;
+ break;
+ case 'c':
+ cert = optarg;
+ break;
+ case 'k':
+ key = optarg;
+ break;
+ case 'n':
+ verify_server = false;
+ break;
+#endif
+ case 'V':
+ (void)printf(_("%s version %s\n"), getprogname(),
+ PACKAGE_VERSION);
+ return 0;
+ default:
+ usage(true);
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+#if defined(HAVE_OPENSSL)
+ /* if no key file is given explicitly, try to load the key from the cert */
+ if (cert != NULL) {
+ if (key == NULL)
+ key = cert;
+ if (port == NULL)
+ port = DEFAULT_PORT_TLS;
+ }
+#endif
+ if (port == NULL)
+ port = DEFAULT_PORT;
+
+ if (sudo_timespecisset(&restart) != (iolog_id != NULL)) {
+ sudo_warnx("%s", U_("both restart point and iolog ID must be specified"));
+ usage(true);
+ }
+ if (sudo_timespecisset(&restart) && (accept_only || reject_reason)) {
+ sudo_warnx("%s", U_("a restart point may not be set when no I/O is sent"));
+ usage(true);
+ }
+
+ /* Remaining arg should be to I/O log dir to send. */
+ if (argc != 1)
+ usage(true);
+ iolog_dir = argv[0];
+ if ((iolog_dir_fd = open(iolog_dir, O_RDONLY)) == -1) {
+ sudo_warn("%s", iolog_dir);
+ goto bad;
+ }
+
+ /* Parse I/O log info file. */
+ if ((evlog = iolog_parse_loginfo(iolog_dir_fd, iolog_dir)) == NULL)
+ goto bad;
+
+ if ((evbase = sudo_ev_base_alloc()) == NULL)
+ sudo_fatal(NULL);
+
+ if (testrun)
+ printf("connecting clients...\n");
+
+ for (int i = 0; i < nr_of_conns; i++) {
+ sock = connect_server(server_name, port);
+ if (sock == -1)
+ goto bad;
+
+ if (!testrun)
+ printf("Connected to %s:%s\n", server_name, port);
+
+ closure = client_closure_alloc(sock, evbase, &elapsed, &restart,
+ iolog_id, reject_reason, accept_only, evlog);
+ if (closure == NULL)
+ goto bad;
+
+ /* Open the I/O log files and seek to restart point if there is one. */
+ if (!iolog_open_all(iolog_dir_fd, iolog_dir, closure->iolog_files, open_mode))
+ goto bad;
+ if (sudo_timespecisset(&closure->restart)) {
+ if (!iolog_seekto(iolog_dir_fd, iolog_dir, closure->iolog_files,
+ &closure->elapsed, &closure->restart))
+ goto bad;
+ }
+
+#if defined(HAVE_OPENSSL)
+ if (cert != NULL) {
+ if (!tls_setup(closure))
+ goto bad;
+ } else
+#endif
+ {
+ /* No TLS, send ClientHello */
+ if (!fmt_client_hello(closure))
+ goto bad;
+ }
+ }
+
+ if (testrun)
+ printf("sending logs...\n");
+
+ struct timespec t_start, t_end, t_result;
+ sudo_gettime_real(&t_start);
+
+ sudo_ev_dispatch(evbase);
+ sudo_ev_base_free(evbase);
+
+ sudo_gettime_real(&t_end);
+ sudo_timespecsub(&t_end, &t_start, &t_result);
+
+ finished = 0;
+ while ((closure = TAILQ_FIRST(&connections)) != NULL) {
+ if (closure->state == FINISHED) {
+ finished++;
+ } else {
+ sudo_warnx(U_("exited prematurely with state %d"), closure->state);
+ sudo_warnx(U_("elapsed time sent to server [%lld, %ld]"),
+ (long long)closure->elapsed.tv_sec, closure->elapsed.tv_nsec);
+ sudo_warnx(U_("commit point received from server [%lld, %ld]"),
+ (long long)closure->committed.tv_sec, closure->committed.tv_nsec);
+ }
+ client_closure_free(closure);
+ }
+ eventlog_free(evlog);
+#if defined(HAVE_OPENSSL)
+ SSL_CTX_free(ssl_ctx);
+#endif
+
+ if (finished != 0) {
+ printf("%d I/O log%s transmitted successfully in %lld.%.9ld seconds\n",
+ finished, nr_of_conns > 1 ? "s" : "",
+ (long long)t_result.tv_sec, t_result.tv_nsec);
+ debug_return_int(EXIT_SUCCESS);
+ }
+
+bad:
+ debug_return_int(EXIT_FAILURE);
+}
diff --git a/logsrvd/sendlog.h b/logsrvd/sendlog.h
new file mode 100644
index 0000000..0262cbb
--- /dev/null
+++ b/logsrvd/sendlog.h
@@ -0,0 +1,76 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2019-2020 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.
+ */
+
+#ifndef SUDO_SENDLOG_H
+#define SUDO_SENDLOG_H
+
+#if PROTOBUF_C_VERSION_NUMBER < 1003000
+# error protobuf-c version 1.30 or higher required
+#endif
+
+#include "config.h"
+
+#if defined(HAVE_OPENSSL)
+# include <openssl/ssl.h>
+#endif
+
+#include "logsrv_util.h"
+
+enum client_state {
+ ERROR,
+ RECV_HELLO,
+ SEND_RESTART,
+ SEND_ACCEPT,
+ SEND_REJECT,
+ SEND_IO,
+ SEND_EXIT,
+ CLOSING,
+ FINISHED
+};
+
+struct client_closure {
+ TAILQ_ENTRY(client_closure) entries;
+ int sock;
+ bool accept_only;
+ bool read_instead_of_write;
+ bool write_instead_of_read;
+ bool temporary_write_event;
+ struct timespec restart;
+ struct timespec elapsed;
+ struct timespec committed;
+ struct timing_closure timing;
+ struct sudo_event_base *evbase;
+ struct connection_buffer read_buf;
+ struct connection_buffer write_buf;
+#if defined(HAVE_OPENSSL)
+ SSL *ssl;
+ struct sudo_event *tls_connect_ev;
+ bool tls_connect_state;
+#endif
+ struct sudo_event *read_ev;
+ struct sudo_event *write_ev;
+ struct eventlog *evlog;
+ struct iolog_file iolog_files[IOFD_MAX];
+ const char *iolog_id;
+ char *reject_reason;
+ char *buf; /* XXX */
+ size_t bufsize; /* XXX */
+ enum client_state state;
+};
+
+#endif /* SUDO_SENDLOG_H */