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

1237 lines
41 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/.
*/
#include "stun.h"
#include "base64.h"
#include "const_time.h"
#include "crc32.h"
#include "juice.h"
#include "log.h"
#include "udp.h"
#include <assert.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define STUN_MAGIC 0x2112A442
#define STUN_FINGERPRINT_XOR 0x5354554E // "STUN"
#define STUN_ATTR_SIZE sizeof(struct stun_attr)
// STUN_MAX_PASSWORD_LEN > HASH_SHA256_SIZE > HASH_MD5_SIZE
#define MAX_HMAC_KEY_LEN STUN_MAX_PASSWORD_LEN
#define MAX_HMAC_INPUT_LEN (STUN_MAX_USERNAME_LEN + STUN_MAX_REALM_LEN + STUN_MAX_PASSWORD_LEN + 2)
#define MAX_USERHASH_INPUT_LEN (STUN_MAX_USERNAME_LEN + STUN_MAX_REALM_LEN + 1)
#ifndef htonll
#define htonll(x) \
((uint64_t)(((uint64_t)htonl((uint32_t)(x))) << 32) | (uint64_t)htonl((uint32_t)((x) >> 32)))
#endif
#ifndef ntohll
#define ntohll(x) htonll(x)
#endif
static size_t align32(size_t len) {
while (len & 0x03)
++len;
return len;
}
static size_t generate_hmac_key(const stun_message_t *msg, const char *password, void *key) {
if (*msg->credentials.realm != '\0') {
// long-term credentials
if (*msg->credentials.username == '\0')
JLOG_WARN("Generating HMAC key for long-term credentials with empty STUN username");
char input[MAX_HMAC_INPUT_LEN];
int input_len = snprintf(input, MAX_HMAC_INPUT_LEN, "%s:%s:%s", msg->credentials.username,
msg->credentials.realm, password ? password : "");
if (input_len < 0)
return 0;
if (input_len >= MAX_HMAC_INPUT_LEN)
input_len = MAX_HMAC_INPUT_LEN - 1;
switch (msg->credentials.password_algorithm) {
case STUN_PASSWORD_ALGORITHM_SHA256:
hash_sha256(input, input_len, key);
return HASH_SHA256_SIZE;
default:
hash_md5(input, input_len, key);
return HASH_MD5_SIZE;
}
} else {
// short-term credentials
int key_len = snprintf((char *)key, MAX_HMAC_KEY_LEN, "%s", password ? password : "");
if (key_len < 0)
return 0;
if (key_len >= MAX_HMAC_KEY_LEN)
key_len = MAX_HMAC_KEY_LEN - 1;
return key_len;
}
}
static size_t generate_password_algorithms_attr(uint8_t *attr) {
// attr size must be at least STUN_PASSWORD_ALGORITHMS_ATTR_MAX_SIZE
struct stun_value_password_algorithm *pwa = (struct stun_value_password_algorithm *)attr;
pwa->algorithm = htons(STUN_PASSWORD_ALGORITHM_SHA256);
pwa->parameters_length = 0;
++pwa;
pwa->algorithm = htons(STUN_PASSWORD_ALGORITHM_MD5);
pwa->parameters_length = 0;
++pwa;
return (uint8_t *)pwa - attr;
}
int stun_write(void *buf, size_t size, const stun_message_t *msg, const char *password) {
uint8_t *begin = buf;
uint8_t *pos = begin;
uint8_t *end = begin + size;
JLOG_VERBOSE("Writing STUN message, class=0x%X, method=0x%X", (unsigned int)msg->msg_class,
(unsigned int)msg->msg_method);
size_t len =
stun_write_header(pos, end - pos, msg->msg_class, msg->msg_method, msg->transaction_id);
if (len <= 0)
goto overflow;
pos += len;
uint8_t *attr_begin = pos;
if (msg->error_code) {
const char *reason = stun_get_error_reason(msg->error_code);
char buffer[sizeof(struct stun_value_error_code) + STUN_MAX_ERROR_REASON_LEN + 1];
struct stun_value_error_code *error = (struct stun_value_error_code *)buffer;
memset(error, 0, sizeof(*error));
error->code_class = (msg->error_code / 100) & 0x07;
error->code_number = msg->error_code % 100;
strcpy((char *)error->reason, reason);
len = stun_write_attr(pos, end - pos, STUN_ATTR_ERROR_CODE, error,
sizeof(struct stun_value_error_code) + strlen(reason));
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->mapped.len) {
JLOG_VERBOSE("Writing XOR mapped address");
uint8_t value[32];
uint8_t mask[16];
*((uint32_t *)mask) = htonl(STUN_MAGIC);
memcpy(mask + 4, msg->transaction_id, 12);
int value_len = stun_write_value_mapped_address(
value, 32, (const struct sockaddr *)&msg->mapped.addr, msg->mapped.len, mask);
if (value_len > 0) {
len = stun_write_attr(pos, end - pos, STUN_ATTR_XOR_MAPPED_ADDRESS, value, value_len);
if (len <= 0)
goto overflow;
pos += len;
}
}
if (msg->priority) {
uint32_t priority = htonl(msg->priority);
len = stun_write_attr(pos, end - pos, STUN_ATTR_PRIORITY, &priority, 4);
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->use_candidate) {
len = stun_write_attr(pos, end - pos, STUN_ATTR_USE_CANDIDATE, NULL, 0);
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->ice_controlling) {
uint64_t ice_controlling = htonll(msg->ice_controlling);
len = stun_write_attr(pos, end - pos, STUN_ATTR_ICE_CONTROLLING, &ice_controlling, 8);
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->ice_controlled) {
uint64_t ice_controlled = htonll(msg->ice_controlled);
len = stun_write_attr(pos, end - pos, STUN_ATTR_ICE_CONTROLLED, &ice_controlled, 8);
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->channel_number) {
struct stun_value_channel_number channel_number;
memset(&channel_number, 0, sizeof(channel_number));
channel_number.channel_number = htons(msg->channel_number);
len = stun_write_attr(pos, end - pos, STUN_ATTR_CHANNEL_NUMBER, &channel_number,
sizeof(channel_number));
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->lifetime_set || msg->lifetime) {
uint32_t lifetime = htonl(msg->lifetime);
len = stun_write_attr(pos, end - pos, STUN_ATTR_LIFETIME, &lifetime, 4);
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->peer.len) {
JLOG_VERBOSE("Writing XOR peer address");
uint8_t value[32];
uint8_t mask[16];
*((uint32_t *)mask) = htonl(STUN_MAGIC);
memcpy(mask + 4, msg->transaction_id, 12);
int value_len = stun_write_value_mapped_address(
value, 32, (const struct sockaddr *)&msg->peer.addr, msg->peer.len, mask);
if (value_len > 0) {
len = stun_write_attr(pos, end - pos, STUN_ATTR_XOR_PEER_ADDRESS, value, value_len);
if (len <= 0)
goto overflow;
pos += len;
}
}
if (msg->relayed.len) {
JLOG_VERBOSE("Writing XOR relay address");
uint8_t value[32];
uint8_t mask[16];
*((uint32_t *)mask) = htonl(STUN_MAGIC);
memcpy(mask + 4, msg->transaction_id, 12);
int value_len = stun_write_value_mapped_address(
value, 32, (const struct sockaddr *)&msg->relayed.addr, msg->relayed.len, mask);
if (value_len > 0) {
len = stun_write_attr(pos, end - pos, STUN_ATTR_XOR_RELAYED_ADDRESS, value, value_len);
if (len <= 0)
goto overflow;
pos += len;
}
}
if (msg->data) {
len = stun_write_attr(pos, end - pos, STUN_ATTR_DATA, (const uint8_t *)msg->data,
msg->data_size);
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->even_port) {
struct stun_value_even_port even_port;
memset(&even_port, 0, sizeof(even_port));
if (msg->next_port)
even_port.r |= 0x80;
len = stun_write_attr(pos, end - pos, STUN_ATTR_CHANNEL_NUMBER, &even_port,
sizeof(even_port));
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->requested_transport) {
struct stun_value_requested_transport requested_transport;
memset(&requested_transport, 0, sizeof(requested_transport));
requested_transport.protocol = 17;
len = stun_write_attr(pos, end - pos, STUN_ATTR_REQUESTED_TRANSPORT, &requested_transport,
sizeof(requested_transport));
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->dont_fragment) {
len = stun_write_attr(pos, end - pos, STUN_ATTR_DONT_FRAGMENT, NULL, 0);
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->reservation_token) {
uint64_t reservation_token = htonll(msg->reservation_token);
len = stun_write_attr(pos, end - pos, STUN_ATTR_RESERVATION_TOKEN, &reservation_token, 8);
if (len <= 0)
goto overflow;
pos += len;
}
const char *software = "libjuice";
len = stun_write_attr(pos, end - pos, STUN_ATTR_SOFTWARE, software, strlen(software));
if (len <= 0)
goto overflow;
pos += len;
if (msg->msg_class == STUN_CLASS_REQUEST) {
if (msg->credentials.enable_userhash) {
len = stun_write_attr(pos, end - pos, STUN_ATTR_USERHASH, msg->credentials.userhash,
USERHASH_SIZE);
if (len <= 0)
goto overflow;
pos += len;
} else if (*msg->credentials.username != '\0') {
len = stun_write_attr(pos, end - pos, STUN_ATTR_USERNAME, msg->credentials.username,
strlen(msg->credentials.username));
if (len <= 0)
goto overflow;
pos += len;
}
}
if (msg->msg_class == STUN_CLASS_REQUEST ||
(msg->msg_class == STUN_CLASS_RESP_ERROR &&
(msg->error_code == 401 || msg->error_code == 438) // Unauthenticated or Stale Nonce
)) {
if (*msg->credentials.realm != '\0') {
len = stun_write_attr(pos, end - pos, STUN_ATTR_REALM, msg->credentials.realm,
strlen(msg->credentials.realm));
if (len <= 0)
goto overflow;
pos += len;
}
if (*msg->credentials.nonce != '\0') {
len = stun_write_attr(pos, end - pos, STUN_ATTR_NONCE, msg->credentials.nonce,
strlen(msg->credentials.nonce));
if (len <= 0)
goto overflow;
pos += len;
if (msg->credentials.password_algorithm > 0) {
len = stun_write_attr(pos, end - pos, STUN_ATTR_PASSWORD_ALGORITHMS,
msg->credentials.password_algorithms_value,
msg->credentials.password_algorithms_value_size);
if (len <= 0)
goto overflow;
pos += len;
} else if (msg->msg_class != STUN_CLASS_REQUEST) {
uint8_t pwa_value[STUN_MAX_PASSWORD_ALGORITHMS_VALUE_SIZE];
size_t pwa_size = generate_password_algorithms_attr(pwa_value);
len = stun_write_attr(pos, end - pos, STUN_ATTR_PASSWORD_ALGORITHMS, pwa_value,
pwa_size);
if (len <= 0)
goto overflow;
pos += len;
}
if (msg->msg_class == STUN_CLASS_REQUEST &&
msg->credentials.password_algorithm != STUN_PASSWORD_ALGORITHM_UNSET) {
struct stun_value_password_algorithm pwa;
pwa.algorithm = htons(msg->credentials.password_algorithm);
len = stun_write_attr(pos, end - pos, STUN_ATTR_PASSWORD_ALGORITHM, &pwa,
sizeof(pwa));
if (len <= 0)
goto overflow;
pos += len;
}
}
}
if (msg->msg_class != STUN_CLASS_INDICATION && password) {
uint8_t key[MAX_HMAC_KEY_LEN];
size_t key_len = generate_hmac_key(msg, password, key);
size_t tmp_length = pos - attr_begin + STUN_ATTR_SIZE + HMAC_SHA1_SIZE;
stun_update_header_length(begin, tmp_length);
uint8_t hmac[HMAC_SHA1_SIZE];
hmac_sha1(begin, pos - begin, key, key_len, hmac);
len = stun_write_attr(pos, end - pos, STUN_ATTR_MESSAGE_INTEGRITY, hmac, HMAC_SHA1_SIZE);
if (len <= 0)
goto overflow;
pos += len;
// According to RFC 8489, the agent must include both MESSAGE-INTEGRITY and
// MESSAGE-INTEGRITY-SHA256. However, this makes legacy agents and servers fail with error
// 420 Unknown Attribute. Therefore, unless the password algorithm SHA-256 is enabled, only
// MESSAGE-INTEGRITY is included in the message for compatibility.
if (msg->credentials.password_algorithm != STUN_PASSWORD_ALGORITHM_UNSET) {
// If the response contains a PASSWORD-ALGORITHMS attribute, all the
// subsequent requests MUST be authenticated using MESSAGE-INTEGRITY-
// SHA256 only.
size_t tmp_length = pos - attr_begin + STUN_ATTR_SIZE + HMAC_SHA256_SIZE;
stun_update_header_length(begin, tmp_length);
uint8_t hmac[HMAC_SHA256_SIZE];
hmac_sha256(begin, pos - begin, key, key_len, hmac);
len = stun_write_attr(pos, end - pos, STUN_ATTR_MESSAGE_INTEGRITY_SHA256, hmac,
HMAC_SHA256_SIZE);
if (len <= 0)
goto overflow;
pos += len;
}
}
size_t length = pos - attr_begin + STUN_ATTR_SIZE + 4;
if (length & 0x03) {
JLOG_ERROR("Written STUN message length is not multiple of 4, length=%zu", length);
return -1;
}
stun_update_header_length(begin, length);
uint32_t fingerprint = htonl(CRC32(buf, pos - begin) ^ STUN_FINGERPRINT_XOR);
len = stun_write_attr(pos, end - pos, STUN_ATTR_FINGERPRINT, &fingerprint, 4);
if (len <= 0)
goto overflow;
pos += len;
return (int)(pos - begin);
overflow:
JLOG_ERROR("Not enough space in buffer for STUN message, size=%zu", size);
return -1;
}
int stun_write_header(void *buf, size_t size, stun_class_t class, stun_method_t method,
const uint8_t *transaction_id) {
if (size < sizeof(struct stun_header))
return -1;
uint16_t type = (uint16_t) class | (uint16_t)method;
struct stun_header *header = buf;
header->type = htons(type);
header->length = htons(0);
header->magic = htonl(STUN_MAGIC);
memcpy(header->transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE);
return sizeof(struct stun_header);
}
size_t stun_update_header_length(void *buf, size_t length) {
struct stun_header *header = buf;
size_t previous = ntohs(header->length);
header->length = htons((uint16_t)length);
return previous;
}
int stun_write_attr(void *buf, size_t size, uint16_t type, const void *value, size_t length) {
JLOG_VERBOSE("Writing STUN attribute type 0x%X, length=%zu", (unsigned int)type, length);
if (size < sizeof(struct stun_attr) + length)
return -1;
struct stun_attr *attr = buf;
attr->type = htons(type);
attr->length = htons((uint16_t)length);
if (length > 0) {
memcpy(attr->value, value, length);
// Pad to align on 4 bytes
while (length & 0x03)
attr->value[length++] = 0;
}
return (int)(sizeof(struct stun_attr) + length);
}
int stun_write_value_mapped_address(void *buf, size_t size, const struct sockaddr *addr,
socklen_t addrlen, const uint8_t *mask) {
if (size < sizeof(struct stun_value_mapped_address))
return -1;
struct stun_value_mapped_address *value = buf;
value->padding = 0;
switch (addr->sa_family) {
case AF_INET: {
value->family = STUN_ADDRESS_FAMILY_IPV4;
if (size < sizeof(struct stun_value_mapped_address) + 4)
return -1;
if (addrlen < (socklen_t)sizeof(struct sockaddr_in))
return -1;
JLOG_VERBOSE("Writing IPv4 address");
const struct sockaddr_in *sin = (const struct sockaddr_in *)addr;
value->port = sin->sin_port ^ *((uint16_t *)mask);
const uint8_t *bytes = (const uint8_t *)&sin->sin_addr;
for (int i = 0; i < 4; ++i)
value->address[i] = bytes[i] ^ mask[i];
return sizeof(struct stun_value_mapped_address) + 4;
}
case AF_INET6: {
value->family = STUN_ADDRESS_FAMILY_IPV6;
if (size < sizeof(struct stun_value_mapped_address) + 16)
return -1;
if (addrlen < (socklen_t)sizeof(struct sockaddr_in6))
return -1;
JLOG_VERBOSE("Writing IPv6 address");
const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)addr;
value->port = sin6->sin6_port ^ *((uint16_t *)mask);
const uint8_t *bytes = (const uint8_t *)&sin6->sin6_addr;
for (int i = 0; i < 16; ++i)
value->address[i] = bytes[i] ^ mask[i];
return sizeof(struct stun_value_mapped_address) + 16;
}
default: {
JLOG_DEBUG("Unknown address family %u", (unsigned int)addr->sa_family);
return -1;
}
}
}
bool is_stun_datagram(const void *data, size_t size) {
// RFC 8489: The most significant 2 bits of every STUN message MUST be zeroes. This can be used
// to differentiate STUN packets from other protocols when STUN is multiplexed with other
// protocols on the same port.
if (!size || *((uint8_t *)data) & 0xC0) {
JLOG_VERBOSE("Not a STUN message: first 2 bits are not zeroes");
return false;
}
if (size < sizeof(struct stun_header)) {
JLOG_VERBOSE("Not a STUN message: message too short, size=%zu", size);
return false;
}
const struct stun_header *header = data;
if (ntohl(header->magic) != STUN_MAGIC) {
JLOG_VERBOSE("Not a STUN message: magic number invalid");
return false;
}
// RFC 8489: The message length MUST contain the size of the message in bytes, not including the
// 20-byte STUN header. Since all STUN attributes are padded to a multiple of 4 bytes, the last
// 2 bits of this field are always zero. This provides another way to distinguish STUN packets
// from packets of other protocols.
const size_t length = ntohs(header->length);
if (length & 0x03) {
JLOG_VERBOSE("Not a STUN message: invalid length %zu not multiple of 4", length);
return false;
}
if (size != sizeof(struct stun_header) + length) {
JLOG_VERBOSE("Not a STUN message: invalid length %zu while expecting %zu", length,
size - sizeof(struct stun_header));
return false;
}
return true;
}
int stun_read(void *data, size_t size, stun_message_t *msg) {
memset(msg, 0, sizeof(*msg));
if (size < sizeof(struct stun_header)) {
JLOG_ERROR("STUN message too short, size=%zu", size);
return -1;
}
const struct stun_header *header = data;
const size_t length = ntohs(header->length);
if (size < sizeof(struct stun_header) + length) {
JLOG_ERROR("Invalid STUN message length, length=%zu, available=%zu", length,
size - sizeof(struct stun_header));
return -1;
}
uint16_t type = ntohs(header->type);
msg->msg_class = (stun_class_t)(type & STUN_CLASS_MASK);
msg->msg_method = (stun_method_t)(type & ~STUN_CLASS_MASK);
memcpy(msg->transaction_id, header->transaction_id, STUN_TRANSACTION_ID_SIZE);
JLOG_VERBOSE("Reading STUN message, class=0x%X, method=0x%X", (unsigned int)msg->msg_class,
(unsigned int)msg->msg_method);
uint32_t security_bits = 0;
uint8_t *begin = data;
uint8_t *attr_begin = begin + sizeof(struct stun_header);
uint8_t *end = attr_begin + length;
const uint8_t *pos = attr_begin;
while (pos < end) {
int ret = stun_read_attr(pos, end - pos, msg, begin, attr_begin, &security_bits);
if (ret <= 0) {
JLOG_DEBUG("Reading STUN attribute failed");
return -1;
}
pos += ret;
}
JLOG_VERBOSE("Finished reading STUN attributes");
stun_credentials_t *credentials = &msg->credentials;
// RFC 8489: If the response is an error response with an error code of 401 (Unauthenticated) or
// 438 (Stale Nonce), the client MUST test if the NONCE attribute value starts with the "nonce
// cookie". If so and the "nonce cookie" has the STUN Security Feature "Password algorithms"
// bit set to 1 but no PASSWORD-ALGORITHMS attribute is present, then the client MUST NOT retry
// the request with a new transaction. See
// https://www.rfc-editor.org/rfc/rfc8489.html#section-9.2.5
if (msg->msg_class == STUN_CLASS_RESP_ERROR &&
(msg->error_code == 401 || msg->error_code == 438) &&
security_bits & STUN_SECURITY_PASSWORD_ALGORITHMS_BIT &&
credentials->password_algorithms_value_size == 0) {
JLOG_INFO("STUN Security Feature \"Password algorithms\" bit is set in %u error response "
"but the corresponding attribute is missing",
msg->error_code);
msg->error_code = STUN_ERROR_INTERNAL_VALIDATION_FAILED; // so the agent will give up
}
// RFC 8489: If the request contains neither the PASSWORD-ALGORITHMS nor the
// PASSWORD-ALGORITHM algorithm, then the request is processed as though
// PASSWORD-ALGORITHM were MD5.
// Otherwise, unless (1) PASSWORD-ALGORITHM and PASSWORD-ALGORITHMS are both
// present, (2) PASSWORD-ALGORITHMS matches the value sent in the response that sent
// this NONCE, and (3) PASSWORD-ALGORITHM matches one of the entries in
// PASSWORD-ALGORITHMS, the server MUST generate an error response with an error code of
// 400 (Bad Request). See https://www.rfc-editor.org/rfc/rfc8489.html#section-9.2.4
if (!STUN_IS_RESPONSE(msg->msg_class)) {
if (credentials->password_algorithms_value_size == 0 &&
credentials->password_algorithm == STUN_PASSWORD_ALGORITHM_UNSET) {
credentials->password_algorithm = STUN_PASSWORD_ALGORITHM_MD5;
} else if (credentials->password_algorithm == STUN_PASSWORD_ALGORITHM_UNSET) {
JLOG_INFO("No suitable password algorithm in STUN request");
msg->error_code = STUN_ERROR_INTERNAL_VALIDATION_FAILED;
} else if (credentials->password_algorithms_value_size == 0) {
JLOG_INFO("Missing password algorithms list in STUN request");
msg->error_code = STUN_ERROR_INTERNAL_VALIDATION_FAILED;
} else {
uint8_t pwa_value[STUN_MAX_PASSWORD_ALGORITHMS_VALUE_SIZE];
size_t pwa_size = generate_password_algorithms_attr(pwa_value);
if (pwa_size != credentials->password_algorithms_value_size ||
memcmp(credentials->password_algorithms_value, pwa_value, pwa_size) != 0) {
JLOG_INFO("Password algorithms list is invalid in STUN request");
msg->error_code = STUN_ERROR_INTERNAL_VALIDATION_FAILED;
}
}
}
if (security_bits & STUN_SECURITY_USERNAME_ANONYMITY_BIT) {
JLOG_DEBUG("Remote agent supports user anonymity");
credentials->enable_userhash = true;
}
return (int)(sizeof(struct stun_header) + length);
}
int stun_read_attr(const void *data, size_t size, stun_message_t *msg, uint8_t *begin,
uint8_t *attr_begin, uint32_t *security_bits) {
// RFC 8489: When present, the FINGERPRINT attribute MUST be the last attribute in the
// message and thus will appear after MESSAGE-INTEGRITY and MESSAGE-INTEGRITY-SHA256.
if (msg->has_fingerprint) {
JLOG_DEBUG("Invalid STUN attribute after fingerprint");
return -1;
}
if (size < sizeof(struct stun_attr)) {
JLOG_VERBOSE("STUN attribute too short");
return -1;
}
const struct stun_attr *attr = data;
size_t length = ntohs(attr->length);
stun_attr_type_t type = (stun_attr_type_t)ntohs(attr->type);
JLOG_VERBOSE("Reading attribute 0x%X, length=%zu", (unsigned int)type, length);
if (size < sizeof(struct stun_attr) + length) {
JLOG_DEBUG("STUN attribute length invalid, length=%zu, available=%zu", length,
size - sizeof(struct stun_attr));
return -1;
}
// RFC 8489: Note that agents MUST ignore all attributes that follow MESSAGE-INTEGRITY, with
// the exception of the MESSAGE-INTEGRITY-SHA256 and FINGERPRINT attributes.
if (msg->has_integrity && type != STUN_ATTR_MESSAGE_INTEGRITY &&
type != STUN_ATTR_MESSAGE_INTEGRITY_SHA256 && type != STUN_ATTR_FINGERPRINT) {
JLOG_DEBUG("Ignoring STUN attribute 0x%X after message integrity", (unsigned int)type);
while (length & 0x03)
++length; // attributes are aligned on 4 bytes
return (int)(sizeof(struct stun_attr) + length);
}
switch (type) {
case STUN_ATTR_MAPPED_ADDRESS: {
JLOG_VERBOSE("Reading mapped address");
uint8_t zero_mask[16] = {0};
if (stun_read_value_mapped_address(attr->value, length, &msg->mapped, zero_mask) < 0)
return -1;
break;
}
case STUN_ATTR_XOR_MAPPED_ADDRESS: {
JLOG_VERBOSE("Reading XOR mapped address");
uint8_t mask[16];
*((uint32_t *)mask) = htonl(STUN_MAGIC);
memcpy(mask + 4, msg->transaction_id, 12);
if (stun_read_value_mapped_address(attr->value, length, &msg->mapped, mask) < 0)
return -1;
break;
}
case STUN_ATTR_ALTERNATE_SERVER: {
JLOG_VERBOSE("Reading alternate server");
uint8_t zero_mask[16] = {0};
if (stun_read_value_mapped_address(attr->value, length, &msg->alternate_server, zero_mask) <
0)
return -1;
break;
}
case STUN_ATTR_ERROR_CODE: {
JLOG_VERBOSE("Reading error code");
if (length < sizeof(struct stun_value_error_code)) {
JLOG_DEBUG("STUN error code value too short, length=%zu", length);
return -1;
}
const struct stun_value_error_code *error =
(const struct stun_value_error_code *)attr->value;
msg->error_code = (error->code_class & 0x07) * 100 + error->code_number;
if (msg->error_code == 401 || msg->error_code == 438) { // Unauthenticated or Stale Nonce
JLOG_DEBUG("Got STUN error code %u", msg->error_code);
} else if (JLOG_INFO_ENABLED) {
size_t reason_length = length - sizeof(struct stun_value_error_code);
if (reason_length >= STUN_MAX_ERROR_REASON_LEN)
reason_length = STUN_MAX_ERROR_REASON_LEN - 1;
char buffer[STUN_MAX_ERROR_REASON_LEN];
memcpy(buffer, (const char *)error->reason, reason_length);
buffer[reason_length] = '\0';
JLOG_INFO("Got STUN error code %u, reason \"%s\"", msg->error_code, buffer);
}
break;
}
case STUN_ATTR_UNKNOWN_ATTRIBUTES: {
JLOG_VERBOSE("Reading STUN unknown attributes");
const uint16_t *attributes = (const uint16_t *)attr->value;
for (int i = 0; i < (int)ntohs(attr->length) / 2; ++i) {
stun_attr_type_t type = (stun_attr_type_t)ntohs(attributes[i]);
JLOG_INFO("Got unknown attribute response for attribute 0x%X", (unsigned int)type);
}
break;
}
case STUN_ATTR_USERNAME: {
JLOG_VERBOSE("Reading username");
if (length + 1 > STUN_MAX_USERNAME_LEN) {
JLOG_WARN("STUN username attribute value too long, length=%zu", length);
return -1;
}
memcpy(msg->credentials.username, (const char *)attr->value, length);
msg->credentials.username[length] = '\0';
JLOG_VERBOSE("Got username: %s", msg->credentials.username);
break;
}
case STUN_ATTR_MESSAGE_INTEGRITY: {
JLOG_VERBOSE("Reading message integrity");
if (length != HMAC_SHA1_SIZE) {
JLOG_DEBUG("STUN message integrity length invalid, length=%zu", length);
return -1;
}
msg->has_integrity = true;
break;
}
case STUN_ATTR_MESSAGE_INTEGRITY_SHA256: {
JLOG_VERBOSE("Reading message integrity SHA256");
if (length != HMAC_SHA256_SIZE) {
JLOG_DEBUG("STUN message integrity SHA256 length invalid, length=%zu", length);
return -1;
}
msg->has_integrity = true;
break;
}
case STUN_ATTR_FINGERPRINT: {
JLOG_VERBOSE("Reading fingerprint");
if (length != 4) {
JLOG_DEBUG("STUN fingerprint length invalid, length=%zu", length);
return -1;
}
size_t tmp_length = (uint8_t *)data - attr_begin + STUN_ATTR_SIZE + 4;
size_t prev_length = stun_update_header_length(begin, tmp_length);
uint32_t expected = CRC32(begin, (uint8_t *)data - begin) ^ STUN_FINGERPRINT_XOR;
stun_update_header_length(begin, prev_length);
uint32_t fingerprint = ntohl(*((uint32_t *)attr->value));
if (fingerprint != expected) {
JLOG_ERROR("STUN fingerprint check failed, expected=%lX, actual=%lX",
(unsigned long)expected, (unsigned long)fingerprint);
return -1;
}
JLOG_VERBOSE("STUN fingerprint check succeeded");
msg->has_fingerprint = true;
break;
}
case STUN_ATTR_REALM: {
JLOG_VERBOSE("Reading realm");
if (length + 1 > STUN_MAX_REALM_LEN) {
JLOG_WARN("STUN realm attribute value too long, length=%zu", length);
return -1;
}
memcpy(msg->credentials.realm, (const char *)attr->value, length);
msg->credentials.realm[length] = '\0';
JLOG_VERBOSE("Got realm: %s", msg->credentials.realm);
break;
}
case STUN_ATTR_NONCE: {
JLOG_VERBOSE("Reading nonce");
if (length + 1 > STUN_MAX_NONCE_LEN) {
JLOG_WARN("STUN nonce attribute value too long, length=%zu", length);
return -1;
}
memcpy(msg->credentials.nonce, (const char *)attr->value, length);
msg->credentials.nonce[length] = '\0';
JLOG_VERBOSE("Got nonce: %s", msg->credentials.nonce);
// If the nonce of a response starts with the nonce cookie, decode the Security Feature bits
// See https://www.rfc-editor.org/rfc/rfc8489.html#section-9.2
if (STUN_IS_RESPONSE(msg->msg_class) &&
strlen(msg->credentials.nonce) > STUN_NONCE_COOKIE_LEN + 4 &&
strncmp(msg->credentials.nonce, STUN_NONCE_COOKIE, STUN_NONCE_COOKIE_LEN) == 0) {
char encoded_security_bits[5];
memcpy(encoded_security_bits, msg->credentials.nonce + STUN_NONCE_COOKIE_LEN, 4);
encoded_security_bits[4] = '\0';
uint8_t bytes[4];
bytes[0] = 0;
int len = BASE64_DECODE(encoded_security_bits, bytes + 1, 3);
if (len == 3) {
*security_bits = ntohl(*((uint32_t *)bytes));
JLOG_VERBOSE("Nonce has cookie, Security Feature bits are 0x%lX",
(unsigned long)*security_bits);
} else {
JLOG_WARN("Nonce has cookie, but the encoded Security Feature bits field \"%s\" is "
"invalid",
encoded_security_bits);
security_bits = 0;
}
} else if (msg->msg_class == STUN_CLASS_RESP_ERROR) {
JLOG_DEBUG("Remote agent does not support RFC 8489");
}
break;
}
case STUN_ATTR_PASSWORD_ALGORITHM: {
JLOG_VERBOSE("Reading password algorithm");
if (length < sizeof(struct stun_value_password_algorithm)) {
JLOG_WARN("STUN password algorithm value too short, length=%zu", length);
return -1;
}
if (!STUN_IS_RESPONSE(msg->msg_class)) {
const struct stun_value_password_algorithm *pwa =
(const struct stun_value_password_algorithm *)attr->value;
stun_password_algorithm_t algorithm = ntohs(pwa->algorithm);
if (algorithm == STUN_PASSWORD_ALGORITHM_MD5 ||
algorithm == STUN_PASSWORD_ALGORITHM_SHA256)
msg->credentials.password_algorithm = algorithm;
else
JLOG_WARN("Unknown password algorithm 0x%hX", algorithm);
} else {
JLOG_WARN("Found password algorithm in response, ignoring");
}
break;
}
case STUN_ATTR_PASSWORD_ALGORITHMS: {
JLOG_VERBOSE("Reading password algorithms list");
if (length < sizeof(struct stun_value_password_algorithm)) {
JLOG_WARN("STUN password algorithms list too short, length=%zu", length);
return -1;
}
if (length > STUN_MAX_PASSWORD_ALGORITHMS_VALUE_SIZE) {
JLOG_WARN("STUN password algorithms list too long, length=%zu", length);
return -1;
}
memcpy(msg->credentials.password_algorithms_value, attr->value, length);
msg->credentials.password_algorithms_value_size = length;
if (!STUN_IS_RESPONSE(msg->msg_class)) {
const uint8_t *pos = attr->value;
const uint8_t *end = pos + length;
while (pos < end) {
if ((size_t)(end - pos) < sizeof(struct stun_value_password_algorithm)) {
JLOG_WARN("STUN password algorithms list truncated, available=%zu", end - pos);
return -1;
}
const struct stun_value_password_algorithm *pwa =
(const struct stun_value_password_algorithm *)pos;
stun_password_algorithm_t algorithm = ntohs(pwa->algorithm);
size_t parameters_length = ntohs(pwa->parameters_length);
size_t padded_length = align32(parameters_length);
pos += sizeof(struct stun_value_password_algorithm);
if ((size_t)(end - pos) < padded_length) {
JLOG_WARN(
"STUN password algorithm parameters too long, length=%zu, padded=%zu, "
"available=%zu",
parameters_length, padded_length, end - pos);
return -1;
}
pos += padded_length;
if (algorithm == STUN_PASSWORD_ALGORITHM_MD5 ||
algorithm == STUN_PASSWORD_ALGORITHM_SHA256) {
msg->credentials.password_algorithm = algorithm;
break;
}
JLOG_DEBUG("Unknown password algorithm 0x%hX", algorithm);
}
}
break;
}
case STUN_ATTR_USERHASH: {
JLOG_VERBOSE("Reading user hash");
if (length != USERHASH_SIZE) {
JLOG_WARN("STUN user hash value too long, length=%zu", length);
return -1;
}
memcpy(msg->credentials.userhash, attr->value, USERHASH_SIZE);
msg->credentials.enable_userhash = true;
break;
}
case STUN_ATTR_SOFTWARE: {
JLOG_VERBOSE("Reading software");
if (length + 1 > STUN_MAX_SOFTWARE_LEN) {
JLOG_WARN("STUN software attribute value too long, length=%zu", length);
return -1;
}
char buffer[STUN_MAX_SOFTWARE_LEN];
memcpy(buffer, (const char *)attr->value, length);
buffer[length] = '\0';
JLOG_VERBOSE("Remote agent is \"%s\"", buffer);
break;
}
case STUN_ATTR_PRIORITY: {
JLOG_VERBOSE("Reading priority");
if (length != 4) {
JLOG_DEBUG("STUN priority length invalid, length=%zu", length);
return -1;
}
msg->priority = ntohl(*((uint32_t *)attr->value));
JLOG_VERBOSE("Got priority: %lu", (unsigned long)msg->priority);
break;
}
case STUN_ATTR_USE_CANDIDATE: {
JLOG_VERBOSE("Found use candidate flag");
msg->use_candidate = true;
break;
}
case STUN_ATTR_ICE_CONTROLLING: {
JLOG_VERBOSE("Found ICE controlling attribute");
if (length != 8) {
JLOG_DEBUG("STUN ICE controlling attribute length invalid, length=%zu", length);
return -1;
}
msg->ice_controlling = ntohll(*((uint64_t *)attr->value));
break;
}
case STUN_ATTR_ICE_CONTROLLED: {
JLOG_VERBOSE("Found ICE controlled attribute");
if (length != 8) {
JLOG_DEBUG("STUN ICE controlled attribute length invalid, length=%zu", length);
return -1;
}
msg->ice_controlled = ntohll(*((uint64_t *)attr->value));
break;
}
case STUN_ATTR_CHANNEL_NUMBER: {
JLOG_VERBOSE("Reading channel number attribute");
if (length < sizeof(struct stun_value_channel_number)) {
JLOG_DEBUG("STUN channel number attribute value too short, length=%zu", length);
return -1;
}
const struct stun_value_channel_number *channel_number =
(const struct stun_value_channel_number *)attr->value;
msg->channel_number = ntohs(channel_number->channel_number);
break;
}
case STUN_ATTR_LIFETIME: {
JLOG_VERBOSE("Reading lifetime attribute");
if (length != 4) {
JLOG_DEBUG("STUN lifetime attribute length invalid, length=%zu", length);
return -1;
}
msg->lifetime = ntohl(*((uint32_t *)attr->value));
msg->lifetime_set = true;
break;
}
case STUN_ATTR_XOR_PEER_ADDRESS: {
JLOG_VERBOSE("Reading XOR peer address");
uint8_t mask[16];
*((uint32_t *)mask) = htonl(STUN_MAGIC);
memcpy(mask + 4, msg->transaction_id, 12);
if (stun_read_value_mapped_address(attr->value, length, &msg->peer, mask) < 0)
return -1;
break;
}
case STUN_ATTR_XOR_RELAYED_ADDRESS: {
JLOG_VERBOSE("Reading XOR relayed address");
uint8_t mask[16];
*((uint32_t *)mask) = htonl(STUN_MAGIC);
memcpy(mask + 4, msg->transaction_id, 12);
if (stun_read_value_mapped_address(attr->value, length, &msg->relayed, mask) < 0)
return -1;
break;
}
case STUN_ATTR_DATA: {
JLOG_VERBOSE("Found data");
msg->data = (const char *)attr->value;
msg->data_size = length;
break;
}
case STUN_ATTR_EVEN_PORT: {
JLOG_VERBOSE("Found even port attribute");
if (length < 1) {
JLOG_DEBUG("STUN even port attribute length invalid, length=%zu", length);
return -1;
}
msg->even_port = true;
msg->next_port = ((struct stun_value_even_port *)attr->value)->r & 0x80;
break;
}
case STUN_ATTR_REQUESTED_TRANSPORT: {
JLOG_VERBOSE("Found requested transport attribute");
if (length < sizeof(struct stun_value_requested_transport)) {
JLOG_DEBUG("STUN requested transport attribute length invalid, length=%zu", length);
return -1;
}
const struct stun_value_requested_transport *requested_transport =
(const struct stun_value_requested_transport *)attr->value;
if (requested_transport->protocol != 17) { // UDP
JLOG_WARN("Unexpected requested transport protocol: %d",
(int)requested_transport->protocol);
return -1;
}
msg->requested_transport = true;
break;
}
case STUN_ATTR_DONT_FRAGMENT: {
JLOG_VERBOSE("Found don't fragment attribute");
msg->dont_fragment = true;
break;
}
case STUN_ATTR_RESERVATION_TOKEN: {
JLOG_VERBOSE("Found reservation token");
if (length != 8) {
JLOG_DEBUG("STUN reservation token length invalid, length=%zu", length);
return -1;
}
msg->reservation_token = ntohll(*((uint64_t *)attr->value));
break;
}
default: {
// Ignore
if (STUN_IS_OPTIONAL_ATTR(type))
JLOG_DEBUG("Ignoring unknown optional STUN attribute type 0x%X", (unsigned int)type);
else
JLOG_WARN("Unknown STUN attribute type 0x%X, ignoring", (unsigned int)type);
break;
}
}
return (int)(sizeof(struct stun_attr) + align32(length));
}
int stun_read_value_mapped_address(const void *data, size_t size, addr_record_t *mapped,
const uint8_t *mask) {
size_t len = sizeof(struct stun_value_mapped_address);
if (size < len) {
JLOG_VERBOSE("STUN mapped address value too short, size=%zu", size);
return -1;
}
const struct stun_value_mapped_address *value = data;
stun_address_family_t family = (stun_address_family_t)value->family;
switch (family) {
case STUN_ADDRESS_FAMILY_IPV4: {
len += 4;
if (size < len) {
JLOG_DEBUG("IPv4 mapped address value too short, size=%zu", size);
return -1;
}
JLOG_VERBOSE("Reading IPv4 address");
mapped->len = sizeof(struct sockaddr_in);
struct sockaddr_in *sin = (struct sockaddr_in *)&mapped->addr;
sin->sin_family = AF_INET;
sin->sin_port = value->port ^ *((uint16_t *)mask);
uint8_t *bytes = (uint8_t *)&sin->sin_addr;
for (int i = 0; i < 4; ++i)
bytes[i] = value->address[i] ^ mask[i];
break;
}
case STUN_ADDRESS_FAMILY_IPV6: {
len += 16;
if (size < len) {
JLOG_DEBUG("IPv6 mapped address value too short, size=%zu", size);
return -1;
}
JLOG_VERBOSE("Reading IPv6 address");
mapped->len = sizeof(struct sockaddr_in6);
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&mapped->addr;
sin6->sin6_family = AF_INET6;
sin6->sin6_port = value->port ^ *((uint16_t *)mask);
uint8_t *bytes = (uint8_t *)&sin6->sin6_addr;
for (int i = 0; i < 16; ++i)
bytes[i] = value->address[i] ^ mask[i];
break;
}
default: {
JLOG_DEBUG("Unknown STUN address family 0x%X", (unsigned int)family);
len = size;
break;
}
}
return (int)len;
}
bool stun_check_integrity(void *buf, size_t size, const stun_message_t *msg, const char *password) {
if (!msg->has_integrity)
return false;
const struct stun_header *header = buf;
const size_t length = ntohs(header->length);
if (size < sizeof(struct stun_header) + length)
return false;
uint8_t key[MAX_HMAC_KEY_LEN];
size_t key_len = generate_hmac_key(msg, password, key);
bool success = false;
uint8_t *begin = buf;
const uint8_t *attr_begin = begin + sizeof(struct stun_header);
const uint8_t *end = attr_begin + length;
const uint8_t *pos = attr_begin;
while (pos < end) {
const struct stun_attr *attr = (const struct stun_attr *)pos;
size_t attr_length = ntohs(attr->length);
if (size < sizeof(struct stun_attr) + attr_length)
return false;
stun_attr_type_t type = (stun_attr_type_t)ntohs(attr->type);
switch (type) {
case STUN_ATTR_MESSAGE_INTEGRITY: {
if (attr_length != HMAC_SHA1_SIZE)
return false;
size_t tmp_length = pos - attr_begin + STUN_ATTR_SIZE + HMAC_SHA1_SIZE;
size_t prev_length = stun_update_header_length(begin, tmp_length);
uint8_t hmac[HMAC_SHA1_SIZE];
hmac_sha1(begin, pos - begin, key, key_len, hmac);
stun_update_header_length(begin, prev_length);
const uint8_t *expected_hmac = attr->value;
if (const_time_memcmp(hmac, expected_hmac, HMAC_SHA1_SIZE) != 0) {
JLOG_DEBUG("STUN message integrity SHA1 check failed");
return false;
}
success = true;
break;
}
case STUN_ATTR_MESSAGE_INTEGRITY_SHA256: {
if (attr_length != HMAC_SHA256_SIZE)
return false;
size_t tmp_length = pos - attr_begin + STUN_ATTR_SIZE + HMAC_SHA256_SIZE;
size_t prev_length = stun_update_header_length(begin, tmp_length);
uint8_t hmac[HMAC_SHA256_SIZE];
hmac_sha256(begin, pos - begin, key, key_len, hmac);
stun_update_header_length(begin, prev_length);
const uint8_t *expected_hmac = attr->value;
if (const_time_memcmp(hmac, expected_hmac, HMAC_SHA256_SIZE) != 0) {
JLOG_DEBUG("STUN message integrity SHA256 check failed");
return false;
}
success = true;
break;
}
default:
// Ignore
break;
}
pos += sizeof(struct stun_attr) + align32(attr_length);
}
if (!success)
return false;
JLOG_VERBOSE("STUN message integrity check succeeded");
return true;
}
void stun_prepend_nonce_cookie(char *nonce) {
// RFC 8489: To indicate that it supports this specification, a server MUST prepend the
// NONCE attribute value with the character string composed of "obMatJos2" concatenated with
// the (4-character) base64 [RFC4648] encoding of the 24-bit STUN Security Features See
// https://www.rfc-editor.org/rfc/rfc8489.html#section-9.2
char copy[STUN_MAX_NONCE_LEN];
strcpy(copy, nonce);
char encoded_security_bits[5];
uint32_t security_bits =
htonl(STUN_SECURITY_PASSWORD_ALGORITHMS_BIT | STUN_SECURITY_USERNAME_ANONYMITY_BIT);
BASE64_ENCODE((uint8_t *)&security_bits + 1, 3, encoded_security_bits, 5);
snprintf(nonce, STUN_MAX_NONCE_LEN, "%s%s%.*s", STUN_NONCE_COOKIE, encoded_security_bits,
STUN_MAX_NONCE_LEN - (STUN_NONCE_COOKIE_LEN + 5), copy);
}
void stun_compute_userhash(const char *username, const char *realm, uint8_t *out) {
char input[MAX_USERHASH_INPUT_LEN];
int input_len = snprintf(input, MAX_USERHASH_INPUT_LEN, "%s:%s", username, realm);
if (input_len < 0)
return;
if (input_len >= MAX_USERHASH_INPUT_LEN)
input_len = MAX_USERHASH_INPUT_LEN - 1;
hash_sha256(input, input_len, out);
}
void stun_process_credentials(const stun_credentials_t *credentials, stun_credentials_t *dst) {
char username[STUN_MAX_USERNAME_LEN];
strcpy(username, dst->username);
*dst = *credentials;
strcpy(dst->username, username);
if (credentials->enable_userhash)
stun_compute_userhash(username, credentials->realm, dst->userhash);
}
const char *stun_get_error_reason(unsigned int code) {
switch (code) {
case 0:
return "";
case 300:
return "Try Alternate";
case 400:
return "Bad Request";
case 401:
return "Unauthenticated";
case 403:
return "Forbidden";
case 420:
return "Unknown Attribute";
case 437:
return "Allocation Mismatch";
case 438:
return "Stale Nonce";
case 440:
return "Address Family not Supported";
case 441:
return "Wrong credentials";
case 442:
return "Unsupported Transport Protocol";
case 443:
return "Peer Address Family Mismatch";
case 486:
return "Allocation Quota Reached";
case 500:
return "Server Error";
case 508:
return "Insufficient Capacity";
default:
return "Error";
}
}
JUICE_EXPORT bool _juice_is_stun_datagram(const void *data, size_t size) {
return is_stun_datagram(data, size);
}
JUICE_EXPORT int _juice_stun_read(void *data, size_t size, stun_message_t *msg) {
return stun_read(data, size, msg);
}
JUICE_EXPORT bool _juice_stun_check_integrity(void *buf, size_t size, const stun_message_t *msg,
const char *password) {
return stun_check_integrity(buf, size, msg, password);
}