/* * 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 #ifdef HAVE_STDBOOL_H # include #else # include "compat/stdbool.h" #endif /* HAVE_STDBOOL_H */ #if defined(HAVE_STDINT_H) # include #elif defined(HAVE_INTTYPES_H) # include #endif #include #include #include #include #include #ifdef HAVE_GETOPT_LONG # include # else # include "compat/getopt.h" #endif /* HAVE_GETOPT_LONG */ #define NEED_INET_NTOP /* to expose sudo_inet_ntop in sudo_compat.h */ #include "pathnames.h" #include "sudo_compat.h" #include "sudo_conf.h" #include "sudo_debug.h" #include "sudo_event.h" #include "sudo_eventlog.h" #include "sudo_fatal.h" #include "sudo_gettext.h" #include "sudo_json.h" #include "sudo_iolog.h" #include "sudo_queue.h" #include "sudo_rand.h" #include "sudo_util.h" #include "logsrvd.h" #include "hostcheck.h" #ifndef O_NOFOLLOW # define O_NOFOLLOW 0 #endif /* * Sudo I/O audit server. */ static int logsrvd_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER; TAILQ_HEAD(connection_list, connection_closure); static struct connection_list connections = TAILQ_HEAD_INITIALIZER(connections); static struct listener_list listeners = TAILQ_HEAD_INITIALIZER(listeners); static const char server_id[] = "Sudo Audit Server " PACKAGE_VERSION; static const char *conf_file = _PATH_SUDO_LOGSRVD_CONF; /* Event loop callbacks. */ static void client_msg_cb(int fd, int what, void *v); static void server_msg_cb(int fd, int what, void *v); static void server_commit_cb(int fd, int what, void *v); #if defined(HAVE_OPENSSL) static void tls_handshake_cb(int fd, int what, void *v); #endif /* * Free a struct connection_closure container and its contents. */ static void connection_closure_free(struct connection_closure *closure) { debug_decl(connection_closure_free, SUDO_DEBUG_UTIL); if (closure != NULL) { bool shutting_down = closure->state == SHUTDOWN; struct sudo_event_base *evbase = closure->evbase; struct connection_buffer *buf; TAILQ_REMOVE(&connections, closure, entries); if (closure->state == CONNECTING && closure->journal != NULL) { /* Failed to relay journal file, retry later. */ logsrvd_queue_insert(closure); } if (closure->relay_closure != NULL) relay_closure_free(closure->relay_closure); #if defined(HAVE_OPENSSL) if (closure->ssl != NULL) { /* Must call SSL_shutdown() before closing closure->sock. */ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "closing down TLS connection from %s", closure->ipaddr); if (SSL_shutdown(closure->ssl) == 0) SSL_shutdown(closure->ssl); SSL_free(closure->ssl); } #endif if (closure->sock != -1) { shutdown(closure->sock, SHUT_RDWR); close(closure->sock); } iolog_close_all(closure); sudo_ev_free(closure->commit_ev); sudo_ev_free(closure->read_ev); sudo_ev_free(closure->write_ev); #if defined(HAVE_OPENSSL) sudo_ev_free(closure->ssl_accept_ev); #endif eventlog_free(closure->evlog); free(closure->read_buf.data); while ((buf = TAILQ_FIRST(&closure->write_bufs)) != NULL) { sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, "discarding write buffer %p, len %u", buf, buf->len - buf->off); TAILQ_REMOVE(&closure->write_bufs, buf, entries); free(buf->data); free(buf); } while ((buf = TAILQ_FIRST(&closure->free_bufs)) != NULL) { TAILQ_REMOVE(&closure->free_bufs, buf, entries); free(buf->data); free(buf); } free(closure->journal_path); if (closure->journal != NULL) fclose(closure->journal); free(closure); if (shutting_down && TAILQ_EMPTY(&connections)) sudo_ev_loopbreak(evbase); } debug_return; } /* * Allocate a new connection closure. */ struct connection_closure * connection_closure_alloc(int fd, bool tls, bool relay_only, struct sudo_event_base *base) { struct connection_closure *closure; debug_decl(connection_closure_alloc, SUDO_DEBUG_UTIL); if ((closure = calloc(1, sizeof(*closure))) == NULL) debug_return_ptr(NULL); closure->iolog_dir_fd = -1; closure->sock = relay_only ? -1 : fd; closure->evbase = base; TAILQ_INIT(&closure->write_bufs); TAILQ_INIT(&closure->free_bufs); /* Use different message handlers depending on the operating mode. */ if (relay_only) { closure->cms = &cms_relay; } else if (logsrvd_conf_relay_store_first()) { closure->store_first = true; closure->cms = &cms_journal; } else { closure->cms = &cms_local; } TAILQ_INSERT_TAIL(&connections, closure, entries); closure->read_buf.size = 64 * 1024; closure->read_buf.data = malloc(closure->read_buf.size); if (closure->read_buf.data == NULL) goto bad; closure->read_ev = sudo_ev_alloc(fd, SUDO_EV_READ|SUDO_EV_PERSIST, client_msg_cb, closure); if (closure->read_ev == NULL) goto bad; if (!relay_only) { closure->write_ev = sudo_ev_alloc(fd, SUDO_EV_WRITE|SUDO_EV_PERSIST, server_msg_cb, closure); if (closure->write_ev == NULL) goto bad; closure->commit_ev = sudo_ev_alloc(-1, SUDO_EV_TIMEOUT, server_commit_cb, closure); if (closure->commit_ev == NULL) goto bad; } #if defined(HAVE_OPENSSL) if (tls) { closure->ssl_accept_ev = sudo_ev_alloc(fd, SUDO_EV_READ, tls_handshake_cb, closure); if (closure->ssl_accept_ev == NULL) goto bad; } #endif debug_return_ptr(closure); bad: connection_closure_free(closure); debug_return_ptr(NULL); } /* * Close the client connection when finished. * If in store-and-forward mode, initiate a relay connection. * Otherwise, free the connection closure, removing any events. */ void connection_close(struct connection_closure *closure) { struct connection_closure *new_closure; debug_decl(connection_close, SUDO_DEBUG_UTIL); if (closure == NULL) debug_return; /* Final state should be FINISHED except on error. */ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "%s: closure %p, final state %d, relay_closure %p, " "journal file %p, journal path %s", __func__, closure, closure->state, closure->relay_closure, closure->journal, closure->journal_path ? closure->journal_path : ""); /* * If we finished a client connection in store-and-forward mode, * create a new connection for the relay and replay the journal. */ if (closure->store_first && closure->state == FINISHED && closure->relay_closure == NULL && closure->journal != NULL) { new_closure = connection_closure_alloc(fileno(closure->journal), false, true, closure->evbase); if (new_closure != NULL) { /* Re-parent journal settings. */ new_closure->journal = closure->journal; closure->journal = NULL; new_closure->journal_path = closure->journal_path; closure->journal_path = NULL; /* Connect to the first relay available asynchronously. */ if (!connect_relay(new_closure)) { sudo_warnx("%s", U_("unable to connect to relay")); connection_closure_free(new_closure); } } } if (closure->state == FINISHED && closure->journal_path != NULL) { /* Journal relayed successfully, remove backing file. */ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "removing journal file %s", closure->journal_path); unlink(closure->journal_path); /* Process the next outgoing file (if any). */ logsrvd_queue_enable(0, closure->evbase); } connection_closure_free(closure); debug_return; } struct connection_buffer * get_free_buf(size_t len, struct connection_closure *closure) { struct connection_buffer *buf; debug_decl(get_free_buf, SUDO_DEBUG_UTIL); buf = TAILQ_FIRST(&closure->free_bufs); if (buf != NULL) { TAILQ_REMOVE(&closure->free_bufs, buf, entries); } else { if ((buf = calloc(1, sizeof(*buf))) == NULL) { sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_ptr(NULL); } } if (len > buf->size) { free(buf->data); buf->size = sudo_pow2_roundup(len); if ((buf->data = malloc(buf->size)) == NULL) { sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); free(buf); buf = NULL; } } debug_return_ptr(buf); } static bool fmt_server_message(struct connection_closure *closure, ServerMessage *msg) { struct connection_buffer *buf = NULL; uint32_t msg_len; bool ret = false; size_t len; debug_decl(fmt_server_message, SUDO_DEBUG_UTIL); len = server_message__get_packed_size(msg); if (len > MESSAGE_SIZE_MAX) { sudo_warnx(U_("server message too large: %zu"), len); goto done; } /* Wire message size is used for length encoding, precedes message. */ msg_len = htonl((uint32_t)len); len += sizeof(msg_len); sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "size + server message %zu bytes", len); if ((buf = get_free_buf(len, closure)) == NULL) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "unable to allocate connection_buffer"); goto done; } memcpy(buf->data, &msg_len, sizeof(msg_len)); server_message__pack(msg, buf->data + sizeof(msg_len)); buf->len = len; TAILQ_INSERT_TAIL(&closure->write_bufs, buf, entries); ret = true; done: debug_return_bool(ret); } static bool fmt_hello_message(struct connection_closure *closure) { ServerMessage msg = SERVER_MESSAGE__INIT; ServerHello hello = SERVER_HELLO__INIT; debug_decl(fmt_hello_message, SUDO_DEBUG_UTIL); /* TODO: implement redirect and servers array. */ hello.server_id = (char *)server_id; hello.subcommands = true; msg.u.hello = &hello; msg.type_case = SERVER_MESSAGE__TYPE_HELLO; debug_return_bool(fmt_server_message(closure, &msg)); } bool fmt_log_id_message(const char *id, struct connection_closure *closure) { ServerMessage msg = SERVER_MESSAGE__INIT; debug_decl(fmt_log_id_message, SUDO_DEBUG_UTIL); msg.u.log_id = (char *)id; msg.type_case = SERVER_MESSAGE__TYPE_LOG_ID; debug_return_bool(fmt_server_message(closure, &msg)); } static bool fmt_error_message(const char *errstr, struct connection_closure *closure) { ServerMessage msg = SERVER_MESSAGE__INIT; debug_decl(fmt_error_message, SUDO_DEBUG_UTIL); msg.u.error = (char *)errstr; msg.type_case = SERVER_MESSAGE__TYPE_ERROR; debug_return_bool(fmt_server_message(closure, &msg)); } /* * Format a ServerMessage with the error string and add it to the write queue. * Also sets the error flag state to true. * Returns true if successfully scheduled, else false. */ bool schedule_error_message(const char *errstr, struct connection_closure *closure) { bool ret = false; debug_decl(schedule_error_message, SUDO_DEBUG_UTIL); sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "send error to client: %s", errstr ? errstr : "none"); /* Prevent further reads from the client, just write the error. */ sudo_ev_del(closure->evbase, closure->read_ev); if (errstr == NULL || closure->error || closure->write_ev == NULL) goto done; /* Format error message and add to the write queue. */ if (!fmt_error_message(errstr, closure)) goto done; if (sudo_ev_add(closure->evbase, closure->write_ev, logsrvd_conf_server_timeout(), true) == -1) { sudo_warnx("%s", U_("unable to add event to queue")); goto done; } ret = true; done: closure->error = true; debug_return_bool(ret); } /* * AcceptMessage handler. */ static bool handle_accept(AcceptMessage *msg, uint8_t *buf, size_t len, struct connection_closure *closure) { const char *source = closure->journal_path ? closure->journal_path : closure->ipaddr; bool ret; debug_decl(handle_accept, SUDO_DEBUG_UTIL); /* We can get an AcceptMessage for a sub-command during a session. */ if (closure->state == EXITED || closure->state == FINISHED) { sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); closure->errstr = _("state machine error"); debug_return_bool(false); } /* Check that message is valid. */ if (msg->submit_time == NULL || msg->n_info_msgs == 0) { sudo_warnx(U_("%s: %s"), source, U_("invalid AcceptMessage")); closure->errstr = _("invalid AcceptMessage"); debug_return_bool(false); } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received AcceptMessage from %s", __func__, source); ret = closure->cms->accept(msg, buf, len, closure); if (ret && closure->state == INITIAL) { if (msg->expect_iobufs) closure->log_io = true; closure->state = RUNNING; } debug_return_bool(ret); } /* * RejectMessage handler. */ static bool handle_reject(RejectMessage *msg, uint8_t *buf, size_t len, struct connection_closure *closure) { const char *source = closure->journal_path ? closure->journal_path : closure->ipaddr; bool ret; debug_decl(handle_reject, SUDO_DEBUG_UTIL); /* We can get a RejectMessage for a sub-command during a session. */ if (closure->state == EXITED || closure->state == FINISHED) { sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); closure->errstr = _("state machine error"); debug_return_bool(false); } /* Check that message is valid. */ if (msg->submit_time == NULL || msg->n_info_msgs == 0) { sudo_warnx(U_("%s: %s"), source, U_("invalid RejectMessage")); closure->errstr = _("invalid RejectMessage"); debug_return_bool(false); } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received RejectMessage from %s", __func__, source); ret = closure->cms->reject(msg, buf, len, closure); if (ret && closure->state == INITIAL) { closure->state = FINISHED; } debug_return_bool(ret); } static bool handle_exit(ExitMessage *msg, uint8_t *buf, size_t len, struct connection_closure *closure) { const char *source = closure->journal_path ? closure->journal_path : closure->ipaddr; bool ret; debug_decl(handle_exit, SUDO_DEBUG_UTIL); if (closure->state != RUNNING) { sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); closure->errstr = _("state machine error"); debug_return_bool(false); } /* Check that message is valid. */ if (msg->run_time == NULL) { sudo_warnx(U_("%s: %s"), source, U_("invalid ExitMessage")); closure->errstr = _("invalid ExitMessage"); debug_return_bool(false); } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ExitMessage from %s", source, __func__); ret = closure->cms->exit(msg, buf, len, closure); if (ret) { if (sudo_timespecisset(&closure->elapsed_time)) { sudo_debug_printf(SUDO_DEBUG_INFO, "%s: elapsed time: [%lld, %ld]", __func__, (long long)closure->elapsed_time.tv_sec, closure->elapsed_time.tv_nsec); } if (closure->log_io) { /* Command exited, client waiting for final commit point. */ closure->state = EXITED; /* Relay host will send the final commit point. */ if (closure->relay_closure == NULL) { struct timespec tv = { 0, 0 }; if (sudo_ev_add(closure->evbase, closure->commit_ev, &tv, false) == -1) { sudo_warnx("%s", U_("unable to add event to queue")); ret = false; } } } else { /* No commit point to send to client, we are finished. */ closure->state = FINISHED; } } sudo_ev_del(closure->evbase, closure->read_ev); debug_return_bool(ret); } static bool handle_restart(RestartMessage *msg, uint8_t *buf, size_t len, struct connection_closure *closure) { const char *source = closure->journal_path ? closure->journal_path : closure->ipaddr; bool ret = true; debug_decl(handle_restart, SUDO_DEBUG_UTIL); if (closure->state != INITIAL) { sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); closure->errstr = _("state machine error"); debug_return_bool(false); } /* Check that message is valid. */ if (msg->log_id == NULL || msg->resume_point == NULL) { sudo_warnx(U_("%s: %s"), source, U_("invalid RestartMessage")); closure->errstr = _("invalid RestartMessage"); debug_return_bool(false); } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received RestartMessage for %s from %s", __func__, msg->log_id, source); /* Only I/O logs are restartable. */ closure->log_io = true; if (closure->cms->restart(msg, buf, len, closure)) { /* Successfully restarted. */ closure->state = RUNNING; } else { /* Report error to client before closing the connection. */ sudo_debug_printf(SUDO_DEBUG_WARN, "%s: unable to restart I/O log", __func__); if (!schedule_error_message(closure->errstr, closure)) ret = false; } debug_return_bool(ret); } static bool handle_alert(AlertMessage *msg, uint8_t *buf, size_t len, struct connection_closure *closure) { const char *source = closure->journal_path ? closure->journal_path : closure->ipaddr; debug_decl(handle_alert, SUDO_DEBUG_UTIL); /* Check that message is valid. */ if (msg->alert_time == NULL || msg->reason == NULL) { sudo_warnx(U_("%s: %s"), source, U_("invalid AlertMessage")); closure->errstr = _("invalid AlertMessage"); debug_return_bool(false); } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received AlertMessage from %s", source, __func__); debug_return_bool(closure->cms->alert(msg, buf, len, closure)); } /* Enable a commit event if not relaying and it is not already pending. */ static bool enable_commit(struct connection_closure *closure) { debug_decl(enable_commit, SUDO_DEBUG_UTIL); if (closure->relay_closure == NULL) { if (!ISSET(closure->commit_ev->flags, SUDO_EVQ_INSERTED)) { struct timespec tv = { ACK_FREQUENCY, 0 }; if (sudo_ev_add(closure->evbase, closure->commit_ev, &tv, false) == -1) { sudo_warnx("%s", U_("unable to add event to queue")); debug_return_bool(false); } } } debug_return_bool(true); } static bool handle_iobuf(int iofd, IoBuffer *iobuf, uint8_t *buf, size_t len, struct connection_closure *closure) { const char *source = closure->journal_path ? closure->journal_path : closure->ipaddr; debug_decl(handle_iobuf, SUDO_DEBUG_UTIL); if (closure->state != RUNNING) { sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); closure->errstr = _("state machine error"); debug_return_bool(false); } if (!closure->log_io) { sudo_warnx(U_("%s: unexpected IoBuffer"), source); closure->errstr = _("protocol error"); debug_return_bool(false); } /* Check that message is valid. */ if (iobuf->delay == NULL) { sudo_warnx(U_("%s: %s"), source, U_("invalid IoBuffer")); closure->errstr = _("invalid IoBuffer"); debug_return_bool(false); } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received IoBuffer from %s", source, __func__); if (!closure->cms->iobuf(iofd, iobuf, buf, len, closure)) debug_return_bool(false); if (!enable_commit(closure)) debug_return_bool(false); debug_return_bool(true); } static bool handle_winsize(ChangeWindowSize *msg, uint8_t *buf, size_t len, struct connection_closure *closure) { const char *source = closure->journal_path ? closure->journal_path : closure->ipaddr; debug_decl(handle_winsize, SUDO_DEBUG_UTIL); if (closure->state != RUNNING) { sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); closure->errstr = _("state machine error"); debug_return_bool(false); } if (!closure->log_io) { sudo_warnx(U_("%s: unexpected IoBuffer"), source); closure->errstr = _("protocol error"); debug_return_bool(false); } /* Check that message is valid. */ if (msg->delay == NULL) { sudo_warnx(U_("%s: %s"), source, U_("invalid ChangeWindowSize")); closure->errstr = _("invalid ChangeWindowSize"); debug_return_bool(false); } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ChangeWindowSize from %s", source, __func__); if (!closure->cms->winsize(msg, buf, len, closure)) debug_return_bool(false); if (!enable_commit(closure)) debug_return_bool(false); debug_return_bool(true); } static bool handle_suspend(CommandSuspend *msg, uint8_t *buf, size_t len, struct connection_closure *closure) { const char *source = closure->journal_path ? closure->journal_path : closure->ipaddr; debug_decl(handle_syspend, SUDO_DEBUG_UTIL); if (closure->state != RUNNING) { sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); closure->errstr = _("state machine error"); debug_return_bool(false); } if (!closure->log_io) { sudo_warnx(U_("%s: unexpected IoBuffer"), source); closure->errstr = _("protocol error"); debug_return_bool(false); } /* Check that message is valid. */ if (msg->delay == NULL || msg->signal == NULL) { sudo_warnx(U_("%s: %s"), source, U_("invalid CommandSuspend")); closure->errstr = _("invalid CommandSuspend"); debug_return_bool(false); } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received CommandSuspend from %s", source, __func__); if (!closure->cms->suspend(msg, buf, len, closure)) debug_return_bool(false); if (!enable_commit(closure)) debug_return_bool(false); debug_return_bool(true); } static bool handle_client_hello(ClientHello *msg, uint8_t *buf, size_t len, struct connection_closure *closure) { const char *source = closure->journal_path ? closure->journal_path : closure->ipaddr; debug_decl(handle_client_hello, SUDO_DEBUG_UTIL); if (closure->state != INITIAL) { sudo_warnx(U_("unexpected state %d for %s"), closure->state, source); closure->errstr = _("state machine error"); debug_return_bool(false); } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ClientHello", __func__); sudo_debug_printf(SUDO_DEBUG_INFO, "%s: client ID %s", __func__, msg->client_id ? msg->client_id : "unknown"); debug_return_bool(true); } static bool handle_client_message(uint8_t *buf, size_t len, struct connection_closure *closure) { const char *source = closure->journal_path ? closure->journal_path : closure->ipaddr; ClientMessage *msg; bool ret = false; debug_decl(handle_client_message, SUDO_DEBUG_UTIL); /* TODO: can we extract type_case without unpacking for relay case? */ msg = client_message__unpack(NULL, len, buf); if (msg == NULL) { sudo_warnx(U_("unable to unpack %s size %zu"), "ClientMessage", len); debug_return_bool(false); } switch (msg->type_case) { case CLIENT_MESSAGE__TYPE_ACCEPT_MSG: ret = handle_accept(msg->u.accept_msg, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_REJECT_MSG: ret = handle_reject(msg->u.reject_msg, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_EXIT_MSG: ret = handle_exit(msg->u.exit_msg, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_RESTART_MSG: ret = handle_restart(msg->u.restart_msg, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_ALERT_MSG: ret = handle_alert(msg->u.alert_msg, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_TTYIN_BUF: ret = handle_iobuf(IOFD_TTYIN, msg->u.ttyin_buf, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_TTYOUT_BUF: ret = handle_iobuf(IOFD_TTYOUT, msg->u.ttyout_buf, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_STDIN_BUF: ret = handle_iobuf(IOFD_STDIN, msg->u.stdin_buf, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_STDOUT_BUF: ret = handle_iobuf(IOFD_STDOUT, msg->u.stdout_buf, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_STDERR_BUF: ret = handle_iobuf(IOFD_STDERR, msg->u.stderr_buf, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_WINSIZE_EVENT: ret = handle_winsize(msg->u.winsize_event, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_SUSPEND_EVENT: ret = handle_suspend(msg->u.suspend_event, buf, len, closure); break; case CLIENT_MESSAGE__TYPE_HELLO_MSG: ret = handle_client_hello(msg->u.hello_msg, buf, len, closure); break; default: sudo_warnx(U_("unexpected type_case value %d in %s from %s"), msg->type_case, "ClientMessage", source); closure->errstr = _("unrecognized ClientMessage type"); break; } client_message__free_unpacked(msg, NULL); debug_return_bool(ret); } static void shutdown_cb(int unused, int what, void *v) { struct sudo_event_base *base = v; debug_decl(shutdown_cb, SUDO_DEBUG_UTIL); sudo_ev_loopbreak(base); debug_return; } /* * Shut down active client connections if any, or exit immediately. */ static void server_shutdown(struct sudo_event_base *base) { struct connection_closure *closure, *next; struct sudo_event *ev; struct timespec tv = { 0, 0 }; debug_decl(server_shutdown, SUDO_DEBUG_UTIL); if (TAILQ_EMPTY(&connections)) { sudo_ev_loopbreak(base); debug_return; } TAILQ_FOREACH_SAFE(closure, &connections, entries, next) { closure->state = SHUTDOWN; sudo_ev_del(base, closure->read_ev); if (closure->relay_closure != NULL) { /* Connection being relayed, check for pending I/O. */ relay_shutdown(closure); } else if (closure->log_io) { /* Schedule final commit point for the connection. */ if (sudo_ev_add(base, closure->commit_ev, &tv, false) == -1) { sudo_warnx("%s", U_("unable to add event to queue")); } } else { /* No commit point, close connection immediately. */ connection_close(closure); } } if (!TAILQ_EMPTY(&connections)) { /* We need a timed event to exit even if clients time out. */ ev = sudo_ev_alloc(-1, SUDO_EV_TIMEOUT, shutdown_cb, base); if (ev != NULL) { tv.tv_sec = SHUTDOWN_TIMEO; if (sudo_ev_add(base, ev, &tv, false) == -1) { sudo_warnx("%s", U_("unable to add event to queue")); } } } debug_return; } /* * Send a server message to the client. */ static void server_msg_cb(int fd, int what, void *v) { struct connection_closure *closure = v; struct connection_buffer *buf; ssize_t nwritten; debug_decl(server_msg_cb, SUDO_DEBUG_UTIL); /* For TLS we may need to write as part of SSL_read(). */ if (closure->read_instead_of_write) { closure->read_instead_of_write = false; /* Delete write event if it was only due to SSL_read(). */ if (closure->temporary_write_event) { closure->temporary_write_event = false; sudo_ev_del(closure->evbase, closure->write_ev); } client_msg_cb(fd, what, v); debug_return; } if (what == SUDO_EV_TIMEOUT) { sudo_warnx(U_("timed out writing to client %s"), closure->ipaddr); goto finished; } if ((buf = TAILQ_FIRST(&closure->write_bufs)) == NULL) { sudo_warnx(U_("missing write buffer for client %s"), closure->ipaddr); goto finished; } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending %u bytes to client (%s)", __func__, buf->len - buf->off, closure->ipaddr); #if defined(HAVE_OPENSSL) if (closure->ssl != NULL) { nwritten = SSL_write(closure->ssl, buf->data + buf->off, buf->len - buf->off); if (nwritten <= 0) { const char *errstr; int err = SSL_get_error(closure->ssl, nwritten); switch (err) { case SSL_ERROR_WANT_READ: /* ssl wants to read, read event always active */ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, "SSL_write returns SSL_ERROR_WANT_READ"); /* Redirect persistent read event to finish SSL_write() */ closure->write_instead_of_read = true; debug_return; case SSL_ERROR_WANT_WRITE: /* ssl wants to write more, write event remains active */ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, "SSL_write returns SSL_ERROR_WANT_WRITE"); debug_return; case SSL_ERROR_SYSCALL: sudo_warn("%s: SSL_write", closure->ipaddr); goto finished; default: errstr = ERR_reason_error_string(ERR_get_error()); sudo_warnx("%s: SSL_write: %s", closure->ipaddr, errstr ? errstr : strerror(errno)); goto finished; } } } else #endif { nwritten = write(fd, buf->data + buf->off, buf->len - buf->off); } if (nwritten == -1) { if (errno == EAGAIN || errno == EINTR) debug_return; sudo_warn("%s: write", closure->ipaddr); goto finished; } buf->off += nwritten; if (buf->off == buf->len) { /* sent entire message, move buf to free list */ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: finished sending %u bytes to client", __func__, buf->len); buf->off = 0; buf->len = 0; TAILQ_REMOVE(&closure->write_bufs, buf, entries); TAILQ_INSERT_TAIL(&closure->free_bufs, buf, entries); if (TAILQ_EMPTY(&closure->write_bufs)) { /* Write queue empty, check state. */ sudo_ev_del(closure->evbase, closure->write_ev); if (closure->error || closure->state == FINISHED || closure->state == SHUTDOWN) goto finished; } } debug_return; finished: connection_close(closure); debug_return; } /* * Receive client message(s). */ static void client_msg_cb(int fd, int what, void *v) { struct connection_closure *closure = v; struct connection_buffer *buf = &closure->read_buf; const char *source = closure->journal_path ? closure->journal_path : closure->ipaddr; uint32_t msg_len; ssize_t nread; debug_decl(client_msg_cb, SUDO_DEBUG_UTIL); /* For TLS we may need to read as part of SSL_write(). */ if (closure->write_instead_of_read) { closure->write_instead_of_read = false; server_msg_cb(fd, what, v); debug_return; } if (what == SUDO_EV_TIMEOUT) { sudo_warnx(U_("timed out reading from client %s"), closure->ipaddr); goto close_connection; } #if defined(HAVE_OPENSSL) if (closure->ssl != NULL) { nread = SSL_read(closure->ssl, buf->data + buf->len, buf->size); if (nread <= 0) { const char *errstr; int err = SSL_get_error(closure->ssl, nread); switch (err) { case SSL_ERROR_ZERO_RETURN: /* ssl connection shutdown cleanly */ nread = 0; break; case SSL_ERROR_WANT_READ: /* ssl wants to read more, read event is always active */ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, "SSL_read returns SSL_ERROR_WANT_READ"); /* Read event is always active. */ debug_return; case SSL_ERROR_WANT_WRITE: /* ssl wants to write, schedule a write if not pending */ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, "SSL_read returns SSL_ERROR_WANT_WRITE"); if (!sudo_ev_pending(closure->write_ev, SUDO_EV_WRITE, NULL)) { /* Enable a temporary write event. */ if (sudo_ev_add(closure->evbase, closure->write_ev, logsrvd_conf_server_timeout(), false) == -1) { sudo_warnx("%s", U_("unable to add event to queue")); closure->errstr = _("unable to allocate memory"); goto send_error; } closure->temporary_write_event = true; } /* Redirect write event to finish SSL_read() */ closure->read_instead_of_write = true; debug_return; case SSL_ERROR_SYSCALL: if (nread == 0) { /* EOF, handled below */ sudo_warnx(U_("EOF from %s without proper TLS shutdown"), closure->ipaddr); break; } sudo_warn("%s: SSL_read", closure->ipaddr); goto close_connection; default: errstr = ERR_reason_error_string(ERR_get_error()); sudo_warnx("%s: SSL_read: %s", closure->ipaddr, errstr ? errstr : strerror(errno)); goto close_connection; } } } else #endif { nread = read(fd, buf->data + buf->len, buf->size - buf->len); } sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received %zd bytes from client %s", __func__, nread, closure->ipaddr); switch (nread) { case -1: if (errno == EAGAIN || errno == EINTR) debug_return; sudo_warn("%s: read", closure->ipaddr); goto close_connection; case 0: if (closure->state != FINISHED) { sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, "unexpected EOF"); } goto close_connection; default: break; } buf->len += nread; while (buf->len - buf->off >= sizeof(msg_len)) { /* Read wire message size (uint32_t in network byte order). */ memcpy(&msg_len, buf->data + buf->off, sizeof(msg_len)); msg_len = ntohl(msg_len); if (msg_len > MESSAGE_SIZE_MAX) { sudo_warnx(U_("client message too large: %zu"), (size_t)msg_len); closure->errstr = _("client message too large"); goto send_error; } if (msg_len + sizeof(msg_len) > buf->len - buf->off) { /* Incomplete message, we'll read the rest next time. */ if (!expand_buf(buf, msg_len + sizeof(msg_len))) { closure->errstr = _("unable to allocate memory"); goto send_error; } debug_return; } /* Parse ClientMessage (could be zero bytes). */ sudo_debug_printf(SUDO_DEBUG_INFO, "%s: parsing ClientMessage, size %u", __func__, msg_len); buf->off += sizeof(msg_len); if (!handle_client_message(buf->data + buf->off, msg_len, closure)) { sudo_warnx(U_("%s: %s"), source, U_("invalid ClientMessage")); closure->errstr = _("invalid ClientMessage"); goto send_error; } buf->off += msg_len; } buf->len -= buf->off; buf->off = 0; if (closure->state == FINISHED) goto close_connection; debug_return; send_error: /* * Try to send client an error message before closing the connection. */ if (!schedule_error_message(closure->errstr, closure)) goto close_connection; debug_return; close_connection: connection_close(closure); debug_return; } /* * Format and schedule a commit_point message. */ bool schedule_commit_point(TimeSpec *commit_point, struct connection_closure *closure) { debug_decl(schedule_commit_point, SUDO_DEBUG_UTIL); if (closure->write_ev != NULL) { /* Send an acknowledgement of what we've committed to disk. */ ServerMessage msg = SERVER_MESSAGE__INIT; msg.u.commit_point = commit_point; msg.type_case = SERVER_MESSAGE__TYPE_COMMIT_POINT; sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending commit point [%lld, %ld]", __func__, (long long)commit_point->tv_sec, (long)commit_point->tv_nsec); if (!fmt_server_message(closure, &msg)) goto bad; if (sudo_ev_add(closure->evbase, closure->write_ev, logsrvd_conf_server_timeout(), false) == -1) { sudo_warnx("%s", U_("unable to add event to queue")); goto bad; } } if (closure->state == EXITED) closure->state = FINISHED; debug_return_bool(true); bad: debug_return_bool(false); } /* * Time-based event that fires periodically to report to the client * what has been committed to disk. */ static void server_commit_cb(int unused, int what, void *v) { struct connection_closure *closure = v; TimeSpec commit_point = TIME_SPEC__INIT; debug_decl(server_commit_cb, SUDO_DEBUG_UTIL); /* Flush I/O logs before sending commit point if needed. */ if (!iolog_get_flush()) iolog_flush_all(closure); commit_point.tv_sec = closure->elapsed_time.tv_sec; commit_point.tv_nsec = closure->elapsed_time.tv_nsec; if (!schedule_commit_point(&commit_point, closure)) connection_close(closure); debug_return; } /* * Begin the sudo logserver protocol. * When we enter the event loop the ServerHello message will be written * and any pending ClientMessage will be read. */ bool start_protocol(struct connection_closure *closure) { const struct timespec *timeout = logsrvd_conf_server_timeout(); debug_decl(start_protocol, SUDO_DEBUG_UTIL); if (closure->relay_closure != NULL && closure->relay_closure->relays != NULL) { /* No longer need the stashed relays list. */ address_list_delref(closure->relay_closure->relays); closure->relay_closure->relays = NULL; closure->relay_closure->relay_addr = NULL; } /* When replaying a journal there is no write event. */ if (closure->write_ev != NULL) { if (!fmt_hello_message(closure)) debug_return_bool(false); if (sudo_ev_add(closure->evbase, closure->write_ev, timeout, false) == -1) debug_return_bool(false); } /* No read timeout, client messages may happen at arbitrary times. */ if (sudo_ev_add(closure->evbase, closure->read_ev, NULL, false) == -1) debug_return_bool(false); debug_return_bool(true); } #if defined(HAVE_OPENSSL) static int verify_peer_identity(int preverify_ok, X509_STORE_CTX *ctx) { HostnameValidationResult result; struct connection_closure *closure; SSL *ssl; X509 *current_cert; X509 *peer_cert; debug_decl(verify_peer_identity, SUDO_DEBUG_UTIL); /* if pre-verification of the cert failed, just propagate that result back */ if (preverify_ok != 1) { debug_return_int(0); } /* since this callback is called for each cert in the chain, * check that current cert is the peer's certificate */ current_cert = X509_STORE_CTX_get_current_cert(ctx); peer_cert = X509_STORE_CTX_get0_cert(ctx); if (current_cert != peer_cert) { debug_return_int(1); } /* read out the attached object (closure) from the ssl connection object */ ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); closure = (struct connection_closure *)SSL_get_ex_data(ssl, 1); result = validate_hostname(peer_cert, closure->ipaddr, closure->ipaddr, 1); switch(result) { case MatchFound: debug_return_int(1); default: sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "hostname validation failed"); debug_return_int(0); } } /* * Set the TLS verify callback to verify_peer_identity(). */ static void set_tls_verify_peer(void) { SSL_CTX *server_ctx = logsrvd_server_tls_ctx(); SSL_CTX *relay_ctx = logsrvd_relay_tls_ctx(); debug_decl(set_tls_verify_peer, SUDO_DEBUG_UTIL); if (server_ctx != NULL && logsrvd_conf_server_tls_check_peer()) { /* Verify server cert during the handshake. */ SSL_CTX_set_verify(server_ctx, SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_peer_identity); } if (relay_ctx != NULL && logsrvd_conf_relay_tls_check_peer()) { /* Verify relay cert during the handshake. */ SSL_CTX_set_verify(relay_ctx, SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_peer_identity); } debug_return; } static void tls_handshake_cb(int fd, int what, void *v) { struct connection_closure *closure = v; const char *errstr; int err, handshake_status; debug_decl(tls_handshake_cb, SUDO_DEBUG_UTIL); if (what == SUDO_EV_TIMEOUT) { sudo_warnx("TLS handshake with %s timed out", closure->ipaddr); goto bad; } handshake_status = SSL_accept(closure->ssl); err = SSL_get_error(closure->ssl, handshake_status); switch (err) { case SSL_ERROR_NONE: /* ssl handshake was successful */ sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "TLS handshake successful"); break; case SSL_ERROR_WANT_READ: /* ssl handshake is ongoing, re-schedule the SSL_accept() call */ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, "SSL_accept returns SSL_ERROR_WANT_READ"); if (what != SUDO_EV_READ) { if (sudo_ev_set(closure->ssl_accept_ev, closure->sock, SUDO_EV_READ, tls_handshake_cb, closure) == -1) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "unable to set ssl_accept_ev to SUDO_EV_READ"); goto bad; } } if (sudo_ev_add(closure->evbase, closure->ssl_accept_ev, logsrvd_conf_server_timeout(), false) == -1) { sudo_warnx("%s", U_("unable to add event to queue")); goto bad; } debug_return; case SSL_ERROR_WANT_WRITE: /* ssl handshake is ongoing, re-schedule the SSL_accept() call */ sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO, "SSL_accept returns SSL_ERROR_WANT_WRITE"); if (what != SUDO_EV_WRITE) { if (sudo_ev_set(closure->ssl_accept_ev, closure->sock, SUDO_EV_WRITE, tls_handshake_cb, closure) == -1) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "unable to set ssl_accept_ev to SUDO_EV_WRITE"); goto bad; } } if (sudo_ev_add(closure->evbase, closure->ssl_accept_ev, logsrvd_conf_server_timeout(), false) == -1) { sudo_warnx("%s", U_("unable to add event to queue")); goto bad; } debug_return; case SSL_ERROR_SYSCALL: sudo_warn("%s: SSL_accept", closure->ipaddr); goto bad; default: errstr = ERR_reason_error_string(ERR_get_error()); sudo_warnx("%s: SSL_accept: %s", closure->ipaddr, errstr ? errstr : strerror(errno)); goto bad; } sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "TLS version: %s, negotiated cipher suite: %s", SSL_get_version(closure->ssl), SSL_get_cipher(closure->ssl)); /* Start the actual protocol now that the TLS handshake is complete. */ if (!TAILQ_EMPTY(logsrvd_conf_relay_address()) && !closure->store_first) { if (!connect_relay(closure)) goto bad; } else { if (!start_protocol(closure)) goto bad; } debug_return; bad: connection_close(closure); debug_return; } #endif /* HAVE_OPENSSL */ /* * New connection. * Allocate a connection closure and optionally perform TLS handshake. */ static bool new_connection(int sock, bool tls, const union sockaddr_union *sa_un, struct sudo_event_base *evbase) { struct connection_closure *closure; debug_decl(new_connection, SUDO_DEBUG_UTIL); if ((closure = connection_closure_alloc(sock, tls, false, evbase)) == NULL) goto bad; /* store the peer's IP address in the closure object */ if (sa_un->sa.sa_family == AF_INET) { inet_ntop(AF_INET, &sa_un->sin.sin_addr, closure->ipaddr, sizeof(closure->ipaddr)); #ifdef HAVE_STRUCT_IN6_ADDR } else if (sa_un->sa.sa_family == AF_INET6) { inet_ntop(AF_INET6, &sa_un->sin6.sin6_addr, closure->ipaddr, sizeof(closure->ipaddr)); #endif /* HAVE_STRUCT_IN6_ADDR */ } else { errno = EAFNOSUPPORT; sudo_warn("%s", U_("unable to get remote IP addr")); goto bad; } sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "connection from %s", closure->ipaddr); #if defined(HAVE_OPENSSL) /* If TLS is enabled, perform the TLS handshake first. */ if (tls) { const char *errstr; /* Create the SSL object for the closure and attach it to the socket */ if ((closure->ssl = SSL_new(logsrvd_server_tls_ctx())) == NULL) { errstr = ERR_reason_error_string(ERR_get_error()); sudo_warnx(U_("%s: %s"), "SSL_new", errstr ? errstr : strerror(errno)); goto bad; } if (SSL_set_fd(closure->ssl, closure->sock) != 1) { errstr = ERR_reason_error_string(ERR_get_error()); sudo_warnx(U_("%s: %s"), "SSL_set_fd", errstr ? errstr : strerror(errno)); goto bad; } /* attach the closure object to the ssl connection object to make it available during hostname matching */ if (SSL_set_ex_data(closure->ssl, 1, closure) <= 0) { errstr = ERR_reason_error_string(ERR_get_error()); sudo_warnx(U_("Unable to attach user data to the ssl object: %s"), errstr ? errstr : strerror(errno)); goto bad; } /* Enable SSL_accept to begin handshake with client. */ if (sudo_ev_add(evbase, closure->ssl_accept_ev, logsrvd_conf_server_timeout(), false) == -1) { sudo_warnx("%s", U_("unable to add event to queue")); goto bad; } } #endif /* If no TLS handshake, start the protocol immediately. */ if (!tls) { if (!TAILQ_EMPTY(logsrvd_conf_relay_address()) && !closure->store_first) { if (!connect_relay(closure)) goto bad; } else { if (!start_protocol(closure)) goto bad; } } debug_return_bool(true); bad: connection_close(closure); debug_return_bool(false); } static int create_listener(struct server_address *addr) { int flags, on, sock; const char *family = "inet4"; debug_decl(create_listener, SUDO_DEBUG_UTIL); if ((sock = socket(addr->sa_un.sa.sa_family, SOCK_STREAM, 0)) == -1) { sudo_warn("socket"); goto bad; } on = 1; #ifdef HAVE_STRUCT_IN6_ADDR if (addr->sa_un.sa.sa_family == AF_INET6) { family = "inet6"; # ifdef IPV6_V6ONLY /* Disable IPv4-mapped IPv6 addresses. */ if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) == -1) sudo_warn("IPV6_V6ONLY"); # endif } #endif if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) sudo_warn("SO_REUSEADDR"); if (bind(sock, &addr->sa_un.sa, addr->sa_size) == -1) { /* TODO: only warn once for IPv4 and IPv6 or disambiguate */ sudo_warn("%s (%s)", addr->sa_str, family); goto bad; } if (listen(sock, SOMAXCONN) == -1) { sudo_warn("listen"); goto bad; } flags = fcntl(sock, F_GETFL, 0); if (flags == -1 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) { sudo_warn("fcntl(O_NONBLOCK)"); goto bad; } sudo_debug_printf(SUDO_DEBUG_INFO, "listening on %s (%s)", addr->sa_str, family); debug_return_int(sock); bad: if (sock != -1) close(sock); debug_return_int(-1); } static void listener_cb(int fd, int what, void *v) { struct listener *l = v; struct sudo_event_base *evbase = sudo_ev_get_base(l->ev); union sockaddr_union sa_un; socklen_t salen = sizeof(sa_un); int sock; debug_decl(listener_cb, SUDO_DEBUG_UTIL); memset(&sa_un, 0, sizeof(sa_un)); sock = accept(fd, &sa_un.sa, &salen); if (sock != -1) { if (logsrvd_conf_server_tcp_keepalive()) { int keepalive = 1; if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive)) == -1) { sudo_warn("SO_KEEPALIVE"); } } if (!new_connection(sock, l->tls, &sa_un, evbase)) { /* TODO: pause accepting on ENOMEM */ sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "unable to start new connection"); } } else { if (errno == EAGAIN || errno == EINTR) debug_return; /* TODO: pause accepting on ENFILE and EMFILE */ sudo_warn("accept"); } debug_return; } static bool register_listener(struct server_address *addr, struct sudo_event_base *evbase) { struct listener *l; int sock; debug_decl(register_listener, SUDO_DEBUG_UTIL); sock = create_listener(addr); if (sock == -1) debug_return_bool(false); /* TODO: make non-fatal */ if ((l = malloc(sizeof(*l))) == NULL) sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); l->sock = sock; l->tls = addr->tls; l->ev = sudo_ev_alloc(sock, SUDO_EV_READ|SUDO_EV_PERSIST, listener_cb, l); if (l->ev == NULL) sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); if (sudo_ev_add(evbase, l->ev, NULL, false) == -1) sudo_fatal("%s", U_("unable to add event to queue")); TAILQ_INSERT_TAIL(&listeners, l, entries); debug_return_bool(true); } /* * Register listeners and set the TLS verify callback. */ static bool server_setup(struct sudo_event_base *base) { struct server_address *addr; struct listener *l; int nlisteners = 0; bool ret; debug_decl(server_setup, SUDO_DEBUG_UTIL); /* Free old listeners (if any) and register new ones. */ while ((l = TAILQ_FIRST(&listeners)) != NULL) { TAILQ_REMOVE(&listeners, l, entries); sudo_ev_free(l->ev); close(l->sock); free(l); } TAILQ_FOREACH(addr, logsrvd_conf_server_listen_address(), entries) { nlisteners += register_listener(addr, base); } ret = nlisteners > 0; #if defined(HAVE_OPENSSL) if (ret) set_tls_verify_peer(); #endif debug_return_bool(ret); } /* * Reload config and re-initialize listeners. */ static void server_reload(struct sudo_event_base *evbase) { debug_decl(server_reload, SUDO_DEBUG_UTIL); sudo_debug_printf(SUDO_DEBUG_INFO, "reloading server config"); if (logsrvd_conf_read(conf_file)) { /* Re-initialize listeners. */ if (!server_setup(evbase)) sudo_fatalx("%s", U_("unable to setup listen socket")); /* Re-read sudo.conf and re-initialize debugging. */ sudo_debug_deregister(logsrvd_debug_instance); logsrvd_debug_instance = SUDO_DEBUG_INSTANCE_INITIALIZER; if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) != -1) { logsrvd_debug_instance = sudo_debug_register(getprogname(), NULL, NULL, sudo_conf_debug_files(getprogname()), -1); } } debug_return; } /* * Dump server information to the debug file. * Includes information about listeners and client connections. */ static void server_dump_stats(void) { struct server_address *addr; struct connection_closure *closure; int n; debug_decl(server_dump_stats, SUDO_DEBUG_UTIL); sudo_debug_printf(SUDO_DEBUG_INFO, "%s", server_id); sudo_debug_printf(SUDO_DEBUG_INFO, "configuration file: %s", conf_file); sudo_debug_printf(SUDO_DEBUG_INFO, "listen addresses:"); n = 0; TAILQ_FOREACH(addr, logsrvd_conf_server_listen_address(), entries) { union sockaddr_union *sa_un = &addr->sa_un; char ipaddr[INET6_ADDRSTRLEN]; switch (sa_un->sa.sa_family) { case AF_INET: inet_ntop(AF_INET, &sa_un->sin.sin_addr, ipaddr, sizeof(ipaddr)); break; #ifdef HAVE_STRUCT_IN6_ADDR case AF_INET6: inet_ntop(AF_INET6, &sa_un->sin6.sin6_addr, ipaddr, sizeof(ipaddr)); break; #endif /* HAVE_STRUCT_IN6_ADDR */ default: (void)strlcpy(ipaddr, "[unknown]", sizeof(ipaddr)); break; } sudo_debug_printf(SUDO_DEBUG_INFO, " %d: %s [%s]", ++n, addr->sa_str, ipaddr); } if (!TAILQ_EMPTY(&connections)) { n = 0; sudo_debug_printf(SUDO_DEBUG_INFO, "client connections:"); TAILQ_FOREACH(closure, &connections, entries) { struct relay_closure *relay_closure = closure->relay_closure; n++; if (closure->sock == -1) { sudo_debug_printf(SUDO_DEBUG_INFO, " %2d: journal %s", n, closure->journal_path ? closure->journal_path : "none"); sudo_debug_printf(SUDO_DEBUG_INFO, " %2d: fd %d", n, closure->journal ? fileno(closure->journal) : -1); } else { sudo_debug_printf(SUDO_DEBUG_INFO, " %2d: addr %s%s", n, closure->ipaddr, closure->tls ? " (TLS)" : ""); sudo_debug_printf(SUDO_DEBUG_INFO, " %2d: sock %d", n, closure->sock); } if (relay_closure != NULL) { sudo_debug_printf(SUDO_DEBUG_INFO, " relay: %s (%s)", relay_closure->relay_name.name, relay_closure->relay_name.ipaddr); sudo_debug_printf(SUDO_DEBUG_INFO, " relay sock: %d", relay_closure->sock); } sudo_debug_printf(SUDO_DEBUG_INFO, " state: %d", closure->state); if (closure->errstr != NULL) { sudo_debug_printf(SUDO_DEBUG_INFO, " error: %s", closure->errstr); } sudo_debug_printf(SUDO_DEBUG_INFO, " log I/O: %s", closure->log_io ? "true" : "false"); sudo_debug_printf(SUDO_DEBUG_INFO, " store first: %s", closure->store_first ? "true" : "false"); if (sudo_timespecisset(&closure->elapsed_time)) { sudo_debug_printf(SUDO_DEBUG_INFO, " elapsed time: [%lld, %ld]", (long long)closure->elapsed_time.tv_sec, (long)closure->elapsed_time.tv_nsec); } } sudo_debug_printf(SUDO_DEBUG_INFO, "%d client connection(s)\n", n); } logsrvd_queue_dump(); debug_return; } static void signal_cb(int signo, int what, void *v) { struct sudo_event_base *base = v; debug_decl(signal_cb, SUDO_DEBUG_UTIL); switch (signo) { case SIGHUP: server_reload(base); break; case SIGINT: case SIGTERM: /* Shut down active connections. */ server_shutdown(base); break; case SIGUSR1: server_dump_stats(); break; default: sudo_warnx(U_("unexpected signal %d"), signo); break; } debug_return; } static void register_signal(int signo, struct sudo_event_base *base) { struct sudo_event *ev; debug_decl(register_signal, SUDO_DEBUG_UTIL); ev = sudo_ev_alloc(signo, SUDO_EV_SIGNAL, signal_cb, base); if (ev == NULL) sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); if (sudo_ev_add(base, ev, NULL, false) == -1) sudo_fatal("%s", U_("unable to add event to queue")); debug_return; } static void logsrvd_cleanup(void) { /* TODO: cleanup like on signal */ return; } /* * Write the process ID into a file, typically /var/run/sudo/sudo_logsrvd.pid. * If the parent directory doesn't exist, it will be created. */ static void write_pidfile(void) { FILE *fp; int dfd, fd; mode_t oldmask; const char *pid_file = logsrvd_conf_pid_file(); debug_decl(write_pidfile, SUDO_DEBUG_UTIL); if (pid_file == NULL) debug_return; /* Default logsrvd umask is more restrictive (077). */ oldmask = umask(S_IWGRP|S_IWOTH); dfd = sudo_open_parent_dir(pid_file, ROOT_UID, ROOT_GID, S_IRWXU|S_IXGRP|S_IXOTH, false); if (dfd != -1) { const char *base = sudo_basename(pid_file); fd = openat(dfd, base, O_WRONLY|O_CREAT|O_NOFOLLOW, 0644); if (fd == -1 || (fp = fdopen(fd, "w")) == NULL) { sudo_warn("%s", pid_file); if (fd != -1) close(fd); } else { fprintf(fp, "%u\n", (unsigned int)getpid()); fflush(fp); if (ferror(fp)) sudo_warn("%s", pid_file); fclose(fp); } close(dfd); } umask(oldmask); debug_return; } /* * Fork, detach from the terminal and write pid file unless nofork set. */ static void daemonize(bool nofork) { int fd; debug_decl(daemonize, SUDO_DEBUG_UTIL); if (chdir("/") == -1) sudo_warn("chdir(\"/\")"); if (!nofork) { switch (sudo_debug_fork()) { case -1: sudo_fatal("fork"); case 0: /* child */ break; default: /* parent, exit */ _exit(EXIT_SUCCESS); } /* detach from terminal and write pid file. */ if (setsid() == -1) sudo_fatal("setsid"); write_pidfile(); if ((fd = open(_PATH_DEVNULL, O_RDWR)) != -1) { (void) dup2(fd, STDIN_FILENO); (void) dup2(fd, STDOUT_FILENO); (void) dup2(fd, STDERR_FILENO); if (fd > STDERR_FILENO) (void) close(fd); } } else { if ((fd = open(_PATH_DEVNULL, O_RDWR)) != -1) { /* Preserve stdout/stderr in nofork mode (if open). */ (void) dup2(fd, STDIN_FILENO); if (fcntl(STDOUT_FILENO, F_GETFL) == -1) (void) dup2(fd, STDOUT_FILENO); if (fcntl(STDERR_FILENO, F_GETFL) == -1) (void) dup2(fd, STDERR_FILENO); if (fd > STDERR_FILENO) (void) close(fd); } } /* Disable logging to stderr after we become a daemon. */ logsrvd_warn_stderr(false); debug_return; } static void usage(bool fatal) { fprintf(stderr, "usage: %s [-n] [-f conf_file] [-R percentage]\n", getprogname()); if (fatal) exit(EXIT_FAILURE); } static void help(void) { printf("%s - %s\n\n", getprogname(), _("sudo log server")); usage(false); printf("\n%s\n", _("Options:")); printf(" -f, --file %s\n", _("path to configuration file")); printf(" -h, --help %s\n", _("display help message and exit")); printf(" -n, --no-fork %s\n", _("do not fork, run in the foreground")); printf(" -R, --random-drop %s\n", _("percent chance connections will drop")); printf(" -V, --version %s\n", _("display version information and exit")); putchar('\n'); exit(EXIT_SUCCESS); } static const char short_opts[] = "f:hnR:V"; static struct option long_opts[] = { { "file", required_argument, NULL, 'f' }, { "help", no_argument, NULL, 'h' }, { "no-fork", no_argument, NULL, 'n' }, { "random-drop", required_argument, NULL, 'R' }, { "version", no_argument, NULL, 'V' }, { NULL, no_argument, NULL, 0 }, }; sudo_dso_public int main(int argc, char *argv[]); int main(int argc, char *argv[]) { struct sudo_event_base *evbase; bool nofork = false; int ch; debug_decl_vars(main, SUDO_DEBUG_MAIN); #if defined(SUDO_DEVEL) && defined(__OpenBSD__) { extern char *malloc_options; malloc_options = "S"; } #endif initprogname(argc > 0 ? argv[0] : "sudo_logsrvd"); setlocale(LC_ALL, ""); bindtextdomain("sudo", LOCALEDIR); /* XXX - add logsrvd domain */ textdomain("sudo"); /* Create files readable/writable only by owner. */ umask(S_IRWXG|S_IRWXO); /* Register fatal/fatalx callback. */ sudo_fatal_callback_register(logsrvd_cleanup); /* Read sudo.conf and initialize the debug subsystem. */ if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1) exit(EXIT_FAILURE); logsrvd_debug_instance = sudo_debug_register(getprogname(), NULL, NULL, sudo_conf_debug_files(getprogname()), -1); if (protobuf_c_version_number() < 1003000) sudo_fatalx("%s", U_("Protobuf-C version 1.3 or higher required")); while ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { switch (ch) { case 'f': conf_file = optarg; break; case 'h': help(); break; case 'n': nofork = true; break; case 'R': /* random connection drop probability as a percentage (debug) */ if (!set_random_drop(optarg)) sudo_fatalx(U_("invalid random drop value: %s"), optarg); break; case 'V': (void)printf(_("%s version %s\n"), getprogname(), PACKAGE_VERSION); return 0; default: usage(true); } } /* Read sudo_logsrvd.conf */ if (!logsrvd_conf_read(conf_file)) exit(EXIT_FAILURE); if ((evbase = sudo_ev_base_alloc()) == NULL) sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); /* Initialize listeners. */ if (!server_setup(evbase)) sudo_fatalx("%s", U_("unable to setup listen socket")); register_signal(SIGHUP, evbase); register_signal(SIGINT, evbase); register_signal(SIGTERM, evbase); register_signal(SIGUSR1, evbase); /* Point of no return. */ daemonize(nofork); signal(SIGPIPE, SIG_IGN); logsrvd_queue_scan(evbase); sudo_ev_dispatch(evbase); if (!nofork && logsrvd_conf_pid_file() != NULL) unlink(logsrvd_conf_pid_file()); logsrvd_conf_cleanup(); debug_return_int(1); }