summaryrefslogtreecommitdiffstats
path: root/logsrvd
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--logsrvd/Makefile.in595
-rw-r--r--logsrvd/iolog_writer.c892
-rw-r--r--logsrvd/logsrv_util.c185
-rw-r--r--logsrvd/logsrv_util.h63
-rw-r--r--logsrvd/logsrvd.c2012
-rw-r--r--logsrvd/logsrvd.h266
-rw-r--r--logsrvd/logsrvd_conf.c1907
-rw-r--r--logsrvd/logsrvd_journal.c616
-rw-r--r--logsrvd/logsrvd_local.c713
-rw-r--r--logsrvd/logsrvd_queue.c287
-rw-r--r--logsrvd/logsrvd_relay.c1257
-rw-r--r--logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.1253
-rw-r--r--logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.2255
-rw-r--r--logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.3253
-rw-r--r--logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.4255
-rw-r--r--logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.5255
-rw-r--r--logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.6255
-rw-r--r--logsrvd/regress/corpus/seed/logsrvd_conf/logsrvd.conf.7255
-rw-r--r--logsrvd/regress/fuzz/fuzz_logsrvd_conf.c139
-rw-r--r--logsrvd/regress/fuzz/fuzz_logsrvd_conf.dict44
-rw-r--r--logsrvd/regress/logsrvd_conf/cacert.pem34
-rw-r--r--logsrvd/regress/logsrvd_conf/logsrvd_cert.pem28
-rw-r--r--logsrvd/regress/logsrvd_conf/logsrvd_conf_test.c89
-rw-r--r--logsrvd/regress/logsrvd_conf/logsrvd_dhparams.pem8
-rw-r--r--logsrvd/regress/logsrvd_conf/logsrvd_key.pem28
-rw-r--r--logsrvd/regress/logsrvd_conf/sudo_logsrvd.conf.1.in252
-rw-r--r--logsrvd/regress/logsrvd_conf/sudo_logsrvd.conf.2.in252
-rw-r--r--logsrvd/regress/logsrvd_conf/tls/sudo_logsrvd.conf.1.in252
-rw-r--r--logsrvd/regress/logsrvd_conf/tls/sudo_logsrvd.conf.2.in252
-rw-r--r--logsrvd/sendlog.c1881
-rw-r--r--logsrvd/sendlog.h82
-rw-r--r--logsrvd/tls_client.c251
-rw-r--r--logsrvd/tls_common.h52
-rw-r--r--logsrvd/tls_init.c383
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 */