blob: 2ca9e786772903ccb2b59127df2dc4a6c8215313 [file] [edit]
/*
* Copyright (C) 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "Helpers/cocoa/MiniTURNServer.h"
#import "Helpers/Utilities.h"
#import <CommonCrypto/CommonDigest.h>
#import <CommonCrypto/CommonHMAC.h>
#import <atomic>
#import <netinet/in.h>
#import <optional>
#import <string>
#import <wtf/BlockPtr.h>
#import <wtf/Function.h>
#import <wtf/Lock.h>
#import <wtf/OSObjectPtr.h>
#import <wtf/StdLibExtras.h>
#import <wtf/ThreadSafeRefCounted.h>
#import <wtf/Vector.h>
#import <wtf/cocoa/SpanCocoa.h>
#import <wtf/darwin/DispatchExtras.h>
namespace TestWebKitAPI {
// STUN/TURN wire constants (RFC 5389 / RFC 5766).
static constexpr uint32_t kStunMagicCookie = 0x2112A442;
static constexpr uint16_t kStunHeaderSize = 20;
static constexpr uint16_t kStunMethodBinding = 0x001;
static constexpr uint16_t kStunMethodAllocate = 0x003;
static constexpr uint16_t kStunMethodRefresh = 0x004;
static constexpr uint16_t kStunMethodCreatePermission = 0x008;
static constexpr uint16_t kStunAttrMessageIntegrity = 0x0008;
static constexpr uint16_t kStunAttrErrorCode = 0x0009;
static constexpr uint16_t kStunAttrLifetime = 0x000D;
static constexpr uint16_t kStunAttrRealm = 0x0014;
static constexpr uint16_t kStunAttrNonce = 0x0015;
static constexpr uint16_t kStunAttrXorRelayedAddress = 0x0016;
static constexpr uint16_t kStunAttrXorMappedAddress = 0x0020;
// Long-term credentials shared with the JS side of the test.
static constexpr auto kMockTurnUsername = "testUser";
static constexpr auto kMockTurnPassword = "testPass";
static constexpr auto kMockTurnRealm = "webkit.test";
static constexpr auto kMockTurnNonce = "webkit-mock-turn-nonce-12345678";
class MiniTURNServerState : public ThreadSafeRefCounted<MiniTURNServerState> {
public:
static Ref<MiniTURNServerState> create() { return adoptRef(*new MiniTURNServerState); }
void recordRequest(MiniTURNServer::Transport transport, uint16_t method)
{
Locker locker { m_lock };
m_requests.append({ transport, method });
}
Vector<MiniTURNServer::Request> takeRequests()
{
Locker locker { m_lock };
return std::exchange(m_requests, { });
}
void setUDPPort(uint16_t port) { m_udpPort.store(port); }
void setTCPPort(uint16_t port) { m_tcpPort.store(port); }
uint16_t udpPort() const { return m_udpPort.load(); }
uint16_t tcpPort() const { return m_tcpPort.load(); }
RetainPtr<nw_listener_t> startListener(MiniTURNServer::Transport, Function<void(uint16_t)>&&);
private:
MiniTURNServerState() = default;
void handleStunMessage(std::span<const uint8_t>, MiniTURNServer::Transport, const sockaddr* peer, NOESCAPE const Function<void(std::span<const uint8_t>)>& reply);
void receiveUDPDatagram(RetainPtr<nw_connection_t>&&);
void receiveTCPBytes(RetainPtr<nw_connection_t>&&, Vector<uint8_t>&& accumulated = { });
Lock m_lock;
Vector<MiniTURNServer::Request> m_requests WTF_GUARDED_BY_LOCK(m_lock);
std::atomic<uint16_t> m_udpPort { 0 };
std::atomic<uint16_t> m_tcpPort { 0 };
};
static uint16_t extractStunMethod(uint16_t messageType)
{
return (messageType & 0x000F) | ((messageType & 0x00E0) >> 1) | ((messageType & 0x3E00) >> 2);
}
static uint16_t makeStunSuccessMessageType(uint16_t method)
{
// Success response class bits are C1=1, C0=0 → 0x0100.
uint16_t type = 0x0100;
type |= (method & 0x000F);
type |= (method & 0x0070) << 1;
type |= (method & 0x0F80) << 2;
return type;
}
static uint16_t makeStunErrorMessageType(uint16_t method)
{
// Error response class bits are C1=1, C0=1 → 0x0110.
uint16_t type = 0x0110;
type |= (method & 0x000F);
type |= (method & 0x0070) << 1;
type |= (method & 0x0F80) << 2;
return type;
}
static void writeU16BE(Vector<uint8_t>& out, uint16_t v)
{
out.append(static_cast<uint8_t>(v >> 8));
out.append(static_cast<uint8_t>(v & 0xff));
}
static void writeU32BE(Vector<uint8_t>& out, uint32_t v)
{
writeU16BE(out, static_cast<uint16_t>(v >> 16));
writeU16BE(out, static_cast<uint16_t>(v & 0xffff));
}
static void appendXorAddressAttributeV4(Vector<uint8_t>& out, uint16_t attrType, uint32_t ipv4HostOrder, uint16_t port)
{
writeU16BE(out, attrType);
writeU16BE(out, 8);
out.append(0x00);
out.append(0x01);
writeU16BE(out, port ^ static_cast<uint16_t>(kStunMagicCookie >> 16));
writeU32BE(out, ipv4HostOrder ^ kStunMagicCookie);
}
static void appendXorAddressAttributeV6(Vector<uint8_t>& out, uint16_t attrType, std::span<const uint8_t, 16> ipv6, uint16_t port, std::span<const uint8_t> txnId)
{
writeU16BE(out, attrType);
writeU16BE(out, 20);
out.append(0x00);
out.append(0x02);
writeU16BE(out, port ^ static_cast<uint16_t>(kStunMagicCookie >> 16));
uint8_t xorKey[16] = {
0x21, 0x12, 0xA4, 0x42,
txnId[0], txnId[1], txnId[2], txnId[3],
txnId[4], txnId[5], txnId[6], txnId[7],
txnId[8], txnId[9], txnId[10], txnId[11],
};
uint8_t xored[16];
for (size_t i = 0; i < 16; ++i)
xored[i] = ipv6[i] ^ xorKey[i];
out.append(std::span<const uint8_t> { xored, 16 });
}
static void appendLifetimeAttribute(Vector<uint8_t>& out, uint32_t seconds)
{
writeU16BE(out, kStunAttrLifetime);
writeU16BE(out, 4);
writeU32BE(out, seconds);
}
static void appendPadTo4(Vector<uint8_t>& out)
{
while (out.size() % 4)
out.append(0);
}
static void appendStringAttribute(Vector<uint8_t>& out, uint16_t attrType, const char* value)
{
auto length = strlen(value);
writeU16BE(out, attrType);
writeU16BE(out, static_cast<uint16_t>(length));
out.append(std::span<const uint8_t> { reinterpret_cast<const uint8_t*>(value), length });
appendPadTo4(out);
}
static void appendErrorCodeAttribute(Vector<uint8_t>& out, uint16_t errorCode, const char* reason)
{
auto reasonLength = strlen(reason);
writeU16BE(out, kStunAttrErrorCode);
writeU16BE(out, static_cast<uint16_t>(4 + reasonLength));
out.append(0x00);
out.append(0x00);
out.append(static_cast<uint8_t>(errorCode / 100));
out.append(static_cast<uint8_t>(errorCode % 100));
out.append(std::span<const uint8_t> { reinterpret_cast<const uint8_t*>(reason), reasonLength });
appendPadTo4(out);
}
// Long-term credential key = MD5(username ":" realm ":" password) per RFC 5389 §15.4.
static Vector<uint8_t> computeLongTermAuthKey(const char* username, const char* realm, const char* password)
{
std::string input;
input.reserve(strlen(username) + strlen(realm) + strlen(password) + 2);
input.append(username);
input.push_back(':');
input.append(realm);
input.push_back(':');
input.append(password);
Vector<uint8_t> key(CC_MD5_DIGEST_LENGTH);
CC_MD5(input.data(), static_cast<CC_LONG>(input.size()), key.mutableSpan().data());
return key;
}
// Appends MESSAGE-INTEGRITY to `message` per RFC 5389 §15.4.
static void appendMessageIntegrityAttribute(Vector<uint8_t>& message, std::span<const uint8_t> key)
{
constexpr uint16_t messageIntegrityAttrSize = 24; // 4-byte header + 20-byte HMAC.
uint16_t finalBodyLength = static_cast<uint16_t>(message.size() - kStunHeaderSize + messageIntegrityAttrSize);
message[2] = static_cast<uint8_t>(finalBodyLength >> 8);
message[3] = static_cast<uint8_t>(finalBodyLength & 0xff);
uint8_t hmac[CC_SHA1_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA1, key.data(), key.size(), message.span().data(), message.size(), hmac);
writeU16BE(message, kStunAttrMessageIntegrity);
writeU16BE(message, static_cast<uint16_t>(CC_SHA1_DIGEST_LENGTH));
message.append(std::span<const uint8_t> { hmac, CC_SHA1_DIGEST_LENGTH });
}
static bool hasMessageIntegrityAttribute(std::span<const uint8_t> message)
{
if (message.size() < kStunHeaderSize)
return false;
size_t pos = kStunHeaderSize;
while (pos + 4 <= message.size()) {
uint16_t attrType = (static_cast<uint16_t>(message[pos]) << 8) | message[pos + 1];
uint16_t attrLen = (static_cast<uint16_t>(message[pos + 2]) << 8) | message[pos + 3];
pos += 4;
if (attrType == kStunAttrMessageIntegrity)
return true;
pos += (attrLen + 3) & ~static_cast<size_t>(3);
}
return false;
}
static RetainPtr<nw_endpoint_t> peerEndPointAddress(nw_connection_t conn)
{
RetainPtr endpoint = adoptNS(nw_connection_copy_endpoint(conn));
if (nw_endpoint_get_type(endpoint.get()) != nw_endpoint_type_address)
return { };
return endpoint;
}
void MiniTURNServerState::handleStunMessage(std::span<const uint8_t> message, MiniTURNServer::Transport transport, const sockaddr* peer, NOESCAPE const Function<void(std::span<const uint8_t>)>& reply)
{
if (!peer) {
NSLog(@"MiniTURNServer handleStunMessage: no sockaddr available");
return;
}
if (message.size() < kStunHeaderSize)
return;
uint16_t messageType = (static_cast<uint16_t>(message[0]) << 8) | message[1];
uint16_t method = extractStunMethod(messageType);
uint32_t cookie = (static_cast<uint32_t>(message[4]) << 24)
| (static_cast<uint32_t>(message[5]) << 16)
| (static_cast<uint32_t>(message[6]) << 8)
| message[7];
if (cookie != kStunMagicCookie)
return;
recordRequest(transport, method);
if (method != kStunMethodBinding && method != kStunMethodAllocate
&& method != kStunMethodRefresh && method != kStunMethodCreatePermission)
return;
auto txnId = message.subspan(8, 12);
bool authenticated = hasMessageIntegrityAttribute(message);
// If the request lacks MESSAGE-INTEGRITY, the client is doing its first request and we reply with a 401 challenge.
if (method != kStunMethodBinding && !authenticated) {
Vector<uint8_t> body;
appendErrorCodeAttribute(body, 401, "Unauthorized");
appendStringAttribute(body, kStunAttrRealm, kMockTurnRealm);
appendStringAttribute(body, kStunAttrNonce, kMockTurnNonce);
Vector<uint8_t> out;
writeU16BE(out, makeStunErrorMessageType(method));
writeU16BE(out, static_cast<uint16_t>(body.size()));
writeU32BE(out, kStunMagicCookie);
out.append(txnId);
out.append(body.span());
reply(out.span());
return;
}
// Extract peer address + port and decide which family to encode.
bool peerIsV4 = false;
uint16_t clientPort = 0;
uint32_t clientV4HostOrder = 0;
const uint8_t* clientV6Bytes = nullptr;
if (peer->sa_family == AF_INET) {
peerIsV4 = true;
auto* v4 = reinterpret_cast<const sockaddr_in*>(peer);
clientPort = ntohs(v4->sin_port);
clientV4HostOrder = ntohl(v4->sin_addr.s_addr);
} else if (peer->sa_family == AF_INET6) {
auto* v6 = reinterpret_cast<const sockaddr_in6*>(peer);
clientPort = ntohs(v6->sin6_port);
if (IN6_IS_ADDR_V4MAPPED(&v6->sin6_addr)) {
peerIsV4 = true;
clientV4HostOrder = ntohl(*reinterpret_cast<const uint32_t*>(&v6->sin6_addr.s6_addr[12]));
} else
clientV6Bytes = v6->sin6_addr.s6_addr;
} else
return;
// Emit XOR-MAPPED / XOR-RELAYED in the family matching the peer so libwebrtc's TurnPort accepts them alongside its own network family.
auto appendPeerXorMapped = [&](Vector<uint8_t>& out) {
if (peerIsV4)
appendXorAddressAttributeV4(out, kStunAttrXorMappedAddress, clientV4HostOrder, clientPort);
else
appendXorAddressAttributeV6(out, kStunAttrXorMappedAddress, std::span<const uint8_t, 16> { clientV6Bytes, 16 }, clientPort, txnId);
};
auto appendLoopbackXorRelayed = [&](Vector<uint8_t>& out) {
uint16_t basePort = transport == MiniTURNServer::Transport::Udp ? udpPort() : tcpPort();
uint16_t relayPort = basePort + 1;
if (peerIsV4)
appendXorAddressAttributeV4(out, kStunAttrXorRelayedAddress, INADDR_LOOPBACK, relayPort);
else {
static constexpr uint8_t v6Loopback[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
appendXorAddressAttributeV6(out, kStunAttrXorRelayedAddress, std::span<const uint8_t, 16> { v6Loopback, 16 }, relayPort, txnId);
}
};
Vector<uint8_t> body;
if (method == kStunMethodAllocate) {
appendLoopbackXorRelayed(body);
appendPeerXorMapped(body);
appendLifetimeAttribute(body, 600);
} else if (method == kStunMethodRefresh)
appendLifetimeAttribute(body, 600);
else if (method == kStunMethodBinding)
appendPeerXorMapped(body);
Vector<uint8_t> out;
writeU16BE(out, makeStunSuccessMessageType(method));
writeU16BE(out, static_cast<uint16_t>(body.size()));
writeU32BE(out, kStunMagicCookie);
out.append(txnId);
out.append(body.span());
if (authenticated) {
auto key = computeLongTermAuthKey(kMockTurnUsername, kMockTurnRealm, kMockTurnPassword);
appendMessageIntegrityAttribute(out, key.span());
}
reply(out.span());
}
static void sendReply(nw_connection_t connection, std::span<const uint8_t> reply, bool isComplete)
{
OSObjectPtr data = adoptOSObject(dispatch_data_create(reply.data(), reply.size(), nullptr, DISPATCH_DATA_DESTRUCTOR_DEFAULT));
nw_connection_send(connection, data.get(), NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, isComplete, ^(nw_error_t sendError) {
if (sendError)
NSLog(@"MiniTURNServer send error %d", nw_error_get_error_code(sendError));
});
}
void MiniTURNServerState::receiveUDPDatagram(RetainPtr<nw_connection_t>&& connection)
{
auto* rawConnection = connection.get();
nw_connection_receive_message(rawConnection, makeBlockPtr([protectedThis = Ref { *this }, connection = WTF::move(connection)](dispatch_data_t content, nw_content_context_t, bool, nw_error_t error) mutable {
if (error) {
NSLog(@"MiniTURNServer UDP receive error %d", nw_error_get_error_code(error));
return;
}
if (content && dispatch_data_get_size(content) >= kStunHeaderSize) {
Vector<uint8_t> bytes;
dispatch_data_apply_span(content, [&](std::span<const uint8_t> chunk) {
bytes.append(chunk);
return true;
});
RetainPtr endpoint = peerEndPointAddress(connection.get());
protectedThis->handleStunMessage(bytes.span(), MiniTURNServer::Transport::Udp, nw_endpoint_get_address(endpoint.get()), [&connection](auto reply) {
bool isComplete = true;
sendReply(connection.get(), reply, isComplete);
});
}
protectedThis->receiveUDPDatagram(WTF::move(connection));
}).get());
}
void MiniTURNServerState::receiveTCPBytes(RetainPtr<nw_connection_t>&& connection, Vector<uint8_t>&& accumulated)
{
auto* rawConnection = connection.get();
nw_connection_receive(rawConnection, 1, std::numeric_limits<uint32_t>::max(), makeBlockPtr([protectedThis = Ref { *this }, connection = WTF::move(connection), accumulated = WTF::move(accumulated)](dispatch_data_t content, nw_content_context_t, bool isComplete, nw_error_t error) mutable {
if (error) {
NSLog(@"MiniTURNServer TCP receive error %d", nw_error_get_error_code(error));
return;
}
if (content) {
dispatch_data_apply_span(content, [&](std::span<const uint8_t> chunk) {
accumulated.append(chunk);
return true;
});
}
RetainPtr endpoint = peerEndPointAddress(connection.get());
while (accumulated.size() >= kStunHeaderSize) {
uint16_t bodyLen = (static_cast<uint16_t>(accumulated[2]) << 8) | accumulated[3];
size_t total = kStunHeaderSize + bodyLen;
if (accumulated.size() < total)
break;
std::span<const uint8_t> message { accumulated.span().data(), total };
protectedThis->handleStunMessage(message, MiniTURNServer::Transport::Tcp, nw_endpoint_get_address(endpoint.get()), [&connection](auto reply) {
bool isComplete = false;
sendReply(connection.get(), reply, isComplete);
});
accumulated.removeAt(0, total);
}
if (!isComplete)
protectedThis->receiveTCPBytes(WTF::move(connection), WTF::move(accumulated));
}).get());
}
RetainPtr<nw_listener_t> MiniTURNServerState::startListener(MiniTURNServer::Transport transport, Function<void(uint16_t)>&& onReady)
{
bool isUDP = transport == MiniTURNServer::Transport::Udp;
RetainPtr<nw_parameters_t> parameters = isUDP
? adoptNS(nw_parameters_create_secure_udp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION))
: adoptNS(nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION));
RetainPtr listener = adoptNS(nw_listener_create(parameters.get()));
nw_listener_set_queue(listener.get(), mainDispatchQueueSingleton());
nw_listener_set_state_changed_handler(listener.get(), makeBlockPtr([listener, onReady = WTF::move(onReady), isUDP](nw_listener_state_t state, nw_error_t error) mutable {
if (state == nw_listener_state_failed) {
NSLog(@"MiniTURNServer %s listener failed with error %d", isUDP ? "UDP" : "TCP", error ? nw_error_get_error_code(error) : 0);
return;
}
if (state == nw_listener_state_ready && onReady) {
uint16_t port = nw_listener_get_port(listener.get());
auto handler = std::exchange(onReady, { });
handler(port);
}
}).get());
nw_listener_set_new_connection_handler(listener.get(), makeBlockPtr([protectedThis = Ref { *this }, isUDP](nw_connection_t connection) {
nw_connection_set_queue(connection, mainDispatchQueueSingleton());
nw_connection_start(connection);
if (isUDP)
protectedThis->receiveUDPDatagram({ connection });
else
protectedThis->receiveTCPBytes({ connection });
}).get());
nw_listener_start(listener.get());
return listener;
}
MiniTURNServer::MiniTURNServer()
: m_state(MiniTURNServerState::create())
{
bool udpReady = false;
bool tcpReady = false;
m_udpListener = m_state->startListener(Transport::Udp, [&udpReady, state = Ref { m_state }](uint16_t port) {
state->setUDPPort(port);
udpReady = true;
});
m_tcpListener = m_state->startListener(Transport::Tcp, [&tcpReady, state = Ref { m_state }](uint16_t port) {
state->setTCPPort(port);
tcpReady = true;
});
Util::run(&udpReady);
Util::run(&tcpReady);
}
MiniTURNServer::~MiniTURNServer()
{
if (auto listener = std::exchange(m_udpListener, { }))
nw_listener_cancel(listener.get());
if (auto listener = std::exchange(m_tcpListener, { }))
nw_listener_cancel(listener.get());
}
uint16_t MiniTURNServer::udpPort() const
{
return m_state->udpPort();
}
uint16_t MiniTURNServer::tcpPort() const
{
return m_state->tcpPort();
}
auto MiniTURNServer::takeRequests() -> Vector<Request>
{
return m_state->takeRequests();
}
bool MiniTURNServer::isStunMethodAllocate(uint16_t method)
{
return method == kStunMethodAllocate;
}
} // namespace TestWebKitAPI