Files
crossdesk/thirdparty/libjuice/src/server.c
2023-07-13 16:58:20 +08:00

1144 lines
35 KiB
C

/**
* Copyright (c) 2020 Paul-Louis Ageneau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
#ifndef NO_SERVER
#include "server.h"
#include "const_time.h"
#include "hmac.h"
#include "ice.h"
#include "juice.h"
#include "log.h"
#include "random.h"
#include "stun.h"
#include "turn.h"
#include "udp.h"
#include <assert.h>
#include <inttypes.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#endif
#define ALLOCATION_LIFETIME 600000 // ms
// RFC 8656: The Permission Lifetime MUST be 300 seconds (= 5 minutes)
#define PERMISSION_LIFETIME 300000 // ms
// RFC 8656: Channel bindings last for 10 minutes unless refreshed
#define BIND_LIFETIME 600000 // ms
#define MAX_RELAYED_RECORDS_COUNT 8
#define BUFFER_SIZE 4096
static char *alloc_string_copy(const char *orig, bool *alloc_failed) {
if (!orig)
return NULL;
char *copy = malloc(strlen(orig) + 1);
if (!copy) {
if (alloc_failed)
*alloc_failed = true;
return NULL;
}
strcpy(copy, orig);
return copy;
}
static server_turn_alloc_t *find_allocation(server_turn_alloc_t allocs[], int size,
const addr_record_t *record, bool allow_deleted) {
unsigned long key = addr_record_hash(record, true) % size;
unsigned long pos = key;
while (!(allocs[pos].state == SERVER_TURN_ALLOC_EMPTY ||
(allow_deleted && allocs[pos].state == SERVER_TURN_ALLOC_DELETED) ||
addr_record_is_equal(&allocs[pos].record, record, true))) {
pos = (pos + 1) % size;
if (pos == key) {
JLOG_VERBOSE("TURN allocation map is full");
return NULL;
}
}
return allocs + pos;
}
static void delete_allocation(server_turn_alloc_t *alloc) {
if (alloc->state != SERVER_TURN_ALLOC_FULL)
return;
++alloc->credentials->allocations_quota;
alloc->state = SERVER_TURN_ALLOC_DELETED;
turn_destroy_map(&alloc->map);
closesocket(alloc->sock);
alloc->sock = INVALID_SOCKET;
alloc->credentials = NULL;
}
static thread_return_t THREAD_CALL server_thread_entry(void *arg) {
server_run((juice_server_t *)arg);
return (thread_return_t)0;
}
juice_server_t *server_create(const juice_server_config_t *config) {
JLOG_VERBOSE("Creating server");
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData)) {
JLOG_FATAL("WSAStartup failed");
return NULL;
}
#endif
juice_server_t *server = calloc(1, sizeof(juice_server_t));
if (!server) {
JLOG_FATAL("Memory allocation for server data failed");
return NULL;
}
udp_socket_config_t socket_config;
memset(&socket_config, 0, sizeof(socket_config));
socket_config.bind_address = config->bind_address;
socket_config.port_begin = config->port;
socket_config.port_end = config->port;
server->sock = udp_create_socket(&socket_config);
if (server->sock == INVALID_SOCKET) {
JLOG_FATAL("Server socket opening failed");
free(server);
return NULL;
}
mutex_init(&server->mutex, MUTEX_RECURSIVE);
bool alloc_failed = false;
server->config.max_allocations =
config->max_allocations > 0 ? config->max_allocations : SERVER_DEFAULT_MAX_ALLOCATIONS;
server->config.max_peers = config->max_peers;
server->config.bind_address = alloc_string_copy(config->bind_address, &alloc_failed);
server->config.external_address = alloc_string_copy(config->external_address, &alloc_failed);
server->config.port = config->port;
server->config.relay_port_range_begin = config->relay_port_range_begin;
server->config.relay_port_range_end = config->relay_port_range_end;
server->config.realm = alloc_string_copy(
config->realm && *config->realm != '\0' ? config->realm : SERVER_DEFAULT_REALM,
&alloc_failed);
if (alloc_failed) {
JLOG_FATAL("Memory allocation for server configuration failed");
goto error;
}
// Don't copy credentials but process them
server->config.credentials = NULL;
server->config.credentials_count = 0;
if (config->credentials_count <= 0) {
// TURN disabled
JLOG_INFO("TURN relaying disabled, STUN-only mode");
server->allocs = NULL;
server->allocs_count = 0;
} else {
// TURN enabled
server->allocs = calloc(server->config.max_allocations, sizeof(server_turn_alloc_t));
if (!server->allocs) {
JLOG_FATAL("Memory allocation for TURN allocation table failed");
goto error;
}
server->allocs_count = (int)server->config.max_allocations;
for (int i = 0; i < config->credentials_count; ++i) {
juice_server_credentials_t *credentials = config->credentials + i;
if (server->config.max_allocations < credentials->allocations_quota)
server->config.max_allocations = credentials->allocations_quota;
if (!server_do_add_credentials(server, credentials, 0)) { // never expires
JLOG_FATAL("Failed to add TURN credentials");
goto error;
}
}
juice_credentials_list_t *node = server->credentials;
while (node) {
juice_server_credentials_t *credentials = &node->credentials;
if (credentials->allocations_quota == 0) // unlimited
credentials->allocations_quota = server->config.max_allocations;
node = node->next;
}
}
server->config.port = udp_get_port(server->sock);
server->nonce_key_timestamp = 0;
if (server->config.max_peers == 0)
server->config.max_peers = SERVER_DEFAULT_MAX_PEERS;
if (server->config.bind_address)
JLOG_INFO("Created server on %s:%hu", server->config.bind_address, server->config.port);
else
JLOG_INFO("Created server on port %hu", server->config.port);
int ret = thread_init(&server->thread, server_thread_entry, server);
if (ret) {
JLOG_FATAL("Thread creation failed, error=%d", ret);
goto error;
}
return server;
error:
server_do_destroy(server);
return NULL;
}
void server_do_destroy(juice_server_t *server) {
JLOG_DEBUG("Destroying server");
closesocket(server->sock);
mutex_destroy(&server->mutex);
server_turn_alloc_t *end = server->allocs + server->allocs_count;
for (server_turn_alloc_t *alloc = server->allocs; alloc < end; ++alloc) {
delete_allocation(alloc);
}
free((void *)server->allocs);
juice_credentials_list_t *node = server->credentials;
while (node) {
juice_credentials_list_t *prev = node;
node = node->next;
free((void *)prev->credentials.username);
free((void *)prev->credentials.password);
free(prev);
}
free((void *)server->config.bind_address);
free((void *)server->config.external_address);
free((void *)server->config.realm);
free(server);
#ifdef _WIN32
WSACleanup();
#endif
JLOG_VERBOSE("Destroyed server");
}
void server_destroy(juice_server_t *server) {
mutex_lock(&server->mutex);
JLOG_VERBOSE("Waiting for server thread");
server->thread_stopped = true;
mutex_unlock(&server->mutex);
server_interrupt(server);
thread_join(server->thread, NULL);
server_do_destroy(server);
}
uint16_t server_get_port(juice_server_t *server) {
mutex_lock(&server->mutex);
uint16_t port = server->config.port; // updated at creation
mutex_unlock(&server->mutex);
return port;
}
int server_add_credentials(juice_server_t *server, const juice_server_credentials_t *credentials,
timediff_t lifetime) {
mutex_lock(&server->mutex);
if (server->config.max_allocations < credentials->allocations_quota)
server->config.max_allocations = credentials->allocations_quota;
if (server->allocs_count < (int)server->config.max_allocations) {
if (server->allocs_count == 0)
JLOG_INFO("Enabling TURN relaying");
server_turn_alloc_t *reallocated =
realloc(server->allocs, server->config.max_allocations * sizeof(server_turn_alloc_t));
if (!reallocated) {
JLOG_ERROR("Memory allocation for TURN allocation table failed");
mutex_unlock(&server->mutex);
return -1;
}
memset(reallocated + server->allocs_count, 0,
((int)server->config.max_allocations - server->allocs_count) *
sizeof(server_turn_alloc_t));
server->allocs_count = (int)server->config.max_allocations;
server->allocs = reallocated;
}
juice_credentials_list_t *node = server_do_add_credentials(server, credentials, lifetime);
if (!node) {
mutex_unlock(&server->mutex);
return -1;
}
if (node->credentials.allocations_quota == 0) // unlimited
node->credentials.allocations_quota = server->config.max_allocations;
mutex_unlock(&server->mutex);
return 0;
}
juice_credentials_list_t *server_do_add_credentials(juice_server_t *server,
const juice_server_credentials_t *credentials,
timediff_t lifetime) {
juice_credentials_list_t *node = calloc(1, sizeof(juice_credentials_list_t));
if (!node) {
JLOG_ERROR("Memory allocation for TURN credentials failed");
goto error;
}
bool alloc_failed = false;
node->credentials.username =
alloc_string_copy(credentials->username ? credentials->username : "", &alloc_failed);
node->credentials.password =
alloc_string_copy(credentials->password ? credentials->password : "", &alloc_failed);
node->credentials.allocations_quota = credentials->allocations_quota;
if (alloc_failed) {
JLOG_ERROR("Memory allocation for TURN credentials failed");
goto error;
}
stun_compute_userhash(node->credentials.username, server->config.realm, node->userhash);
if (lifetime > 0)
node->timestamp = current_timestamp() + lifetime;
else
node->timestamp = 0; // never expires
node->next = server->credentials;
server->credentials = node;
return server->credentials;
error:
if (node) {
free((void *)node->credentials.username);
free((void *)node->credentials.password);
free(node);
}
return NULL;
}
void server_run(juice_server_t *server) {
mutex_lock(&server->mutex);
nfds_t nfd = 0;
struct pollfd *pfd = NULL;
// Main loop
timestamp_t next_timestamp;
while (server_bookkeeping(server, &next_timestamp) == 0) {
timediff_t timediff = next_timestamp - current_timestamp();
if (timediff < 0)
timediff = 0;
if (!pfd || nfd != (nfds_t)(1 + server->allocs_count)) {
free(pfd);
nfd = (nfds_t)(1 + server->allocs_count);
pfd = calloc(nfd, sizeof(struct pollfd));
if (!pfd) {
JLOG_FATAL("Memory allocation for poll descriptors failed");
break;
}
}
pfd[0].fd = server->sock;
pfd[0].events = POLLIN;
for (int i = 0; i < server->allocs_count; ++i) {
server_turn_alloc_t *alloc = server->allocs + i;
if (alloc->state == SERVER_TURN_ALLOC_FULL) {
pfd[1 + i].fd = alloc->sock;
pfd[1 + i].events = POLLIN;
} else {
pfd[1 + i].fd = -1; // ignore
}
}
JLOG_VERBOSE("Entering poll for %d ms", (int)timediff);
mutex_unlock(&server->mutex);
int ret = poll(pfd, nfd, (int)timediff);
mutex_lock(&server->mutex);
JLOG_VERBOSE("Leaving poll");
if (ret < 0) {
if (sockerrno == SEINTR || sockerrno == SEAGAIN) {
JLOG_VERBOSE("poll interrupted");
continue;
} else {
JLOG_FATAL("poll failed, errno=%d", sockerrno);
break;
}
}
if (server->thread_stopped) {
JLOG_VERBOSE("Server destruction requested");
break;
}
if (pfd[0].revents & POLLNVAL || pfd[0].revents & POLLERR) {
JLOG_FATAL("Error when polling server socket");
break;
}
if (pfd[0].revents & POLLIN) {
if (server_recv(server) < 0)
break;
}
for (int i = 0; i < server->allocs_count; ++i) {
server_turn_alloc_t *alloc = server->allocs + i;
if (alloc->state == SERVER_TURN_ALLOC_FULL && pfd[1 + i].revents & POLLIN)
server_forward(server, alloc);
}
}
JLOG_DEBUG("Leaving server thread");
free(pfd);
mutex_unlock(&server->mutex);
}
int server_send(juice_server_t *server, const addr_record_t *dst, const char *data, size_t size) {
JLOG_VERBOSE("Sending datagram, size=%d", size);
int ret = udp_sendto(server->sock, data, size, dst);
if (ret < 0 && sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK)
JLOG_WARN("Send failed, errno=%d", sockerrno);
return ret;
}
int server_stun_send(juice_server_t *server, const addr_record_t *dst, const stun_message_t *msg,
const char *password) {
char buffer[BUFFER_SIZE];
int size = stun_write(buffer, BUFFER_SIZE, msg, password);
if (size <= 0) {
JLOG_ERROR("STUN message write failed");
return -1;
}
if (server_send(server, dst, buffer, size) < 0) {
JLOG_WARN("STUN message send failed, errno=%d", sockerrno);
return -1;
}
return 0;
}
int server_recv(juice_server_t *server) {
JLOG_VERBOSE("Receiving datagrams");
while (true) {
char buffer[BUFFER_SIZE];
addr_record_t record;
int len = udp_recvfrom(server->sock, buffer, BUFFER_SIZE, &record);
if (len < 0) {
if (sockerrno == SEAGAIN || sockerrno == SEWOULDBLOCK) {
JLOG_VERBOSE("No more datagrams to receive");
break;
}
JLOG_ERROR("recvfrom failed, errno=%d", sockerrno);
return -1;
}
if (len == 0) {
// Empty datagram (used to interrupt)
continue;
}
addr_unmap_inet6_v4mapped((struct sockaddr *)&record.addr, &record.len);
server_input(server, buffer, len, &record);
}
return 0;
}
int server_forward(juice_server_t *server, server_turn_alloc_t *alloc) {
JLOG_VERBOSE("Forwarding datagrams");
while (true) {
char buffer[BUFFER_SIZE];
addr_record_t record;
int len = udp_recvfrom(alloc->sock, buffer, BUFFER_SIZE, &record);
if (len < 0) {
if (sockerrno == SEAGAIN || sockerrno == SEWOULDBLOCK) {
break;
}
JLOG_WARN("recvfrom failed, errno=%d", sockerrno);
return -1;
}
addr_unmap_inet6_v4mapped((struct sockaddr *)&record.addr, &record.len);
uint16_t channel;
if (turn_get_bound_channel(&alloc->map, &record, &channel)) {
// Use ChannelData
len = turn_wrap_channel_data(buffer, BUFFER_SIZE, buffer, len, channel);
if (len <= 0) {
JLOG_ERROR("TURN ChannelData wrapping failed");
return -1;
}
JLOG_VERBOSE("Forwarding as ChannelData, size=%d", len);
int ret = udp_sendto(server->sock, buffer, len, &alloc->record);
if (ret < 0 && sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK)
JLOG_WARN("Send failed, errno=%d", sockerrno);
return ret;
} else {
// Use TURN Data indication
JLOG_VERBOSE("Forwarding as TURN Data indication");
stun_message_t msg;
memset(&msg, 0, sizeof(msg));
msg.msg_class = STUN_CLASS_INDICATION;
msg.msg_method = STUN_METHOD_DATA;
msg.peer = record;
msg.data = buffer;
msg.data_size = len;
juice_random(msg.transaction_id, STUN_TRANSACTION_ID_SIZE);
return server_stun_send(server, &alloc->record, &msg, NULL);
}
}
return 0;
}
int server_input(juice_server_t *server, char *buf, size_t len, const addr_record_t *src) {
JLOG_VERBOSE("Received datagram, size=%d", len);
if (is_stun_datagram(buf, len)) {
if (JLOG_DEBUG_ENABLED) {
char src_str[ADDR_MAX_STRING_LEN];
addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN);
JLOG_DEBUG("Received STUN datagram from %s", src_str);
}
stun_message_t msg;
if (stun_read(buf, len, &msg) < 0) {
JLOG_ERROR("STUN message reading failed");
return -1;
}
return server_dispatch_stun(server, buf, len, &msg, src);
}
if (is_channel_data(buf, len)) {
if (JLOG_DEBUG_ENABLED) {
char src_str[ADDR_MAX_STRING_LEN];
addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN);
JLOG_DEBUG("Received ChannelData datagram from %s", src_str);
}
return server_process_channel_data(server, buf, len, src);
}
if (JLOG_WARN_ENABLED) {
char src_str[ADDR_MAX_STRING_LEN];
addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN);
JLOG_WARN("Received unexpected non-STUN datagram from %s, ignoring", src_str);
}
return -1;
}
int server_interrupt(juice_server_t *server) {
JLOG_VERBOSE("Interrupting server thread");
mutex_lock(&server->mutex);
if (server->sock == INVALID_SOCKET) {
mutex_unlock(&server->mutex);
return -1;
}
if (udp_sendto_self(server->sock, NULL, 0) < 0) {
if (sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK) {
JLOG_WARN("Failed to interrupt thread by triggering socket, errno=%d", sockerrno);
mutex_unlock(&server->mutex);
return -1;
}
}
mutex_unlock(&server->mutex);
return 0;
}
int server_bookkeeping(juice_server_t *server, timestamp_t *next_timestamp) {
timestamp_t now = current_timestamp();
*next_timestamp = now + 60000;
// Handle allocations
for (int i = 0; i < server->allocs_count; ++i) {
server_turn_alloc_t *alloc = server->allocs + i;
if (alloc->state != SERVER_TURN_ALLOC_FULL)
continue;
if (alloc->timestamp <= now) {
JLOG_DEBUG("Allocation timed out");
delete_allocation(alloc);
continue;
}
if (alloc->timestamp < *next_timestamp)
*next_timestamp = alloc->timestamp;
}
// Handle credentials
juice_credentials_list_t **pnode = &server->credentials; // We are deleting some elements
while (*pnode) {
if ((*pnode)->timestamp && (*pnode)->timestamp <= now) {
JLOG_DEBUG("Credentials timed out");
juice_credentials_list_t *next = (*pnode)->next;
free((void *)(*pnode)->credentials.username);
free((void *)(*pnode)->credentials.password);
free((*pnode));
*pnode = next;
continue;
}
pnode = &(*pnode)->next;
}
return 0;
}
void server_get_nonce(juice_server_t *server, const addr_record_t *src, char *nonce) {
timestamp_t now = current_timestamp();
if (now >= server->nonce_key_timestamp) {
juice_random(server->nonce_key, SERVER_NONCE_KEY_SIZE);
server->nonce_key_timestamp = now + SERVER_NONCE_KEY_LIFETIME;
}
uint8_t digest[HMAC_SHA256_SIZE];
hmac_sha256(&src->addr, src->len, server->nonce_key, SERVER_NONCE_KEY_SIZE, digest);
size_t len = HMAC_SHA256_SIZE;
if (len > STUN_MAX_NONCE_LEN)
len = STUN_MAX_NONCE_LEN;
// RFC 4648 base64url character table
const char *table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
for (size_t i = 0; i < len; ++i)
nonce[i] = table[digest[i] % 64];
nonce[len] = '\0';
stun_prepend_nonce_cookie(nonce);
}
void server_prepare_credentials(juice_server_t *server, const addr_record_t *src,
const juice_server_credentials_t *credentials,
stun_message_t *msg) {
snprintf(msg->credentials.realm, STUN_MAX_REALM_LEN, "%s", server->config.realm);
server_get_nonce(server, src, msg->credentials.nonce);
if (credentials)
snprintf(msg->credentials.username, STUN_MAX_USERNAME_LEN, "%s", credentials->username);
}
int server_dispatch_stun(juice_server_t *server, void *buf, size_t size, stun_message_t *msg,
const addr_record_t *src) {
if (!(msg->msg_class == STUN_CLASS_REQUEST ||
(msg->msg_class == STUN_CLASS_INDICATION &&
(msg->msg_method == STUN_METHOD_BINDING || msg->msg_method == STUN_METHOD_SEND)))) {
JLOG_WARN("Unexpected STUN message, class=0x%X, method=0x%X", msg->msg_class,
msg->msg_method);
return -1;
}
if (server->allocs_count == 0 && msg->msg_method != STUN_METHOD_BINDING) {
// TURN support is disabled
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
400, // Bad request
NULL);
}
if (msg->error_code == STUN_ERROR_INTERNAL_VALIDATION_FAILED) {
if (msg->msg_class == STUN_CLASS_REQUEST) {
JLOG_WARN("Invalid STUN message, answering bad request error response");
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
400, // Bad request
NULL);
} else {
JLOG_WARN("Invalid STUN message, dropping");
return -1;
}
}
juice_server_credentials_t *credentials = NULL;
if (msg->msg_method != STUN_METHOD_BINDING && msg->msg_class != STUN_CLASS_INDICATION) {
if (!msg->has_integrity || //
*msg->credentials.realm == '\0' || *msg->credentials.nonce == '\0' ||
(*msg->credentials.username == '\0' && !msg->credentials.enable_userhash)) {
JLOG_DEBUG("Answering STUN unauthorized error response");
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
401, // Unauthorized
NULL); // No username
}
char nonce[STUN_MAX_NONCE_LEN];
server_get_nonce(server, src, nonce);
if (strcmp(msg->credentials.nonce, nonce) != 0 ||
strcmp(msg->credentials.realm, server->config.realm) != 0) {
JLOG_DEBUG("Answering STUN stale nonce error response");
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
438, // Stale nonce
NULL); // No username
}
timestamp_t now = current_timestamp();
if (msg->credentials.enable_userhash) {
juice_credentials_list_t *node = server->credentials;
while (node) {
if ((!node->timestamp || node->timestamp > now) &&
const_time_memcmp(node->userhash, msg->credentials.userhash, USERHASH_SIZE) ==
0) {
credentials = &node->credentials;
}
node = node->next;
}
if (credentials)
snprintf(msg->credentials.username, STUN_MAX_USERNAME_LEN, "%s",
credentials->username);
else
JLOG_WARN("No credentials for userhash");
} else {
juice_credentials_list_t *node = server->credentials;
while (node) {
if ((!node->timestamp || node->timestamp > now) &&
const_time_strcmp(node->credentials.username, msg->credentials.username) == 0) {
credentials = &node->credentials;
}
node = node->next;
}
if (!credentials)
JLOG_WARN("No credentials for username \"%s\"", msg->credentials.username);
}
if (!credentials) {
server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
401, // Unauthorized
NULL); // No username
return -1;
}
// Check credentials
if (!stun_check_integrity(buf, size, msg, credentials->password)) {
JLOG_WARN("STUN authentication failed for username \"%s\"", msg->credentials.username);
server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
401, // Unauthorized
NULL); // No username
return -1;
}
}
switch (msg->msg_method) {
case STUN_METHOD_BINDING:
return server_process_stun_binding(server, msg, src);
case STUN_METHOD_ALLOCATE:
case STUN_METHOD_REFRESH:
return server_process_turn_allocate(server, msg, src, credentials);
case STUN_METHOD_CREATE_PERMISSION:
return server_process_turn_create_permission(server, msg, src, credentials);
case STUN_METHOD_CHANNEL_BIND:
return server_process_turn_channel_bind(server, msg, src, credentials);
case STUN_METHOD_SEND:
return server_process_turn_send(server, msg, src);
default:
JLOG_WARN("Unknown STUN method 0x%X, ignoring", msg->msg_method);
return -1;
}
}
int server_answer_stun_binding(juice_server_t *server, const uint8_t *transaction_id,
const addr_record_t *src) {
JLOG_DEBUG("Answering STUN Binding request");
stun_message_t ans;
memset(&ans, 0, sizeof(ans));
ans.msg_class = STUN_CLASS_RESP_SUCCESS;
ans.msg_method = STUN_METHOD_BINDING;
ans.mapped = *src;
memcpy(ans.transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE);
char buffer[BUFFER_SIZE];
int size = stun_write(buffer, BUFFER_SIZE, &ans, NULL);
if (size <= 0) {
JLOG_ERROR("STUN message write failed");
return -1;
}
if (server_send(server, src, buffer, size) < 0) {
JLOG_WARN("STUN message send failed, errno=%d", sockerrno);
return -1;
}
return 0;
}
int server_answer_stun_error(juice_server_t *server, const uint8_t *transaction_id,
const addr_record_t *src, stun_method_t method, unsigned int code,
const juice_server_credentials_t *credentials) {
JLOG_DEBUG("Answering STUN error response with code %u", code);
stun_message_t ans;
memset(&ans, 0, sizeof(ans));
ans.msg_class = STUN_CLASS_RESP_ERROR;
ans.msg_method = method;
ans.error_code = code;
memcpy(ans.transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE);
if (method != STUN_METHOD_BINDING)
server_prepare_credentials(server, src, credentials, &ans);
return server_stun_send(server, src, &ans, credentials ? credentials->password : NULL);
}
int server_process_stun_binding(juice_server_t *server, const stun_message_t *msg,
const addr_record_t *src) {
if (JLOG_INFO_ENABLED) {
char src_str[ADDR_MAX_STRING_LEN];
addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN);
JLOG_INFO("Got STUN binding from client %s", src_str);
}
return server_answer_stun_binding(server, msg->transaction_id, src);
}
int server_process_turn_allocate(juice_server_t *server, const stun_message_t *msg,
const addr_record_t *src,
juice_server_credentials_t *credentials) {
if (msg->msg_class != STUN_CLASS_REQUEST)
return -1;
if (msg->msg_method != STUN_METHOD_ALLOCATE && msg->msg_method != STUN_METHOD_REFRESH)
return -1;
JLOG_DEBUG("Processing TURN Allocate request");
server_turn_alloc_t *alloc = find_allocation(server->allocs, server->allocs_count, src, true);
if (!alloc) {
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
486, // Allocation quota reached
credentials);
}
if (alloc->state == SERVER_TURN_ALLOC_FULL) {
// Allocation exists
if (msg->msg_method == STUN_METHOD_ALLOCATE &&
memcmp(alloc->transaction_id, msg->transaction_id, STUN_TRANSACTION_ID_SIZE) != 0) {
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
437, // Allocation mismatch
credentials);
}
if (alloc->credentials != credentials) {
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
441, // Wrong credentials
credentials);
}
} else {
// Allocation does not exist
if (msg->msg_method == STUN_METHOD_REFRESH) {
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
437, // Allocation mismatch
credentials);
}
if (credentials->allocations_quota <= 0) {
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
486, // Allocation quota reached
credentials);
}
udp_socket_config_t socket_config;
memset(&socket_config, 0, sizeof(socket_config));
socket_config.bind_address = server->config.bind_address;
socket_config.port_begin = server->config.relay_port_range_begin;
socket_config.port_end = server->config.relay_port_range_end;
alloc->sock = udp_create_socket(&socket_config);
if (alloc->sock == INVALID_SOCKET) {
server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, 500,
credentials);
return -1;
}
if (turn_init_map(&alloc->map, server->config.max_peers) < 0) {
closesocket(alloc->sock);
alloc->sock = INVALID_SOCKET;
server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, 500,
credentials);
return -1;
}
alloc->state = SERVER_TURN_ALLOC_FULL;
alloc->record = *src;
alloc->credentials = credentials;
--credentials->allocations_quota;
}
uint32_t lifetime = ALLOCATION_LIFETIME / 1000;
if (msg->lifetime_set && msg->lifetime < lifetime)
lifetime = msg->lifetime;
alloc->timestamp = current_timestamp() + lifetime * 1000;
memcpy(alloc->transaction_id, msg->transaction_id, STUN_TRANSACTION_ID_SIZE);
addr_record_t records[MAX_RELAYED_RECORDS_COUNT];
const addr_record_t *relayed = NULL;
if (lifetime == 0) {
delete_allocation(alloc);
} else {
int count = 0;
if (server->config.external_address) {
char service[8];
snprintf(service, 8, "%hu", udp_get_port(alloc->sock));
count = addr_resolve(server->config.external_address, service, records,
MAX_RELAYED_RECORDS_COUNT);
if (count <= 0) {
JLOG_ERROR("Specified external address is invalid");
goto error;
}
} else {
count = udp_get_addrs(alloc->sock, records, MAX_RELAYED_RECORDS_COUNT);
if (count <= 0) {
JLOG_ERROR("No local address found");
goto error;
}
}
if (count > MAX_RELAYED_RECORDS_COUNT)
count = MAX_RELAYED_RECORDS_COUNT;
for (int i = 0; i < count; ++i) {
const addr_record_t *record = records + i;
if (record->addr.ss_family == AF_INET || !relayed) {
relayed = record;
if (record->addr.ss_family == AF_INET)
break;
}
}
if (!relayed) {
JLOG_ERROR("No advertisable relayed address found");
goto error;
}
if (JLOG_INFO_ENABLED) {
char src_str[ADDR_MAX_STRING_LEN];
addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN);
char relayed_str[ADDR_MAX_STRING_LEN];
addr_record_to_string(relayed, relayed_str, ADDR_MAX_STRING_LEN);
JLOG_INFO("Allocated TURN relayed address %s for client %s", relayed_str, src_str);
}
}
stun_message_t ans;
memset(&ans, 0, sizeof(ans));
ans.msg_class = STUN_CLASS_RESP_SUCCESS;
ans.msg_method = msg->msg_method;
ans.lifetime = lifetime;
ans.lifetime_set = true;
ans.mapped = *src;
if (relayed)
ans.relayed = *relayed;
memcpy(ans.transaction_id, msg->transaction_id, STUN_TRANSACTION_ID_SIZE);
server_prepare_credentials(server, src, credentials, &ans);
return server_stun_send(server, src, &ans, credentials->password);
error:
delete_allocation(alloc);
server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, 500, credentials);
return -1;
}
int server_process_turn_create_permission(juice_server_t *server, const stun_message_t *msg,
const addr_record_t *src,
const juice_server_credentials_t *credentials) {
if (msg->msg_class != STUN_CLASS_REQUEST)
return -1;
JLOG_DEBUG("Processing STUN CreatePermission request");
if (!msg->peer.len) {
JLOG_WARN("Missing peer address in TURN CreatePermission request");
return -1;
}
server_turn_alloc_t *alloc = find_allocation(server->allocs, server->allocs_count, src, false);
if (!alloc || alloc->state != SERVER_TURN_ALLOC_FULL) {
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
437, // Allocation mismatch
credentials);
}
if (alloc->credentials != credentials) {
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
441, // Wrong credentials
credentials);
}
if (!turn_set_permission(&alloc->map, msg->transaction_id, &msg->peer, PERMISSION_LIFETIME)) {
server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, 500,
credentials);
return -1;
}
stun_message_t ans;
memset(&ans, 0, sizeof(ans));
ans.msg_class = STUN_CLASS_RESP_SUCCESS;
ans.msg_method = STUN_METHOD_CREATE_PERMISSION;
memcpy(ans.transaction_id, msg->transaction_id, STUN_TRANSACTION_ID_SIZE);
server_prepare_credentials(server, src, credentials, &ans);
return server_stun_send(server, src, &ans, credentials->password);
}
int server_process_turn_channel_bind(juice_server_t *server, const stun_message_t *msg,
const addr_record_t *src,
const juice_server_credentials_t *credentials) {
if (msg->msg_class != STUN_CLASS_REQUEST)
return -1;
JLOG_DEBUG("Processing STUN ChannelBind request");
if (!msg->peer.len) {
JLOG_WARN("Missing peer address in TURN ChannelBind request");
return -1;
}
if (!msg->channel_number) {
JLOG_WARN("Missing channel number in TURN ChannelBind request");
return -1;
}
server_turn_alloc_t *alloc = find_allocation(server->allocs, server->allocs_count, src, false);
if (!alloc || alloc->state != SERVER_TURN_ALLOC_FULL) {
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
437, // Allocation mismatch
credentials);
}
if (alloc->credentials != credentials) {
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
441, // Wrong credentials
credentials);
}
uint16_t channel = msg->channel_number;
if (!is_valid_channel(channel)) {
JLOG_WARN("TURN channel 0x%hX is invalid", channel);
return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method,
400, // Bad request
credentials);
}
if (!turn_bind_channel(&alloc->map, &msg->peer, msg->transaction_id, channel, BIND_LIFETIME)) {
server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, 500,
credentials);
return -1;
}
stun_message_t ans;
memset(&ans, 0, sizeof(ans));
ans.msg_class = STUN_CLASS_RESP_SUCCESS;
ans.msg_method = STUN_METHOD_CHANNEL_BIND;
memcpy(ans.transaction_id, msg->transaction_id, STUN_TRANSACTION_ID_SIZE);
server_prepare_credentials(server, src, credentials, &ans);
return server_stun_send(server, src, &ans, credentials->password);
}
int server_process_turn_send(juice_server_t *server, const stun_message_t *msg,
const addr_record_t *src) {
if (msg->msg_class != STUN_CLASS_INDICATION)
return -1;
JLOG_DEBUG("Processing STUN Send indication");
if (!msg->data) {
JLOG_WARN("Missing data in TURN Send indication");
return -1;
}
if (!msg->peer.len) {
JLOG_WARN("Missing peer address in TURN Send indication");
return -1;
}
server_turn_alloc_t *alloc = find_allocation(server->allocs, server->allocs_count, src, false);
if (!alloc || alloc->state != SERVER_TURN_ALLOC_FULL) {
JLOG_WARN("Allocation mismatch for TURN Send indication");
return -1;
}
if (!turn_has_permission(&alloc->map, &msg->peer)) {
JLOG_WARN("No permission for peer address");
return -1;
}
JLOG_VERBOSE("Forwarding datagram to peer, size=%zu", msg->data_size);
int ret = udp_sendto(alloc->sock, msg->data, msg->data_size, &msg->peer);
if (ret < 0 && sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK)
JLOG_WARN("Forwarding failed, errno=%d", sockerrno);
return ret;
}
int server_process_channel_data(juice_server_t *server, char *buf, size_t len,
const addr_record_t *src) {
server_turn_alloc_t *alloc = find_allocation(server->allocs, server->allocs_count, src, false);
if (!alloc || alloc->state != SERVER_TURN_ALLOC_FULL) {
JLOG_WARN("Allocation mismatch for TURN Channel Data");
return -1;
}
if (len < sizeof(struct channel_data_header)) {
JLOG_WARN("ChannelData is too short");
return -1;
}
const struct channel_data_header *header = (const struct channel_data_header *)buf;
buf += sizeof(struct channel_data_header);
len -= sizeof(struct channel_data_header);
uint16_t channel = ntohs(header->channel_number);
uint16_t length = ntohs(header->length);
JLOG_VERBOSE("Received ChannelData, channel=0x%hX, length=%hu", channel, length);
if (length > len) {
JLOG_WARN("ChannelData has invalid length");
return -1;
}
len = length;
addr_record_t record;
if (!turn_find_bound_channel(&alloc->map, channel, &record)) {
JLOG_WARN("Channel 0x%hX is not bound", channel);
return -1;
}
JLOG_VERBOSE("Forwarding datagram to peer, size=%zu", len);
int ret = udp_sendto(alloc->sock, buf, len, &record);
if (ret < 0 && sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK)
JLOG_WARN("Send failed, errno=%d", sockerrno);
return 0;
}
#endif // ifndef NO_SERVER