diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 14:37:38 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 14:37:38 +0000 |
commit | ae581a19fbe896a797450b9d9573fb66f2735227 (patch) | |
tree | 56c40be8518a29c9351364d13a9676aa83932dc0 /logsrvd | |
parent | Initial commit. (diff) | |
download | sudo-ae581a19fbe896a797450b9d9573fb66f2735227.tar.xz sudo-ae581a19fbe896a797450b9d9573fb66f2735227.zip |
Adding upstream version 1.9.13p3.upstream/1.9.13p3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'logsrvd')
34 files changed, 14601 insertions, 0 deletions
diff --git a/logsrvd/Makefile.in b/logsrvd/Makefile.in new file mode 100644 index 0000000..2a1f0d1 --- /dev/null +++ b/logsrvd/Makefile.in @@ -0,0 +1,595 @@ +# +# SPDX-License-Identifier: ISC +# +# Copyright (c) 2019-2021 Todd C. Miller <Todd.Miller@sudo.ws> +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# +# @configure_input@ +# + +#### Start of system configuration section. #### + +srcdir = @srcdir@ +abs_srcdir = @abs_srcdir@ +top_srcdir = @top_srcdir@ +abs_top_srcdir = @abs_top_srcdir@ +top_builddir = @top_builddir@ +abs_top_builddir = @abs_top_builddir@ +devdir = @devdir@ +scriptdir = $(top_srcdir)/scripts +incdir = $(top_srcdir)/include +rundir = @rundir@ +cross_compiling = @CROSS_COMPILING@ + +# Compiler & tools to use +CC = @CC@ +LIBTOOL = @LIBTOOL@ +SHA1SUM = @SHA1SUM@ +EGREP = @EGREP@ +SED = @SED@ + +# Our install program supports extra flags... +INSTALL = $(SHELL) $(scriptdir)/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 \ + $(top_builddir)/lib/protobuf-c/libprotobuf-c.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 +HARDENING_CFLAGS = @HARDENING_CFLAGS@ +HARDENING_LDFLAGS = @HARDENING_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@ + +# Fuzzers +LIBFUZZSTUB = $(top_builddir)/lib/fuzzstub/libsudo_fuzzstub.la +LIB_FUZZING_ENGINE = @FUZZ_ENGINE@ +FUZZ_PROGS = fuzz_logsrvd_conf +FUZZ_SEED_CORPUS = ${FUZZ_PROGS:=_seed_corpus.zip} +FUZZ_LIBS = $(LIB_FUZZING_ENGINE) $(LIBS) +FUZZ_LDFLAGS = $(LDFLAGS) +FUZZ_MAX_LEN = 4096 +FUZZ_RUNS = 8192 +FUZZ_VERBOSE = + +TEST_PROGS = logsrvd_conf_test +TEST_LIBS = $(LIBS) +TEST_LDFLAGS = $(LDFLAGS) +TEST_VERBOSE = + +# 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 \ + logsrvd_journal.o logsrvd_local.o logsrvd_relay.o \ + logsrvd_queue.o tls_client.o tls_init.o + +SENDLOG_OBJS = logsrv_util.o sendlog.o tls_client.o tls_init.o + +IOBJS = $(LOGSRVD_OBJS:.o=.i) $(SENDLOG_OBJS:.o=.i) + +POBJS = $(IOBJS:.i=.plog) + +LIBOBJDIR = $(top_builddir)/@ac_config_libobj_dir@/ + +VERSION = @PACKAGE_VERSION@ + +FUZZ_LOGSRVD_CONF_OBJS = fuzz_logsrvd_conf.o logsrvd_conf.o tls_init.o + +FUZZ_LOGSRVD_CONF_CORPUS = $(srcdir)/regress/corpus/seed/logsrvd_conf/logsrvd.conf.* + +CONF_TEST_OBJS = logsrvd_conf_test.o logsrvd_conf.o tls_init.o + +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) $(HARDENING_CFLAGS) $< + +.c.lo: + $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_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) $(HARDENING_LDFLAGS) $(LIBS) + +sudo_sendlog: $(SENDLOG_OBJS) $(LT_LIBS) + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(SENDLOG_OBJS) $(LDFLAGS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(LIBS) + +fuzz_logsrvd_conf: $(FUZZ_LOGSRVD_CONF_OBJS) $(LIBFUZZSTUB) $(LT_LIBS) + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(FUZZ_LOGSRVD_CONF_OBJS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(FUZZ_LDFLAGS) $(FUZZ_LIBS) + +logsrvd_conf_test: $(CONF_TEST_OBJS) $(LT_LIBS) + $(LIBTOOL) $(LTFLAGS) --mode=link $(CC) -o $@ $(CONF_TEST_OBJS) $(ASAN_LDFLAGS) $(PIE_LDFLAGS) $(HARDENING_LDFLAGS) $(TEST_LDFLAGS) $(TEST_LIBS) + +fuzz_logsrvd_conf_seed_corpus.zip: + tdir=fuzz_logsrvd_conf.$$$$; \ + mkdir $$tdir; \ + for f in $(FUZZ_LOGSRVD_CONF_CORPUS); do \ + cp $$f $$tdir/`$(SHA1SUM) $$f | $(SED) -e 's/^.*= *//' -e 's/ .*//'`; \ + done; \ + zip -j $@ $$tdir/*; \ + rm -rf $$tdir + +run-fuzz_logsrvd_conf: fuzz_logsrvd_conf + l=`locale -a 2>&1 | $(EGREP) -i '^C\.UTF-?8$$' | $(SED) 1q` || true; \ + test -n "$$l" || l="C"; \ + LC_ALL="$$l"; export LC_ALL; \ + unset LANG || LANG=; \ + unset LANGUAGE || LANGUAGE=; \ + MALLOC_OPTIONS=S; export MALLOC_OPTIONS; \ + MALLOC_CONF="abort:true,junk:true"; export MALLOC_CONF; \ + umask 022; \ + corpus=regress/corpus/logsrvd_conf; \ + mkdir -p $$corpus; \ + for f in $(FUZZ_LOGSRVD_CONF_CORPUS); do \ + cp $$f $$corpus; \ + done; \ + ./fuzz_logsrvd_conf -dict=$(srcdir)/regress/fuzz/fuzz_logsrvd_conf.dict -max_len=$(FUZZ_MAX_LEN) -runs=$(FUZZ_RUNS) $(FUZZ_VERBOSE) $$corpus + +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: + +install-fuzzer: $(FUZZ_PROGS) $(FUZZ_SEED_CORPUS) + @if test X"$(FUZZ_DESTDIR)" = X""; then \ + echo "must set FUZZ_DESTDIR for install-fuzzer target"; \ + else \ + cp $(FUZZ_PROGS) $(FUZZ_SEED_CORPUS) $(FUZZ_DESTDIR); \ + fi + +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) + +fuzz: run-fuzz_logsrvd_conf + +check-fuzzer: $(FUZZ_PROGS) + @if test X"$(cross_compiling)" != X"yes"; then \ + l=`locale -a 2>&1 | $(EGREP) -i '^C\.UTF-?8$$' | $(SED) 1q` || true; \ + test -n "$$l" || l="C"; \ + LC_ALL="$$l"; export LC_ALL; \ + unset LANG || LANG=; \ + unset LANGUAGE || LANGUAGE=; \ + MALLOC_OPTIONS=S; export MALLOC_OPTIONS; \ + MALLOC_CONF="abort:true,junk:true"; export MALLOC_CONF; \ + echo "fuzz_logsrvd_conf: verifying corpus"; \ + ./fuzz_logsrvd_conf $(FUZZ_VERBOSE) $(FUZZ_LOGSRVD_CONF_CORPUS); \ + fi + +check: $(TEST_PROGS) check-fuzzer + @if test X"$(cross_compiling)" != X"yes"; then \ + l=`locale -a 2>&1 | $(EGREP) -i '^C\.UTF-?8$$' | $(SED) 1q` || true; \ + test -n "$$l" || l="C"; \ + LC_ALL="$$l"; export LC_ALL; \ + unset LANG || LANG=; \ + unset LANGUAGE || LANGUAGE=; \ + MALLOC_OPTIONS=S; export MALLOC_OPTIONS; \ + MALLOC_CONF="abort:true,junk:true"; export MALLOC_CONF; \ + builddir=$(abs_top_builddir)/logsrvd; \ + cd $(srcdir) || exit 1; \ + if test -n "@LIBTLS@"; then \ + $$builddir/logsrvd_conf_test $(TEST_VERBOSE) \ + regress/logsrvd_conf/tls/*.in; \ + else \ + $$builddir/logsrvd_conf_test $(TEST_VERBOSE) \ + regress/logsrvd_conf/*.in; \ + fi; \ + fi + +check-verbose: check + +clean: + -$(LIBTOOL) $(LTFLAGS) --mode=clean rm -f $(PROGS) $(FUZZ_PROGS) \ + $(TEST_PROGS) *.lo *.o *.la + -rm -f *.i *.plog stamp-* core *.core core.* + -rm -rf regress/corpus/logsrvd_conf + +mostlyclean: clean + +distclean: clean + -rm -rf Makefile .libs + +clobber: distclean + +realclean: distclean + rm -f TAGS tags + +cleandir: realclean + +.PHONY: clean mostlyclean distclean cleandir clobber realclean \ + $(FUZZ_SEED_CORPUS) run-fuzz_logsrvd_conf + +# Autogenerated dependencies, do not modify +fuzz_logsrvd_conf.o: $(srcdir)/regress/fuzz/fuzz_logsrvd_conf.c \ + $(incdir)/compat/stdbool.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_eventlog.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/logsrv_util.h $(srcdir)/logsrvd.h \ + $(srcdir)/tls_common.h $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/regress/fuzz/fuzz_logsrvd_conf.c +fuzz_logsrvd_conf.i: $(srcdir)/regress/fuzz/fuzz_logsrvd_conf.c \ + $(incdir)/compat/stdbool.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_eventlog.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_iolog.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/logsrv_util.h $(srcdir)/logsrvd.h \ + $(srcdir)/tls_common.h $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +fuzz_logsrvd_conf.plog: fuzz_logsrvd_conf.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/fuzz/fuzz_logsrvd_conf.c --i-file $< --output-file $@ +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_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 $(srcdir)/tls_common.h \ + $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_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_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 $(srcdir)/tls_common.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) $(HARDENING_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 $(srcdir)/tls_common.h \ + $(top_builddir)/config.h $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_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 $(srcdir)/tls_common.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 \ + $(srcdir)/tls_common.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_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 \ + $(srcdir)/tls_common.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 $@ +logsrvd_conf_test.o: $(srcdir)/regress/logsrvd_conf/logsrvd_conf_test.c \ + $(incdir)/compat/stdbool.h $(incdir)/log_server.pb-c.h \ + $(incdir)/protobuf-c/protobuf-c.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_iolog.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/logsrv_util.h \ + $(srcdir)/logsrvd.h $(srcdir)/tls_common.h \ + $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/regress/logsrvd_conf/logsrvd_conf_test.c +logsrvd_conf_test.i: $(srcdir)/regress/logsrvd_conf/logsrvd_conf_test.c \ + $(incdir)/compat/stdbool.h $(incdir)/log_server.pb-c.h \ + $(incdir)/protobuf-c/protobuf-c.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_iolog.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/logsrv_util.h \ + $(srcdir)/logsrvd.h $(srcdir)/tls_common.h \ + $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +logsrvd_conf_test.plog: logsrvd_conf_test.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/regress/logsrvd_conf/logsrvd_conf_test.c --i-file $< --output-file $@ +logsrvd_journal.o: $(srcdir)/logsrvd_journal.c $(incdir)/compat/stdbool.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)/logsrvd.h \ + $(srcdir)/tls_common.h $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/logsrvd_journal.c +logsrvd_journal.i: $(srcdir)/logsrvd_journal.c $(incdir)/compat/stdbool.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)/logsrvd.h \ + $(srcdir)/tls_common.h $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +logsrvd_journal.plog: logsrvd_journal.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/logsrvd_journal.c --i-file $< --output-file $@ +logsrvd_local.o: $(srcdir)/logsrvd_local.c $(incdir)/compat/stdbool.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 $(srcdir)/tls_common.h \ + $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/logsrvd_local.c +logsrvd_local.i: $(srcdir)/logsrvd_local.c $(incdir)/compat/stdbool.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 $(srcdir)/tls_common.h \ + $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +logsrvd_local.plog: logsrvd_local.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/logsrvd_local.c --i-file $< --output-file $@ +logsrvd_queue.o: $(srcdir)/logsrvd_queue.c $(incdir)/compat/stdbool.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)/logsrvd.h $(srcdir)/tls_common.h \ + $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/logsrvd_queue.c +logsrvd_queue.i: $(srcdir)/logsrvd_queue.c $(incdir)/compat/stdbool.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)/logsrvd.h $(srcdir)/tls_common.h \ + $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +logsrvd_queue.plog: logsrvd_queue.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/logsrvd_queue.c --i-file $< --output-file $@ +logsrvd_relay.o: $(srcdir)/logsrvd_relay.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_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)/logsrvd.h \ + $(srcdir)/tls_common.h $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/logsrvd_relay.c +logsrvd_relay.i: $(srcdir)/logsrvd_relay.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_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)/logsrvd.h \ + $(srcdir)/tls_common.h $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +logsrvd_relay.plog: logsrvd_relay.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/logsrvd_relay.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 \ + $(srcdir)/tls_common.h $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_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 \ + $(srcdir)/tls_common.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 $@ +tls_client.o: $(srcdir)/tls_client.c $(incdir)/compat/stdbool.h \ + $(incdir)/hostcheck.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/logsrv_util.h \ + $(srcdir)/tls_common.h $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/tls_client.c +tls_client.i: $(srcdir)/tls_client.c $(incdir)/compat/stdbool.h \ + $(incdir)/hostcheck.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/logsrv_util.h \ + $(srcdir)/tls_common.h $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +tls_client.plog: tls_client.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/tls_client.c --i-file $< --output-file $@ +tls_init.o: $(srcdir)/tls_init.c $(incdir)/compat/stdbool.h \ + $(incdir)/hostcheck.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(srcdir)/tls_common.h $(top_builddir)/config.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/tls_init.c +tls_init.i: $(srcdir)/tls_init.c $(incdir)/compat/stdbool.h \ + $(incdir)/hostcheck.h $(incdir)/sudo_compat.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(srcdir)/tls_common.h $(top_builddir)/config.h + $(CC) -E -o $@ $(CPPFLAGS) $< +tls_init.plog: tls_init.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/tls_init.c --i-file $< --output-file $@ diff --git a/logsrvd/iolog_writer.c b/logsrvd/iolog_writer.c new file mode 100644 index 0000000..400a68f --- /dev/null +++ b/logsrvd/iolog_writer.c @@ -0,0 +1,892 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> + +#include <errno.h> +#include <fcntl.h> +#include <inttypes.h> +#include <limits.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "sudo_compat.h" +#include "sudo_debug.h" +#include "sudo_eventlog.h" +#include "sudo_gettext.h" +#include "sudo_iolog.h" +#include "sudo_fatal.h" +#include "sudo_queue.h" +#include "sudo_util.h" + +#include "logsrvd.h" + +static bool +type_matches(InfoMessage *info, const char *source, + InfoMessage__ValueCase value_case) +{ + const void *val = info->u.strval; /* same for strlistval */ + debug_decl(type_matches, SUDO_DEBUG_UTIL); + + if (info->key == NULL) { + sudo_warnx(U_("%s: protocol error: NULL key"), source); + debug_return_bool(false); + } + if (info->value_case != value_case) { + sudo_warnx(U_("%s: protocol error: wrong type for %s"), + source, info->key); + debug_return_bool(false); + } + if (value_case != INFO_MESSAGE__VALUE_NUMVAL && val == NULL) { + sudo_warnx(U_("%s: protocol error: NULL value found in %s"), + source, info->key); + debug_return_bool(false); + } + debug_return_bool(true); +} + +/* + * 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_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + for (i = 0; i < len; i++) { + if ((dst[i] = strdup(src[i])) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + 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 connection_closure *closure) +{ + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + struct eventlog *evlog; + unsigned char uuid[16]; + size_t idx; + debug_decl(evlog_new, SUDO_DEBUG_UTIL); + + evlog = calloc(1, sizeof(*evlog)); + if (evlog == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + + /* Create a UUID to store in the event log. */ + sudo_uuid_create(uuid); + if (sudo_uuid_to_string(uuid, evlog->uuid_str, sizeof(evlog->uuid_str)) == NULL) { + sudo_warnx("%s", U_("unable to generate UUID")); + goto bad; + } + + /* Client/peer IP address. */ + evlog->peeraddr = closure->ipaddr; + + /* 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; + evlog->exit_value = -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 (type_matches(info, source, INFO_MESSAGE__VALUE_NUMVAL)) { + if (info->u.numval <= 0 || info->u.numval > INT_MAX) { + errno = ERANGE; + sudo_warn(U_("%s: %s"), source, "columns"); + } else { + evlog->columns = info->u.numval; + } + } + continue; + } + if (strcmp(key, "command") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRVAL)) { + if ((evlog->command = strdup(info->u.strval)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + goto bad; + } + } + continue; + } + break; + case 'l': + if (strcmp(key, "lines") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_NUMVAL)) { + if (info->u.numval <= 0 || info->u.numval > INT_MAX) { + errno = ERANGE; + sudo_warn(U_("%s: %s"), source, "lines"); + } else { + evlog->lines = info->u.numval; + } + } + continue; + } + break; + case 'r': + if (strcmp(key, "runargv") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRLISTVAL)) { + evlog->argv = strlist_copy(info->u.strlistval); + if (evlog->argv == NULL) + goto bad; + } + continue; + } + if (strcmp(key, "runchroot") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRVAL)) { + if ((evlog->runchroot = strdup(info->u.strval)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + goto bad; + } + } + continue; + } + if (strcmp(key, "runcwd") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRVAL)) { + if ((evlog->runcwd = strdup(info->u.strval)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + goto bad; + } + } + continue; + } + if (strcmp(key, "runenv") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRLISTVAL)) { + evlog->envp = strlist_copy(info->u.strlistval); + if (evlog->envp == NULL) + goto bad; + } + continue; + } + if (strcmp(key, "rungid") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_NUMVAL)) { + if (info->u.numval < 0 || info->u.numval > UINT_MAX) { + errno = ERANGE; + sudo_warn(U_("%s: %s"), source, "rungid"); + } else { + evlog->rungid = info->u.numval; + } + } + continue; + } + if (strcmp(key, "rungroup") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRVAL)) { + if ((evlog->rungroup = strdup(info->u.strval)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + goto bad; + } + } + continue; + } + if (strcmp(key, "runuid") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_NUMVAL)) { + if (info->u.numval < 0 || info->u.numval > UINT_MAX) { + errno = ERANGE; + sudo_warn(U_("%s: %s"), source, "runuid"); + } else { + evlog->runuid = info->u.numval; + } + } + continue; + } + if (strcmp(key, "runuser") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRVAL)) { + if ((evlog->runuser = strdup(info->u.strval)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + goto bad; + } + } + continue; + } + break; + case 's': + if (strcmp(key, "submitcwd") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRVAL)) { + if ((evlog->cwd = strdup(info->u.strval)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + goto bad; + } + } + continue; + } + if (strcmp(key, "submitgroup") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRVAL)) { + if ((evlog->submitgroup = strdup(info->u.strval)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + goto bad; + } + } + continue; + } + if (strcmp(key, "submithost") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRVAL)) { + if ((evlog->submithost = strdup(info->u.strval)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + goto bad; + } + } + continue; + } + if (strcmp(key, "submituser") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRVAL)) { + if ((evlog->submituser = strdup(info->u.strval)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + goto bad; + } + } + continue; + } + break; + case 't': + if (strcmp(key, "ttyname") == 0) { + if (type_matches(info, source, INFO_MESSAGE__VALUE_STRVAL)) { + if ((evlog->ttyname = strdup(info->u.strval)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + goto bad; + } + } + continue; + } + break; + } + } + + /* Check for required settings */ + if (evlog->submituser == NULL) { + sudo_warnx(U_("%s: protocol error: %s missing from AcceptMessage"), + source, "submituser"); + goto bad; + } + if (evlog->submithost == NULL) { + sudo_warnx(U_("%s: protocol error: %s missing from AcceptMessage"), + source, "submithost"); + goto bad; + } + if (evlog->runuser == NULL) { + sudo_warnx(U_("%s: protocol error: %s missing from AcceptMessage"), + source, "runuser"); + goto bad; + } + if (evlog->command == NULL) { + sudo_warnx(U_("%s: protocol error: %s missing from AcceptMessage"), + source, "command"); + goto bad; + } + + /* Other settings that must exist for event logging. */ + if (evlog->cwd == NULL) { + if ((evlog->cwd = strdup("unknown")) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + } + if (evlog->runcwd == NULL) { + if ((evlog->runcwd = strdup(evlog->cwd)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + } + if (evlog->submitgroup == NULL) { + /* TODO: make submitgroup required */ + if ((evlog->submitgroup = strdup("unknown")) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + } + if (evlog->ttyname == NULL) { + if ((evlog->ttyname = strdup("unknown")) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + 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_warnx(U_("%s: unable to format session id"), __func__); + 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_warnx(U_("%s: %s is not set"), __func__, "submituser"); + 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_warnx(U_("%s: %s is not set"), __func__, "submitgroup"); + 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_warnx(U_("%s: %s is not set"), __func__, "runuser"); + 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_warnx(U_("%s: %s is not set"), __func__, "rungroup"); + 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_warnx(U_("%s: %s is not set"), __func__, "submithost"); + 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_warnx(U_("%s: %s is not set"), __func__, "command"); + 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_warnx(U_("unable to expand iolog path %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_warnx(U_("unable to expand iolog path %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_warn("%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_warnx(U_("unable to create iolog path %s"), pathbuf); + goto bad; + } + if ((evlog->iolog_path = strdup(pathbuf)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + 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_warn("%s", evlog->iolog_path); + goto bad; + } + + debug_return_bool(true); +bad: + free(evlog->iolog_path); + evlog->iolog_path = NULL; + debug_return_bool(false); +} + +bool +iolog_create(int iofd, struct connection_closure *closure) +{ + debug_decl(iolog_create, SUDO_DEBUG_UTIL); + + if (iofd < 0 || iofd >= IOFD_MAX) { + sudo_warnx(U_("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_all, 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_warnx(U_("error closing iofd %d: %s"), i, errstr); + } + } + if (closure->iolog_dir_fd != -1) + close(closure->iolog_dir_fd); + + debug_return; +} + +bool +iolog_flush_all(struct connection_closure *closure) +{ + const char *errstr; + int i, ret = true; + debug_decl(iolog_flush_all, SUDO_DEBUG_UTIL); + + for (i = 0; i < IOFD_MAX; i++) { + if (!closure->iolog_files[i].enabled) + continue; + if (!iolog_flush(&closure->iolog_files[i], &errstr)) { + sudo_warnx(U_("error flushing iofd %d: %s"), i, errstr); + ret = false; + } + } + + debug_return_bool(ret); +} + +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); +} + +/* + * Like rename(2) but changes UID as needed. + */ +static bool +iolog_rename(const char *from, const char *to) +{ + bool ok, uid_changed = false; + debug_decl(iolog_rename, SUDO_DEBUG_UTIL); + + ok = rename(from, to) == 0; + if (!ok && errno == EACCES) { + uid_changed = iolog_swapids(false); + if (uid_changed) + ok = rename(from, to) == 0; + } + + if (uid_changed) { + if (!iolog_swapids(true)) + ok = false; + } + debug_return_bool(ok); +} + +/* Compressed logs don't support random access, need to rewrite them. */ +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); + + memset(&timing, 0, sizeof(timing)); + timing.decimal = "."; + + /* 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_warnx(U_("invalid I/O log %s: %s referenced but not present"), + evlog->iolog_path, iolog_fd_to_name(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_warnx(U_("%s: unable to find resume point [%lld, %ld]"), + evlog->iolog_path, (long long)target->tv_sec, target->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)) { + errno = ENAMETOOLONG; + sudo_warn("%s/restart.XXXXXX", evlog->iolog_path); + goto done; + } + if (!iolog_mkdtemp(tmpdir)) { + sudo_warn(U_("unable to mkdir %s"), tmpdir); + goto done; + } + if ((tmpdir_fd = iolog_openat(AT_FDCWD, tmpdir, O_RDONLY)) == -1) { + sudo_warn(U_("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_warn(U_("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_warnx(U_("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)) { + errno = ENAMETOOLONG; + sudo_warn("%s/%s", tmpdir, name); + goto done; + } + len = snprintf(to, sizeof(to), "%s/%s", evlog->iolog_path, + name); + if (len < 0 || len >= ssizeof(from)) { + errno = ENAMETOOLONG; + sudo_warn("%s/%s", evlog->iolog_path, name); + goto done; + } + if (!iolog_rename(from, to)) { + sudo_warn(U_("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); +} + +/* + * Add given delta to elapsed time. + * We cannot use timespecadd here since delta is not struct timespec. + */ +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; + } + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "%s: delta [%lld, %d], elapsed time now [%lld, %ld]", + __func__, (long long)delta->tv_sec, delta->tv_nsec, + (long long)elapsed->tv_sec, elapsed->tv_nsec); + + debug_return; +} diff --git a/logsrvd/logsrv_util.c b/logsrvd/logsrv_util.c new file mode 100644 index 0000000..f70604d --- /dev/null +++ b/logsrvd/logsrv_util.c @@ -0,0 +1,185 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2020 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/types.h> + +#include <errno.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "sudo_compat.h" +#include "sudo_debug.h" +#include "sudo_fatal.h" +#include "sudo_gettext.h" +#include "sudo_iolog.h" +#include "sudo_util.h" + +#include "logsrv_util.h" + +/* + * Expand buf as needed or just reset it. + */ +bool +expand_buf(struct connection_buffer *buf, unsigned int needed) +{ + void *newdata; + debug_decl(expand_buf, SUDO_DEBUG_UTIL); + + if (buf->size < needed) { + /* Expand buffer. */ + needed = sudo_pow2_roundup(needed); + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "expanding buffer from %u to %u", buf->size, needed); + if ((newdata = malloc(needed)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + if (buf->len != buf->off) + 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->len != buf->off) { + 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); + + if (!sudo_timespecisset(target)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "resuming at start of file [0, 0]"); + debug_return_bool(true); + } + + memset(&timing, 0, sizeof(timing)); + timing.decimal = "."; + + /* Parse timing file until we reach the target point. */ + for (;;) { + switch (iolog_read_timing_record(&iolog_files[IOFD_TIMING], &timing)) { + case 0: + break; + case 1: + /* EOF reading timing file. */ + sudo_warnx(U_("%s/%s: unable to find resume point [%lld, %ld]"), + iolog_path, "timing", (long long)target->tv_sec, + target->tv_nsec); + goto bad; + default: + /* Error printed by iolog_read_timing_record(). */ + goto bad; + } + sudo_timespecadd(elapsed_time, &timing.delay, 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_("%s/%s: unable to find resume point [%lld, %ld]"), + iolog_path, "timing", (long long)target->tv_sec, + target->tv_nsec); + 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..df68b8e --- /dev/null +++ b/logsrvd/logsrv_util.h @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2020 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SUDO_LOGSRV_UTIL_H +#define SUDO_LOGSRV_UTIL_H + +#include <netinet/in.h> /* for INET_ADDRSTRLEN and INET6_ADDRSTRLEN */ + +#ifndef INET_ADDRSTRLEN +# define INET_ADDRSTRLEN 16 +#endif +#ifndef INET6_ADDRSTRLEN +# define INET6_ADDRSTRLEN 46 +#endif + +/* 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 peer_info { + const char *name; +#if defined(HAVE_STRUCT_IN6_ADDR) + char ipaddr[INET6_ADDRSTRLEN]; +#else + char ipaddr[INET_ADDRSTRLEN]; +#endif +}; + +struct connection_buffer { + TAILQ_ENTRY(connection_buffer) entries; + uint8_t *data; + unsigned int size; + unsigned int len; + unsigned int off; +}; +TAILQ_HEAD(connection_buffer_list, connection_buffer); + +/* 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..17edb84 --- /dev/null +++ b/logsrvd/logsrvd.c @@ -0,0 +1,2012 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#ifdef HAVE_GETOPT_LONG +# include <getopt.h> +# else +# include "compat/getopt.h" +#endif /* HAVE_GETOPT_LONG */ + +#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 "logsrvd.h" +#include "hostcheck.h" + +#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; + +/* Event loop callbacks. */ +static void client_msg_cb(int fd, int what, void *v); +static void server_msg_cb(int fd, int what, void *v); +static void server_commit_cb(int fd, int what, void *v); +#if defined(HAVE_OPENSSL) +static void tls_handshake_cb(int fd, int what, void *v); +#endif + +/* + * 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; + struct connection_buffer *buf; + + TAILQ_REMOVE(&connections, closure, entries); + + if (closure->state == CONNECTING && closure->journal != NULL) { + /* Failed to relay journal file, retry later. */ + logsrvd_queue_insert(closure); + } + if (closure->relay_closure != NULL) + relay_closure_free(closure->relay_closure); +#if defined(HAVE_OPENSSL) + if (closure->ssl != NULL) { + /* Must call SSL_shutdown() before closing closure->sock. */ + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "closing down TLSÂ connection from %s", closure->ipaddr); + if (SSL_shutdown(closure->ssl) == 0) + SSL_shutdown(closure->ssl); + SSL_free(closure->ssl); + } +#endif + if (closure->sock != -1) { + shutdown(closure->sock, SHUT_RDWR); + 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); + while ((buf = TAILQ_FIRST(&closure->write_bufs)) != NULL) { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "discarding write buffer %p, len %u", buf, buf->len - buf->off); + TAILQ_REMOVE(&closure->write_bufs, buf, entries); + free(buf->data); + free(buf); + } + while ((buf = TAILQ_FIRST(&closure->free_bufs)) != NULL) { + TAILQ_REMOVE(&closure->free_bufs, buf, entries); + free(buf->data); + free(buf); + } + free(closure->journal_path); + if (closure->journal != NULL) + fclose(closure->journal); + free(closure); + + if (shutting_down && TAILQ_EMPTY(&connections)) + sudo_ev_loopbreak(evbase); + } + + debug_return; +} + +/* + * Allocate a new connection closure. + */ +struct connection_closure * +connection_closure_alloc(int fd, bool tls, bool relay_only, + 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 = relay_only ? -1 : fd; + closure->evbase = base; + TAILQ_INIT(&closure->write_bufs); + TAILQ_INIT(&closure->free_bufs); + + /* Use different message handlers depending on the operating mode. */ + if (relay_only) { + closure->cms = &cms_relay; + } else if (logsrvd_conf_relay_store_first()) { + closure->store_first = true; + closure->cms = &cms_journal; + } else { + closure->cms = &cms_local; + } + + 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->read_ev = sudo_ev_alloc(fd, SUDO_EV_READ|SUDO_EV_PERSIST, + client_msg_cb, closure); + if (closure->read_ev == NULL) + goto bad; + + if (!relay_only) { + closure->write_ev = sudo_ev_alloc(fd, SUDO_EV_WRITE|SUDO_EV_PERSIST, + server_msg_cb, closure); + if (closure->write_ev == NULL) + goto bad; + + closure->commit_ev = sudo_ev_alloc(-1, SUDO_EV_TIMEOUT, + server_commit_cb, closure); + if (closure->commit_ev == NULL) + goto bad; + } +#if defined(HAVE_OPENSSL) + if (tls) { + closure->ssl_accept_ev = sudo_ev_alloc(fd, 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); +} + +/* + * Close the client connection when finished. + * If in store-and-forward mode, initiate a relay connection. + * Otherwise, free the connection closure, removing any events. + */ +void +connection_close(struct connection_closure *closure) +{ + struct connection_closure *new_closure; + debug_decl(connection_close, SUDO_DEBUG_UTIL); + + if (closure == NULL) + debug_return; + + /* Final state should be FINISHED except on error. */ + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "%s: closure %p, final state %d, relay_closure %p, " + "journal file %p, journal path %s", __func__, closure, + closure->state, closure->relay_closure, closure->journal, + closure->journal_path ? closure->journal_path : ""); + + /* + * If we finished a client connection in store-and-forward mode, + * create a new connection for the relay and replay the journal. + */ + if (closure->store_first && closure->state == FINISHED && + closure->relay_closure == NULL && closure->journal != NULL) { + new_closure = connection_closure_alloc(fileno(closure->journal), false, + true, closure->evbase); + if (new_closure != NULL) { + /* Re-parent journal settings. */ + new_closure->journal = closure->journal; + closure->journal = NULL; + new_closure->journal_path = closure->journal_path; + closure->journal_path = NULL; + + /* Connect to the first relay available asynchronously. */ + if (!connect_relay(new_closure)) { + sudo_warnx("%s", U_("unable to connect to relay")); + connection_closure_free(new_closure); + } + } + } + if (closure->state == FINISHED && closure->journal_path != NULL) { + /* Journal relayed successfully, remove backing file. */ + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "removing journal file %s", closure->journal_path); + unlink(closure->journal_path); + + /* Process the next outgoing file (if any). */ + logsrvd_queue_enable(0, closure->evbase); + } + connection_closure_free(closure); + + debug_return; +} + +struct connection_buffer * +get_free_buf(size_t len, struct connection_closure *closure) +{ + struct connection_buffer *buf; + debug_decl(get_free_buf, SUDO_DEBUG_UTIL); + + buf = TAILQ_FIRST(&closure->free_bufs); + if (buf != NULL) { + TAILQ_REMOVE(&closure->free_bufs, buf, entries); + } else { + if ((buf = calloc(1, sizeof(*buf))) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_ptr(NULL); + } + } + + if (len > buf->size) { + free(buf->data); + buf->size = sudo_pow2_roundup(len); + if ((buf->data = malloc(buf->size)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + free(buf); + buf = NULL; + } + } + + debug_return_ptr(buf); +} + +static bool +fmt_server_message(struct connection_closure *closure, ServerMessage *msg) +{ + struct connection_buffer *buf = NULL; + uint32_t msg_len; + bool ret = false; + size_t len; + debug_decl(fmt_server_message, SUDO_DEBUG_UTIL); + + len = server_message__get_packed_size(msg); + if (len > MESSAGE_SIZE_MAX) { + sudo_warnx(U_("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); + + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "size + server message %zu bytes", len); + + if ((buf = get_free_buf(len, closure)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate connection_buffer"); + goto done; + } + memcpy(buf->data, &msg_len, sizeof(msg_len)); + server_message__pack(msg, buf->data + sizeof(msg_len)); + buf->len = len; + TAILQ_INSERT_TAIL(&closure->write_bufs, buf, entries); + + ret = true; + +done: + debug_return_bool(ret); +} + +static bool +fmt_hello_message(struct connection_closure *closure) +{ + 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; + hello.subcommands = true; + msg.u.hello = &hello; + msg.type_case = SERVER_MESSAGE__TYPE_HELLO; + + debug_return_bool(fmt_server_message(closure, &msg)); +} + +bool +fmt_log_id_message(const char *id, struct connection_closure *closure) +{ + 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(closure, &msg)); +} + +static bool +fmt_error_message(const char *errstr, struct connection_closure *closure) +{ + 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(closure, &msg)); +} + +/* + * Format a ServerMessage with the error string and add it to the write queue. + * Also sets the error flag state to true. + * Returns true if successfully scheduled, else false. + */ +bool +schedule_error_message(const char *errstr, struct connection_closure *closure) +{ + bool ret = false; + debug_decl(schedule_error_message, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "send error to client: %s", errstr ? errstr : "none"); + + /* Prevent further reads from the client, just write the error. */ + sudo_ev_del(closure->evbase, closure->read_ev); + + if (errstr == NULL || closure->error || closure->write_ev == NULL) + goto done; + + /* Format error message and add to the write queue. */ + if (!fmt_error_message(errstr, closure)) + goto done; + if (sudo_ev_add(closure->evbase, closure->write_ev, + logsrvd_conf_server_timeout(), true) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto done; + } + ret = true; + +done: + closure->error = true; + debug_return_bool(ret); +} + +/* + * AcceptMessage handler. + */ +static bool +handle_accept(AcceptMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + bool ret; + debug_decl(handle_accept, SUDO_DEBUG_UTIL); + + /* We can get an AcceptMessage for a sub-command during a session. */ + if (closure->state == EXITED || closure->state == FINISHED) { + sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); + 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_warnx(U_("%s: %s"), source, U_("invalid AcceptMessage")); + closure->errstr = _("invalid AcceptMessage"); + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received AcceptMessage from %s", + __func__, source); + + ret = closure->cms->accept(msg, buf, len, closure); + if (ret && closure->state == INITIAL) { + if (msg->expect_iobufs) + closure->log_io = true; + closure->state = RUNNING; + } + debug_return_bool(ret); +} + +/* + * RejectMessage handler. + */ +static bool +handle_reject(RejectMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + bool ret; + debug_decl(handle_reject, SUDO_DEBUG_UTIL); + + /* We can get a RejectMessage for a sub-command during a session. */ + if (closure->state == EXITED || closure->state == FINISHED) { + sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); + 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_warnx(U_("%s: %s"), source, U_("invalid RejectMessage")); + closure->errstr = _("invalid RejectMessage"); + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received RejectMessage from %s", + __func__, source); + + ret = closure->cms->reject(msg, buf, len, closure); + if (ret && closure->state == INITIAL) { + closure->state = FINISHED; + } + + debug_return_bool(ret); +} + +static bool +handle_exit(ExitMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + bool ret; + debug_decl(handle_exit, SUDO_DEBUG_UTIL); + + if (closure->state != RUNNING) { + sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); + closure->errstr = _("state machine error"); + debug_return_bool(false); + } + + /* Check that message is valid. */ + if (msg->run_time == NULL) { + sudo_warnx(U_("%s: %s"), source, U_("invalid ExitMessage")); + closure->errstr = _("invalid ExitMessage"); + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ExitMessage from %s", + source, __func__); + + ret = closure->cms->exit(msg, buf, len, closure); + if (ret) { + if (sudo_timespecisset(&closure->elapsed_time)) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: elapsed time: [%lld, %ld]", + __func__, (long long)closure->elapsed_time.tv_sec, + closure->elapsed_time.tv_nsec); + } + + if (closure->log_io) { + /* Command exited, client waiting for final commit point. */ + closure->state = EXITED; + + /* Relay host will send the final commit point. */ + if (closure->relay_closure == NULL) { + struct timespec tv = { 0, 0 }; + if (sudo_ev_add(closure->evbase, closure->commit_ev, &tv, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + ret = false; + } + } + } else { + /* No commit point to send to client, we are finished. */ + closure->state = FINISHED; + } + } + sudo_ev_del(closure->evbase, closure->read_ev); + + debug_return_bool(ret); +} + +static bool +handle_restart(RestartMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + bool ret = true; + debug_decl(handle_restart, SUDO_DEBUG_UTIL); + + if (closure->state != INITIAL) { + sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); + closure->errstr = _("state machine error"); + debug_return_bool(false); + } + + /* Check that message is valid. */ + if (msg->log_id == NULL || msg->resume_point == NULL) { + sudo_warnx(U_("%s: %s"), source, U_("invalid RestartMessage")); + closure->errstr = _("invalid RestartMessage"); + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: received RestartMessage for %s from %s", __func__, msg->log_id, + source); + + /* Only I/O logs are restartable. */ + closure->log_io = true; + + if (closure->cms->restart(msg, buf, len, closure)) { + /* Successfully restarted. */ + closure->state = RUNNING; + } else { + /* Report error to client before closing the connection. */ + sudo_debug_printf(SUDO_DEBUG_WARN, "%s: unable to restart I/O log", + __func__); + if (!schedule_error_message(closure->errstr, closure)) + ret = false; + } + + debug_return_bool(ret); +} + +static bool +handle_alert(AlertMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + debug_decl(handle_alert, SUDO_DEBUG_UTIL); + + /* Check that message is valid. */ + if (msg->alert_time == NULL || msg->reason == NULL) { + sudo_warnx(U_("%s: %s"), source, U_("invalid AlertMessage")); + closure->errstr = _("invalid AlertMessage"); + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received AlertMessage from %s", + source, __func__); + + debug_return_bool(closure->cms->alert(msg, buf, len, closure)); +} + +/* Enable a commit event if not relaying and it is not already pending. */ +static bool +enable_commit(struct connection_closure *closure) +{ + debug_decl(enable_commit, SUDO_DEBUG_UTIL); + + if (closure->relay_closure == NULL) { + 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_warnx("%s", U_("unable to add event to queue")); + debug_return_bool(false); + } + } + } + debug_return_bool(true); +} + +static bool +handle_iobuf(int iofd, IoBuffer *iobuf, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + debug_decl(handle_iobuf, SUDO_DEBUG_UTIL); + + if (closure->state != RUNNING) { + sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); + closure->errstr = _("state machine error"); + debug_return_bool(false); + } + if (!closure->log_io) { + sudo_warnx(U_("%s: unexpected IoBuffer"), source); + closure->errstr = _("protocol error"); + debug_return_bool(false); + } + + /* Check that message is valid. */ + if (iobuf->delay == NULL) { + sudo_warnx(U_("%s: %s"), source, U_("invalid IoBuffer")); + closure->errstr = _("invalid IoBuffer"); + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received IoBuffer from %s", + source, __func__); + + if (!closure->cms->iobuf(iofd, iobuf, buf, len, closure)) + debug_return_bool(false); + if (!enable_commit(closure)) + debug_return_bool(false); + + debug_return_bool(true); +} + +static bool +handle_winsize(ChangeWindowSize *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + debug_decl(handle_winsize, SUDO_DEBUG_UTIL); + + if (closure->state != RUNNING) { + sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); + closure->errstr = _("state machine error"); + debug_return_bool(false); + } + if (!closure->log_io) { + sudo_warnx(U_("%s: unexpected IoBuffer"), source); + closure->errstr = _("protocol error"); + debug_return_bool(false); + } + + /* Check that message is valid. */ + if (msg->delay == NULL) { + sudo_warnx(U_("%s: %s"), source, U_("invalid ChangeWindowSize")); + closure->errstr = _("invalid ChangeWindowSize"); + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ChangeWindowSize from %s", + source, __func__); + + if (!closure->cms->winsize(msg, buf, len, closure)) + debug_return_bool(false); + if (!enable_commit(closure)) + debug_return_bool(false); + + debug_return_bool(true); +} + +static bool +handle_suspend(CommandSuspend *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + debug_decl(handle_syspend, SUDO_DEBUG_UTIL); + + if (closure->state != RUNNING) { + sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); + closure->errstr = _("state machine error"); + debug_return_bool(false); + } + if (!closure->log_io) { + sudo_warnx(U_("%s: unexpected IoBuffer"), source); + closure->errstr = _("protocol error"); + debug_return_bool(false); + } + + /* Check that message is valid. */ + if (msg->delay == NULL || msg->signal == NULL) { + sudo_warnx(U_("%s: %s"), source, U_("invalid CommandSuspend")); + closure->errstr = _("invalid CommandSuspend"); + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received CommandSuspend from %s", + source, __func__); + + if (!closure->cms->suspend(msg, buf, len, closure)) + debug_return_bool(false); + if (!enable_commit(closure)) + debug_return_bool(false); + + debug_return_bool(true); +} + +static bool +handle_client_hello(ClientHello *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + debug_decl(handle_client_hello, SUDO_DEBUG_UTIL); + + if (closure->state != INITIAL) { + sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); + 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 ? msg->client_id : "unknown"); + + debug_return_bool(true); +} + +static bool +handle_client_message(uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + ClientMessage *msg; + bool ret = false; + debug_decl(handle_client_message, SUDO_DEBUG_UTIL); + + /* TODO: can we extract type_case without unpacking for relay case? */ + msg = client_message__unpack(NULL, len, buf); + if (msg == NULL) { + sudo_warnx(U_("unable to unpack %s size %zu"), "ClientMessage", len); + debug_return_bool(false); + } + + switch (msg->type_case) { + case CLIENT_MESSAGE__TYPE_ACCEPT_MSG: + ret = handle_accept(msg->u.accept_msg, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_REJECT_MSG: + ret = handle_reject(msg->u.reject_msg, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_EXIT_MSG: + ret = handle_exit(msg->u.exit_msg, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_RESTART_MSG: + ret = handle_restart(msg->u.restart_msg, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_ALERT_MSG: + ret = handle_alert(msg->u.alert_msg, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_TTYIN_BUF: + ret = handle_iobuf(IOFD_TTYIN, msg->u.ttyin_buf, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_TTYOUT_BUF: + ret = handle_iobuf(IOFD_TTYOUT, msg->u.ttyout_buf, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_STDIN_BUF: + ret = handle_iobuf(IOFD_STDIN, msg->u.stdin_buf, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_STDOUT_BUF: + ret = handle_iobuf(IOFD_STDOUT, msg->u.stdout_buf, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_STDERR_BUF: + ret = handle_iobuf(IOFD_STDERR, msg->u.stderr_buf, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_WINSIZE_EVENT: + ret = handle_winsize(msg->u.winsize_event, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_SUSPEND_EVENT: + ret = handle_suspend(msg->u.suspend_event, buf, len, closure); + break; + case CLIENT_MESSAGE__TYPE_HELLO_MSG: + ret = handle_client_hello(msg->u.hello_msg, buf, len, closure); + break; + default: + sudo_warnx(U_("unexpected type_case value %d in %s from %s"), + msg->type_case, "ClientMessage", source); + 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); + + 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->relay_closure != NULL) { + /* Connection being relayed, check for pending I/O. */ + relay_shutdown(closure); + } else if (closure->log_io) { + /* Schedule final commit point for the connection. */ + if (sudo_ev_add(base, closure->commit_ev, &tv, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + } + } else { + /* No commit point, close connection immediately. */ + connection_close(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_warnx("%s", U_("unable to add event to queue")); + } + } + } + + 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; + 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_warnx(U_("timed out writing to client %s"), closure->ipaddr); + goto finished; + } + + if ((buf = TAILQ_FIRST(&closure->write_bufs)) == NULL) { + sudo_warnx(U_("missing write buffer for client %s"), closure->ipaddr); + goto finished; + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending %u bytes to client (%s)", + __func__, buf->len - buf->off, closure->ipaddr); + +#if defined(HAVE_OPENSSL) + if (closure->ssl != NULL) { + nwritten = SSL_write(closure->ssl, buf->data + buf->off, + buf->len - buf->off); + if (nwritten <= 0) { + const char *errstr; + 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_warn("%s: SSL_write", closure->ipaddr); + goto finished; + default: + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("%s: SSL_write: %s", closure->ipaddr, + errstr ? errstr : strerror(errno)); + goto finished; + } + } + } else +#endif + { + nwritten = write(fd, buf->data + buf->off, buf->len - buf->off); + } + + if (nwritten == -1) { + if (errno == EAGAIN || errno == EINTR) + debug_return; + sudo_warn("%s: write", closure->ipaddr); + goto finished; + } + buf->off += nwritten; + + if (buf->off == buf->len) { + /* sent entire message, move buf to free list */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: finished sending %u bytes to client", __func__, buf->len); + buf->off = 0; + buf->len = 0; + TAILQ_REMOVE(&closure->write_bufs, buf, entries); + TAILQ_INSERT_TAIL(&closure->free_bufs, buf, entries); + if (TAILQ_EMPTY(&closure->write_bufs)) { + /* Write queue empty, check state. */ + sudo_ev_del(closure->evbase, closure->write_ev); + if (closure->error || closure->state == FINISHED || + closure->state == SHUTDOWN) + goto finished; + } + } + debug_return; + +finished: + connection_close(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; + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + 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_warnx(U_("timed out reading from client %s"), closure->ipaddr); + goto close_connection; + } + +#if defined(HAVE_OPENSSL) + if (closure->ssl != NULL) { + nread = SSL_read(closure->ssl, buf->data + buf->len, buf->size); + if (nread <= 0) { + const char *errstr; + 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_server_timeout(), false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + closure->errstr = _("unable to allocate memory"); + goto send_error; + } + closure->temporary_write_event = true; + } + /* Redirect write event to finish SSL_read() */ + closure->read_instead_of_write = true; + debug_return; + case SSL_ERROR_SYSCALL: + if (nread == 0) { + /* EOF, handled below */ + sudo_warnx(U_("EOF from %s without proper TLS shutdown"), + closure->ipaddr); + break; + } + sudo_warn("%s: SSL_read", closure->ipaddr); + goto close_connection; + default: + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("%s: SSL_read: %s", closure->ipaddr, + errstr ? errstr : strerror(errno)); + goto close_connection; + } + } + } else +#endif + { + nread = read(fd, buf->data + buf->len, buf->size - buf->len); + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received %zd bytes from client %s", + __func__, nread, closure->ipaddr); + switch (nread) { + case -1: + if (errno == EAGAIN || errno == EINTR) + debug_return; + sudo_warn("%s: read", closure->ipaddr); + goto close_connection; + case 0: + if (closure->state != FINISHED) { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "unexpected EOF"); + } + goto close_connection; + 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_("client message too large: %zu"), (size_t)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))) { + closure->errstr = _("unable to allocate memory"); + goto send_error; + } + 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_warnx(U_("%s: %s"), source, U_("invalid ClientMessage")); + closure->errstr = _("invalid ClientMessage"); + goto send_error; + } + buf->off += msg_len; + } + buf->len -= buf->off; + buf->off = 0; + + if (closure->state == FINISHED) + goto close_connection; + + debug_return; + +send_error: + /* + * Try to send client an error message before closing the connection. + */ + if (!schedule_error_message(closure->errstr, closure)) + goto close_connection; + debug_return; + +close_connection: + connection_close(closure); + debug_return; +} + +/* + * Format and schedule a commit_point message. + */ +bool +schedule_commit_point(TimeSpec *commit_point, + struct connection_closure *closure) +{ + debug_decl(schedule_commit_point, SUDO_DEBUG_UTIL); + + if (closure->write_ev != NULL) { + /* Send an acknowledgement of what we've committed to disk. */ + ServerMessage msg = SERVER_MESSAGE__INIT; + 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)commit_point->tv_sec, (long)commit_point->tv_nsec); + + if (!fmt_server_message(closure, &msg)) + goto bad; + if (sudo_ev_add(closure->evbase, closure->write_ev, + logsrvd_conf_server_timeout(), false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto bad; + } + } + + if (closure->state == EXITED) + closure->state = FINISHED; + debug_return_bool(true); +bad: + debug_return_bool(false); +} + +/* + * Time-based event that fires periodically to report to the client + * what has been committed to disk. + */ +static void +server_commit_cb(int unused, int what, void *v) +{ + struct connection_closure *closure = v; + TimeSpec commit_point = TIME_SPEC__INIT; + debug_decl(server_commit_cb, SUDO_DEBUG_UTIL); + + /* Flush I/O logs before sending commit point if needed. */ + if (!iolog_get_flush()) + iolog_flush_all(closure); + + commit_point.tv_sec = closure->elapsed_time.tv_sec; + commit_point.tv_nsec = closure->elapsed_time.tv_nsec; + if (!schedule_commit_point(&commit_point, closure)) + connection_close(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. + */ +bool +start_protocol(struct connection_closure *closure) +{ + const struct timespec *timeout = logsrvd_conf_server_timeout(); + debug_decl(start_protocol, SUDO_DEBUG_UTIL); + + if (closure->relay_closure != NULL && closure->relay_closure->relays != NULL) { + /* No longer need the stashed relays list. */ + address_list_delref(closure->relay_closure->relays); + closure->relay_closure->relays = NULL; + closure->relay_closure->relay_addr = NULL; + } + + /* When replaying a journal there is no write event. */ + if (closure->write_ev != NULL) { + if (!fmt_hello_message(closure)) + 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); + } +} + +/* + * Set the TLS verify callback to verify_peer_identity(). + */ +static void +set_tls_verify_peer(void) +{ + SSL_CTX *server_ctx = logsrvd_server_tls_ctx(); + SSL_CTX *relay_ctx = logsrvd_relay_tls_ctx(); + debug_decl(set_tls_verify_peer, SUDO_DEBUG_UTIL); + + if (server_ctx != NULL && logsrvd_conf_server_tls_check_peer()) { + /* Verify server cert during the handshake. */ + SSL_CTX_set_verify(server_ctx, + SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_peer_identity); + } + if (relay_ctx != NULL && logsrvd_conf_relay_tls_check_peer()) { + /* Verify relay cert during the handshake. */ + SSL_CTX_set_verify(relay_ctx, + SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_peer_identity); + } + + debug_return; +} + +static void +tls_handshake_cb(int fd, int what, void *v) +{ + struct connection_closure *closure = v; + const char *errstr; + int err, handshake_status; + debug_decl(tls_handshake_cb, SUDO_DEBUG_UTIL); + + if (what == SUDO_EV_TIMEOUT) { + sudo_warnx("TLS handshake with %s timed out", closure->ipaddr); + goto bad; + } + + handshake_status = SSL_accept(closure->ssl); + err = SSL_get_error(closure->ssl, handshake_status); + switch (err) { + 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_server_timeout(), false) == -1) { + sudo_warnx("%s", U_("unable to add event 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_server_timeout(), false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto bad; + } + debug_return; + case SSL_ERROR_SYSCALL: + sudo_warn("%s: SSL_accept", closure->ipaddr); + goto bad; + default: + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("%s: SSL_accept: %s", closure->ipaddr, + errstr ? errstr : strerror(errno)); + 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 (!TAILQ_EMPTY(logsrvd_conf_relay_address()) && !closure->store_first) { + if (!connect_relay(closure)) + goto bad; + } else { + if (!start_protocol(closure)) + goto bad; + } + + debug_return; +bad: + connection_close(closure); + debug_return; +} +#endif /* HAVE_OPENSSL */ + +/* + * New connection. + * Allocate a connection closure and optionally perform TLS handshake. + */ +static bool +new_connection(int sock, bool tls, const union sockaddr_union *sa_un, + struct sudo_event_base *evbase) +{ + struct connection_closure *closure; + debug_decl(new_connection, SUDO_DEBUG_UTIL); + + if ((closure = connection_closure_alloc(sock, tls, false, evbase)) == NULL) + goto bad; + + /* store the peer's IP address in the closure object */ + if (sa_un->sa.sa_family == AF_INET) { + inet_ntop(AF_INET, &sa_un->sin.sin_addr, closure->ipaddr, + sizeof(closure->ipaddr)); +#ifdef HAVE_STRUCT_IN6_ADDR + } else if (sa_un->sa.sa_family == AF_INET6) { + inet_ntop(AF_INET6, &sa_un->sin6.sin6_addr, closure->ipaddr, + sizeof(closure->ipaddr)); +#endif /* HAVE_STRUCT_IN6_ADDR */ + } else { + errno = EAFNOSUPPORT; + sudo_warn("%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) { + const char *errstr; + + /* Create the SSL object for the closure and attach it to the socket */ + if ((closure->ssl = SSL_new(logsrvd_server_tls_ctx())) == NULL) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("%s: %s"), "SSL_new", + errstr ? errstr : strerror(errno)); + goto bad; + } + + if (SSL_set_fd(closure->ssl, closure->sock) != 1) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("%s: %s"), "SSL_set_fd", + errstr ? errstr : strerror(errno)); + 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) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("Unable to attach user data to the ssl object: %s"), + errstr ? errstr : strerror(errno)); + goto bad; + } + + /* Enable SSL_accept to begin handshake with client. */ + if (sudo_ev_add(evbase, closure->ssl_accept_ev, + logsrvd_conf_server_timeout(), false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto bad; + } + } +#endif + /* If no TLS handshake, start the protocol immediately. */ + if (!tls) { + if (!TAILQ_EMPTY(logsrvd_conf_relay_address()) && !closure->store_first) { + if (!connect_relay(closure)) + goto bad; + } else { + if (!start_protocol(closure)) + goto bad; + } + } + + debug_return_bool(true); +bad: + connection_close(closure); + debug_return_bool(false); +} + +static int +create_listener(struct server_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 sa_un; + socklen_t salen = sizeof(sa_un); + int sock; + debug_decl(listener_cb, SUDO_DEBUG_UTIL); + + memset(&sa_un, 0, sizeof(sa_un)); + sock = accept(fd, &sa_un.sa, &salen); + if (sock != -1) { + if (logsrvd_conf_server_tcp_keepalive()) { + int keepalive = 1; + if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, + sizeof(keepalive)) == -1) { + sudo_warn("SO_KEEPALIVE"); + } + } + if (!new_connection(sock, l->tls, &sa_un, evbase)) { + /* TODO: pause accepting on ENOMEM */ + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to start new connection"); + } + } else { + if (errno == EAGAIN || errno == EINTR) + debug_return; + /* TODO: pause accepting on ENFILE and EMFILE */ + sudo_warn("accept"); + } + + debug_return; +} + +static bool +register_listener(struct server_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_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + 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_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + 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 set the TLS verify callback. + */ +static bool +server_setup(struct sudo_event_base *base) +{ + struct server_address *addr; + struct listener *l; + int nlisteners = 0; + bool ret; + 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_server_listen_address(), entries) { + nlisteners += register_listener(addr, base); + } + ret = nlisteners > 0; + +#if defined(HAVE_OPENSSL) + if (ret) + set_tls_verify_peer(); +#endif + + debug_return_bool(ret); +} + +/* + * Reload config and re-initialize listeners. + */ +static void +server_reload(struct sudo_event_base *evbase) +{ + 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. */ + if (!server_setup(evbase)) + sudo_fatalx("%s", U_("unable to 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()), -1); + } + } + + debug_return; +} + +/* + * Dump server information to the debug file. + * Includes information about listeners and client connections. + */ +static void +server_dump_stats(void) +{ + struct server_address *addr; + struct connection_closure *closure; + int n; + debug_decl(server_dump_stats, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s", server_id); + sudo_debug_printf(SUDO_DEBUG_INFO, "configuration file: %s", conf_file); + + sudo_debug_printf(SUDO_DEBUG_INFO, "listen addresses:"); + n = 0; + TAILQ_FOREACH(addr, logsrvd_conf_server_listen_address(), entries) { + union sockaddr_union *sa_un = &addr->sa_un; + char ipaddr[INET6_ADDRSTRLEN]; + + switch (sa_un->sa.sa_family) { + case AF_INET: + inet_ntop(AF_INET, &sa_un->sin.sin_addr, ipaddr, sizeof(ipaddr)); + break; +#ifdef HAVE_STRUCT_IN6_ADDR + case AF_INET6: + inet_ntop(AF_INET6, &sa_un->sin6.sin6_addr, ipaddr, sizeof(ipaddr)); + break; +#endif /* HAVE_STRUCT_IN6_ADDR */ + default: + (void)strlcpy(ipaddr, "[unknown]", sizeof(ipaddr)); + break; + } + sudo_debug_printf(SUDO_DEBUG_INFO, " %d: %s [%s]", ++n, + addr->sa_str, ipaddr); + } + + if (!TAILQ_EMPTY(&connections)) { + n = 0; + sudo_debug_printf(SUDO_DEBUG_INFO, "client connections:"); + TAILQ_FOREACH(closure, &connections, entries) { + struct relay_closure *relay_closure = closure->relay_closure; + + n++; + if (closure->sock == -1) { + sudo_debug_printf(SUDO_DEBUG_INFO, " %2d: journal %s", n, + closure->journal_path ? closure->journal_path : "none"); + sudo_debug_printf(SUDO_DEBUG_INFO, " %2d: fd %d", n, + closure->journal ? fileno(closure->journal) : -1); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, " %2d: addr %s%s", n, + closure->ipaddr, closure->tls ? " (TLS)" : ""); + sudo_debug_printf(SUDO_DEBUG_INFO, " %2d: sock %d", n, + closure->sock); + } + if (relay_closure != NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO, " relay: %s (%s)", + relay_closure->relay_name.name, + relay_closure->relay_name.ipaddr); + sudo_debug_printf(SUDO_DEBUG_INFO, " relay sock: %d", + relay_closure->sock); + } + sudo_debug_printf(SUDO_DEBUG_INFO, " state: %d", closure->state); + if (closure->errstr != NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO, " error: %s", + closure->errstr); + } + sudo_debug_printf(SUDO_DEBUG_INFO, " log I/O: %s", + closure->log_io ? "true" : "false"); + sudo_debug_printf(SUDO_DEBUG_INFO, " store first: %s", + closure->store_first ? "true" : "false"); + if (sudo_timespecisset(&closure->elapsed_time)) { + sudo_debug_printf(SUDO_DEBUG_INFO, + " elapsed time: [%lld, %ld]", + (long long)closure->elapsed_time.tv_sec, + (long)closure->elapsed_time.tv_nsec); + } + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%d client connection(s)\n", n); + } + logsrvd_queue_dump(); + + 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; + case SIGUSR1: + server_dump_stats(); + break; + default: + sudo_warnx(U_("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_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + 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 dfd, fd; + mode_t oldmask; + const char *pid_file = logsrvd_conf_pid_file(); + debug_decl(write_pidfile, SUDO_DEBUG_UTIL); + + if (pid_file == NULL) + debug_return; + + /* Default logsrvd umask is more restrictive (077). */ + oldmask = umask(S_IWGRP|S_IWOTH); + + dfd = sudo_open_parent_dir(pid_file, ROOT_UID, ROOT_GID, + S_IRWXU|S_IXGRP|S_IXOTH, false); + if (dfd != -1) { + const char *base = sudo_basename(pid_file); + fd = openat(dfd, base, 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); + } + close(dfd); + } + umask(oldmask); + + 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 (chdir("/") == -1) + sudo_warn("chdir(\"/\")"); + + if (!nofork) { + switch (sudo_debug_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 ((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); + } + } else { + if ((fd = open(_PATH_DEVNULL, O_RDWR)) != -1) { + /* Preserve stdout/stderr in nofork mode (if open). */ + (void) dup2(fd, STDIN_FILENO); + if (fcntl(STDOUT_FILENO, F_GETFL) == -1) + (void) dup2(fd, STDOUT_FILENO); + if (fcntl(STDERR_FILENO, F_GETFL) == -1) + (void) dup2(fd, STDERR_FILENO); + if (fd > STDERR_FILENO) + (void) close(fd); + } + } + + /* Disable logging to stderr after we become a daemon. */ + logsrvd_warn_stderr(false); + + 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) +{ + printf("%s - %s\n\n", getprogname(), _("sudo log server")); + usage(false); + printf("\n%s\n", _("Options:")); + printf(" -f, --file %s\n", + _("path to configuration file")); + printf(" -h, --help %s\n", + _("display help message and exit")); + printf(" -n, --no-fork %s\n", + _("do not fork, run in the foreground")); + printf(" -R, --random-drop %s\n", + _("percent chance connections will drop")); + printf(" -V, --version %s\n", + _("display version information and exit")); + putchar('\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; + 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"); + + /* Create files readable/writable only by owner. */ + umask(S_IRWXG|S_IRWXO); + + /* 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()), -1); + + 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) */ + if (!set_random_drop(optarg)) + sudo_fatalx(U_("invalid random drop value: %s"), optarg); + 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_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + /* Initialize listeners. */ + if (!server_setup(evbase)) + sudo_fatalx("%s", U_("unable to setup listen socket")); + + register_signal(SIGHUP, evbase); + register_signal(SIGINT, evbase); + register_signal(SIGTERM, evbase); + register_signal(SIGUSR1, evbase); + + /* Point of no return. */ + daemonize(nofork); + signal(SIGPIPE, SIG_IGN); + + logsrvd_queue_scan(evbase); + sudo_ev_dispatch(evbase); + if (!nofork && logsrvd_conf_pid_file() != NULL) + unlink(logsrvd_conf_pid_file()); + logsrvd_conf_cleanup(); + + debug_return_int(1); +} diff --git a/logsrvd/logsrvd.h b/logsrvd/logsrvd.h new file mode 100644 index 0000000..4d371f3 --- /dev/null +++ b/logsrvd/logsrvd.h @@ -0,0 +1,266 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SUDO_LOGSRVD_H +#define SUDO_LOGSRVD_H + +#include "log_server.pb-c.h" +#if PROTOBUF_C_VERSION_NUMBER < 1003000 +# error protobuf-c version 1.30 or higher required +#endif + +#include <config.h> + +#if defined(HAVE_OPENSSL) +# if defined(HAVE_WOLFSSL) +# include <wolfssl/options.h> +# endif +# include <openssl/ssl.h> +# include <openssl/err.h> +#endif + +#include "logsrv_util.h" +#include "tls_common.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 + +/* Template for mkstemp(3) when creating temporary files. */ +#define RELAY_TEMPLATE "relay.XXXXXXXX" + +/* + * Connection status. + * In the RUNNING state we expect I/O log buffers. + */ +enum connection_status { + INITIAL, + CONNECTING, + RUNNING, + EXITED, + SHUTDOWN, + FINISHED +}; + +/* + * Per-connection relay state. + */ +struct relay_closure { + struct server_address_list *relays; + struct server_address *relay_addr; + struct sudo_event *read_ev; + struct sudo_event *write_ev; + struct sudo_event *connect_ev; + struct connection_buffer read_buf; + struct connection_buffer_list write_bufs; + struct peer_info relay_name; +#if defined(HAVE_OPENSSL) + struct tls_client_closure tls_client; +#endif + int sock; + bool read_instead_of_write; + bool write_instead_of_read; + bool temporary_write_event; +}; + +/* + * Per-connection state. + */ +struct connection_closure { + TAILQ_ENTRY(connection_closure) entries; + struct client_message_switch *cms; + struct relay_closure *relay_closure; + struct eventlog *evlog; + struct timespec elapsed_time; + struct connection_buffer read_buf; + struct connection_buffer_list write_bufs; + struct connection_buffer_list free_bufs; + 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; + FILE *journal; + char *journal_path; + struct iolog_file iolog_files[IOFD_MAX]; + int iolog_dir_fd; + int sock; + enum connection_status state; + bool error; + bool tls; + bool log_io; + bool store_first; + bool read_instead_of_write; + bool write_instead_of_read; + bool temporary_write_event; +#ifdef HAVE_STRUCT_IN6_ADDR + char ipaddr[INET6_ADDRSTRLEN]; +#else + char ipaddr[INET_ADDRSTRLEN]; +#endif +}; + +/* Client message switch. */ +struct client_message_switch { + bool (*accept)(AcceptMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure); + bool (*reject)(RejectMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure); + bool (*exit)(ExitMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure); + bool (*restart)(RestartMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure); + bool (*alert)(AlertMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure); + bool (*iobuf)(int iofd, IoBuffer *iobuf, uint8_t *buf, size_t len, + struct connection_closure *closure); + bool (*suspend)(CommandSuspend *msg, uint8_t *buf, size_t len, + struct connection_closure *closure); + bool (*winsize)(ChangeWindowSize *msg, uint8_t *buf, size_t len, + struct connection_closure *closure); +}; + +union sockaddr_union { + struct sockaddr sa; + struct sockaddr_in sin; +#ifdef HAVE_STRUCT_IN6_ADDR + struct sockaddr_in6 sin6; +#endif +}; + +/* + * List of server addresses. + */ +struct server_address { + TAILQ_ENTRY(server_address) entries; + char *sa_host; + char *sa_str; + union sockaddr_union sa_un; + socklen_t sa_size; + bool tls; +}; +TAILQ_HEAD(server_address_list, server_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); + +/* + * Queue of finished journal files to be relayed. + */ +struct outgoing_journal { + TAILQ_ENTRY(outgoing_journal) entries; + char *journal_path; +}; +TAILQ_HEAD(outgoing_journal_queue, outgoing_journal); + +/* iolog_writer.c */ +struct eventlog *evlog_new(TimeSpec *submit_time, InfoMessage **info_msgs, size_t infolen, struct connection_closure *closure); +bool iolog_init(AcceptMessage *msg, struct connection_closure *closure); +bool iolog_create(int iofd, struct connection_closure *closure); +void iolog_close_all(struct connection_closure *closure); +bool iolog_flush_all(struct connection_closure *closure); +bool iolog_rewrite(const struct timespec *target, struct connection_closure *closure); +void update_elapsed_time(TimeSpec *delta, struct timespec *elapsed); + +/* logsrvd.c */ +extern struct client_message_switch cms_local; +bool start_protocol(struct connection_closure *closure); +void connection_close(struct connection_closure *closure); +bool schedule_commit_point(TimeSpec *commit_point, struct connection_closure *closure); +bool fmt_log_id_message(const char *id, struct connection_closure *closure); +bool schedule_error_message(const char *errstr, struct connection_closure *closure); +struct connection_buffer *get_free_buf(size_t, struct connection_closure *closure); +struct connection_closure *connection_closure_alloc(int fd, bool tls, bool relay_only, struct sudo_event_base *base); + +/* logsrvd_conf.c */ +bool logsrvd_conf_read(const char *path); +const char *logsrvd_conf_iolog_dir(void); +const char *logsrvd_conf_iolog_file(void); +bool logsrvd_conf_iolog_log_passwords(void); +void *logsrvd_conf_iolog_passprompt_regex(void); +struct server_address_list *logsrvd_conf_server_listen_address(void); +struct server_address_list *logsrvd_conf_relay_address(void); +const char *logsrvd_conf_relay_dir(void); +bool logsrvd_conf_relay_store_first(void); +bool logsrvd_conf_relay_tcp_keepalive(void); +bool logsrvd_conf_server_tcp_keepalive(void); +const char *logsrvd_conf_pid_file(void); +struct timespec *logsrvd_conf_server_timeout(void); +struct timespec *logsrvd_conf_relay_connect_timeout(void); +struct timespec *logsrvd_conf_relay_timeout(void); +time_t logsrvd_conf_relay_retry_interval(void); +#if defined(HAVE_OPENSSL) +bool logsrvd_conf_server_tls_check_peer(void); +SSL_CTX *logsrvd_server_tls_ctx(void); +bool logsrvd_conf_relay_tls_check_peer(void); +SSL_CTX *logsrvd_relay_tls_ctx(void); +#endif +bool logsrvd_conf_log_exit(void); +uid_t logsrvd_conf_iolog_uid(void); +gid_t logsrvd_conf_iolog_gid(void); +mode_t logsrvd_conf_iolog_mode(void); +void address_list_addref(struct server_address_list *); +void address_list_delref(struct server_address_list *); +void logsrvd_conf_cleanup(void); +void logsrvd_warn_stderr(bool enabled); + +/* logsrvd_journal.c */ +extern struct client_message_switch cms_journal; + +/* logsrvd_local.c */ +extern struct client_message_switch cms_local; +bool set_random_drop(const char *dropstr); +bool store_accept_local(AcceptMessage *msg, uint8_t *buf, size_t len, struct connection_closure *closure); +bool store_reject_local(RejectMessage *msg, uint8_t *buf, size_t len, struct connection_closure *closure); +bool store_exit_local(ExitMessage *msg, uint8_t *buf, size_t len, struct connection_closure *closure); +bool store_restart_local(RestartMessage *msg, uint8_t *buf, size_t len, struct connection_closure *closure); +bool store_alert_local(AlertMessage *msg, uint8_t *buf, size_t len, struct connection_closure *closure); +bool store_iobuf_local(int iofd, IoBuffer *iobuf, uint8_t *buf, size_t len, struct connection_closure *closure); +bool store_winsize_local(ChangeWindowSize *msg, uint8_t *buf, size_t len, struct connection_closure *closure); +bool store_suspend_local(CommandSuspend *msg, uint8_t *buf, size_t len, struct connection_closure *closure); + +/* logsrvd_queue.c */ +bool logsrvd_queue_enable(time_t timeout, struct sudo_event_base *evbase); +bool logsrvd_queue_insert(struct connection_closure *closure); +bool logsrvd_queue_scan(struct sudo_event_base *evbase); +void logsrvd_queue_dump(void); + +/* logsrvd_relay.c */ +extern struct client_message_switch cms_relay; +void relay_closure_free(struct relay_closure *relay_closure); +bool connect_relay(struct connection_closure *closure); +bool relay_shutdown(struct connection_closure *closure); + +#endif /* SUDO_LOGSRVD_H */ diff --git a/logsrvd/logsrvd_conf.c b/logsrvd/logsrvd_conf.c new file mode 100644 index 0000000..1cde0ba --- /dev/null +++ b/logsrvd/logsrvd_conf.c @@ -0,0 +1,1907 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> + +#include <errno.h> +#include <ctype.h> +#include <fcntl.h> +#include <limits.h> +#include <netdb.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <time.h> +#include <unistd.h> +#include <grp.h> +#include <pwd.h> +#ifndef HAVE_GETADDRINFO +# include "compat/getaddrinfo.h" +#endif + +#include "pathnames.h" +#include "sudo_compat.h" +#include "sudo_debug.h" +#include "sudo_eventlog.h" +#include "sudo_fatal.h" +#include "sudo_gettext.h" +#include "sudo_iolog.h" +#include "sudo_util.h" + +#include "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" + +/* Evaluates to true if at least one TLS field is set, else false. */ +# define TLS_CONFIGURED(_s) \ + ((_s).tls_key_path != NULL || (_s).tls_cert_path != NULL || \ + (_s).tls_cacert_path != NULL || (_s).tls_dhparams_path != NULL || \ + (_s).tls_ciphers_v12 != NULL || (_s).tls_ciphers_v13 != NULL || \ + (_s).tls_verify != -1) + +/* Evaluates to the relay-specific TLS setting, falling back to server. */ +# define TLS_RELAY_STR(_c, _f) \ + ((_c)->relay._f != NULL ? (_c)->relay._f : (_c)->server._f) + +# define TLS_RELAY_INT(_c, _f) \ + ((_c)->relay._f != -1 ? (_c)->relay._f : (_c)->server._f) +#endif + +enum server_log_type { + SERVER_LOG_NONE, + SERVER_LOG_STDERR, + SERVER_LOG_SYSLOG, + SERVER_LOG_FILE +}; + +struct logsrvd_config; +typedef bool (*logsrvd_conf_cb_t)(struct logsrvd_config *, const char *, size_t); + +struct logsrvd_config_entry { + const char *conf_str; + logsrvd_conf_cb_t setter; + size_t offset; +}; + +struct logsrvd_config_section { + const char *name; + struct logsrvd_config_entry *entries; +}; + +struct address_list_container { + unsigned int refcnt; + struct server_address_list addrs; +}; + +static struct logsrvd_config { + struct logsrvd_config_server { + struct address_list_container addresses; + struct timespec timeout; + bool tcp_keepalive; + enum server_log_type log_type; + FILE *log_stream; + char *log_file; + char *pid_file; +#if defined(HAVE_OPENSSL) + char *tls_key_path; + char *tls_cert_path; + char *tls_cacert_path; + char *tls_dhparams_path; + char *tls_ciphers_v12; + char *tls_ciphers_v13; + int tls_check_peer; + int tls_verify; + SSL_CTX *ssl_ctx; +#endif + } server; + struct logsrvd_config_relay { + struct address_list_container relays; + struct timespec connect_timeout; + struct timespec timeout; + time_t retry_interval; + char *relay_dir; + bool tcp_keepalive; + bool store_first; +#if defined(HAVE_OPENSSL) + char *tls_key_path; + char *tls_cert_path; + char *tls_cacert_path; + char *tls_dhparams_path; + char *tls_ciphers_v12; + char *tls_ciphers_v13; + int tls_check_peer; + int tls_verify; + SSL_CTX *ssl_ctx; +#endif + } relay; + struct logsrvd_config_iolog { + bool compress; + bool flush; + bool gid_set; + bool log_passwords; + uid_t uid; + gid_t gid; + mode_t mode; + unsigned int maxseq; + char *iolog_dir; + char *iolog_file; + void *passprompt_regex; + } iolog; + struct logsrvd_config_eventlog { + int log_type; + bool log_exit; + enum eventlog_format log_format; + } eventlog; + struct logsrvd_config_syslog { + unsigned int maxlen; + int server_facility; + int facility; + int acceptpri; + int rejectpri; + int alertpri; + } syslog; + struct logsrvd_config_logfile { + char *path; + char *time_format; + FILE *stream; + } logfile; +} *logsrvd_config; + +static bool logsrvd_warn_enable_stderr = true; + +/* eventlog getters */ +bool +logsrvd_conf_log_exit(void) +{ + return logsrvd_config->eventlog.log_exit; +} + +/* iolog getters */ +uid_t +logsrvd_conf_iolog_uid(void) +{ + return logsrvd_config->iolog.uid; +} + +gid_t +logsrvd_conf_iolog_gid(void) +{ + return logsrvd_config->iolog.gid; +} + +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; +} + +bool +logsrvd_conf_iolog_log_passwords(void) +{ + return logsrvd_config->iolog.log_passwords; +} + +void * +logsrvd_conf_iolog_passprompt_regex(void) +{ + return logsrvd_config->iolog.passprompt_regex; +} + +/* server getters */ +struct server_address_list * +logsrvd_conf_server_listen_address(void) +{ + return &logsrvd_config->server.addresses.addrs; +} + +bool +logsrvd_conf_server_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_server_timeout(void) +{ + if (sudo_timespecisset(&logsrvd_config->server.timeout)) { + return &logsrvd_config->server.timeout; + } + + return NULL; +} + +#if defined(HAVE_OPENSSL) +SSL_CTX * +logsrvd_server_tls_ctx(void) +{ + return logsrvd_config->server.ssl_ctx; +} + +bool +logsrvd_conf_server_tls_check_peer(void) +{ + return logsrvd_config->server.tls_check_peer; +} +#endif + +/* relay getters */ +struct server_address_list * +logsrvd_conf_relay_address(void) +{ + return &logsrvd_config->relay.relays.addrs; +} + +const char * +logsrvd_conf_relay_dir(void) +{ + return logsrvd_config->relay.relay_dir; +} + +bool +logsrvd_conf_relay_store_first(void) +{ + return logsrvd_config->relay.store_first; +} + +bool +logsrvd_conf_relay_tcp_keepalive(void) +{ + return logsrvd_config->relay.tcp_keepalive; +} + +struct timespec * +logsrvd_conf_relay_timeout(void) +{ + if (sudo_timespecisset(&logsrvd_config->relay.timeout)) { + return &logsrvd_config->relay.timeout; + } + + return NULL; +} + +struct timespec * +logsrvd_conf_relay_connect_timeout(void) +{ + if (sudo_timespecisset(&logsrvd_config->relay.connect_timeout)) { + return &logsrvd_config->relay.connect_timeout; + } + + return NULL; +} + +time_t +logsrvd_conf_relay_retry_interval(void) +{ + return logsrvd_config->relay.retry_interval; +} + +#if defined(HAVE_OPENSSL) +SSL_CTX * +logsrvd_relay_tls_ctx(void) +{ + if (logsrvd_config->relay.ssl_ctx != NULL) + return logsrvd_config->relay.ssl_ctx; + return logsrvd_config->server.ssl_ctx; +} + +bool +logsrvd_conf_relay_tls_check_peer(void) +{ + if (logsrvd_config->relay.tls_check_peer != -1) + return logsrvd_config->relay.tls_check_peer; + return logsrvd_config->server.tls_check_peer; +} +#endif + +/* I/O log callbacks */ +static bool +cb_iolog_dir(struct logsrvd_config *config, const char *path, size_t offset) +{ + debug_decl(cb_iolog_dir, SUDO_DEBUG_UTIL); + + free(config->iolog.iolog_dir); + if ((config->iolog.iolog_dir = strdup(path)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + debug_return_bool(true); +} + +static bool +cb_iolog_file(struct logsrvd_config *config, const char *path, size_t offset) +{ + debug_decl(cb_iolog_file, SUDO_DEBUG_UTIL); + + free(config->iolog.iolog_file); + if ((config->iolog.iolog_file = strdup(path)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + debug_return_bool(true); +} + +static bool +cb_iolog_compress(struct logsrvd_config *config, const char *str, size_t offset) +{ + 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_log_passwords(struct logsrvd_config *config, const char *str, size_t offset) +{ + int val; + debug_decl(cb_iolog_log_passwords, SUDO_DEBUG_UTIL); + + if ((val = sudo_strtobool(str)) == -1) + debug_return_bool(false); + + config->iolog.log_passwords = val; + debug_return_bool(true); +} + +static bool +cb_iolog_flush(struct logsrvd_config *config, const char *str, size_t offset) +{ + 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, size_t offset) +{ + struct passwd *pw; + debug_decl(cb_iolog_user, SUDO_DEBUG_UTIL); + + if ((pw = getpwnam(user)) == NULL) { + sudo_warnx(U_("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, size_t offset) +{ + struct group *gr; + debug_decl(cb_iolog_group, SUDO_DEBUG_UTIL); + + if ((gr = getgrnam(group)) == NULL) { + sudo_warnx(U_("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, size_t offset) +{ + const char *errstr; + mode_t mode; + debug_decl(cb_iolog_mode, SUDO_DEBUG_UTIL); + + mode = sudo_strtomode(str, &errstr); + if (errstr != NULL) { + sudo_warnx(U_("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, size_t offset) +{ + 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_warnx(U_("invalid value for %s: %s"), "maxseq", 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); +} + +static bool +cb_iolog_passprompt_regex(struct logsrvd_config *config, const char *str, size_t offset) +{ + debug_decl(cb_iolog_passprompt_regex, SUDO_DEBUG_UTIL); + + if (config->iolog.passprompt_regex == NULL) { + /* Lazy alloc of the passprompt regex handle. */ + config->iolog.passprompt_regex = iolog_pwfilt_alloc(); + if (config->iolog.passprompt_regex == NULL) + debug_return_bool(false); + } + debug_return_bool(iolog_pwfilt_add(config->iolog.passprompt_regex, str)); +} + +/* Server callbacks */ +static bool +append_address(struct server_address_list *addresses, const char *str, + bool allow_wildcard) +{ + struct addrinfo hints, *res, *res0 = NULL; + char *sa_str = NULL, *sa_host = NULL; + char *copy, *host, *port; + bool tls, ret = false; + int error; + debug_decl(append_address, SUDO_DEBUG_UTIL); + + if ((copy = strdup(str)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + 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') { + if (!allow_wildcard) + goto done; + host = NULL; + } + +#if !defined(HAVE_OPENSSL) + if (tls) { + sudo_warnx("%s", U_("TLS not supported")); + goto done; + } +#endif + + /* Only make a single copy of the string + host for all addresses. */ + if ((sa_str = sudo_rcstr_dup(str)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + if (host != NULL && (sa_host = sudo_rcstr_dup(host)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + + /* 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; +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + hints.ai_flags |= AI_NUMERICSERV; +#endif + 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 server_address *addr; + + if ((addr = malloc(sizeof(*addr))) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + addr->sa_str = sudo_rcstr_addref(sa_str); + addr->sa_host = sudo_rcstr_addref(sa_host); + + memcpy(&addr->sa_un, res->ai_addr, res->ai_addrlen); + addr->sa_size = res->ai_addrlen; + addr->tls = tls; + TAILQ_INSERT_TAIL(addresses, addr, entries); + } + + ret = true; +done: + sudo_rcstr_delref(sa_str); + sudo_rcstr_delref(sa_host); + if (res0 != NULL) + freeaddrinfo(res0); + free(copy); + debug_return_bool(ret); +} + +static bool +cb_server_listen_address(struct logsrvd_config *config, const char *str, size_t offset) +{ + return append_address(&config->server.addresses.addrs, str, true); +} + +static bool +cb_server_timeout(struct logsrvd_config *config, const char *str, size_t offset) +{ + time_t timeout; + const char *errstr; + debug_decl(cb_server_timeout, SUDO_DEBUG_UTIL); + + timeout = sudo_strtonum(str, 0, TIME_T_MAX, &errstr); + if (errstr != NULL) + debug_return_bool(false); + + config->server.timeout.tv_sec = timeout; + + debug_return_bool(true); +} + +static bool +cb_server_keepalive(struct logsrvd_config *config, const char *str, size_t offset) +{ + int val; + debug_decl(cb_server_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_server_pid_file(struct logsrvd_config *config, const char *str, size_t offset) +{ + char *copy = NULL; + debug_decl(cb_server_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_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + } + + free(config->server.pid_file); + config->server.pid_file = copy; + + debug_return_bool(true); +} + +static bool +cb_server_log(struct logsrvd_config *config, const char *str, size_t offset) +{ + char *copy = NULL; + enum server_log_type log_type = SERVER_LOG_NONE; + debug_decl(cb_server_log, SUDO_DEBUG_UTIL); + + /* An empty value means to disable the server log. */ + if (*str != '\0') { + if (*str == '/') { + log_type = SERVER_LOG_FILE; + if ((copy = strdup(str)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + debug_return_bool(false); + } + } else if (strcmp(str, "stderr") == 0) { + log_type = SERVER_LOG_STDERR; + } else if (strcmp(str, "syslog") == 0) { + log_type = SERVER_LOG_SYSLOG; + } else { + debug_return_bool(false); + } + } + + free(config->server.log_file); + config->server.log_file = copy; + config->server.log_type = log_type; + + debug_return_bool(true); +} + +#if defined(HAVE_OPENSSL) +static bool +cb_tls_key(struct logsrvd_config *config, const char *path, size_t offset) +{ + char **p = (char **)((char *)config + offset); + debug_decl(cb_tls_key, SUDO_DEBUG_UTIL); + + free(*p); + if ((*p = strdup(path)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + debug_return_bool(true); +} + +static bool +cb_tls_cacert(struct logsrvd_config *config, const char *path, size_t offset) +{ + char **p = (char **)((char *)config + offset); + debug_decl(cb_tls_cacert, SUDO_DEBUG_UTIL); + + free(*p); + if ((*p = strdup(path)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + debug_return_bool(true); +} + +static bool +cb_tls_cert(struct logsrvd_config *config, const char *path, size_t offset) +{ + char **p = (char **)((char *)config + offset); + debug_decl(cb_tls_cert, SUDO_DEBUG_UTIL); + + free(*p); + if ((*p = strdup(path)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + debug_return_bool(true); +} + +static bool +cb_tls_dhparams(struct logsrvd_config *config, const char *path, size_t offset) +{ + char **p = (char **)((char *)config + offset); + debug_decl(cb_tls_dhparams, SUDO_DEBUG_UTIL); + + free(*p); + if ((*p = strdup(path)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + debug_return_bool(true); +} + +static bool +cb_tls_ciphers12(struct logsrvd_config *config, const char *str, size_t offset) +{ + char **p = (char **)((char *)config + offset); + debug_decl(cb_tls_ciphers12, SUDO_DEBUG_UTIL); + + free(*p); + if ((*p = strdup(str)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + debug_return_bool(true); +} + +static bool +cb_tls_ciphers13(struct logsrvd_config *config, const char *str, size_t offset) +{ + char **p = (char **)((char *)config + offset); + debug_decl(cb_tls_ciphers13, SUDO_DEBUG_UTIL); + + free(*p); + if ((*p = strdup(str)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + debug_return_bool(true); +} + +static bool +cb_tls_verify(struct logsrvd_config *config, const char *str, size_t offset) +{ + int *p = (int *)((char *)config + offset); + int val; + debug_decl(cb_tls_verify, SUDO_DEBUG_UTIL); + + if ((val = sudo_strtobool(str)) == -1) + debug_return_bool(false); + + *p = val; + debug_return_bool(true); +} + +static bool +cb_tls_checkpeer(struct logsrvd_config *config, const char *str, size_t offset) +{ + int *p = (int *)((char *)config + offset); + int val; + debug_decl(cb_tls_checkpeer, SUDO_DEBUG_UTIL); + + if ((val = sudo_strtobool(str)) == -1) + debug_return_bool(false); + + *p = val; + debug_return_bool(true); +} +#endif + +/* relay callbacks */ +static bool +cb_relay_host(struct logsrvd_config *config, const char *str, size_t offset) +{ + return append_address(&config->relay.relays.addrs, str, false); +} + +static bool +cb_relay_timeout(struct logsrvd_config *config, const char *str, size_t offset) +{ + time_t timeout; + const char *errstr; + debug_decl(cb_relay_timeout, SUDO_DEBUG_UTIL); + + timeout = sudo_strtonum(str, 0, TIME_T_MAX, &errstr); + if (errstr != NULL) + debug_return_bool(false); + + config->server.timeout.tv_sec = timeout; + + debug_return_bool(true); +} + +static bool +cb_relay_connect_timeout(struct logsrvd_config *config, const char *str, size_t offset) +{ + time_t timeout; + const char *errstr; + debug_decl(cb_relay_connect_timeout, SUDO_DEBUG_UTIL); + + timeout = sudo_strtonum(str, 0, TIME_T_MAX, &errstr); + if (errstr != NULL) + debug_return_bool(false); + + config->relay.connect_timeout.tv_sec = timeout; + + debug_return_bool(true); +} + +static bool +cb_relay_dir(struct logsrvd_config *config, const char *str, size_t offset) +{ + char *copy = NULL; + debug_decl(cb_relay_dir, SUDO_DEBUG_UTIL); + + if ((copy = strdup(str)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + + free(config->relay.relay_dir); + config->relay.relay_dir = copy; + + debug_return_bool(true); +} + +static bool +cb_retry_interval(struct logsrvd_config *config, const char *str, size_t offset) +{ + time_t interval; + const char *errstr; + debug_decl(cb_retry_interval, SUDO_DEBUG_UTIL); + + interval = sudo_strtonum(str, 0, TIME_T_MAX, &errstr); + if (errstr != NULL) + debug_return_bool(false); + + config->relay.retry_interval = interval; + + debug_return_bool(true); +} + +static bool +cb_relay_store_first(struct logsrvd_config *config, const char *str, size_t offset) +{ + int val; + debug_decl(cb_relay_store_first, SUDO_DEBUG_UTIL); + + if ((val = sudo_strtobool(str)) == -1) + debug_return_bool(false); + + config->relay.store_first = val; + debug_return_bool(true); +} + +static bool +cb_relay_keepalive(struct logsrvd_config *config, const char *str, size_t offset) +{ + int val; + debug_decl(cb_relay_keepalive, SUDO_DEBUG_UTIL); + + if ((val = sudo_strtobool(str)) == -1) + debug_return_bool(false); + + config->relay.tcp_keepalive = val; + debug_return_bool(true); +} + +/* eventlog callbacks */ +static bool +cb_eventlog_type(struct logsrvd_config *config, const char *str, size_t offset) +{ + 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, size_t offset) +{ + 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); +} + +static bool +cb_eventlog_exit(struct logsrvd_config *config, const char *str, size_t offset) +{ + int val; + debug_decl(cb_eventlog_exit, SUDO_DEBUG_UTIL); + + if ((val = sudo_strtobool(str)) == -1) + debug_return_bool(false); + + config->eventlog.log_exit = val; + debug_return_bool(true); +} + +/* syslog callbacks */ +static bool +cb_syslog_maxlen(struct logsrvd_config *config, const char *str, size_t offset) +{ + 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_server_facility(struct logsrvd_config *config, const char *str, size_t offset) +{ + int logfac; + debug_decl(cb_syslog_server_facility, SUDO_DEBUG_UTIL); + + if (!sudo_str2logfac(str, &logfac)) { + sudo_warnx(U_("unknown syslog facility %s"), str); + debug_return_bool(false); + } + + config->syslog.server_facility = logfac; + + debug_return_bool(true); +} + +static bool +cb_syslog_facility(struct logsrvd_config *config, const char *str, size_t offset) +{ + int logfac; + debug_decl(cb_syslog_facility, SUDO_DEBUG_UTIL); + + if (!sudo_str2logfac(str, &logfac)) { + sudo_warnx(U_("unknown syslog facility %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, size_t offset) +{ + int logpri; + debug_decl(cb_syslog_acceptpri, SUDO_DEBUG_UTIL); + + if (!sudo_str2logpri(str, &logpri)) { + sudo_warnx(U_("unknown 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, size_t offset) +{ + int logpri; + debug_decl(cb_syslog_rejectpri, SUDO_DEBUG_UTIL); + + if (!sudo_str2logpri(str, &logpri)) { + sudo_warnx(U_("unknown syslog priority %s"), str); + debug_return_bool(false); + } + + config->syslog.rejectpri = logpri; + + debug_return_bool(true); +} + +static bool +cb_syslog_alertpri(struct logsrvd_config *config, const char *str, size_t offset) +{ + int logpri; + debug_decl(cb_syslog_alertpri, SUDO_DEBUG_UTIL); + + if (!sudo_str2logpri(str, &logpri)) { + sudo_warnx(U_("unknown 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, size_t offset) +{ + char *copy = NULL; + debug_decl(cb_logfile_path, SUDO_DEBUG_UTIL); + + if (*str != '/') { + sudo_warnx(U_("%s: not a fully qualified path"), str); + debug_return_bool(false); + } + if ((copy = strdup(str)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + 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, size_t offset) +{ + char *copy = NULL; + debug_decl(cb_logfile_time_format, SUDO_DEBUG_UTIL); + + if ((copy = strdup(str)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + + free(config->logfile.time_format); + config->logfile.time_format = copy; + + debug_return_bool(true); +} + +void +address_list_addref(struct server_address_list *al) +{ + struct address_list_container *container = + __containerof(al, struct address_list_container, addrs); + container->refcnt++; +} + +void +address_list_delref(struct server_address_list *al) +{ + struct address_list_container *container = + __containerof(al, struct address_list_container, addrs); + if (--container->refcnt == 0) { + struct server_address *addr; + while ((addr = TAILQ_FIRST(al))) { + TAILQ_REMOVE(al, addr, entries); + sudo_rcstr_delref(addr->sa_str); + sudo_rcstr_delref(addr->sa_host); + free(addr); + } + } +} + +static struct logsrvd_config_entry server_conf_entries[] = { + { "listen_address", cb_server_listen_address }, + { "timeout", cb_server_timeout }, + { "tcp_keepalive", cb_server_keepalive }, + { "pid_file", cb_server_pid_file }, + { "server_log", cb_server_log }, +#if defined(HAVE_OPENSSL) + { "tls_key", cb_tls_key, offsetof(struct logsrvd_config, server.tls_key_path) }, + { "tls_cacert", cb_tls_cacert, offsetof(struct logsrvd_config, server.tls_cacert_path) }, + { "tls_cert", cb_tls_cert, offsetof(struct logsrvd_config, server.tls_cert_path) }, + { "tls_dhparams", cb_tls_dhparams, offsetof(struct logsrvd_config, server.tls_dhparams_path) }, + { "tls_ciphers_v12", cb_tls_ciphers12, offsetof(struct logsrvd_config, server.tls_ciphers_v12) }, + { "tls_ciphers_v13", cb_tls_ciphers13, offsetof(struct logsrvd_config, server.tls_ciphers_v13) }, + { "tls_checkpeer", cb_tls_checkpeer, offsetof(struct logsrvd_config, server.tls_check_peer) }, + { "tls_verify", cb_tls_verify, offsetof(struct logsrvd_config, server.tls_verify) }, +#endif + { NULL } +}; + +static struct logsrvd_config_entry relay_conf_entries[] = { + { "relay_host", cb_relay_host }, + { "timeout", cb_relay_timeout }, + { "connect_timeout", cb_relay_connect_timeout }, + { "relay_dir", cb_relay_dir }, + { "retry_interval", cb_retry_interval }, + { "store_first", cb_relay_store_first }, + { "tcp_keepalive", cb_relay_keepalive }, +#if defined(HAVE_OPENSSL) + { "tls_key", cb_tls_key, offsetof(struct logsrvd_config, relay.tls_key_path) }, + { "tls_cacert", cb_tls_cacert, offsetof(struct logsrvd_config, relay.tls_cacert_path) }, + { "tls_cert", cb_tls_cert, offsetof(struct logsrvd_config, relay.tls_cert_path) }, + { "tls_dhparams", cb_tls_dhparams, offsetof(struct logsrvd_config, relay.tls_dhparams_path) }, + { "tls_ciphers_v12", cb_tls_ciphers12, offsetof(struct logsrvd_config, relay.tls_ciphers_v12) }, + { "tls_ciphers_v13", cb_tls_ciphers13, offsetof(struct logsrvd_config, relay.tls_ciphers_v13) }, + { "tls_checkpeer", cb_tls_checkpeer, offsetof(struct logsrvd_config, relay.tls_check_peer) }, + { "tls_verify", cb_tls_verify, offsetof(struct logsrvd_config, relay.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 }, + { "log_passwords", cb_iolog_log_passwords }, + { "maxseq", cb_iolog_maxseq }, + { "passprompt_regex", cb_iolog_passprompt_regex }, + { NULL } +}; + +static struct logsrvd_config_entry eventlog_conf_entries[] = { + { "log_type", cb_eventlog_type }, + { "log_format", cb_eventlog_format }, + { "log_exit", cb_eventlog_exit }, + { NULL } +}; + +static struct logsrvd_config_entry syslog_conf_entries[] = { + { "maxlen", cb_syslog_maxlen }, + { "server_facility", cb_syslog_server_facility }, + { "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 }, + { "relay", relay_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 *cp, *section_name = line + 1; + + if ((ep = strchr(section_name, ']')) == NULL) { + sudo_warnx(U_("%s:%d unmatched '[': %s"), + path, lineno, line); + goto done; + } + for (cp = ep + 1; *cp != '\0'; cp++) { + if (!isspace((unsigned char)*cp)) { + sudo_warnx(U_("%s:%d garbage after ']': %s"), + path, lineno, line); + goto done; + } + } + *ep = '\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, entry->offset)) { + 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 [%s] illegal key: %s"), path, lineno, + conf_section->name, line); + goto done; + } + } + ret = true; + +done: + free(line); + debug_return_bool(ret); +} + +static FILE * +logsrvd_open_log_file(const char *path, int flags) +{ + mode_t oldmask; + FILE *fp = NULL; + const char *omode; + int fd; + debug_decl(logsrvd_open_log_file, SUDO_DEBUG_UTIL); + + if (ISSET(flags, O_APPEND)) { + omode = "a"; + } else { + omode = "w"; + } + oldmask = umask(S_IRWXG|S_IRWXO); + fd = open(path, flags, S_IRUSR|S_IWUSR); + (void)umask(oldmask); + if (fd == -1 || (fp = fdopen(fd, omode)) == NULL) { + sudo_warn(U_("unable to open log file %s"), path); + if (fd != -1) + close(fd); + } + + debug_return_ptr(fp); +} + +static FILE * +logsrvd_open_eventlog(struct logsrvd_config *config) +{ + int 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; + } else { + flags = O_WRONLY|O_APPEND|O_CREAT; + } + debug_return_ptr(logsrvd_open_log_file(config->logfile.path, flags)); +} + +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 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; +} + +/* Set I/O log configuration settings from logsrvd config. */ +static void +logsrvd_conf_iolog_setconf(struct logsrvd_config *config) +{ + debug_decl(logsrvd_conf_iolog_setconf, SUDO_DEBUG_UTIL); + + 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); + + debug_return; +} + +/* + * Conversation function for use by sudo_warn/sudo_fatal. + * Logs to stdout/stderr. + */ +static int +logsrvd_conv_stderr(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback) +{ + int i; + debug_decl(logsrvd_conv_stderr, SUDO_DEBUG_UTIL); + + for (i = 0; i < num_msgs; i++) { + if (fputs(msgs[i].msg, stderr) == EOF) + debug_return_int(-1); + } + + debug_return_int(0); +} + +/* + * Conversation function for use by sudo_warn/sudo_fatal. + * Acts as a no-op log sink. + */ +static int +logsrvd_conv_none(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback) +{ + /* Also write to stderr if still in the foreground. */ + if (logsrvd_warn_enable_stderr) { + (void)logsrvd_conv_stderr(num_msgs, msgs, replies, callback); + } + + return 0; +} + +/* + * Conversation function for use by sudo_warn/sudo_fatal. + * Logs to syslog. + */ +static int +logsrvd_conv_syslog(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback) +{ + char *buf = NULL, *cp = NULL; + const char *progname; + size_t proglen, bufsize = 0; + int i; + debug_decl(logsrvd_conv_syslog, SUDO_DEBUG_UTIL); + + if (logsrvd_config == NULL) { + debug_return_int(logsrvd_conv_stderr(num_msgs, msgs, replies, callback)); + } + + /* Also write to stderr if still in the foreground. */ + if (logsrvd_warn_enable_stderr) { + (void)logsrvd_conv_stderr(num_msgs, msgs, replies, callback); + } + + /* + * Concat messages into a flag string that we can syslog. + */ + progname = getprogname(); + proglen = strlen(progname); + for (i = 0; i < num_msgs; i++) { + const char *msg = msgs[i].msg; + size_t len = strlen(msg); + size_t used = (size_t)(cp - buf); + + /* Strip leading "sudo_logsrvd: " prefix. */ + if (strncmp(msg, progname, proglen) == 0) { + msg += proglen; + len -= proglen; + if (len == 0) { + /* Skip over ": " string that follows program name. */ + if (i + 1 < num_msgs && strcmp(msgs[i + 1].msg, ": ") == 0) { + i++; + continue; + } + } else if (msg[0] == ':' && msg[1] == ' ') { + /* Handle "progname: " */ + msg += 2; + len -= 2; + } + } + + /* Strip off trailing newlines. */ + while (len > 1 && msg[len - 1] == '\n') + len--; + if (len == 0) + continue; + + if (len >= bufsize - used) { + bufsize += 1024; + char *tmp = realloc(buf, bufsize); + if (tmp == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + free(buf); + debug_return_int(-1); + } + buf = tmp; + cp = tmp + used; + } + memcpy(cp, msg, len); + cp[len] = '\0'; + cp += len; + } + if (buf != NULL) { + openlog(progname, 0, logsrvd_config->syslog.server_facility); + syslog(LOG_ERR, "%s", buf); + free(buf); + + /* Restore old syslog settings. */ + if (logsrvd_config->eventlog.log_type == EVLOG_SYSLOG) + openlog("sudo", 0, logsrvd_config->syslog.facility); + } + + debug_return_int(0); +} + +/* + * Conversation function for use by sudo_warn/sudo_fatal. + * Logs to an already-open log file. + */ +static int +logsrvd_conv_logfile(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback) +{ + const char *progname; + size_t proglen; + int i; + debug_decl(logsrvd_conv_logfile, SUDO_DEBUG_UTIL); + + if (logsrvd_config == NULL) { + debug_return_int(logsrvd_conv_stderr(num_msgs, msgs, replies, callback)); + } + + /* Also write to stderr if still in the foreground. */ + if (logsrvd_warn_enable_stderr) { + (void)logsrvd_conv_stderr(num_msgs, msgs, replies, callback); + } + + if (logsrvd_config->server.log_stream == NULL) { + errno = EBADF; + debug_return_int(-1); + } + + progname = getprogname(); + proglen = strlen(progname); + for (i = 0; i < num_msgs; i++) { + const char *msg = msgs[i].msg; + size_t len = strlen(msg); + + /* Strip leading "sudo_logsrvd: " prefix. */ + if (strncmp(msg, progname, proglen) == 0) { + msg += proglen; + len -= proglen; + if (len == 0) { + /* Skip over ": " string that follows program name. */ + if (i + 1 < num_msgs && strcmp(msgs[i + 1].msg, ": ") == 0) { + i++; + continue; + } + } else if (msg[0] == ':' && msg[1] == ' ') { + /* Handle "progname: " */ + msg += 2; + len -= 2; + } + } + + if (fwrite(msg, len, 1, logsrvd_config->server.log_stream) != 1) + debug_return_int(-1); + } + + debug_return_int(0); +} + +/* Free the specified struct logsrvd_config and its contents. */ +static void +logsrvd_conf_free(struct logsrvd_config *config) +{ + debug_decl(logsrvd_conf_free, SUDO_DEBUG_UTIL); + + if (config == NULL) + debug_return; + + /* struct logsrvd_config_server */ + address_list_delref(&config->server.addresses.addrs); + free(config->server.pid_file); + free(config->server.log_file); + if (config->server.log_stream != NULL) + fclose(config->server.log_stream); +#if defined(HAVE_OPENSSL) + free(config->server.tls_key_path); + free(config->server.tls_cert_path); + free(config->server.tls_cacert_path); + free(config->server.tls_dhparams_path); + free(config->server.tls_ciphers_v12); + free(config->server.tls_ciphers_v13); + + if (config->server.ssl_ctx != NULL) + SSL_CTX_free(config->server.ssl_ctx); +#endif + + /* struct logsrvd_config_relay */ + address_list_delref(&config->relay.relays.addrs); + free(config->relay.relay_dir); +#if defined(HAVE_OPENSSL) + free(config->relay.tls_key_path); + free(config->relay.tls_cert_path); + free(config->relay.tls_cacert_path); + free(config->relay.tls_dhparams_path); + free(config->relay.tls_ciphers_v12); + free(config->relay.tls_ciphers_v13); + + if (config->relay.ssl_ctx != NULL) + SSL_CTX_free(config->relay.ssl_ctx); +#endif + + /* struct logsrvd_config_iolog */ + free(config->iolog.iolog_dir); + free(config->iolog.iolog_file); + iolog_pwfilt_free(config->iolog.passprompt_regex); + + /* struct logsrvd_config_logfile */ + free(config->logfile.path); + free(config->logfile.time_format); + if (config->logfile.stream != NULL) + fclose(config->logfile.stream); + + free(config); + + debug_return; +} + +/* Allocate a new struct logsrvd_config and set default values. */ +static 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_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_ptr(NULL); + } + + /* Relay defaults */ + TAILQ_INIT(&config->relay.relays.addrs); + config->relay.relays.refcnt = 1; + config->relay.timeout.tv_sec = DEFAULT_SOCKET_TIMEOUT_SEC; + config->relay.connect_timeout.tv_sec = DEFAULT_SOCKET_TIMEOUT_SEC; + config->relay.tcp_keepalive = true; + config->relay.retry_interval = 30; + if (!cb_relay_dir(config, _PATH_SUDO_RELAY_DIR, 0)) + goto bad; +#if defined(HAVE_OPENSSL) + config->relay.tls_verify = -1; + config->relay.tls_check_peer = -1; +#endif + + /* Server defaults */ + TAILQ_INIT(&config->server.addresses.addrs); + config->server.addresses.refcnt = 1; + config->server.timeout.tv_sec = DEFAULT_SOCKET_TIMEOUT_SEC; + config->server.tcp_keepalive = true; + config->server.log_type = SERVER_LOG_SYSLOG; + config->server.pid_file = strdup(_PATH_SUDO_LOGSRVD_PID); + if (config->server.pid_file == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + 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_cacert_path = strdup(DEFAULT_CA_CERT_PATH); + if (config->server.tls_cacert_path == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + } + if (access(DEFAULT_SERVER_CERT_PATH, R_OK) == 0) { + config->server.tls_cert_path = strdup(DEFAULT_SERVER_CERT_PATH); + if (config->server.tls_cert_path == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + } + config->server.tls_key_path = strdup(DEFAULT_SERVER_KEY_PATH); + if (config->server.tls_key_path == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto bad; + } + config->server.tls_verify = true; + config->server.tls_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, 0)) + goto bad; + if (!cb_iolog_file(config, "%{seq}", 0)) + goto bad; + config->iolog.uid = ROOT_UID; + config->iolog.gid = ROOT_GID; + config->iolog.gid_set = false; + config->iolog.log_passwords = true; + + /* Event log defaults */ + config->eventlog.log_type = EVLOG_SYSLOG; + config->eventlog.log_format = EVLOG_SUDO; + config->eventlog.log_exit = false; + + /* Syslog defaults */ + config->syslog.maxlen = 960; + config->syslog.server_facility = LOG_DAEMON; + if (!cb_syslog_facility(config, LOGFAC, 0)) { + sudo_warnx(U_("unknown syslog facility %s"), LOGFAC); + goto bad; + } + if (!cb_syslog_acceptpri(config, PRI_SUCCESS, 0)) { + sudo_warnx(U_("unknown syslog priority %s"), PRI_SUCCESS); + goto bad; + } + if (!cb_syslog_rejectpri(config, PRI_FAILURE, 0)) { + sudo_warnx(U_("unknown syslog priority %s"), PRI_FAILURE); + goto bad; + } + if (!cb_syslog_alertpri(config, PRI_FAILURE, 0)) { + sudo_warnx(U_("unknown syslog priority %s"), PRI_FAILURE); + goto bad; + } + + /* Log file defaults */ + if (!cb_logfile_time_format(config, "%h %e %T", 0)) + goto bad; + if (!cb_logfile_path(config, _PATH_SUDO_LOGFILE, 0)) + goto bad; + + debug_return_ptr(config); +bad: + logsrvd_conf_free(config); + debug_return_ptr(NULL); +} + +static bool +logsrvd_conf_apply(struct logsrvd_config *config) +{ +#if defined(HAVE_OPENSSL) + struct server_address *addr; +#endif + debug_decl(logsrvd_conf_apply, SUDO_DEBUG_UTIL); + + /* There can be multiple passprompt regular expressions. */ + if (config->iolog.passprompt_regex == NULL) { + if (!cb_iolog_passprompt_regex(config, PASSPROMPT_REGEX, 0)) + debug_return_bool(false); + } + + /* There can be multiple addresses so we can't set a default earlier. */ + if (TAILQ_EMPTY(&config->server.addresses.addrs)) { + /* Enable plaintext listender. */ + if (!cb_server_listen_address(config, "*:" DEFAULT_PORT, 0)) + debug_return_bool(false); +#if defined(HAVE_OPENSSL) + /* If a certificate was specified, enable the TLS listener too. */ + if (config->server.tls_cert_path != NULL) { + if (!cb_server_listen_address(config, "*:" DEFAULT_PORT_TLS "(tls)", 0)) + debug_return_bool(false); + } + } else { + /* Check that TLS configuration is valid. */ + TAILQ_FOREACH(addr, &config->server.addresses.addrs, 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_cert_path == NULL) { + config->server.tls_cert_path = + strdup(DEFAULT_SERVER_CERT_PATH); + if (config->server.tls_cert_path == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + debug_return_bool(false); + } + } + break; + } +#endif /* HAVE_OPENSSL */ + } + +#if defined(HAVE_OPENSSL) + TAILQ_FOREACH(addr, &config->server.addresses.addrs, entries) { + if (!addr->tls) + continue; + /* Create a TLS context for the server. */ + config->server.ssl_ctx = init_tls_context( + config->server.tls_cacert_path, config->server.tls_cert_path, + config->server.tls_key_path, config->server.tls_dhparams_path, + config->server.tls_ciphers_v12, config->server.tls_ciphers_v13, + config->server.tls_verify); + if (config->server.ssl_ctx == NULL) { + sudo_warnx("%s", U_("unable to initialize server TLS context")); + debug_return_bool(false); + } + break; + } + + if (TLS_CONFIGURED(config->relay)) { + TAILQ_FOREACH(addr, &config->relay.relays.addrs, entries) { + if (!addr->tls) + continue; + /* Create a TLS context for the relay. */ + config->relay.ssl_ctx = init_tls_context( + TLS_RELAY_STR(config, tls_cacert_path), + TLS_RELAY_STR(config, tls_cert_path), + TLS_RELAY_STR(config, tls_key_path), + TLS_RELAY_STR(config, tls_dhparams_path), + TLS_RELAY_STR(config, tls_ciphers_v12), + TLS_RELAY_STR(config, tls_ciphers_v13), + TLS_RELAY_INT(config, tls_verify)); + if (config->relay.ssl_ctx == NULL) { + sudo_warnx("%s", U_("unable to initialize relay TLS context")); + debug_return_bool(false); + } + break; + } + } +#endif /* HAVE_OPENSSL */ + + /* Clear store_first if not relaying. */ + if (TAILQ_EMPTY(&config->relay.relays.addrs)) + config->relay.store_first = false; + + /* Open server log if specified. */ + switch (config->server.log_type) { + case SERVER_LOG_SYSLOG: + sudo_warn_set_conversation(logsrvd_conv_syslog); + break; + case SERVER_LOG_FILE: + config->server.log_stream = + logsrvd_open_log_file(config->server.log_file, O_WRONLY|O_APPEND|O_CREAT); + if (config->server.log_stream == NULL) + debug_return_bool(false); + sudo_warn_set_conversation(logsrvd_conv_logfile); + break; + case SERVER_LOG_NONE: + sudo_warn_set_conversation(logsrvd_conv_none); + break; + case SERVER_LOG_STDERR: + /* Default is stderr. */ + sudo_warn_set_conversation(NULL); + break; + default: + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "cannot open unknown log type %d", config->eventlog.log_type); + break; + } + + /* 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; + } + + /* + * Update event and I/O log library config and install the new + * logsrvd config. We must not fail past this point or the event + * and I/O log config will be inconsistent with the logsrvd config. + */ + logsrvd_conf_iolog_setconf(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); +} + +void +logsrvd_conf_cleanup(void) +{ + debug_decl(logsrvd_conf_cleanup, SUDO_DEBUG_UTIL); + + logsrvd_conf_free(logsrvd_config); + logsrvd_config = NULL; + + debug_return; +} + +void +logsrvd_warn_stderr(bool enabled) +{ + logsrvd_warn_enable_stderr = enabled; +} diff --git a/logsrvd/logsrvd_journal.c b/logsrvd/logsrvd_journal.c new file mode 100644 index 0000000..354d71c --- /dev/null +++ b/logsrvd/logsrvd_journal.c @@ -0,0 +1,616 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "sudo_compat.h" +#include "sudo_conf.h" +#include "sudo_debug.h" +#include "sudo_event.h" +#include "sudo_eventlog.h" +#include "sudo_fatal.h" +#include "sudo_gettext.h" +#include "sudo_iolog.h" +#include "sudo_util.h" + +#include "logsrvd.h" + +/* + * Helper function to set closure->journal and closure->journal_path. + */ +static bool +journal_fdopen(int fd, const char *journal_path, + struct connection_closure *closure) +{ + debug_decl(journal_fdopen, SUDO_DEBUG_UTIL); + + free(closure->journal_path); + closure->journal_path = strdup(journal_path); + if (closure->journal_path == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + + /* Defer fdopen() until last--it cannot be undone. */ + if (closure->journal != NULL) + fclose(closure->journal); + if ((closure->journal = fdopen(fd, "r+")) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to fdopen journal file %s", journal_path); + debug_return_bool(false); + } + + debug_return_bool(true); +} + +static int +journal_mkstemp(const char *parent_dir, char *pathbuf, int pathlen) +{ + int len, dfd = -1, fd = -1; + mode_t dirmode, oldmask; + char *template; + debug_decl(journal_mkstemp, SUDO_DEBUG_UTIL); + + /* umask must not be more restrictive than the file modes. */ + dirmode = logsrvd_conf_iolog_mode() | S_IXUSR; + if (dirmode & (S_IRGRP|S_IWGRP)) + dirmode |= S_IXGRP; + if (dirmode & (S_IROTH|S_IWOTH)) + dirmode |= S_IXOTH; + oldmask = umask(ACCESSPERMS & ~dirmode); + + len = snprintf(pathbuf, pathlen, "%s/%s/%s", + logsrvd_conf_relay_dir(), parent_dir, RELAY_TEMPLATE); + if (len >= pathlen) { + errno = ENAMETOOLONG; + sudo_warn("%s/%s/%s", logsrvd_conf_relay_dir(), parent_dir, + RELAY_TEMPLATE); + goto done; + } + dfd = sudo_open_parent_dir(pathbuf, logsrvd_conf_iolog_uid(), + logsrvd_conf_iolog_gid(), S_IRWXU|S_IXGRP|S_IXOTH, false); + if (dfd == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to create parent dir for %s", pathbuf); + goto done; + } + template = pathbuf + (len - strlen(RELAY_TEMPLATE)); + if ((fd = mkostempsat(dfd, template, 0, 0)) == -1) { + sudo_warn(U_("%s: %s"), "mkstemp", pathbuf); + goto done; + } + +done: + umask(oldmask); + if (dfd != -1) + close(dfd); + + debug_return_int(fd); +} + +/* + * Create a temporary file in the relay dir and store it in the closure. + */ +static bool +journal_create(struct connection_closure *closure) +{ + char journal_path[PATH_MAX]; + int fd; + debug_decl(journal_create, SUDO_DEBUG_UTIL); + + fd = journal_mkstemp("incoming", journal_path, sizeof(journal_path)); + if (fd == -1) { + closure->errstr = _("unable to create journal file"); + debug_return_bool(false); + } + if (!sudo_lock_file(fd, SUDO_TLOCK)) { + sudo_warn(U_("unable to lock %s"), journal_path); + unlink(journal_path); + close(fd); + closure->errstr = _("unable to lock journal file"); + debug_return_bool(false); + } + if (!journal_fdopen(fd, journal_path, closure)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to fdopen journal file %s", journal_path); + unlink(journal_path); + close(fd); + closure->errstr = _("unable to open journal file"); + debug_return_bool(false); + } + + debug_return_bool(true); +} + +/* + * Flush any buffered data, rewind journal to the beginning and + * move to the outgoing directory. + * The actual open file is closed in connection_closure_free(). + */ +static bool +journal_finish(struct connection_closure *closure) +{ + char outgoing_path[PATH_MAX]; + size_t len; + int fd; + debug_decl(journal_finish, SUDO_DEBUG_UTIL); + + if (fflush(closure->journal) != 0) { + closure->errstr = _("unable to write journal file"); + debug_return_bool(false); + } + rewind(closure->journal); + + /* Move journal to the outgoing directory. */ + fd = journal_mkstemp("outgoing", outgoing_path, sizeof(outgoing_path)); + if (fd == -1) { + closure->errstr = _("unable to rename journal file"); + debug_return_bool(false); + } + close(fd); + if (rename(closure->journal_path, outgoing_path) == -1) { + sudo_warn(U_("unable to rename %s to %s"), closure->journal_path, + outgoing_path); + closure->errstr = _("unable to rename journal file"); + unlink(outgoing_path); + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "renamed %s -> %s", closure->journal_path, outgoing_path); + len = strlen(outgoing_path); + if (strlen(closure->journal_path) == len) { + /* This should always be true. */ + memcpy(closure->journal_path, outgoing_path, len); + } else { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "length mismatch %zu != %zu", strlen(closure->journal_path), len); + free(closure->journal_path); + closure->journal_path = strdup(outgoing_path); + if (closure->journal_path == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + closure->errstr = _("unable to allocate memory"); + debug_return_bool(false); + } + } + + debug_return_bool(true); +} + +/* + * Seek ahead in the journal to the specified target time. + * Returns true if we reached the target time exactly, else false. + */ +static bool +journal_seek(struct timespec *target, struct connection_closure *closure) +{ + ClientMessage *msg = NULL; + size_t nread, bufsize = 0; + uint8_t *buf = NULL; + uint32_t msg_len; + bool ret = false; + debug_decl(journal_seek, SUDO_DEBUG_UTIL); + + for (;;) { + TimeSpec *delay = NULL; + + /* Read message size (uint32_t in network byte order). */ + nread = fread(&msg_len, sizeof(msg_len), 1, closure->journal); + if (nread != 1) { + if (feof(closure->journal)) { + sudo_warnx(U_("%s: %s"), closure->journal_path, + U_("unexpected EOF reading journal file")); + closure->errstr = _("unexpected EOF reading journal file"); + } else { + sudo_warn(U_("%s: %s"), closure->journal_path, + U_("error reading journal file")); + closure->errstr = _("error reading journal file"); + } + break; + } + msg_len = ntohl(msg_len); + if (msg_len > MESSAGE_SIZE_MAX) { + sudo_warnx(U_("%s: %s"), closure->journal_path, + U_("client message too large")); + closure->errstr = _("client message too large"); + break; + } + + /* Read actual message now that we know the size. */ + if (msg_len != 0) { + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "%s: reading message %u bytes", closure->journal_path, msg_len); + + if (msg_len > bufsize) { + bufsize = sudo_pow2_roundup(msg_len); + free(buf); + if ((buf = malloc(bufsize)) == NULL) { + closure->errstr = _("unable to allocate memory"); + break; + } + } + + nread = fread(buf, msg_len, 1, closure->journal); + if (nread != 1) { + if (feof(closure->journal)) { + sudo_warnx(U_("%s: %s"), closure->journal_path, + U_("unexpected EOF reading journal file")); + closure->errstr = _("unexpected EOF reading journal file"); + } else { + sudo_warn(U_("%s: %s"), closure->journal_path, + U_("error reading journal file")); + closure->errstr = _("error reading journal file"); + } + break; + } + } + + client_message__free_unpacked(msg, NULL); + msg = client_message__unpack(NULL, msg_len, buf); + if (msg == NULL) { + sudo_warnx(U_("unable to unpack %s size %zu"), "ClientMessage", + (size_t)msg_len); + closure->errstr = _("invalid journal file, unable to restart"); + break; + } + + switch (msg->type_case) { + case CLIENT_MESSAGE__TYPE_HELLO_MSG: + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "seeking past ClientHello (%d)", msg->type_case); + break; + case CLIENT_MESSAGE__TYPE_ACCEPT_MSG: + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "seeking past AcceptMessage (%d)", msg->type_case); + break; + case CLIENT_MESSAGE__TYPE_REJECT_MSG: + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "seeking past RejectMessage (%d)", msg->type_case); + break; + case CLIENT_MESSAGE__TYPE_EXIT_MSG: + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "seeking past ExitMessage (%d)", msg->type_case); + break; + case CLIENT_MESSAGE__TYPE_RESTART_MSG: + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "seeking past RestartMessage (%d)", msg->type_case); + break; + case CLIENT_MESSAGE__TYPE_ALERT_MSG: + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "seeking past AlertMessage (%d)", msg->type_case); + break; + case CLIENT_MESSAGE__TYPE_TTYIN_BUF: + delay = msg->u.ttyin_buf->delay; + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "read IoBuffer (%d), delay [%lld, %ld]", msg->type_case, + (long long)delay->tv_sec, (long)delay->tv_nsec); + break; + case CLIENT_MESSAGE__TYPE_TTYOUT_BUF: + delay = msg->u.ttyout_buf->delay; + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "read IoBuffer (%d), delay [%lld, %ld]", msg->type_case, + (long long)delay->tv_sec, (long)delay->tv_nsec); + break; + case CLIENT_MESSAGE__TYPE_STDIN_BUF: + delay = msg->u.stdin_buf->delay; + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "read IoBuffer (%d), delay [%lld, %ld]", msg->type_case, + (long long)delay->tv_sec, (long)delay->tv_nsec); + break; + case CLIENT_MESSAGE__TYPE_STDOUT_BUF: + delay = msg->u.stdout_buf->delay; + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "read stdout_buf (%d), delay [%lld, %ld]", msg->type_case, + (long long)delay->tv_sec, (long)delay->tv_nsec); + break; + case CLIENT_MESSAGE__TYPE_STDERR_BUF: + delay = msg->u.stderr_buf->delay; + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "read stderr_buf (%d), delay [%lld, %ld]", msg->type_case, + (long long)delay->tv_sec, (long)delay->tv_nsec); + break; + case CLIENT_MESSAGE__TYPE_WINSIZE_EVENT: + delay = msg->u.winsize_event->delay; + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "read ChangeWindowSize (%d), delay [%lld, %ld]", msg->type_case, + (long long)delay->tv_sec, (long)delay->tv_nsec); + break; + case CLIENT_MESSAGE__TYPE_SUSPEND_EVENT: + delay = msg->u.suspend_event->delay; + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "read CommandSuspend (%d), delay [%lld, %ld]", msg->type_case, + (long long)delay->tv_sec, (long)delay->tv_nsec); + break; + default: + sudo_warnx(U_("unexpected type_case value %d in %s from %s"), + msg->type_case, "ClientMessage", closure->journal_path); + break; + } + if (delay != NULL) + update_elapsed_time(delay, &closure->elapsed_time); + + if (sudo_timespeccmp(&closure->elapsed_time, target, >=)) { + if (sudo_timespeccmp(&closure->elapsed_time, target, ==)) { + ret = true; + break; + } + + /* Mismatch between resume point and stored log. */ + closure->errstr = _("invalid journal file, unable to restart"); + sudo_warnx(U_("%s: unable to find resume point [%lld, %ld]"), + closure->journal_path, (long long)target->tv_sec, + target->tv_nsec); + break; + } + } + + client_message__free_unpacked(msg, NULL); + free(buf); + + debug_return_bool(ret); +} + +/* + * Restart an existing journal. + * Seeks to the resume_point in RestartMessage before continuing. + * Returns true if we reached the target time exactly, else false. + */ +static bool +journal_restart(RestartMessage *msg, uint8_t *buf, size_t buflen, + struct connection_closure *closure) +{ + struct timespec target; + int fd, len; + char *cp, journal_path[PATH_MAX]; + debug_decl(journal_restart, SUDO_DEBUG_UTIL); + + /* Strip off leading hostname from log_id. */ + if ((cp = strchr(msg->log_id, '/')) != NULL) { + if (cp != msg->log_id) + cp++; + } else { + cp = msg->log_id; + } + len = snprintf(journal_path, sizeof(journal_path), "%s/incoming/%s", + logsrvd_conf_relay_dir(), cp); + if (len >= ssizeof(journal_path)) { + errno = ENAMETOOLONG; + sudo_warn("%s/incoming/%s", logsrvd_conf_relay_dir(), cp); + closure->errstr = _("unable to create journal file"); + debug_return_bool(false); + } + if ((fd = open(journal_path, O_RDWR)) == -1) { + sudo_warn(U_("unable to open %s"), journal_path); + closure->errstr = _("unable to create journal file"); + debug_return_bool(false); + } + if (!journal_fdopen(fd, journal_path, closure)) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + close(fd); + closure->errstr = _("unable to allocate memory"); + debug_return_bool(false); + } + + /* Seek forward to resume point. */ + target.tv_sec = msg->resume_point->tv_sec; + target.tv_nsec = msg->resume_point->tv_nsec; + if (!journal_seek(&target, closure)) { + sudo_warn(U_("unable to seek to [%lld, %ld] in journal file %s"), + (long long)target.tv_sec, target.tv_nsec, journal_path); + debug_return_bool(false); + } + + debug_return_bool(true); +} + +static bool +journal_write(uint8_t *buf, size_t len, struct connection_closure *closure) +{ + uint32_t msg_len; + debug_decl(journal_write, SUDO_DEBUG_UTIL); + + /* 32-bit message length in network byte order. */ + msg_len = htonl((uint32_t)len); + if (fwrite(&msg_len, 1, sizeof(msg_len), closure->journal) != sizeof(msg_len)) { + closure->errstr = _("unable to write journal file"); + debug_return_bool(false); + } + /* message payload */ + if (fwrite(buf, 1, len, closure->journal) != len) { + closure->errstr = _("unable to write journal file"); + debug_return_bool(false); + } + debug_return_bool(true); +} + +/* + * Store an AcceptMessage from the client in the journal. + */ +static bool +journal_accept(AcceptMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + debug_decl(journal_accept, SUDO_DEBUG_UTIL); + + if (closure->journal_path != NULL) { + /* Re-use existing journal file. */ + debug_return_bool(journal_write(buf, len, closure)); + } + + /* Store message in a journal for later relaying. */ + if (!journal_create(closure)) + debug_return_bool(false); + if (!journal_write(buf, len, closure)) + debug_return_bool(false); + + if (msg->expect_iobufs) { + /* Send log ID to client for restarting connections. */ + if (!fmt_log_id_message(closure->journal_path, closure)) + debug_return_bool(false); + if (sudo_ev_add(closure->evbase, closure->write_ev, + logsrvd_conf_server_timeout(), false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + debug_return_bool(false); + } + } + + debug_return_bool(true); +} + +/* + * Store a RejectMessage from the client in the journal. + */ +static bool +journal_reject(RejectMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + debug_decl(journal_reject, SUDO_DEBUG_UTIL); + + /* Store message in a journal for later relaying. */ + if (closure->journal_path == NULL) { + if (!journal_create(closure)) + debug_return_bool(false); + } + if (!journal_write(buf, len, closure)) + debug_return_bool(false); + + debug_return_bool(true); +} + +/* + * Store an ExitMessage from the client in the journal. + */ +static bool +journal_exit(ExitMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + debug_decl(journal_exit, SUDO_DEBUG_UTIL); + + /* Store exit message in journal. */ + if (!journal_write(buf, len, closure)) + debug_return_bool(false); + if (!journal_finish(closure)) + debug_return_bool(false); + + debug_return_bool(true); +} + +/* + * Store an AlertMessage from the client in the journal. + */ +static bool +journal_alert(AlertMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + debug_decl(journal_alert, SUDO_DEBUG_UTIL); + + /* Store message in a journal for later relaying. */ + if (closure->journal_path == NULL) { + if (!journal_create(closure)) + debug_return_bool(false); + } + if (!journal_write(buf, len, closure)) + debug_return_bool(false); + + debug_return_bool(true); +} + +/* + * Store an IoBuffer from the client in the journal. + */ +static bool +journal_iobuf(int iofd, IoBuffer *iobuf, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + debug_decl(journal_iobuf, SUDO_DEBUG_UTIL); + + if (!journal_write(buf, len, closure)) + debug_return_bool(false); + update_elapsed_time(iobuf->delay, &closure->elapsed_time); + + debug_return_bool(true); +} + +/* + * Store a CommandSuspend message from the client in the journal. + */ +static bool +journal_suspend(CommandSuspend *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + debug_decl(journal_suspend, SUDO_DEBUG_UTIL); + + update_elapsed_time(msg->delay, &closure->elapsed_time); + + debug_return_bool(journal_write(buf, len, closure)); +} + +/* + * Store a ChangeWindowSize message from the client in the journal. + */ +static bool +journal_winsize(ChangeWindowSize *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + debug_decl(journal_winsize, SUDO_DEBUG_UTIL); + + update_elapsed_time(msg->delay, &closure->elapsed_time); + + debug_return_bool(journal_write(buf, len, closure)); +} + +struct client_message_switch cms_journal = { + journal_accept, + journal_reject, + journal_exit, + journal_restart, + journal_alert, + journal_iobuf, + journal_suspend, + journal_winsize +}; diff --git a/logsrvd/logsrvd_local.c b/logsrvd/logsrvd_local.c new file mode 100644 index 0000000..99b0a3f --- /dev/null +++ b/logsrvd/logsrvd_local.c @@ -0,0 +1,713 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/uio.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "sudo_compat.h" +#include "sudo_conf.h" +#include "sudo_debug.h" +#include "sudo_event.h" +#include "sudo_eventlog.h" +#include "sudo_fatal.h" +#include "sudo_gettext.h" +#include "sudo_json.h" +#include "sudo_iolog.h" +#include "sudo_rand.h" +#include "sudo_util.h" + +#include "logsrvd.h" + +struct logsrvd_info_closure { + InfoMessage **info_msgs; + size_t infolen; +}; + +static double random_drop; + +bool +set_random_drop(const char *dropstr) +{ + char *ep; + debug_decl(set_random_drop, SUDO_DEBUG_UTIL); + + errno = 0; + random_drop = strtod(dropstr, &ep); + if (*ep != '\0' || errno != 0) + debug_return_bool(false); + random_drop /= 100.0; /* convert from percentage */ + + debug_return_bool(true); +} + +static bool +logsrvd_json_log_cb(struct json_container *jsonc, 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(jsonc, info->key, &json_value)) + goto bad; + break; + case INFO_MESSAGE__VALUE_STRVAL: + if (info->u.strval == NULL) { + sudo_warnx(U_("%s: protocol error: NULL value found in %s"), + "local", info->key); + break; + } + json_value.type = JSON_STRING; + json_value.u.string = info->u.strval; + if (!sudo_json_add_value(jsonc, info->key, &json_value)) + goto bad; + break; + case INFO_MESSAGE__VALUE_STRLISTVAL: { + InfoMessage__StringList *strlist = info->u.strlistval; + size_t n; + + if (strlist == NULL) { + sudo_warnx(U_("%s: protocol error: NULL value found in %s"), + "local", info->key); + break; + } + if (!sudo_json_open_array(jsonc, info->key)) + goto bad; + for (n = 0; n < strlist->n_strings; n++) { + if (strlist->strings[n] == NULL) { + sudo_warnx(U_("%s: protocol error: NULL value found in %s"), + "local", info->key); + break; + } + json_value.type = JSON_STRING; + json_value.u.string = strlist->strings[n]; + if (!sudo_json_add_value(jsonc, NULL, &json_value)) + goto bad; + } + if (!sudo_json_close_array(jsonc)) + goto bad; + break; + } + case INFO_MESSAGE__VALUE_NUMLISTVAL: { + InfoMessage__NumberList *numlist = info->u.numlistval; + size_t n; + + if (numlist == NULL) { + sudo_warnx(U_("%s: protocol error: NULL value found in %s"), + "local", info->key); + break; + } + if (!sudo_json_open_array(jsonc, info->key)) + goto bad; + for (n = 0; n < numlist->n_numbers; n++) { + json_value.type = JSON_NUMBER; + json_value.u.number = numlist->numbers[n]; + if (!sudo_json_add_value(jsonc, NULL, &json_value)) + goto bad; + } + if (!sudo_json_close_array(jsonc)) + goto bad; + break; + } + default: + sudo_warnx(U_("unexpected value_case %d in %s from %s"), + info->value_case, "InfoMessage", "local"); + break; + } + } + debug_return_bool(true); +bad: + debug_return_bool(false); +} + +/* + * Parse and store an AcceptMessage locally. + */ +bool +store_accept_local(AcceptMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct logsrvd_info_closure info = { msg->info_msgs, msg->n_info_msgs }; + bool new_session = closure->evlog == NULL; + struct eventlog *evlog = NULL; + char *log_id = NULL; + bool ret = false; + debug_decl(store_accept_local, SUDO_DEBUG_UTIL); + + /* Store sudo-style event and I/O logs. */ + evlog = evlog_new(msg->submit_time, msg->info_msgs, msg->n_info_msgs, + closure); + if (evlog == NULL) { + closure->errstr = _("error parsing AcceptMessage"); + goto done; + } + + /* Additional setup for the initial command in the session. */ + if (new_session) { + closure->evlog = evlog; + + /* 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"); + goto done; + } + closure->log_io = true; + log_id = closure->evlog->iolog_path; + } + } else if (closure->log_io) { + /* Sub-command from an existing session, set iolog and offset. */ + evlog->iolog_path = strdup(closure->evlog->iolog_path); + if (evlog->iolog_path == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + closure->errstr = _("unable to allocate memory"); + goto done; + } + if (closure->evlog->iolog_file != NULL) { + evlog->iolog_file = evlog->iolog_path + + (closure->evlog->iolog_file - closure->evlog->iolog_path); + } + sudo_timespecsub(&evlog->submit_time, &closure->evlog->submit_time, + &evlog->iolog_offset); + } + + if (!eventlog_accept(evlog, 0, logsrvd_json_log_cb, &info)) { + closure->errstr = _("error logging accept event"); + goto done; + } + + if (new_session && log_id != NULL) { + /* Send log ID to client for restarting connections. */ + if (!fmt_log_id_message(log_id, closure)) + goto done; + if (sudo_ev_add(closure->evbase, closure->write_ev, + logsrvd_conf_server_timeout(), false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto done; + } + } + + ret = true; + +done: + if (closure->evlog != evlog) + eventlog_free(evlog); + + debug_return_bool(ret); +} + +/* + * Parse and store a RejectMessage locally. + */ +bool +store_reject_local(RejectMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct logsrvd_info_closure info = { msg->info_msgs, msg->n_info_msgs }; + struct eventlog *evlog = NULL; + bool ret = false; + debug_decl(store_reject_local, SUDO_DEBUG_UTIL); + + evlog = evlog_new(msg->submit_time, msg->info_msgs, msg->n_info_msgs, + closure); + if (evlog == NULL) { + closure->errstr = _("error parsing RejectMessage"); + goto done; + } + + if (closure->evlog == NULL) { + /* Initial command in session. */ + closure->evlog = evlog; + } else if (closure->log_io) { + /* Sub-command from an existing session, set iolog and offset. */ + evlog->iolog_path = strdup(closure->evlog->iolog_path); + if (evlog->iolog_path == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + closure->errstr = _("unable to allocate memory"); + goto done; + } + if (closure->evlog->iolog_file != NULL) { + evlog->iolog_file = evlog->iolog_path + + (closure->evlog->iolog_file - closure->evlog->iolog_path); + } + sudo_timespecsub(&evlog->submit_time, &closure->evlog->submit_time, + &evlog->iolog_offset); + } + + if (!eventlog_reject(evlog, 0, msg->reason, logsrvd_json_log_cb, &info)) { + closure->errstr = _("error logging reject event"); + goto done; + } + + ret = true; + +done: + if (closure->evlog != evlog) + eventlog_free(evlog); + + debug_return_bool(ret); +} + +static bool +store_exit_info_json(int dfd, struct eventlog *evlog) +{ + struct json_container jsonc = { 0 }; + struct json_value json_value; + struct iovec iov[3]; + bool ret = false; + int fd = -1; + off_t pos; + debug_decl(store_exit_info_json, SUDO_DEBUG_UTIL); + + if (!sudo_json_init(&jsonc, 4, false, false, false)) + goto done; + + fd = iolog_openat(dfd, "log.json", O_RDWR); + if (fd == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to open to %s/log.json", evlog->iolog_path); + if (errno == ENOENT) { + /* Ignore missing log.json file. */ + ret = true; + } + goto done; + } + + if (sudo_timespecisset(&evlog->run_time)) { + if (!sudo_json_open_object(&jsonc, "run_time")) + goto done; + + json_value.type = JSON_NUMBER; + json_value.u.number = evlog->run_time.tv_sec; + if (!sudo_json_add_value(&jsonc, "seconds", &json_value)) + goto done; + + json_value.type = JSON_NUMBER; + json_value.u.number = evlog->run_time.tv_nsec; + if (!sudo_json_add_value(&jsonc, "nanoseconds", &json_value)) + goto done; + + if (!sudo_json_close_object(&jsonc)) + goto done; + } + + if (evlog->signal_name != NULL) { + json_value.type = JSON_STRING; + json_value.u.string = evlog->signal_name; + if (!sudo_json_add_value(&jsonc, "signal", &json_value)) + goto done; + + json_value.type = JSON_BOOL; + json_value.u.boolean = evlog->dumped_core; + if (!sudo_json_add_value(&jsonc, "dumped_core", &json_value)) + goto done; + } + + json_value.type = JSON_NUMBER; + json_value.u.number = evlog->exit_value; + if (!sudo_json_add_value(&jsonc, "exit_value", &json_value)) + goto done; + + /* Back up to overwrite the final "\n}\n" */ + pos = lseek(fd, -3, SEEK_END); + if (pos == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to rewind %s/log.json 3 bytes", evlog->iolog_path); + goto done; + } + + /* Append the exit data and close the object. */ + iov[0].iov_base = (char *)","; + iov[0].iov_len = 1; + iov[1].iov_base = sudo_json_get_buf(&jsonc); + iov[1].iov_len = sudo_json_get_len(&jsonc); + iov[2].iov_base = (char *)"\n}\n"; + iov[2].iov_len = 3; + if (writev(fd, iov, 3) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to write %s/log.json", evlog->iolog_path); + /* Back up and try to restore to original state. */ + if (lseek(fd, pos, SEEK_SET) != -1) { + ignore_result(write(fd, "\n}\n", 3)); + } + goto done; + } + + ret = true; + +done: + if (fd != -1) + close(fd); + sudo_json_free(&jsonc); + debug_return_bool(ret); +} + +bool +store_exit_local(ExitMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct eventlog *evlog = closure->evlog; + int flags = 0; + debug_decl(store_exit_local, SUDO_DEBUG_UTIL); + + if (msg->run_time != NULL) { + evlog->run_time.tv_sec = msg->run_time->tv_sec; + evlog->run_time.tv_nsec = msg->run_time->tv_nsec; + } + evlog->exit_value = msg->exit_value; + 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)" : ""); + evlog->signal_name = strdup(msg->signal); + if (evlog->signal_name == NULL) { + closure->errstr = _("unable to allocate memory"); + debug_return_bool(false); + } + evlog->dumped_core = msg->dumped_core; + } else { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "command exited with %d", msg->exit_value); + } + if (logsrvd_conf_log_exit()) { + if (!eventlog_exit(closure->evlog, flags)) { + closure->errstr = _("error logging exit event"); + debug_return_bool(false); + } + } + + if (closure->log_io) { + /* Store the run time and exit status in log.json. */ + if (!store_exit_info_json(closure->iolog_dir_fd, evlog)) { + closure->errstr = _("error logging exit event"); + debug_return_bool(false); + } + + /* Clear write bits from I/O timing file to indicate completion. */ + mode_t mode = logsrvd_conf_iolog_mode(); + CLR(mode, S_IWUSR|S_IWGRP|S_IWOTH); + if (fchmodat(closure->iolog_dir_fd, "timing", mode, 0) == -1) { + sudo_warn("chmod 0%o %s/%s", (unsigned int)mode, "timing", + logsrvd_conf_iolog_dir()); + } + } + + debug_return_bool(true); +} + +bool +store_restart_local(RestartMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct timespec target; + struct stat sb; + int iofd; + debug_decl(store_restart_local, SUDO_DEBUG_UTIL); + + target.tv_sec = msg->resume_point->tv_sec; + target.tv_nsec = msg->resume_point->tv_nsec; + + /* We must allocate closure->evlog for iolog_path. */ + closure->evlog = calloc(1, sizeof(*closure->evlog)); + if (closure->evlog == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + closure->errstr = _("unable to allocate memory"); + goto bad; + } + closure->evlog->iolog_path = strdup(msg->log_id); + if (closure->evlog->iolog_path == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + closure->errstr = _("unable to allocate memory"); + goto bad; + } + + /* We use iolog_dir_fd in calls to openat(2) */ + closure->iolog_dir_fd = + iolog_openat(AT_FDCWD, closure->evlog->iolog_path, O_RDONLY); + if (closure->iolog_dir_fd == -1) { + sudo_warn("%s", closure->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_warn("%s/timing", closure->evlog->iolog_path); + goto bad; + } + if (!ISSET(sb.st_mode, S_IWUSR)) { + sudo_warn(U_("%s: %s"), closure->evlog->iolog_path, + U_("log is already complete, cannot be restarted")); + 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, closure->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, closure->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_warn("%s/timing", closure->evlog->iolog_path); + 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); +} + +bool +store_alert_local(AlertMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct eventlog *evlog = NULL; + struct timespec alert_time; + bool ret = false; + debug_decl(store_alert_local, SUDO_DEBUG_UTIL); + + if (msg->info_msgs != NULL && msg->n_info_msgs != 0) { + evlog = evlog_new(NULL, msg->info_msgs, msg->n_info_msgs, closure); + if (evlog == NULL) { + closure->errstr = _("error parsing AlertMessage"); + goto done; + } + if (closure->evlog == NULL) + closure->evlog = evlog; + } + alert_time.tv_sec = msg->alert_time->tv_sec; + alert_time.tv_nsec = msg->alert_time->tv_nsec; + + if (!eventlog_alert(evlog, 0, &alert_time, msg->reason, NULL)) { + closure->errstr = _("error logging alert event"); + goto done; + } + + ret = true; + +done: + if (closure->evlog != evlog) + eventlog_free(evlog); + + debug_return_bool(ret); +} + +bool +store_iobuf_local(int iofd, IoBuffer *iobuf, uint8_t *buf, size_t buflen, + struct connection_closure *closure) +{ + const struct eventlog *evlog = closure->evlog; + struct ProtobufCBinaryData data = iobuf->data; + char tbuf[1024], *newbuf = NULL; + const char *errstr; + int len; + debug_decl(store_iobuf_local, SUDO_DEBUG_UTIL); + + /* Open log file as needed. */ + if (!closure->iolog_files[iofd].enabled) { + if (!iolog_create(iofd, closure)) + goto bad; + } + + /* Format timing data. */ + /* FIXME - assumes IOFD_* matches IO_EVENT_* */ + len = snprintf(tbuf, sizeof(tbuf), "%d %lld.%09d %zu\n", + iofd, (long long)iobuf->delay->tv_sec, (int)iobuf->delay->tv_nsec, + data.len); + if (len < 0 || len >= ssizeof(tbuf)) { + sudo_warnx(U_("unable to format timing buffer, length %d"), len); + goto bad; + } + + if (!logsrvd_conf_iolog_log_passwords()) { + if (!iolog_pwfilt_run(logsrvd_conf_iolog_passprompt_regex(), iofd, + (char *)data.data, data.len, &newbuf)) + goto bad; + if (newbuf != NULL) + data.data = (uint8_t *)newbuf; + } + + /* Write to specified I/O log file. */ + if (!iolog_write(&closure->iolog_files[iofd], data.data, data.len, &errstr)) { + sudo_warnx(U_("%s/%s: %s"), evlog->iolog_path, iolog_fd_to_name(iofd), + errstr); + goto bad; + } + + /* Write timing data. */ + if (!iolog_write(&closure->iolog_files[IOFD_TIMING], tbuf, + len, &errstr)) { + sudo_warnx(U_("%s/%s: %s"), evlog->iolog_path, + iolog_fd_to_name(IOFD_TIMING), errstr); + goto bad; + } + + update_elapsed_time(iobuf->delay, &closure->elapsed_time); + + /* 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) { + closure->errstr = _("randomly dropping connection"); + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "randomly dropping connection (%f < %f)", randval, random_drop); + goto bad; + } + } + + free(newbuf); + debug_return_bool(true); +bad: + free(newbuf); + if (closure->errstr == NULL) + closure->errstr = _("error writing IoBuffer"); + debug_return_bool(false); +} + +bool +store_winsize_local(ChangeWindowSize *msg, uint8_t *buf, size_t buflen, + struct connection_closure *closure) +{ + const char *errstr; + char tbuf[1024]; + int len; + debug_decl(store_winsize_local, 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_warnx(U_("unable to format timing buffer, length %d"), len); + goto bad; + } + + /* Write timing data. */ + if (!iolog_write(&closure->iolog_files[IOFD_TIMING], tbuf, + len, &errstr)) { + sudo_warnx(U_("%s/%s: %s"), closure->evlog->iolog_path, + iolog_fd_to_name(IOFD_TIMING), errstr); + goto bad; + } + + update_elapsed_time(msg->delay, &closure->elapsed_time); + + debug_return_bool(true); +bad: + if (closure->errstr == NULL) + closure->errstr = _("error writing ChangeWindowSize"); + debug_return_bool(false); +} + +bool +store_suspend_local(CommandSuspend *msg, uint8_t *buf, size_t buflen, + struct connection_closure *closure) +{ + const char *errstr; + char tbuf[1024]; + int len; + debug_decl(store_suspend_local, 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_warnx(U_("unable to format timing buffer, length %d"), len); + goto bad; + } + + /* Write timing data. */ + if (!iolog_write(&closure->iolog_files[IOFD_TIMING], tbuf, + len, &errstr)) { + sudo_warnx(U_("%s/%s: %s"), closure->evlog->iolog_path, + iolog_fd_to_name(IOFD_TIMING), errstr); + goto bad; + } + + update_elapsed_time(msg->delay, &closure->elapsed_time); + + debug_return_bool(true); +bad: + if (closure->errstr == NULL) + closure->errstr = _("error writing CommandSuspend"); + debug_return_bool(false); +} + +struct client_message_switch cms_local = { + store_accept_local, + store_reject_local, + store_exit_local, + store_restart_local, + store_alert_local, + store_iobuf_local, + store_suspend_local, + store_winsize_local +}; diff --git a/logsrvd/logsrvd_queue.c b/logsrvd/logsrvd_queue.c new file mode 100644 index 0000000..f164e82 --- /dev/null +++ b/logsrvd/logsrvd_queue.c @@ -0,0 +1,287 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <dirent.h> +#include <time.h> +#include <unistd.h> + +#include "sudo_compat.h" +#include "sudo_conf.h" +#include "sudo_debug.h" +#include "sudo_event.h" +#include "sudo_eventlog.h" +#include "sudo_fatal.h" +#include "sudo_gettext.h" +#include "sudo_iolog.h" +#include "sudo_queue.h" +#include "sudo_util.h" + +#include "logsrvd.h" + +#if defined(HAVE_STRUCT_DIRENT_D_NAMLEN) && HAVE_STRUCT_DIRENT_D_NAMLEN +# define NAMLEN(dirent) (dirent)->d_namlen +#else +# define NAMLEN(dirent) strlen((dirent)->d_name) +#endif + +static struct outgoing_journal_queue outgoing_journal_queue = + TAILQ_HEAD_INITIALIZER(outgoing_journal_queue); + +static struct sudo_event *outgoing_queue_event; + +/* + * Callback that runs when the outgoing queue retry timer fires. + * Tries to relay the first entry in the outgoing queue. + */ +static void +outgoing_queue_cb(int unused, int what, void *v) +{ + struct connection_closure *closure; + struct outgoing_journal *oj, *next; + struct sudo_event_base *evbase = v; + bool success = false; + debug_decl(outgoing_queue_cb, SUDO_DEBUG_UTIL); + + /* Must have at least one relay server. */ + if (TAILQ_EMPTY(logsrvd_conf_relay_address())) + debug_return; + + /* Process first journal. */ + TAILQ_FOREACH_SAFE(oj, &outgoing_journal_queue, entries, next) { + FILE *fp; + int fd; + + fd = open(oj->journal_path, O_RDWR); + if (fd == -1) { + if (errno == ENOENT) { + TAILQ_REMOVE(&outgoing_journal_queue, oj, entries); + free(oj->journal_path); + free(oj); + } + continue; + } + if (!sudo_lock_file(fd, SUDO_TLOCK)) { + sudo_warn(U_("unable to lock %s"), oj->journal_path); + close(fd); + continue; + } + fp = fdopen(fd, "r"); + if (fp == NULL) { + sudo_warn(U_("unable to open %s"), oj->journal_path); + close(fd); + break; + } + + /* Allocate a connection closure and fill in journal vars. */ + closure = connection_closure_alloc(fd, false, true, evbase); + if (closure == NULL) { + fclose(fp); + break; + } + closure->journal = fp; + closure->journal_path = oj->journal_path; + + /* Done with oj now, closure owns journal_path. */ + TAILQ_REMOVE(&outgoing_journal_queue, oj, entries); + free(oj); + + success = connect_relay(closure); + if (!success) { + sudo_warnx("%s", U_("unable to connect to relay")); + connection_close(closure); + } + break; + } +} + +/* + * Schedule the outgoing_queue_event, creating it as necessary. + * The event will fire after the specified timeout elapses. + */ +bool +logsrvd_queue_enable(time_t timeout, struct sudo_event_base *evbase) +{ + debug_decl(logsrvd_queue_enable, SUDO_DEBUG_UTIL); + + if (!TAILQ_EMPTY(&outgoing_journal_queue)) { + struct timespec tv = { timeout, 0 }; + + if (outgoing_queue_event == NULL) { + outgoing_queue_event = sudo_ev_alloc(-1, SUDO_EV_TIMEOUT, + outgoing_queue_cb, evbase); + if (outgoing_queue_event == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + debug_return_bool(false); + } + } + if (sudo_ev_add(evbase, outgoing_queue_event, &tv, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + debug_return_bool(false); + } + } + debug_return_bool(true); +} + +/* + * Allocate a queue item based on the connection and push it on + * the outgoing queue. + * Consumes journal_path from the closure. + */ +bool +logsrvd_queue_insert(struct connection_closure *closure) +{ + struct outgoing_journal *oj; + debug_decl(logsrvd_queue_insert, SUDO_DEBUG_UTIL); + + if (closure->journal_path == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "missing journal_path for closure %p", closure); + debug_return_bool(false); + } + + if ((oj = malloc(sizeof(*oj))) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_bool(false); + } + oj->journal_path = closure->journal_path; + closure->journal_path = NULL; + TAILQ_INSERT_TAIL(&outgoing_journal_queue, oj, entries); + + if (!logsrvd_queue_enable(logsrvd_conf_relay_retry_interval(), + closure->evbase)) + debug_return_bool(false); + + debug_return_bool(true); +} + +/* + * Scan the outgoing queue at startup and populate the + * outgoing_journal_queue. + */ +bool +logsrvd_queue_scan(struct sudo_event_base *evbase) +{ + char path[PATH_MAX]; + struct dirent *dent; + size_t prefix_len; + int dirlen; + DIR *dirp; + debug_decl(logsrvd_queue_scan, SUDO_DEBUG_UTIL); + + /* Must have at least one relay server. */ + if (TAILQ_EMPTY(logsrvd_conf_relay_address())) + debug_return_bool(true); + + dirlen = snprintf(path, sizeof(path), "%s/outgoing/%s", + logsrvd_conf_relay_dir(), RELAY_TEMPLATE); + if (dirlen >= ssizeof(path)) { + errno = ENAMETOOLONG; + sudo_warn("%s/outgoing/%s", logsrvd_conf_relay_dir(), RELAY_TEMPLATE); + debug_return_bool(false); + } + dirlen -= sizeof(RELAY_TEMPLATE) - 1; + path[dirlen] = '\0'; + + dirp = opendir(path); + if (dirp == NULL) { + sudo_warn("opendir %s", path); + debug_return_bool(false); + } + prefix_len = strcspn(RELAY_TEMPLATE, "X"); + while ((dent = readdir(dirp)) != NULL) { + struct outgoing_journal *oj; + + /* Skip anything that is not a relay temp file. */ + if (NAMLEN(dent) != sizeof(RELAY_TEMPLATE) - 1) + continue; + if (strncmp(dent->d_name, RELAY_TEMPLATE, prefix_len) != 0) + continue; + + /* Add to queue. */ + path[dirlen] = '\0'; + if (strlcat(path, dent->d_name, sizeof(path)) >= sizeof(path)) + continue; + if ((oj = malloc(sizeof(*oj))) == NULL) + goto oom; + if ((oj->journal_path = strdup(path)) == NULL) { + free(oj); + goto oom; + } + TAILQ_INSERT_TAIL(&outgoing_journal_queue, oj, entries); + } + closedir(dirp); + + /* Process the queue immediately. */ + if (!logsrvd_queue_enable(0, evbase)) + debug_return_bool(false); + + debug_return_bool(true); +oom: + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + closedir(dirp); + debug_return_bool(false); +} + +/* + * Dump outgoing queue in response to SIGUSR1. + */ +void +logsrvd_queue_dump(void) +{ + struct outgoing_journal *oj; + debug_decl(logsrvd_queue_dump, SUDO_DEBUG_UTIL); + + if (TAILQ_EMPTY(&outgoing_journal_queue)) + debug_return; + + sudo_debug_printf(SUDO_DEBUG_INFO, "outgoing journal queue:"); + TAILQ_FOREACH(oj, &outgoing_journal_queue, entries) { + sudo_debug_printf(SUDO_DEBUG_INFO, " %s", oj->journal_path); + } +} diff --git a/logsrvd/logsrvd_relay.c b/logsrvd/logsrvd_relay.c new file mode 100644 index 0000000..f9e7f30 --- /dev/null +++ b/logsrvd/logsrvd_relay.c @@ -0,0 +1,1257 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#define NEED_INET_NTOP /* to expose sudo_inet_ntop in sudo_compat.h */ + +#include "sudo_compat.h" +#include "sudo_debug.h" +#include "sudo_event.h" +#include "sudo_eventlog.h" +#include "sudo_gettext.h" +#include "sudo_iolog.h" +#include "sudo_fatal.h" +#include "sudo_queue.h" +#include "sudo_util.h" + +#include "logsrvd.h" + +static void relay_client_msg_cb(int fd, int what, void *v); +static void relay_server_msg_cb(int fd, int what, void *v); +static void connect_cb(int sock, int what, void *v); +static bool start_relay(int sock, struct connection_closure *closure); + +/* + * Free a struct relay_closure container and its contents. + */ +void +relay_closure_free(struct relay_closure *relay_closure) +{ + struct connection_buffer *buf; + debug_decl(relay_closure_free, SUDO_DEBUG_UTIL); + +#if defined(HAVE_OPENSSL) + if (relay_closure->tls_client.ssl != NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "closing down TLSÂ connection to %s", + relay_closure->relay_name.name); + if (SSL_shutdown(relay_closure->tls_client.ssl) == 0) + SSL_shutdown(relay_closure->tls_client.ssl); + SSL_free(relay_closure->tls_client.ssl); + } +#endif + if (relay_closure->relays != NULL) + address_list_delref(relay_closure->relays); + sudo_rcstr_delref(relay_closure->relay_name.name); + sudo_ev_free(relay_closure->read_ev); + sudo_ev_free(relay_closure->write_ev); + sudo_ev_free(relay_closure->connect_ev); + free(relay_closure->read_buf.data); + while ((buf = TAILQ_FIRST(&relay_closure->write_bufs)) != NULL) { + TAILQ_REMOVE(&relay_closure->write_bufs, buf, entries); + free(buf->data); + free(buf); + } + if (relay_closure->sock != -1) { + shutdown(relay_closure->sock, SHUT_RDWR); + close(relay_closure->sock); + } + free(relay_closure); + + debug_return; +} + +/* + * Allocate a relay closure. + * Note that allocation of the events is deferred until we know the socket. + */ +static struct relay_closure * +relay_closure_alloc(void) +{ + struct relay_closure *relay_closure; + debug_decl(relay_closure_alloc, SUDO_DEBUG_UTIL); + + if ((relay_closure = calloc(1, sizeof(*relay_closure))) == NULL) + debug_return_ptr(NULL); + + /* We take a reference to relays so it doesn't change while connecting. */ + relay_closure->sock = -1; + relay_closure->relays = logsrvd_conf_relay_address(); + address_list_addref(relay_closure->relays); + TAILQ_INIT(&relay_closure->write_bufs); + + relay_closure->read_buf.size = 8 * 1024; + relay_closure->read_buf.data = malloc(relay_closure->read_buf.size); + if (relay_closure->read_buf.data == NULL) + goto bad; + + debug_return_ptr(relay_closure); +bad: + relay_closure_free(relay_closure); + debug_return_ptr(NULL); +} + +/* + * Allocate a new buffer, copy buf to it and insert on the write queue. + * On success the relay write event is enabled. + * The length parameter does not include space for the message's wire size. + */ +static bool +relay_enqueue_write(uint8_t *msgbuf, size_t len, + struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + struct connection_buffer *buf; + uint32_t msg_len; + bool ret = false; + debug_decl(relay_enqueue_write, SUDO_DEBUG_UTIL); + + /* Wire message size is used for length encoding, precedes message. */ + msg_len = htonl((uint32_t)len); + + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "size + client message %zu bytes", len); + + if ((buf = get_free_buf(sizeof(msg_len) + len, closure)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate connection_buffer"); + goto done; + } + memcpy(buf->data, &msg_len, sizeof(msg_len)); + memcpy(buf->data + sizeof(msg_len), msgbuf, len); + buf->len = sizeof(msg_len) + len; + + if (sudo_ev_add(closure->evbase, relay_closure->write_ev, NULL, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto done; + } + + TAILQ_INSERT_TAIL(&relay_closure->write_bufs, buf, entries); + buf = NULL; + + ret = true; + +done: + if (buf != NULL) { + free(buf->data); + free(buf); + } + debug_return_bool(ret); +} + +/* + * 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_closure *closure, ClientMessage *msg) +{ + struct relay_closure *relay_closure = closure->relay_closure; + struct connection_buffer *buf = NULL; + 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); + + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "size + client message %zu bytes", len); + + if ((buf = get_free_buf(len, closure)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to allocate connection_buffer"); + goto done; + } + memcpy(buf->data, &msg_len, sizeof(msg_len)); + client_message__pack(msg, buf->data + sizeof(msg_len)); + buf->len = len; + TAILQ_INSERT_TAIL(&relay_closure->write_bufs, buf, entries); + + ret = true; + +done: + debug_return_bool(ret); +} + +static bool +fmt_client_hello(struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + ClientHello hello_msg = CLIENT_HELLO__INIT; + bool ret; + debug_decl(fmt_client_hello, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending ClientHello", __func__); + hello_msg.client_id = (char *)"Sudo Logsrvd " PACKAGE_VERSION; + + client_msg.u.hello_msg = &hello_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_HELLO_MSG; + ret = fmt_client_message(closure, &client_msg); + if (ret) { + if (sudo_ev_add(closure->evbase, relay_closure->read_ev, NULL, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + ret = false; + } + if (sudo_ev_add(closure->evbase, relay_closure->write_ev, NULL, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + ret = false; + } + } + + debug_return_bool(ret); +} + +#if defined(HAVE_OPENSSL) +/* Wrapper for start_relay() called via tls_connect_cb() */ +static bool +tls_client_start_fn(struct tls_client_closure *tls_client) +{ + sudo_ev_free(tls_client->tls_connect_ev); + tls_client->tls_connect_ev = NULL; + return start_relay(SSL_get_fd(tls_client->ssl), tls_client->parent_closure); +} + +/* Perform TLS connection to the relay host. */ +static bool +connect_relay_tls(struct connection_closure *closure) +{ + struct tls_client_closure *tls_client = &closure->relay_closure->tls_client; + SSL_CTX *ssl_ctx = logsrvd_relay_tls_ctx(); + debug_decl(connect_relay_tls, SUDO_DEBUG_UTIL); + + /* Populate struct tls_client_closure. */ + tls_client->parent_closure = closure; + tls_client->evbase = closure->evbase; + tls_client->tls_connect_ev = sudo_ev_alloc(closure->relay_closure->sock, + SUDO_EV_WRITE, tls_connect_cb, tls_client); + if (tls_client->tls_connect_ev == NULL) + goto bad; + tls_client->peer_name = &closure->relay_closure->relay_name; + tls_client->connect_timeout = *logsrvd_conf_relay_connect_timeout(); + tls_client->start_fn = tls_client_start_fn; + if (!tls_ctx_client_setup(ssl_ctx, closure->relay_closure->sock, tls_client)) + goto bad; + + debug_return_bool(true); +bad: + debug_return_bool(false); +} +#endif /* HAVE_OPENSSL */ + +/* + * Try to connect to the next relay host. + * Returns 0 on success, -1 on error, setting errno. + * If there is no next relay, errno is set to ENOENT. + */ +static int +connect_relay_next(struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + struct server_address *relay; + int ret, sock = -1; + char *addr; + debug_decl(connect_relay_next, SUDO_DEBUG_UTIL); + + /* Get next relay or return ENOENT none are left. */ + if (relay_closure->relay_addr != NULL) { + relay = TAILQ_NEXT(relay_closure->relay_addr, entries); + } else { + relay = TAILQ_FIRST(relay_closure->relays); + } + if (relay == NULL) { + errno = ENOENT; + goto bad; + } + relay_closure->relay_addr = relay; + + sock = socket(relay->sa_un.sa.sa_family, SOCK_STREAM, 0); + if (sock == -1) { + sudo_warn("socket"); + goto bad; + } + if (logsrvd_conf_relay_tcp_keepalive()) { + int keepalive = 1; + if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, + sizeof(keepalive)) == -1) { + sudo_warn("SO_KEEPALIVE"); + } + } + ret = fcntl(sock, F_GETFL, 0); + if (ret == -1 || fcntl(sock, F_SETFL, ret | O_NONBLOCK) == -1) { + sudo_warn("fcntl(O_NONBLOCK)"); + goto bad; + } + + ret = connect(sock, &relay->sa_un.sa, relay->sa_size); + if (ret == -1 && errno != EINPROGRESS) + goto bad; + + switch (relay->sa_un.sa.sa_family) { + case AF_INET: + addr = (char *)&relay->sa_un.sin.sin_addr; + break; +#ifdef HAVE_STRUCT_IN6_ADDR + case AF_INET6: + addr = (char *)&relay->sa_un.sin6.sin6_addr; + break; +#endif + default: + errno = EAFNOSUPPORT; + sudo_warn("connect"); + goto bad; + } + inet_ntop(relay->sa_un.sa.sa_family, addr, + relay_closure->relay_name.ipaddr, + sizeof(relay_closure->relay_name.ipaddr)); + relay_closure->relay_name.name = sudo_rcstr_addref(relay->sa_host); + + if (ret == 0) { + if (relay_closure->sock != -1) { + shutdown(relay_closure->sock, SHUT_RDWR); + close(relay_closure->sock); + } + relay_closure->sock = sock; +#if defined(HAVE_OPENSSL) + /* Relay connection succeeded, start TLS handshake. */ + if (relay_closure->relay_addr->tls) { + if (!connect_relay_tls(closure)) + goto bad; + } else +#endif + { + /* Connection succeeded without blocking. */ + if (!start_relay(sock, closure)) + goto bad; + } + } else { + /* Connection will be completed in connect_cb(). */ + relay_closure->connect_ev = sudo_ev_alloc(sock, SUDO_EV_WRITE, + connect_cb, closure); + if (relay_closure->connect_ev == NULL) + goto bad; + if (sudo_ev_add(closure->evbase, relay_closure->connect_ev, + logsrvd_conf_relay_connect_timeout(), false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto bad; + } + if (relay_closure->sock != -1) { + shutdown(relay_closure->sock, SHUT_RDWR); + close(relay_closure->sock); + } + relay_closure->sock = sock; + closure->state = CONNECTING; + } + debug_return_int(ret); + +bad: + /* Connection or system error. */ + if (sock != -1) { + shutdown(sock, SHUT_RDWR); + close(sock); + } + sudo_rcstr_delref(relay_closure->relay_name.name); + relay_closure->relay_name.name = NULL; + sudo_ev_free(relay_closure->connect_ev); + relay_closure->connect_ev = NULL; + debug_return_int(-1); +} + +static void +connect_cb(int sock, int what, void *v) +{ + struct connection_closure *closure = v; + struct relay_closure *relay_closure = closure->relay_closure; + int errnum, optval, ret; + socklen_t optlen = sizeof(optval); + debug_decl(connect_cb, SUDO_DEBUG_UTIL); + + if (what == SUDO_EV_TIMEOUT) { + errnum = ETIMEDOUT; + } else { + ret = getsockopt(sock, SOL_SOCKET, SO_ERROR, &optval, &optlen); + errnum = ret == 0 ? optval : errno; + } + if (errnum == 0) { + closure->state = INITIAL; +#if defined(HAVE_OPENSSL) + /* Relay connection succeeded, start TLS handshake. */ + if (relay_closure->relay_addr->tls) { + if (!connect_relay_tls(closure)) { + closure->errstr = _("TLS handshake with relay host failed"); + if (!schedule_error_message(closure->errstr, closure)) + connection_close(closure); + } + } else +#endif + { + /* Relay connection succeeded, start talking to the client. */ + if (!start_relay(sock, closure)) { + closure->errstr = _("unable to allocate memory"); + if (!schedule_error_message(closure->errstr, closure)) + connection_close(closure); + } + } + } else { + /* Connection failed, try next relay (if any). */ + int res; + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "unable to connect to relay %s (%s): %s", + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr, + strerror(errnum)); + while ((res = connect_relay_next(closure)) == -1) { + if (errno == ENOENT || errno == EINPROGRESS) { + /* Out of relays or connecting asynchronously. */ + break; + } + } + if (res == -1 && errno != EINPROGRESS) { + closure->errstr = _("unable to connect to relay host"); + if (!schedule_error_message(closure->errstr, closure)) + connection_close(closure); + } + } + + debug_return; +} + +/* Connect to the first available relay host. */ +bool +connect_relay(struct connection_closure *closure) +{ + struct relay_closure *relay_closure; + int res; + debug_decl(connect_relay, SUDO_DEBUG_UTIL); + + relay_closure = closure->relay_closure = relay_closure_alloc(); + if (relay_closure == NULL) + debug_return_bool(false); + + while ((res = connect_relay_next(closure)) == -1) { + if (errno == ENOENT || errno == EINPROGRESS) { + /* Out of relays or connecting asynchronously. */ + break; + } + } + + if (res == -1 && errno != EINPROGRESS) + debug_return_bool(false); + + /* Switch to relay client message handlers. */ + closure->cms = &cms_relay; + debug_return_bool(true); +} + +/* + * Respond to a ServerHello message from the relay. + * Returns true on success, false on error. + */ +static bool +handle_server_hello(ServerHello *msg, struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + debug_decl(handle_server_hello, SUDO_DEBUG_UTIL); + + if (closure->state != INITIAL) { + sudo_warnx(U_("unexpected state %d for %s"), closure->state, + relay_closure->relay_name.ipaddr); + closure->errstr = _("state machine error"); + debug_return_bool(false); + } + + /* Check that ServerHello is valid. */ + if (msg->server_id == NULL || msg->server_id[0] == '\0') { + sudo_warnx(U_("%s: invalid ServerHello, missing server_id"), + relay_closure->relay_name.ipaddr); + closure->errstr = _("invalid ServerHello"); + debug_return_bool(false); + } + + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "relay server %s (%s) ID %s", relay_closure->relay_name.name, + relay_closure->relay_name.ipaddr, msg->server_id); + + /* TODO: handle redirect */ + + debug_return_bool(true); +} + +/* + * Respond to a CommitPoint message from the relay. + * Returns true on success, false on error. + */ +static bool +handle_commit_point(TimeSpec *commit_point, struct connection_closure *closure) +{ + debug_decl(handle_commit_point, SUDO_DEBUG_UTIL); + + if (closure->state < RUNNING) { + sudo_warnx(U_("unexpected state %d for %s"), closure->state, + closure->relay_closure->relay_name.ipaddr); + closure->errstr = _("state machine error"); + debug_return_bool(false); + } + + /* Pass commit point from relay to client. */ + debug_return_bool(schedule_commit_point(commit_point, closure)); +} + +/* + * Respond to a LogId message from the relay. + * Always returns true. + */ +static bool +handle_log_id(char *id, struct connection_closure *closure) +{ + char *new_id; + bool ret = false; + int len; + debug_decl(handle_log_id, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "log ID %s from relay %s (%s)", id, + closure->relay_closure->relay_name.name, + closure->relay_closure->relay_name.ipaddr); + + /* No client connection when replaying a journaled entry. */ + if (closure->write_ev == NULL) + debug_return_bool(true); + + /* Generate a new log ID that includes the relay host. */ + len = asprintf(&new_id, "%s/%s", id, + closure->relay_closure->relay_name.name); + if (len != -1) { + if (fmt_log_id_message(id, closure)) { + if (sudo_ev_add(closure->evbase, closure->write_ev, + logsrvd_conf_relay_timeout(), false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + } else { + ret = true; + } + } + free(new_id); + } + + debug_return_bool(ret); +} + +/* + * Respond to a ServerError message from the relay. + * Always returns false. + */ +static bool +handle_server_error(char *errmsg, struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + debug_decl(handle_server_error, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "error message received from relay %s (%s): %s", + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr, + errmsg); + + /* Server will drop connection after the error message. */ + sudo_ev_del(closure->evbase, closure->relay_closure->read_ev); + sudo_ev_del(closure->evbase, closure->relay_closure->write_ev); + + if (!schedule_error_message(errmsg, closure)) + debug_return_bool(false); + + debug_return_bool(true); +} + +/* + * Respond to a ServerAbort message from the server. + * Always returns false. + */ +static bool +handle_server_abort(char *errmsg, struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + debug_decl(handle_server_abort, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "abort message received from relay %s (%s): %s", + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr, + errmsg); + + if (!schedule_error_message(errmsg, closure)) + debug_return_bool(false); + + debug_return_bool(true); +} + +/* + * Respond to a ServerMessage from the relay. + * Returns true on success, false on error. + */ +static bool +handle_server_message(uint8_t *buf, size_t len, struct connection_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(U_("unable to unpack %s size %zu"), "ServerMessage", len); + debug_return_bool(false); + } + + switch (msg->type_case) { + case SERVER_MESSAGE__TYPE_HELLO: + if ((ret = handle_server_hello(msg->u.hello, closure))) { + /* Relay server said hello, start talking to client. */ + ret = start_protocol(closure); + } + break; + case SERVER_MESSAGE__TYPE_COMMIT_POINT: + ret = handle_commit_point(msg->u.commit_point, closure); + 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); + break; + case SERVER_MESSAGE__TYPE_ABORT: + ret = handle_server_abort(msg->u.abort, closure); + break; + default: + sudo_warnx(U_("unexpected type_case value %d in %s from %s"), + msg->type_case, "ServerMessage", + closure->relay_closure->relay_name.ipaddr); + closure->errstr = _("unrecognized ServerMessage type"); + break; + } + + server_message__free_unpacked(msg, NULL); + debug_return_bool(ret); +} + +/* + * Read and unpack a ServerMessage from the relay (read callback). + */ +static void +relay_server_msg_cb(int fd, int what, void *v) +{ + struct connection_closure *closure = v; + struct relay_closure *relay_closure = closure->relay_closure; + struct connection_buffer *buf = &relay_closure->read_buf; + ssize_t nread; + uint32_t msg_len; + debug_decl(relay_server_msg_cb, SUDO_DEBUG_UTIL); + + /* For TLS we may need to read as part of SSL_write(). */ + if (relay_closure->write_instead_of_read) { + relay_closure->write_instead_of_read = false; + relay_client_msg_cb(fd, what, v); + debug_return; + } + + if (what == SUDO_EV_TIMEOUT) { + sudo_warnx(U_("timed out reading from relay %s (%s)"), + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + closure->errstr = _("timeout reading from relay"); + goto send_error; + } + +#if defined(HAVE_OPENSSL) + if (relay_closure->tls_client.ssl != NULL) { + SSL *ssl = relay_closure->tls_client.ssl; + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: ServerMessage from relay %s (%s) [TLS]", __func__, + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + nread = SSL_read(ssl, buf->data + buf->len, buf->size - buf->len); + if (nread <= 0) { + const char *errstr; + int err; + + switch (SSL_get_error(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(relay_closure->write_ev, SUDO_EV_WRITE, NULL)) { + /* Enable a temporary write event. */ + if (sudo_ev_add(closure->evbase, relay_closure->write_ev, NULL, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + closure->errstr = _("unable to allocate memory"); + goto send_error; + } + relay_closure->temporary_write_event = true; + } + /* Redirect write event to finish SSL_read() */ + relay_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 !defined(HAVE_WOLFSSL) + if (closure->state == INITIAL && + ERR_GET_REASON(err) == SSL_R_TLSV1_ALERT_INTERNAL_ERROR) { + errstr = _("relay host name does not match certificate"); + closure->errstr = errstr; + } else +#endif + { + errstr = ERR_reason_error_string(err); + closure->errstr = _("error reading from relay"); + } + sudo_warnx("%s: SSL_read: %s", + relay_closure->relay_name.ipaddr, + errstr ? errstr : strerror(errno)); + goto send_error; + case SSL_ERROR_SYSCALL: + if (nread == 0) { + /* EOF, handled below */ + sudo_warnx(U_("EOF from %s without proper TLS shutdown"), + relay_closure->relay_name.ipaddr); + break; + } + sudo_warn("%s: SSL_read", relay_closure->relay_name.ipaddr); + closure->errstr = _("error reading from relay"); + goto send_error; + default: + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("%s: SSL_read: %s", + relay_closure->relay_name.ipaddr, + errstr ? errstr : strerror(errno)); + closure->errstr = _("error reading from relay"); + goto send_error; + } + } + } else +#endif + { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: ServerMessage from relay %s (%s)", __func__, + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + nread = read(fd, buf->data + buf->len, buf->size - buf->len); + } + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: received %zd bytes from relay %s (%s)", __func__, nread, + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + switch (nread) { + case -1: + if (errno == EAGAIN || errno == EINTR) + debug_return; + sudo_warn("%s: read", relay_closure->relay_name.ipaddr); + closure->errstr = _("unable to read from relay"); + goto send_error; + case 0: + /* EOF from relay server, close the socket. */ + shutdown(relay_closure->sock, SHUT_RDWR); + close(relay_closure->sock); + relay_closure->sock = -1; + sudo_ev_del(closure->evbase, relay_closure->read_ev); + sudo_ev_del(closure->evbase, relay_closure->write_ev); + + if (closure->state != FINISHED) { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "premature EOF from %s (%s) [state %d]", + relay_closure->relay_name.name, + relay_closure->relay_name.ipaddr, closure->state); + closure->errstr = _("relay server closed connection"); + goto send_error; + } + if (closure->sock == -1) + connection_close(closure); + debug_return; + 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: %zu"), (size_t)msg_len); + closure->errstr = _("server 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))) { + closure->errstr = _("unable to allocate memory"); + goto send_error; + } + 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 send_error; + buf->off += msg_len; + } + buf->len -= buf->off; + buf->off = 0; + debug_return; + +send_error: + /* + * Try to send client an error message before closing connection. + * If we are already in an error state, just give up. + */ + if (!schedule_error_message(closure->errstr, closure)) + goto close_connection; + debug_return; + +close_connection: + connection_close(closure); + debug_return; +} + +/* + * Forward a ClientMessage to the relay (write callback). + */ +static void +relay_client_msg_cb(int fd, int what, void *v) +{ + struct connection_closure *closure = v; + struct relay_closure *relay_closure = closure->relay_closure; + struct connection_buffer *buf; + ssize_t nwritten; + debug_decl(relay_client_msg_cb, SUDO_DEBUG_UTIL); + + /* For TLS we may need to write as part of SSL_read(). */ + if (relay_closure->read_instead_of_write) { + relay_closure->read_instead_of_write = false; + /* Delete write event if it was only due to SSL_read(). */ + if (relay_closure->temporary_write_event) { + relay_closure->temporary_write_event = false; + sudo_ev_del(closure->evbase, relay_closure->write_ev); + } + relay_server_msg_cb(fd, what, v); + debug_return; + } + + if (what == SUDO_EV_TIMEOUT) { + sudo_warnx(U_("timed out writing to relay %s (%s)"), + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + closure->errstr = _("timeout writing to relay"); + goto send_error; + } + + if ((buf = TAILQ_FIRST(&relay_closure->write_bufs)) == NULL) { + sudo_warnx(U_("missing write buffer for client %s"), + relay_closure->relay_name.ipaddr); + goto close_connection; + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending %u bytes to server %s (%s)", + __func__, buf->len - buf->off, relay_closure->relay_name.name, + relay_closure->relay_name.ipaddr); + +#if defined(HAVE_OPENSSL) + if (relay_closure->tls_client.ssl != NULL) { + SSL *ssl = relay_closure->tls_client.ssl; + nwritten = SSL_write(ssl, buf->data + buf->off, buf->len - buf->off); + if (nwritten <= 0) { + const char *errstr; + + switch (SSL_get_error(ssl, nwritten)) { + case SSL_ERROR_ZERO_RETURN: + /* ssl connection shutdown cleanly */ + shutdown(relay_closure->sock, SHUT_RDWR); + close(relay_closure->sock); + relay_closure->sock = -1; + sudo_ev_del(closure->evbase, relay_closure->read_ev); + sudo_ev_del(closure->evbase, relay_closure->write_ev); + + if (closure->state != FINISHED) { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "premature EOF from %s (state %d)", + relay_closure->relay_name.ipaddr, closure->state); + closure->errstr = _("relay server closed connection"); + goto send_error; + } + debug_return; + 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() */ + relay_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("%s: SSL_write", + relay_closure->relay_name.ipaddr); + closure->errstr = _("error writing to relay"); + goto send_error; + default: + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("%s: SSL_write: %s", + relay_closure->relay_name.ipaddr, + errstr ? errstr : strerror(errno)); + closure->errstr = _("error writing to relay"); + goto send_error; + } + } + } else +#endif + { + nwritten = write(fd, buf->data + buf->off, buf->len - buf->off); + if (nwritten == -1) { + if (errno == EAGAIN || errno == EINTR) + debug_return; + sudo_warn("%s: write", relay_closure->relay_name.ipaddr); + closure->errstr = _("error writing to relay"); + goto send_error; + } + } + buf->off += nwritten; + + if (buf->off == buf->len) { + /* sent entire message, move buf to free list */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: finished sending %u bytes to server", __func__, buf->len); + buf->off = 0; + buf->len = 0; + TAILQ_REMOVE(&relay_closure->write_bufs, buf, entries); + TAILQ_INSERT_TAIL(&closure->free_bufs, buf, entries); + if (TAILQ_EMPTY(&relay_closure->write_bufs)) + sudo_ev_del(closure->evbase, relay_closure->write_ev); + } + debug_return; + +send_error: + /* + * Try to send client an error message before closing connection. + * If we are already in an error state, just give up. + */ + if (!schedule_error_message(closure->errstr, closure)) + goto close_connection; + debug_return; + +close_connection: + connection_close(closure); + debug_return; +} + +/* Begin the conversation with the relay host. */ +static bool +start_relay(int sock, struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + debug_decl(start_relay, SUDO_DEBUG_UTIL); + + /* No longer need the connect event. */ + sudo_ev_free(relay_closure->connect_ev); + relay_closure->connect_ev = NULL; + + /* Allocate relay read/write events now that we know the socket. */ + relay_closure->read_ev = sudo_ev_alloc(sock, SUDO_EV_READ|SUDO_EV_PERSIST, + relay_server_msg_cb, closure); + relay_closure->write_ev = sudo_ev_alloc(sock, SUDO_EV_WRITE|SUDO_EV_PERSIST, + relay_client_msg_cb, closure); + if (relay_closure->read_ev == NULL || relay_closure->write_ev == NULL) + debug_return_bool(false); + + /* Start communication with the relay server by saying hello. */ + debug_return_bool(fmt_client_hello(closure)); +} + +/* + * Relay an AcceptMessage from the client to the relay server. + */ +static bool +relay_accept(AcceptMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + debug_decl(relay_accept, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: relaying AcceptMessage from %s to %s (%s)", __func__, source, + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + + debug_return_bool(relay_enqueue_write(buf, len, closure)); +} + +/* + * Relay a RejectMessage from the client to the relay server. + */ +static bool +relay_reject(RejectMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + debug_decl(relay_reject, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: relaying RejectMessage from %s to %s (%s)", __func__, source, + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + + debug_return_bool(relay_enqueue_write(buf, len, closure)); +} + +/* + * Relay an ExitMessage from the client to the relay server. + */ +static bool +relay_exit(ExitMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + debug_decl(relay_exit, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: relaying ExitMessage from %s to %s (%s)", __func__, source, + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + + debug_return_bool(relay_enqueue_write(buf, len, closure)); +} + +/* + * Relay a RestartMessage from the client to the relay server. + * We must rebuild the packed message because the log_id is modified. + */ +static bool +relay_restart(RestartMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + struct sudo_event_base *evbase = closure->evbase; + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + RestartMessage restart_msg = *msg; + char *cp; + bool ret; + debug_decl(relay_restart, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: relaying RestartMessage from %s to %s (%s)", __func__, source, + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + + /* + * We prepend "relayhost/" to the log ID before relaying it to + * the client. Perform the reverse operation before passing the + * log ID to the relay host. + */ + if ((cp = strchr(restart_msg.log_id, '/')) != NULL) { + if (cp != restart_msg.log_id) + restart_msg.log_id = cp + 1; + } + + client_msg.u.restart_msg = &restart_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_RESTART_MSG; + ret = fmt_client_message(closure, &client_msg); + if (ret) { + if (sudo_ev_add(evbase, relay_closure->write_ev, NULL, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + ret = false; + } + } + + debug_return_bool(ret); +} + +/* + * Relay an AlertMessage from the client to the relay server. + */ +static bool +relay_alert(AlertMessage *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + bool ret; + debug_decl(relay_alert, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: relaying AlertMessage from %s to %s (%s)", __func__, source, + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + + ret = relay_enqueue_write(buf, len, closure); + + debug_return_bool(ret); +} + +/* + * Relay a CommandSuspend from the client to the relay server. + */ +static bool +relay_suspend(CommandSuspend *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + bool ret; + debug_decl(relay_suspend, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: relaying CommandSuspend from %s to %s (%s)", __func__, source, + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + + ret = relay_enqueue_write(buf, len, closure); + + debug_return_bool(ret); +} + +/* + * Relay a ChangeWindowSize from the client to the relay server. + */ +static bool +relay_winsize(ChangeWindowSize *msg, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + bool ret; + debug_decl(relay_winsize, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: relaying ChangeWindowSize from %s to %s (%s)", __func__, source, + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + + ret = relay_enqueue_write(buf, len, closure); + + debug_return_bool(ret); +} + +/* + * Relay an IoBuffer from the client to the relay server. + */ +static bool +relay_iobuf(int iofd, IoBuffer *iobuf, uint8_t *buf, size_t len, + struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + const char *source = closure->journal_path ? closure->journal_path : + closure->ipaddr; + bool ret; + debug_decl(relay_iobuf, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: relaying IoBuffer from %s to %s (%s)", __func__, source, + relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); + + ret = relay_enqueue_write(buf, len, closure); + + debug_return_bool(ret); +} + +/* + * Shutdown relay connection when server is exiting. + */ +bool +relay_shutdown(struct connection_closure *closure) +{ + struct relay_closure *relay_closure = closure->relay_closure; + debug_decl(relay_shutdown, SUDO_DEBUG_UTIL); + + /* Close connection unless relay events are pending. */ + if (!sudo_ev_pending(relay_closure->read_ev, SUDO_EV_READ, NULL) && + !sudo_ev_pending(relay_closure->write_ev, SUDO_EV_WRITE, NULL) && + TAILQ_EMPTY(&relay_closure->write_bufs)) { + connection_close(closure); + } + + debug_return_bool(true); +} + +struct client_message_switch cms_relay = { + relay_accept, + relay_reject, + relay_exit, + relay_restart, + relay_alert, + relay_iobuf, + relay_suspend, + relay_winsize +}; diff --git a/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.1 b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.1 new file mode 100644 index 0000000..5fd7d3f --- /dev/null +++ b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.1 @@ -0,0 +1,253 @@ +# +# sudo logsrv daemon configuration +# + +[server] +# The host name or IP address and port to listen on with an optional TLS +# flag. If no port is specified, port 30343 will be used for plaintext +# connections and port 30344 will be used to TLS connections. +# The following forms are accepted: +# listen_address = hostname(tls) +# listen_address = hostname:port(tls) +# listen_address = IPv4_address(tls) +# listen_address = IPv4_address:port(tls) +# listen_address = [IPv6_address](tls) +# listen_address = [IPv6_address]:port(tls) +# +# The (tls) suffix should be omitted for plaintext connections. +# +# Multiple listen_address settings may be specified. +# The default is to listen on all addresses. +#listen_address = *:30343 +#listen_address = *:30344(tls) + +# The file containing the ID of the running sudo_logsrvd process. +#pid_file = /var/run/sudo/sudo_logsrvd.pid + +# Where to log server warnings: none, stderr, syslog, or a path name. +#server_log = syslog + +# If true, enable the SO_KEEPALIVE socket option on client connections. +# Defaults to true. +#tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the client to +# respond. A value of 0 will disable the timeout. The default value is 30. +#timeout = 30 + +# If true, the server will validate its own certificate at startup. +# Defaults to true. +#tls_verify = true + +# If true, client certificates will be validated by the server; +# clients without a valid certificate will be unable to connect. +# By default, client certs are not checked. +#tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +#tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# Required for TLS connections. +#tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# Required for TLS connections. +#tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default cipher list is HIGH:!aNULL. +#tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default cipher list is TLS_AES_256_GCM_SHA384. +#tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# If not set, the server will use the OpenSSL defaults. +#tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[relay] +# The host name or IP address and port to send logs to in relay mode. +# The syntax is identical to listen_address with the exception of +# the wild card ('*') syntax. When this setting is enabled, logs will +# be relayed to the specified host instead of being stored locally. +# This setting is not enabled by default. +#relay_host = relayhost.dom.ain +#relay_host = relayhost.dom.ain(tls) + +# The amount of time, in seconds, the server will wait for a connection +# to the relay server to complete. A value of 0 will disable the timeout. +# The default value is 30. +#connect_timeout = 30 + +# The directory to store messages in before they are sent to the relay. +# Messages are stored in wire format. +# The default value is /var/log/sudo_logsrvd. +#relay_dir = /var/log/sudo_logsrvd + +# The number of seconds to wait after a connection error before +# making a new attempt to forward a message to a relay host. +# The default value is 30. +#retry_interval = 30 + +# Whether to store the log before relaying it. If true, enable store +# and forward mode. If false, the client connection is immediately +# relayed. Defaults to false. +#store_first = true + +# If true, enable the SO_KEEPALIVE socket option on relay connections. +# Defaults to true. +#tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the relay to +# respond. A value of 0 will disable the timeout. The default value is 30. +#timeout = 30 + +# If true, the server's relay certificate will be verified at startup. +# The default is to use the value in the [server] section. +#tls_verify = true + +# Whether to verify the relay's certificate for TLS connections. +# The default is to use the value in the [server] section. +#tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +# The default is to use the value in the [server] section. +#tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# The default is to use the certificate in the [server] section. +#tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# The default is to use the key in the [server] section. +#tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default is to use the value in the [server] section. +#tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default is to use the value in the [server] section. +#tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# The default is to use the value in the [server] section. +#tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[iolog] +# The top-level directory to use when constructing the path name for the +# I/O log directory. The session sequence number, if any, is stored here. +#iolog_dir = /var/log/sudo-io + +# The path name, relative to iolog_dir, in which to store I/O logs. +# Note that iolog_file may contain directory components. +#iolog_file = %{seq} + +# If set, I/O logs will be compressed using zlib. Enabling compression can +# make it harder to view the logs in real-time as the program is executing. +#iolog_compress = false + +# If set, I/O log data is flushed to disk after each write instead of +# buffering it. This makes it possible to view the logs in real-time +# as the program is executing but reduces the effectiveness of compression. +#iolog_flush = true + +# The group to use when creating new I/O log files and directories. +# If iolog_group is not set, the primary group-ID of the user specified +# by iolog_user is used. If neither iolog_group nor iolog_user +# are set, I/O log files and directories are created with group-ID 0. +#iolog_group = wheel + +# The user to use when setting the user-ID and group-ID of new I/O +# log files and directories. If iolog_group is set, it will be used +# instead of the user's primary group-ID. By default, I/O log files +# and directories are created with user and group-ID 0. +#iolog_user = root + +# The file mode to use when creating I/O log files. The file permissions +# will always include the owner read and write bits, even if they are +# not present in the specified mode. When creating I/O log directories, +# search (execute) bits are added to match the read and write bits +# specified by iolog_mode. +#iolog_mode = 0600 + +# If disabled, sudo_logsrvd will attempt to avoid logging plaintext +# password in the terminal input using passprompt_regex. +#log_passwords = true + +# The maximum sequence number that will be substituted for the "%{seq}" +# escape in the I/O log file. While the value substituted for "%{seq}" +# is in base 36, maxseq itself should be expressed in decimal. Values +# larger than 2176782336 (which corresponds to the base 36 sequence +# number "ZZZZZZ") will be silently truncated to 2176782336. +#maxseq = 2176782336 + +# One or more POSIX extended regular expressions used to match +# password prompts in the terminal output when log_passwords is +# disabled. Multiple passprompt_regex settings may be specified. +#passprompt_regex = [Pp]assword[: ]* +#passprompt_regex = [Pp]assword for [a-z0-9]+: * + +[eventlog] +# Where to log accept, reject, exit, and alert events. +# Accepted values are syslog, logfile, or none. +# Defaults to syslog +#log_type = syslog + +# Whether to log an event when a command exits or is terminated by a signal. +# Defaults to false +#log_exit = true + +# Event log format. +# Supported log formats are "sudo" and "json" +# Defaults to sudo +#log_format = sudo + +[syslog] +# The maximum length of a syslog payload. +# On many systems, syslog(3) has a relatively small log buffer. +# IETF RFC 5424 states that syslog servers must support messages +# of at least 480 bytes and should support messages up to 2048 bytes. +# Messages larger than this value will be split into multiple messages. +#maxlen = 960 + +# The syslog facility to use for event log messages. +# The following syslog facilities are supported: authpriv (if your OS +# supports it), auth, daemon, user, local0, local1, local2, local3, +# local4, local5, local6, and local7. +#facility = authpriv + +# Syslog priority to use for event log accept messages, when the command +# is allowed by the security policy. The following syslog priorities are +# supported: alert, crit, debug, emerg, err, info, notice, warning, none. +#accept_priority = notice + +# Syslog priority to use for event log reject messages, when the command +# is not allowed by the security policy. +#reject_priority = alert + +# Syslog priority to use for event log alert messages reported by the +# client. +#alert_priority = alert + +# The syslog facility to use for server warning messages. +# Defaults to daemon. +#server_facility = daemon + +[logfile] +# The path to the file-based event log. +# This path must be fully-qualified and start with a '/' character. +#path = /var/log/sudo + +# The format string used when formatting the date and time for +# file-based event logs. Formatting is performed via strftime(3) so +# any format string supported by that function is allowed. +#time_format = %h %e %T diff --git a/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.2 b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.2 new file mode 100644 index 0000000..a860082 --- /dev/null +++ b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.2 @@ -0,0 +1,255 @@ +# +# sudo logsrv daemon configuration +# + +[server] +# The host name or IP address and port to listen on with an optional TLS +# flag. If no port is specified, port 30343 will be used for plaintext +# connections and port 30344 will be used to TLS connections. +# The following forms are accepted: +# listen_address = hostname(tls) +# listen_address = hostname:port(tls) +# listen_address = IPv4_address(tls) +# listen_address = IPv4_address:port(tls) +# listen_address = [IPv6_address](tls) +# listen_address = [IPv6_address]:port(tls) +# +# The (tls) suffix should be omitted for plaintext connections. +# +# Multiple listen_address settings may be specified. +# The default is to listen on all addresses. +#listen_address = *:30343 +listen_address = *:30344(tls) + +# The file containing the ID of the running sudo_logsrvd process. +pid_file = /var/run/sudo/sudo_logsrvd.pid + +# Where to log server warnings: none, stderr, syslog, or a path name. +server_log = syslog + +# If true, enable the SO_KEEPALIVE socket option on client connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the client to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server will validate its own certificate at startup. +# Defaults to true. +tls_verify = true + +# If true, client certificates will be validated by the server; +# clients without a valid certificate will be unable to connect. +# By default, client certs are not checked. +tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# Required for TLS connections. +tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# Required for TLS connections. +tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default cipher list is HIGH:!aNULL. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default cipher list is TLS_AES_256_GCM_SHA384. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# If not set, the server will use the OpenSSL defaults. +tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[relay] +# The host name or IP address and port to send logs to in relay mode. +# The syntax is identical to listen_address with the exception of +# the wild card ('*') syntax. When this setting is enabled, logs will +# be relayed to the specified host instead of being stored locally. +# This setting is not enabled by default. +#relay_host = relayhost.dom.ain +#relay_host = relayhost.dom.ain(tls) +relay_host = localhost(tls) + +# The amount of time, in seconds, the server will wait for a connection +# to the relay server to complete. A value of 0 will disable the timeout. +# The default value is 30. +connect_timeout = 30 + +# The directory to store messages in before they are sent to the relay. +# Messages are stored in wire format. +# The default value is /var/log/sudo_logsrvd. +relay_dir = /var/log/sudo_logsrvd + +# The number of seconds to wait after a connection error before +# making a new attempt to forward a message to a relay host. +# The default value is 30. +retry_interval = 30 + +# Whether to store the log before relaying it. If true, enable store +# and forward mode. If false, the client connection is immediately +# relayed. Defaults to false. +#store_first = true + +# If true, enable the SO_KEEPALIVE socket option on relay connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the relay to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server's relay certificate will be verified at startup. +# The default is to use the value in the [server] section. +#tls_verify = true + +# Whether to verify the relay's certificate for TLS connections. +# The default is to use the value in the [server] section. +#tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +# The default is to use the value in the [server] section. +#tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# The default is to use the certificate in the [server] section. +#tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# The default is to use the key in the [server] section. +#tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default is to use the value in the [server] section. +#tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default is to use the value in the [server] section. +#tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# The default is to use the value in the [server] section. +#tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[iolog] +# The top-level directory to use when constructing the path name for the +# I/O log directory. The session sequence number, if any, is stored here. +iolog_dir = /var/log/sudo-io + +# The path name, relative to iolog_dir, in which to store I/O logs. +# Note that iolog_file may contain directory components. +iolog_file = %{seq} + +# If set, I/O logs will be compressed using zlib. Enabling compression can +# make it harder to view the logs in real-time as the program is executing. +iolog_compress = false + +# If set, I/O log data is flushed to disk after each write instead of +# buffering it. This makes it possible to view the logs in real-time +# as the program is executing but reduces the effectiveness of compression. +iolog_flush = true + +# The group to use when creating new I/O log files and directories. +# If iolog_group is not set, the primary group-ID of the user specified +# by iolog_user is used. If neither iolog_group nor iolog_user +# are set, I/O log files and directories are created with group-ID 0. +iolog_group = wheel + +# The user to use when setting the user-ID and group-ID of new I/O +# log files and directories. If iolog_group is set, it will be used +# instead of the user's primary group-ID. By default, I/O log files +# and directories are created with user and group-ID 0. +iolog_user = root + +# The file mode to use when creating I/O log files. The file permissions +# will always include the owner read and write bits, even if they are +# not present in the specified mode. When creating I/O log directories, +# search (execute) bits are added to match the read and write bits +# specified by iolog_mode. +iolog_mode = 0600 + +# If disabled, sudo_logsrvd will attempt to avoid logging plaintext +# password in the terminal input using passprompt_regex. +log_passwords = true + +# The maximum sequence number that will be substituted for the "%{seq}" +# escape in the I/O log file. While the value substituted for "%{seq}" +# is in base 36, maxseq itself should be expressed in decimal. Values +# larger than 2176782336 (which corresponds to the base 36 sequence +# number "ZZZZZZ") will be silently truncated to 2176782336. +maxseq = 2176782336 + +# One or more POSIX extended regular expressions used to match +# password prompts in the terminal output when log_passwords is +# disabled. Multiple passprompt_regex settings may be specified. +#passprompt_regex = [Pp]assword[: ]* +#passprompt_regex = [Pp]assword for [a-z0-9]+: * +passprompt_regex = [Pp]assword[: ]* + +[eventlog] +# Where to log accept, reject, exit, and alert events. +# Accepted values are syslog, logfile, or none. +# Defaults to syslog +log_type = syslog + +# Whether to log an event when a command exits or is terminated by a signal. +# Defaults to false +log_exit = true + +# Event log format. +# Supported log formats are "sudo" and "json" +# Defaults to sudo +log_format = sudo + +[syslog] +# The maximum length of a syslog payload. +# On many systems, syslog(3) has a relatively small log buffer. +# IETF RFC 5424 states that syslog servers must support messages +# of at least 480 bytes and should support messages up to 2048 bytes. +# Messages larger than this value will be split into multiple messages. +maxlen = 960 + +# The syslog facility to use for event log messages. +# The following syslog facilities are supported: authpriv (if your OS +# supports it), auth, daemon, user, local0, local1, local2, local3, +# local4, local5, local6, and local7. +facility = authpriv + +# Syslog priority to use for event log accept messages, when the command +# is allowed by the security policy. The following syslog priorities are +# supported: alert, crit, debug, emerg, err, info, notice, warning, none. +accept_priority = notice + +# Syslog priority to use for event log reject messages, when the command +# is not allowed by the security policy. +reject_priority = alert + +# Syslog priority to use for event log alert messages reported by the +# client. +alert_priority = alert + +# The syslog facility to use for server warning messages. +# Defaults to daemon. +server_facility = daemon + +[logfile] +# The path to the file-based event log. +# This path must be fully-qualified and start with a '/' character. +path = /var/log/sudo + +# The format string used when formatting the date and time for +# file-based event logs. Formatting is performed via strftime(3) so +# any format string supported by that function is allowed. +time_format = %h %e %T diff --git a/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.3 b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.3 new file mode 100644 index 0000000..b0fba8a --- /dev/null +++ b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.3 @@ -0,0 +1,253 @@ +# +# sudo logsrv daemon configuration +# + +[server] +# The host name or IP address and port to listen on with an optional TLS +# flag. If no port is specified, port 30343 will be used for plaintext +# connections and port 30344 will be used to TLS connections. +# The following forms are accepted: +# listen_address = hostname(tls) +# listen_address = hostname:port(tls) +# listen_address = IPv4_address(tls) +# listen_address = IPv4_address:port(tls) +# listen_address = [IPv6_address](tls) +# listen_address = [IPv6_address]:port(tls) +# +# The (tls) suffix should be omitted for plaintext connections. +# +# Multiple listen_address settings may be specified. +# The default is to listen on all addresses. +listen_address = *:30343 +#listen_address = *:30344(tls) + +# The file containing the ID of the running sudo_logsrvd process. +pid_file = /var/run/sudo/sudo_logsrvd.pid + +# Where to log server warnings: none, stderr, syslog, or a path name. +#server_log = syslog + +# If true, enable the SO_KEEPALIVE socket option on client connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the client to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 0 + +# If true, the server will validate its own certificate at startup. +# Defaults to true. +#tls_verify = true + +# If true, client certificates will be validated by the server; +# clients without a valid certificate will be unable to connect. +# By default, client certs are not checked. +#tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +#tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# Required for TLS connections. +#tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# Required for TLS connections. +#tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default cipher list is HIGH:!aNULL. +#tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default cipher list is TLS_AES_256_GCM_SHA384. +#tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# If not set, the server will use the OpenSSL defaults. +#tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[relay] +# The host name or IP address and port to send logs to in relay mode. +# The syntax is identical to listen_address with the exception of +# the wild card ('*') syntax. When this setting is enabled, logs will +# be relayed to the specified host instead of being stored locally. +# This setting is not enabled by default. +#relay_host = relayhost.dom.ain +#relay_host = relayhost.dom.ain(tls) + +# The amount of time, in seconds, the server will wait for a connection +# to the relay server to complete. A value of 0 will disable the timeout. +# The default value is 30. +#connect_timeout = 30 + +# The directory to store messages in before they are sent to the relay. +# Messages are stored in wire format. +# The default value is /var/log/sudo_logsrvd. +#relay_dir = /var/log/sudo_logsrvd + +# The number of seconds to wait after a connection error before +# making a new attempt to forward a message to a relay host. +# The default value is 30. +#retry_interval = 30 + +# Whether to store the log before relaying it. If true, enable store +# and forward mode. If false, the client connection is immediately +# relayed. Defaults to false. +#store_first = true + +# If true, enable the SO_KEEPALIVE socket option on relay connections. +# Defaults to true. +#tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the relay to +# respond. A value of 0 will disable the timeout. The default value is 30. +#timeout = 30 + +# If true, the server's relay certificate will be verified at startup. +# The default is to use the value in the [server] section. +#tls_verify = true + +# Whether to verify the relay's certificate for TLS connections. +# The default is to use the value in the [server] section. +#tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +# The default is to use the value in the [server] section. +#tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# The default is to use the certificate in the [server] section. +#tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# The default is to use the key in the [server] section. +#tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default is to use the value in the [server] section. +#tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default is to use the value in the [server] section. +#tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# The default is to use the value in the [server] section. +#tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[iolog] +# The top-level directory to use when constructing the path name for the +# I/O log directory. The session sequence number, if any, is stored here. +iolog_dir = /var/log/sudo-io/%{hostname}/%{user} + +# The path name, relative to iolog_dir, in which to store I/O logs. +# Note that iolog_file may contain directory components. +iolog_file = %{seq} + +# If set, I/O logs will be compressed using zlib. Enabling compression can +# make it harder to view the logs in real-time as the program is executing. +iolog_compress = true + +# If set, I/O log data is flushed to disk after each write instead of +# buffering it. This makes it possible to view the logs in real-time +# as the program is executing but reduces the effectiveness of compression. +iolog_flush = false + +# The group to use when creating new I/O log files and directories. +# If iolog_group is not set, the primary group-ID of the user specified +# by iolog_user is used. If neither iolog_group nor iolog_user +# are set, I/O log files and directories are created with group-ID 0. +iolog_group = sudo + +# The user to use when setting the user-ID and group-ID of new I/O +# log files and directories. If iolog_group is set, it will be used +# instead of the user's primary group-ID. By default, I/O log files +# and directories are created with user and group-ID 0. +iolog_user = sudo + +# The file mode to use when creating I/O log files. The file permissions +# will always include the owner read and write bits, even if they are +# not present in the specified mode. When creating I/O log directories, +# search (execute) bits are added to match the read and write bits +# specified by iolog_mode. +iolog_mode = 0640 + +# If disabled, sudo_logsrvd will attempt to avoid logging plaintext +# password in the terminal input using passprompt_regex. +#log_passwords = true + +# The maximum sequence number that will be substituted for the "%{seq}" +# escape in the I/O log file. While the value substituted for "%{seq}" +# is in base 36, maxseq itself should be expressed in decimal. Values +# larger than 2176782336 (which corresponds to the base 36 sequence +# number "ZZZZZZ") will be silently truncated to 2176782336. +maxseq = 999999999 + +# One or more POSIX extended regular expressions used to match +# password prompts in the terminal output when log_passwords is +# disabled. Multiple passprompt_regex settings may be specified. +#passprompt_regex = [Pp]assword[: ]* +#passprompt_regex = [Pp]assword for [a-z0-9]+: * + +[eventlog] +# Where to log accept, reject, exit, and alert events. +# Accepted values are syslog, logfile, or none. +# Defaults to syslog +log_type = logfile + +# Whether to log an event when a command exits or is terminated by a signal. +# Defaults to false +log_exit = false + +# Event log format. +# Supported log formats are "sudo" and "json" +# Defaults to sudo +log_format = json + +[syslog] +# The maximum length of a syslog payload. +# On many systems, syslog(3) has a relatively small log buffer. +# IETF RFC 5424 states that syslog servers must support messages +# of at least 480 bytes and should support messages up to 2048 bytes. +# Messages larger than this value will be split into multiple messages. +#maxlen = 960 + +# The syslog facility to use for event log messages. +# The following syslog facilities are supported: authpriv (if your OS +# supports it), auth, daemon, user, local0, local1, local2, local3, +# local4, local5, local6, and local7. +#facility = authpriv + +# Syslog priority to use for event log accept messages, when the command +# is allowed by the security policy. The following syslog priorities are +# supported: alert, crit, debug, emerg, err, info, notice, warning, none. +#accept_priority = notice + +# Syslog priority to use for event log reject messages, when the command +# is not allowed by the security policy. +#reject_priority = alert + +# Syslog priority to use for event log alert messages reported by the +# client. +#alert_priority = alert + +# The syslog facility to use for server warning messages. +# Defaults to daemon. +#server_facility = daemon + +[logfile] +# The path to the file-based event log. +# This path must be fully-qualified and start with a '/' character. +path = /var/log/sudo.log + +# The format string used when formatting the date and time for +# file-based event logs. Formatting is performed via strftime(3) so +# any format string supported by that function is allowed. +time_format = %a %b %e %H:%M:%S %Z diff --git a/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.4 b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.4 new file mode 100644 index 0000000..7d6ec4e --- /dev/null +++ b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.4 @@ -0,0 +1,255 @@ +# +# sudo logsrv daemon configuration +# + +[server] +# The host name or IP address and port to listen on with an optional TLS +# flag. If no port is specified, port 30343 will be used for plaintext +# connections and port 30344 will be used to TLS connections. +# The following forms are accepted: +# listen_address = hostname(tls) +# listen_address = hostname:port(tls) +# listen_address = IPv4_address(tls) +# listen_address = IPv4_address:port(tls) +# listen_address = [IPv6_address](tls) +# listen_address = [IPv6_address]:port(tls) +# +# The (tls) suffix should be omitted for plaintext connections. +# +# Multiple listen_address settings may be specified. +# The default is to listen on all addresses. +#listen_address = *:30343 +listen_address = *:30344(tls) + +# The file containing the ID of the running sudo_logsrvd process. +pid_file = /var/run/sudo/sudo_logsrvd.pid + +# Where to log server warnings: none, stderr, syslog, or a path name. +server_log = syslog + +# If true, enable the SO_KEEPALIVE socket option on client connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the client to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server will validate its own certificate at startup. +# Defaults to true. +tls_verify = true + +# If true, client certificates will be validated by the server; +# clients without a valid certificate will be unable to connect. +# By default, client certs are not checked. +tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# Required for TLS connections. +tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# Required for TLS connections. +tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default cipher list is HIGH:!aNULL. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default cipher list is TLS_AES_256_GCM_SHA384. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# If not set, the server will use the OpenSSL defaults. +tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[relay] +# The host name or IP address and port to send logs to in relay mode. +# The syntax is identical to listen_address with the exception of +# the wild card ('*') syntax. When this setting is enabled, logs will +# be relayed to the specified host instead of being stored locally. +# This setting is not enabled by default. +#relay_host = relayhost.dom.ain +#relay_host = relayhost.dom.ain(tls) +relay_host = localhost(tls) + +# The amount of time, in seconds, the server will wait for a connection +# to the relay server to complete. A value of 0 will disable the timeout. +# The default value is 30. +connect_timeout = 30 + +# The directory to store messages in before they are sent to the relay. +# Messages are stored in wire format. +# The default value is /var/log/sudo_logsrvd. +relay_dir = /var/log/sudo_logsrvd + +# The number of seconds to wait after a connection error before +# making a new attempt to forward a message to a relay host. +# The default value is 30. +retry_interval = 30 + +# Whether to store the log before relaying it. If true, enable store +# and forward mode. If false, the client connection is immediately +# relayed. Defaults to false. +#store_first = true + +# If true, enable the SO_KEEPALIVE socket option on relay connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the relay to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server's relay certificate will be verified at startup. +# The default is to use the value in the [server] section. +tls_verify = true + +# Whether to verify the relay's certificate for TLS connections. +# The default is to use the value in the [server] section. +tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +# The default is to use the value in the [server] section. +tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# The default is to use the certificate in the [server] section. +tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# The default is to use the key in the [server] section. +tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default is to use the value in the [server] section. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default is to use the value in the [server] section. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# The default is to use the value in the [server] section. +tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[iolog] +# The top-level directory to use when constructing the path name for the +# I/O log directory. The session sequence number, if any, is stored here. +iolog_dir = /var/log/sudo-io + +# The path name, relative to iolog_dir, in which to store I/O logs. +# Note that iolog_file may contain directory components. +iolog_file = %{seq} + +# If set, I/O logs will be compressed using zlib. Enabling compression can +# make it harder to view the logs in real-time as the program is executing. +iolog_compress = false + +# If set, I/O log data is flushed to disk after each write instead of +# buffering it. This makes it possible to view the logs in real-time +# as the program is executing but reduces the effectiveness of compression. +iolog_flush = true + +# The group to use when creating new I/O log files and directories. +# If iolog_group is not set, the primary group-ID of the user specified +# by iolog_user is used. If neither iolog_group nor iolog_user +# are set, I/O log files and directories are created with group-ID 0. +#iolog_group = wheel + +# The user to use when setting the user-ID and group-ID of new I/O +# log files and directories. If iolog_group is set, it will be used +# instead of the user's primary group-ID. By default, I/O log files +# and directories are created with user and group-ID 0. +iolog_user = root + +# The file mode to use when creating I/O log files. The file permissions +# will always include the owner read and write bits, even if they are +# not present in the specified mode. When creating I/O log directories, +# search (execute) bits are added to match the read and write bits +# specified by iolog_mode. +iolog_mode = 0600 + +# If disabled, sudo_logsrvd will attempt to avoid logging plaintext +# password in the terminal input using passprompt_regex. +log_passwords = true + +# The maximum sequence number that will be substituted for the "%{seq}" +# escape in the I/O log file. While the value substituted for "%{seq}" +# is in base 36, maxseq itself should be expressed in decimal. Values +# larger than 2176782336 (which corresponds to the base 36 sequence +# number "ZZZZZZ") will be silently truncated to 2176782336. +maxseq = 2176782336 + +# One or more POSIX extended regular expressions used to match +# password prompts in the terminal output when log_passwords is +# disabled. Multiple passprompt_regex settings may be specified. +#passprompt_regex = [Pp]assword[: ]* +#passprompt_regex = [Pp]assword for [a-z0-9]+: * +passprompt_regex = [Pp]assword[: ]* + +[eventlog] +# Where to log accept, reject, exit, and alert events. +# Accepted values are syslog, logfile, or none. +# Defaults to syslog +log_type = syslog + +# Whether to log an event when a command exits or is terminated by a signal. +# Defaults to false +log_exit = true + +# Event log format. +# Supported log formats are "sudo" and "json" +# Defaults to sudo +log_format = sudo + +[syslog] +# The maximum length of a syslog payload. +# On many systems, syslog(3) has a relatively small log buffer. +# IETF RFC 5424 states that syslog servers must support messages +# of at least 480 bytes and should support messages up to 2048 bytes. +# Messages larger than this value will be split into multiple messages. +maxlen = 960 + +# The syslog facility to use for event log messages. +# The following syslog facilities are supported: authpriv (if your OS +# supports it), auth, daemon, user, local0, local1, local2, local3, +# local4, local5, local6, and local7. +facility = unknown + +# Syslog priority to use for event log accept messages, when the command +# is allowed by the security policy. The following syslog priorities are +# supported: alert, crit, debug, emerg, err, info, notice, warning, none. +accept_priority = notice + +# Syslog priority to use for event log reject messages, when the command +# is not allowed by the security policy. +reject_priority = alert + +# Syslog priority to use for event log alert messages reported by the +# client. +alert_priority = alert + +# The syslog facility to use for server warning messages. +# Defaults to daemon. +server_facility = daemon + +[logfile] +# The path to the file-based event log. +# This path must be fully-qualified and start with a '/' character. +path = /var/log/sudo + +# The format string used when formatting the date and time for +# file-based event logs. Formatting is performed via strftime(3) so +# any format string supported by that function is allowed. +time_format = %h %e %T diff --git a/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.5 b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.5 new file mode 100644 index 0000000..5a4019f --- /dev/null +++ b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.5 @@ -0,0 +1,255 @@ +# +# sudo logsrv daemon configuration +# + +[server] +# The host name or IP address and port to listen on with an optional TLS +# flag. If no port is specified, port 30343 will be used for plaintext +# connections and port 30344 will be used to TLS connections. +# The following forms are accepted: +# listen_address = hostname(tls) +# listen_address = hostname:port(tls) +# listen_address = IPv4_address(tls) +# listen_address = IPv4_address:port(tls) +# listen_address = [IPv6_address](tls) +# listen_address = [IPv6_address]:port(tls) +# +# The (tls) suffix should be omitted for plaintext connections. +# +# Multiple listen_address settings may be specified. +# The default is to listen on all addresses. +#listen_address = *:30343 +listen_address = *:30344(tls) + +# The file containing the ID of the running sudo_logsrvd process. +pid_file = /var/run/sudo/sudo_logsrvd.pid + +# Where to log server warnings: none, stderr, syslog, or a path name. +server_log = syslog + +# If true, enable the SO_KEEPALIVE socket option on client connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the client to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server will validate its own certificate at startup. +# Defaults to true. +tls_verify = true + +# If true, client certificates will be validated by the server; +# clients without a valid certificate will be unable to connect. +# By default, client certs are not checked. +tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# Required for TLS connections. +tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# Required for TLS connections. +tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default cipher list is HIGH:!aNULL. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default cipher list is TLS_AES_256_GCM_SHA384. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# If not set, the server will use the OpenSSL defaults. +tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[relay] +# The host name or IP address and port to send logs to in relay mode. +# The syntax is identical to listen_address with the exception of +# the wild card ('*') syntax. When this setting is enabled, logs will +# be relayed to the specified host instead of being stored locally. +# This setting is not enabled by default. +#relay_host = relayhost.dom.ain +#relay_host = relayhost.dom.ain(tls) +relay_host = localhost(tls) + +# The amount of time, in seconds, the server will wait for a connection +# to the relay server to complete. A value of 0 will disable the timeout. +# The default value is 30. +connect_timeout = 30 + +# The directory to store messages in before they are sent to the relay. +# Messages are stored in wire format. +# The default value is /var/log/sudo_logsrvd. +relay_dir = /var/log/sudo_logsrvd + +# The number of seconds to wait after a connection error before +# making a new attempt to forward a message to a relay host. +# The default value is 30. +retry_interval = 30 + +# Whether to store the log before relaying it. If true, enable store +# and forward mode. If false, the client connection is immediately +# relayed. Defaults to false. +#store_first = true + +# If true, enable the SO_KEEPALIVE socket option on relay connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the relay to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server's relay certificate will be verified at startup. +# The default is to use the value in the [server] section. +tls_verify = true + +# Whether to verify the relay's certificate for TLS connections. +# The default is to use the value in the [server] section. +tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +# The default is to use the value in the [server] section. +tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# The default is to use the certificate in the [server] section. +tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# The default is to use the key in the [server] section. +tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default is to use the value in the [server] section. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default is to use the value in the [server] section. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# The default is to use the value in the [server] section. +tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[iolog] +# The top-level directory to use when constructing the path name for the +# I/O log directory. The session sequence number, if any, is stored here. +iolog_dir = /var/log/sudo-io + +# The path name, relative to iolog_dir, in which to store I/O logs. +# Note that iolog_file may contain directory components. +iolog_file = %{seq} + +# If set, I/O logs will be compressed using zlib. Enabling compression can +# make it harder to view the logs in real-time as the program is executing. +iolog_compress = false + +# If set, I/O log data is flushed to disk after each write instead of +# buffering it. This makes it possible to view the logs in real-time +# as the program is executing but reduces the effectiveness of compression. +iolog_flush = true + +# The group to use when creating new I/O log files and directories. +# If iolog_group is not set, the primary group-ID of the user specified +# by iolog_user is used. If neither iolog_group nor iolog_user +# are set, I/O log files and directories are created with group-ID 0. +#iolog_group = wheel + +# The user to use when setting the user-ID and group-ID of new I/O +# log files and directories. If iolog_group is set, it will be used +# instead of the user's primary group-ID. By default, I/O log files +# and directories are created with user and group-ID 0. +iolog_user = root + +# The file mode to use when creating I/O log files. The file permissions +# will always include the owner read and write bits, even if they are +# not present in the specified mode. When creating I/O log directories, +# search (execute) bits are added to match the read and write bits +# specified by iolog_mode. +iolog_mode = 0600 + +# If disabled, sudo_logsrvd will attempt to avoid logging plaintext +# password in the terminal input using passprompt_regex. +log_passwords = true + +# The maximum sequence number that will be substituted for the "%{seq}" +# escape in the I/O log file. While the value substituted for "%{seq}" +# is in base 36, maxseq itself should be expressed in decimal. Values +# larger than 2176782336 (which corresponds to the base 36 sequence +# number "ZZZZZZ") will be silently truncated to 2176782336. +maxseq = 2176782336 + +# One or more POSIX extended regular expressions used to match +# password prompts in the terminal output when log_passwords is +# disabled. Multiple passprompt_regex settings may be specified. +#passprompt_regex = [Pp]assword[: ]* +#passprompt_regex = [Pp]assword for [a-z0-9]+: * +passprompt_regex = [Pp]assword[: ]* + +[eventlog] +# Where to log accept, reject, exit, and alert events. +# Accepted values are syslog, logfile, or none. +# Defaults to syslog +log_type = syslog + +# Whether to log an event when a command exits or is terminated by a signal. +# Defaults to false +log_exit = true + +# Event log format. +# Supported log formats are "sudo" and "json" +# Defaults to sudo +log_format = sudo + +[syslog] +# The maximum length of a syslog payload. +# On many systems, syslog(3) has a relatively small log buffer. +# IETF RFC 5424 states that syslog servers must support messages +# of at least 480 bytes and should support messages up to 2048 bytes. +# Messages larger than this value will be split into multiple messages. +maxlen = 960 + +# The syslog facility to use for event log messages. +# The following syslog facilities are supported: authpriv (if your OS +# supports it), auth, daemon, user, local0, local1, local2, local3, +# local4, local5, local6, and local7. +facility = authpriv + +# Syslog priority to use for event log accept messages, when the command +# is allowed by the security policy. The following syslog priorities are +# supported: alert, crit, debug, emerg, err, info, notice, warning, none. +accept_priority = low + +# Syslog priority to use for event log reject messages, when the command +# is not allowed by the security policy. +reject_priority = alert + +# Syslog priority to use for event log alert messages reported by the +# client. +alert_priority = alert + +# The syslog facility to use for server warning messages. +# Defaults to daemon. +server_facility = daemon + +[logfile] +# The path to the file-based event log. +# This path must be fully-qualified and start with a '/' character. +path = /var/log/sudo + +# The format string used when formatting the date and time for +# file-based event logs. Formatting is performed via strftime(3) so +# any format string supported by that function is allowed. +time_format = %h %e %T diff --git a/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.6 b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.6 new file mode 100644 index 0000000..33f1cf2 --- /dev/null +++ b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.6 @@ -0,0 +1,255 @@ +# +# sudo logsrv daemon configuration +# + +[server] +# The host name or IP address and port to listen on with an optional TLS +# flag. If no port is specified, port 30343 will be used for plaintext +# connections and port 30344 will be used to TLS connections. +# The following forms are accepted: +# listen_address = hostname(tls) +# listen_address = hostname:port(tls) +# listen_address = IPv4_address(tls) +# listen_address = IPv4_address:port(tls) +# listen_address = [IPv6_address](tls) +# listen_address = [IPv6_address]:port(tls) +# +# The (tls) suffix should be omitted for plaintext connections. +# +# Multiple listen_address settings may be specified. +# The default is to listen on all addresses. +#listen_address = *:30343 +listen_address = *:30344(tls) + +# The file containing the ID of the running sudo_logsrvd process. +pid_file = /var/run/sudo/sudo_logsrvd.pid + +# Where to log server warnings: none, stderr, syslog, or a path name. +server_log = syslog + +# If true, enable the SO_KEEPALIVE socket option on client connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the client to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server will validate its own certificate at startup. +# Defaults to true. +tls_verify = true + +# If true, client certificates will be validated by the server; +# clients without a valid certificate will be unable to connect. +# By default, client certs are not checked. +tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# Required for TLS connections. +tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# Required for TLS connections. +tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default cipher list is HIGH:!aNULL. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default cipher list is TLS_AES_256_GCM_SHA384. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# If not set, the server will use the OpenSSL defaults. +tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[relay] +# The host name or IP address and port to send logs to in relay mode. +# The syntax is identical to listen_address with the exception of +# the wild card ('*') syntax. When this setting is enabled, logs will +# be relayed to the specified host instead of being stored locally. +# This setting is not enabled by default. +#relay_host = relayhost.dom.ain +#relay_host = relayhost.dom.ain(tls) +relay_host = localhost(tls) + +# The amount of time, in seconds, the server will wait for a connection +# to the relay server to complete. A value of 0 will disable the timeout. +# The default value is 30. +connect_timeout = 30 + +# The directory to store messages in before they are sent to the relay. +# Messages are stored in wire format. +# The default value is /var/log/sudo_logsrvd. +relay_dir = /var/log/sudo_logsrvd + +# The number of seconds to wait after a connection error before +# making a new attempt to forward a message to a relay host. +# The default value is 30. +retry_interval = 30 + +# Whether to store the log before relaying it. If true, enable store +# and forward mode. If false, the client connection is immediately +# relayed. Defaults to false. +#store_first = true + +# If true, enable the SO_KEEPALIVE socket option on relay connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the relay to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server's relay certificate will be verified at startup. +# The default is to use the value in the [server] section. +tls_verify = true + +# Whether to verify the relay's certificate for TLS connections. +# The default is to use the value in the [server] section. +tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +# The default is to use the value in the [server] section. +tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# The default is to use the certificate in the [server] section. +tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# The default is to use the key in the [server] section. +tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default is to use the value in the [server] section. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default is to use the value in the [server] section. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# The default is to use the value in the [server] section. +tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[iolog] +# The top-level directory to use when constructing the path name for the +# I/O log directory. The session sequence number, if any, is stored here. +iolog_dir = /var/log/sudo-io + +# The path name, relative to iolog_dir, in which to store I/O logs. +# Note that iolog_file may contain directory components. +iolog_file = %{seq} + +# If set, I/O logs will be compressed using zlib. Enabling compression can +# make it harder to view the logs in real-time as the program is executing. +iolog_compress = false + +# If set, I/O log data is flushed to disk after each write instead of +# buffering it. This makes it possible to view the logs in real-time +# as the program is executing but reduces the effectiveness of compression. +iolog_flush = true + +# The group to use when creating new I/O log files and directories. +# If iolog_group is not set, the primary group-ID of the user specified +# by iolog_user is used. If neither iolog_group nor iolog_user +# are set, I/O log files and directories are created with group-ID 0. +#iolog_group = wheel + +# The user to use when setting the user-ID and group-ID of new I/O +# log files and directories. If iolog_group is set, it will be used +# instead of the user's primary group-ID. By default, I/O log files +# and directories are created with user and group-ID 0. +iolog_user = root + +# The file mode to use when creating I/O log files. The file permissions +# will always include the owner read and write bits, even if they are +# not present in the specified mode. When creating I/O log directories, +# search (execute) bits are added to match the read and write bits +# specified by iolog_mode. +iolog_mode = 0600 + +# If disabled, sudo_logsrvd will attempt to avoid logging plaintext +# password in the terminal input using passprompt_regex. +log_passwords = true + +# The maximum sequence number that will be substituted for the "%{seq}" +# escape in the I/O log file. While the value substituted for "%{seq}" +# is in base 36, maxseq itself should be expressed in decimal. Values +# larger than 2176782336 (which corresponds to the base 36 sequence +# number "ZZZZZZ") will be silently truncated to 2176782336. +maxseq = 2176782336 + +# One or more POSIX extended regular expressions used to match +# password prompts in the terminal output when log_passwords is +# disabled. Multiple passprompt_regex settings may be specified. +#passprompt_regex = [Pp]assword[: ]* +#passprompt_regex = [Pp]assword for [a-z0-9]+: * +passprompt_regex = [Pp]assword[: ]* + +[eventlog] +# Where to log accept, reject, exit, and alert events. +# Accepted values are syslog, logfile, or none. +# Defaults to syslog +log_type = syslog + +# Whether to log an event when a command exits or is terminated by a signal. +# Defaults to false +log_exit = true + +# Event log format. +# Supported log formats are "sudo" and "json" +# Defaults to sudo +log_format = sudo + +[syslog] +# The maximum length of a syslog payload. +# On many systems, syslog(3) has a relatively small log buffer. +# IETF RFC 5424 states that syslog servers must support messages +# of at least 480 bytes and should support messages up to 2048 bytes. +# Messages larger than this value will be split into multiple messages. +maxlen = 960 + +# The syslog facility to use for event log messages. +# The following syslog facilities are supported: authpriv (if your OS +# supports it), auth, daemon, user, local0, local1, local2, local3, +# local4, local5, local6, and local7. +facility = authpriv + +# Syslog priority to use for event log accept messages, when the command +# is allowed by the security policy. The following syslog priorities are +# supported: alert, crit, debug, emerg, err, info, notice, warning, none. +accept_priority = notice + +# Syslog priority to use for event log reject messages, when the command +# is not allowed by the security policy. +reject_priority = alert + +# Syslog priority to use for event log alert messages reported by the +# client. +alert_priority = high + +# The syslog facility to use for server warning messages. +# Defaults to daemon. +server_facility = daemon + +[logfile] +# The path to the file-based event log. +# This path must be fully-qualified and start with a '/' character. +path = /var/log/sudo + +# The format string used when formatting the date and time for +# file-based event logs. Formatting is performed via strftime(3) so +# any format string supported by that function is allowed. +time_format = %h %e %T diff --git a/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.7 b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.7 new file mode 100644 index 0000000..65a2551 --- /dev/null +++ b/logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.7 @@ -0,0 +1,255 @@ +# +# sudo logsrv daemon configuration +# + +[server] +# The host name or IP address and port to listen on with an optional TLS +# flag. If no port is specified, port 30343 will be used for plaintext +# connections and port 30344 will be used to TLS connections. +# The following forms are accepted: +# listen_address = hostname(tls) +# listen_address = hostname:port(tls) +# listen_address = IPv4_address(tls) +# listen_address = IPv4_address:port(tls) +# listen_address = [IPv6_address](tls) +# listen_address = [IPv6_address]:port(tls) +# +# The (tls) suffix should be omitted for plaintext connections. +# +# Multiple listen_address settings may be specified. +# The default is to listen on all addresses. +#listen_address = *:30343 +listen_address = *:30344(tls) + +# The file containing the ID of the running sudo_logsrvd process. +pid_file = /var/run/sudo/sudo_logsrvd.pid + +# Where to log server warnings: none, stderr, syslog, or a path name. +server_log = syslog + +# If true, enable the SO_KEEPALIVE socket option on client connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the client to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server will validate its own certificate at startup. +# Defaults to true. +tls_verify = true + +# If true, client certificates will be validated by the server; +# clients without a valid certificate will be unable to connect. +# By default, client certs are not checked. +tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# Required for TLS connections. +tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# Required for TLS connections. +tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default cipher list is HIGH:!aNULL. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default cipher list is TLS_AES_256_GCM_SHA384. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# If not set, the server will use the OpenSSL defaults. +tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[relay] +# The host name or IP address and port to send logs to in relay mode. +# The syntax is identical to listen_address with the exception of +# the wild card ('*') syntax. When this setting is enabled, logs will +# be relayed to the specified host instead of being stored locally. +# This setting is not enabled by default. +#relay_host = relayhost.dom.ain +#relay_host = relayhost.dom.ain(tls) +relay_host = localhost(tls) + +# The amount of time, in seconds, the server will wait for a connection +# to the relay server to complete. A value of 0 will disable the timeout. +# The default value is 30. +connect_timeout = 30 + +# The directory to store messages in before they are sent to the relay. +# Messages are stored in wire format. +# The default value is /var/log/sudo_logsrvd. +relay_dir = /var/log/sudo_logsrvd + +# The number of seconds to wait after a connection error before +# making a new attempt to forward a message to a relay host. +# The default value is 30. +retry_interval = 30 + +# Whether to store the log before relaying it. If true, enable store +# and forward mode. If false, the client connection is immediately +# relayed. Defaults to false. +#store_first = true + +# If true, enable the SO_KEEPALIVE socket option on relay connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the relay to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server's relay certificate will be verified at startup. +# The default is to use the value in the [server] section. +#tls_verify = true + +# Whether to verify the relay's certificate for TLS connections. +# The default is to use the value in the [server] section. +#tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +# The default is to use the value in the [server] section. +#tls_cacert = /etc/ssl/sudo/cacert.pem + +# Path to the server's certificate file in PEM format. +# The default is to use the certificate in the [server] section. +#tls_cert = /etc/ssl/sudo/certs/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# The default is to use the key in the [server] section. +#tls_key = /etc/ssl/sudo/private/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# NOTE that this setting is only effective if the negotiated protocol +# is TLS version 1.2. +# The default is to use the value in the [server] section. +#tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default is to use the value in the [server] section. +#tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# The default is to use the value in the [server] section. +#tls_dhparams = /etc/ssl/sudo/logsrvd_dhparams.pem + +[iolog] +# The top-level directory to use when constructing the path name for the +# I/O log directory. The session sequence number, if any, is stored here. +iolog_dir = /var/log/sudo-io + +# The path name, relative to iolog_dir, in which to store I/O logs. +# Note that iolog_file may contain directory components. +iolog_file = %{seq} + +# If set, I/O logs will be compressed using zlib. Enabling compression can +# make it harder to view the logs in real-time as the program is executing. +iolog_compress = false + +# If set, I/O log data is flushed to disk after each write instead of +# buffering it. This makes it possible to view the logs in real-time +# as the program is executing but reduces the effectiveness of compression. +iolog_flush = true + +# The group to use when creating new I/O log files and directories. +# If iolog_group is not set, the primary group-ID of the user specified +# by iolog_user is used. If neither iolog_group nor iolog_user +# are set, I/O log files and directories are created with group-ID 0. +iolog_group = wheel + +# The user to use when setting the user-ID and group-ID of new I/O +# log files and directories. If iolog_group is set, it will be used +# instead of the user's primary group-ID. By default, I/O log files +# and directories are created with user and group-ID 0. +iolog_user = root + +# The file mode to use when creating I/O log files. The file permissions +# will always include the owner read and write bits, even if they are +# not present in the specified mode. When creating I/O log directories, +# search (execute) bits are added to match the read and write bits +# specified by iolog_mode. +iolog_mode = 0600 + +# If disabled, sudo_logsrvd will attempt to avoid logging plaintext +# password in the terminal input using passprompt_regex. +log_passwords = false + +# The maximum sequence number that will be substituted for the "%{seq}" +# escape in the I/O log file. While the value substituted for "%{seq}" +# is in base 36, maxseq itself should be expressed in decimal. Values +# larger than 2176782336 (which corresponds to the base 36 sequence +# number "ZZZZZZ") will be silently truncated to 2176782336. +maxseq = 2176782336 + +# One or more POSIX extended regular expressions used to match +# password prompts in the terminal output when log_passwords is +# disabled. Multiple passprompt_regex settings may be specified. +#passprompt_regex = [Pp]assword[: ]* +#passprompt_regex = [Pp]assword for [a-z0-9]+: * +passprompt_regex = [Pp]assword[: ]\+++++++++++ + +[eventlog] +# Where to log accept, reject, exit, and alert events. +# Accepted values are syslog, logfile, or none. +# Defaults to syslog +log_type = syslog + +# Whether to log an event when a command exits or is terminated by a signal. +# Defaults to false +log_exit = true + +# Event log format. +# Supported log formats are "sudo" and "json" +# Defaults to sudo +log_format = sudo + +[syslog] +# The maximum length of a syslog payload. +# On many systems, syslog(3) has a relatively small log buffer. +# IETF RFC 5424 states that syslog servers must support messages +# of at least 480 bytes and should support messages up to 2048 bytes. +# Messages larger than this value will be split into multiple messages. +maxlen = 960 + +# The syslog facility to use for event log messages. +# The following syslog facilities are supported: authpriv (if your OS +# supports it), auth, daemon, user, local0, local1, local2, local3, +# local4, local5, local6, and local7. +facility = authpriv + +# Syslog priority to use for event log accept messages, when the command +# is allowed by the security policy. The following syslog priorities are +# supported: alert, crit, debug, emerg, err, info, notice, warning, none. +accept_priority = notice + +# Syslog priority to use for event log reject messages, when the command +# is not allowed by the security policy. +reject_priority = alert + +# Syslog priority to use for event log alert messages reported by the +# client. +alert_priority = alert + +# The syslog facility to use for server warning messages. +# Defaults to daemon. +server_facility = daemon + +[logfile] +# The path to the file-based event log. +# This path must be fully-qualified and start with a '/' character. +path = /var/log/sudo + +# The format string used when formatting the date and time for +# file-based event logs. Formatting is performed via strftime(3) so +# any format string supported by that function is allowed. +time_format = %h %e %T diff --git a/logsrvd/regress/fuzz/fuzz_logsrvd_conf.c b/logsrvd/regress/fuzz/fuzz_logsrvd_conf.c new file mode 100644 index 0000000..39cd663 --- /dev/null +++ b/logsrvd/regress/fuzz/fuzz_logsrvd_conf.c @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2021-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <config.h> + +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <fcntl.h> +#include <regex.h> +#include <time.h> +#include <unistd.h> +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif + +#include "sudo_compat.h" +#include "sudo_conf.h" +#include "sudo_debug.h" +#include "sudo_eventlog.h" +#include "sudo_fatal.h" +#include "sudo_iolog.h" +#include "sudo_plugin.h" +#include "sudo_util.h" + +#include "logsrvd.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); + +/* + * Stub version that always succeeds for small inputs and fails for large. + * We want to fuzz our parser, not libc's regular expression code. + */ +bool +sudo_regex_compile_v1(void *v, const char *pattern, const char **errstr) +{ + regex_t *preg = v; + + if (strlen(pattern) > 32) { + *errstr = "invalid regular expression"; + return false; + } + + /* hopefully avoid regfree() crashes */ + memset(preg, 0, sizeof(*preg)); + return true; +} + +static int +fuzz_conversation(int num_msgs, const struct sudo_conv_message msgs[], + struct sudo_conv_reply replies[], struct sudo_conv_callback *callback) +{ + int n; + + for (n = 0; n < num_msgs; n++) { + const struct sudo_conv_message *msg = &msgs[n]; + + switch (msg->msg_type & 0xff) { + case SUDO_CONV_PROMPT_ECHO_ON: + case SUDO_CONV_PROMPT_MASK: + case SUDO_CONV_PROMPT_ECHO_OFF: + /* input not supported */ + return -1; + case SUDO_CONV_ERROR_MSG: + case SUDO_CONV_INFO_MSG: + /* no output for fuzzers */ + break; + default: + return -1; + } + } + return 0; +} + +int +LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + char tempfile[] = "/tmp/logsrvd_conf.XXXXXX"; + size_t nwritten; + int fd; + + initprogname("fuzz_logsrvd_conf"); + if (getenv("SUDO_FUZZ_VERBOSE") == NULL) + sudo_warn_set_conversation(fuzz_conversation); + + /* logsrvd_conf_read() uses a conf file path, not an open file. */ + fd = mkstemp(tempfile); + if (fd == -1) + return 0; + nwritten = write(fd, data, size); + if (nwritten != size) { + close(fd); + return 0; + } + close(fd); + + if (logsrvd_conf_read(tempfile)) { + /* public config getters */ + logsrvd_conf_iolog_dir(); + logsrvd_conf_iolog_file(); + logsrvd_conf_iolog_mode(); + logsrvd_conf_pid_file(); + logsrvd_conf_relay_address(); + logsrvd_conf_relay_connect_timeout(); + logsrvd_conf_relay_tcp_keepalive(); + logsrvd_conf_relay_timeout(); + logsrvd_conf_server_listen_address(); + logsrvd_conf_server_tcp_keepalive(); + logsrvd_conf_server_timeout(); + + /* free config */ + logsrvd_conf_cleanup(); + } + + unlink(tempfile); + + fflush(stdout); + + return 0; +} diff --git a/logsrvd/regress/fuzz/fuzz_logsrvd_conf.dict b/logsrvd/regress/fuzz/fuzz_logsrvd_conf.dict new file mode 100644 index 0000000..d9f5d10 --- /dev/null +++ b/logsrvd/regress/fuzz/fuzz_logsrvd_conf.dict @@ -0,0 +1,44 @@ +"[server]" +"listen_address" +"pid_file" +"tcp_keepalive" +"timeout" +"tls_verify" +"tls_checkpeer" +"tls_cacert" +"tls_cert" +"tls_key" +"tls_ciphers_v12" +"tls_ciphers_v13" +"tls_dhparams" + +"[relay]" +"relay_host" +"connect_timeout" + +"[iolog]" +"iolog_dir" +"iolog_file" +"iolog_compress" +"iolog_flush" +"iolog_group" +"iolog_user" +"iolog_mode" +"log_passwords" +"maxseq" +"passprompt_regex" + +"[eventlog]" +"log_type" +"log_format" + +"[syslog]" +"maxlen" +"facility" +"accept_priority" +"reject_priority" +"alert_priority" + +"[logfile]" +"path" +"time_format" diff --git a/logsrvd/regress/logsrvd_conf/cacert.pem b/logsrvd/regress/logsrvd_conf/cacert.pem new file mode 100644 index 0000000..f74402d --- /dev/null +++ b/logsrvd/regress/logsrvd_conf/cacert.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF/DCCA+SgAwIBAgIUOEgkFv51VLpqhnSlwmvTCjeq81kwDQYJKoZIhvcNAQEL +BQAwgYUxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhDb2xvcmFkbzEQMA4GA1UEBwwH +Qm91bGRlcjEVMBMGA1UECgwMU3VkbyBQcm9qZWN0MSMwIQYDVQQLDBpTdWRvIENl +cnRpZmljYXRlIEF1dGhvcml0eTEVMBMGA1UEAwwMU3VkbyBSb290IENBMCAXDTIy +MDYwMjE2NDQxMVoYDzIxMjIwNTA5MTY0NDExWjCBhTELMAkGA1UEBhMCVVMxETAP +BgNVBAgMCENvbG9yYWRvMRAwDgYDVQQHDAdCb3VsZGVyMRUwEwYDVQQKDAxTdWRv +IFByb2plY3QxIzAhBgNVBAsMGlN1ZG8gQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRUw +EwYDVQQDDAxTdWRvIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQC8DASzPZlyg51mWLAJYPZDgHZL1gXQ9Nb+iYYfePz+9LPJ/ObPrDIWNC6j +wDIPPOlB6+UzVKDR7JtqVo4kzea1C+cZilCo5nX3uIvzsn484vtmUMTPCIBZ3mYq +zz4jIgkHxZwZlEhGRhf12sY+XeVwOvm73/iaODvodqjoQlvwwdZokov5HN1N8rCT ++uMH1TOPgz8pE365FjDmfZzizoslbxmoQLLmewFYsKQBpXMMurYkoXlSezoe+LWI +lm9HEZjo6/YtMjypA1S02CXmB9Y2wSMOCzLfrLsqJe8x3yZ/clOdAIGFmPBNbAE5 +mpLT3tCf1n1xBRXQoQ33fNcWgyR3hPdI5EcKmhR5RS5fO7KOaBdOInmqvIOlr+yT +jQVIk0jyEpW8Hf5vJypCsItgHtG2dz7XCoDVXKF7b270N+gSMhIa9XZLFiSsIgoM +uJfDe+URHL0+UMohcwkRknTnU1DR+uGZi29oIe8eYdvraV9XpOTySVa4HM9ZG8Yv +24EjzCJYGvSL1VFIK+q1NLt8uxXDPUAW4J8R9Teka5Hhkv6+iXGpYUqgy+jPW1yo +shLBKn+/T+CkjStAyRwezm9pCUdJOMGRuQIEOAYBJwbzY+Qwe8va//r+K44ORG6N +6Rq9QDApOxCn9lGuWiQM/jhOyN5vuMMPMKct8KatFw7d1hx5fwIDAQABo2AwXjAd +BgNVHQ4EFgQU/nKxgas9kLS2L7jmi1A+lowSMiAwHwYDVR0jBBgwFoAU/nKxgas9 +kLS2L7jmi1A+lowSMiAwDwYDVR0TAQH/BAUwAwEB/zALBgNVHQ8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggIBAIaABzzebiw6xi9PFhBlfK+KYVJqTdwgLqOzLsL4qgPc +J1XK+aQSC/WDleBVzMXeZdwKoRaU3Wcy+By6HWV52gjqOyhBlI2VgSDLGOYbXucM +eCTlrlRoap6ut1PVMuuVoSjQi0DMbhkz3ZGx/a0STefGADu0R9JTaJJN36JfTjSH +RBRDnhsEgZU5FVmTZqkZgATjRd7NwgmGAt17FvBuwBSAkt3NZmJTt7TzsCvtVBK2 +lkT4H+8m59lAp8Rk8RthRcAPQtMKsuvORBtwhbpLHbo9ilMRMc5rNc8IY9pzcQ0N +sMzyk8SIRov/PBnC1SPK+/jRhzLA/1gyzg5dt2jQIE5GhNqDQxi+f5HTMKklO8C9 +KHSeu9DZ32pBaNZPPvECkSZoTIsKroVvzuL/4drg4qxQFT/az4Z/rwnfVK8MYhDP +jKK19diEt36cQiDEr7WRCdhy8QmI49EBqE57LjOju5cuBXJnBFI05gbC4bQCzqZm +G2fHeHDX+QeBSfgzOP1aerd8mLiRymoJuBYDY50UzkGgg0gPoSVQKqE5YnYxP/Sz +HYoLv7N6COWqbtY9nmJTHwGGWoH40bIqSY8mGe34AZ7a/zVtvlcAgThlOH82dnPJ +vfUOIbVoOOliY2O7J0TZJGQVgsH5qNd4rdyKoL7kl59sU+wl/5UVME8pA+B/LFNF +-----END CERTIFICATE----- diff --git a/logsrvd/regress/logsrvd_conf/logsrvd_cert.pem b/logsrvd/regress/logsrvd_conf/logsrvd_cert.pem new file mode 100644 index 0000000..52ef5b6 --- /dev/null +++ b/logsrvd/regress/logsrvd_conf/logsrvd_cert.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEwzCCAqugAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgYUxCzAJBgNVBAYTAlVT +MREwDwYDVQQIDAhDb2xvcmFkbzEQMA4GA1UEBwwHQm91bGRlcjEVMBMGA1UECgwM +U3VkbyBQcm9qZWN0MSMwIQYDVQQLDBpTdWRvIENlcnRpZmljYXRlIEF1dGhvcml0 +eTEVMBMGA1UEAwwMU3VkbyBSb290IENBMCAXDTIyMDYwMjE2NDUzM1oYDzIxMjIw +NTA5MTY0NTMzWjBlMQswCQYDVQQGEwJVUzERMA8GA1UECAwIQ29sb3JhZG8xFTAT +BgNVBAoMDFN1ZG8gUHJvamVjdDEYMBYGA1UECwwPU3VkbyBMb2cgU2VydmVyMRIw +EAYDVQQDDAkxMjcuMC4wLjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQC7/TKl0yMsu+65gomOkJN+LlVAqVHuONQXCC2zBpSNsP7mXaxx0uhDDxg6kope +J5f1diNX/Y5F60AlQn1a8cKOM1Cwvz7seTEZ1mCJay82Q3oyCCcSTjAa4ZDZbiag +n4e4WYqIOw5EE0DPk37UTdsqUfy90JxCUxSBMY5FQUJbc86ZadsWPb5SzsHTXfi2 +a5vyqHMm7dJ/C30cyJ8jDkChReO78DrQIZHpuj0T7otKxwQu0tkQ1bKEto7hEeOl +TblxrUZRkpumSUhFaZYt1DL6mrFinLtU9fYEFxE8f530D9mUtsZuPwYdqkantk7J +GqnpjwP0ypWFP0ckwJcn5FJHAgMBAAGjWjBYMAkGA1UdEwQCMAAwCwYDVR0PBAQD +AgXgMB0GA1UdDgQWBBTvAo8XDnHpwGZEAyUTSmiialQzSDAfBgNVHSMEGDAWgBT+ +crGBqz2QtLYvuOaLUD6WjBIyIDANBgkqhkiG9w0BAQsFAAOCAgEAHgUddk7bMYU5 +hdJiToCp0w32LQpHt8EepG4pWEzNdlnxBEb2D2f57JS3gVDoAyTAWxYipEdtCYx5 +2hMR4qrZ7G7G0D8XLj1A22nVlFUOqaUUIJRG1fFBGMM/T9CP1WLN2V2rYNoMFUdB +3aahuVKYK4TawWBhEA0cnZJeHwpg/0/B7jxYWtKF2ys8CdqBd9rgPoKZF/QfxKmz +otR7oZZuEaY9/kIkDtFaNb81JMbc/9RyBgB+5rQ8RmPcXDJ5aow5XvTWbx0LAwZZ +u1c104UxwEy062WLnpluqZ3obyJsA8G3X4kI/CffCGCjIIdnRPYQiBngKL4hvAUs +g/sD7Y1TrSWnEPJebpQwwYS4Y1HMPioDYJiGiehzZzUWWAC4itrj8mnycrTlvnev +wMh5XGHqAXd5iF+Ztw1thj2dRiVpLkyKEiPLEpTI3QL1xwnyK28fPZJyMeJ/WNJ3 +Yb51qlZw2pH8kfXoOaIINUC9ZsJujm+SBbO0JX9BK95w+23WGd8cSHRNEytsLESP +rvwljeCwQ7OqTmxT9iUBS8QZUM0ov3bF/oKpmcJe3xCAQCr2H41Fa1/CPrrco0Ar +Te0qU+Qy1ir5Qfu13qiU6Ea0d7PcOdYEe6sjHUQ4Z/o+/uQ1NJS26ahPNKOChcDe ++r8pnbtZ9uW6dQjdU6Yk6Gl6Z/vOxvU= +-----END CERTIFICATE----- diff --git a/logsrvd/regress/logsrvd_conf/logsrvd_conf_test.c b/logsrvd/regress/logsrvd_conf/logsrvd_conf_test.c new file mode 100644 index 0000000..84e66ed --- /dev/null +++ b/logsrvd/regress/logsrvd_conf/logsrvd_conf_test.c @@ -0,0 +1,89 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <config.h> + +#include <sys/socket.h> + +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "sudo_compat.h" +#include "sudo_util.h" +#include "sudo_iolog.h" +#include "sudo_queue.h" +#include "logsrvd.h" + +sudo_dso_public int main(int argc, char *argv[]); + +static void +usage(void) +{ + fprintf(stderr, "usage: %s [-v] conf_file\n", getprogname()); + exit(EXIT_FAILURE); +} + +/* + * Simple test driver for logsrvd_conf_read(). + * Just pases the file, errors to standard error. + */ +int +main(int argc, char *argv[]) +{ + bool verbose = false; + int ch, ntests, errors = 0; + + initprogname(argc > 0 ? argv[0] : "conf_test"); + + while ((ch = getopt(argc, argv, "v")) != -1) { + switch (ch) { + case 'v': + verbose = true; + break; + default: + usage(); + } + } + argc -= optind; + argv += optind; + + if (argc < 1) + usage(); + + for (ntests = 0; ntests < argc; ntests++) { + const char *path = argv[ntests]; + if (verbose) + printf("reading %s\n", path); + if (!logsrvd_conf_read(path)) + errors++; + } + logsrvd_conf_cleanup(); + + if (ntests != 0) { + printf("%s: %d tests run, %d errors, %d%% success rate\n", + getprogname(), ntests, errors, (ntests - errors) * 100 / ntests); + } + return errors; +} diff --git a/logsrvd/regress/logsrvd_conf/logsrvd_dhparams.pem b/logsrvd/regress/logsrvd_conf/logsrvd_dhparams.pem new file mode 100644 index 0000000..508846e --- /dev/null +++ b/logsrvd/regress/logsrvd_conf/logsrvd_dhparams.pem @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA/QJRAmmGCZw79LyKinHUA0fEEzDiUkhuILieN0LLruznj4RBebQi +0sEa7YrFPG7z/eLU/aoBaJmWiX3ZOGReM1NoMJgZJezkY3HBiHombb9lBJHOSaHK +rT6viG3tBiu3DiByC+hdcp9xWfXkxgC944tIiTdFJtgYWw1KUBRHnSMob+ulZ2VE +COZE8HX7Nbp26fsfOKgcb/AX0fMLOetG0aaSgYAtyOGx1toRAFhEcdq/lusdkbzy +SUWwXfMXZorZoPudn31w7IN2wvDtP7v5fGqx6e9c91Orhy96sC7jmwedK/BGnkRi +XwnI6LNXwg30g4vLuinegqcNzmqcFY0wIwIBAg== +-----END DH PARAMETERS----- diff --git a/logsrvd/regress/logsrvd_conf/logsrvd_key.pem b/logsrvd/regress/logsrvd_conf/logsrvd_key.pem new file mode 100644 index 0000000..e586169 --- /dev/null +++ b/logsrvd/regress/logsrvd_conf/logsrvd_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7/TKl0yMsu+65 +gomOkJN+LlVAqVHuONQXCC2zBpSNsP7mXaxx0uhDDxg6kopeJ5f1diNX/Y5F60Al +Qn1a8cKOM1Cwvz7seTEZ1mCJay82Q3oyCCcSTjAa4ZDZbiagn4e4WYqIOw5EE0DP +k37UTdsqUfy90JxCUxSBMY5FQUJbc86ZadsWPb5SzsHTXfi2a5vyqHMm7dJ/C30c +yJ8jDkChReO78DrQIZHpuj0T7otKxwQu0tkQ1bKEto7hEeOlTblxrUZRkpumSUhF +aZYt1DL6mrFinLtU9fYEFxE8f530D9mUtsZuPwYdqkantk7JGqnpjwP0ypWFP0ck +wJcn5FJHAgMBAAECggEAA4H+N7l3v6t/ZmyKslU2EnXLUB3KfOrPb6hc90WOmy49 +pSuuTLz7adh6CbTeTeE96/wuWYqjq+AaVvszvrg+Xj3MqhiHd9Rdwmgbp0MBakyv +ls72zXRrJycIk8mfgR5x1MRYvaGTlXWa9KgsIzw+Anftnyw3yOJf+1oNmAE7ENzf +c3IBeQF040ahQleUoF6msNvjVrcKSiOpM8x+ectrx6S1vJP9rJFRp2g5Vlroskcs +ztPF4P9MFgsAzDd3HPtzBHXzoDNAlTwBbT6Ins6CeWENJY2KjRqntfQUJZfa12cQ +XE4v4HIBm2u8MxxjW4B3dNXcy4JY5yQu0RULJnGzYQKBgQDIuT2cVmdRDa9jyBUq +XWpMOwo4jHm1qTr750dvq93z2fSSKjmee67xq1hYER+elhm9dSyRBFBwpabv5eNH +4cZuvhlv4kJiIkXqohmhU1iQg4L5sKgXY9M8+MDsOr9SyCnAb7P/VyL2WsNgCB8l +40We9feeCFQFyZi24IsusGCIGQKBgQDvwjAljmzBaTrjZlWhAEGoxCZ9yql+U7VJ +6qu9hiSqetRDTAlo/ozFNvb5o0BCkOIg23zS48Fd99B082eYpXOpECK6rVOwdWwV +aOQyoXIEFm4ihK/okELKaI9vsZjA7gAbIyzLMPPnXL3zGYk+yQbOBEuOBBa8o478 +lFhvgW55XwKBgH9wJK7CqNvsLWPTn6SDJL77aRTYE1oD9OAESfWbj9KHmeDHEEgP +zNXA7NkVHhcow3TnFQGJVK0Ab0m2kiOMM9kRtsKzS2RU0EEU4+LqMLun05tFzqLz +DSWT5aDV96zOSrvT79r47sisfYjV/zil4Aj5r1nVfcsi4GOTkqp07wTZAoGBAOFH +Wkv/nkrBYJbI0g6cmhVEcVJi+Y18g+w3NzW2dH9HOGkfafwgqg6ojbmU3k1tqzvq +YEgbvtZXgqRRDPdOBvZE9gznzaoROwSG8VxtfB9BIC0I9eyUmF1tj9EIU5p8Rtc4 +3t7xWUv8RXLFfMLkyqMLQB7p0p9fI3xKuynSuQYLAoGAPK8qQvaR91sZLAvfkEYT +jeIAwr7ExN6W+lIO9FW9ctfCu/aePTsGSmH6TX0JIZN/6lZcTamaY45IRxJh7TgD +ZHBSRxkYIGQcsH2Eb6MwouQYuFWWicf3dY+oI0wejLZ+1TsBRJSsegV/36KgWVw3 +gAbhy0D+TKSGwK1tBLgKnB8= +-----END PRIVATE KEY----- diff --git a/logsrvd/regress/logsrvd_conf/sudo_logsrvd.conf.1.in b/logsrvd/regress/logsrvd_conf/sudo_logsrvd.conf.1.in new file mode 100644 index 0000000..ab92b8b --- /dev/null +++ b/logsrvd/regress/logsrvd_conf/sudo_logsrvd.conf.1.in @@ -0,0 +1,252 @@ +# +# sudo logsrv daemon configuration +# + +[server] +# The host name or IP address and port to listen on with an optional TLS +# flag. If no port is specified, port 30343 will be used for plaintext +# connections and port 30344 will be used to TLS connections. +# The following forms are accepted: +# listen_address = hostname(tls) +# listen_address = hostname:port(tls) +# listen_address = IPv4_address(tls) +# listen_address = IPv4_address:port(tls) +# listen_address = [IPv6_address](tls) +# listen_address = [IPv6_address]:port(tls) +# +# The (tls) suffix should be omitted for plaintext connections. +# +# Multiple listen_address settings may be specified. +# The default is to listen on all addresses. +listen_address = *:30343 +#listen_address = *:30344(tls) + +# The file containing the ID of the running sudo_logsrvd process. +pid_file = /var/run/sudo/sudo_logsrvd.pid + +# Where to log server warnings: none, stderr, syslog, or a path name. +server_log = syslog + +# If true, enable the SO_KEEPALIVE socket option on client connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the client to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server will validate its own certificate at startup. +# Defaults to true. +#tls_verify = true + +# If true, client certificates will be validated by the server; +# clients without a valid certificate will be unable to connect. +# By default, client certs are not checked. +#tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +#tls_cacert = regress/logsrvd_conf/cacert.pem + +# Path to the server's certificate file in PEM format. +# Required for TLS connections. +#tls_cert = regress/logsrvd_conf/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# Required for TLS connections. +#tls_key = regress/logsrvd_conf/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# This setting is only effective if the negotiated protocol is TLS version +# 1.2. The default cipher list is HIGH:!aNULL. +#tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default cipher list is TLS_AES_256_GCM_SHA384. +#tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# If not set, the server will use the OpenSSL defaults. +#tls_dhparams = regress/logsrvd_conf/logsrvd_dhparams.pem + +[relay] +# The host name or IP address and port to send logs to in relay mode. +# The syntax is identical to listen_address with the exception of +# the wild card ('*') syntax. When this setting is enabled, logs will +# be relayed to the specified host instead of being stored locally. +# This setting is not enabled by default. +#relay_host = relayhost.dom.ain +relay_host = 127.0.0.1 + +# The amount of time, in seconds, the server will wait for a connection +# to the relay server to complete. A value of 0 will disable the timeout. +# The default value is 30. +connect_timeout = 30 + +# The directory to store messages in before they are sent to the relay. +# Messages are stored in wire format. +# The default value is /var/log/sudo_logsrvd. +relay_dir = /var/log/sudo_logsrvd + +# The number of seconds to wait after a connection error before +# making a new attempt to forward a message to a relay host. +# The default value is 30. +retry_interval = 30 + +# Whether to store the log before relaying it. If true, enable store +# and forward mode. If false, the client connection is immediately +# relayed. Defaults to false. +store_first = true + +# If true, enable the SO_KEEPALIVE socket option on relay connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the relay to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server's relay certificate will be verified at startup. +# The default is to use the value in the [server] section. +#tls_verify = true + +# Whether to verify the relay's certificate for TLS connections. +# The default is to use the value in the [server] section. +#tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +# The default is to use the value in the [server] section. +#tls_cacert = regress/logsrvd_conf/cacert.pem + +# Path to the server's certificate file in PEM format. +# The default is to use the certificate in the [server] section. +#tls_cert = regress/logsrvd_conf/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# The default is to use the key in the [server] section. +#tls_key = regress/logsrvd_conf/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# this setting is only effective if the negotiated protocol is TLS version +# 1.2. The default is to use the value in the [server] section. +#tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default is to use the value in the [server] section. +#tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# The default is to use the value in the [server] section. +#tls_dhparams = regress/logsrvd_conf/logsrvd_dhparams.pem + +[iolog] +# The top-level directory to use when constructing the path name for the +# I/O log directory. The session sequence number, if any, is stored here. +iolog_dir = /var/log/sudo-io + +# The path name, relative to iolog_dir, in which to store I/O logs. +# It is possible for iolog_file to contain directory components. +iolog_file = %{seq} + +# If set, I/O logs will be compressed using zlib. Enabling compression can +# make it harder to view the logs in real-time as the program is executing. +iolog_compress = false + +# If set, I/O log data is flushed to disk after each write instead of +# buffering it. This makes it possible to view the logs in real-time +# as the program is executing but reduces the effectiveness of compression. +iolog_flush = true + +# The group to use when creating new I/O log files and directories. +# If iolog_group is not set, the primary group-ID of the user specified +# by iolog_user is used. If neither iolog_group nor iolog_user +# are set, I/O log files and directories are created with group-ID 0. +#iolog_group = wheel + +# The user to use when setting the user-ID and group-ID of new I/O +# log files and directories. If iolog_group is set, it will be used +# instead of the user's primary group-ID. By default, I/O log files +# and directories are created with user and group-ID 0. +#iolog_user = root + +# The file mode to use when creating I/O log files. The file permissions +# will always include the owner read and write bits, even if they are +# not present in the specified mode. When creating I/O log directories, +# search (execute) bits are added to match the read and write bits +# specified by iolog_mode. +iolog_mode = 0600 + +# If disabled, sudo_logsrvd will attempt to avoid logging plaintext +# password in the terminal input using passprompt_regex. +log_passwords = true + +# The maximum sequence number that will be substituted for the "%{seq}" +# escape in the I/O log file. While the value substituted for "%{seq}" +# is in base 36, maxseq itself should be expressed in decimal. Values +# larger than 2176782336 (which corresponds to the base 36 sequence +# number "ZZZZZZ") will be silently truncated to 2176782336. +maxseq = 2176782336 + +# One or more POSIX extended regular expressions used to match +# password prompts in the terminal output when log_passwords is +# disabled. Multiple passprompt_regex settings may be specified. +#passprompt_regex = [Pp]assword[: ]* +passprompt_regex = [Pp]assword for [a-z0-9]+: * + +[eventlog] +# Where to log accept, reject, exit, and alert events. +# Accepted values are syslog, logfile, or none. +# Defaults to syslog +log_type = syslog + +# Whether to log an event when a command exits or is terminated by a signal. +# Defaults to false +log_exit = true + +# Event log format. +# Supported log formats are "sudo" and "json" +# Defaults to sudo +log_format = sudo + +[syslog] +# The maximum length of a syslog payload. +# On many systems, syslog(3) has a relatively small log buffer. +# IETF RFC 5424 states that syslog servers must support messages +# of at least 480 bytes and should support messages up to 2048 bytes. +# Messages larger than this value will be split into multiple messages. +maxlen = 960 + +# The syslog facility to use for event log messages. +# The following syslog facilities are supported: authpriv (if your OS +# supports it), auth, daemon, user, local0, local1, local2, local3, +# local4, local5, local6, and local7. +#facility = authpriv +facility = auth + +# Syslog priority to use for event log accept messages, when the command +# is allowed by the security policy. The following syslog priorities are +# supported: alert, crit, debug, emerg, err, info, notice, warning, none. +accept_priority = notice + +# Syslog priority to use for event log reject messages, when the command +# is not allowed by the security policy. +reject_priority = alert + +# Syslog priority to use for event log alert messages reported by the +# client. +alert_priority = alert + +# The syslog facility to use for server warning messages. +# Defaults to daemon. +server_facility = daemon + +[logfile] +# The path to the file-based event log. +# This path must be fully-qualified and start with a '/' character. +path = /var/log/sudo.log + +# The format string used when formatting the date and time for +# file-based event logs. Formatting is performed via strftime(3) so +# any format string supported by that function is allowed. +time_format = %h %e %T diff --git a/logsrvd/regress/logsrvd_conf/sudo_logsrvd.conf.2.in b/logsrvd/regress/logsrvd_conf/sudo_logsrvd.conf.2.in new file mode 100644 index 0000000..01b91ff --- /dev/null +++ b/logsrvd/regress/logsrvd_conf/sudo_logsrvd.conf.2.in @@ -0,0 +1,252 @@ +# +# sudo logsrv daemon configuration +# + +[server] +# The host name or IP address and port to listen on with an optional TLS +# flag. If no port is specified, port 30343 will be used for plaintext +# connections and port 30344 will be used to TLS connections. +# The following forms are accepted: +# listen_address = hostname(tls) +# listen_address = hostname:port(tls) +# listen_address = IPv4_address(tls) +# listen_address = IPv4_address:port(tls) +# listen_address = [IPv6_address](tls) +# listen_address = [IPv6_address]:port(tls) +# +# The (tls) suffix should be omitted for plaintext connections. +# +# Multiple listen_address settings may be specified. +# The default is to listen on all addresses. +listen_address = 172.0.0.1:30343 +#listen_address = 172.0.0.1:30344(tls) + +# The file containing the ID of the running sudo_logsrvd process. +pid_file = /var/run/sudo/sudo_logsrvd.pid + +# Where to log server warnings: none, stderr, syslog, or a path name. +server_log = stderr + +# If true, enable the SO_KEEPALIVE socket option on client connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the client to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server will validate its own certificate at startup. +# Defaults to true. +#tls_verify = false + +# If true, client certificates will be validated by the server; +# clients without a valid certificate will be unable to connect. +# By default, client certs are not checked. +#tls_checkpeer = true + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +#tls_cacert = regress/logsrvd_conf/cacert.pem + +# Path to the server's certificate file in PEM format. +# Required for TLS connections. +#tls_cert = regress/logsrvd_conf/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# Required for TLS connections. +#tls_key = regress/logsrvd_conf/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# This setting is only effective if the negotiated protocol is TLS version +# 1.2. The default cipher list is HIGH:!aNULL. +#tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default cipher list is TLS_AES_256_GCM_SHA384. +#tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# If not set, the server will use the OpenSSL defaults. +#tls_dhparams = regress/logsrvd_conf/logsrvd_dhparams.pem + +[relay] +# The host name or IP address and port to send logs to in relay mode. +# The syntax is identical to listen_address with the exception of +# the wild card ('*') syntax. When this setting is enabled, logs will +# be relayed to the specified host instead of being stored locally. +# This setting is not enabled by default. +#relay_host = relayhost.dom.ain +relay_host = 127.0.0.1 + +# The amount of time, in seconds, the server will wait for a connection +# to the relay server to complete. A value of 0 will disable the timeout. +# The default value is 30. +connect_timeout = 30 + +# The directory to store messages in before they are sent to the relay. +# Messages are stored in wire format. +# The default value is /var/log/sudo_logsrvd. +relay_dir = /var/log/sudo_logsrvd + +# The number of seconds to wait after a connection error before +# making a new attempt to forward a message to a relay host. +# The default value is 30. +retry_interval = 30 + +# Whether to store the log before relaying it. If true, enable store +# and forward mode. If false, the client connection is immediately +# relayed. Defaults to false. +store_first = true + +# If true, enable the SO_KEEPALIVE socket option on relay connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the relay to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server's relay certificate will be verified at startup. +# The default is to use the value in the [server] section. +#tls_verify = true + +# Whether to verify the relay's certificate for TLS connections. +# The default is to use the value in the [server] section. +#tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +# The default is to use the value in the [server] section. +#tls_cacert = regress/logsrvd_conf/cacert.pem + +# Path to the server's certificate file in PEM format. +# The default is to use the certificate in the [server] section. +#tls_cert = regress/logsrvd_conf/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# The default is to use the key in the [server] section. +#tls_key = regress/logsrvd_conf/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# this setting is only effective if the negotiated protocol is TLS version +# 1.2. The default is to use the value in the [server] section. +#tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default is to use the value in the [server] section. +#tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# The default is to use the value in the [server] section. +#tls_dhparams = regress/logsrvd_conf/logsrvd_dhparams.pem + +[iolog] +# The top-level directory to use when constructing the path name for the +# I/O log directory. The session sequence number, if any, is stored here. +iolog_dir = /var/log/sudo-io + +# The path name, relative to iolog_dir, in which to store I/O logs. +# It is possible for iolog_file to contain directory components. +iolog_file = %{seq} + +# If set, I/O logs will be compressed using zlib. Enabling compression can +# make it harder to view the logs in real-time as the program is executing. +iolog_compress = false + +# If set, I/O log data is flushed to disk after each write instead of +# buffering it. This makes it possible to view the logs in real-time +# as the program is executing but reduces the effectiveness of compression. +iolog_flush = true + +# The group to use when creating new I/O log files and directories. +# If iolog_group is not set, the primary group-ID of the user specified +# by iolog_user is used. If neither iolog_group nor iolog_user +# are set, I/O log files and directories are created with group-ID 0. +#iolog_group = wheel + +# The user to use when setting the user-ID and group-ID of new I/O +# log files and directories. If iolog_group is set, it will be used +# instead of the user's primary group-ID. By default, I/O log files +# and directories are created with user and group-ID 0. +#iolog_user = root + +# The file mode to use when creating I/O log files. The file permissions +# will always include the owner read and write bits, even if they are +# not present in the specified mode. When creating I/O log directories, +# search (execute) bits are added to match the read and write bits +# specified by iolog_mode. +iolog_mode = 0600 + +# If disabled, sudo_logsrvd will attempt to avoid logging plaintext +# password in the terminal input using passprompt_regex. +log_passwords = true + +# The maximum sequence number that will be substituted for the "%{seq}" +# escape in the I/O log file. While the value substituted for "%{seq}" +# is in base 36, maxseq itself should be expressed in decimal. Values +# larger than 2176782336 (which corresponds to the base 36 sequence +# number "ZZZZZZ") will be silently truncated to 2176782336. +maxseq = 2176782336 + +# One or more POSIX extended regular expressions used to match +# password prompts in the terminal output when log_passwords is +# disabled. Multiple passprompt_regex settings may be specified. +#passprompt_regex = [Pp]assword[: ]* +passprompt_regex = [Pp]assword for [a-z0-9]+: * + +[eventlog] +# Where to log accept, reject, exit, and alert events. +# Accepted values are syslog, logfile, or none. +# Defaults to syslog +log_type = none + +# Whether to log an event when a command exits or is terminated by a signal. +# Defaults to false +log_exit = true + +# Event log format. +# Supported log formats are "sudo" and "json" +# Defaults to sudo +log_format = json + +[syslog] +# The maximum length of a syslog payload. +# On many systems, syslog(3) has a relatively small log buffer. +# IETF RFC 5424 states that syslog servers must support messages +# of at least 480 bytes and should support messages up to 2048 bytes. +# Messages larger than this value will be split into multiple messages. +maxlen = 960 + +# The syslog facility to use for event log messages. +# The following syslog facilities are supported: authpriv (if your OS +# supports it), auth, daemon, user, local0, local1, local2, local3, +# local4, local5, local6, and local7. +#facility = authpriv +facility = daemon + +# Syslog priority to use for event log accept messages, when the command +# is allowed by the security policy. The following syslog priorities are +# supported: alert, crit, debug, emerg, err, info, notice, warning, none. +accept_priority = notice + +# Syslog priority to use for event log reject messages, when the command +# is not allowed by the security policy. +reject_priority = alert + +# Syslog priority to use for event log alert messages reported by the +# client. +alert_priority = alert + +# The syslog facility to use for server warning messages. +# Defaults to daemon. +server_facility = daemon + +[logfile] +# The path to the file-based event log. +# This path must be fully-qualified and start with a '/' character. +path = /var/log/sudo.log + +# The format string used when formatting the date and time for +# file-based event logs. Formatting is performed via strftime(3) so +# any format string supported by that function is allowed. +time_format = %h %e %T diff --git a/logsrvd/regress/logsrvd_conf/tls/sudo_logsrvd.conf.1.in b/logsrvd/regress/logsrvd_conf/tls/sudo_logsrvd.conf.1.in new file mode 100644 index 0000000..6d97f44 --- /dev/null +++ b/logsrvd/regress/logsrvd_conf/tls/sudo_logsrvd.conf.1.in @@ -0,0 +1,252 @@ +# +# sudo logsrv daemon configuration +# + +[server] +# The host name or IP address and port to listen on with an optional TLS +# flag. If no port is specified, port 30343 will be used for plaintext +# connections and port 30344 will be used to TLS connections. +# The following forms are accepted: +# listen_address = hostname(tls) +# listen_address = hostname:port(tls) +# listen_address = IPv4_address(tls) +# listen_address = IPv4_address:port(tls) +# listen_address = [IPv6_address](tls) +# listen_address = [IPv6_address]:port(tls) +# +# The (tls) suffix should be omitted for plaintext connections. +# +# Multiple listen_address settings may be specified. +# The default is to listen on all addresses. +listen_address = *:30343 +listen_address = *:30344(tls) + +# The file containing the ID of the running sudo_logsrvd process. +pid_file = /var/run/sudo/sudo_logsrvd.pid + +# Where to log server warnings: none, stderr, syslog, or a path name. +server_log = syslog + +# If true, enable the SO_KEEPALIVE socket option on client connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the client to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server will validate its own certificate at startup. +# Defaults to true. +tls_verify = true + +# If true, client certificates will be validated by the server; +# clients without a valid certificate will be unable to connect. +# By default, client certs are not checked. +tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +tls_cacert = regress/logsrvd_conf/cacert.pem + +# Path to the server's certificate file in PEM format. +# Required for TLS connections. +tls_cert = regress/logsrvd_conf/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# Required for TLS connections. +tls_key = regress/logsrvd_conf/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# This setting is only effective if the negotiated protocol is TLS version +# 1.2. The default cipher list is HIGH:!aNULL. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default cipher list is TLS_AES_256_GCM_SHA384. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# If not set, the server will use the OpenSSL defaults. +tls_dhparams = regress/logsrvd_conf/logsrvd_dhparams.pem + +[relay] +# The host name or IP address and port to send logs to in relay mode. +# The syntax is identical to listen_address with the exception of +# the wild card ('*') syntax. When this setting is enabled, logs will +# be relayed to the specified host instead of being stored locally. +# This setting is not enabled by default. +#relay_host = relayhost.dom.ain +relay_host = 127.0.0.1(tls) + +# The amount of time, in seconds, the server will wait for a connection +# to the relay server to complete. A value of 0 will disable the timeout. +# The default value is 30. +connect_timeout = 30 + +# The directory to store messages in before they are sent to the relay. +# Messages are stored in wire format. +# The default value is /var/log/sudo_logsrvd. +relay_dir = /var/log/sudo_logsrvd + +# The number of seconds to wait after a connection error before +# making a new attempt to forward a message to a relay host. +# The default value is 30. +retry_interval = 30 + +# Whether to store the log before relaying it. If true, enable store +# and forward mode. If false, the client connection is immediately +# relayed. Defaults to false. +store_first = true + +# If true, enable the SO_KEEPALIVE socket option on relay connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the relay to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server's relay certificate will be verified at startup. +# The default is to use the value in the [server] section. +tls_verify = true + +# Whether to verify the relay's certificate for TLS connections. +# The default is to use the value in the [server] section. +tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +# The default is to use the value in the [server] section. +tls_cacert = regress/logsrvd_conf/cacert.pem + +# Path to the server's certificate file in PEM format. +# The default is to use the certificate in the [server] section. +tls_cert = regress/logsrvd_conf/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# The default is to use the key in the [server] section. +tls_key = regress/logsrvd_conf/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# this setting is only effective if the negotiated protocol is TLS version +# 1.2. The default is to use the value in the [server] section. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default is to use the value in the [server] section. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# The default is to use the value in the [server] section. +tls_dhparams = regress/logsrvd_conf/logsrvd_dhparams.pem + +[iolog] +# The top-level directory to use when constructing the path name for the +# I/O log directory. The session sequence number, if any, is stored here. +iolog_dir = /var/log/sudo-io + +# The path name, relative to iolog_dir, in which to store I/O logs. +# It is possible for iolog_file to contain directory components. +iolog_file = %{seq} + +# If set, I/O logs will be compressed using zlib. Enabling compression can +# make it harder to view the logs in real-time as the program is executing. +iolog_compress = false + +# If set, I/O log data is flushed to disk after each write instead of +# buffering it. This makes it possible to view the logs in real-time +# as the program is executing but reduces the effectiveness of compression. +iolog_flush = true + +# The group to use when creating new I/O log files and directories. +# If iolog_group is not set, the primary group-ID of the user specified +# by iolog_user is used. If neither iolog_group nor iolog_user +# are set, I/O log files and directories are created with group-ID 0. +#iolog_group = wheel + +# The user to use when setting the user-ID and group-ID of new I/O +# log files and directories. If iolog_group is set, it will be used +# instead of the user's primary group-ID. By default, I/O log files +# and directories are created with user and group-ID 0. +#iolog_user = root + +# The file mode to use when creating I/O log files. The file permissions +# will always include the owner read and write bits, even if they are +# not present in the specified mode. When creating I/O log directories, +# search (execute) bits are added to match the read and write bits +# specified by iolog_mode. +iolog_mode = 0600 + +# If disabled, sudo_logsrvd will attempt to avoid logging plaintext +# password in the terminal input using passprompt_regex. +log_passwords = true + +# The maximum sequence number that will be substituted for the "%{seq}" +# escape in the I/O log file. While the value substituted for "%{seq}" +# is in base 36, maxseq itself should be expressed in decimal. Values +# larger than 2176782336 (which corresponds to the base 36 sequence +# number "ZZZZZZ") will be silently truncated to 2176782336. +maxseq = 2176782336 + +# One or more POSIX extended regular expressions used to match +# password prompts in the terminal output when log_passwords is +# disabled. Multiple passprompt_regex settings may be specified. +#passprompt_regex = [Pp]assword[: ]* +passprompt_regex = [Pp]assword for [a-z0-9]+: * + +[eventlog] +# Where to log accept, reject, exit, and alert events. +# Accepted values are syslog, logfile, or none. +# Defaults to syslog +log_type = syslog + +# Whether to log an event when a command exits or is terminated by a signal. +# Defaults to false +log_exit = true + +# Event log format. +# Supported log formats are "sudo" and "json" +# Defaults to sudo +log_format = sudo + +[syslog] +# The maximum length of a syslog payload. +# On many systems, syslog(3) has a relatively small log buffer. +# IETF RFC 5424 states that syslog servers must support messages +# of at least 480 bytes and should support messages up to 2048 bytes. +# Messages larger than this value will be split into multiple messages. +maxlen = 960 + +# The syslog facility to use for event log messages. +# The following syslog facilities are supported: authpriv (if your OS +# supports it), auth, daemon, user, local0, local1, local2, local3, +# local4, local5, local6, and local7. +#facility = authpriv +facility = auth + +# Syslog priority to use for event log accept messages, when the command +# is allowed by the security policy. The following syslog priorities are +# supported: alert, crit, debug, emerg, err, info, notice, warning, none. +accept_priority = notice + +# Syslog priority to use for event log reject messages, when the command +# is not allowed by the security policy. +reject_priority = alert + +# Syslog priority to use for event log alert messages reported by the +# client. +alert_priority = alert + +# The syslog facility to use for server warning messages. +# Defaults to daemon. +server_facility = daemon + +[logfile] +# The path to the file-based event log. +# This path must be fully-qualified and start with a '/' character. +path = /var/log/sudo.log + +# The format string used when formatting the date and time for +# file-based event logs. Formatting is performed via strftime(3) so +# any format string supported by that function is allowed. +time_format = %h %e %T diff --git a/logsrvd/regress/logsrvd_conf/tls/sudo_logsrvd.conf.2.in b/logsrvd/regress/logsrvd_conf/tls/sudo_logsrvd.conf.2.in new file mode 100644 index 0000000..0e71f67 --- /dev/null +++ b/logsrvd/regress/logsrvd_conf/tls/sudo_logsrvd.conf.2.in @@ -0,0 +1,252 @@ +# +# sudo logsrv daemon configuration +# + +[server] +# The host name or IP address and port to listen on with an optional TLS +# flag. If no port is specified, port 30343 will be used for plaintext +# connections and port 30344 will be used to TLS connections. +# The following forms are accepted: +# listen_address = hostname(tls) +# listen_address = hostname:port(tls) +# listen_address = IPv4_address(tls) +# listen_address = IPv4_address:port(tls) +# listen_address = [IPv6_address](tls) +# listen_address = [IPv6_address]:port(tls) +# +# The (tls) suffix should be omitted for plaintext connections. +# +# Multiple listen_address settings may be specified. +# The default is to listen on all addresses. +listen_address = 172.0.0.1:30343 +listen_address = 172.0.0.1:30344(tls) + +# The file containing the ID of the running sudo_logsrvd process. +pid_file = /var/run/sudo/sudo_logsrvd.pid + +# Where to log server warnings: none, stderr, syslog, or a path name. +server_log = stderr + +# If true, enable the SO_KEEPALIVE socket option on client connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the client to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server will validate its own certificate at startup. +# Defaults to true. +tls_verify = false + +# If true, client certificates will be validated by the server; +# clients without a valid certificate will be unable to connect. +# By default, client certs are not checked. +tls_checkpeer = true + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +tls_cacert = regress/logsrvd_conf/cacert.pem + +# Path to the server's certificate file in PEM format. +# Required for TLS connections. +tls_cert = regress/logsrvd_conf/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# Required for TLS connections. +tls_key = regress/logsrvd_conf/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# This setting is only effective if the negotiated protocol is TLS version +# 1.2. The default cipher list is HIGH:!aNULL. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default cipher list is TLS_AES_256_GCM_SHA384. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# If not set, the server will use the OpenSSL defaults. +tls_dhparams = regress/logsrvd_conf/logsrvd_dhparams.pem + +[relay] +# The host name or IP address and port to send logs to in relay mode. +# The syntax is identical to listen_address with the exception of +# the wild card ('*') syntax. When this setting is enabled, logs will +# be relayed to the specified host instead of being stored locally. +# This setting is not enabled by default. +#relay_host = relayhost.dom.ain +relay_host = 127.0.0.1(tls) + +# The amount of time, in seconds, the server will wait for a connection +# to the relay server to complete. A value of 0 will disable the timeout. +# The default value is 30. +connect_timeout = 30 + +# The directory to store messages in before they are sent to the relay. +# Messages are stored in wire format. +# The default value is /var/log/sudo_logsrvd. +relay_dir = /var/log/sudo_logsrvd + +# The number of seconds to wait after a connection error before +# making a new attempt to forward a message to a relay host. +# The default value is 30. +retry_interval = 30 + +# Whether to store the log before relaying it. If true, enable store +# and forward mode. If false, the client connection is immediately +# relayed. Defaults to false. +store_first = true + +# If true, enable the SO_KEEPALIVE socket option on relay connections. +# Defaults to true. +tcp_keepalive = true + +# The amount of time, in seconds, the server will wait for the relay to +# respond. A value of 0 will disable the timeout. The default value is 30. +timeout = 30 + +# If true, the server's relay certificate will be verified at startup. +# The default is to use the value in the [server] section. +tls_verify = true + +# Whether to verify the relay's certificate for TLS connections. +# The default is to use the value in the [server] section. +tls_checkpeer = false + +# Path to a certificate authority bundle file in PEM format to use +# instead of the system's default certificate authority database. +# The default is to use the value in the [server] section. +tls_cacert = regress/logsrvd_conf/cacert.pem + +# Path to the server's certificate file in PEM format. +# The default is to use the certificate in the [server] section. +tls_cert = regress/logsrvd_conf/logsrvd_cert.pem + +# Path to the server's private key file in PEM format. +# The default is to use the key in the [server] section. +tls_key = regress/logsrvd_conf/logsrvd_key.pem + +# TLS cipher list (see "CIPHER LIST FORMAT" in the openssl-ciphers manual). +# this setting is only effective if the negotiated protocol is TLS version +# 1.2. The default is to use the value in the [server] section. +tls_ciphers_v12 = HIGH:!aNULL + +# TLS cipher list if the negotiated protocol is TLS version 1.3. +# The default is to use the value in the [server] section. +tls_ciphers_v13 = TLS_AES_256_GCM_SHA384 + +# Path to the Diffie-Hellman parameter file in PEM format. +# The default is to use the value in the [server] section. +tls_dhparams = regress/logsrvd_conf/logsrvd_dhparams.pem + +[iolog] +# The top-level directory to use when constructing the path name for the +# I/O log directory. The session sequence number, if any, is stored here. +iolog_dir = /var/log/sudo-io + +# The path name, relative to iolog_dir, in which to store I/O logs. +# It is possible for iolog_file to contain directory components. +iolog_file = %{seq} + +# If set, I/O logs will be compressed using zlib. Enabling compression can +# make it harder to view the logs in real-time as the program is executing. +iolog_compress = false + +# If set, I/O log data is flushed to disk after each write instead of +# buffering it. This makes it possible to view the logs in real-time +# as the program is executing but reduces the effectiveness of compression. +iolog_flush = true + +# The group to use when creating new I/O log files and directories. +# If iolog_group is not set, the primary group-ID of the user specified +# by iolog_user is used. If neither iolog_group nor iolog_user +# are set, I/O log files and directories are created with group-ID 0. +#iolog_group = wheel + +# The user to use when setting the user-ID and group-ID of new I/O +# log files and directories. If iolog_group is set, it will be used +# instead of the user's primary group-ID. By default, I/O log files +# and directories are created with user and group-ID 0. +#iolog_user = root + +# The file mode to use when creating I/O log files. The file permissions +# will always include the owner read and write bits, even if they are +# not present in the specified mode. When creating I/O log directories, +# search (execute) bits are added to match the read and write bits +# specified by iolog_mode. +iolog_mode = 0600 + +# If disabled, sudo_logsrvd will attempt to avoid logging plaintext +# password in the terminal input using passprompt_regex. +log_passwords = true + +# The maximum sequence number that will be substituted for the "%{seq}" +# escape in the I/O log file. While the value substituted for "%{seq}" +# is in base 36, maxseq itself should be expressed in decimal. Values +# larger than 2176782336 (which corresponds to the base 36 sequence +# number "ZZZZZZ") will be silently truncated to 2176782336. +maxseq = 2176782336 + +# One or more POSIX extended regular expressions used to match +# password prompts in the terminal output when log_passwords is +# disabled. Multiple passprompt_regex settings may be specified. +#passprompt_regex = [Pp]assword[: ]* +passprompt_regex = [Pp]assword for [a-z0-9]+: * + +[eventlog] +# Where to log accept, reject, exit, and alert events. +# Accepted values are syslog, logfile, or none. +# Defaults to syslog +log_type = none + +# Whether to log an event when a command exits or is terminated by a signal. +# Defaults to false +log_exit = true + +# Event log format. +# Supported log formats are "sudo" and "json" +# Defaults to sudo +log_format = json + +[syslog] +# The maximum length of a syslog payload. +# On many systems, syslog(3) has a relatively small log buffer. +# IETF RFC 5424 states that syslog servers must support messages +# of at least 480 bytes and should support messages up to 2048 bytes. +# Messages larger than this value will be split into multiple messages. +maxlen = 960 + +# The syslog facility to use for event log messages. +# The following syslog facilities are supported: authpriv (if your OS +# supports it), auth, daemon, user, local0, local1, local2, local3, +# local4, local5, local6, and local7. +#facility = authpriv +facility = daemon + +# Syslog priority to use for event log accept messages, when the command +# is allowed by the security policy. The following syslog priorities are +# supported: alert, crit, debug, emerg, err, info, notice, warning, none. +accept_priority = notice + +# Syslog priority to use for event log reject messages, when the command +# is not allowed by the security policy. +reject_priority = alert + +# Syslog priority to use for event log alert messages reported by the +# client. +alert_priority = alert + +# The syslog facility to use for server warning messages. +# Defaults to daemon. +server_facility = daemon + +[logfile] +# The path to the file-based event log. +# This path must be fully-qualified and start with a '/' character. +path = /var/log/sudo.log + +# The format string used when formatting the date and time for +# file-based event logs. Formatting is performed via strftime(3) so +# any format string supported by that function is allowed. +time_format = %h %e %T diff --git a/logsrvd/sendlog.c b/logsrvd/sendlog.c new file mode 100644 index 0000000..98d5ecc --- /dev/null +++ b/logsrvd/sendlog.c @@ -0,0 +1,1881 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <netdb.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#ifndef HAVE_GETADDRINFO +# include "compat/getaddrinfo.h" +#endif +#ifdef HAVE_GETOPT_LONG +# include <getopt.h> +# else +# include "compat/getopt.h" +#endif /* HAVE_GETOPT_LONG */ + +#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 "sendlog.h" +#include "hostcheck.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 struct peer_info server_info = { "localhost" }; +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] [-s stop-point] [-t number] /path/to/iolog\n", + getprogname()); + if (fatal) + exit(EXIT_FAILURE); +} + +static void +help(void) +{ + printf("%s - %s\n\n", getprogname(), + _("send sudo I/O log to remote server")); + usage(false); + printf("\n%s\n", _("Options:")); + printf(" --help %s\n", + _("display help message and exit")); + printf(" -A, --accept %s\n", + _("only send an accept event (no I/O)")); +#if defined(HAVE_OPENSSL) + printf(" -b, --ca-bundle %s\n", + _("certificate bundle file to verify server's cert against")); + printf(" -c, --cert %s\n", + _("certificate file for TLS handshake")); +#endif + printf(" -h, --host %s\n", + _("host to send logs to")); + printf(" -i, --iolog_id %s\n", + _("remote ID of I/O log to be resumed")); +#if defined(HAVE_OPENSSL) + printf(" -k, --key %s\n", + _("private key file")); + printf(" -n, --no-verify %s\n", + _("do not verify server certificate")); +#endif + printf(" -p, --port %s\n", + _("port to use when connecting to host")); + printf(" -r, --restart %s\n", + _("restart previous I/O log transfer")); + printf(" -R, --reject %s\n", + _("reject the command with the given reason")); + printf(" -s, --stop-after %s\n", + _("stop transfer after reaching this time")); + printf(" -t, --test %s\n", + _("test audit server by sending selected I/O log n times in parallel")); + printf(" -V, --version %s\n", + _("display version information and exit")); + putchar('\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(struct peer_info *server, 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(server->name, port, &hints, &res0); + if (error != 0) { + sudo_warnx(U_("unable to look up %s:%s: %s"), server->name, 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->ipaddr[0] == '\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->ipaddr, + sizeof(server->ipaddr)) == 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); +} + +/* + * Get a buffer from the free list if possible, else allocate a new one. + */ +static struct connection_buffer * +get_free_buf(size_t len, struct client_closure *closure) +{ + struct connection_buffer *buf; + debug_decl(get_free_buf, SUDO_DEBUG_UTIL); + + buf = TAILQ_FIRST(&closure->free_bufs); + if (buf != NULL) { + TAILQ_REMOVE(&closure->free_bufs, buf, entries); + } else { + if ((buf = calloc(1, sizeof(*buf))) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_ptr(NULL); + } + } + + if (len > buf->size) { + free(buf->data); + buf->size = sudo_pow2_roundup(len); + if ((buf->data = malloc(buf->size)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + free(buf); + buf = NULL; + } + } + + debug_return_ptr(buf); +} + +/* + * 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_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + 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 client_closure *closure, ClientMessage *msg) +{ + struct connection_buffer *buf = NULL; + 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); + + if (!TAILQ_EMPTY(&closure->write_bufs)) { + buf = TAILQ_FIRST(&closure->write_bufs); + if (len > buf->size - buf->len) { + /* Too small. */ + buf = NULL; + } + } + if (buf == NULL) { + if ((buf = get_free_buf(len, closure)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + TAILQ_INSERT_TAIL(&closure->write_bufs, buf, entries); + } + + memcpy(buf->data + buf->len, &msg_len, sizeof(msg_len)); + client_message__pack(msg, buf->data + buf->len + sizeof(msg_len)); + buf->len += len; + + ret = true; + +done: + debug_return_bool(ret); +} + +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 = (char *)"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, &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); +} + +#if defined(HAVE_OPENSSL) +/* Wrapper for fmt_client_hello() called via tls_connect_cb() */ +static bool +tls_start_fn(struct tls_client_closure *tls_client) +{ + return fmt_client_hello(tls_client->parent_closure); +} +#endif /* HAVE_OPENSSL */ + +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; +} + +/* + * Convert a NULL-terminated string vector (argv, envp) to a + * StringList with an associated size. + * Performs a shallow copy of the strings (copies pointers). + */ +static InfoMessage__StringList * +vec_to_stringlist(char * const *vec) +{ + InfoMessage__StringList *strlist; + size_t len; + debug_decl(vec_to_stringlist, SUDO_DEBUG_UTIL); + + strlist = malloc(sizeof(*strlist)); + if (strlist == NULL) + goto done; + info_message__string_list__init(strlist); + + /* Convert vec into a StringList. */ + for (len = 0; vec[len] != NULL; len++) { + continue; + } + strlist->strings = reallocarray(NULL, len, sizeof(char *)); + if (strlist->strings == NULL) { + free(strlist); + strlist = NULL; + goto done; + } + strlist->n_strings = len; + for (len = 0; vec[len] != NULL; len++) { + strlist->strings[len] = vec[len]; + } + +done: + debug_return_ptr(strlist); +} + +/* + * Split command + args separated by whitespace into a StringList. + * Returns a StringList containing command and args, reusing the contents + * of "command", which is modified. + */ +static InfoMessage__StringList * +command_to_stringlist(char *command) +{ + InfoMessage__StringList *strlist; + char *cp; + size_t len; + debug_decl(command_to_stringlist, SUDO_DEBUG_UTIL); + + strlist = malloc(sizeof(*strlist)); + if (strlist == NULL) + debug_return_ptr(NULL); + info_message__string_list__init(strlist); + + for (cp = command, len = 0;;) { + len++; + if ((cp = strchr(cp, ' ')) == NULL) + break; + cp++; + } + strlist->strings = reallocarray(NULL, len, sizeof(char *)); + if (strlist->strings == NULL) { + free(strlist); + debug_return_ptr(NULL); + } + strlist->n_strings = len; + + for (cp = command, len = 0;;) { + strlist->strings[len++] = cp; + if ((cp = strchr(cp, ' ')) == NULL) + break; + *cp++ = '\0'; + } + + debug_return_ptr(strlist); +} + +/* + * Build runargv StringList using either argv or command in evlog. + * Truncated command in evlog after first space as a side effect. + */ +static InfoMessage__StringList * +fmt_runargv(const struct eventlog *evlog) +{ + InfoMessage__StringList *runargv; + debug_decl(fmt_runargv, SUDO_DEBUG_UTIL); + + /* We may have runargv from the log.json file. */ + if (evlog->argv != NULL && evlog->argv[0] != NULL) { + /* Convert evlog->argv into a StringList. */ + runargv = vec_to_stringlist(evlog->argv); + if (runargv != NULL) { + /* Make sure command doesn't include arguments. */ + char *cp = strchr(evlog->command, ' '); + if (cp != NULL) + *cp = '\0'; + } + } else { + /* No log.json file, split command into a StringList. */ + runargv = command_to_stringlist(evlog->command); + } + + debug_return_ptr(runargv); +} + +/* + * Build runenv StringList from env in evlog, if present. + */ +static InfoMessage__StringList * +fmt_runenv(const struct eventlog *evlog) +{ + debug_decl(fmt_runenv, SUDO_DEBUG_UTIL); + + /* Only present in log.json. */ + if (evlog->envp == NULL || evlog->envp[0] == NULL) + debug_return_ptr(NULL); + + debug_return_ptr(vec_to_stringlist(evlog->envp)); +} + +static InfoMessage ** +fmt_info_messages(const struct eventlog *evlog, char *hostname, + size_t *n_info_msgs) +{ + InfoMessage **info_msgs = NULL; + InfoMessage__StringList *runargv = NULL; + InfoMessage__StringList *runenv = NULL; + size_t info_msgs_size, n = 0; + debug_decl(fmt_info_messages, SUDO_DEBUG_UTIL); + + runargv = fmt_runargv(evlog); + if (runargv == NULL) + goto oom; + + /* runenv is only present in log.json */ + runenv = fmt_runenv(evlog); + + /* The sudo I/O log info file has limited info. */ + info_msgs_size = 13; + 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]); + } + +#define fill_str(_n, _v) do { \ + info_msgs[n]->key = (char *)(_n); \ + info_msgs[n]->u.strval = (_v); \ + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; \ + n++; \ +} while (0) + +#define fill_strlist(_n, _v) do { \ + info_msgs[n]->key = (char *)(_n); \ + info_msgs[n]->u.strlistval = (_v); \ + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRLISTVAL; \ + n++; \ +} while (0) + +#define fill_num(_n, _v) do { \ + info_msgs[n]->key = (char *)(_n); \ + info_msgs[n]->u.numval = (_v); \ + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_NUMVAL; \ + n++; \ +} while (0) + + /* Fill in info_msgs */ + n = 0; + fill_num("columns", evlog->columns); + fill_str("command", evlog->command); + fill_num("lines", evlog->lines); + fill_strlist("runargv", runargv); + runargv = NULL; + if (runenv != NULL) { + fill_strlist("runenv", runenv); + runenv = NULL; + } + if (evlog->rungid != (gid_t)-1) { + fill_num("rungid", evlog->rungid); + } + if (evlog->rungroup != NULL) { + fill_str("rungroup", evlog->rungroup); + } + if (evlog->runuid != (uid_t)-1) { + fill_num("runuid", evlog->runuid); + } + fill_str("runuser", evlog->runuser); + fill_str("submitcwd", evlog->cwd); + fill_str("submithost", hostname); + fill_str("submituser", evlog->submituser); + fill_str("ttyname", evlog->ttyname); + + /* 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); + } + if (runenv != NULL) { + free(runenv->strings); + free(runenv); + } + *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, &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, &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, &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 list. + * 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; + TimeSpec run_time = TIME_SPEC__INIT; + struct eventlog *evlog = closure->evlog; + bool ret = false; + debug_decl(fmt_exit_message, SUDO_DEBUG_UTIL); + + if (evlog->exit_value != -1) + exit_msg.exit_value = evlog->exit_value; + if (sudo_timespecisset(&evlog->run_time)) { + run_time.tv_sec = evlog->run_time.tv_sec; + run_time.tv_nsec = evlog->run_time.tv_nsec; + exit_msg.run_time = &run_time; + } + if (evlog->signal_name != NULL) { + exit_msg.signal = evlog->signal_name; + exit_msg.dumped_core = evlog->dumped_core; + } + + if (evlog->signal_name != NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending ExitMessage, signal %s, run_time [%lld, %ld]", + __func__, evlog->signal_name, (long long)evlog->run_time.tv_sec, + evlog->run_time.tv_nsec); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending ExitMessage, exit value %d, run_time [%lld, %ld]", + __func__, evlog->exit_value, (long long)evlog->run_time.tv_sec, + evlog->run_time.tv_nsec); + } + + /* Send ClientMessage */ + client_msg.u.exit_msg = &exit_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_EXIT_MSG; + if (!fmt_client_message(closure, &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 the closure's write buffer list. + * Returns true on success, false on failure. + */ +static bool +fmt_io_buf(int type, struct client_closure *closure) +{ + 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(closure, &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 the closure's write buffer list. + * Returns true on success, false on failure. + */ +static bool +fmt_winsize(struct client_closure *closure) +{ + 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(closure, &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 the closure's write buffer list. + * Returns true on success, false on failure. + */ +static bool +fmt_suspend(struct client_closure *closure) +{ + 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(closure, &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 list. + * Returns true on success, false on failure. + */ +static bool +fmt_next_iolog(struct client_closure *closure) +{ + struct timing_closure *timing = &closure->timing; + bool ret = false; + debug_decl(fmt_next_iolog, SUDO_DEBUG_UTIL); + + for (;;) { + const int timing_status = iolog_read_timing_record( + &closure->iolog_files[IOFD_TIMING], timing); + switch (timing_status) { + 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(&closure->elapsed, &timing->delay, &closure->elapsed); + + /* If there is a stopping point, make sure we haven't reached it. */ + if (sudo_timespecisset(&closure->stop_after)) { + if (sudo_timespeccmp(&closure->elapsed, &closure->stop_after, >)) { + /* Reached limit, force premature end. */ + sudo_timespecsub(&closure->elapsed, &timing->delay, + &closure->elapsed); + debug_return_bool(false); + } + } + + /* If we have a restart point, ignore records until we hit it. */ + if (sudo_timespecisset(&closure->restart)) { + if (sudo_timespeccmp(&closure->restart, &closure->elapsed, >=)) + continue; + sudo_timespecclear(&closure->restart); /* caught up */ + } + + switch (timing->event) { + case IO_EVENT_STDIN: + ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDIN_BUF, closure); + break; + case IO_EVENT_STDOUT: + ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDOUT_BUF, closure); + break; + case IO_EVENT_STDERR: + ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDERR_BUF, closure); + break; + case IO_EVENT_TTYIN: + ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_TTYIN_BUF, closure); + break; + case IO_EVENT_TTYOUT: + ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_TTYOUT_BUF, closure); + break; + case IO_EVENT_WINSIZE: + ret = fmt_winsize(closure); + break; + case IO_EVENT_SUSPEND: + ret = fmt_suspend(closure); + break; + default: + sudo_warnx(U_("unexpected I/O event %d"), timing->event); + break; + } + + /* Keep filling write buffer as long as we only have one of them. */ + if (!ret) + break; + if (TAILQ_NEXT(TAILQ_FIRST(&closure->write_bufs), entries) != NULL) + 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(U_("unable to unpack %s size %zu"), "ServerMessage", len); + 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) { + SSL *ssl = closure->tls_client.ssl; + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: reading ServerMessage (TLS)", __func__); + nread = SSL_read(ssl, buf->data + buf->len, buf->size - buf->len); + if (nread <= 0) { + const char *errstr; + int err; + + switch (SSL_get_error(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 !defined(HAVE_WOLFSSL) + if (closure->state == RECV_HELLO && + ERR_GET_REASON(err) == SSL_R_TLSV1_ALERT_INTERNAL_ERROR) { + errstr = U_("host name does not match certificate"); + } else +#endif + { + errstr = ERR_reason_error_string(err); + } + sudo_warnx("%s", errstr ? errstr : strerror(errno)); + 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 ? errstr : strerror(errno)); + 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 || errno == EINTR) + 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; + ssize_t nwritten; + debug_decl(client_msg_cb, SUDO_DEBUG_UTIL); + + if ((buf = TAILQ_FIRST(&closure->write_bufs)) == NULL) { + sudo_warnx(U_("missing write buffer for client %s"), "localhost"); + goto bad; + } + + /* 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) { + SSL *ssl = closure->tls_client.ssl; + nwritten = SSL_write(ssl, buf->data + buf->off, buf->len - buf->off); + if (nwritten <= 0) { + const char *errstr; + + switch (SSL_get_error(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 ? errstr : strerror(errno)); + goto bad; + } + } + } else +#endif + { + nwritten = send(fd, buf->data + buf->off, buf->len - buf->off, 0); + } + if (nwritten == -1) { + if (errno == EAGAIN || errno == EINTR) + debug_return; + 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; + TAILQ_REMOVE(&closure->write_bufs, buf, entries); + TAILQ_INSERT_TAIL(&closure->free_bufs, buf, entries); + if (TAILQ_EMPTY(&closure->write_bufs)) { + /* Write queue empty, check state. */ + 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); +} + +/* + * Free client closure contents. + */ +static void +client_closure_free(struct client_closure *closure) +{ + struct connection_buffer *buf; + debug_decl(connection_closure_free, SUDO_DEBUG_UTIL); + + if (closure != NULL) { + TAILQ_REMOVE(&connections, closure, entries); +#if defined(HAVE_OPENSSL) + if (closure->tls_client.ssl != NULL) { + if (SSL_shutdown(closure->tls_client.ssl) == 0) + SSL_shutdown(closure->tls_client.ssl); + SSL_free(closure->tls_client.ssl); + } + sudo_ev_free(closure->tls_client.tls_connect_ev); +#endif + sudo_ev_free(closure->read_ev); + sudo_ev_free(closure->write_ev); + free(closure->read_buf.data); + free(closure->buf); + while ((buf = TAILQ_FIRST(&closure->write_bufs)) != NULL) { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "discarding write buffer %p, len %u", buf, buf->len - buf->off); + TAILQ_REMOVE(&closure->write_bufs, buf, entries); + free(buf->data); + free(buf); + } + while ((buf = TAILQ_FIRST(&closure->free_bufs)) != NULL) { + TAILQ_REMOVE(&closure->free_bufs, buf, entries); + free(buf->data); + free(buf); + } + shutdown(closure->sock, SHUT_RDWR); + 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 *restart, struct timespec *stop_after, const char *iolog_id, + char *reject_reason, bool accept_only, struct eventlog *evlog) +{ + struct connection_buffer *buf; + 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_INIT(&closure->write_bufs); + TAILQ_INIT(&closure->free_bufs); + + TAILQ_INSERT_TAIL(&connections, closure, entries); + + closure->state = RECV_HELLO; + closure->accept_only = accept_only; + closure->reject_reason = reject_reason; + closure->evlog = evlog; + + closure->restart.tv_sec = restart->tv_sec; + closure->restart.tv_nsec = restart->tv_nsec; + closure->stop_after.tv_sec = stop_after->tv_sec; + closure->stop_after.tv_nsec = stop_after->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; + + buf = get_free_buf(64 * 1024, closure); + if (buf == NULL) + goto bad; + TAILQ_INSERT_TAIL(&closure->free_bufs, buf, entries); + + 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_client.tls_connect_ev = sudo_ev_alloc(sock, SUDO_EV_WRITE, + tls_connect_cb, &closure->tls_client); + if (closure->tls_client.tls_connect_ev == NULL) + goto bad; + closure->tls_client.evbase = base; + closure->tls_client.parent_closure = closure; + closure->tls_client.peer_name = &server_info; + closure->tls_client.connect_timeout.tv_sec = TLS_HANDSHAKE_TIMEO_SEC; + closure->tls_client.start_fn = tls_start_fn; + } +#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:s:t:b:c:k:V"; +#else +static const char short_opts[] = "Ah:i:Ip:r:R:t:s: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' }, + { "stop-after", required_argument, NULL, 's' }, + { "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 stop_after = { 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()), -1); + + 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_info.name = optarg; + break; + case 'i': + iolog_id = optarg; + break; + case 'p': + port = optarg; + break; + case 'R': + reject_reason = optarg; + break; + case 'r': + if (!parse_timespec(&restart, optarg)) + goto bad; + open_mode = "r+"; + break; + case 's': + if (!parse_timespec(&stop_after, optarg)) + goto bad; + 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(U_("%s: %s"), __func__, U_("unable to allocate memory")); + + if (testrun) + printf("connecting clients...\n"); + + for (int i = 0; i < nr_of_conns; i++) { + sock = connect_server(&server_info, port); + if (sock == -1) + goto bad; + + if (!testrun) + printf("Connected to %s:%s\n", server_info.name, port); + + closure = client_closure_alloc(sock, evbase, &restart, &stop_after, + 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_client_setup(closure->sock, ca_bundle, cert, key, NULL, + NULL, NULL, verify_server, false, &closure->tls_client)) + 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..74014ef --- /dev/null +++ b/logsrvd/sendlog.h @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2020 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SUDO_SENDLOG_H +#define SUDO_SENDLOG_H + +#include "log_server.pb-c.h" +#if PROTOBUF_C_VERSION_NUMBER < 1003000 +# error protobuf-c version 1.30 or higher required +#endif + +#include <config.h> + +#if defined(HAVE_OPENSSL) +# if defined(HAVE_WOLFSSL) +# include <wolfssl/options.h> +# endif +# include <openssl/ssl.h> +# include <openssl/err.h> +#endif + +#include "logsrv_util.h" +#include "tls_common.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 stop_after; + struct timespec elapsed; + struct timespec committed; + struct timing_closure timing; + struct sudo_event_base *evbase; + struct connection_buffer read_buf; + struct connection_buffer_list write_bufs; + struct connection_buffer_list free_bufs; +#if defined(HAVE_OPENSSL) + struct tls_client_closure tls_client; +#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 */ diff --git a/logsrvd/tls_client.c b/logsrvd/tls_client.c new file mode 100644 index 0000000..dde6a60 --- /dev/null +++ b/logsrvd/tls_client.c @@ -0,0 +1,251 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <errno.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#include "sudo_compat.h" +#include "sudo_debug.h" +#include "sudo_event.h" +#include "sudo_fatal.h" +#include "sudo_gettext.h" +#include "sudo_util.h" + +#include "logsrv_util.h" +#include "tls_common.h" +#include "hostcheck.h" + +#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) +{ + HostnameValidationResult result; + struct peer_info *peer_info; + 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); + } + + /* Fetch the attached peer_info from the ssl connection object. */ + ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); + peer_info = SSL_get_ex_data(ssl, 1); + + /* + * Validate the cert based on the host name and IP address. + * If host name is not known, validate_hostname() can resolve it. + */ + result = validate_hostname(peer_cert, + peer_info->name ? peer_info->name : peer_info->ipaddr, + peer_info->ipaddr, peer_info->name ? 0 : 1); + + debug_return_int(result == MatchFound); +} + +void +tls_connect_cb(int sock, int what, void *v) +{ + struct tls_client_closure *tls_client = v; + struct sudo_event_base *evbase = tls_client->evbase; + const struct timespec *timeout = &tls_client->connect_timeout; + 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(tls_client->ssl); + + if (con_stat == 1) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "SSL_connect successful"); + tls_client->tls_connect_state = true; + } else { + switch (SSL_get_error(tls_client->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(tls_client->tls_connect_ev, + SSL_get_fd(tls_client->ssl), SUDO_EV_READ, + tls_connect_cb, tls_client) == -1) { + sudo_warnx("%s", U_("unable to set event")); + goto bad; + } + } + if (sudo_ev_add(evbase, tls_client->tls_connect_ev, timeout, 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(tls_client->tls_connect_ev, + SSL_get_fd(tls_client->ssl), SUDO_EV_WRITE, + tls_connect_cb, tls_client) == -1) { + sudo_warnx("%s", U_("unable to set event")); + goto bad; + } + } + if (sudo_ev_add(evbase, tls_client->tls_connect_ev, timeout, 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 ? errstr : strerror(errno)); + goto bad; + } + } + + if (tls_client->tls_connect_state) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "TLS version: %s, negotiated cipher suite: %s", + SSL_get_version(tls_client->ssl), SSL_get_cipher(tls_client->ssl)); + + /* Done with TLS connect, send ClientHello */ + sudo_ev_free(tls_client->tls_connect_ev); + tls_client->tls_connect_ev = NULL; + if (!tls_client->start_fn(tls_client)) + goto bad; + } + + debug_return; + +bad: + sudo_ev_loopbreak(evbase); + debug_return; +} + +bool +tls_ctx_client_setup(SSL_CTX *ssl_ctx, int sock, + struct tls_client_closure *closure) +{ + const char *errstr; + bool ret = false; + debug_decl(tls_ctx_client_setup, SUDO_DEBUG_UTIL); + + 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 ? errstr : strerror(errno)); + goto done; + } + + if (SSL_set_ex_data(closure->ssl, 1, closure->peer_name) <= 0) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("Unable to attach user data to the ssl object: %s"), + errstr ? errstr : strerror(errno)); + goto done; + } + + if (SSL_set_fd(closure->ssl, sock) <= 0) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("Unable to attach socket to the ssl object: %s"), + errstr ? errstr : strerror(errno)); + goto done; + } + + if (sudo_ev_add(closure->evbase, closure->tls_connect_ev, NULL, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto done; + } + + ret = true; + +done: + debug_return_bool(ret); +} + +bool +tls_client_setup(int sock, const char *ca_bundle_file, const char *cert_file, + const char *key_file, const char *dhparam_file, const char *ciphers_v12, + const char *ciphers_v13, bool verify_server, bool check_peer, + struct tls_client_closure *closure) +{ + SSL_CTX *ssl_ctx; + debug_decl(tls_client_setup, SUDO_DEBUG_UTIL); + + ssl_ctx = init_tls_context(ca_bundle_file, cert_file, key_file, + dhparam_file, ciphers_v12, ciphers_v13, verify_server); + if (ssl_ctx == NULL) { + sudo_warnx("%s", U_("unable to initialize TLS context")); + debug_return_bool(false); + } + + if (check_peer) { + /* Verify server cert during the handshake. */ + SSL_CTX_set_verify(ssl_ctx, + SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_peer_identity); + } + + debug_return_bool(tls_ctx_client_setup(ssl_ctx, sock, closure)); +} +#endif /* HAVE_OPENSSL */ diff --git a/logsrvd/tls_common.h b/logsrvd/tls_common.h new file mode 100644 index 0000000..505801c --- /dev/null +++ b/logsrvd/tls_common.h @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2021 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SUDO_TLS_COMMON_H +#define SUDO_TLS_COMMON_H + +#include <config.h> + +#if defined(HAVE_OPENSSL) +# if defined(HAVE_WOLFSSL) +# include <wolfssl/options.h> +# endif +# include <openssl/ssl.h> +# include <openssl/err.h> + +struct tls_client_closure { + SSL *ssl; + void *parent_closure; + struct sudo_event_base *evbase; /* duplicated */ + struct sudo_event *tls_connect_ev; + struct peer_info *peer_name; + struct timespec connect_timeout; + bool (*start_fn)(struct tls_client_closure *); + bool tls_connect_state; +}; + +/* tls_client.c */ +void tls_connect_cb(int sock, int what, void *v); +bool tls_client_setup(int sock, const char *ca_bundle_file, const char *cert_file, const char *key_file, const char *dhparam_file, const char *ciphers_v12, const char *ciphers_v13, bool verify_server, bool check_peer, struct tls_client_closure *closure); +bool tls_ctx_client_setup(SSL_CTX *ssl_ctx, int sock, struct tls_client_closure *closure); + +/* tls_init.c */ +SSL_CTX *init_tls_context(const char *ca_bundle_file, const char *cert_file, const char *key_file, const char *dhparam_file, const char *ciphers_v12, const char *ciphers_v13, bool verify_cert); + +#endif /* HAVE_OPENSSL */ + +#endif /* SUDO_TLS_COMMON_H */ diff --git a/logsrvd/tls_init.c b/logsrvd/tls_init.c new file mode 100644 index 0000000..65cc8da --- /dev/null +++ b/logsrvd/tls_init.c @@ -0,0 +1,383 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2022 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <fcntl.h> +#include <unistd.h> + +#include "sudo_compat.h" +#include "sudo_debug.h" +#include "sudo_event.h" +#include "sudo_fatal.h" +#include "sudo_gettext.h" + +#include "tls_common.h" +#include "hostcheck.h" + +#define DEFAULT_CIPHER_LST12 "HIGH:!aNULL" +#define DEFAULT_CIPHER_LST13 "TLS_AES_256_GCM_SHA384" + +#if defined(HAVE_OPENSSL) +# include <openssl/bio.h> +# include <openssl/dh.h> + +static bool +verify_cert_chain(SSL_CTX *ctx, const char *cert_file) +{ +#ifdef HAVE_SSL_CTX_GET0_CERTIFICATE + const char *errstr; + bool ret = false; + X509_STORE_CTX *store_ctx = NULL; + X509_STORE *ca_store; + STACK_OF(X509) *chain_certs; + X509 *x509; + debug_decl(verify_cert_chain, SUDO_DEBUG_UTIL); + + if ((x509 = SSL_CTX_get0_certificate(ctx)) == NULL) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("SSL_CTX_get0_certificate: %s", + errstr ? errstr : strerror(errno)); + goto done; + } + + if ((store_ctx = X509_STORE_CTX_new()) == NULL) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("X509_STORE_CTX_new: %s", + errstr ? errstr : strerror(errno)); + goto done; + } + + if (!SSL_CTX_get0_chain_certs(ctx, &chain_certs)) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("SSL_CTX_get0_chain_certs: %s: %s", cert_file, + errstr ? errstr : strerror(errno)); + goto done; + } + + ca_store = SSL_CTX_get_cert_store(ctx); +#ifdef X509_V_FLAG_X509_STRICT + if (ca_store != NULL) + X509_STORE_set_flags(ca_store, X509_V_FLAG_X509_STRICT); +#endif + + if (!X509_STORE_CTX_init(store_ctx, ca_store, x509, chain_certs)) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("X509_STORE_CTX_init: %s", + errstr ? errstr : strerror(errno)); + goto done; + } + + if (X509_verify_cert(store_ctx) <= 0) { + errstr = + X509_verify_cert_error_string(X509_STORE_CTX_get_error(store_ctx)); + sudo_warnx("X509_verify_cert: %s: %s", cert_file, errstr); + goto done; + } + + ret = true; +done: + 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 char *ciphers_v12, + const char *ciphers_v13) +{ + const char *errstr; + int success = 0; + debug_decl(init_tls_ciphersuites, SUDO_DEBUG_UTIL); + + if (ciphers_v12 != NULL) { + /* try to set TLS v1.2 ciphersuite list from config if given */ + success = SSL_CTX_set_cipher_list(ctx, ciphers_v12); + if (success) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "TLS 1.2 ciphersuite list set to %s", ciphers_v12); + } else { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("unable to set TLS 1.2 ciphersuite to %s: %s"), + ciphers_v12, errstr ? errstr : strerror(errno)); + } + } + if (!success) { + /* fallback to default ciphersuites for TLS v1.2 */ + if (SSL_CTX_set_cipher_list(ctx, 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"), + DEFAULT_CIPHER_LST12, errstr ? errstr : strerror(errno)); + debug_return_bool(false); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "TLS v1.2 ciphersuite list set to %s (default)", + DEFAULT_CIPHER_LST12); + } + } + +# if defined(HAVE_SSL_CTX_SET_CIPHERSUITES) + success = 0; + if (ciphers_v13 != NULL) { + /* try to set TLSv1.3 ciphersuite list from config */ + success = SSL_CTX_set_ciphersuites(ctx, ciphers_v13); + if (success) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "TLS v1.3 ciphersuite list set to %s", ciphers_v13); + } else { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("unable to set TLS 1.3 ciphersuite to %s: %s"), + ciphers_v13, errstr ? errstr : strerror(errno)); + } + } + if (!success) { + /* fallback to default ciphersuites for TLS v1.3 */ + if (SSL_CTX_set_ciphersuites(ctx, 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"), + DEFAULT_CIPHER_LST13, errstr ? errstr : strerror(errno)); + debug_return_bool(false); + } else { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "TLS v1.3 ciphersuite list set to %s (default)", + DEFAULT_CIPHER_LST13); + } + } +# endif + + debug_return_bool(true); +} + +/* + * Load diffie-hellman parameters from bio and store in ctx. + * Returns true on success, else false. + */ +#ifdef HAVE_SSL_CTX_SET0_TMP_DH_PKEY +static bool +set_dhparams_bio(SSL_CTX *ctx, BIO *bio) +{ + EVP_PKEY *dhparams; + bool ret = false; + debug_decl(set_dhparams_bio, SUDO_DEBUG_UTIL); + + dhparams = PEM_read_bio_Parameters(bio, NULL); + if (dhparams != NULL) { + /* dhparams is owned by ctx on success. */ + ret = SSL_CTX_set0_tmp_dh_pkey(ctx, dhparams); + if (!ret) { + const char *errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("unable to set diffie-hellman parameters: %s"), + errstr ? errstr : strerror(errno)); + EVP_PKEY_free(dhparams); + } + } + debug_return_bool(ret); +} +#else +static bool +set_dhparams_bio(SSL_CTX *ctx, BIO *bio) +{ + DH *dhparams; + bool ret = false; + debug_decl(set_dhparams_bio, SUDO_DEBUG_UTIL); + + dhparams = PEM_read_bio_DHparams(bio, NULL, NULL, NULL); + if (dhparams != NULL) { + /* LEAK: dhparams leaked on config reload */ + ret = SSL_CTX_set_tmp_dh(ctx, dhparams); + if (!ret) { + const char *errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("unable to set diffie-hellman parameters: %s"), + errstr ? errstr : strerror(errno)); + DH_free(dhparams); + } + } + debug_return_bool(ret); +} +#endif /* HAVE_SSL_CTX_SET0_TMP_DH_PKEY */ + +/* + * Load diffie-hellman parameters from the specified file and store in ctx. + * Returns true on success, else false. + */ +static bool +set_dhparams(SSL_CTX *ctx, const char *dhparam_file) +{ + BIO *bio; + bool ret = false; + debug_decl(set_dhparams, SUDO_DEBUG_UTIL); + + bio = BIO_new_file(dhparam_file, "r"); + if (bio != NULL) { + if (set_dhparams_bio(ctx, bio)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "loaded diffie-hellman parameters from %s", dhparam_file); + ret = true; + } + BIO_free(bio); + } else { + sudo_warn(U_("unable to open %s"), dhparam_file); + } + + debug_return_bool(ret); +} + +SSL_CTX * +init_tls_context(const char *ca_bundle_file, const char *cert_file, + const char *key_file, const char *dhparam_file, const char *ciphers_v12, + const char *ciphers_v13, bool verify_cert) +{ + SSL_CTX *ctx = NULL; + const char *errstr; + static bool initialized; + debug_decl(init_tls_context, SUDO_DEBUG_UTIL); + + /* Only initialize the SSL library once. */ + if (!initialized) { + SSL_library_init(); + OpenSSL_add_all_algorithms(); + SSL_load_error_strings(); + initialized = true; + } + + /* Create the ssl context and enforce TLS 1.2 or higher. */ + if ((ctx = SSL_CTX_new(TLS_method())) == NULL) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("unable to create TLS context: %s"), + errstr ? errstr : strerror(errno)); + goto bad; + } +#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 ? errstr : strerror(errno)); + 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 (ca_bundle_file != NULL) { + STACK_OF(X509_NAME) *cacerts = + SSL_load_client_CA_file(ca_bundle_file); + + if (cacerts == NULL) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("%s: %s"), ca_bundle_file, + errstr ? errstr : strerror(errno)); + goto bad; + } + SSL_CTX_set_client_CA_list(ctx, cacerts); + + if (SSL_CTX_load_verify_locations(ctx, ca_bundle_file, NULL) <= 0) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("SSL_CTX_load_verify_locations: %s: %s", ca_bundle_file, + errstr ? errstr : strerror(errno)); + 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 ? errstr : strerror(errno)); + goto bad; + } + } + + if (cert_file != NULL) { + if (!SSL_CTX_use_certificate_chain_file(ctx, cert_file)) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("%s: %s"), cert_file, + errstr ? errstr : strerror(errno)); + goto bad; + } + if (key_file == NULL) { + /* No explicit key file set, try to use the cert file. */ + key_file = cert_file; + } + if (!SSL_CTX_use_PrivateKey_file(ctx, key_file, SSL_FILETYPE_PEM) || + !SSL_CTX_check_private_key(ctx)) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("%s: %s"), key_file, + errstr ? errstr : strerror(errno)); + goto bad; + } + + /* Optionally verify the certificate we are using. */ + if (verify_cert) { + if (!verify_cert_chain(ctx, cert_file)) + goto bad; + } else { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "skipping local cert check"); + } + } + + /* Initialize TLS 1.2 1.3 ciphersuites. */ + if (!init_tls_ciphersuites(ctx, ciphers_v12, ciphers_v13)) { + goto bad; + } + + /* + * Load diffie-hellman parameters from a file if specified. + * Failure to open the file is not a fatal error. + */ + if (dhparam_file != NULL) { + if (!set_dhparams(ctx, dhparam_file)) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "unable to load dhparam file, using default parameters"); + } + } else { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "dhparam file not specified, using default parameters"); + } + + goto done; + +bad: + SSL_CTX_free(ctx); + ctx = NULL; + +done: + debug_return_ptr(ctx); +} +#endif /* HAVE_OPENSSL */ |