diff --git a/CMakeLists.txt b/CMakeLists.txt index 142d913920..b70c033c7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -582,6 +582,8 @@ CHECK_SYMBOL_EXISTS(TAILQ_FOREACH sys/queue.h EVENT__HAVE_TAILQFOREACH) CHECK_CONST_EXISTS(CTL_KERN sys/sysctl.h EVENT__HAVE_DECL_CTL_KERN) CHECK_CONST_EXISTS(KERN_ARND sys/sysctl.h EVENT__HAVE_DECL_KERN_ARND) CHECK_SYMBOL_EXISTS(F_SETFD fcntl.h EVENT__HAVE_SETFD) +CHECK_CONST_EXISTS(SO_TIMESTAMP sys/socket.h EVENT__HAVE_DECL_SO_TIMESTAMP) +CHECK_CONST_EXISTS(SO_TIMESTAMPNS sys/socket.h EVENT__HAVE_DECL_SO_TIMESTAMPNS) CHECK_TYPE_SIZE(fd_mask EVENT__HAVE_FD_MASK) @@ -841,7 +843,16 @@ if(EVENT__HAVE_EVENT_PORTS) endif() if (NOT EVENT__DISABLE_OPENSSL) - find_package(OpenSSL REQUIRED) + find_package(OpenSSL 3.0.0 REQUIRED) + + set(CMAKE_REQUIRED_INCLUDES ${OPENSSL_INCLUDE_DIR}) + check_symbol_exists(LIBRESSL_VERSION_NUMBER "openssl/opensslv.h" EVENT__HAVE_LIBRESSL) + unset(CMAKE_REQUIRED_INCLUDES) + if (EVENT__HAVE_LIBRESSL) + message(FATAL_ERROR + "Libevent requires OpenSSL >= 3.0.0; LibreSSL is not supported " + "(found headers reporting LIBRESSL_VERSION_NUMBER)") + endif() set(EVENT__HAVE_OPENSSL 1) diff --git a/ChangeLog b/ChangeLog index 471cfbda88..56fa87a3b1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,23 @@ +Changes in version 2.1.13-stable-couchbase (05 August 2026) + + This release applies additional patches on top of the upstream + 2.1.13-stable release to add new features required by Couchbase. + + New features (evbuffer, bufferevent): + - Add kernel socket receive timestamp support via BEV_OPT_RECV_TIMESTAMPS + and evbuffer_get_timestamp(), for TCP socket bufferevents. Currently + only supported on Linux (SO_TIMESTAMPNS/SO_TIMESTAMP); unsupported + on other platforms. + Build requirement changes (bufferevent_openssl): + - Libevent now requires OpenSSL >= 3.0.0 to build the openssl + bufferevent backend; older OpenSSL 1.x releases and LibreSSL + are no longer supported. + - CMake and Autotools now check the OpenSSL version (and reject + LibreSSL) at configure time, instead of failing later with a + compiler error in openssl-compat.h. + + + Changes in version 2.1.13-stable (01 July 2026) This release contains several security fixes, affecting users of the diff --git a/buffer.c b/buffer.c index f68f31c3e6..259315292c 100644 --- a/buffer.c +++ b/buffer.c @@ -722,7 +722,15 @@ advance_last_with_data(struct evbuffer *buf) int evbuffer_commit_space(struct evbuffer *buf, - struct evbuffer_iovec *vec, int n_vecs) + struct evbuffer_iovec *vec, int n_vecs) +{ + return evbuffer_commit_space_with_timespec(buf, vec, n_vecs, NULL); +} + +int +evbuffer_commit_space_with_timespec(struct evbuffer *buf, + struct evbuffer_iovec *vec, int n_vecs, + const struct timespec *ts) { struct evbuffer_chain *chain, **firstchainp, **chainp; int result = -1; @@ -744,6 +752,19 @@ evbuffer_commit_space(struct evbuffer *buf, goto done; buf->last->off += vec[0].iov_len; added = vec[0].iov_len; + if (ts && added) { + if (buf->last->timestamp.valid == 0 && buf->last->off == added) { + buf->last->timestamp.ts = *ts; + buf->last->timestamp.valid = 1; + } else if (buf->last->off != added) { + /* This chain already held data from an earlier + * commit; it can no longer represent a single + * timestamp for all of its bytes. Invalidate + * rather than let the earlier timestamp be + * silently misattributed to this newer data. */ + buf->last->timestamp.valid = 0; + } + } if (added) advance_last_with_data(buf); goto okay; @@ -773,6 +794,15 @@ evbuffer_commit_space(struct evbuffer *buf, for (i=0; ioff += vec[i].iov_len; added += vec[i].iov_len; + if (ts && vec[i].iov_len) { + if ((*chainp)->timestamp.valid == 0 && (*chainp)->off == vec[i].iov_len) { + (*chainp)->timestamp.ts = *ts; + (*chainp)->timestamp.valid = 1; + } else if ((*chainp)->off != vec[i].iov_len) { + /* See the n_vecs==1 case above. */ + (*chainp)->timestamp.valid = 0; + } + } if (vec[i].iov_len) { buf->last_with_datap = chainp; } @@ -842,10 +872,12 @@ PRESERVE_PINNED(struct evbuffer *src, struct evbuffer_chain **first, memcpy(tmp->buffer, chain->buffer + chain->misalign, chain->off); tmp->off = chain->off; + tmp->timestamp = chain->timestamp; *src->last_with_datap = tmp; src->last = tmp; chain->misalign += chain->off; chain->off = 0; + chain->timestamp.valid = 0; } else { src->last = *src->last_with_datap; *pinned = NULL; @@ -940,6 +972,7 @@ APPEND_CHAIN_MULTICAST(struct evbuffer *dst, struct evbuffer *src) tmp->off = chain->off; tmp->flags |= EVBUFFER_MULTICAST|EVBUFFER_IMMUTABLE; tmp->buffer = chain->buffer; + tmp->timestamp = chain->timestamp; evbuffer_chain_insert(dst, tmp); } } @@ -1150,6 +1183,7 @@ evbuffer_drain(struct evbuffer *buf, size_t len) EVUTIL_ASSERT(remaining == 0); chain->misalign += chain->off; chain->off = 0; + chain->timestamp.valid = 0; break; } else evbuffer_chain_free(chain); @@ -1391,6 +1425,11 @@ evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) } if (CHAIN_PINNED(chain)) { + /* Pinned chain case: expand in-place by appending data from + * subsequent chains. Timestamps from subsequent chains being + * consolidated are intentionally discarded; only this chain's + * timestamp is preserved as it contains the oldest data. + */ size_t old_off = chain->off; if (CHAIN_SPACE_LEN(chain) < size - chain->off) { /* not enough room at end of chunk. */ @@ -1402,6 +1441,11 @@ evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) size -= old_off; chain = chain->next; } else if (chain->buffer_len - chain->misalign >= (size_t)size) { + /* Sufficient space case: expand in-place without reallocation + * by appending data from subsequent chains. Timestamps from + * subsequent chains being consolidated are intentionally discarded; + * only this chain's timestamp is preserved. + */ /* already have enough space in the first chain */ size_t old_off = chain->off; buffer = chain->buffer + chain->misalign + chain->off; @@ -1416,12 +1460,20 @@ evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) } buffer = tmp->buffer; tmp->off = size; + /* tmp is freshly zeroed by evbuffer_chain_new(), so this + * copy is equivalent whether or not chain->timestamp.valid + * is set -- no need to guard it. */ + tmp->timestamp = chain->timestamp; buf->first = tmp; } /* TODO(niels): deal with buffers that point to NULL like sendfile */ - /* Copy and free every chunk that will be entirely pulled into tmp */ + /* Copy and free every chunk that will be entirely pulled into tmp. + * Timestamps of the consumed chains are intentionally discarded: tmp + * keeps only the timestamp of the chain holding the oldest data (set + * above), so that a chain with no timestamp of its own is never + * mis-attributed a later chain's timestamp. */ last_with_data = *buf->last_with_datap; for (; chain != NULL && (size_t)size >= chain->off; chain = next) { next = chain->next; @@ -1440,6 +1492,8 @@ evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) } if (chain != NULL) { + /* chain's own timestamp (if any) belongs to the data left behind + * in chain, not to tmp; see the discard rationale above. */ memcpy(buffer, chain->buffer + chain->misalign, size); chain->misalign += size; chain->off -= size; @@ -2027,6 +2081,7 @@ evbuffer_expand_singlechain(struct evbuffer *buf, size_t datlen) tmp->off = chain->off; memcpy(tmp->buffer, chain->buffer + chain->misalign, chain->off); + tmp->timestamp = chain->timestamp; /* fix up the list */ EVUTIL_ASSERT(*chainp == chain); result = *chainp = tmp; @@ -2283,14 +2338,39 @@ get_n_bytes_readable_on_socket(evutil_socket_t fd) #endif } -/* TODO(niels): should this function return ev_ssize_t and take ev_ssize_t - * as howmuch? */ -int -evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) +/** + * Reads data from a socket optionally with kernel timestamp support. + * + * @param buf the evbuffer to populate + * @param fd the file descriptor to use + * @param howmuch the amount of data to read (this will be adjusted; + * see below) + * @param use_recvmsg try to use recvmsg to read the data (and try to + * read kernel timestamps) + * + * We'll try to read howmuch bytes, unless howmuch is negative or greater + * than EVBUFFER_MAX_READ, in which case we try to read EVBUFFER_MAX_READ + * bytes instead. Either way, if the socket has fewer bytes available right + * now (via FIONREAD), we only try to read that many: no single call ever + * reads more than EVBUFFER_MAX_READ bytes, regardless of howmuch. + * + * If use_recvmsg is nonzero, it attempts to retrieve the SO_TIMESTAMPNS or + * SO_TIMESTAMP ancillary data from recvmsg() and associate it with the buffer + * data. Each recvmsg() call writes into a newly allocated chain, so every + * call gets its own chain and its timestamp is preserved independently. + * + * TODO(niels): should this function return ev_ssize_t and take ev_ssize_t + * as howmuch? + */ +static int +evbuffer_read_impl_(struct evbuffer *buf, evutil_socket_t fd, int howmuch, int use_recvmsg) { struct evbuffer_chain **chainp; int n; int result; +#ifndef _WIN32 + struct evbuffer_chain *new_chain = NULL; +#endif #ifdef USE_IOVEC_IMPL int nvecs, i, remaining; @@ -2298,6 +2378,9 @@ evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) struct evbuffer_chain *chain; unsigned char *p; #endif + struct timespec ts; + int ts_found = 0; + memset(&ts, 0, sizeof(ts)); EVBUFFER_LOCK(buf); @@ -2313,26 +2396,49 @@ evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) howmuch = n; #ifdef USE_IOVEC_IMPL - /* Since we can use iovecs, we're willing to use the last - * NUM_READ_IOVEC chains. */ - if (evbuffer_expand_fast_(buf, howmuch, NUM_READ_IOVEC) == -1) { - result = -1; - goto done; - } else { + { IOV_TYPE vecs[NUM_READ_IOVEC]; +#ifndef _WIN32 + if (use_recvmsg) { + /* Allocate a fresh chain for each recvmsg() call so that + * every timestamped call gets its own chain with an independent timestamp. */ + struct evbuffer_chain **tp; + new_chain = evbuffer_chain_new(howmuch); + if (!new_chain) { + result = -1; + goto done; + } + evbuffer_chain_insert(buf, new_chain); + tp = buf->last_with_datap; + while (*tp && *tp != new_chain) { + tp = &(*tp)->next; + } + chainp = tp; + nvecs = 1; + vecs[0].iov_base = (void *)CHAIN_SPACE_PTR(new_chain); + vecs[0].iov_len = (size_t)howmuch; + } else +#endif + /* Since we can use iovecs, we're willing to use the last + * NUM_READ_IOVEC chains. */ + if (evbuffer_expand_fast_(buf, howmuch, NUM_READ_IOVEC) == -1) { + result = -1; + goto done; + } else { #ifdef EVBUFFER_IOVEC_IS_NATIVE_ - nvecs = evbuffer_read_setup_vecs_(buf, howmuch, vecs, - NUM_READ_IOVEC, &chainp, 1); + nvecs = evbuffer_read_setup_vecs_(buf, howmuch, vecs, + NUM_READ_IOVEC, &chainp, 1); #else - /* We aren't using the native struct iovec. Therefore, - we are on win32. */ - struct evbuffer_iovec ev_vecs[NUM_READ_IOVEC]; - nvecs = evbuffer_read_setup_vecs_(buf, howmuch, ev_vecs, 2, - &chainp, 1); - - for (i=0; i < nvecs; ++i) - WSABUF_FROM_EVBUFFER_IOV(&vecs[i], &ev_vecs[i]); + /* We aren't using the native struct iovec. Therefore, + we are on win32. */ + struct evbuffer_iovec ev_vecs[NUM_READ_IOVEC]; + nvecs = evbuffer_read_setup_vecs_(buf, howmuch, ev_vecs, 2, + &chainp, 1); + + for (i=0; i < nvecs; ++i) + WSABUF_FROM_EVBUFFER_IOV(&vecs[i], &ev_vecs[i]); #endif + } #ifdef _WIN32 { @@ -2349,7 +2455,77 @@ evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) n = bytesRead; } #else - n = readv(fd, vecs, nvecs); + if (use_recvmsg) { + struct msghdr msg; + /* Control message buffer for cmsg data. + * Sized to accommodate timestamp messages (SCM_TIMESTAMPNS, + * SCM_TIMESTAMP). */ +#define EVBUFFER_RECVMSG_CTRLFN_SZ \ + (CMSG_SPACE(sizeof(struct timespec)) + \ + CMSG_SPACE(sizeof(struct timeval))) + union { + unsigned char buf[EVBUFFER_RECVMSG_CTRLFN_SZ]; + struct cmsghdr align; + } control; +#undef EVBUFFER_RECVMSG_CTRLFN_SZ + + /* Setup message header */ + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = vecs; + msg.msg_iovlen = nvecs; + msg.msg_control = control.buf; + msg.msg_controllen = sizeof(control); + + /* Receive with ancillary data */ + n = recvmsg(fd, &msg, 0); + + if (n > 0) { + struct cmsghdr *cmsg; + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level != SOL_SOCKET) { + continue; + } + /* MSG_CTRUNC only means the tail of the control + * buffer was dropped; a cmsg we actually got + * here is intact regardless of that flag, and + * the cmsg_len checks below are what protect + * against reading a cmsg that was itself cut + * short. */ +#if EVENT__HAVE_DECL_SO_TIMESTAMPNS + if (cmsg->cmsg_type == SCM_TIMESTAMPNS) { + if (cmsg->cmsg_len < CMSG_LEN(sizeof(struct timespec))) { + continue; + } + ts = *(struct timespec *)(void *)CMSG_DATA(cmsg); + ts_found = 1; + continue; + } +#endif + +#if EVENT__HAVE_DECL_SO_TIMESTAMP + if (cmsg->cmsg_type == SCM_TIMESTAMP) { + struct timeval *tv; + if (ts_found) { + /* A nanosecond-precision SCM_TIMESTAMPNS + * already won; don't let a coarser + * SCM_TIMESTAMP overwrite it. */ + continue; + } + if (cmsg->cmsg_len < CMSG_LEN(sizeof(struct timeval))) { + continue; + } + tv = (struct timeval *)(void *)CMSG_DATA(cmsg); + ts.tv_sec = tv->tv_sec; + ts.tv_nsec = tv->tv_usec * 1000L; + ts_found = 1; + continue; + } +#endif + } + } + } else { + n = readv(fd, vecs, nvecs); + } #endif } @@ -2372,12 +2548,16 @@ evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) #endif #endif /* USE_IOVEC_IMPL */ - if (n == -1) { - result = -1; - goto done; - } - if (n == 0) { - result = 0; + if (n <= 0) { + result = n; +#ifndef _WIN32 + if (new_chain) { + int saved_errno = EVUTIL_SOCKET_ERROR(); + evbuffer_free_trailing_empty_chains(buf); + buf->last = *buf->last_with_datap; + EVUTIL_SET_SOCKET_ERROR(saved_errno); + } +#endif goto done; } @@ -2394,8 +2574,16 @@ evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) if ((ev_ssize_t)space < remaining) { (*chainp)->off += space; remaining -= (int)space; + if (ts_found && (*chainp)->timestamp.valid == 0) { + (*chainp)->timestamp.ts = ts; + (*chainp)->timestamp.valid = 1; + } } else { (*chainp)->off += remaining; + if (ts_found && (*chainp)->timestamp.valid == 0) { + (*chainp)->timestamp.ts = ts; + (*chainp)->timestamp.valid = 1; + } buf->last_with_datap = chainp; break; } @@ -2416,6 +2604,53 @@ evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) return result; } +int +evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) +{ + return evbuffer_read_impl_(buf, fd, howmuch, 0); +} + +int +evbuffer_read_with_timestamp_( + struct evbuffer *buf, evutil_socket_t fd, int howmuch) +{ + return evbuffer_read_impl_(buf, fd, howmuch, 1); +} + +int evbuffer_get_timestamp( + struct evbuffer *buf, struct timespec *timestamp) +{ + int result = -1; + if (!timestamp) { + return -1; + } + EVBUFFER_LOCK(buf); + { + if (buf->first && buf->first->timestamp.valid) { + *timestamp = buf->first->timestamp.ts; + result = 0; + } + } + EVBUFFER_UNLOCK(buf); + return result; +} + +/* Invalidate the timestamp (if any) on buf's current tail chain. Used when + * switching a bufferevent to a different fd: the tail chain may still have + * spare capacity and undrained data timestamped from the old fd, and a + * later plain (non-timestamped) read on the new fd could otherwise extend + * that same chain, making evbuffer_get_timestamp() misattribute the old + * fd's timestamp to the new fd's data. */ +void +evbuffer_invalidate_last_chain_timestamp_(struct evbuffer *buf) +{ + EVBUFFER_LOCK(buf); + if (buf->last) { + buf->last->timestamp.valid = 0; + } + EVBUFFER_UNLOCK(buf); +} + #ifdef USE_IOVEC_IMPL static inline int evbuffer_write_iovec(struct evbuffer *buffer, evutil_socket_t fd, diff --git a/bufferevent-internal.h b/bufferevent-internal.h index 20a8a324b4..f57074bb8c 100644 --- a/bufferevent-internal.h +++ b/bufferevent-internal.h @@ -231,6 +231,9 @@ struct bufferevent_private { struct sockaddr_storage conn_address; struct evdns_getaddrinfo_request *dns_request; + + /** Flag: set if receive timestamps are enabled */ + unsigned recv_timestamps_enabled : 1; }; /** Possible operations for a control callback. */ @@ -453,6 +456,9 @@ EVENT2_EXPORT_SYMBOL int bufferevent_socket_set_conn_address_(struct bufferevent *bev, struct sockaddr *addr, size_t addrlen); +EVENT2_EXPORT_SYMBOL +int be_socket_enable_timestamps_(evutil_socket_t fd); + /** Internal use: We have just successfully read data into an inbuf, so * reset the read timeout (if any). */ diff --git a/bufferevent.c b/bufferevent.c index 08c0486c08..18508ee021 100644 --- a/bufferevent.c +++ b/bufferevent.c @@ -925,6 +925,18 @@ bufferevent_get_enabled(struct bufferevent *bufev) return r; } +int +bufferevent_get_recv_timestamps_enabled(struct bufferevent *bev) +{ + struct bufferevent_private *bev_p = BEV_UPCAST(bev); + int r; + + BEV_LOCK(bev); + r = bev_p->recv_timestamps_enabled; + BEV_UNLOCK(bev); + return r; +} + struct bufferevent * bufferevent_get_underlying(struct bufferevent *bev) { diff --git a/bufferevent_openssl.c b/bufferevent_openssl.c index 520e2d6ffe..4e428099f0 100644 --- a/bufferevent_openssl.c +++ b/bufferevent_openssl.c @@ -53,6 +53,16 @@ #include #endif +#ifdef EVENT__HAVE_SYS_SOCKET_H +#include +#endif +#ifdef EVENT__HAVE_SYS_UIO_H +#include +#endif +#ifdef EVENT__HAVE_NETINET_IN_H +#include +#endif + #include "event2/bufferevent.h" #include "event2/bufferevent_struct.h" #include "event2/bufferevent_ssl.h" @@ -83,6 +93,7 @@ /* every BIO type needs its own integer type value. */ #define BIO_TYPE_LIBEVENT 57 +#define BIO_TYPE_LIBEVENT_RECVMSG (58 | BIO_TYPE_SOURCE_SINK) /* ???? Arguably, we should set BIO_TYPE_FILTER or BIO_TYPE_SOURCE_SINK on * this. */ @@ -229,21 +240,27 @@ bio_bufferevent_puts(BIO *b, const char *s) /* Method table for the bufferevent BIO */ static BIO_METHOD *methods_bufferevent; +static void +init_methods_bufferevent(void) +{ + methods_bufferevent = BIO_meth_new(BIO_TYPE_LIBEVENT, "bufferevent"); + if (methods_bufferevent == NULL) { + return; + } + BIO_meth_set_write(methods_bufferevent, bio_bufferevent_write); + BIO_meth_set_read(methods_bufferevent, bio_bufferevent_read); + BIO_meth_set_puts(methods_bufferevent, bio_bufferevent_puts); + BIO_meth_set_ctrl(methods_bufferevent, bio_bufferevent_ctrl); + BIO_meth_set_create(methods_bufferevent, bio_bufferevent_new); + BIO_meth_set_destroy(methods_bufferevent, bio_bufferevent_free); +} + /* Return the method table for the bufferevents BIO */ static BIO_METHOD * BIO_s_bufferevent(void) { - if (methods_bufferevent == NULL) { - methods_bufferevent = BIO_meth_new(BIO_TYPE_LIBEVENT, "bufferevent"); - if (methods_bufferevent == NULL) - return NULL; - BIO_meth_set_write(methods_bufferevent, bio_bufferevent_write); - BIO_meth_set_read(methods_bufferevent, bio_bufferevent_read); - BIO_meth_set_puts(methods_bufferevent, bio_bufferevent_puts); - BIO_meth_set_ctrl(methods_bufferevent, bio_bufferevent_ctrl); - BIO_meth_set_create(methods_bufferevent, bio_bufferevent_new); - BIO_meth_set_destroy(methods_bufferevent, bio_bufferevent_free); - } + static CRYPTO_ONCE once = CRYPTO_ONCE_STATIC_INIT; + CRYPTO_THREAD_run_once(&once, init_methods_bufferevent); return methods_bufferevent; } @@ -328,6 +345,323 @@ struct bufferevent_openssl { unsigned old_state : 2; }; +struct bio_socket_recvmsg_data { + evutil_socket_t fd; + struct bufferevent_openssl *bev_ssl; + /* Timestamp of the oldest recvmsg() not yet consumed by do_read(): + * once set, it is left untouched by further recvmsg() calls until + * do_read() reads and clears it, so a TLS record whose reassembly + * requires several recvmsg() calls is attributed the first call's + * timestamp rather than the last. */ + struct timespec last_recv_ts; + int last_recv_ts_valid; + /* Sticky fallback: sample the timestamp attributed to a data-bearing + * recvmsg() call whose bytes are still buffered inside OpenSSL, + * unconsumed by do_read(). Reset to 0 by any recvmsg() call that + * returns data without a cmsg timestamp while last_recv_ts_valid is + * already 0, so do_read() never attributes unrelated later data to a + * stale earlier timestamp. */ + int has_recv_ts; +}; + +static int +bio_socket_recvmsg_new(BIO *b) +{ + struct bio_socket_recvmsg_data *data = mm_calloc(1, sizeof(*data)); + if (!data) { + return 0; + } + data->fd = -1; + data->bev_ssl = NULL; + data->last_recv_ts_valid = 0; + BIO_set_init(b, 1); + BIO_set_data(b, data); + return 1; +} + +static int +bio_socket_recvmsg_free(BIO *b) +{ + struct bio_socket_recvmsg_data *data; + if (!b) { + return 0; + } + data = BIO_get_data(b); + if (data) { + if (BIO_get_shutdown(b) && data->fd != EVUTIL_INVALID_SOCKET) { + evutil_closesocket(data->fd); + } + mm_free(data); + BIO_set_data(b, NULL); + } + BIO_set_init(b, 0); + return 1; +} + +static int +bio_socket_recvmsg_read(BIO *b, char *out, int outlen) +{ + struct bio_socket_recvmsg_data *data = BIO_get_data(b); + int r; + if (!data || data->fd < 0) { + return -1; + } + + BIO_clear_retry_flags(b); + BIO_clear_flags(b, BIO_FLAGS_IN_EOF); + +#if defined(_WIN32) + r = recv(data->fd, out, outlen, 0); +#else + if (data->bev_ssl && data->bev_ssl->bev.recv_timestamps_enabled) { + struct msghdr msg; + struct iovec iov; + union { + unsigned char buf[ + CMSG_SPACE(sizeof(struct timespec)) + /* SCM_TIMESTAMPNS */ + CMSG_SPACE(sizeof(struct timeval)) /* SCM_TIMESTAMP */ + ]; + struct cmsghdr align; + } control; + struct timespec ts; + int ts_found = 0; + + memset(&ts, 0, sizeof(ts)); + iov.iov_base = out; + iov.iov_len = outlen; + + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control.buf; + msg.msg_controllen = sizeof(control); + + r = recvmsg(data->fd, &msg, 0); + + if (r > 0) { + struct cmsghdr *cmsg; + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level != SOL_SOCKET) { + continue; + } + /* MSG_CTRUNC only means the tail of the control + * buffer was dropped; a cmsg we actually got + * here is intact regardless of that flag, and + * the cmsg_len checks below are what protect + * against reading a cmsg that was itself cut + * short. */ +#if EVENT__HAVE_DECL_SO_TIMESTAMPNS + if (cmsg->cmsg_type == SCM_TIMESTAMPNS) { + if (cmsg->cmsg_len < CMSG_LEN(sizeof(struct timespec))) { + continue; + } + ts = *(struct timespec *)(void *)CMSG_DATA(cmsg); + ts_found = 1; + continue; + } +#endif + +#if EVENT__HAVE_DECL_SO_TIMESTAMP + if (cmsg->cmsg_type == SCM_TIMESTAMP) { + struct timeval *tv; + if (ts_found) { + /* A nanosecond-precision SCM_TIMESTAMPNS + * already won; don't let a coarser + * SCM_TIMESTAMP overwrite it. */ + continue; + } + if (cmsg->cmsg_len < CMSG_LEN(sizeof(struct timeval))) { + continue; + } + tv = (struct timeval *)(void *)CMSG_DATA(cmsg); + ts.tv_sec = tv->tv_sec; + ts.tv_nsec = tv->tv_usec * 1000L; + ts_found = 1; + continue; + } +#endif + } + if (ts_found) { + if (!data->last_recv_ts_valid && !data->has_recv_ts) { + data->last_recv_ts = ts; + data->last_recv_ts_valid = 1; + data->has_recv_ts = 1; + } + /* else: already holding an older unconsumed + * timestamp ("oldest wins"); leave it as-is. */ + } else if (!data->last_recv_ts_valid) { + /* This read returned data but no cmsg timestamp + * (e.g. MSG_CTRUNC). do_read() already consumed + * and cleared last_recv_ts_valid, but has_recv_ts + * can still be sticky from that earlier recvmsg() + * while its bytes are still buffered inside + * OpenSSL. Clear it so do_read()'s fallback + * doesn't reuse that stale timestamp for this + * unrelated data. */ + data->has_recv_ts = 0; + } + } + } else { + r = recv(data->fd, out, outlen, 0); + } +#endif + + if (r < 0) { + int err = EVUTIL_SOCKET_ERROR(); + if (EVUTIL_ERR_RW_RETRIABLE(err)) { + BIO_set_retry_read(b); + } + } else if (r == 0) { + BIO_set_flags(b, BIO_FLAGS_IN_EOF); + } + return r; +} + +static struct bio_socket_recvmsg_data * +get_bio_recvmsg_data(SSL *ssl) +{ + BIO *rbio; + if (!ssl) { + return NULL; + } + rbio = SSL_get_rbio(ssl); + if (rbio && BIO_method_type(rbio) == BIO_TYPE_LIBEVENT_RECVMSG) { + return BIO_get_data(rbio); + } + return NULL; +} + +/* do_write()/do_handshake() can make OpenSSL perform an incidental read + * on the rbio (e.g. to process a post-handshake session ticket, or to + * service a renegotiation) whose bytes are consumed internally and + * never handed back through do_read(); any timestamp bio_data recorded + * for such a read must not be left lying around to be misattributed to + * later, unrelated application data. But if bio_data already had a + * timestamp pending *before* the write/handshake ran, it belongs to an + * earlier do_read() that is still waiting on more ciphertext for the + * same record, and must survive untouched. So only clear whichever of + * the two fields this call newly set; leave alone whatever was already + * set beforehand. */ +static void +clear_new_bio_recvmsg_ts(struct bio_socket_recvmsg_data *bio_data, + int had_last_recv_ts_valid, int had_has_recv_ts) +{ + if (!bio_data) { + return; + } + if (!had_last_recv_ts_valid) { + bio_data->last_recv_ts_valid = 0; + } + if (!had_has_recv_ts) { + bio_data->has_recv_ts = 0; + } +} + +static int +bio_socket_recvmsg_write(BIO *b, const char *in, int inlen) +{ + struct bio_socket_recvmsg_data *data = BIO_get_data(b); + int r; + if (!data || data->fd < 0) { + return -1; + } + + BIO_clear_retry_flags(b); + r = send(data->fd, in, inlen, 0); + if (r < 0) { + int err = EVUTIL_SOCKET_ERROR(); + if (EVUTIL_ERR_RW_RETRIABLE(err)) { + BIO_set_retry_write(b); + } + } + return r; +} + +static long +bio_socket_recvmsg_ctrl(BIO *b, int cmd, long num, void *ptr) +{ + struct bio_socket_recvmsg_data *data = BIO_get_data(b); + long ret = 1; + if (!data) { + return 0; + } + + switch (cmd) { + case BIO_C_SET_FD: + if (ptr) { + data->fd = (evutil_socket_t)(*(int *)ptr); + } + BIO_set_shutdown(b, (int)num); + BIO_set_init(b, 1); + ret = 1; + break; + case BIO_C_GET_FD: + if (BIO_get_init(b)) { + if (ptr) { + *(int *)ptr = (int)data->fd; + } + ret = data->fd; + } else { + ret = -1; + } + break; + case BIO_CTRL_GET_CLOSE: + ret = BIO_get_shutdown(b); + break; + case BIO_CTRL_SET_CLOSE: + BIO_set_shutdown(b, (int)num); + ret = 1; + break; + case BIO_CTRL_DUP: + case BIO_CTRL_FLUSH: + ret = 1; + break; + case BIO_CTRL_EOF: + ret = BIO_test_flags(b, BIO_FLAGS_IN_EOF) != 0; + break; + default: + ret = 0; + break; + } + return ret; +} + +static BIO_METHOD *methods_socket_recvmsg; + +static void +init_methods_socket_recvmsg(void) +{ + methods_socket_recvmsg = + BIO_meth_new(BIO_TYPE_LIBEVENT_RECVMSG, "socket_recvmsg"); + if (methods_socket_recvmsg == NULL) { + return; + } + BIO_meth_set_write(methods_socket_recvmsg, bio_socket_recvmsg_write); + BIO_meth_set_read(methods_socket_recvmsg, bio_socket_recvmsg_read); + BIO_meth_set_ctrl(methods_socket_recvmsg, bio_socket_recvmsg_ctrl); + BIO_meth_set_create(methods_socket_recvmsg, bio_socket_recvmsg_new); + BIO_meth_set_destroy(methods_socket_recvmsg, bio_socket_recvmsg_free); +} + +static BIO_METHOD * +BIO_s_socket_recvmsg(void) +{ + static CRYPTO_ONCE once = CRYPTO_ONCE_STATIC_INIT; + CRYPTO_THREAD_run_once(&once, init_methods_socket_recvmsg); + return methods_socket_recvmsg; +} + +static BIO * +BIO_new_socket_recvmsg(evutil_socket_t fd, int close_flag) +{ + BIO *bio = BIO_new(BIO_s_socket_recvmsg()); + if (!bio) { + return NULL; + } + BIO_int_ctrl(bio, BIO_C_SET_FD, close_flag, (int)fd); + return bio; +} + static int be_openssl_enable(struct bufferevent *, short); static int be_openssl_disable(struct bufferevent *, short); static void be_openssl_unlink(struct bufferevent *); @@ -591,9 +925,15 @@ do_read(struct bufferevent_openssl *bev_ssl, int n_to_read) { struct evbuffer_iovec space[2]; int result = 0; + struct bio_socket_recvmsg_data *bio_data; + struct timespec first_ts = {0, 0}; + int first_ts_valid = 0; + if (bev_ssl->bev.read_suspended) return 0; + bio_data = get_bio_recvmsg_data(bev_ssl->ssl); + atmost = bufferevent_get_read_max_(&bev_ssl->bev); if (n_to_read > atmost) n_to_read = atmost; @@ -603,34 +943,80 @@ do_read(struct bufferevent_openssl *bev_ssl, int n_to_read) { return OP_ERR; for (i=0; ibev.read_suspended) break; + if (bev_ssl->underlying && + BEV_UPCAST(bev_ssl->underlying)->recv_timestamps_enabled) { + /* evbuffer_get_timestamp() locks the underlying + * evbuffer; only pay for that when the underlying + * bufferevent actually has receive timestamps armed + * -- otherwise it can never find one anyway. At most + * two iovecs are reserved per do_read() call, so this + * is at most one extra locked call, not a per-byte + * cost. */ + if (evbuffer_get_timestamp( + bufferevent_get_input(bev_ssl->underlying), + &underlying_ts) == 0) { + underlying_ts_valid = 1; + } + } ERR_clear_error(); r = SSL_read(bev_ssl->ssl, space[i].iov_base, space[i].iov_len); if (r>0) { result |= OP_MADE_PROGRESS; - if (bev_ssl->read_blocked_on_write) - if (clear_rbow(bev_ssl) < 0) - return OP_ERR | result; + if (bev_ssl->read_blocked_on_write) { + if (clear_rbow(bev_ssl) < 0) { + result |= OP_ERR; + goto out; + } + } ++n_used; space[i].iov_len = r; decrement_buckets(bev_ssl); + + /* Store timestamp from first successful read (oldest data). + All iovecs from the same reserve_space call are committed + together, so we use the timestamp from the first read. + bio_data->last_recv_ts holds the oldest not-yet-consumed + recvmsg() timestamp (see bio_socket_recvmsg_data); clear it + once read so the next SSL_read() starts tracking fresh. */ + if (n_used == 1) { + if (bio_data && bio_data->last_recv_ts_valid) { + first_ts = bio_data->last_recv_ts; + first_ts_valid = 1; + bio_data->last_recv_ts_valid = 0; + } else if (bio_data && bio_data->has_recv_ts) { + first_ts = bio_data->last_recv_ts; + first_ts_valid = 1; + } else if (underlying_ts_valid) { + first_ts = underlying_ts; + first_ts_valid = 1; + } + } } else { int err = SSL_get_error(bev_ssl->ssl, r); print_err(err); switch (err) { case SSL_ERROR_WANT_READ: /* Can't read until underlying has more data. */ - if (bev_ssl->read_blocked_on_write) - if (clear_rbow(bev_ssl) < 0) - return OP_ERR | result; + if (bev_ssl->read_blocked_on_write) { + if (clear_rbow(bev_ssl) < 0) { + result |= OP_ERR; + goto out; + } + } break; case SSL_ERROR_WANT_WRITE: /* This read operation requires a write, and the * underlying is full */ - if (!bev_ssl->read_blocked_on_write) - if (set_rbow(bev_ssl) < 0) - return OP_ERR | result; + if (!bev_ssl->read_blocked_on_write) { + if (set_rbow(bev_ssl) < 0) { + result |= OP_ERR; + goto out; + } + } break; default: conn_closed(bev_ssl, BEV_EVENT_READING, err, r); @@ -641,10 +1027,35 @@ do_read(struct bufferevent_openssl *bev_ssl, int n_to_read) { } } +out: + /* Commit all filled iovecs together to avoid data corruption when + evbuffer_reserve_space() returns multiple vectors. Individual commits + can cause buffer accounting issues when the second vector is in a + different chain than the first. */ if (n_used) { - evbuffer_commit_space(input, space, n_used); - if (bev_ssl->underlying) + if (first_ts_valid) { + evbuffer_commit_space_with_timespec(input, space, n_used, &first_ts); + } else { + evbuffer_commit_space(input, space, n_used); + } + if (bev_ssl->underlying) { BEV_RESET_GENERIC_READ_TIMEOUT(bev); + } + } + /* has_recv_ts stays set as long as SSL_pending() has more already- + * decrypted bytes left over from the recvmsg() call last_recv_ts was + * captured from, so a later do_read() call still attributes them to + * that timestamp instead of falling through to whatever timestamp a + * subsequent, unrelated recvmsg() captures. Once SSL_pending() is + * empty there is nothing left for it to protect, so retire it here; + * otherwise it would stay set forever after the first timestamped + * read (bio_socket_recvmsg_read() refuses to record a new timestamp + * while has_recv_ts is set), and every later record would be + * mis-attributed to the very first timestamp ever captured on this + * connection. */ + if (bio_data && bio_data->has_recv_ts && + SSL_pending(bev_ssl->ssl) == 0) { + bio_data->has_recv_ts = 0; } return result; @@ -660,6 +1071,10 @@ do_write(struct bufferevent_openssl *bev_ssl, int atmost) struct evbuffer *output = bev->output; struct evbuffer_iovec space[8]; int result = 0; + struct bio_socket_recvmsg_data *bio_data = + get_bio_recvmsg_data(bev_ssl->ssl); + int had_last_recv_ts_valid = bio_data && bio_data->last_recv_ts_valid; + int had_has_recv_ts = bio_data && bio_data->has_recv_ts; if (bev_ssl->last_write > 0) atmost = bev_ssl->last_write; @@ -667,8 +1082,10 @@ do_write(struct bufferevent_openssl *bev_ssl, int atmost) atmost = bufferevent_get_write_max_(&bev_ssl->bev); n = evbuffer_peek(output, atmost, NULL, space, 8); - if (n < 0) - return OP_ERR | result; + if (n < 0) { + result |= OP_ERR; + goto out; + } if (n > 8) n = 8; @@ -687,9 +1104,12 @@ do_write(struct bufferevent_openssl *bev_ssl, int atmost) space[i].iov_len); if (r > 0) { result |= OP_MADE_PROGRESS; - if (bev_ssl->write_blocked_on_read) - if (clear_wbor(bev_ssl) < 0) - return OP_ERR | result; + if (bev_ssl->write_blocked_on_read) { + if (clear_wbor(bev_ssl) < 0) { + result |= OP_ERR; + goto out; + } + } n_written += r; bev_ssl->last_write = -1; decrement_buckets(bev_ssl); @@ -699,17 +1119,23 @@ do_write(struct bufferevent_openssl *bev_ssl, int atmost) switch (err) { case SSL_ERROR_WANT_WRITE: /* Can't read until underlying has more data. */ - if (bev_ssl->write_blocked_on_read) - if (clear_wbor(bev_ssl) < 0) - return OP_ERR | result; + if (bev_ssl->write_blocked_on_read) { + if (clear_wbor(bev_ssl) < 0) { + result |= OP_ERR; + goto out; + } + } bev_ssl->last_write = space[i].iov_len; break; case SSL_ERROR_WANT_READ: /* This read operation requires a write, and the * underlying is full */ - if (!bev_ssl->write_blocked_on_read) - if (set_wbor(bev_ssl) < 0) - return OP_ERR | result; + if (!bev_ssl->write_blocked_on_read) { + if (set_wbor(bev_ssl) < 0) { + result |= OP_ERR; + goto out; + } + } bev_ssl->last_write = space[i].iov_len; break; default: @@ -721,6 +1147,7 @@ do_write(struct bufferevent_openssl *bev_ssl, int atmost) break; } } +out: if (n_written) { evbuffer_drain(output, n_written); if (bev_ssl->underlying) @@ -728,6 +1155,17 @@ do_write(struct bufferevent_openssl *bev_ssl, int atmost) bufferevent_trigger_nolock_(bev, EV_WRITE, BEV_OPT_DEFER_CALLBACKS); } + /* Every exit from this function (error or not) must pass through + * here: do_write() may have serviced an SSL renegotiation read + * before failing, so an early return that skipped this would leave + * a stale recv timestamp for do_read() to misattribute later. But + * if that incidental read left already-decrypted bytes in + * SSL_pending() that do_read() hasn't consumed yet, the timestamp + * still belongs to them -- mirrors do_handshake()'s identical + * guard. */ + if (SSL_pending(bev_ssl->ssl) == 0) { + clear_new_bio_recvmsg_ts(bio_data, had_last_recv_ts_valid, had_has_recv_ts); + } return result; } @@ -1021,6 +1459,10 @@ static int do_handshake(struct bufferevent_openssl *bev_ssl) { int r; + struct bio_socket_recvmsg_data *bio_data = + get_bio_recvmsg_data(bev_ssl->ssl); + int had_last_recv_ts_valid = bio_data && bio_data->last_recv_ts_valid; + int had_has_recv_ts = bio_data && bio_data->has_recv_ts; switch (bev_ssl->state) { default: @@ -1031,6 +1473,15 @@ do_handshake(struct bufferevent_openssl *bev_ssl) case BUFFEREVENT_SSL_ACCEPTING: ERR_clear_error(); r = SSL_do_handshake(bev_ssl->ssl); + /* If the handshake's final read also picked up the start of + * the first application-data record (peer sent both in the + * same segment), that data is now sitting in OpenSSL's + * internal buffer for do_read() to decrypt without a new + * recvmsg() call -- don't clear the timestamp out from under + * it. Mirrors do_read()'s own has_recv_ts retirement rule. */ + if (SSL_pending(bev_ssl->ssl) == 0) { + clear_new_bio_recvmsg_ts(bio_data, had_last_recv_ts_valid, had_has_recv_ts); + } break; } decrement_buckets(bev_ssl); @@ -1232,6 +1683,22 @@ be_openssl_destruct(struct bufferevent *bev) { struct bufferevent_openssl *bev_ssl = upcast(bev); + if (!bev_ssl->underlying && bev_ssl->ssl) { + BIO *rbio = SSL_get_rbio(bev_ssl->ssl); + if (rbio && BIO_method_type(rbio) == BIO_TYPE_LIBEVENT_RECVMSG) { + struct bio_socket_recvmsg_data *bio_data = BIO_get_data(rbio); + /* bufferevent teardown can be deferred to the event + * loop, so a new bufferevent may already have claimed + * this BIO (see bufferevent_openssl_new_impl()) by the + * time this destruct runs. Only clear the pointer if + * it still refers to us -- otherwise we'd null out the + * new owner's reference out from under it. */ + if (bio_data && bio_data->bev_ssl == bev_ssl) { + bio_data->bev_ssl = NULL; + } + } + } + if (bev_ssl->bev.options & BEV_OPT_CLOSE_ON_FREE) { if (! bev_ssl->underlying) { evutil_socket_t fd = EVUTIL_INVALID_SOCKET; @@ -1306,8 +1773,28 @@ be_openssl_ctrl(struct bufferevent *bev, case BEV_CTRL_SET_FD: if (!bev_ssl->underlying) { BIO *bio; - bio = BIO_new_socket((int)data->fd, 0); + if (bev_ssl->bev.options & BEV_OPT_RECV_TIMESTAMPS) { + if (be_socket_enable_timestamps_(data->fd) >= 0) { + bev_ssl->bev.recv_timestamps_enabled = 1; + } else { + bev_ssl->bev.recv_timestamps_enabled = 0; + } + } + if (bev_ssl->bev.recv_timestamps_enabled) { + bio = BIO_new_socket_recvmsg((int)data->fd, 0); + } else { + bio = BIO_new_socket((int)data->fd, 0); + } + if (!bio) { + return -1; + } SSL_set_bio(bev_ssl->ssl, bio, bio); + if (bio && BIO_method_type(bio) == BIO_TYPE_LIBEVENT_RECVMSG) { + struct bio_socket_recvmsg_data *bio_data = BIO_get_data(bio); + if (bio_data) { + bio_data->bev_ssl = bev_ssl; + } + } } else { BIO *bio; if (!(bio = BIO_new_bufferevent(bev_ssl->underlying))) @@ -1393,6 +1880,39 @@ bufferevent_openssl_new_impl(struct event_base *base, if (be_openssl_set_fd(bev_ssl, state, fd)) goto err; + /* The fd, if any, was already armed for SO_TIMESTAMP(NS) and the BIO + * decided upon (plain socket vs. recvmsg) by our caller. Only report + * timestamps as enabled if this bufferevent actually requested them + * *and* the recvmsg BIO ended up installed: e.g. + * bufferevent_openssl_socket_new() skips the swap for split rbio/wbio + * pairs, in which case reads never go through bio_socket_recvmsg_read() + * and no timestamps are ever collected. If a recvmsg BIO is present + * but wasn't requested here -- e.g. inherited from a previous owner + * of the same SSL object -- swap it back to a plain socket BIO so + * this bufferevent doesn't silently inherit recvmsg()'s overhead. */ + { + BIO *rbio = SSL_get_rbio(ssl); + if (rbio && BIO_method_type(rbio) == BIO_TYPE_LIBEVENT_RECVMSG) { + if (options & BEV_OPT_RECV_TIMESTAMPS) { + struct bio_socket_recvmsg_data *bio_data = BIO_get_data(rbio); + bev_ssl->bev.recv_timestamps_enabled = 1; + if (bio_data) { + bio_data->bev_ssl = bev_ssl; + } + } else { + evutil_socket_t rfd = BIO_get_fd(rbio, NULL); + if (rfd >= 0) { + int close_flag = BIO_get_shutdown(rbio); + BIO *plain_bio = BIO_new_socket((int)rfd, close_flag); + if (plain_bio) { + BIO_set_shutdown(rbio, 0); + SSL_set_bio(ssl, plain_bio, plain_bio); + } + } + } + } + } + if (underlying) { bufferevent_setwatermark(underlying, EV_READ, 0, 0); bufferevent_enable(underlying, EV_READ|EV_WRITE); @@ -1449,6 +1969,7 @@ bufferevent_openssl_socket_new(struct event_base *base, /* Does the SSL already have an fd? */ BIO *bio = SSL_get_wbio(ssl); long have_fd = -1; + int recv_timestamps_enabled = 0; if (bio) have_fd = BIO_get_fd(bio, NULL); @@ -1465,12 +1986,55 @@ bufferevent_openssl_socket_new(struct event_base *base, This is probably an error on our part. Fail. */ goto err; } + /* Only safe to arm timestamps / replace the BIO when the SSL uses + * a single BIO for both directions: SSL_set_bio() below replaces + * both the rbio and wbio slots, so if the caller had configured + * distinct BIOs (e.g. via SSL_set_rfd()/SSL_set_wfd() or + * SSL_set_bio() with a filter chain on the read side), doing this + * would free the real rbio and silently redirect reads onto the + * write-side fd. `fd` here comes from the write BIO, so in the + * split-BIO case it isn't even the fd data is read from -- arming + * SO_TIMESTAMP(NS) on it would just be a wasted/misleading + * setsockopt() on the wrong socket. */ + if ((options & BEV_OPT_RECV_TIMESTAMPS) && fd >= 0 && + SSL_get_rbio(ssl) == SSL_get_wbio(ssl) && + BIO_method_type(bio) == BIO_TYPE_SOCKET) { + if (be_socket_enable_timestamps_(fd) >= 0) { + recv_timestamps_enabled = 1; + } + } + if (recv_timestamps_enabled) { + int close_flag = BIO_get_close(bio); + BIO *new_bio = BIO_new_socket_recvmsg((int)fd, close_flag); + if (new_bio) { + /* Prevent the old BIO from closing fd when SSL_set_bio frees it. */ + BIO_set_close(bio, BIO_NOCLOSE); + SSL_set_bio(ssl, new_bio, new_bio); + bio = new_bio; + } + } + /* fd ownership belongs to the bufferevent (via + * BEV_OPT_CLOSE_ON_FREE, see be_openssl_destruct()), not to + * the BIO, regardless of close_flag above or of the BIO's + * type. */ BIO_set_close(bio, 0); } else { /* The SSL isn't configured with a BIO with an fd. */ if (fd >= 0) { /* ... and we have an fd we want to use. */ - bio = BIO_new_socket((int)fd, 0); + if (options & BEV_OPT_RECV_TIMESTAMPS) { + if (be_socket_enable_timestamps_(fd) >= 0) { + recv_timestamps_enabled = 1; + } + } + if (recv_timestamps_enabled) { + bio = BIO_new_socket_recvmsg((int)fd, 0); + } else { + bio = BIO_new_socket((int)fd, 0); + } + if (!bio) { + goto err; + } SSL_set_bio(ssl, bio, bio); } else { /* Leave the fd unset. */ diff --git a/bufferevent_sock.c b/bufferevent_sock.c index 543fce4bb2..df44891b92 100644 --- a/bufferevent_sock.c +++ b/bufferevent_sock.c @@ -64,12 +64,14 @@ #include "event2/util.h" #include "event2/bufferevent.h" #include "event2/buffer.h" +#include "event2/buffer_compat.h" #include "event2/bufferevent_struct.h" #include "event2/bufferevent_compat.h" #include "event2/event.h" #include "log-internal.h" #include "mm-internal.h" #include "bufferevent-internal.h" +#include "evbuffer-internal.h" #include "util-internal.h" #ifdef _WIN32 #include "iocp-internal.h" @@ -84,6 +86,95 @@ static int be_socket_ctrl(struct bufferevent *, enum bufferevent_ctrl_op, union static void be_socket_setfd(struct bufferevent *, evutil_socket_t); +/* ======================================================================== + * Socket receive timestamp support (SO_TIMESTAMP) + * ======================================================================== */ + +/* SO_TIMESTAMP on a SOCK_STREAM socket is a silent no-op on classic BSD + * kernels (macOS, FreeBSD, OpenBSD, NetBSD, DragonFly): setsockopt() + * succeeds but recvmsg() never delivers cmsgs. The SO_TYPE probe below + * exists only to detect that case, so compile (and pay for) it solely + * on the platforms where it's actually true and where SO_TIMESTAMPNS + * isn't available to sidestep the problem entirely -- everywhere else + * (notably Linux) the probe would run every time and never change the + * outcome. */ +#if (defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \ + defined(__NetBSD__) || defined(__DragonFly__)) && \ + !EVENT__HAVE_DECL_SO_TIMESTAMPNS +#define EVENT__SOCK_TIMESTAMP_STREAM_IS_NOOP_ 1 +#endif + +/** + * Enable SO_TIMESTAMP socket option for kernel receive timestamping + * + * Returns: + * 1 = SO_TIMESTAMPNS enabled (nanosecond precision) + * 0 = SO_TIMESTAMP enabled (microsecond precision) + * -1 = timestamps not available on this platform + */ +int +be_socket_enable_timestamps_(evutil_socket_t fd) +{ + int on = 1; +#if defined(SOL_SOCKET) && defined(SO_TYPE) && defined(SOCK_STREAM) + int type = 0; + ev_socklen_t len = sizeof(type); +#endif +#if defined(AF_UNIX) || defined(AF_LOCAL) + struct sockaddr_storage ss; + ev_socklen_t sslen = sizeof(ss); +#endif + + if (fd < 0) { + return -1; + } + +#if defined(SOL_SOCKET) && defined(SO_TYPE) && defined(SOCK_STREAM) + if (getsockopt(fd, SOL_SOCKET, SO_TYPE, (void *)&type, &len) != 0 || type != SOCK_STREAM) { + /* Receive timestamps are only supported on stream (SOCK_STREAM / TCP) sockets */ + return -1; + } +#endif + +#if defined(AF_UNIX) || defined(AF_LOCAL) + if (getsockname(fd, (struct sockaddr *)&ss, &sslen) == 0) { +#ifdef AF_UNIX + if (ss.ss_family == AF_UNIX) { + return -1; + } +#endif +#if defined(AF_LOCAL) && (AF_LOCAL != AF_UNIX) + if (ss.ss_family == AF_LOCAL) { + return -1; + } +#endif + } +#endif + +#if defined(EVENT__SOCK_TIMESTAMP_STREAM_IS_NOOP_) + /* On BSD systems (macOS, FreeBSD, OpenBSD, etc.), SO_TIMESTAMP on + * SOCK_STREAM sockets is a silent kernel no-op: setsockopt succeeds, + * but recvmsg() never delivers cmsgs. Do not pretend timestamps work. */ + return -1; +#endif + +#if EVENT__HAVE_DECL_SO_TIMESTAMPNS + /* Try nanosecond precision first (Linux 2.6.22+) */ + if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPNS, &on, sizeof(on)) == 0) { + return 1; + } +#endif + +#if EVENT__HAVE_DECL_SO_TIMESTAMP + /* Fall back to microsecond precision */ + if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &on, sizeof(on)) == 0) { + return 0; + } +#endif + + return -1; +} + const struct bufferevent_ops bufferevent_ops_socket = { "socket", evutil_offsetof(struct bufferevent_private, bev), @@ -191,7 +282,15 @@ bufferevent_readcb(evutil_socket_t fd, short event, void *arg) goto done; evbuffer_unfreeze(input, 0); - res = evbuffer_read(input, fd, (int)howmuch); /* XXXX evbuffer_read would do better to take and return ev_ssize_t */ + + if (bufev_p->recv_timestamps_enabled) { + /* Use recvmsg() to capture timestamps */ + res = evbuffer_read_with_timestamp_(input, fd, (int)howmuch); + } else { + /* Use standard read when timestamps not enabled */ + res = evbuffer_read(input, fd, (int)howmuch); + } + evbuffer_freeze(input, 0); if (res == -1) { @@ -374,6 +473,13 @@ bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, evbuffer_add_cb(bufev->output, bufferevent_socket_outbuf_cb, bufev); + /* Enable receive timestamps if requested */ + if ((options & BEV_OPT_RECV_TIMESTAMPS) && fd >= 0) { + if (be_socket_enable_timestamps_(fd) >= 0) { + bufev_p->recv_timestamps_enabled = 1; + } + } + evbuffer_freeze(bufev->input, 0); evbuffer_freeze(bufev->output, 1); @@ -630,6 +736,15 @@ be_socket_setfd(struct bufferevent *bufev, evutil_socket_t fd) BEV_LOCK(bufev); EVUTIL_ASSERT(BEV_IS_SOCKET(bufev)); + /* The new fd has not had timestamps armed on it yet. */ + bufev_p->recv_timestamps_enabled = 0; + + /* The input buffer's tail chain may still have undrained data and + * spare capacity timestamped from the old fd; don't let a later + * plain read on the new fd extend it and misattribute that + * timestamp to the new fd's data. */ + evbuffer_invalidate_last_chain_timestamp_(bufev->input); + event_del(&bufev->ev_read); event_del(&bufev->ev_write); @@ -641,9 +756,17 @@ be_socket_setfd(struct bufferevent *bufev, evutil_socket_t fd) event_assign(&bufev->ev_write, bufev->ev_base, fd, EV_WRITE|EV_PERSIST|EV_FINALIZE, bufferevent_writecb, bufev); - if (fd >= 0) + if (fd >= 0) { bufferevent_enable(bufev, bufev->enabled); + /* Enable receive timestamps if requested */ + if (bufev_p->options & BEV_OPT_RECV_TIMESTAMPS) { + if (be_socket_enable_timestamps_(fd) >= 0) { + bufev_p->recv_timestamps_enabled = 1; + } + } + } + evutil_getaddrinfo_cancel_async_(bufev_p->dns_request); BEV_UNLOCK(bufev); diff --git a/configure.ac b/configure.ac index 38b6f247d4..e5d1153359 100644 --- a/configure.ac +++ b/configure.ac @@ -342,6 +342,13 @@ if test "x$ac_cv_header_sys_sysctl_h" = "xyes"; then ) fi +if test "x$ac_cv_header_sys_socket_h" = "xyes"; then + AC_CHECK_DECLS([SO_TIMESTAMP, SO_TIMESTAMPNS], [], [], + [[#include + #include ]] + ) +fi + AM_CONDITIONAL(BUILD_WIN32, test x$bwin32 = xtrue) AM_CONDITIONAL(BUILD_CYGWIN, test x$cygwin = xtrue) AM_CONDITIONAL(BUILD_MIDIPIX, test x$midipix = xtrue) diff --git a/evbuffer-internal.h b/evbuffer-internal.h index d09b4f1ddd..a60cd6ddd3 100644 --- a/evbuffer-internal.h +++ b/evbuffer-internal.h @@ -204,6 +204,15 @@ struct evbuffer_chain { /** number of references to this chain */ int refcnt; + + /** Timestamp support. */ + struct { + /* The timespec for the oldest data in this chunk */ + struct timespec ts; + /* valid is set to a non-zero value when ts is set */ + int valid; + } timestamp; + /** Usually points to the read-write memory belonging to this * buffer allocated as part of the evbuffer_chain allocation. * For mmap, this can be a read-only buffer and @@ -326,6 +335,32 @@ int evbuffer_read_setup_vecs_(struct evbuffer *buf, ev_ssize_t howmuch, struct evbuffer_iovec *vecs, int n_vecs, struct evbuffer_chain ***chainp, int exact); +/* Invalidate the timestamp (if any) on buf's current tail chain. See + * buffer.c for details on when this is needed. */ +void evbuffer_invalidate_last_chain_timestamp_(struct evbuffer *buf); + +/* Like evbuffer_read(), but uses recvmsg() to also capture the kernel + * receive timestamp (SO_TIMESTAMPNS/SO_TIMESTAMP) of each read, if the fd + * has one of those socket options armed (see be_socket_enable_timestamps_() + * in bufferevent_sock.c). Retrieve the captured timestamp via + * evbuffer_get_timestamp(). + * + * Internal-only: reachable exclusively through bufferevent_readcb(), which + * only calls this once be_socket_enable_timestamps_() has confirmed the fd + * is a SOCK_STREAM, non-AF_UNIX socket. This matters because recvmsg() also + * retrieves any SCM_RIGHTS ancillary data as a side effect, and that cmsg + * is not otherwise handled here (see buffer.c); SCM_RIGHTS can only ever + * arrive on an AF_UNIX socket, so this restriction is what makes it safe to + * leave unhandled. Do not expose this as a public API without re-adding + * SCM_RIGHTS handling for arbitrary caller-supplied fds. + * + * Marked EVENT2_EXPORT_SYMBOL (matching be_socket_enable_timestamps_()) so + * the test suite can call it directly despite not being in the public + * header; this does not add it to the documented/public API. */ +EVENT2_EXPORT_SYMBOL +int evbuffer_read_with_timestamp_(struct evbuffer *buffer, evutil_socket_t fd, + int howmuch); + /* Helper macro: copies an evbuffer_iovec in ei to a win32 WSABUF in i. */ #define WSABUF_FROM_EVBUFFER_IOV(i,ei) do { \ (i)->buf = (ei)->iov_base; \ diff --git a/event-config.h.cmake b/event-config.h.cmake index fccf0cf059..51eb1dd6a3 100644 --- a/event-config.h.cmake +++ b/event-config.h.cmake @@ -78,6 +78,12 @@ /* Define to 1 if you have `getrandom' function. */ #cmakedefine EVENT__HAVE_GETRANDOM 1 +/* Define to 1 if you have the declaration of `SO_TIMESTAMP'. */ +#define EVENT__HAVE_DECL_SO_TIMESTAMP @EVENT__HAVE_DECL_SO_TIMESTAMP@ + +/* Define to 1 if you have the declaration of `SO_TIMESTAMPNS'. */ +#define EVENT__HAVE_DECL_SO_TIMESTAMPNS @EVENT__HAVE_DECL_SO_TIMESTAMPNS@ + /* Define if /dev/poll is available */ #cmakedefine EVENT__HAVE_DEVPOLL 1 diff --git a/include/event2/buffer.h b/include/event2/buffer.h index 88af3ae141..98516e329c 100644 --- a/include/event2/buffer.h +++ b/include/event2/buffer.h @@ -322,7 +322,24 @@ evbuffer_reserve_space(struct evbuffer *buf, ev_ssize_t size, */ EVENT2_EXPORT_SYMBOL int evbuffer_commit_space(struct evbuffer *buf, - struct evbuffer_iovec *vec, int n_vecs); + struct evbuffer_iovec *vec, int n_vecs); + +/** + Commits the space reserved by evbuffer_reserve_space() and + associates a timespec with the committed chains. + + @param buf the evbuffer in which to reserve space. + @param vec one or two extents returned by evbuffer_reserve_space. + @param n_vecs the number of extents. + @param ts pointer to timespec (or NULL if not valid). + @return 0 on success, -1 on error + @see evbuffer_reserve_space() +*/ +EVENT2_EXPORT_SYMBOL +int evbuffer_commit_space_with_timespec(struct evbuffer *buf, + struct evbuffer_iovec *vec, int n_vecs, + const struct timespec *ts); + /** Append data to the end of an evbuffer. @@ -734,6 +751,32 @@ int evbuffer_write_atmost(struct evbuffer *buffer, evutil_socket_t fd, EVENT2_EXPORT_SYMBOL int evbuffer_read(struct evbuffer *buffer, evutil_socket_t fd, int howmuch); +/** + * Get the timestamp stored for the oldest data chain in the buffer. + * + * Returns the kernel receive timestamp associated with the oldest chain + * currently in the buffer. For TCP stream sockets, this is the timestamp + * of the last segment in the oldest read call. + * If the buffer is empty or no timestamp is available, returns -1. + * + * Note: Timestamps are stored per internal chain. When evbuffer_pullup() + * consolidates multiple chains, only the timestamp from the first (oldest) + * chain is preserved. Also, reads may append new data into an existing chain + * that does not yet have a timestamp; in that case, draining some (but not all) + * bytes from that chain will not change the reported timestamp. + * + * On TCP stream sockets, kernel receive timestamp delivery (e.g. via + * SO_TIMESTAMPNS) is best-effort; evbuffer_get_timestamp() returns -1 if + * the kernel did not attach timestamp metadata to the received data. + * + * @param buffer The buffer to read from + * @param timestamp where to store the result + * @return 0 success (timestamp was stored) + * -1 failure (buffer empty or no timestamp available) + */ +EVENT2_EXPORT_SYMBOL +int evbuffer_get_timestamp(struct evbuffer *buffer, struct timespec *timestamp); + /** Search for a string within an evbuffer. diff --git a/include/event2/bufferevent.h b/include/event2/bufferevent.h index 48cd153563..09b6e48a1e 100644 --- a/include/event2/bufferevent.h +++ b/include/event2/bufferevent.h @@ -170,7 +170,31 @@ enum bufferevent_options { * bufferevent. This option currently requires that * BEV_OPT_DEFER_CALLBACKS also be set; a future version of Libevent * might remove the requirement.*/ - BEV_OPT_UNLOCK_CALLBACKS = (1<<3) + BEV_OPT_UNLOCK_CALLBACKS = (1<<3), + + /** If set, capture kernel-measured receive timestamps for + * stream (SOCK_STREAM / TCP) socket bufferevents. Timestamps + * can be retrieved from the input buffer using + * evbuffer_get_timestamp(). Supported for stream socket + * bufferevents created with bufferevent_socket_new() and + * OpenSSL socket bufferevents created with + * bufferevent_openssl_socket_new(). + * + * Datagram sockets (SOCK_DGRAM / UDP) are not supported. + * + * This option can silently fail to take effect: unsupported on + * non-stream (SOCK_DGRAM) sockets, AF_UNIX/AF_LOCAL sockets, + * and on SOCK_STREAM sockets on BSD-derived kernels (macOS, + * FreeBSD, OpenBSD, NetBSD, DragonFly) that lack + * SO_TIMESTAMPNS, where SO_TIMESTAMP is a silent no-op for + * stream sockets. For OpenSSL bufferevents, it additionally + * requires the SSL to use a single BIO for both reading and + * writing. Use bufferevent_get_recv_timestamps_enabled() once the + * fd is set (immediately, if a real fd was passed to + * bufferevent_socket_new(); otherwise after + * bufferevent_socket_connect() or bufferevent_setfd()) to check + * whether it actually took effect. */ + BEV_OPT_RECV_TIMESTAMPS = (1<<4) }; /** @@ -482,6 +506,28 @@ int bufferevent_disable(struct bufferevent *bufev, short event); EVENT2_EXPORT_SYMBOL short bufferevent_get_enabled(struct bufferevent *bufev); +/** + Check whether kernel receive timestamps ended up enabled on a + bufferevent created with BEV_OPT_RECV_TIMESTAMPS. + + The option can silently fail to take effect -- e.g. on non-stream + (SOCK_DGRAM) sockets, on AF_UNIX/AF_LOCAL sockets, on SOCK_STREAM sockets + on BSD-derived kernels without SO_TIMESTAMPNS, or for an OpenSSL + bufferevent whose rbio/wbio are not a single plain socket BIO -- in + which case the bufferevent is created successfully but + evbuffer_get_timestamp() will never return a timestamp. Call this once + the bufferevent's fd is set to detect that case: immediately, if a real + fd was passed to bufferevent_socket_new()/bufferevent_openssl_socket_new(); + otherwise after bufferevent_socket_connect() or bufferevent_setfd(). + Before the fd is set, this always returns 0. + + @param bev the bufferevent to inspect + @return 1 if receive timestamps are armed on this bufferevent's + socket, 0 otherwise + */ +EVENT2_EXPORT_SYMBOL +int bufferevent_get_recv_timestamps_enabled(struct bufferevent *bev); + /** Set the read and write timeout for a bufferevent. diff --git a/m4/libevent_openssl.m4 b/m4/libevent_openssl.m4 index a5ea676200..83897e03d1 100644 --- a/m4/libevent_openssl.m4 +++ b/m4/libevent_openssl.m4 @@ -42,6 +42,24 @@ case "$enable_openssl" in CPPFLAGS_SAVE=$CPPFLAGS CPPFLAGS="$CPPFLAGS $OPENSSL_INCS" AC_CHECK_HEADERS([openssl/ssl.h], [], [have_openssl=no]) + if test "$have_openssl" = "yes"; then + AC_MSG_CHECKING([whether OpenSSL is >= 3.0.0 and not LibreSSL]) + AC_PREPROC_IFELSE( + [AC_LANG_PROGRAM([[ +#include +#if defined(LIBRESSL_VERSION_NUMBER) +#error unsupported-libressl +#endif +#if OPENSSL_VERSION_NUMBER < 0x30000000L +#error openssl-too-old +#endif + ]])], + [AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no]) + AC_MSG_ERROR([Libevent requires OpenSSL >= 3.0.0; LibreSSL is not \ +supported. Point PKG_CONFIG_PATH/CFLAGS/LDFLAGS at a suitable OpenSSL, or use \ +--disable-openssl to build without TLS support.])]) + fi CPPFLAGS=$CPPFLAGS_SAVE AC_SUBST(OPENSSL_INCS) AC_SUBST(OPENSSL_LIBS) diff --git a/openssl-compat.h b/openssl-compat.h index a23e34251b..326eb15a82 100644 --- a/openssl-compat.h +++ b/openssl-compat.h @@ -1,47 +1,16 @@ #ifndef OPENSSL_COMPAT_H #define OPENSSL_COMPAT_H +#include #include #include "util-internal.h" -#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || \ - (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L) - -static inline BIO_METHOD *BIO_meth_new(int type, const char *name) -{ - BIO_METHOD *biom = calloc(1, sizeof(BIO_METHOD)); - - if (biom != NULL) { - biom->type = type; - biom->name = name; - } - return biom; -} - -#define BIO_meth_set_write(b, f) (b)->bwrite = (f) -#define BIO_meth_set_read(b, f) (b)->bread = (f) -#define BIO_meth_set_puts(b, f) (b)->bputs = (f) -#define BIO_meth_set_ctrl(b, f) (b)->ctrl = (f) -#define BIO_meth_set_create(b, f) (b)->create = (f) -#define BIO_meth_set_destroy(b, f) (b)->destroy = (f) - -#define BIO_set_init(b, val) (b)->init = (val) -#define BIO_set_data(b, val) (b)->ptr = (val) -#define BIO_set_shutdown(b, val) (b)->shutdown = (val) -#define BIO_get_init(b) (b)->init -#define BIO_get_data(b) (b)->ptr -#define BIO_get_shutdown(b) (b)->shutdown - -#define TLS_method SSLv23_method - -#define X509_getm_notBefore X509_get_notBefore -#define X509_getm_notAfter X509_get_notAfter - -#endif /* (OPENSSL_VERSION_NUMBER < 0x10100000L) || \ - (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L) */ +#if defined(LIBRESSL_VERSION_NUMBER) +#error "Libevent requires OpenSSL >= 3.0.0; LibreSSL is not supported" +#endif -#if defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER >= 0x20700000L -#define BIO_get_init(b) (b)->init +#if (OPENSSL_VERSION_NUMBER < 0x30000000L) +#error "Libevent requires OpenSSL >= 3.0.0" #endif #endif /* OPENSSL_COMPAT_H */ diff --git a/sample/https-client.c b/sample/https-client.c index 5136acebd7..3fcb882916 100644 --- a/sample/https-client.c +++ b/sample/https-client.c @@ -339,15 +339,6 @@ main(int argc, char **argv) } uri[sizeof(uri) - 1] = '\0'; -#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || \ - (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L) - // Initialize OpenSSL - SSL_library_init(); - ERR_load_crypto_strings(); - SSL_load_error_strings(); - OpenSSL_add_all_algorithms(); -#endif - /* This isn't strictly necessary... OpenSSL performs RAND_poll * automatically on first use of random number generator. */ r = RAND_poll(); @@ -519,22 +510,6 @@ main(int argc, char **argv) SSL_CTX_free(ssl_ctx); if (type == HTTP && ssl) SSL_free(ssl); -#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || \ - (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L) - EVP_cleanup(); - ERR_free_strings(); - -#if OPENSSL_VERSION_NUMBER < 0x10000000L - ERR_remove_state(0); -#else - ERR_remove_thread_state(NULL); -#endif - - CRYPTO_cleanup_all_ex_data(); - - sk_SSL_COMP_free(SSL_COMP_get_compression_methods()); -#endif /* (OPENSSL_VERSION_NUMBER < 0x10100000L) || \ - (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L) */ #ifdef _WIN32 WSACleanup(); diff --git a/sample/le-proxy.c b/sample/le-proxy.c index b38b06b481..9e2afcff99 100644 --- a/sample/le-proxy.c +++ b/sample/le-proxy.c @@ -271,13 +271,6 @@ main(int argc, char **argv) if (use_ssl) { int r; -#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || \ - (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L) - SSL_library_init(); - ERR_load_crypto_strings(); - SSL_load_error_strings(); - OpenSSL_add_all_algorithms(); -#endif r = RAND_poll(); if (r == 0) { fprintf(stderr, "RAND_poll() failed.\n"); diff --git a/sample/openssl_hostname_validation.c b/sample/openssl_hostname_validation.c index 4036ccbaab..584753db7e 100644 --- a/sample/openssl_hostname_validation.c +++ b/sample/openssl_hostname_validation.c @@ -48,11 +48,6 @@ SOFTWARE. #define HOSTNAME_MAX_SIZE 255 -#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || \ - (defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L) -#define ASN1_STRING_get0_data ASN1_STRING_data -#endif - /** * Tries to find a match for hostname in the certificate's Common Name field. * diff --git a/test/regress_buffer.c b/test/regress_buffer.c index d4e1190af9..466bdbedb7 100644 --- a/test/regress_buffer.c +++ b/test/regress_buffer.c @@ -453,10 +453,238 @@ test_evbuffer_pullup_with_empty(void *ptr) tt_mem_op(evbuffer_pullup(buf, 3), ==, "foo", 3); end: - if (buf) + if (buf) { + evbuffer_free(buf); + } +} + +static void +test_evbuffer_get_timestamp(void *ptr) +{ + struct evbuffer *buf = NULL; + struct timespec ts, ts2; + struct timeval tv_sleep = { 0, 10000 }; /* 10 ms */ + int on = 1; + int r; + int ts_supported = 1; + + evutil_socket_t fd_pair[2] = { -1, -1 }; + + /* 1. Ensure empty buffer returns -1 */ + buf = evbuffer_new(); + tt_assert(buf); + tt_int_op(evbuffer_get_timestamp(buf, &ts), ==, -1); + + /* Create TCP socketpair */ + tt_assert(evutil_ersatz_socketpair_(AF_INET, SOCK_STREAM, 0, fd_pair) == 0); + evutil_make_socket_nonblocking(fd_pair[0]); + evutil_make_socket_nonblocking(fd_pair[1]); + + /* 2. Configure socket option for receive timestamps */ +#ifdef SO_TIMESTAMPNS + if (setsockopt(fd_pair[1], SOL_SOCKET, SO_TIMESTAMPNS, (void *)&on, sizeof(on)) == -1) { + ts_supported = 0; + } +#elif defined(SO_TIMESTAMP) + if (setsockopt(fd_pair[1], SOL_SOCKET, SO_TIMESTAMP, (void *)&on, sizeof(on)) == -1) { + ts_supported = 0; + } +#else + ts_supported = 0; +#endif + + /* If timestamps not supported, skip the timestamp checks */ + if (!ts_supported) { + tt_skip(); + goto end; + } + + /* Test EAGAIN handling and ensure no trailing empty chain remains */ + r = evbuffer_read_with_timestamp_(buf, fd_pair[1], 1024); + tt_int_op(r, ==, -1); + tt_assert(EVUTIL_ERR_RW_RETRIABLE(evutil_socket_geterror(fd_pair[1]))); + tt_ptr_op(buf->first, ==, NULL); + tt_ptr_op(buf->last, ==, NULL); + + /* 3. Send packet A */ + r = send(fd_pair[0], "packetA", 7, 0); + tt_int_op(r, ==, 7); + + /* Sleep briefly to let the kernel process the packet and stamp it */ + evutil_usleep_(&tv_sleep); + + /* 4. Read packet A with timestamp */ + r = evbuffer_read_with_timestamp_(buf, fd_pair[1], 1024); + tt_int_op(r, ==, 7); + + /* 5. Fetch and verify timestamp A (if kernel delivered timestamps on loopback TCP) */ + if (evbuffer_get_timestamp(buf, &ts) != 0) { + tt_skip(); + goto end; + } + tt_assert(ts.tv_sec > 0); + TT_BLATHER(("Captured timestamp A: %lld.%09ld", (long long)ts.tv_sec, (long)ts.tv_nsec)); + + /* 6. Send packet B */ + r = send(fd_pair[0], "packetB", 7, 0); + tt_int_op(r, ==, 7); + + evutil_usleep_(&tv_sleep); + + /* 7. Read packet B with timestamp */ + r = evbuffer_read_with_timestamp_(buf, fd_pair[1], 1024); + tt_int_op(r, ==, 7); + + /* 8. Fetch timestamp and verify it still returns packet A's (oldest first) */ + tt_int_op(evbuffer_get_timestamp(buf, &ts2), ==, 0); + tt_int_op(ts.tv_sec, ==, ts2.tv_sec); + tt_int_op(ts.tv_nsec, ==, ts2.tv_nsec); + + /* 9. Drain packet A's bytes. Packet A is 7 bytes. + * Draining 3 bytes should still keep packet A's timestamp. */ + tt_int_op(evbuffer_drain(buf, 3), ==, 0); + tt_int_op(evbuffer_get_timestamp(buf, &ts2), ==, 0); + tt_int_op(ts.tv_sec, ==, ts2.tv_sec); + tt_int_op(ts.tv_nsec, ==, ts2.tv_nsec); + + /* 10. Drain remaining 4 bytes of packet A. + * Each recvmsg() call writes into its own fresh chain, so after fully + * draining packet A the buffer exposes packet B's chain and its + * timestamp. The timestamp must be >= packet A's timestamp. */ + tt_int_op(evbuffer_drain(buf, 4), ==, 0); + tt_int_op(evbuffer_get_timestamp(buf, &ts2), ==, 0); + tt_assert(ts2.tv_sec >= ts.tv_sec); + if (ts2.tv_sec == ts.tv_sec) { + tt_assert(ts2.tv_nsec >= ts.tv_nsec); + } + TT_BLATHER(("Captured oldest timestamp after draining A: %lld.%09ld", + (long long)ts2.tv_sec, (long)ts2.tv_nsec)); + + /* 11. Fully drain the buffer. Assert evbuffer_get_timestamp returns -1. */ + tt_int_op(evbuffer_drain(buf, 7), ==, 0); + tt_int_op(evbuffer_get_timestamp(buf, &ts2), ==, -1); + + end: + if (buf) { evbuffer_free(buf); + } + if (fd_pair[0] != -1) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] != -1) { + evutil_closesocket(fd_pair[1]); + } } +static void +test_evbuffer_get_timestamp_unstamped_then_stamped(void *ptr) +{ + struct evbuffer *buf = NULL; + struct timespec ts, ts2; + struct timeval tv_sleep = { 0, 10000 }; /* 10 ms */ + int on = 1; + int r; + int ts_supported = 1; + struct evbuffer_iovec vec[2]; + evutil_socket_t fd_pair[2] = { -1, -1 }; + + buf = evbuffer_new(); + tt_assert(buf); + + /* 1. Test evbuffer_commit_space_with_timespec retroactive stamping guard */ + tt_int_op(evbuffer_reserve_space(buf, 64, vec, 2), >=, 1); + vec[0].iov_len = 10; + memcpy(vec[0].iov_base, "1234567890", 10); + /* Commit without timestamp */ + tt_int_op(evbuffer_commit_space_with_timespec(buf, vec, 1, NULL), ==, 0); + tt_int_op(evbuffer_get_timestamp(buf, &ts), ==, -1); + + /* Reserve space again (will get remaining space in same tail chain) */ + tt_int_op(evbuffer_reserve_space(buf, 32, vec, 2), >=, 1); + vec[0].iov_len = 10; + memcpy(vec[0].iov_base, "abcdefghij", 10); + ts2.tv_sec = 1234567; + ts2.tv_nsec = 890; + /* Commit WITH timestamp into chain that already had pre-existing data */ + tt_int_op(evbuffer_commit_space_with_timespec(buf, vec, 1, &ts2), ==, 0); + + /* Pre-existing data in chain was not retroactively stamped */ + tt_int_op(evbuffer_get_timestamp(buf, &ts), ==, -1); + + /* Clear buffer */ + evbuffer_drain(buf, evbuffer_get_length(buf)); + + /* 2. Test evbuffer_read_with_timestamp_ when tail chain already has unstamped data */ + tt_assert(evutil_ersatz_socketpair_(AF_INET, SOCK_STREAM, 0, fd_pair) == 0); + evutil_make_socket_nonblocking(fd_pair[0]); + evutil_make_socket_nonblocking(fd_pair[1]); + + /* Send packet 1 BEFORE arming timestamps */ + r = send(fd_pair[0], "unarmed1", 8, 0); + tt_int_op(r, ==, 8); + + evutil_usleep_(&tv_sleep); + + /* Read packet 1 (unarmed, so tail chain gets off=8, timestamp.valid=0) */ + r = evbuffer_read_with_timestamp_(buf, fd_pair[1], 1024); + tt_int_op(r, ==, 8); + tt_int_op(evbuffer_get_timestamp(buf, &ts), ==, -1); + + /* Now arm timestamps */ +#ifdef SO_TIMESTAMPNS + if (setsockopt(fd_pair[1], SOL_SOCKET, SO_TIMESTAMPNS, (void *)&on, sizeof(on)) == -1) { + ts_supported = 0; + } +#elif defined(SO_TIMESTAMP) + if (setsockopt(fd_pair[1], SOL_SOCKET, SO_TIMESTAMP, (void *)&on, sizeof(on)) == -1) { + ts_supported = 0; + } +#else + ts_supported = 0; +#endif + + if (!ts_supported) { + tt_skip(); + goto end; + } + + /* Send packet 2 AFTER arming timestamps */ + r = send(fd_pair[0], "armed2", 6, 0); + tt_int_op(r, ==, 6); + + evutil_usleep_(&tv_sleep); + + /* Read packet 2 (armed). Must allocate a fresh chain for packet 2 */ + r = evbuffer_read_with_timestamp_(buf, fd_pair[1], 1024); + tt_int_op(r, ==, 6); + + /* Head of buffer is packet 1 (unstamped), so get_timestamp returns -1 */ + tt_int_op(evbuffer_get_timestamp(buf, &ts), ==, -1); + + /* Drain packet 1 (8 bytes). Buffer head becomes packet 2's chain */ + tt_int_op(evbuffer_drain(buf, 8), ==, 0); + + /* Now evbuffer_get_timestamp returns packet 2's valid timestamp (if supported by kernel) */ + if (evbuffer_get_timestamp(buf, &ts) != 0) { + tt_skip(); + goto end; + } + tt_assert(ts.tv_sec > 0); + + end: + if (buf) { + evbuffer_free(buf); + } + if (fd_pair[0] != -1) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] != -1) { + evutil_closesocket(fd_pair[1]); + } +} + + + static void test_evbuffer_remove_buffer_with_empty_front(void *ptr) { @@ -2878,6 +3106,8 @@ struct testcase_t evbuffer_testcases[] = { { "copyout", test_evbuffer_copyout, 0, NULL, NULL}, { "file_segment_add_cleanup_cb", test_evbuffer_file_segment_add_cleanup_cb, 0, NULL, NULL }, { "pullup_with_empty", test_evbuffer_pullup_with_empty, 0, NULL, NULL }, + { "get_timestamp", test_evbuffer_get_timestamp, TT_FORK, &basic_setup, NULL }, + { "get_timestamp_unstamped_then_stamped", test_evbuffer_get_timestamp_unstamped_then_stamped, TT_FORK, &basic_setup, NULL }, #define ADDFILE_TEST(name, parameters) \ { name, test_evbuffer_add_file, TT_FORK|TT_NEED_BASE, \ diff --git a/test/regress_bufferevent.c b/test/regress_bufferevent.c index c276a0e5d1..24e6571625 100644 --- a/test/regress_bufferevent.c +++ b/test/regress_bufferevent.c @@ -1354,6 +1354,229 @@ test_bufferevent_filter_data_stuck(void *arg) bufferevent_free(filter); } +static void +bufferevent_recv_timestamps_readcb(struct bufferevent *bev, void *ctx) +{ + int *done = ctx; + struct timespec ts; + char tmp[32]; + int r; + + /* Fetch and verify timestamps BEFORE draining the buffer! */ + if (evbuffer_get_timestamp(bufferevent_get_input(bev), &ts) < 0) { + /* Timestamp capture not supported/enabled on this platform/socket. */ + *done = 0; + event_base_loopexit(bufferevent_get_base(bev), NULL); + goto end; + } + + tt_assert(ts.tv_sec > 0); + r = bufferevent_read(bev, tmp, sizeof(tmp)); + tt_int_op(r, ==, 14); + tt_mem_op(tmp, ==, "timestamp_test", 14); + + *done = 1; + + end: + event_base_loopexit(bufferevent_get_base(bev), NULL); +} + +static void +test_bufferevent_recv_timestamps(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev1 = NULL; + struct bufferevent *bev2 = NULL; + struct timespec ts; + int done = 0; + evutil_socket_t fd_pair[2] = { -1, -1 }; + evutil_socket_t new_pair[2] = { -1, -1 }; + + /* Create TCP socketpair */ + tt_assert(evutil_ersatz_socketpair_(AF_INET, SOCK_STREAM, 0, fd_pair) == 0); + evutil_make_socket_nonblocking(fd_pair[0]); + evutil_make_socket_nonblocking(fd_pair[1]); + + /* 1. Create bufferevents (bev2 has BEV_OPT_RECV_TIMESTAMPS enabled) */ + bev1 = bufferevent_socket_new(data->base, fd_pair[0], BEV_OPT_CLOSE_ON_FREE); + tt_assert(bev1); + fd_pair[0] = -1; /* bev1 owns it now */ + bev2 = bufferevent_socket_new(data->base, fd_pair[1], BEV_OPT_CLOSE_ON_FREE | BEV_OPT_RECV_TIMESTAMPS); + tt_assert(bev2); + fd_pair[1] = -1; /* bev2 owns it now */ + + /* 2. Verify that initially no timestamps are present */ + tt_int_op(evbuffer_get_timestamp(bufferevent_get_input(bev2), &ts), ==, -1); + + /* 3. Enable writing on bev1 and write data */ + tt_int_op(bufferevent_enable(bev1, EV_WRITE), ==, 0); + tt_int_op(bufferevent_write(bev1, "timestamp_test", 14), ==, 0); + + /* Configure callback and enable read on bev2 */ + bufferevent_setcb(bev2, bufferevent_recv_timestamps_readcb, NULL, NULL, &done); + tt_int_op(bufferevent_enable(bev2, EV_READ), ==, 0); + + /* 4. Dispatch event loop and wait for arrival */ + event_base_dispatch(data->base); + + /* If timestamp capture failed (not supported), skip the test */ + if (done == 0) { + tt_skip(); + goto end; + } + + tt_int_op(done, ==, 1); + + /* 5. Swap bev2 onto a brand new fd via bufferevent_setfd() and verify + * that receive timestamps get re-armed on it too: recv_timestamps_enabled + * must not stay stuck "on" from the old fd and suppress the setsockopt() + * on the new one. */ + tt_assert(evutil_ersatz_socketpair_(AF_INET, SOCK_STREAM, 0, new_pair) == 0); + evutil_make_socket_nonblocking(new_pair[0]); + evutil_make_socket_nonblocking(new_pair[1]); + + { + /* bufferevent_setfd() does not close the fd it displaces; + * close it ourselves or it leaks (it's already been marked + * as not owned by fd_pair[1] above, at step 1). */ + evutil_socket_t old_fd = bufferevent_getfd(bev2); + tt_int_op(bufferevent_setfd(bev2, new_pair[1]), ==, 0); + new_pair[1] = -1; /* bev2 owns it now */ + if (old_fd != -1) { + evutil_closesocket(old_fd); + } + } + + done = 0; + tt_int_op(send(new_pair[0], "timestamp_test", 14, 0), ==, 14); + event_base_dispatch(data->base); + tt_int_op(done, ==, 1); + + end: + if (bev1) { + bufferevent_free(bev1); + } + if (bev2) { + bufferevent_free(bev2); + } + if (fd_pair[0] != -1) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] != -1) { + evutil_closesocket(fd_pair[1]); + } + if (new_pair[0] != -1) { + evutil_closesocket(new_pair[0]); + } + if (new_pair[1] != -1) { + evutil_closesocket(new_pair[1]); + } +} + +static void +bufferevent_recv_timestamps_tcp_readcb(struct bufferevent *bev, void *ctx) +{ + int *done = ctx; + struct timespec ts; + char tmp[32]; + int r; + int ts_result; + + ts_result = evbuffer_get_timestamp(bufferevent_get_input(bev), &ts); +#if EVENT__HAVE_DECL_SO_TIMESTAMPNS + if (ts_result == 0) { + tt_assert(ts.tv_sec > 0); + } +#else + tt_int_op(ts_result, ==, -1); +#endif + + r = bufferevent_read(bev, tmp, sizeof(tmp)); + tt_int_op(r, ==, 14); + tt_mem_op(tmp, ==, "timestamp_test", 14); + + *done = 1; + + end: + event_base_loopexit(bufferevent_get_base(bev), NULL); +} + +static void +test_bufferevent_recv_timestamps_tcp(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev1 = NULL; + struct bufferevent *bev2 = NULL; + int done = 0; + evutil_socket_t fd_pair[2] = { -1, -1 }; + + tt_assert(evutil_ersatz_socketpair_(AF_INET, SOCK_STREAM, 0, fd_pair) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[0]) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[1]) == 0); + + bev1 = bufferevent_socket_new(data->base, fd_pair[0], BEV_OPT_CLOSE_ON_FREE); + tt_assert(bev1); + fd_pair[0] = -1; + + bev2 = bufferevent_socket_new(data->base, fd_pair[1], BEV_OPT_CLOSE_ON_FREE | BEV_OPT_RECV_TIMESTAMPS); + tt_assert(bev2); + fd_pair[1] = -1; + + bufferevent_setcb(bev2, bufferevent_recv_timestamps_tcp_readcb, NULL, NULL, &done); + tt_int_op(bufferevent_enable(bev1, EV_WRITE), ==, 0); + tt_int_op(bufferevent_enable(bev2, EV_READ), ==, 0); + + tt_int_op(bufferevent_write(bev1, "timestamp_test", 14), ==, 0); + event_base_dispatch(data->base); + + tt_int_op(done, ==, 1); + + end: + if (bev1) { + bufferevent_free(bev1); + } + if (bev2) { + bufferevent_free(bev2); + } + if (fd_pair[0] >= 0) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] >= 0) { + evutil_closesocket(fd_pair[1]); + } +} + +#if defined(AF_UNIX) +static void +test_bufferevent_recv_timestamps_af_unix(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev = NULL; + evutil_socket_t fd_pair[2] = { -1, -1 }; + + if (evutil_socketpair(AF_UNIX, SOCK_STREAM, 0, fd_pair) < 0) { + tt_skip(); + } + tt_assert(evutil_make_socket_nonblocking(fd_pair[0]) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[1]) == 0); + + bev = bufferevent_socket_new(data->base, fd_pair[0], BEV_OPT_CLOSE_ON_FREE | BEV_OPT_RECV_TIMESTAMPS); + tt_assert(bev); + fd_pair[0] = -1; + + /* Verify receive timestamps are reported as unsupported for AF_UNIX socket */ + tt_int_op(be_socket_enable_timestamps_(bufferevent_getfd(bev)), ==, -1); + + end: + if (bev) { + bufferevent_free(bev); + } + if (fd_pair[1] >= 0) { + evutil_closesocket(fd_pair[1]); + } +} +#endif + struct testcase_t bufferevent_testcases[] = { LEGACY(bufferevent, TT_ISOLATED), @@ -1429,6 +1652,17 @@ struct testcase_t bufferevent_testcases[] = { { "bufferevent_filter_data_stuck", test_bufferevent_filter_data_stuck, TT_FORK|TT_NEED_BASE, &basic_setup, NULL }, + { "bufferevent_recv_timestamps", + test_bufferevent_recv_timestamps, + TT_FORK|TT_NEED_BASE, &basic_setup, NULL }, + { "bufferevent_recv_timestamps_tcp", + test_bufferevent_recv_timestamps_tcp, + TT_FORK|TT_NEED_BASE, &basic_setup, NULL }, +#if defined(AF_UNIX) + { "bufferevent_recv_timestamps_af_unix", + test_bufferevent_recv_timestamps_af_unix, + TT_FORK|TT_NEED_BASE, &basic_setup, NULL }, +#endif END_OF_TESTCASES, }; diff --git a/test/regress_http.c b/test/regress_http.c index 6056e39670..1ac5b49af7 100644 --- a/test/regress_http.c +++ b/test/regress_http.c @@ -61,6 +61,10 @@ #include "event2/bufferevent_ssl.h" #include "event2/util.h" #include "event2/listener.h" +#ifdef EVENT__HAVE_OPENSSL +#include +#include +#endif #include "log-internal.h" #include "http-internal.h" #include "regress.h" @@ -121,13 +125,20 @@ static struct bufferevent * https_bev(struct event_base *base, void *arg) { SSL *ssl = SSL_new(get_ssl_ctx()); + struct bufferevent *bev; SSL_use_certificate(ssl, ssl_getcert(ssl_getkey())); SSL_use_PrivateKey(ssl, ssl_getkey()); - return bufferevent_openssl_socket_new( + bev = bufferevent_openssl_socket_new( base, -1, ssl, BUFFEREVENT_SSL_ACCEPTING, BEV_OPT_CLOSE_ON_FREE); + if (!bev) { + SSL_free(ssl); + return NULL; + } + bufferevent_openssl_set_allow_dirty_shutdown(bev, 1); + return bev; } #endif static struct evhttp * @@ -3078,10 +3089,24 @@ http_incomplete_errorcb(struct bufferevent *bev, short what, void *arg) if (what & BEV_EVENT_CONNECTED) return; - if (what == (BEV_EVENT_READING|BEV_EVENT_EOF)) + if (what == (BEV_EVENT_READING|BEV_EVENT_EOF)) { test_ok++; - else + } else if (what == (BEV_EVENT_READING|BEV_EVENT_ERROR)) { +#ifdef EVENT__HAVE_OPENSSL + /* Under SSL, raw socket shutdowns trigger TLS alert protocol errors on OpenSSL 3.0. + * We accept this as a successful termination for this incomplete request test. */ + if (ERR_GET_REASON(bufferevent_get_openssl_error(bev)) == + SSL_R_UNEXPECTED_EOF_WHILE_READING) { + test_ok++; + } else { + test_ok = -2; + } +#else test_ok = -2; +#endif + } else { + test_ok = -2; + } event_base_loopexit(exit_base,NULL); } diff --git a/test/regress_ssl.c b/test/regress_ssl.c index e7c4023e0b..26d8ca6225 100644 --- a/test/regress_ssl.c +++ b/test/regress_ssl.c @@ -988,6 +988,616 @@ regress_bufferevent_openssl_wm(void *arg) event_base_loop(base, EVLOOP_ONCE); } +static void +bufferevent_openssl_recv_timestamps_readcb(struct bufferevent *bev, void *ctx) +{ + int *done = ctx; + struct timespec ts; + struct evbuffer *input; + char tmp[32]; + int r; + int ts_result; + + /* Fetch and verify timestamps BEFORE draining the buffer! */ + input = bufferevent_get_input(bev); + ts_result = evbuffer_get_timestamp(input, &ts); + +#if EVENT__HAVE_DECL_SO_TIMESTAMPNS + if (ts_result == 0) { + tt_assert(ts.tv_sec > 0); + } +#else + tt_int_op(ts_result, ==, -1); +#endif + + r = bufferevent_read(bev, tmp, sizeof(tmp)); + tt_int_op(r, ==, 14); + tt_mem_op(tmp, ==, "timestamp_test", 14); + + *done = 1; + + end: + event_base_loopexit(bufferevent_get_base(bev), NULL); +} + +static void +test_eventcb(struct bufferevent *bev, short what, void *ctx) +{ + TT_BLATHER(("test_eventcb: %p got event %d", bev, (int)what)); + if (what & BEV_EVENT_ERROR) { + unsigned long err; + while ((err = ERR_get_error())) { + TT_BLATHER((" SSL error: %s", ERR_error_string(err, NULL))); + } + } +} + +static void +test_bufferevent_openssl_direct_recv_timestamps(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev1 = NULL; + struct bufferevent *bev2 = NULL; + SSL *ssl1 = NULL, *ssl2 = NULL; + struct timespec ts; + int done = 0; + evutil_socket_t fd_pair[2] = { -1, -1 }; + + /* Create a TCP socketpair */ + tt_assert(evutil_ersatz_socketpair_(AF_INET, SOCK_STREAM, 0, fd_pair) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[0]) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[1]) == 0); + + ssl1 = SSL_new(get_ssl_ctx()); + ssl2 = SSL_new(get_ssl_ctx()); + tt_assert(ssl1); + tt_assert(ssl2); + + SSL_use_certificate(ssl2, the_cert); + SSL_use_PrivateKey(ssl2, the_key); + + /* Create direct socket openssl bufferevents. + * bev2 has BEV_OPT_RECV_TIMESTAMPS enabled. */ + bev1 = bufferevent_openssl_socket_new( + data->base, fd_pair[0], ssl1, BUFFEREVENT_SSL_CONNECTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + tt_assert(bev1); + fd_pair[0] = -1; + + bev2 = bufferevent_openssl_socket_new( + data->base, fd_pair[1], ssl2, BUFFEREVENT_SSL_ACCEPTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS | BEV_OPT_RECV_TIMESTAMPS); + tt_assert(bev2); + fd_pair[1] = -1; + + /* Verify initially no timestamps are present */ + tt_int_op(evbuffer_get_timestamp(bufferevent_get_input(bev2), &ts), ==, -1); + + /* Configure callbacks */ + bufferevent_setcb(bev1, NULL, NULL, test_eventcb, NULL); + bufferevent_setcb(bev2, bufferevent_openssl_recv_timestamps_readcb, NULL, test_eventcb, &done); + tt_int_op(bufferevent_enable(bev1, EV_READ|EV_WRITE), ==, 0); + tt_int_op(bufferevent_enable(bev2, EV_READ|EV_WRITE), ==, 0); + + /* Write data from bev1 */ + tt_int_op(bufferevent_write(bev1, "timestamp_test", 14), ==, 0); + + /* Dispatch base */ + event_base_dispatch(data->base); + + tt_int_op(done, ==, 1); + + end: + if (bev1) { + bufferevent_free(bev1); + } + if (bev2) { + bufferevent_free(bev2); + } + if (fd_pair[0] >= 0) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] >= 0) { + evutil_closesocket(fd_pair[1]); + } +} + +static void +bufferevent_openssl_filter_recv_timestamps_readcb(struct bufferevent *bev, void *ctx) +{ + int *done = ctx; + struct timespec ts; + struct evbuffer *input; + char tmp[32]; + int r; + int ts_result; + + input = bufferevent_get_input(bev); + ts_result = evbuffer_get_timestamp(input, &ts); + +#if EVENT__HAVE_DECL_SO_TIMESTAMPNS + if (ts_result == 0) { + tt_assert(ts.tv_sec > 0); + } +#else + tt_int_op(ts_result, ==, -1); +#endif + + r = bufferevent_read(bev, tmp, sizeof(tmp)); + tt_int_op(r, ==, 9); + tt_mem_op(tmp, ==, "test_data", 9); + + *done = 1; + + end: + event_base_loopexit(bufferevent_get_base(bev), NULL); +} + +static void +test_bufferevent_openssl_filter_recv_timestamps(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev1 = NULL; + struct bufferevent *bev2 = NULL; + struct bufferevent *underlying_bev1 = NULL; + struct bufferevent *underlying_bev2 = NULL; + SSL *ssl1 = NULL, *ssl2 = NULL; + int done = 0; + evutil_socket_t fd_pair[2] = {-1, -1}; + + /* Create TCP socketpair */ + tt_assert(evutil_ersatz_socketpair_(AF_INET, SOCK_STREAM, 0, fd_pair) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[0]) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[1]) == 0); + + ssl1 = SSL_new(get_ssl_ctx()); + ssl2 = SSL_new(get_ssl_ctx()); + tt_assert(ssl1); + tt_assert(ssl2); + + SSL_use_certificate(ssl2, the_cert); + SSL_use_PrivateKey(ssl2, the_key); + + /* Create underlying socket bufferevents */ + underlying_bev1 = bufferevent_socket_new(data->base, fd_pair[0], + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + tt_assert(underlying_bev1); + fd_pair[0] = -1; + + underlying_bev2 = bufferevent_socket_new(data->base, fd_pair[1], + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS | BEV_OPT_RECV_TIMESTAMPS); + tt_assert(underlying_bev2); + fd_pair[1] = -1; + + /* Create filtered openssl bufferevents that wrap the socket bufferevents */ + bev1 = bufferevent_openssl_filter_new(data->base, underlying_bev1, ssl1, + BUFFEREVENT_SSL_CONNECTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + tt_assert(bev1); + underlying_bev1 = NULL; /* ownership transferred */ + + bev2 = bufferevent_openssl_filter_new(data->base, underlying_bev2, ssl2, + BUFFEREVENT_SSL_ACCEPTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + tt_assert(bev2); + underlying_bev2 = NULL; /* ownership transferred */ + + /* Configure callbacks for basic data flow */ + bufferevent_setcb(bev1, NULL, NULL, test_eventcb, NULL); + bufferevent_setcb(bev2, bufferevent_openssl_filter_recv_timestamps_readcb, NULL, test_eventcb, &done); + tt_int_op(bufferevent_enable(bev1, EV_READ | EV_WRITE), ==, 0); + tt_int_op(bufferevent_enable(bev2, EV_READ | EV_WRITE), ==, 0); + + /* Write data from bev1 */ + tt_int_op(bufferevent_write(bev1, "test_data", 9), ==, 0); + + /* Dispatch base - just ensure filtered mode with timestamp code paths works + */ + event_base_dispatch(data->base); + + tt_int_op(done, ==, 1); + +end: + if (bev1) { + bufferevent_free(bev1); + } + if (bev2) { + bufferevent_free(bev2); + } + if (underlying_bev1) { + bufferevent_free(underlying_bev1); + } + if (underlying_bev2) { + bufferevent_free(underlying_bev2); + } + if (fd_pair[0] >= 0) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] >= 0) { + evutil_closesocket(fd_pair[1]); + } +} + +static void +test_bufferevent_openssl_split_bio_recv_timestamps(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev = NULL; + SSL *ssl = NULL; + BIO *rbio = NULL, *wbio = NULL; + evutil_socket_t fd_r = -1, fd_w = -1; + + /* Two distinct TCP sockets, so SO_TIMESTAMP can be tested on the + * write-side fd while rbio/wbio remain genuinely distinct BIOs, as + * happens with SSL_set_rfd()/SSL_set_wfd() or a read-side filter + * chain installed via SSL_set_bio(). */ + fd_r = socket(AF_INET, SOCK_STREAM, 0); + tt_assert(fd_r != EVUTIL_INVALID_SOCKET); + tt_assert(evutil_make_socket_nonblocking(fd_r) == 0); + fd_w = socket(AF_INET, SOCK_STREAM, 0); + tt_assert(fd_w != EVUTIL_INVALID_SOCKET); + tt_assert(evutil_make_socket_nonblocking(fd_w) == 0); + + ssl = SSL_new(get_ssl_ctx()); + tt_assert(ssl); + + rbio = BIO_new_socket((int)fd_r, BIO_NOCLOSE); + tt_assert(rbio); + wbio = BIO_new_socket((int)fd_w, BIO_NOCLOSE); + tt_assert(wbio); + SSL_set_bio(ssl, rbio, wbio); /* ssl now owns rbio/wbio */ + + /* BUFFEREVENT_SSL_OPEN skips the handshake, so this only exercises + * bufferevent_openssl_socket_new()'s BIO setup logic. */ + bev = bufferevent_openssl_socket_new(data->base, -1, ssl, + BUFFEREVENT_SSL_OPEN, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_RECV_TIMESTAMPS); + tt_assert(bev); + + /* The rbio/wbio must be left untouched: SSL_set_bio() replaces both + * slots at once, so blindly calling it here for a split pair would + * silently free the real rbio (losing any buffered read state) and + * redirect reads onto the write-side fd. */ + tt_ptr_op(SSL_get_rbio(ssl), ==, rbio); + tt_ptr_op(SSL_get_wbio(ssl), ==, wbio); + + /* fd_w must not have had SO_TIMESTAMP(NS) armed on it either: it's + * the write-side fd, not the one data is actually read from, so + * arming it would just be a wasted/misleading setsockopt() on the + * wrong socket. */ + { + int on = 0; + ev_socklen_t len = sizeof(on); +#ifdef SO_TIMESTAMPNS + tt_assert(getsockopt(fd_w, SOL_SOCKET, SO_TIMESTAMPNS, (void *)&on, &len) == 0); + tt_int_op(on, ==, 0); +#elif defined(SO_TIMESTAMP) + tt_assert(getsockopt(fd_w, SOL_SOCKET, SO_TIMESTAMP, (void *)&on, &len) == 0); + tt_int_op(on, ==, 0); +#endif + } + + end: + if (bev) { + /* Frees ssl (BEV_OPT_CLOSE_ON_FREE), which frees rbio/wbio and + * closes fd_w; fd_r is not owned by anything here. */ + bufferevent_free(bev); + } else { + /* No bev means fd_w's ownership was never handed off: wbio + * (if any) was created BIO_NOCLOSE, so freeing ssl does not + * close it, and fd_w must be closed here instead. */ + if (ssl) { + SSL_free(ssl); + } + if (fd_w != -1) { + evutil_closesocket(fd_w); + } + } + if (fd_r != -1) { + evutil_closesocket(fd_r); + } +} + +static void +bufferevent_openssl_recv_ts_interleaved_readcb(struct bufferevent *bev, void *ctx) +{ + struct timespec *out_ts = ctx; + char tmp[32]; + int r; + + /* Fetch the timestamp BEFORE draining the buffer! */ + tt_int_op(evbuffer_get_timestamp(bufferevent_get_input(bev), out_ts), ==, 0); + + r = bufferevent_read(bev, tmp, sizeof(tmp)); + tt_int_op(r, ==, 9); + tt_mem_op(tmp, ==, "chunkedts", 9); + + end: + event_base_loopexit(bufferevent_get_base(bev), NULL); +} + +static void +bufferevent_openssl_recv_ts_interleaved_eventcb(struct bufferevent *bev, short what, void *ctx) +{ + int *connected = ctx; + if (what & BEV_EVENT_CONNECTED) { + *connected = 1; + } + if (what & BEV_EVENT_ERROR) { + unsigned long err; + while ((err = ERR_get_error())) { + TT_BLATHER((" SSL error: %s", ERR_error_string(err, NULL))); + } + } +} + +/* Regression test for the bug fixed alongside this test: do_write() and + * do_handshake() used to unconditionally clear any BIO-cached recv + * timestamp, including one a still-in-progress do_read() was waiting to + * consume. So a write interleaved between the two recvmsg() calls needed + * to reassemble a single TLS record (because it arrived as two separate + * physical reads) would cause the record to be attributed the *second* + * recvmsg()'s timestamp instead of the first (oldest) one. + * + * To reproduce that deterministically: + * 1. Encrypt one small record via a direct SSL_write() into a memory + * BIO, so we get our hands on the exact ciphertext bytes without + * letting OpenSSL send them. + * 2. send() the first half of those bytes raw, and let the receiving + * bufferevent_openssl partially read it: not enough ciphertext yet, + * so SSL_read() returns WANT_READ and the timestamp of this first + * read is cached, unconsumed. + * 3. Capture a wall-clock cutoff, then perform an ordinary write on the + * *receiving* bufferevent, forcing its do_write() to run while that + * cached timestamp is still pending. + * 4. send() the second half of the ciphertext -- necessarily stamped + * with a kernel timestamp *after* the cutoff -- and let do_read() + * finish reassembling the record. + * With the bug, the record ends up stamped with the second (post-cutoff) + * recvmsg()'s timestamp; with the fix, it keeps the first (pre-cutoff) + * one. A plain "is there a timestamp at all" check can't tell the two + * apart, since both would report success either way. + * + * Some platforms' kernels never deliver TCP receive timestamps at all + * (e.g. macOS treats SO_TIMESTAMP on SOCK_STREAM sockets as a silent + * no-op); a single ordinary write is used as an upfront probe for that, + * and the test skips instead of asserting anything meaningless if it + * doesn't work. */ +static void +test_bufferevent_openssl_recv_ts_interleaved_write(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev1 = NULL; + struct bufferevent *bev2 = NULL; + SSL *ssl1 = NULL, *ssl2 = NULL; + BIO *mem_wbio = NULL; + struct timespec ts; + struct timeval cutoff; + char tmp[32]; + int connected = 0; + int i; + unsigned char *ciphertext = NULL; + long ciphertext_len; + long first_len; + evutil_socket_t fd_send; + evutil_socket_t fd_pair[2] = { -1, -1 }; + int r; + + tt_assert(evutil_ersatz_socketpair_(AF_INET, SOCK_STREAM, 0, fd_pair) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[0]) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[1]) == 0); + + ssl1 = SSL_new(get_ssl_ctx()); + ssl2 = SSL_new(get_ssl_ctx()); + tt_assert(ssl1); + tt_assert(ssl2); + + SSL_use_certificate(ssl2, the_cert); + SSL_use_PrivateKey(ssl2, the_key); + + /* bev1 is the sender: a plain OpenSSL client, no timestamps needed. + * bev2 is the receiver under test. */ + bev1 = bufferevent_openssl_socket_new( + data->base, fd_pair[0], ssl1, BUFFEREVENT_SSL_CONNECTING, + BEV_OPT_CLOSE_ON_FREE); + tt_assert(bev1); + fd_pair[0] = -1; + + bev2 = bufferevent_openssl_socket_new( + data->base, fd_pair[1], ssl2, BUFFEREVENT_SSL_ACCEPTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_RECV_TIMESTAMPS); + tt_assert(bev2); + fd_pair[1] = -1; + + bufferevent_setcb(bev1, NULL, NULL, + bufferevent_openssl_recv_ts_interleaved_eventcb, &connected); + bufferevent_setcb(bev2, NULL, NULL, test_eventcb, NULL); + tt_int_op(bufferevent_enable(bev1, EV_READ|EV_WRITE), ==, 0); + tt_int_op(bufferevent_enable(bev2, EV_READ|EV_WRITE), ==, 0); + + /* Drive the handshake to completion. */ + for (i = 0; i < 100 && !connected; ++i) { + event_base_loop(data->base, EVLOOP_ONCE); + } + tt_assert(connected); + + /* Probe: does this platform's kernel deliver TCP recv timestamps at + * all? An ordinary single-shot write should get one if so. */ + tt_int_op(bufferevent_write(bev1, "probe", 5), ==, 0); + for (i = 0; i < 100 && evbuffer_get_length(bufferevent_get_input(bev2)) < 5; ++i) { + event_base_loop(data->base, EVLOOP_ONCE); + } + tt_int_op(evbuffer_get_length(bufferevent_get_input(bev2)), ==, 5); + if (evbuffer_get_timestamp(bufferevent_get_input(bev2), &ts) != 0) { + tt_skip(); + } + tt_int_op(bufferevent_read(bev2, tmp, sizeof(tmp)), ==, 5); + + /* Encrypt the real test record into a memory BIO instead of letting + * it go out over the wire, so we can send its ciphertext ourselves + * in two separate raw chunks. */ + mem_wbio = BIO_new(BIO_s_mem()); + tt_assert(mem_wbio); + SSL_set_bio(ssl1, SSL_get_rbio(ssl1), mem_wbio); /* ssl1 now owns mem_wbio */ + + r = SSL_write(ssl1, "chunkedts", 9); + tt_int_op(r, ==, 9); + + ciphertext_len = BIO_get_mem_data(mem_wbio, &ciphertext); + tt_assert(ciphertext_len > 1); + first_len = ciphertext_len / 2; + + fd_send = bufferevent_getfd(bev1); + tt_assert(fd_send >= 0); + + /* Send the first half; let bev2 partially read it (not enough + * ciphertext for a full record yet, so SSL_read() returns + * WANT_READ and the timestamp of this read is cached, pending). */ + r = send(fd_send, ciphertext, first_len, 0); + tt_int_op(r, ==, first_len); + event_base_loop(data->base, EVLOOP_ONCE); + + /* Everything sent after this point is necessarily stamped later by + * the kernel than everything already delivered above. */ + tt_assert(evutil_gettimeofday(&cutoff, NULL) == 0); + + /* Interleave an ordinary write on bev2 -- the receiver -- forcing + * its do_write() to run while the timestamp above is still + * pending. */ + tt_int_op(bufferevent_write(bev2, "X", 1), ==, 0); + event_base_loop(data->base, EVLOOP_ONCE); + + /* Send the second half; bev2 can now finish reassembling the + * record. */ + r = send(fd_send, ciphertext + first_len, ciphertext_len - first_len, 0); + tt_int_op(r, ==, ciphertext_len - first_len); + + bufferevent_setcb(bev2, bufferevent_openssl_recv_ts_interleaved_readcb, + NULL, test_eventcb, &ts); + event_base_dispatch(data->base); + + /* The record must be stamped with the first (pre-cutoff) recvmsg()'s + * timestamp, not the second (post-cutoff) one. */ + tt_assert(ts.tv_sec < cutoff.tv_sec || + (ts.tv_sec == cutoff.tv_sec && ts.tv_nsec < cutoff.tv_usec * 1000L)); + + end: + if (bev1) { + bufferevent_free(bev1); + } + if (bev2) { + bufferevent_free(bev2); + } + if (fd_pair[0] >= 0) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] >= 0) { + evutil_closesocket(fd_pair[1]); + } +} + +/* Regression test: has_recv_ts must be retired once SSL_pending() has no + * more leftover decrypted bytes from the read it was captured for. + * Without that, bio_socket_recvmsg_read() would refuse to ever record a + * new timestamp again once has_recv_ts got set once, so every record + * after the first would be silently mis-attributed to the very first + * timestamp ever captured on the connection -- two records sent well + * apart in time would still come back with identical timestamps. */ +static void +test_bufferevent_openssl_recv_ts_multiple_records(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev1 = NULL; + struct bufferevent *bev2 = NULL; + SSL *ssl1 = NULL, *ssl2 = NULL; + struct timespec ts1, ts2; + struct timeval delay = { 0, 20 * 1000 }; + char tmp[32]; + int connected = 0; + int i; + evutil_socket_t fd_pair[2] = { -1, -1 }; + + tt_assert(evutil_ersatz_socketpair_(AF_INET, SOCK_STREAM, 0, fd_pair) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[0]) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[1]) == 0); + + ssl1 = SSL_new(get_ssl_ctx()); + ssl2 = SSL_new(get_ssl_ctx()); + tt_assert(ssl1); + tt_assert(ssl2); + + SSL_use_certificate(ssl2, the_cert); + SSL_use_PrivateKey(ssl2, the_key); + + bev1 = bufferevent_openssl_socket_new( + data->base, fd_pair[0], ssl1, BUFFEREVENT_SSL_CONNECTING, + BEV_OPT_CLOSE_ON_FREE); + tt_assert(bev1); + fd_pair[0] = -1; + + bev2 = bufferevent_openssl_socket_new( + data->base, fd_pair[1], ssl2, BUFFEREVENT_SSL_ACCEPTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_RECV_TIMESTAMPS); + tt_assert(bev2); + fd_pair[1] = -1; + + bufferevent_setcb(bev1, NULL, NULL, + bufferevent_openssl_recv_ts_interleaved_eventcb, &connected); + bufferevent_setcb(bev2, NULL, NULL, test_eventcb, NULL); + tt_int_op(bufferevent_enable(bev1, EV_READ|EV_WRITE), ==, 0); + tt_int_op(bufferevent_enable(bev2, EV_READ|EV_WRITE), ==, 0); + + /* Drive the handshake to completion. */ + for (i = 0; i < 100 && !connected; ++i) { + event_base_loop(data->base, EVLOOP_ONCE); + } + tt_assert(connected); + + /* First record. */ + tt_int_op(bufferevent_write(bev1, "record-one", 10), ==, 0); + for (i = 0; i < 100 && evbuffer_get_length(bufferevent_get_input(bev2)) < 10; ++i) { + event_base_loop(data->base, EVLOOP_ONCE); + } + tt_int_op(evbuffer_get_length(bufferevent_get_input(bev2)), ==, 10); + if (evbuffer_get_timestamp(bufferevent_get_input(bev2), &ts1) != 0) { + /* This platform/sandbox doesn't actually deliver kernel recv + * timestamps at runtime; nothing to regress-test here. */ + tt_skip(); + } + tt_int_op(bufferevent_read(bev2, tmp, sizeof(tmp)), ==, 10); + + /* Make sure the kernel clock actually advances before the next + * record is sent, so a stuck timestamp can't accidentally read back + * as correct. */ + evutil_usleep_(&delay); + + /* Second, entirely separate record. */ + tt_int_op(bufferevent_write(bev1, "record-two", 10), ==, 0); + for (i = 0; i < 100 && evbuffer_get_length(bufferevent_get_input(bev2)) < 10; ++i) { + event_base_loop(data->base, EVLOOP_ONCE); + } + tt_int_op(evbuffer_get_length(bufferevent_get_input(bev2)), ==, 10); + tt_int_op(evbuffer_get_timestamp(bufferevent_get_input(bev2), &ts2), ==, 0); + tt_int_op(bufferevent_read(bev2, tmp, sizeof(tmp)), ==, 10); + + tt_assert(ts2.tv_sec > ts1.tv_sec || + (ts2.tv_sec == ts1.tv_sec && ts2.tv_nsec > ts1.tv_nsec)); + + end: + if (bev1) { + bufferevent_free(bev1); + } + if (bev2) { + bufferevent_free(bev2); + } + if (fd_pair[0] >= 0) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] >= 0) { + evutil_closesocket(fd_pair[1]); + } +} + struct testcase_t ssl_testcases[] = { #define T(a) ((void *)(a)) { "bufferevent_socketpair", regress_bufferevent_openssl, @@ -1071,6 +1681,16 @@ struct testcase_t ssl_testcases[] = { TT_FORK|TT_NEED_BASE, &ssl_setup, T(REGRESS_DEFERRED_CALLBACKS) }, { "bufferevent_wm_filter_defer", regress_bufferevent_openssl_wm, TT_FORK|TT_NEED_BASE, &ssl_setup, T(REGRESS_OPENSSL_FILTER|REGRESS_DEFERRED_CALLBACKS) }, + { "bufferevent_openssl_direct_recv_timestamps", test_bufferevent_openssl_direct_recv_timestamps, + TT_FORK|TT_NEED_BASE, &ssl_setup, NULL }, + { "bufferevent_openssl_filter_recv_timestamps", test_bufferevent_openssl_filter_recv_timestamps, + TT_FORK|TT_NEED_BASE, &ssl_setup, NULL }, + { "bufferevent_openssl_split_bio_recv_timestamps", test_bufferevent_openssl_split_bio_recv_timestamps, + TT_FORK|TT_NEED_BASE, &ssl_setup, NULL }, + { "bufferevent_openssl_recv_ts_interleaved_write", test_bufferevent_openssl_recv_ts_interleaved_write, + TT_FORK|TT_NEED_BASE, &ssl_setup, NULL }, + { "bufferevent_openssl_recv_ts_multiple_records", test_bufferevent_openssl_recv_ts_multiple_records, + TT_FORK|TT_NEED_BASE, &ssl_setup, NULL }, #undef T diff --git a/whatsnew-2.1.txt b/whatsnew-2.1.txt index c1f4df8f78..d14b7ada93 100644 --- a/whatsnew-2.1.txt +++ b/whatsnew-2.1.txt @@ -358,6 +358,14 @@ And also libevent supports openssl 1.1. + Kernel socket receive timestamp support: + - evbuffer_read_with_timestamp(): Reads from a socket while capturing kernel + receive timestamps via recvmsg() ancillary data (SO_TIMESTAMPNS / SO_TIMESTAMP). + - evbuffer_get_timestamp(): Retrieves the receive timestamp of the oldest data + chain currently stored in the buffer. + - BEV_OPT_RECV_TIMESTAMPS: Option flag for socket and OpenSSL bufferevents to + enable kernel receive timestamp collection on incoming data. + 1.7. New functions and features: evdns The previous evdns interface used an "open a test UDP socket" trick in