From f8e5c55a036f0e2e2a958e30456270f3f9eba933 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 27 Apr 2024 14:52:13 +0200 Subject: Adding upstream version 1.9.5p2. Signed-off-by: Daniel Baumann --- logsrvd/Makefile.in | 314 ++++++++ logsrvd/iolog_writer.c | 1088 ++++++++++++++++++++++++++ logsrvd/logsrv_util.c | 159 ++++ logsrvd/logsrv_util.h | 43 + logsrvd/logsrvd.c | 2042 ++++++++++++++++++++++++++++++++++++++++++++++++ logsrvd/logsrvd.h | 162 ++++ logsrvd/logsrvd_conf.c | 1143 +++++++++++++++++++++++++++ logsrvd/sendlog.c | 1893 ++++++++++++++++++++++++++++++++++++++++++++ logsrvd/sendlog.h | 76 ++ 9 files changed, 6920 insertions(+) create mode 100644 logsrvd/Makefile.in create mode 100644 logsrvd/iolog_writer.c create mode 100644 logsrvd/logsrv_util.c create mode 100644 logsrvd/logsrv_util.h create mode 100644 logsrvd/logsrvd.c create mode 100644 logsrvd/logsrvd.h create mode 100644 logsrvd/logsrvd_conf.c create mode 100644 logsrvd/sendlog.c create mode 100644 logsrvd/sendlog.h (limited to 'logsrvd') 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 +# +# 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 + * + * 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 +#include +#include +#include + +#include +#include +#include +#include +#ifdef HAVE_STDBOOL_H +# include +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#include +#include +#include +#include +#include + +#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 + * + * 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 + +#include +#ifdef HAVE_STDBOOL_H +# include +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include +#elif defined(HAVE_INTTYPES_H) +# include +#endif +#include +#include +#include +#include +#include + +#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 + * + * 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 + * + * 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#ifdef HAVE_STDBOOL_H +# include +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include +#elif defined(HAVE_INTTYPES_H) +# include +#endif +#include +#include +#include +#include +#include +#ifdef HAVE_GETOPT_LONG +# include +# else +# include "compat/getopt.h" +#endif /* HAVE_GETOPT_LONG */ + +#if defined(HAVE_OPENSSL) +# include +# include +#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 + * + * 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 +#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 + * + * 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 +#include +#include +#include + +#include +#include +#include +#include +#include +#ifdef HAVE_STDBOOL_H +# include +#else +# include "compat/stdbool.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#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 + * + * 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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#ifdef HAVE_STDBOOL_H +# include +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include +#elif defined(HAVE_INTTYPES_H) +# include +#endif +#include +#include +#include +#include +#include +#ifndef HAVE_GETADDRINFO +# include "compat/getaddrinfo.h" +#endif +#ifdef HAVE_GETOPT_LONG +# include +# else +# include "compat/getopt.h" +#endif /* HAVE_GETOPT_LONG */ + +#if defined(HAVE_OPENSSL) +# include +# include +#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 + * + * 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 +#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 */ -- cgit v1.2.3