diff options
Diffstat (limited to '')
-rw-r--r-- | logsrvd/sendlog.c | 1893 |
1 files changed, 1893 insertions, 0 deletions
diff --git a/logsrvd/sendlog.c b/logsrvd/sendlog.c new file mode 100644 index 0000000..2912d9e --- /dev/null +++ b/logsrvd/sendlog.c @@ -0,0 +1,1893 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2019-2020 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "config.h" + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <netdb.h> +#ifdef HAVE_STDBOOL_H +# include <stdbool.h> +#else +# include "compat/stdbool.h" +#endif /* HAVE_STDBOOL_H */ +#if defined(HAVE_STDINT_H) +# include <stdint.h> +#elif defined(HAVE_INTTYPES_H) +# include <inttypes.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#ifndef HAVE_GETADDRINFO +# include "compat/getaddrinfo.h" +#endif +#ifdef HAVE_GETOPT_LONG +# include <getopt.h> +# else +# include "compat/getopt.h" +#endif /* HAVE_GETOPT_LONG */ + +#if defined(HAVE_OPENSSL) +# include <openssl/ssl.h> +# include <openssl/err.h> +#endif + +#include "sudo_compat.h" +#include "sudo_conf.h" +#include "sudo_debug.h" +#include "sudo_event.h" +#include "sudo_eventlog.h" +#include "sudo_fatal.h" +#include "sudo_gettext.h" +#include "sudo_iolog.h" +#include "sudo_util.h" + +#include "hostcheck.h" +#include "log_server.pb-c.h" +#include "sendlog.h" + +#if defined(HAVE_OPENSSL) +# define TLS_HANDSHAKE_TIMEO_SEC 10 +#endif + +TAILQ_HEAD(connection_list, client_closure); +static struct connection_list connections = TAILQ_HEAD_INITIALIZER(connections); + +static const char *server_name = "localhost"; +#if defined(HAVE_STRUCT_IN6_ADDR) +static char server_ip[INET6_ADDRSTRLEN]; +#else +static char server_ip[INET_ADDRSTRLEN]; +#endif +static char *iolog_dir; +static bool testrun = false; +static int nr_of_conns = 1; +static int finished_transmissions = 0; + +#if defined(HAVE_OPENSSL) +static SSL_CTX *ssl_ctx = NULL; +static const char *ca_bundle = NULL; +static const char *cert = NULL; +static const char *key = NULL; +static bool verify_server = true; +#endif + +/* Server callback may redirect to client callback for TLS. */ +static void client_msg_cb(int fd, int what, void *v); +static void server_msg_cb(int fd, int what, void *v); + +static void +usage(bool fatal) +{ +#if defined(HAVE_OPENSSL) + fprintf(stderr, "usage: %s [-AnV] [-b ca_bundle] [-c cert_file] [-h host] " + "[-i iolog-id] [-k key_file] [-p port] " +#else + fprintf(stderr, "usage: %s [-AnV] [-h host] [-i iolog-id] [-p port] " +#endif + "[-r restart-point] [-R reject-reason] [-t number] /path/to/iolog\n", + getprogname()); + if (fatal) + exit(EXIT_FAILURE); +} + +static void +help(void) +{ + (void)printf(_("%s - send sudo I/O log to remote server\n\n"), + getprogname()); + usage(false); + (void)puts(_("\nOptions:\n" + " --help display help message and exit\n" + " -A, --accept only send an accept event (no I/O)\n" + " -h, --host host to send logs to\n" + " -i, --iolog_id remote ID of I/O log to be resumed\n" + " -p, --port port to use when connecting to host\n" + " -r, --restart restart previous I/O log transfer\n" + " -R, --reject reject the command with the given reason\n" +#if defined(HAVE_OPENSSL) + " -b, --ca-bundle certificate bundle file to verify server's cert against\n" + " -c, --cert certificate file for TLS handshake\n" + " -k, --key private key file\n" + " -n, --no-verify do not verify server certificate\n" +#endif + " -t, --test test audit server by sending selected I/O log n times in parallel\n" + " -V, --version display version information and exit\n")); + exit(EXIT_SUCCESS); +} + +/* + * Connect to specified host:port + * If host has multiple addresses, the first one that connects is used. + * Returns open socket or -1 on error. + */ +static int +connect_server(const char *host, const char *port) +{ + struct addrinfo hints, *res, *res0; + const char *addr, *cause = "getaddrinfo"; + int error, sock, save_errno; + debug_decl(connect_server, SUDO_DEBUG_UTIL); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + error = getaddrinfo(host, port, &hints, &res0); + if (error != 0) { + sudo_warnx(U_("unable to look up %s:%s: %s"), host, port, + gai_strerror(error)); + debug_return_int(-1); + } + + sock = -1; + for (res = res0; res; res = res->ai_next) { + sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (sock == -1) { + cause = "socket"; + continue; + } + if (connect(sock, res->ai_addr, res->ai_addrlen) == -1) { + cause = "connect"; + save_errno = errno; + close(sock); + errno = save_errno; + sock = -1; + continue; + } + if (*server_ip == '\0') { + switch (res->ai_family) { + case AF_INET: + addr = (char *)&((struct sockaddr_in *)res->ai_addr)->sin_addr; + break; + case AF_INET6: + addr = (char *)&((struct sockaddr_in6 *)res->ai_addr)->sin6_addr; + break; + default: + cause = "ai_family"; + save_errno = EAFNOSUPPORT; + close(sock); + errno = save_errno; + sock = -1; + continue; + } + if (inet_ntop(res->ai_family, addr, server_ip, + sizeof(server_ip)) == NULL) { + sudo_warnx("%s", U_("unable to get server IP addr")); + } + } + break; /* success */ + } + freeaddrinfo(res0); + + if (sock != -1) { + int flags = fcntl(sock, F_GETFL, 0); + if (flags == -1 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) { + cause = "fcntl(O_NONBLOCK)"; + save_errno = errno; + close(sock); + errno = save_errno; + sock = -1; + } + } + if (sock == -1) + sudo_warn("%s", cause); + + debug_return_int(sock); +} + +/* + * Read the next I/O buffer as described by closure->timing. + */ +static bool +read_io_buf(struct client_closure *closure) +{ + struct timing_closure *timing = &closure->timing; + const char *errstr = NULL; + size_t nread; + debug_decl(read_io_buf, SUDO_DEBUG_UTIL); + + if (!closure->iolog_files[timing->event].enabled) { + errno = ENOENT; + sudo_warn("%s/%s", iolog_dir, iolog_fd_to_name(timing->event)); + debug_return_bool(false); + } + + /* Expand buf as needed. */ + if (timing->u.nbytes > closure->bufsize) { + free(closure->buf); + closure->bufsize = sudo_pow2_roundup(timing->u.nbytes); + if ((closure->buf = malloc(closure->bufsize)) == NULL) { + sudo_warn(NULL); + timing->u.nbytes = 0; + debug_return_bool(false); + } + } + + nread = iolog_read(&closure->iolog_files[timing->event], closure->buf, + timing->u.nbytes, &errstr); + if (nread != timing->u.nbytes) { + sudo_warnx(U_("unable to read %s/%s: %s"), iolog_dir, + iolog_fd_to_name(timing->event), errstr); + debug_return_bool(false); + } + debug_return_bool(true); +} + +/* + * Format a ClientMessage and store the wire format message in buf. + * Returns true on success, false on failure. + */ +static bool +fmt_client_message(struct connection_buffer *buf, ClientMessage *msg) +{ + uint32_t msg_len; + bool ret = false; + size_t len; + debug_decl(fmt_client_message, SUDO_DEBUG_UTIL); + + len = client_message__get_packed_size(msg); + if (len > MESSAGE_SIZE_MAX) { + sudo_warnx(U_("client message too large: %zu"), len); + goto done; + } + /* Wire message size is used for length encoding, precedes message. */ + msg_len = htonl((uint32_t)len); + len += sizeof(msg_len); + + /* Resize buffer as needed. */ + if (len > buf->size) { + free(buf->data); + buf->size = sudo_pow2_roundup(len); + if ((buf->data = malloc(buf->size)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to malloc %u", buf->size); + buf->size = 0; + goto done; + } + } + + memcpy(buf->data, &msg_len, sizeof(msg_len)); + client_message__pack(msg, buf->data + sizeof(msg_len)); + buf->len = len; + ret = true; + +done: + debug_return_bool(ret); +} + +/* + * Split command + args into an array of strings. + * Returns an array containing command and args, reusing space in "command". + * Note that the returned array does not end with a terminating NULL. + */ +static char ** +split_command(char *command, size_t *lenp) +{ + char *cp; + char **args; + size_t len; + debug_decl(split_command, SUDO_DEBUG_UTIL); + + for (cp = command, len = 0;;) { + len++; + if ((cp = strchr(cp, ' ')) == NULL) + break; + cp++; + } + args = reallocarray(NULL, len, sizeof(char *)); + if (args == NULL) + debug_return_ptr(NULL); + + for (cp = command, len = 0;;) { + args[len++] = cp; + if ((cp = strchr(cp, ' ')) == NULL) + break; + *cp++ = '\0'; + } + + *lenp = len; + debug_return_ptr(args); +} + +static bool +fmt_client_hello(struct client_closure *closure) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + ClientHello hello_msg = CLIENT_HELLO__INIT; + bool ret = false; + debug_decl(fmt_client_hello, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending ClientHello", __func__); + hello_msg.client_id = "Sudo Sendlog " PACKAGE_VERSION; + + /* Schedule ClientMessage */ + client_msg.u.hello_msg = &hello_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_HELLO_MSG; + ret = fmt_client_message(&closure->write_buf, &client_msg); + if (ret) { + if (sudo_ev_add(closure->evbase, closure->read_ev, NULL, false) == -1) + ret = false; + if (sudo_ev_add(closure->evbase, closure->write_ev, NULL, false) == -1) + ret = false; + } + + debug_return_bool(ret); +} + +static void +free_info_messages(InfoMessage **info_msgs, size_t n_info_msgs) +{ + debug_decl(free_info_messages, SUDO_DEBUG_UTIL); + + if (info_msgs != NULL) { + while (n_info_msgs-- > 0) { + if (info_msgs[n_info_msgs]->value_case == INFO_MESSAGE__VALUE_STRLISTVAL) { + /* Only strlistval was dynamically allocated */ + free(info_msgs[n_info_msgs]->u.strlistval->strings); + free(info_msgs[n_info_msgs]->u.strlistval); + } + free(info_msgs[n_info_msgs]); + } + free(info_msgs); + } + + debug_return; +} + +static InfoMessage ** +fmt_info_messages(const struct eventlog *evlog, char *hostname, + size_t *n_info_msgs) +{ + InfoMessage **info_msgs = NULL; + InfoMessage__StringList *runargv = NULL; + size_t info_msgs_size, n = 0; + debug_decl(fmt_info_messages, SUDO_DEBUG_UTIL); + + /* Split command into a StringList. */ + runargv = malloc(sizeof(*runargv)); + if (runargv == NULL) + goto oom; + info_message__string_list__init(runargv); + runargv->strings = split_command(evlog->command, &runargv->n_strings); + if (runargv->strings == NULL) + goto oom; + + /* The sudo I/O log info file has limited info. */ + info_msgs_size = 10; + info_msgs = calloc(info_msgs_size, sizeof(InfoMessage *)); + if (info_msgs == NULL) + goto oom; + for (n = 0; n < info_msgs_size; n++) { + info_msgs[n] = malloc(sizeof(InfoMessage)); + if (info_msgs[n] == NULL) + goto oom; + info_message__init(info_msgs[n]); + } + + /* Fill in info_msgs */ + n = 0; + info_msgs[n]->key = "command"; + info_msgs[n]->u.strval = evlog->command; + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + info_msgs[n]->key = "columns"; + info_msgs[n]->u.numval = evlog->columns; + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_NUMVAL; + n++; + + info_msgs[n]->key = "lines"; + info_msgs[n]->u.numval = evlog->lines; + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_NUMVAL; + n++; + + info_msgs[n]->key = "runargv"; + info_msgs[n]->u.strlistval = runargv; + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRLISTVAL; + runargv = NULL; + n++; + + if (evlog->rungroup != NULL) { + info_msgs[n]->key = "rungroup"; + info_msgs[n]->u.strval = evlog->rungroup; + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + } + + info_msgs[n]->key = "runuser"; + info_msgs[n]->u.strval = evlog->runuser; + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + info_msgs[n]->key = "submitcwd"; + info_msgs[n]->u.strval = evlog->cwd; + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + info_msgs[n]->key = "submithost"; + info_msgs[n]->u.strval = hostname; + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + info_msgs[n]->key = "submituser"; + info_msgs[n]->u.strval = evlog->submituser; + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + info_msgs[n]->key = "ttyname"; + info_msgs[n]->u.strval = evlog->ttyname; + info_msgs[n]->value_case = INFO_MESSAGE__VALUE_STRVAL; + n++; + + /* Update n_info_msgs. */ + *n_info_msgs = n; + + /* Avoid leaking unused info_msg structs. */ + while (n < info_msgs_size) { + free(info_msgs[n++]); + } + + debug_return_ptr(info_msgs); + +oom: + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + free_info_messages(info_msgs, n); + if (runargv != NULL) { + free(runargv->strings); + free(runargv); + } + *n_info_msgs = 0; + debug_return_ptr(NULL); +} + +/* + * Build and format a RejectMessage wrapped in a ClientMessage. + * Stores the wire format message in the closure's write buffer. + * Returns true on success, false on failure. + */ +static bool +fmt_reject_message(struct client_closure *closure) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + RejectMessage reject_msg = REJECT_MESSAGE__INIT; + TimeSpec tv = TIME_SPEC__INIT; + size_t n_info_msgs; + bool ret = false; + char *hostname; + debug_decl(fmt_reject_message, SUDO_DEBUG_UTIL); + + /* + * Fill in RejectMessage and add it to ClientMessage. + */ + if ((hostname = sudo_gethostname()) == NULL) { + sudo_warn("gethostname"); + debug_return_bool(false); + } + + /* Sudo I/O logs only store start time in seconds. */ + tv.tv_sec = closure->evlog->submit_time.tv_sec; + tv.tv_nsec = closure->evlog->submit_time.tv_nsec; + reject_msg.submit_time = &tv; + + /* Why the command was rejected. */ + reject_msg.reason = closure->reject_reason; + + reject_msg.info_msgs = fmt_info_messages(closure->evlog, hostname, + &n_info_msgs); + if (reject_msg.info_msgs == NULL) + goto done; + + /* Update n_info_msgs. */ + reject_msg.n_info_msgs = n_info_msgs; + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending RejectMessage, array length %zu", __func__, n_info_msgs); + + /* Schedule ClientMessage */ + client_msg.u.reject_msg = &reject_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_REJECT_MSG; + ret = fmt_client_message(&closure->write_buf, &client_msg); + if (ret) { + if (sudo_ev_add(closure->evbase, closure->write_ev, NULL, false) == -1) + ret = false; + } + +done: + free_info_messages(reject_msg.info_msgs, n_info_msgs); + free(hostname); + + debug_return_bool(ret); +} + +/* + * Build and format an AcceptMessage wrapped in a ClientMessage. + * Stores the wire format message in the closure's write buffer. + * Returns true on success, false on failure. + */ +static bool +fmt_accept_message(struct client_closure *closure) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + AcceptMessage accept_msg = ACCEPT_MESSAGE__INIT; + TimeSpec tv = TIME_SPEC__INIT; + size_t n_info_msgs; + bool ret = false; + char *hostname; + debug_decl(fmt_accept_message, SUDO_DEBUG_UTIL); + + /* + * Fill in AcceptMessage and add it to ClientMessage. + */ + if ((hostname = sudo_gethostname()) == NULL) { + sudo_warn("gethostname"); + debug_return_bool(false); + } + + /* Sudo I/O logs only store start time in seconds. */ + tv.tv_sec = closure->evlog->submit_time.tv_sec; + tv.tv_nsec = closure->evlog->submit_time.tv_nsec; + accept_msg.submit_time = &tv; + + /* Client will send IoBuffer messages. */ + accept_msg.expect_iobufs = !closure->accept_only; + + accept_msg.info_msgs = fmt_info_messages(closure->evlog, hostname, + &n_info_msgs); + if (accept_msg.info_msgs == NULL) + goto done; + + /* Update n_info_msgs. */ + accept_msg.n_info_msgs = n_info_msgs; + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending AcceptMessage, array length %zu", __func__, n_info_msgs); + + /* Schedule ClientMessage */ + client_msg.u.accept_msg = &accept_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_ACCEPT_MSG; + ret = fmt_client_message(&closure->write_buf, &client_msg); + if (ret) { + if (sudo_ev_add(closure->evbase, closure->write_ev, NULL, false) == -1) + ret = false; + } + +done: + free_info_messages(accept_msg.info_msgs, n_info_msgs); + free(hostname); + + debug_return_bool(ret); +} + +/* + * Build and format a RestartMessage wrapped in a ClientMessage. + * Stores the wire format message in the closure's write buffer. + * Returns true on success, false on failure. + */ +static bool +fmt_restart_message(struct client_closure *closure) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + RestartMessage restart_msg = RESTART_MESSAGE__INIT; + TimeSpec tv = TIME_SPEC__INIT; + bool ret = false; + debug_decl(fmt_restart_message, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending RestartMessage, [%lld, %ld]", __func__, + (long long)closure->restart.tv_sec, closure->restart.tv_nsec); + + tv.tv_sec = closure->restart.tv_sec; + tv.tv_nsec = closure->restart.tv_nsec; + restart_msg.resume_point = &tv; + restart_msg.log_id = (char *)closure->iolog_id; + + /* Schedule ClientMessage */ + client_msg.u.restart_msg = &restart_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_RESTART_MSG; + ret = fmt_client_message(&closure->write_buf, &client_msg); + if (ret) { + if (sudo_ev_add(closure->evbase, closure->write_ev, NULL, false) == -1) + ret = false; + } + + debug_return_bool(ret); +} + +/* + * Build and format an ExitMessage wrapped in a ClientMessage. + * Stores the wire format message in the closure's write buffer. + * Returns true on success, false on failure. + */ +static bool +fmt_exit_message(struct client_closure *closure) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + ExitMessage exit_msg = EXIT_MESSAGE__INIT; + bool ret = false; + debug_decl(fmt_exit_message, SUDO_DEBUG_UTIL); + + /* + * We don't have enough data in a sudo I/O log to create a real + * exit message. For example, the exit value and run time are + * not known. This results in a zero-sized message. + */ + exit_msg.exit_value = 0; + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending ExitMessage, exit value %d", + __func__, exit_msg.exit_value); + + /* Send ClientMessage */ + client_msg.u.exit_msg = &exit_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_EXIT_MSG; + if (!fmt_client_message(&closure->write_buf, &client_msg)) + goto done; + + ret = true; + +done: + debug_return_bool(ret); +} + +/* + * Build and format an IoBuffer wrapped in a ClientMessage. + * Stores the wire format message in buf. + * Returns true on success, false on failure. + */ +static bool +fmt_io_buf(int type, struct client_closure *closure, + struct connection_buffer *buf) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + IoBuffer iobuf_msg = IO_BUFFER__INIT; + TimeSpec delay = TIME_SPEC__INIT; + bool ret = false; + debug_decl(fmt_io_buf, SUDO_DEBUG_UTIL); + + if (!read_io_buf(closure)) + goto done; + + /* Fill in IoBuffer. */ + /* TODO: split buffer if it is too large */ + delay.tv_sec = closure->timing.delay.tv_sec; + delay.tv_nsec = closure->timing.delay.tv_nsec; + iobuf_msg.delay = &delay; + iobuf_msg.data.data = (void *)closure->buf; + iobuf_msg.data.len = closure->timing.u.nbytes; + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending IoBuffer length %zu, type %d, size %zu", __func__, + iobuf_msg.data.len, type, io_buffer__get_packed_size(&iobuf_msg)); + + /* Send ClientMessage, it doesn't matter which IoBuffer we set. */ + client_msg.u.ttyout_buf = &iobuf_msg; + client_msg.type_case = type; + if (!fmt_client_message(buf, &client_msg)) + goto done; + + ret = true; + +done: + debug_return_bool(ret); +} + +/* + * Build and format a ChangeWindowSize message wrapped in a ClientMessage. + * Stores the wire format message in buf. + * Returns true on success, false on failure. + */ +static bool +fmt_winsize(struct client_closure *closure, struct connection_buffer *buf) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + ChangeWindowSize winsize_msg = CHANGE_WINDOW_SIZE__INIT; + TimeSpec delay = TIME_SPEC__INIT; + struct timing_closure *timing = &closure->timing; + bool ret = false; + debug_decl(fmt_winsize, SUDO_DEBUG_UTIL); + + /* Fill in ChangeWindowSize message. */ + delay.tv_sec = timing->delay.tv_sec; + delay.tv_nsec = timing->delay.tv_nsec; + winsize_msg.delay = &delay; + winsize_msg.rows = timing->u.winsize.lines; + winsize_msg.cols = timing->u.winsize.cols; + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending ChangeWindowSize, %dx%d", + __func__, winsize_msg.rows, winsize_msg.cols); + + /* Send ClientMessage */ + client_msg.u.winsize_event = &winsize_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_WINSIZE_EVENT; + if (!fmt_client_message(buf, &client_msg)) + goto done; + + ret = true; + +done: + debug_return_bool(ret); +} + +/* + * Build and format a CommandSuspend message wrapped in a ClientMessage. + * Stores the wire format message in buf. + * Returns true on success, false on failure. + */ +static bool +fmt_suspend(struct client_closure *closure, struct connection_buffer *buf) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + CommandSuspend suspend_msg = COMMAND_SUSPEND__INIT; + TimeSpec delay = TIME_SPEC__INIT; + struct timing_closure *timing = &closure->timing; + bool ret = false; + debug_decl(fmt_suspend, SUDO_DEBUG_UTIL); + + /* Fill in CommandSuspend message. */ + delay.tv_sec = timing->delay.tv_sec; + delay.tv_nsec = timing->delay.tv_nsec; + suspend_msg.delay = &delay; + if (sig2str(timing->u.signo, closure->buf) == -1) + goto done; + suspend_msg.signal = closure->buf; + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending CommandSuspend, SIG%s", __func__, suspend_msg.signal); + + /* Send ClientMessage */ + client_msg.u.suspend_event = &suspend_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_SUSPEND_EVENT; + if (!fmt_client_message(buf, &client_msg)) + goto done; + + ret = true; + +done: + debug_return_bool(ret); +} + +/* + * Read the next entry for the I/O log timing file and format a ClientMessage. + * Stores the wire format message in the closure's write buffer. + * Returns true on success, false on failure. + */ +static bool +fmt_next_iolog(struct client_closure *closure) +{ + struct timing_closure *timing = &closure->timing; + struct connection_buffer *buf = &closure->write_buf; + bool ret = false; + debug_decl(fmt_next_iolog, SUDO_DEBUG_UTIL); + + if (buf->len != 0) { + sudo_warnx(U_("%s: write buffer already in use"), __func__); + debug_return_bool(false); + } + + /* TODO: fill write buffer with multiple messages */ +again: + switch (iolog_read_timing_record(&closure->iolog_files[IOFD_TIMING], timing)) { + case 0: + /* OK */ + break; + case 1: + /* no more IO buffers */ + closure->state = SEND_EXIT; + debug_return_bool(fmt_exit_message(closure)); + case -1: + default: + debug_return_bool(false); + } + + /* Track elapsed time for comparison with commit points. */ + sudo_timespecadd(&timing->delay, &closure->elapsed, &closure->elapsed); + + /* If we have a restart point, ignore records until we hit it. */ + if (sudo_timespecisset(&closure->restart)) { + if (sudo_timespeccmp(&closure->restart, &closure->elapsed, >=)) + goto again; + sudo_timespecclear(&closure->restart); /* caught up */ + } + + switch (timing->event) { + case IO_EVENT_STDIN: + ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDIN_BUF, closure, buf); + break; + case IO_EVENT_STDOUT: + ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDOUT_BUF, closure, buf); + break; + case IO_EVENT_STDERR: + ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDERR_BUF, closure, buf); + break; + case IO_EVENT_TTYIN: + ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_TTYIN_BUF, closure, buf); + break; + case IO_EVENT_TTYOUT: + ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_TTYOUT_BUF, closure, buf); + break; + case IO_EVENT_WINSIZE: + ret = fmt_winsize(closure, buf); + break; + case IO_EVENT_SUSPEND: + ret = fmt_suspend(closure, buf); + break; + default: + sudo_warnx(U_("unexpected I/O event %d"), timing->event); + break; + } + + debug_return_bool(ret); +} + +/* + * Additional work to do after a ClientMessage was sent to the server. + * Advances state and formats the next ClientMessage (if any). + */ +static bool +client_message_completion(struct client_closure *closure) +{ + debug_decl(client_message_completion, SUDO_DEBUG_UTIL); + + switch (closure->state) { + case RECV_HELLO: + /* Wait for ServerHello, nothing to write until then. */ + sudo_ev_del(closure->evbase, closure->write_ev); + break; + case SEND_ACCEPT: + if (closure->accept_only) { + closure->state = SEND_EXIT; + debug_return_bool(fmt_exit_message(closure)); + } + FALLTHROUGH; + case SEND_RESTART: + closure->state = SEND_IO; + FALLTHROUGH; + case SEND_IO: + /* fmt_next_iolog() will advance state on EOF. */ + if (!fmt_next_iolog(closure)) + debug_return_bool(false); + break; + case SEND_REJECT: + /* Done writing, wait for server to close connection. */ + sudo_ev_del(closure->evbase, closure->write_ev); + closure->state = FINISHED; + break; + case SEND_EXIT: + /* Done writing, wait for final commit point if sending I/O. */ + sudo_ev_del(closure->evbase, closure->write_ev); + closure->state = closure->accept_only ? FINISHED : CLOSING; + break; + default: + sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state); + debug_return_bool(false); + } + debug_return_bool(true); +} + +/* + * Respond to a ServerHello message from the server. + * Returns true on success, false on error. + */ +static bool +handle_server_hello(ServerHello *msg, struct client_closure *closure) +{ + size_t n; + debug_decl(handle_server_hello, SUDO_DEBUG_UTIL); + + if (closure->state != RECV_HELLO) { + sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state); + debug_return_bool(false); + } + + /* Check that ServerHello is valid. */ + if (msg->server_id == NULL || msg->server_id[0] == '\0') { + sudo_warnx("%s", U_("invalid ServerHello")); + debug_return_bool(false); + } + + if (!testrun) { + printf("Server ID: %s\n", msg->server_id); + /* TODO: handle redirect */ + if (msg->redirect != NULL && msg->redirect[0] != '\0') + printf("Redirect: %s\n", msg->redirect); + for (n = 0; n < msg->n_servers; n++) { + printf("Server %zu: %s\n", n + 1, msg->servers[n]); + } + } + + debug_return_bool(true); +} + +/* + * Respond to a CommitPoint message from the server. + * Returns true on success, false on error. + */ +static bool +handle_commit_point(TimeSpec *commit_point, struct client_closure *closure) +{ + debug_decl(handle_commit_point, SUDO_DEBUG_UTIL); + + /* Only valid after we have sent an IO buffer. */ + if (closure->state < SEND_IO) { + sudo_warnx(U_("%s: unexpected state %d"), __func__, closure->state); + debug_return_bool(false); + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: commit point: [%lld, %d]", + __func__, (long long)commit_point->tv_sec, commit_point->tv_nsec); + closure->committed.tv_sec = commit_point->tv_sec; + closure->committed.tv_nsec = commit_point->tv_nsec; + + debug_return_bool(true); +} + +/* + * Respond to a LogId message from the server. + * Always returns true. + */ +static bool +handle_log_id(char *id, struct client_closure *closure) +{ + debug_decl(handle_log_id, SUDO_DEBUG_UTIL); + + if (!testrun) + printf("Remote log ID: %s\n", id); + + debug_return_bool(true); +} + +/* + * Respond to a ServerError message from the server. + * Always returns false. + */ +static bool +handle_server_error(char *errmsg, struct client_closure *closure) +{ + debug_decl(handle_server_error, SUDO_DEBUG_UTIL); + + sudo_warnx(U_("error message received from server: %s"), errmsg); + debug_return_bool(false); +} + +/* + * Respond to a ServerAbort message from the server. + * Always returns false. + */ +static bool +handle_server_abort(char *errmsg, struct client_closure *closure) +{ + debug_decl(handle_server_abort, SUDO_DEBUG_UTIL); + + sudo_warnx(U_("abort message received from server: %s"), errmsg); + debug_return_bool(false); +} + +/* + * Respond to a ServerMessage from the server. + * Returns true on success, false on error. + */ +static bool +handle_server_message(uint8_t *buf, size_t len, + struct client_closure *closure) +{ + ServerMessage *msg; + bool ret = false; + debug_decl(handle_server_message, SUDO_DEBUG_UTIL); + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: unpacking ServerMessage", __func__); + msg = server_message__unpack(NULL, len, buf); + if (msg == NULL) { + sudo_warnx("%s", U_("unable to unpack ServerMessage")); + debug_return_bool(false); + } + + switch (msg->type_case) { + case SERVER_MESSAGE__TYPE_HELLO: + if ((ret = handle_server_hello(msg->u.hello, closure))) { + if (sudo_timespecisset(&closure->restart)) { + closure->state = SEND_RESTART; + ret = fmt_restart_message(closure); + } else if (closure->reject_reason != NULL) { + closure->state = SEND_REJECT; + ret = fmt_reject_message(closure); + } else { + closure->state = SEND_ACCEPT; + ret = fmt_accept_message(closure); + } + } + break; + case SERVER_MESSAGE__TYPE_COMMIT_POINT: + ret = handle_commit_point(msg->u.commit_point, closure); + if (sudo_timespeccmp(&closure->elapsed, &closure->committed, ==)) { + sudo_ev_del(closure->evbase, closure->read_ev); + closure->state = FINISHED; + if (++finished_transmissions == nr_of_conns) + sudo_ev_loopexit(closure->evbase); + } + break; + case SERVER_MESSAGE__TYPE_LOG_ID: + ret = handle_log_id(msg->u.log_id, closure); + break; + case SERVER_MESSAGE__TYPE_ERROR: + ret = handle_server_error(msg->u.error, closure); + closure->state = ERROR; + break; + case SERVER_MESSAGE__TYPE_ABORT: + ret = handle_server_abort(msg->u.abort, closure); + closure->state = ERROR; + break; + default: + sudo_warnx(U_("%s: unexpected type_case value %d"), + __func__, msg->type_case); + break; + } + + server_message__free_unpacked(msg, NULL); + debug_return_bool(ret); +} + +/* + * Read and unpack a ServerMessage (read callback). + */ +static void +server_msg_cb(int fd, int what, void *v) +{ + struct client_closure *closure = v; + struct connection_buffer *buf = &closure->read_buf; + ssize_t nread; + uint32_t msg_len; + debug_decl(server_msg_cb, SUDO_DEBUG_UTIL); + + /* For TLS we may need to read as part of SSL_write(). */ + if (closure->write_instead_of_read) { + closure->write_instead_of_read = false; + client_msg_cb(fd, what, v); + debug_return; + } + + if (what == SUDO_EV_TIMEOUT) { + sudo_warnx("%s", U_("timeout reading from server")); + goto bad; + } + +#if defined(HAVE_OPENSSL) + if (cert != NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: reading ServerMessage (TLS)", __func__); + nread = SSL_read(closure->ssl, buf->data + buf->len, buf->size - buf->len); + if (nread <= 0) { + const char *errstr; + int err; + + switch (SSL_get_error(closure->ssl, nread)) { + case SSL_ERROR_ZERO_RETURN: + /* ssl connection shutdown cleanly */ + nread = 0; + break; + case SSL_ERROR_WANT_READ: + /* ssl wants to read more, read event is always active */ + sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, + "SSL_read returns SSL_ERROR_WANT_READ"); + debug_return; + case SSL_ERROR_WANT_WRITE: + /* ssl wants to write, schedule a write if not pending */ + sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, + "SSL_read returns SSL_ERROR_WANT_WRITE"); + if (!sudo_ev_pending(closure->write_ev, SUDO_EV_WRITE, NULL)) { + /* Enable a temporary write event. */ + if (sudo_ev_add(closure->evbase, closure->write_ev, NULL, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto bad; + } + closure->temporary_write_event = true; + } + /* Redirect write event to finish SSL_read() */ + closure->read_instead_of_write = true; + debug_return; + case SSL_ERROR_SSL: + /* + * For TLS 1.3, if the cert verify function on the server + * returns an error, OpenSSL will send an internal error + * alert when we read ServerHello. Convert to a more useful + * message and hope that no actual internal error occurs. + */ + err = ERR_get_error(); + if (closure->state == RECV_HELLO && + ERR_GET_REASON(err) == SSL_R_TLSV1_ALERT_INTERNAL_ERROR) { + errstr = "host name does not match certificate"; + } else { + errstr = ERR_reason_error_string(err); + } + sudo_warnx("%s", errstr); + goto bad; + case SSL_ERROR_SYSCALL: + sudo_warn("recv"); + goto bad; + default: + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("recv: %s", errstr); + goto bad; + } + } + } else +#endif + { + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: reading ServerMessage", __func__); + nread = recv(fd, buf->data + buf->len, buf->size - buf->len, 0); + } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received %zd bytes from server", + __func__, nread); + switch (nread) { + case -1: + if (errno == EAGAIN) + debug_return; + sudo_warn("recv"); + goto bad; + case 0: + if (closure->state != FINISHED) + sudo_warnx("%s", U_("premature EOF")); + goto bad; + default: + break; + } + buf->len += nread; + + while (buf->len - buf->off >= sizeof(msg_len)) { + /* Read wire message size (uint32_t in network byte order). */ + memcpy(&msg_len, buf->data + buf->off, sizeof(msg_len)); + msg_len = ntohl(msg_len); + + if (msg_len > MESSAGE_SIZE_MAX) { + sudo_warnx(U_("server message too large: %u"), msg_len); + goto bad; + } + + if (msg_len + sizeof(msg_len) > buf->len - buf->off) { + /* Incomplete message, we'll read the rest next time. */ + if (!expand_buf(buf, msg_len + sizeof(msg_len))) + goto bad; + debug_return; + } + + /* Parse ServerMessage, could be zero bytes. */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: parsing ServerMessage, size %u", __func__, msg_len); + buf->off += sizeof(msg_len); + if (!handle_server_message(buf->data + buf->off, msg_len, closure)) + goto bad; + buf->off += msg_len; + } + buf->len -= buf->off; + buf->off = 0; + debug_return; +bad: + sudo_ev_del(closure->evbase, closure->read_ev); + debug_return; +} + +/* + * Send a ClientMessage to the server (write callback). + */ +static void +client_msg_cb(int fd, int what, void *v) +{ + struct client_closure *closure = v; + struct connection_buffer *buf = &closure->write_buf; + ssize_t nwritten; + debug_decl(client_msg_cb, SUDO_DEBUG_UTIL); + + /* For TLS we may need to write as part of SSL_read(). */ + if (closure->read_instead_of_write) { + closure->read_instead_of_write = false; + /* Delete write event if it was only due to SSL_read(). */ + if (closure->temporary_write_event) { + closure->temporary_write_event = false; + sudo_ev_del(closure->evbase, closure->write_ev); + } + server_msg_cb(fd, what, v); + debug_return; + } + + if (what == SUDO_EV_TIMEOUT) { + sudo_warnx("%s", U_("timeout writing to server")); + goto bad; + } + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending %u bytes to server", __func__, buf->len - buf->off); + +#if defined(HAVE_OPENSSL) + if (cert != NULL) { + nwritten = SSL_write(closure->ssl, buf->data + buf->off, buf->len - buf->off); + if (nwritten <= 0) { + const char *errstr; + + switch (SSL_get_error(closure->ssl, nwritten)) { + case SSL_ERROR_ZERO_RETURN: + /* ssl connection shutdown */ + goto bad; + case SSL_ERROR_WANT_READ: + /* ssl wants to read, read event always active */ + sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, + "SSL_write returns SSL_ERROR_WANT_READ"); + /* Redirect read event to finish SSL_write() */ + closure->write_instead_of_read = true; + debug_return; + case SSL_ERROR_WANT_WRITE: + /* ssl wants to write more, write event remains active */ + sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, + "SSL_write returns SSL_ERROR_WANT_WRITE"); + debug_return; + case SSL_ERROR_SYSCALL: + sudo_warn("recv"); + goto bad; + default: + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx("send: %s", errstr); + goto bad; + } + } + } else +#endif + { + nwritten = send(fd, buf->data + buf->off, buf->len - buf->off, 0); + } + if (nwritten == -1) { + sudo_warn("send"); + goto bad; + } + buf->off += nwritten; + + if (buf->off == buf->len) { + /* sent entire message */ + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: finished sending %u bytes to server", __func__, buf->len); + buf->off = 0; + buf->len = 0; + if (!client_message_completion(closure)) + goto bad; + } + debug_return; + +bad: + sudo_ev_del(closure->evbase, closure->read_ev); + sudo_ev_del(closure->evbase, closure->write_ev); + debug_return; +} + +/* + * Parse a timespec on the command line of the form + * seconds[,nanoseconds] + */ +static bool +parse_timespec(struct timespec *ts, char *strval) +{ + const char *errstr; + char *nsecstr; + debug_decl(parse_timespec, SUDO_DEBUG_UTIL); + + if ((nsecstr = strchr(strval, ',')) != NULL) + *nsecstr++ = '\0'; + + ts->tv_nsec = 0; + ts->tv_sec = sudo_strtonum(strval, 0, TIME_T_MAX, &errstr); + if (errstr != NULL) { + sudo_warnx(U_("%s: %s"), strval, U_(errstr)); + debug_return_bool(false); + } + + if (nsecstr != NULL) { + ts->tv_nsec = sudo_strtonum(nsecstr, 0, LONG_MAX, &errstr); + if (errstr != NULL) { + sudo_warnx(U_("%s: %s"), nsecstr, U_(errstr)); + debug_return_bool(false); + } + } + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: parsed timespec [%lld, %ld]", + __func__, (long long)ts->tv_sec, ts->tv_nsec); + debug_return_bool(true); +} + +#if defined(HAVE_OPENSSL) +/* + * Check that the server's certificate is valid that it contains the + * server name or IP address. + * Returns 0 if the cert is invalid, else 1. + */ +static int +verify_peer_identity(int preverify_ok, X509_STORE_CTX *ctx) +{ + X509 *current_cert; + X509 *peer_cert; + debug_decl(verify_peer_identity, SUDO_DEBUG_UTIL); + + /* if pre-verification of the cert failed, just propagate that result back */ + if (preverify_ok != 1) { + debug_return_int(0); + } + + /* since this callback is called for each cert in the chain, + * check that current cert is the peer's certificate + */ + current_cert = X509_STORE_CTX_get_current_cert(ctx); + peer_cert = X509_STORE_CTX_get0_cert(ctx); + if (current_cert != peer_cert) { + debug_return_int(1); + } + + if (validate_hostname(peer_cert, server_name, server_ip, 0) == MatchFound) { + debug_return_int(1); + } + + debug_return_int(0); +} + +static SSL_CTX * +init_tls_client_context(const char *ca_bundle_file, const char *cert_file, const char *key_file) +{ + const SSL_METHOD *method; + SSL_CTX *ctx = NULL; + debug_decl(init_tls_client_context, SUDO_DEBUG_UTIL); + + SSL_library_init(); + OpenSSL_add_all_algorithms(); + SSL_load_error_strings(); + + if ((method = TLS_client_method()) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "creation of SSL_METHOD failed: %s", + ERR_error_string(ERR_get_error(), NULL)); + goto bad; + } + if ((ctx = SSL_CTX_new(method)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "creation of new SSL_CTX object failed: %s", + ERR_error_string(ERR_get_error(), NULL)); + goto bad; + } +#ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION + if (!SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to restrict min. protocol version: %s", + ERR_error_string(ERR_get_error(), NULL)); + goto bad; + } +#else + SSL_CTX_set_options(ctx, + SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3|SSL_OP_NO_TLSv1|SSL_OP_NO_TLSv1_1); +#endif + + if (cert_file) { + if (!SSL_CTX_use_certificate_chain_file(ctx, cert_file)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to load cert to the ssl context: %s", + ERR_error_string(ERR_get_error(), NULL)); + goto bad; + } + if (!SSL_CTX_use_PrivateKey_file(ctx, key_file, X509_FILETYPE_PEM)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to load key to the ssl context: %s", + ERR_error_string(ERR_get_error(), NULL)); + goto bad; + } + } + + if (ca_bundle_file != NULL) { + /* sets the location of the CA bundle file for verification purposes */ + if (SSL_CTX_load_verify_locations(ctx, ca_bundle_file, NULL) <= 0) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "calling SSL_CTX_load_verify_locations() failed: %s", + ERR_error_string(ERR_get_error(), NULL)); + goto bad; + } + } + + if (verify_server) { + /* verify server cert during the handshake */ + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_peer_identity); + } + + goto done; + +bad: + SSL_CTX_free(ctx); + +done: + debug_return_ptr(ctx); +} + +static void +tls_connect_cb(int sock, int what, void *v) +{ + struct client_closure *closure = v; + struct sudo_event_base *evbase = closure->evbase; + struct timespec timeo = { TLS_HANDSHAKE_TIMEO_SEC, 0 }; + const char *errstr; + int con_stat; + debug_decl(tls_connect_cb, SUDO_DEBUG_UTIL); + + if (what == SUDO_EV_TIMEOUT) { + sudo_warnx("%s", U_("TLS handshake timeout occurred")); + goto bad; + } + + con_stat = SSL_connect(closure->ssl); + + if (con_stat == 1) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "SSL_connect successful"); + closure->tls_connect_state = true; + } else { + switch (SSL_get_error(closure->ssl, con_stat)) { + /* TLS handshake is not finished, reschedule event */ + case SSL_ERROR_WANT_READ: + sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, + "SSL_connect returns SSL_ERROR_WANT_READ"); + if (what != SUDO_EV_READ) { + if (sudo_ev_set(closure->tls_connect_ev, closure->sock, + SUDO_EV_READ, tls_connect_cb, closure) == -1) { + sudo_warnx("%s", U_("unable to set event")); + goto bad; + } + } + if (sudo_ev_add(evbase, closure->tls_connect_ev, &timeo, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto bad; + } + break; + case SSL_ERROR_WANT_WRITE: + sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, + "SSL_connect returns SSL_ERROR_WANT_WRITE"); + if (what != SUDO_EV_WRITE) { + if (sudo_ev_set(closure->tls_connect_ev, closure->sock, + SUDO_EV_WRITE, tls_connect_cb, closure) == -1) { + sudo_warnx("%s", U_("unable to set event")); + goto bad; + } + } + if (sudo_ev_add(evbase, closure->tls_connect_ev, &timeo, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto bad; + } + break; + case SSL_ERROR_SYSCALL: + sudo_warnx(U_("TLS connection failed: %s"), strerror(errno)); + goto bad; + default: + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("TLS connection failed: %s"), errstr); + goto bad; + } + } + + if (closure->tls_connect_state) { + if (!testrun) { + printf("Negotiated protocol version: %s\n", SSL_get_version(closure->ssl)); + printf("Negotiated ciphersuite: %s\n", SSL_get_cipher(closure->ssl)); + } + + /* Done with TLS connect, send ClientHello */ + sudo_ev_free(closure->tls_connect_ev); + closure->tls_connect_ev = NULL; + if (!fmt_client_hello(closure)) + goto bad; + } + + debug_return; + +bad: + sudo_ev_loopbreak(evbase); + debug_return; +} + +static bool +tls_setup(struct client_closure *closure) +{ + const char *errstr; + debug_decl(tls_setup, SUDO_DEBUG_UTIL); + + if ((ssl_ctx = init_tls_client_context(ca_bundle, cert, key)) == NULL) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("Unable to initialize ssl context: %s"), errstr); + goto bad; + } + if ((closure->ssl = SSL_new(ssl_ctx)) == NULL) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("Unable to allocate ssl object: %s"), errstr); + goto bad; + } + if (SSL_set_fd(closure->ssl, closure->sock) <= 0) { + errstr = ERR_reason_error_string(ERR_get_error()); + sudo_warnx(U_("Unable to attach socket to the ssl object: %s"), + errstr); + goto bad; + } + + if (sudo_ev_add(closure->evbase, closure->tls_connect_ev, NULL, false) == -1) { + sudo_warnx("%s", U_("unable to add event to queue")); + goto bad; + } + + debug_return_bool(true); + +bad: + debug_return_bool(false); +} +#endif /* HAVE_OPENSSL */ + +/* + * Free client closure contents. + */ +static void +client_closure_free(struct client_closure *closure) +{ + debug_decl(connection_closure_free, SUDO_DEBUG_UTIL); + + if (closure != NULL) { + TAILQ_REMOVE(&connections, closure, entries); +#if defined(HAVE_OPENSSL) + if (closure->ssl != NULL) { + SSL_shutdown(closure->ssl); + SSL_free(closure->ssl); + } + sudo_ev_free(closure->tls_connect_ev); +#endif + sudo_ev_free(closure->read_ev); + sudo_ev_free(closure->write_ev); + free(closure->read_buf.data); + free(closure->write_buf.data); + free(closure->buf); + close(closure->sock); + free(closure); + } + + debug_return; +} + +/* + * Initialize a new client closure + */ +static struct client_closure * +client_closure_alloc(int sock, struct sudo_event_base *base, + struct timespec *elapsed, struct timespec *restart, const char *iolog_id, + char *reject_reason, bool accept_only, struct eventlog *evlog) +{ + struct client_closure *closure; + debug_decl(client_closure_alloc, SUDO_DEBUG_UTIL); + + if ((closure = calloc(1, sizeof(*closure))) == NULL) + debug_return_ptr(NULL); + + closure->sock = sock; + closure->evbase = base; + + TAILQ_INSERT_TAIL(&connections, closure, entries); + + closure->state = RECV_HELLO; + closure->accept_only = accept_only; + closure->reject_reason = reject_reason; + closure->evlog = evlog; + + closure->elapsed.tv_sec = elapsed->tv_sec; + closure->elapsed.tv_nsec = elapsed->tv_nsec; + closure->restart.tv_sec = restart->tv_sec; + closure->restart.tv_nsec = restart->tv_nsec; + + closure->iolog_id = iolog_id; + + closure->read_buf.size = 8 * 1024; + closure->read_buf.data = malloc(closure->read_buf.size); + if (closure->read_buf.data == NULL) + goto bad; + + closure->read_ev = sudo_ev_alloc(sock, SUDO_EV_READ|SUDO_EV_PERSIST, + server_msg_cb, closure); + if (closure->read_ev == NULL) + goto bad; + + closure->write_ev = sudo_ev_alloc(sock, SUDO_EV_WRITE|SUDO_EV_PERSIST, + client_msg_cb, closure); + if (closure->write_ev == NULL) + goto bad; + +#if defined(HAVE_OPENSSL) + if (cert != NULL) { + closure->tls_connect_ev = sudo_ev_alloc(sock, SUDO_EV_WRITE, + tls_connect_cb, closure); + if (closure->tls_connect_ev == NULL) + goto bad; + } +#endif + + debug_return_ptr(closure); +bad: + client_closure_free(closure); + debug_return_ptr(NULL); +} + +#if defined(HAVE_OPENSSL) +static const char short_opts[] = "Ah:i:np:r:R:t:b:c:k:V"; +#else +static const char short_opts[] = "Ah:i:Ip:r:R:t:V"; +#endif +static struct option long_opts[] = { + { "accept", no_argument, NULL, 'A' }, + { "help", no_argument, NULL, 1 }, + { "host", required_argument, NULL, 'h' }, + { "iolog-id", required_argument, NULL, 'i' }, + { "port", required_argument, NULL, 'p' }, + { "restart", required_argument, NULL, 'r' }, + { "reject", required_argument, NULL, 'R' }, + { "test", optional_argument, NULL, 't' }, +#if defined(HAVE_OPENSSL) + { "ca-bundle", required_argument, NULL, 'b' }, + { "cert", required_argument, NULL, 'c' }, + { "key", required_argument, NULL, 'k' }, + { "no-verify", no_argument, NULL, 'n' }, +#endif + { "version", no_argument, NULL, 'V' }, + { NULL, no_argument, NULL, 0 }, +}; + +sudo_dso_public int main(int argc, char *argv[]); + +int +main(int argc, char *argv[]) +{ + struct client_closure *closure = NULL; + struct sudo_event_base *evbase; + struct eventlog *evlog; + const char *port = NULL; + struct timespec restart = { 0, 0 }; + struct timespec elapsed = { 0, 0 }; + bool accept_only = false; + char *reject_reason = NULL; + const char *iolog_id = NULL; + const char *open_mode = "r"; + const char *errstr; + int ch, sock, iolog_dir_fd, finished; + debug_decl_vars(main, SUDO_DEBUG_MAIN); + +#if defined(SUDO_DEVEL) && defined(__OpenBSD__) + { + extern char *malloc_options; + malloc_options = "S"; + } +#endif + + signal(SIGPIPE, SIG_IGN); + + initprogname(argc > 0 ? argv[0] : "sudo_sendlog"); + setlocale(LC_ALL, ""); + bindtextdomain("sudo", LOCALEDIR); /* XXX - add logsrvd domain */ + textdomain("sudo"); + + /* Read sudo.conf and initialize the debug subsystem. */ + if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1) + exit(EXIT_FAILURE); + sudo_debug_register(getprogname(), NULL, NULL, + sudo_conf_debug_files(getprogname())); + + if (protobuf_c_version_number() < 1003000) + sudo_fatalx("%s", U_("Protobuf-C version 1.3 or higher required")); + + while ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { + switch (ch) { + case 'A': + accept_only = true; + break; + case 'h': + server_name = optarg; + break; + case 'i': + iolog_id = optarg; + break; + case 'R': + reject_reason = optarg; + break; + case 'p': + port = optarg; + break; + case 'r': + if (!parse_timespec(&restart, optarg)) + goto bad; + open_mode = "r+"; + break; + case 't': + nr_of_conns = sudo_strtonum(optarg, 1, INT_MAX, &errstr); + if (errstr != NULL) { + sudo_warnx(U_("%s: %s"), optarg, U_(errstr)); + goto bad; + } + testrun = true; + break; + case 1: + help(); + break; +#if defined(HAVE_OPENSSL) + case 'b': + ca_bundle = optarg; + break; + case 'c': + cert = optarg; + break; + case 'k': + key = optarg; + break; + case 'n': + verify_server = false; + break; +#endif + case 'V': + (void)printf(_("%s version %s\n"), getprogname(), + PACKAGE_VERSION); + return 0; + default: + usage(true); + } + } + argc -= optind; + argv += optind; + +#if defined(HAVE_OPENSSL) + /* if no key file is given explicitly, try to load the key from the cert */ + if (cert != NULL) { + if (key == NULL) + key = cert; + if (port == NULL) + port = DEFAULT_PORT_TLS; + } +#endif + if (port == NULL) + port = DEFAULT_PORT; + + if (sudo_timespecisset(&restart) != (iolog_id != NULL)) { + sudo_warnx("%s", U_("both restart point and iolog ID must be specified")); + usage(true); + } + if (sudo_timespecisset(&restart) && (accept_only || reject_reason)) { + sudo_warnx("%s", U_("a restart point may not be set when no I/O is sent")); + usage(true); + } + + /* Remaining arg should be to I/O log dir to send. */ + if (argc != 1) + usage(true); + iolog_dir = argv[0]; + if ((iolog_dir_fd = open(iolog_dir, O_RDONLY)) == -1) { + sudo_warn("%s", iolog_dir); + goto bad; + } + + /* Parse I/O log info file. */ + if ((evlog = iolog_parse_loginfo(iolog_dir_fd, iolog_dir)) == NULL) + goto bad; + + if ((evbase = sudo_ev_base_alloc()) == NULL) + sudo_fatal(NULL); + + if (testrun) + printf("connecting clients...\n"); + + for (int i = 0; i < nr_of_conns; i++) { + sock = connect_server(server_name, port); + if (sock == -1) + goto bad; + + if (!testrun) + printf("Connected to %s:%s\n", server_name, port); + + closure = client_closure_alloc(sock, evbase, &elapsed, &restart, + iolog_id, reject_reason, accept_only, evlog); + if (closure == NULL) + goto bad; + + /* Open the I/O log files and seek to restart point if there is one. */ + if (!iolog_open_all(iolog_dir_fd, iolog_dir, closure->iolog_files, open_mode)) + goto bad; + if (sudo_timespecisset(&closure->restart)) { + if (!iolog_seekto(iolog_dir_fd, iolog_dir, closure->iolog_files, + &closure->elapsed, &closure->restart)) + goto bad; + } + +#if defined(HAVE_OPENSSL) + if (cert != NULL) { + if (!tls_setup(closure)) + goto bad; + } else +#endif + { + /* No TLS, send ClientHello */ + if (!fmt_client_hello(closure)) + goto bad; + } + } + + if (testrun) + printf("sending logs...\n"); + + struct timespec t_start, t_end, t_result; + sudo_gettime_real(&t_start); + + sudo_ev_dispatch(evbase); + sudo_ev_base_free(evbase); + + sudo_gettime_real(&t_end); + sudo_timespecsub(&t_end, &t_start, &t_result); + + finished = 0; + while ((closure = TAILQ_FIRST(&connections)) != NULL) { + if (closure->state == FINISHED) { + finished++; + } else { + sudo_warnx(U_("exited prematurely with state %d"), closure->state); + sudo_warnx(U_("elapsed time sent to server [%lld, %ld]"), + (long long)closure->elapsed.tv_sec, closure->elapsed.tv_nsec); + sudo_warnx(U_("commit point received from server [%lld, %ld]"), + (long long)closure->committed.tv_sec, closure->committed.tv_nsec); + } + client_closure_free(closure); + } + eventlog_free(evlog); +#if defined(HAVE_OPENSSL) + SSL_CTX_free(ssl_ctx); +#endif + + if (finished != 0) { + printf("%d I/O log%s transmitted successfully in %lld.%.9ld seconds\n", + finished, nr_of_conns > 1 ? "s" : "", + (long long)t_result.tv_sec, t_result.tv_nsec); + debug_return_int(EXIT_SUCCESS); + } + +bad: + debug_return_int(EXIT_FAILURE); +} |