2026-09-18 06:08:44 +08:00

740 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;
hints.ai_flags = AI_ADDRCONFIG;
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);
da_reserve(ctx->candidates, HAPPY_EYEBALLS_MAX_ATTEMPTS);
/* 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;
}
static void *destroy_thread(void *param)
{
struct happy_eyeballs_ctx *context = param;
os_set_thread_name("happy-eyeballs destroy thread");
#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 NULL;
}
int happy_eyeballs_destroy(struct happy_eyeballs_ctx *context)
{
if (context == NULL)
return STATUS_INVALID_ARGUMENT;
/* The destroy happens asynchronously in another thread due to the
* connect() call blocking on Windows. */
pthread_t thread;
pthread_create(&thread, NULL, destroy_thread, context);
pthread_detach(thread);
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;
}