/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2019-2023, 2025 Todd C. Miller <Todd.Miller@sudo.ws>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#ifdef HAVE_STDBOOL_H
# include <stdbool.h>
#else
# include <compat/stdbool.h>
#endif /* HAVE_STDBOOL_H */
#if defined(HAVE_STDINT_H)
# include <stdint.h>
#elif defined(HAVE_INTTYPES_H)
# include <inttypes.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#define NEED_INET_NTOP		/* to expose sudo_inet_ntop in sudo_compat.h */

#include <sudo_compat.h>
#include <sudo_debug.h>
#include <sudo_event.h>
#include <sudo_eventlog.h>
#include <sudo_gettext.h>
#include <sudo_iolog.h>
#include <sudo_fatal.h>
#include <sudo_queue.h>
#include <sudo_util.h>

#include <logsrvd.h>

static void relay_client_msg_cb(int fd, int what, void *v);
static void relay_server_msg_cb(int fd, int what, void *v);
static void connect_cb(int sock, int what, void *v);
static bool start_relay(int sock, struct connection_closure *closure);
static void tls_connect_error_fn(struct tls_client_closure *tls_client);

/*
 * Free a struct relay_closure container and its contents.
 */
void
relay_closure_free(struct relay_closure *relay_closure)
{
    struct connection_buffer *buf;
    debug_decl(relay_closure_free, SUDO_DEBUG_UTIL);

#if defined(HAVE_OPENSSL)
    if (relay_closure->tls_client.ssl != NULL) {
	sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	    "closing down TLS connection to %s",
	    relay_closure->relay_name.name);
	if (SSL_shutdown(relay_closure->tls_client.ssl) == 0)
	    SSL_shutdown(relay_closure->tls_client.ssl);
	SSL_free(relay_closure->tls_client.ssl);
    }
#endif
    if (relay_closure->relays != NULL)
	address_list_delref(relay_closure->relays);
    sudo_rcstr_delref(relay_closure->relay_name.name);
    sudo_ev_free(relay_closure->read_ev);
    sudo_ev_free(relay_closure->write_ev);
    sudo_ev_free(relay_closure->connect_ev);
    free(relay_closure->read_buf.data);
    while ((buf = TAILQ_FIRST(&relay_closure->write_bufs)) != NULL) {
	TAILQ_REMOVE(&relay_closure->write_bufs, buf, entries);
	free(buf->data);
	free(buf);
    }
    if (relay_closure->sock != -1) {
	shutdown(relay_closure->sock, SHUT_RDWR);
	close(relay_closure->sock);
    }
    free(relay_closure);

    debug_return;
}

/*
 * Allocate a relay closure.
 * Note that allocation of the events is deferred until we know the socket.
 */
static struct relay_closure *
relay_closure_alloc(void)
{
    struct relay_closure *relay_closure;
    debug_decl(relay_closure_alloc, SUDO_DEBUG_UTIL);

    if ((relay_closure = calloc(1, sizeof(*relay_closure))) == NULL)
	debug_return_ptr(NULL);

    /* We take a reference to relays so it doesn't change while connecting. */
    relay_closure->sock = -1;
    relay_closure->relays = logsrvd_conf_relay_address();
    address_list_addref(relay_closure->relays);
    TAILQ_INIT(&relay_closure->write_bufs);

    relay_closure->read_buf.size = 8 * 1024;
    relay_closure->read_buf.data = malloc(relay_closure->read_buf.size);
    if (relay_closure->read_buf.data == NULL)
	goto bad;

    debug_return_ptr(relay_closure);
bad:
    relay_closure_free(relay_closure);
    debug_return_ptr(NULL);
}

/*
 * Allocate a new buffer, copy buf to it and insert on the write queue.
 * On success the relay write event is enabled.
 * The length parameter does not include space for the message's wire size.
 */
static bool
relay_enqueue_write(const uint8_t *msgbuf, size_t len,
    struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    struct connection_buffer *buf;
    uint32_t msg_len;
    bool ret = false;
    debug_decl(relay_enqueue_write, SUDO_DEBUG_UTIL);

    /* Wire message size is used for length encoding, precedes message. */
    msg_len = htonl((uint32_t)len);

    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	"size + client message %zu bytes", len);

    if ((buf = get_free_buf(sizeof(msg_len) + len, closure)) == NULL) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
	    "unable to allocate connection_buffer");
	goto done;
    }
    memcpy(buf->data, &msg_len, sizeof(msg_len));
    memcpy(buf->data + sizeof(msg_len), msgbuf, len);
    buf->len = sizeof(msg_len) + len;

    if (sudo_ev_add(closure->evbase, relay_closure->write_ev, NULL, false) == -1) {
	sudo_warnx("%s", U_("unable to add event to queue"));
	goto done;
    }

    TAILQ_INSERT_TAIL(&relay_closure->write_bufs, buf, entries);
    buf = NULL;

    ret = true;

done:
    if (buf != NULL) {
	free(buf->data);
	free(buf);
    }
    debug_return_bool(ret);
}

/*
 * Format a ClientMessage and store the wire format message in buf.
 * Returns true on success, false on failure.
 */
static bool
fmt_client_message(struct connection_closure *closure, ClientMessage *msg)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    struct connection_buffer *buf = NULL;
    uint32_t msg_len;
    bool ret = false;
    size_t len;
    debug_decl(fmt_client_message, SUDO_DEBUG_UTIL);

    len = client_message__get_packed_size(msg);
    if (len > MESSAGE_SIZE_MAX) {
	sudo_warnx(U_("client message too large: %zu"), len);
        goto done;
    }

    /* Wire message size is used for length encoding, precedes message. */
    msg_len = htonl((uint32_t)len);
    len += sizeof(msg_len);

    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	"size + client message %zu bytes", len);

    if ((buf = get_free_buf(len, closure)) == NULL) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
	    "unable to allocate connection_buffer");
        goto done;
    }
    memcpy(buf->data, &msg_len, sizeof(msg_len));
    client_message__pack(msg, buf->data + sizeof(msg_len));
    buf->len = len;
    TAILQ_INSERT_TAIL(&relay_closure->write_bufs, buf, entries);

    ret = true;

done:
    debug_return_bool(ret);
}

static bool
fmt_client_hello(struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    ClientMessage client_msg = CLIENT_MESSAGE__INIT;
    ClientHello hello_msg = CLIENT_HELLO__INIT;
    bool ret;
    debug_decl(fmt_client_hello, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending ClientHello", __func__);
    hello_msg.client_id = (char *)"Sudo Logsrvd " PACKAGE_VERSION;

    client_msg.u.hello_msg = &hello_msg;
    client_msg.type_case = CLIENT_MESSAGE__TYPE_HELLO_MSG;
    ret = fmt_client_message(closure, &client_msg);
    if (ret) {
	if (sudo_ev_add(closure->evbase, relay_closure->read_ev, NULL, false) == -1) {
	    sudo_warnx("%s", U_("unable to add event to queue"));
	    ret = false;
	}
	if (sudo_ev_add(closure->evbase, relay_closure->write_ev, NULL, false) == -1) {
	    sudo_warnx("%s", U_("unable to add event to queue"));
	    ret = false;
	}
    }

    debug_return_bool(ret);
}

#if defined(HAVE_OPENSSL)
/* Wrapper for start_relay() called via tls_connect_cb() */
static bool
tls_client_start_fn(struct tls_client_closure *tls_client)
{
    sudo_ev_free(tls_client->tls_connect_ev);
    tls_client->tls_connect_ev = NULL;
    return start_relay(SSL_get_fd(tls_client->ssl), tls_client->parent_closure);
}

/* Perform TLS connection to the relay host. */
static bool
connect_relay_tls(struct connection_closure *closure)
{
    struct tls_client_closure *tls_client = &closure->relay_closure->tls_client;
    const struct timespec *timeout = logsrvd_conf_relay_connect_timeout();
    SSL_CTX *ssl_ctx = logsrvd_relay_tls_ctx();
    debug_decl(connect_relay_tls, SUDO_DEBUG_UTIL);

    /* Populate struct tls_client_closure. */
    tls_client->parent_closure = closure;
    tls_client->evbase = closure->evbase;
    tls_client->tls_connect_ev = sudo_ev_alloc(closure->relay_closure->sock,
	SUDO_EV_WRITE, tls_connect_cb, tls_client);
    if (tls_client->tls_connect_ev == NULL)
        goto bad;
    tls_client->peer_name = &closure->relay_closure->relay_name;
    if (timeout != NULL) {
	tls_client->connect_timeout = *timeout;
    } else {
	sudo_timespecclear(&tls_client->connect_timeout);
    }
    tls_client->start_fn = tls_client_start_fn;
    tls_client->connect_error_fn = tls_connect_error_fn;
    if (!tls_ctx_client_setup(ssl_ctx, closure->relay_closure->sock, tls_client))
        goto bad;

    debug_return_bool(true);
bad:
    debug_return_bool(false);
}
#endif /* HAVE_OPENSSL */

/*
 * Try to connect to the next relay host.
 * Returns 0 on success, -1 on error, setting errno.
 * If there is no next relay, errno is set to ENOENT.
 */
static int
connect_relay_next(struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    struct server_address *relay;
    int ret, sock = -1;
    char *addr;
    debug_decl(connect_relay_next, SUDO_DEBUG_UTIL);

    /* Get next relay or return ENOENT none are left. */
    if (relay_closure->relay_addr != NULL) {
	relay = TAILQ_NEXT(relay_closure->relay_addr, entries);
    } else {
	relay = TAILQ_FIRST(relay_closure->relays);
    }
    if (relay == NULL) {
	errno = ENOENT;
	goto bad;
    }
    relay_closure->relay_addr = relay;

    sock = socket(relay->sa_un.sa.sa_family, SOCK_STREAM, 0);
    if (sock == -1) {
	sudo_warn("socket");
	goto bad;
    }
    if (logsrvd_conf_relay_tcp_keepalive()) {
	int keepalive = 1;
	if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive,
		sizeof(keepalive)) == -1) {
	    sudo_warn("SO_KEEPALIVE");
	}
    }
    ret = fcntl(sock, F_GETFL, 0);
    if (ret == -1 || fcntl(sock, F_SETFL, ret | O_NONBLOCK) == -1) {
	sudo_warn("fcntl(O_NONBLOCK)");
	goto bad;
    }

    ret = connect(sock, &relay->sa_un.sa, relay->sa_size);
    if (ret == -1 && errno != EINPROGRESS)
	goto bad;

    switch (relay->sa_un.sa.sa_family) {
    case AF_INET:
	addr = (char *)&relay->sa_un.sin.sin_addr;
	break;
#ifdef HAVE_STRUCT_IN6_ADDR
    case AF_INET6:
	addr = (char *)&relay->sa_un.sin6.sin6_addr;
	break;
#endif
    default:
	errno = EAFNOSUPPORT;
	sudo_warn("connect");
	goto bad;
    }
    inet_ntop(relay->sa_un.sa.sa_family, addr,
	relay_closure->relay_name.ipaddr,
	sizeof(relay_closure->relay_name.ipaddr));
    relay_closure->relay_name.name = sudo_rcstr_addref(relay->sa_host);

    if (ret == 0) {
	if (relay_closure->sock != -1) {
	    shutdown(relay_closure->sock, SHUT_RDWR);
	    close(relay_closure->sock);
	}
	relay_closure->sock = sock;
#if defined(HAVE_OPENSSL)
	/* Relay connection succeeded, start TLS handshake. */
	if (relay_closure->relay_addr->tls) {
	    if (!connect_relay_tls(closure))
		goto bad;
	} else
#endif
	{
	    /* Connection succeeded without blocking. */
	    if (!start_relay(sock, closure))
		goto bad;
	}
    } else {
	/* Connection will be completed in connect_cb(). */
	relay_closure->connect_ev = sudo_ev_alloc(sock, SUDO_EV_WRITE,
	    connect_cb, closure);
	if (relay_closure->connect_ev == NULL)
	    goto bad;
	if (sudo_ev_add(closure->evbase, relay_closure->connect_ev,
		logsrvd_conf_relay_connect_timeout(), false) == -1) {
	    sudo_warnx("%s", U_("unable to add event to queue"));
	    goto bad;
	}
	if (relay_closure->sock != -1) {
	    shutdown(relay_closure->sock, SHUT_RDWR);
	    close(relay_closure->sock);
	}
	relay_closure->sock = sock;
	closure->state = CONNECTING;
    }
    debug_return_int(ret);

bad:
    /* Connection or system error. */
    if (sock != -1) {
	shutdown(sock, SHUT_RDWR);
	close(sock);
    }
    sudo_rcstr_delref(relay_closure->relay_name.name);
    relay_closure->relay_name.name = NULL;
    sudo_ev_free(relay_closure->connect_ev);
    relay_closure->connect_ev = NULL;
    debug_return_int(-1);
}

static void
connect_cb(int sock, int what, void *v)
{
    struct connection_closure *closure = v;
    struct relay_closure *relay_closure = closure->relay_closure;
    int errnum, optval, ret;
    socklen_t optlen = sizeof(optval);
    debug_decl(connect_cb, SUDO_DEBUG_UTIL);

    if (what == SUDO_EV_TIMEOUT) {
	errnum = ETIMEDOUT;
    } else {
	ret = getsockopt(sock, SOL_SOCKET, SO_ERROR, &optval, &optlen);
	errnum = ret == 0 ? optval : errno;
    }
    if (errnum == 0) {
	closure->state = INITIAL;
#if defined(HAVE_OPENSSL)
	/* Relay connection succeeded, start TLS handshake. */
	if (relay_closure->relay_addr->tls) {
	    if (!connect_relay_tls(closure)) {
		closure->errstr = _("TLS handshake with relay host failed");
		if (!schedule_error_message(closure->errstr, closure))
		    connection_close(closure);
	    }
	} else
#endif
	{
	    /* Relay connection succeeded, start talking to the client.  */
	    if (!start_relay(sock, closure)) {
		closure->errstr = _("unable to allocate memory");
		if (!schedule_error_message(closure->errstr, closure))
		    connection_close(closure);
	    }
	}
    } else {
	/* Connection failed, try next relay (if any). */
	int res;
	sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
	    "unable to connect to relay %s (%s): %s",
	    relay_closure->relay_name.name, relay_closure->relay_name.ipaddr,
	    strerror(errnum));
	while ((res = connect_relay_next(closure)) == -1) {
	    if (errno == ENOENT || errno == EINPROGRESS) {
		/* Out of relays or connecting asynchronously. */
		break;
	    }
	}
	if (res == -1 && errno != EINPROGRESS) {
	    closure->errstr = _("unable to connect to relay host");
	    if (!schedule_error_message(closure->errstr, closure))
		connection_close(closure);
	}
    }

    debug_return;
}

/* Connect to the first available relay host. */
bool
connect_relay(struct connection_closure *closure)
{
    struct relay_closure *relay_closure;
    int res;
    debug_decl(connect_relay, SUDO_DEBUG_UTIL);

    relay_closure = closure->relay_closure = relay_closure_alloc();
    if (relay_closure == NULL)
	debug_return_bool(false);

    while ((res = connect_relay_next(closure)) == -1) {
	if (errno == ENOENT || errno == EINPROGRESS) {
	    /* Out of relays or connecting asynchronously. */
	    break;
	}
    }

    if (res == -1 && errno != EINPROGRESS)
	debug_return_bool(false);

    /* Switch to relay client message handlers. */
    closure->cms = &cms_relay;
    debug_return_bool(true);
}

/* Called on TLS connection error. */
static void
tls_connect_error_fn(struct tls_client_closure *tls_client)
{
    struct connection_closure *closure = tls_client->parent_closure;
    int res;

    /* TLS connection failed, try next relay (if any). */
    while ((res = connect_relay_next(closure)) == -1) {
	if (errno == ENOENT || errno == EINPROGRESS) {
	    /* Out of relays or connecting asynchronously. */
	    break;
	}
    }
    if (res == -1 && errno != EINPROGRESS) {
	closure->errstr = _("unable to connect to relay host");
	if (!schedule_error_message(closure->errstr, closure))
	    connection_close(closure);
    }
}

/*
 * Respond to a ServerHello message from the relay.
 * Returns true on success, false on error.
 */
static bool
handle_server_hello(const ServerHello *msg, struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    debug_decl(handle_server_hello, SUDO_DEBUG_UTIL);

    if (closure->state != INITIAL) {
	sudo_warnx(U_("unexpected state %d for %s"), closure->state,
	    relay_closure->relay_name.ipaddr);
	closure->errstr = _("state machine error");
	debug_return_bool(false);
    }

    /* Check that ServerHello is valid. */
    if (msg == NULL || msg->server_id == NULL || msg->server_id[0] == '\0') {
	sudo_warnx(U_("%s: invalid ServerHello, missing server_id"),
	    relay_closure->relay_name.ipaddr);
	closure->errstr = _("invalid ServerHello");
	debug_return_bool(false);
    }

    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	"relay server %s (%s) ID %s", relay_closure->relay_name.name,
	relay_closure->relay_name.ipaddr, msg->server_id);

    /* TODO: handle redirect */

    debug_return_bool(true);
}

/*
 * Respond to a commit_point ServerMessage from the relay.
 * Returns true on success, false on error.
 */
static bool
handle_commit_point(const TimeSpec *commit_point,
    struct connection_closure *closure)
{
    debug_decl(handle_commit_point, SUDO_DEBUG_UTIL);

    if (closure->state < RUNNING) {
	sudo_warnx(U_("unexpected state %d for %s"), closure->state,
	    closure->relay_closure->relay_name.ipaddr);
	closure->errstr = _("state machine error");
	debug_return_bool(false);
    }

    /* Check that ServerMessage's commit_point is valid. */
    if (commit_point == NULL) {
	sudo_warnx(U_("%s: invalid ServerMessage, missing commit_point"),
	    closure->relay_closure->relay_name.ipaddr);
	closure->errstr = _("invalid ServerMessage");
	debug_return_bool(false);
    }

    /* Pass commit point from relay to client. */
    debug_return_bool(schedule_commit_point(commit_point, closure));
}

/*
 * Respond to a log_id ServerMessage from the relay.
 * Returns true on success, false on error.
 */
static bool
handle_log_id(const char *id, struct connection_closure *closure)
{
    ServerMessage msg = SERVER_MESSAGE__INIT;
    bool ret = false;
    debug_decl(handle_log_id, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
	"log ID %s from relay %s (%s)", id,
	closure->relay_closure->relay_name.name,
	closure->relay_closure->relay_name.ipaddr);

    /* No client connection when replaying a journaled entry. */
    if (closure->write_ev == NULL)
	debug_return_bool(true);

    if (id[0] == '\0') {
	sudo_warnx(U_("%s: invalid ServerMessage, missing log_id string"),
	    closure->relay_closure->relay_name.ipaddr);
	closure->errstr = _("invalid ServerMessage");
	goto done;
    }

    /*
     * We currently pass log_id to the client without modifying it.
     * TODO: append relay host to the log_id so we can restart the
     *       session with the proper relay if more than one is configured.
     */
    msg.u.log_id = (char *)id;
    msg.type_case = SERVER_MESSAGE__TYPE_LOG_ID;
    ret = fmt_server_message(closure, &msg);
    if (ret) {
	if (sudo_ev_add(closure->evbase, closure->write_ev,
		logsrvd_conf_relay_timeout(), false) == -1) {
	    sudo_warnx("%s", U_("unable to add event to queue"));
	    ret = false;
	}
    }

done:
    debug_return_bool(ret);
}

/*
 * Respond to an error ServerMessage from the relay.
 * Returns true on success, false on error.
 */
static bool
handle_server_error(const char *errmsg, struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    debug_decl(handle_server_error, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
	"error message received from relay %s (%s): %s",
	relay_closure->relay_name.name, relay_closure->relay_name.ipaddr,
	errmsg);

    /* Server will drop connection after the error message. */
    sudo_ev_del(closure->evbase, closure->relay_closure->read_ev);
    sudo_ev_del(closure->evbase, closure->relay_closure->write_ev);

    /* Missing error string. */
    if (errmsg[0] == '\0')
	errmsg = "unknown error";

    if (!schedule_error_message(errmsg, closure))
	debug_return_bool(false);

    debug_return_bool(true);
}

/*
 * Respond to an abort ServerMessage from the relay.
 * Returns true on success, false on error.
 */
static bool
handle_server_abort(const char *errmsg, struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    debug_decl(handle_server_abort, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
	"abort message received from relay %s (%s): %s",
	relay_closure->relay_name.name, relay_closure->relay_name.ipaddr,
	errmsg);

    /* Missing error string. */
    if (errmsg[0] == '\0')
	errmsg = "unknown error";

    if (!schedule_error_message(errmsg, closure))
	debug_return_bool(false);

    debug_return_bool(true);
}

/*
 * Respond to a ServerMessage from the relay.
 * Returns true on success, false on error.
 */
static bool
handle_server_message(const uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    ServerMessage *msg;
    bool ret = false;
    debug_decl(handle_server_message, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: unpacking ServerMessage", __func__);
    msg = server_message__unpack(NULL, len, buf);
    if (msg == NULL) {
	sudo_warnx(U_("unable to unpack %s size %zu"), "ServerMessage", len);
	debug_return_bool(false);
    }

    switch (msg->type_case) {
    case SERVER_MESSAGE__TYPE_HELLO:
	if ((ret = handle_server_hello(msg->u.hello, closure))) {
	    /* Relay server said hello, start talking to client. */
	    ret = start_protocol(closure);
	}
	break;
    case SERVER_MESSAGE__TYPE_COMMIT_POINT:
	ret = handle_commit_point(msg->u.commit_point, closure);
	break;
    case SERVER_MESSAGE__TYPE_LOG_ID:
	ret = handle_log_id(msg->u.log_id, closure);
	break;
    case SERVER_MESSAGE__TYPE_ERROR:
	ret = handle_server_error(msg->u.error, closure);
	break;
    case SERVER_MESSAGE__TYPE_ABORT:
	ret = handle_server_abort(msg->u.abort, closure);
	break;
    default:
	sudo_warnx(U_("unexpected type_case value %d in %s from %s"),
	    msg->type_case, "ServerMessage",
	    closure->relay_closure->relay_name.ipaddr);
	closure->errstr = _("unrecognized ServerMessage type");
	break;
    }

    server_message__free_unpacked(msg, NULL);
    debug_return_bool(ret);
}

/*
 * Read and unpack a ServerMessage from the relay (read callback).
 */
static void
relay_server_msg_cb(int fd, int what, void *v)
{
    struct connection_closure *closure = v;
    struct relay_closure *relay_closure = closure->relay_closure;
    struct connection_buffer *buf = &relay_closure->read_buf;
    size_t nread;
    uint32_t msg_len;
    debug_decl(relay_server_msg_cb, SUDO_DEBUG_UTIL);

    /* For TLS we may need to read as part of SSL_write_ex(). */
    if (relay_closure->write_instead_of_read) {
	relay_closure->write_instead_of_read = false;
        relay_client_msg_cb(fd, what, v);
        debug_return;
    }

    if (what == SUDO_EV_TIMEOUT) {
	sudo_warnx(U_("timed out reading from relay %s (%s)"),
	    relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);
	closure->errstr = _("timeout reading from relay");
        goto send_error;
    }

#if defined(HAVE_OPENSSL)
    if (relay_closure->tls_client.ssl != NULL) {
	SSL *ssl = relay_closure->tls_client.ssl;
	int result;

	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "%s: ServerMessage from relay %s (%s) [TLS]", __func__,
	    relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);
        result = SSL_read_ex(ssl, buf->data + buf->len, buf->size - buf->len,
	    &nread);
        if (result <= 0) {
	    unsigned long errcode;
	    const char *errstr;

            switch (SSL_get_error(ssl, result)) {
		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_ex 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_ex returns SSL_ERROR_WANT_WRITE");
		    if (!sudo_ev_pending(relay_closure->write_ev, SUDO_EV_WRITE, NULL)) {
			/* Enable a temporary write event. */
			if (sudo_ev_add(closure->evbase, relay_closure->write_ev, NULL, false) == -1) {
			    sudo_warnx("%s", U_("unable to add event to queue"));
			    closure->errstr = _("unable to allocate memory");
			    goto send_error;
			}
			relay_closure->temporary_write_event = true;
		    }
		    /* Redirect write event to finish SSL_read_ex() */
		    relay_closure->read_instead_of_write = true;
                    debug_return;
                case SSL_ERROR_SSL:
                    /*
                     * For TLS 1.3, if the cert verify function on the server
                     * returns an error, OpenSSL will send an internal error
                     * alert when we read ServerHello.  Convert to a more useful
                     * message and hope that no actual internal error occurs.
                     */
                    errcode = ERR_get_error();
#if !defined(HAVE_WOLFSSL)
                    if (closure->state == INITIAL &&
                        ERR_GET_REASON(errcode) == SSL_R_TLSV1_ALERT_INTERNAL_ERROR) {
                        errstr = _("relay host name does not match certificate");
			closure->errstr = errstr;
                    } else
#endif
		    {
                        errstr = ERR_reason_error_string(errcode);
			closure->errstr = _("error reading from relay");
                    }
		    sudo_warnx("%s: SSL_read_ex: %s",
			relay_closure->relay_name.ipaddr,
			errstr ? errstr : strerror(errno));
                    goto send_error;
                case SSL_ERROR_SYSCALL:
		    if (nread == 0) {
			/* EOF, handled below */
			sudo_warnx(U_("EOF from %s without proper TLS shutdown"),
			    relay_closure->relay_name.ipaddr);
			break;
		    }
		    sudo_warn("%s: SSL_read_ex", relay_closure->relay_name.ipaddr);
		    closure->errstr = _("error reading from relay");
                    goto send_error;
                default:
                    errstr = ERR_reason_error_string(ERR_get_error());
		    sudo_warnx("%s: SSL_read_ex: %s",
			relay_closure->relay_name.ipaddr,
			errstr ? errstr : strerror(errno));
		    closure->errstr = _("error reading from relay");
                    goto send_error;
            }
        }
    } else
#endif
    {
	ssize_t n;

	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "%s: ServerMessage from relay %s (%s)", __func__,
	    relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);
	n = read(fd, buf->data + buf->len, buf->size - buf->len);
	if (n < 0) {
	    if (errno == EAGAIN || errno == EINTR)
		debug_return;
	    sudo_warn("%s: read", relay_closure->relay_name.ipaddr);
	    closure->errstr = _("error reading from relay");
	    goto send_error;
	}
	nread = (size_t)n;
    }

    sudo_debug_printf(SUDO_DEBUG_INFO,
	"%s: received %zd bytes from relay %s (%s)", __func__, nread,
	relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);
    if (nread == 0) {
	/* EOF from relay server, close the socket. */
	shutdown(relay_closure->sock, SHUT_RDWR);
	close(relay_closure->sock);
	relay_closure->sock = -1;
	sudo_ev_del(closure->evbase, relay_closure->read_ev);
	sudo_ev_del(closure->evbase, relay_closure->write_ev);

	if (closure->state != FINISHED) {
	    sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
		"premature EOF from %s (%s) [state %d]",
		relay_closure->relay_name.name,
		relay_closure->relay_name.ipaddr, closure->state);
	    closure->errstr = _("relay server closed connection");
	    goto send_error;
	}
	if (closure->sock == -1)
	    connection_close(closure);
	debug_return;
    }
    if (nread > SIZE_MAX - buf->len) {
	sudo_warnx(U_("internal error, %s overflow"), __func__);
	closure->errstr = _("error reading from relay");
	goto send_error;
    }
    buf->len += nread;

    while (buf->len - buf->off >= sizeof(msg_len)) {
	/* Read wire message size (uint32_t in network byte order). */
	memcpy(&msg_len, buf->data + buf->off, sizeof(msg_len));
	msg_len = ntohl(msg_len);

	if (msg_len > MESSAGE_SIZE_MAX) {
	    sudo_warnx(U_("server message too large: %zu"), (size_t)msg_len);
	    closure->errstr = _("server message too large");
	    goto send_error;
	}

	if (msg_len + sizeof(msg_len) > buf->len - buf->off) {
	    /* Incomplete message, we'll read the rest next time. */
	    if (!expand_buf(buf, msg_len + sizeof(msg_len))) {
		closure->errstr = _("unable to allocate memory");
		goto send_error;
	    }
	    debug_return;
	}

	/* Parse ServerMessage (could be zero bytes). */
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "%s: parsing ServerMessage, size %u", __func__, msg_len);
	buf->off += sizeof(msg_len);
	if (!handle_server_message(buf->data + buf->off, msg_len, closure))
	    goto send_error;
	buf->off += msg_len;
    }
    if (buf->len != buf->off) {
	memmove(buf->data, buf->data + buf->off, buf->len - buf->off);
    }
    buf->len -= buf->off;
    buf->off = 0;
    debug_return;

send_error:
    /*
     * Try to send client an error message before closing connection.
     * If we are already in an error state, just give up.
     */
    if (!schedule_error_message(closure->errstr, closure))
	goto close_connection;
    debug_return;

close_connection:
    connection_close(closure);
    debug_return;
}

/*
 * Forward a ClientMessage to the relay (write callback).
 */
static void
relay_client_msg_cb(int fd, int what, void *v)
{
    struct connection_closure *closure = v;
    struct relay_closure *relay_closure = closure->relay_closure;
    struct connection_buffer *buf;
    size_t nwritten;
    debug_decl(relay_client_msg_cb, SUDO_DEBUG_UTIL);

    /* For TLS we may need to write as part of SSL_read_ex(). */
    if (relay_closure->read_instead_of_write) {
	relay_closure->read_instead_of_write = false;
        /* Delete write event if it was only due to SSL_read_ex(). */
        if (relay_closure->temporary_write_event) {
            relay_closure->temporary_write_event = false;
            sudo_ev_del(closure->evbase, relay_closure->write_ev);
        }
        relay_server_msg_cb(fd, what, v);
        debug_return;
    }

    if (what == SUDO_EV_TIMEOUT) {
	sudo_warnx(U_("timed out writing to relay %s (%s)"),
	    relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);
	closure->errstr = _("timeout writing to relay");
        goto send_error;
    }

    if ((buf = TAILQ_FIRST(&relay_closure->write_bufs)) == NULL) {
	sudo_warnx(U_("missing write buffer for client %s"),
	    relay_closure->relay_name.ipaddr);
        goto close_connection;
    }

    sudo_debug_printf(SUDO_DEBUG_INFO, "%s: sending %zu bytes to server %s (%s)",
	__func__, buf->len - buf->off, relay_closure->relay_name.name,
	relay_closure->relay_name.ipaddr);

#if defined(HAVE_OPENSSL)
    if (relay_closure->tls_client.ssl != NULL) {
	SSL *ssl = relay_closure->tls_client.ssl;
        const int result = SSL_write_ex(ssl, buf->data + buf->off,
	    buf->len - buf->off, &nwritten);
        if (result <= 0) {
	    const char *errstr;

            switch (SSL_get_error(ssl, result)) {
		case SSL_ERROR_ZERO_RETURN:
		    /* ssl connection shutdown cleanly */
		    shutdown(relay_closure->sock, SHUT_RDWR);
		    close(relay_closure->sock);
		    relay_closure->sock = -1;
		    sudo_ev_del(closure->evbase, relay_closure->read_ev);
		    sudo_ev_del(closure->evbase, relay_closure->write_ev);

		    if (closure->state != FINISHED) {
			sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
			    "premature EOF from %s (state %d)",
			    relay_closure->relay_name.ipaddr, closure->state);
			closure->errstr = _("relay server closed connection");
			goto send_error;
		    }
		    debug_return;
                case SSL_ERROR_WANT_READ:
                    /* ssl wants to read, read event always active */
		    sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
			"SSL_write_ex returns SSL_ERROR_WANT_READ");
		    /* Redirect read event to finish SSL_write_ex() */
		    relay_closure->write_instead_of_read = true;
                    debug_return;
                case SSL_ERROR_WANT_WRITE:
		    /* ssl wants to write more, write event remains active */
		    sudo_debug_printf(SUDO_DEBUG_NOTICE|SUDO_DEBUG_LINENO,
			"SSL_write_ex returns SSL_ERROR_WANT_WRITE");
                    debug_return;
                case SSL_ERROR_SYSCALL:
		    sudo_warn("%s: SSL_write_ex",
			relay_closure->relay_name.ipaddr);
		    closure->errstr = _("error writing to relay");
		    goto send_error;
                default:
		    errstr = ERR_reason_error_string(ERR_get_error());
		    sudo_warnx("%s: SSL_write_ex: %s",
			relay_closure->relay_name.ipaddr,
			errstr ? errstr : strerror(errno));
		    closure->errstr = _("error writing to relay");
		    goto send_error;
            }
        }
    } else
#endif
    {
	const ssize_t n = write(fd, buf->data + buf->off, buf->len - buf->off);
	if (n < 0) {
	    if (errno == EAGAIN || errno == EINTR)
		debug_return;
	    sudo_warn("%s: write", relay_closure->relay_name.ipaddr);
	    closure->errstr = _("error writing to relay");
	    goto send_error;
	}
	nwritten = (size_t)n;
    }
    if (nwritten > SIZE_MAX - buf->off) {
	sudo_warnx(U_("internal error, %s overflow"), __func__);
	closure->errstr = _("error writing to relay");
	goto send_error;
    }
    buf->off += nwritten;

    if (buf->off == buf->len) {
	/* sent entire message, move buf to free list */
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "%s: finished sending %zu bytes to server", __func__, buf->len);
	buf->off = 0;
	buf->len = 0;
	TAILQ_REMOVE(&relay_closure->write_bufs, buf, entries);
	TAILQ_INSERT_TAIL(&closure->free_bufs, buf, entries);
	if (TAILQ_EMPTY(&relay_closure->write_bufs)) {
	    /* Write queue empty, check state. */
	    sudo_ev_del(closure->evbase, relay_closure->write_ev);
	    if (closure->error || closure->state == FINISHED ||
		    closure->state == SHUTDOWN)
		goto close_connection;
	}
    }
    debug_return;

send_error:
    /*
     * Try to send client an error message before closing connection.
     * If we are already in an error state, just give up.
     */
    if (!schedule_error_message(closure->errstr, closure))
	goto close_connection;
    debug_return;

close_connection:
    connection_close(closure);
    debug_return;
}

/* Begin the conversation with the relay host. */
static bool
start_relay(int sock, struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    debug_decl(start_relay, SUDO_DEBUG_UTIL);

    /* No longer need the connect event. */
    sudo_ev_free(relay_closure->connect_ev);
    relay_closure->connect_ev = NULL;

    /* Allocate relay read/write events now that we know the socket. */
    relay_closure->read_ev = sudo_ev_alloc(sock, SUDO_EV_READ|SUDO_EV_PERSIST,
	relay_server_msg_cb, closure);
    relay_closure->write_ev = sudo_ev_alloc(sock, SUDO_EV_WRITE|SUDO_EV_PERSIST,
	relay_client_msg_cb, closure);
    if (relay_closure->read_ev == NULL || relay_closure->write_ev == NULL)
	debug_return_bool(false);

    /* Start communication with the relay server by saying hello. */
    debug_return_bool(fmt_client_hello(closure));
}

/*
 * Relay an AcceptMessage from the client to the relay server.
 */
static bool
relay_accept(const AcceptMessage *msg, const uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(relay_accept, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO,
	"%s: relaying AcceptMessage from %s to %s (%s)", __func__, source,
	relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);

    debug_return_bool(relay_enqueue_write(buf, len, closure));
}

/*
 * Relay a RejectMessage from the client to the relay server.
 */
static bool
relay_reject(const RejectMessage *msg, const uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(relay_reject, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO,
	"%s: relaying RejectMessage from %s to %s (%s)", __func__, source,
	relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);

    debug_return_bool(relay_enqueue_write(buf, len, closure));
}

/*
 * Relay an ExitMessage from the client to the relay server.
 */
static bool
relay_exit(const ExitMessage *msg, const uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(relay_exit, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO,
	"%s: relaying ExitMessage from %s to %s (%s)", __func__, source,
	relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);

    debug_return_bool(relay_enqueue_write(buf, len, closure));
}

/*
 * Relay a RestartMessage from the client to the relay server.
 */
static bool
relay_restart(const RestartMessage *msg, const uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(relay_restart, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO,
	"%s: relaying RestartMessage from %s to %s (%s)", __func__, source,
	relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);

    debug_return_bool(relay_enqueue_write(buf, len, closure));
}

/*
 * Relay an AlertMessage from the client to the relay server.
 */
static bool
relay_alert(const AlertMessage *msg, const uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(relay_alert, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO,
	"%s: relaying AlertMessage from %s to %s (%s)", __func__, source,
	relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);

    debug_return_bool(relay_enqueue_write(buf, len, closure));
}

/*
 * Relay a CommandSuspend from the client to the relay server.
 */
static bool
relay_suspend(const CommandSuspend *msg, const uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(relay_suspend, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO,
	"%s: relaying CommandSuspend from %s to %s (%s)", __func__, source,
	relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);

    debug_return_bool(relay_enqueue_write(buf, len, closure));
}

/*
 * Relay a ChangeWindowSize from the client to the relay server.
 */
static bool
relay_winsize(const ChangeWindowSize *msg, const uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(relay_winsize, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO,
	"%s: relaying ChangeWindowSize from %s to %s (%s)", __func__, source,
	relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);

    debug_return_bool(relay_enqueue_write(buf, len, closure));
}

/*
 * Relay an IoBuffer from the client to the relay server.
 */
static bool
relay_iobuf(int iofd, const IoBuffer *iobuf, const uint8_t *buf, size_t len,
    struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    const char *source = closure->journal_path ? closure->journal_path :
	closure->ipaddr;
    debug_decl(relay_iobuf, SUDO_DEBUG_UTIL);

    sudo_debug_printf(SUDO_DEBUG_INFO,
	"%s: relaying IoBuffer from %s to %s (%s)", __func__, source,
	relay_closure->relay_name.name, relay_closure->relay_name.ipaddr);

    debug_return_bool(relay_enqueue_write(buf, len, closure));
}

/*
 * Shutdown relay connection when server is exiting.
 */
bool
relay_shutdown(struct connection_closure *closure)
{
    struct relay_closure *relay_closure = closure->relay_closure;
    debug_decl(relay_shutdown, SUDO_DEBUG_UTIL);

    /* Close connection unless relay events are pending. */
    if (!sudo_ev_pending(relay_closure->read_ev, SUDO_EV_READ, NULL) &&
	    !sudo_ev_pending(relay_closure->write_ev, SUDO_EV_WRITE, NULL) &&
	    TAILQ_EMPTY(&relay_closure->write_bufs)) {
	connection_close(closure);
    }

    debug_return_bool(true);
}

struct client_message_switch cms_relay = {
    relay_accept,
    relay_reject,
    relay_exit,
    relay_restart,
    relay_alert,
    relay_iobuf,
    relay_suspend,
    relay_winsize
};
