obs-studio/deps/happy-eyeballs/happy-eyeballs.c
James Hurley 19e29500d8 deps: Add Happy Eyeballs (RFC 6555)
This commit adds a utility to connect to a TCP endpoint that may
be dual-stack IPv4/IPv6 using fast fallback. It will attempt the
prefered address family first, followed by the other 200ms later.
The first to connect will be the socket that is handed back
to the caller.
2023-07-24 16:33:31 -07:00

765 lines
21 KiB
C

/**
* Copyright (c) 2023 Twitch Interactive, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "happy-eyeballs.h"
#include "util/darray.h"
#include "util/dstr.h"
#include "util/platform.h"
#include "util/threading.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#ifdef _MSC_VER
#pragma warning(disable : 4996) /* depricated warnings */
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#define GetSockError() WSAGetLastError()
#ifdef _MSC_VER
#define snprintf _snprintf
#endif
#else /* !_WIN32 */
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define GetSockError() errno
#endif
/* ------------------------------------------------------------------------- */
/* happy eyeballs coefficients */
/* this is the same default as libcurl */
#define HAPPY_EYEBALLS_DELAY_MS 200
#define HAPPY_EYEBALLS_MAX_ATTEMPTS 6
/* Total time to wait for sockets or to finish trying; whichever is shorter */
#define HAPPY_EYEBALLS_CONNECTION_TIMEOUT_MS 25000
/* ------------------------------------------------------------------------- */
#ifndef INVALID_SOCKET
#define INVALID_SOCKET ~0
#endif
#define STATUS_SUCCESS 0
#define STATUS_FAILURE -1
#define STATUS_INVALID_ARGUMENT -EINVAL
struct happy_eyeballs_candidate {
SOCKET sockfd;
os_event_t *socket_completed_event;
pthread_t thread;
int error;
};
struct happy_eyeballs_ctx {
/**
* socket_fd will be non-zero upon successful connection to the host
* and port specified.
*/
SOCKET socket_fd;
/**
* winner_addr will be non-zero upon successful connection to the host
* and port specified.
*/
struct sockaddr_storage winner_addr;
/**
* winner_addr_len will be non-zero upon successful connection to the
* host and port specified.
*/
socklen_t winner_addr_len;
/**
* error will be non-zero in case of an error during operation.
*/
int error;
/**
* error_message may be set when there is a non-zero error code.
*/
const char *error_message;
/**
* Set along with bind_addr to hint which interface to use.
*/
socklen_t bind_addr_len;
/**
* Set along with bind_addr_len to hint which interface to use.
*/
struct sockaddr_storage bind_addr;
/**
* List of in-flight connection attempts.
*/
DARRAY(struct happy_eyeballs_candidate) candidates;
/**
* Protects against multiple simultaneous winners writing socket_fd,
* winner_addr, and winner_addr_len
*/
pthread_mutex_t winner_mutex;
/**
* Protects against mutating while iterating the candidate list
*/
pthread_mutex_t candidate_mutex;
/**
* Event that signals completion of the race, either via winner or
* error
*/
os_event_t *race_completed_event;
/**
* Addresses gathered by getaddrinfo
*/
struct addrinfo *addresses;
/**
* Domain name resolution time, in nanoseconds (0 until resolution
* success)
*/
uint64_t name_resolution_time_ns;
/**
* Connection time, in nanoseconds
*/
uint64_t connection_time_start;
uint64_t connection_time_end;
/**
* Indicates whether we are in the initial phase of dispatching
* connections, or if we are waiting for things to connect or time out.
*/
volatile bool is_starting;
};
struct happy_connect_worker_args {
SOCKET sockfd;
struct happy_eyeballs_ctx *context;
struct happy_eyeballs_candidate *candidate;
struct addrinfo *address;
};
static int check_comodo(struct happy_eyeballs_ctx *context)
{
#ifdef _WIN32
/* COMODO security software sandbox blocks all DNS by returning "host
* not found" */
HOSTENT *h = gethostbyname("localhost");
if (!h && GetLastError() == WSAHOST_NOT_FOUND) {
context->error = WSAHOST_NOT_FOUND;
context->error_message =
"happy-eyeballs: Connection test failed. "
"This error is likely caused by Comodo Internet "
"Security running OBS in sandbox mode. Please add "
"OBS to the Comodo automatic sandbox exclusion list, "
"restart OBS and try again (11001).";
return STATUS_FAILURE;
}
#else
(void)context;
#endif
return STATUS_SUCCESS;
}
static int build_addr_list(const char *hostname, int port,
struct happy_eyeballs_ctx *context)
{
struct addrinfo hints = {0};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
if (context->bind_addr_len == sizeof(struct sockaddr_in))
hints.ai_family = AF_INET;
else if (context->bind_addr_len == sizeof(struct sockaddr_in6))
hints.ai_family = AF_INET6;
struct dstr port_str;
dstr_init(&port_str);
dstr_printf(&port_str, "%d", port);
uint64_t start_time = os_gettime_ns();
int err = getaddrinfo(hostname, port_str.array, &hints,
&context->addresses);
context->name_resolution_time_ns = os_gettime_ns() - start_time;
dstr_free(&port_str);
if (err) {
context->error = GetSockError();
context->error_message = strerror(GetSockError());
return STATUS_FAILURE;
}
/* Reorder addresses interleaving address family */
struct addrinfo *prev = context->addresses;
struct addrinfo *cur = prev->ai_next;
while (cur) {
if (prev->ai_family == cur->ai_family &&
(cur->ai_family == AF_INET || cur->ai_family == AF_INET6)) {
/* If the current protocol family matches the previous
* one, look for the next instance of the other kind */
const int target_family =
prev->ai_family == AF_INET ? AF_INET6 : AF_INET;
struct addrinfo *it = cur->ai_next;
struct addrinfo *prev_it = cur;
while (it) {
if (it->ai_family == target_family)
break;
prev_it = it;
it = it->ai_next;
}
if (!it) {
/* we're at the end and haven't found the other
* kind, exit the loop early. */
break;
}
prev->ai_next = it;
prev_it->ai_next = it->ai_next;
it->ai_next = cur;
}
prev = cur;
cur = cur->ai_next;
}
return STATUS_SUCCESS;
}
static void signal_end(struct happy_eyeballs_ctx *context)
{
if (os_event_try(context->race_completed_event) != EAGAIN)
return;
context->connection_time_end = os_gettime_ns();
os_event_signal(context->race_completed_event);
}
/**
* Takes the errors that may have been generated by workers, finds the most
* common one, and sets that to be the overall context error.
*/
static int coalesce_errors(struct happy_eyeballs_ctx *context)
{
/* Don't coalesce errors while starting */
if (os_atomic_load_bool(&context->is_starting))
return STATUS_FAILURE;
/* Don't coalesce errors if we've already completed */
if (os_event_try(context->race_completed_event) != EAGAIN)
return STATUS_FAILURE;
/* We'll use the mode of the errors for now. */
struct mode {
int error;
int count;
};
DARRAY(struct mode) modes = {0};
da_init(modes);
/* Gather all the errors into counts for each error */
pthread_mutex_lock(&context->candidate_mutex);
for (size_t i = 0; i < context->candidates.num; i++) {
int err = context->candidates.array[i].error;
struct mode *mode = NULL;
/* If the error is 0, just move on. */
if (err == 0) {
continue;
}
/* Find an existing index containing this error if it exists */
for (size_t j = 0; j < modes.num && mode == NULL; j++) {
if (modes.array[j].error == err)
mode = &modes.array[j];
}
/* Existing index doesn't exist, take the next available. */
if (mode == NULL) {
mode = da_push_back_new(modes);
}
/* Note the error code and increment the count. */
mode->error = err;
mode->count++;
}
pthread_mutex_unlock(&context->candidate_mutex);
int max_count = 0;
int max_value = 0;
/* Find the error with the most occurrences. */
for (size_t i = 0; i < modes.num; i++) {
if (max_count < modes.array[i].count) {
max_value = modes.array[i].error;
max_count = modes.array[i].count;
}
}
da_free(modes);
/* Set the error */
context->error = max_value;
context->error_message = strerror(context->error);
return STATUS_SUCCESS;
}
static void *happy_connect_worker(void *arg)
{
struct happy_connect_worker_args *args =
(struct happy_connect_worker_args *)arg;
struct happy_eyeballs_ctx *context = args->context;
if (args->sockfd == INVALID_SOCKET) {
goto success;
}
if (os_event_try(args->context->race_completed_event) == 0) {
/* Already lost, don't bother */
goto success;
}
#if !defined(_WIN32) && defined(SO_NOSIGPIPE)
setsockopt(args->sockfd, SOL_SOCKET, SO_NOSIGPIPE, &(int){1},
sizeof(int));
#endif
if (context->bind_addr.ss_family != 0 &&
bind(args->sockfd, (const struct sockaddr *)&context->bind_addr,
context->bind_addr_len) < 0) {
goto failure;
}
if (connect(args->sockfd, args->address->ai_addr,
(int)args->address->ai_addrlen) == 0) {
/* success, check if we're the winner. */
pthread_mutex_lock(&context->winner_mutex);
os_event_signal(args->candidate->socket_completed_event);
if (os_event_try(context->race_completed_event) == EAGAIN) {
/* We are the winner. */
context->socket_fd = args->sockfd;
memcpy(&context->winner_addr, args->address->ai_addr,
args->address->ai_addrlen);
context->winner_addr_len =
(socklen_t)args->address->ai_addrlen;
signal_end(context);
}
pthread_mutex_unlock(&context->winner_mutex);
goto success;
}
failure:
/* failure, note down the error */
args->candidate->error = GetSockError();
pthread_mutex_lock(&context->winner_mutex);
os_event_signal(args->candidate->socket_completed_event);
/* connection candidates may still be getting dispatched, treat as if
* there's an active candidate */
bool active = os_atomic_load_bool(&context->is_starting);
/* check if we are the last worker running. If so, we'll set the error
* status and signal the completion event. */
pthread_mutex_lock(&context->candidate_mutex);
for (size_t i = 0; i < context->candidates.num && !active; i++) {
active = os_event_try(context->candidates.array[i]
.socket_completed_event) ==
EAGAIN;
}
pthread_mutex_unlock(&context->candidate_mutex);
pthread_mutex_unlock(&context->winner_mutex);
if (active || context->error != 0) {
/* we're not last or there is already an error on the context,
* let's exit. */
goto success;
}
/* Ok, we are last. Coalesce errors and signal completion. */
if (coalesce_errors(context) == STATUS_SUCCESS)
signal_end(context);
success:
free(args);
return NULL;
}
static int launch_worker(struct happy_eyeballs_ctx *context,
struct addrinfo *addr)
{
#ifdef _WIN32
SOCKET fd = WSASocket(addr->ai_family, SOCK_STREAM, IPPROTO_TCP, NULL,
0, WSA_FLAG_OVERLAPPED);
#else
SOCKET fd = socket(addr->ai_family, SOCK_STREAM, IPPROTO_TCP);
#endif
if (fd == INVALID_SOCKET) {
context->error = GetSockError();
return STATUS_FAILURE;
}
pthread_mutex_lock(&context->candidate_mutex);
struct happy_eyeballs_candidate *candidate =
da_push_back_new(context->candidates);
candidate->sockfd = fd;
struct happy_connect_worker_args *args =
(struct happy_connect_worker_args *)malloc(
sizeof(struct happy_connect_worker_args));
if (args == NULL) {
context->error = ENOMEM;
context->error_message = "happy-eyeballs: Failed to allocate "
"memory for worker context";
pthread_mutex_unlock(&context->candidate_mutex);
return STATUS_FAILURE;
}
int result = os_event_init(&candidate->socket_completed_event,
OS_EVENT_TYPE_MANUAL);
if (result != 0) {
/* failure to create the socket completed event */
context->error = result;
context->error_message = "happy-eyeballs: Failed to initialize "
"socket completed event";
free(args);
pthread_mutex_unlock(&context->candidate_mutex);
return STATUS_FAILURE;
}
args->sockfd = fd;
args->address = addr;
args->context = context;
args->candidate = candidate;
pthread_mutex_unlock(&context->candidate_mutex);
/* Launch worker thread; `args` ownership is passed to this thread */
result = pthread_create(&candidate->thread, NULL, happy_connect_worker,
args);
if (result != 0) {
/* failure to start the worker thread */
context->error = result;
context->error_message = strerror(result);
os_event_destroy(candidate->socket_completed_event);
free(args);
pthread_mutex_lock(&context->candidate_mutex);
da_erase_item(context->candidates, candidate);
pthread_mutex_unlock(&context->candidate_mutex);
return STATUS_FAILURE;
}
return STATUS_SUCCESS;
}
/* ------------------------------------------------------------------------- */
/* Public functions */
int happy_eyeballs_create(struct happy_eyeballs_ctx **context)
{
if (context == NULL)
return STATUS_INVALID_ARGUMENT;
struct happy_eyeballs_ctx *ctx = (struct happy_eyeballs_ctx *)malloc(
sizeof(struct happy_eyeballs_ctx));
if (ctx == NULL)
return -ENOMEM;
memset(ctx, 0, sizeof(struct happy_eyeballs_ctx));
ctx->socket_fd = INVALID_SOCKET;
da_init(ctx->candidates);
/* race_completed_event will be signalled when there is a winner or all
* attempts have failed */
int result =
os_event_init(&ctx->race_completed_event, OS_EVENT_TYPE_MANUAL);
/* this mutex is used to avoid the situation where we may have two
* simultaneous winners and inconsistent values set to the context. */
if (result == 0)
result = pthread_mutex_init(&ctx->winner_mutex, NULL);
bool have_winner_mutex = result == 0;
/* this mutex is used to avoid the situation where we may be mutating
* the candidate array while iterating it. */
if (result == 0)
result = pthread_mutex_init(&ctx->candidate_mutex, NULL);
bool have_candidate_mutex = result == 0;
if (result == 0) {
*context = ctx;
return STATUS_SUCCESS;
}
/* Failure, cleanup */
if (ctx->race_completed_event)
os_event_destroy((*context)->race_completed_event);
if (have_winner_mutex)
pthread_mutex_destroy(&(*context)->winner_mutex);
if (have_candidate_mutex)
pthread_mutex_destroy(&(*context)->candidate_mutex);
free(ctx);
*context = NULL;
/* Error codes from pthread_mutex_init are positive, os_event_init is
* negative. We have promised to return negative error codes, so we are
* making them all negative here. */
return -abs(result);
}
int happy_eyeballs_connect(struct happy_eyeballs_ctx *context,
const char *hostname, int port)
{
if (hostname == NULL || context == NULL || port == 0)
return STATUS_INVALID_ARGUMENT;
int result = check_comodo(context);
if (result == STATUS_SUCCESS)
result = build_addr_list(hostname, port, context);
if (result != STATUS_SUCCESS)
return result;
context->connection_time_start = os_gettime_ns();
int prev_family = 0;
struct addrinfo *next = context->addresses;
os_atomic_store_bool(&context->is_starting, true);
/* Exit the loop under the following conditions:
* 1. We have reached the maximum allowed number of attempts
* 2. There are no more candidates in the linked list (ie. `next` is
* null)
* 3. We have seen two candidates of the same family in a row, stop
* happy eyeballs and let the previous attempt go it alone. */
for (int i = 0; i < HAPPY_EYEBALLS_MAX_ATTEMPTS && next &&
next->ai_family != prev_family;
i++) {
/* Launch a worker thread for this address */
int result = launch_worker(context, next);
if (result != STATUS_SUCCESS)
return result;
/* Wait until the delay between attempts times out or we get
* signalled... */
result = os_event_timedwait(context->race_completed_event,
HAPPY_EYEBALLS_DELAY_MS);
if (result == 0) {
/* signalled. Break out of the loop. */
break;
} else if (result == EINVAL) {
/* we ran into an error. */
context->error = result;
context->error_message = "happy-eyeballs: Encountered "
"error waiting for "
"race_completed_event";
return STATUS_FAILURE;
}
/* timer timed out, move to the next candidate... */
prev_family = next->ai_family;
next = next->ai_next;
}
os_atomic_store_bool(&context->is_starting, false);
if (happy_eyeballs_try(context) == EAGAIN) {
int active_count = 0;
for (size_t i = 0; i < context->candidates.num; i++)
active_count +=
(os_event_try(
context->candidates.array[i]
.socket_completed_event) ==
EAGAIN);
if (active_count == 0 &&
coalesce_errors(context) == STATUS_SUCCESS)
signal_end(context);
}
return happy_eyeballs_try(context);
}
int happy_eyeballs_try(struct happy_eyeballs_ctx *context)
{
int status = os_event_try(context->race_completed_event);
if (context->error != 0)
return STATUS_FAILURE;
if (status != 0 && status != EAGAIN) {
context->error = status;
context->error_message = strerror(status);
return STATUS_FAILURE;
}
return status;
}
int happy_eyeballs_timedwait_default(struct happy_eyeballs_ctx *context)
{
return happy_eyeballs_timedwait(context,
HAPPY_EYEBALLS_CONNECTION_TIMEOUT_MS);
}
int happy_eyeballs_timedwait(struct happy_eyeballs_ctx *context,
unsigned long time_in_millis)
{
if (context == NULL)
return STATUS_INVALID_ARGUMENT;
int status = os_event_timedwait(context->race_completed_event,
time_in_millis);
if (context->error != 0)
return STATUS_FAILURE;
if (status != 0 && status != ETIMEDOUT) {
context->error = status;
return STATUS_FAILURE;
}
return status;
}
int happy_eyeballs_destroy(struct happy_eyeballs_ctx *context)
{
if (context == NULL)
return STATUS_INVALID_ARGUMENT;
#ifdef _WIN32
#define SHUT_RDWR SD_BOTH
#else
#define closesocket(s) close(s)
#endif
/* We do not need to lock context->candidate_mutex here because the
* candidate array is _only_ mutated in happy_eyeballs_connect. Using
* this function at the same time as connect is a programmer error.
* Locking it here will cause a deadlock with join.
*
* Shutdown non-winning sockets (but keep the socket alive so that
* things error out in threads) */
for (size_t i = 0; i < context->candidates.num; i++) {
if (context->candidates.array[i].sockfd != INVALID_SOCKET &&
context->candidates.array[i].sockfd != context->socket_fd) {
shutdown(context->candidates.array[i].sockfd,
SHUT_RDWR);
}
}
/* Join threads */
for (size_t i = 0; i < context->candidates.num; i++) {
pthread_join(context->candidates.array[i].thread, NULL);
os_event_destroy(
context->candidates.array[i].socket_completed_event);
}
/* Close sockets */
for (size_t i = 0; i < context->candidates.num; i++) {
if (context->candidates.array[i].sockfd != INVALID_SOCKET &&
context->candidates.array[i].sockfd != context->socket_fd) {
closesocket(context->candidates.array[i].sockfd);
}
}
pthread_mutex_destroy(&context->winner_mutex);
pthread_mutex_destroy(&context->candidate_mutex);
os_event_destroy(context->race_completed_event);
if (context->addresses != NULL)
freeaddrinfo(context->addresses);
da_free(context->candidates);
free(context);
return STATUS_SUCCESS;
}
/* ------------------------------------------------------------------------- */
/* Setters & Getters */
int happy_eyeballs_set_bind_addr(struct happy_eyeballs_ctx *context,
socklen_t addr_len,
struct sockaddr_storage *addr_storage)
{
if (!context)
return STATUS_INVALID_ARGUMENT;
if (addr_storage && addr_len > 0) {
memcpy(&context->bind_addr, addr_storage,
sizeof(struct sockaddr_storage));
context->bind_addr_len = addr_len;
} else {
context->bind_addr_len = 0;
memset(&context->bind_addr, 0, sizeof(struct sockaddr_storage));
}
return STATUS_SUCCESS;
}
SOCKET happy_eyeballs_get_socket_fd(const struct happy_eyeballs_ctx *context)
{
return context ? context->socket_fd : STATUS_INVALID_ARGUMENT;
}
int happy_eyeballs_get_remote_addr(const struct happy_eyeballs_ctx *context,
struct sockaddr_storage *addr)
{
if (!context || !addr)
return STATUS_INVALID_ARGUMENT;
if (context->winner_addr_len > 0)
memcpy(addr, &context->winner_addr, context->winner_addr_len);
return context->winner_addr_len;
}
int happy_eyeballs_get_error_code(const struct happy_eyeballs_ctx *context)
{
return context ? context->error : STATUS_INVALID_ARGUMENT;
}
const char *
happy_eyeballs_get_error_message(const struct happy_eyeballs_ctx *context)
{
return context ? context->error_message : NULL;
}
uint64_t happy_eyeballs_get_name_resolution_time_ns(
const struct happy_eyeballs_ctx *context)
{
return context ? context->name_resolution_time_ns : 0;
}
uint64_t
happy_eyeballs_get_connection_time_ns(const struct happy_eyeballs_ctx *context)
{
if (!context ||
context->connection_time_start > context->connection_time_end)
return 0;
return context->connection_time_end - context->connection_time_start;
}