/* * SPDX-License-Identifier: ISC * * Copyright (c) 2019-2022 Todd C. Miller * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /* * 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 #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_STDBOOL_H # include #else # include "compat/stdbool.h" #endif /* HAVE_STDBOOL_H */ #if defined(HAVE_STDINT_H) # include #elif defined(HAVE_INTTYPES_H) # include #endif #include #include #include #include #include #ifndef HAVE_GETADDRINFO # include "compat/getaddrinfo.h" #endif #ifdef HAVE_GETOPT_LONG # include # else # include "compat/getopt.h" #endif /* HAVE_GETOPT_LONG */ #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); }