/** * 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 #include #include #ifdef _WIN32 #ifdef _MSC_VER #pragma warning(disable : 4996) /* depricated warnings */ #endif #include #include #define GetSockError() WSAGetLastError() #ifdef _MSC_VER #define snprintf _snprintf #endif #else /* !_WIN32 */ #include #include #include #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; }