From 4a4ae19df99c817f807aac53f11ec234dd52404c Mon Sep 17 00:00:00 2001 From: AJ Keller Date: Mon, 30 Mar 2026 15:01:23 -0700 Subject: [PATCH 1/3] feat: ICE/DTLS/RTP improvements for aiortc and Pipecat interop Changes developed during Bondu firmware integration (ESP32-S3 IoT toy with full-duplex WebRTC audio via Pipecat/Daily.co). ## ICE/STUN (`agent.c`, `stun.c`, `ice.c`) - Add separate ICE socket to match aiortc's per-component socket model - Fix ICE nomination: accept USE-CANDIDATE on binding requests - Add TURN ChannelData framing support (0x4000 prefix detection) - Add CreatePermission and ChannelBind TURN methods - Fix MESSAGE-INTEGRITY computation for long-term credentials - Add ICE candidate priority calculation per RFC 5245 - Configurable STUN timeouts and retry logic - Verbose ICE state transition logging for debugging ## DTLS-SRTP (`dtls_srtp.c`) - Fix DTLS handshake with aiortc (ECDSA certificate generation) - Add DTLS-SRTP key export for bidirectional media - Handle DTLS retransmission timers properly - Add DTLS state machine logging ## RTP/RTCP (`rtp.c`, `rtcp.c`, `rtp.h`) - Fix RTP header extension parsing (RFC 5285 one-byte) - Add SSRC late-binding for incoming streams - Fix byte order issues on little-endian hosts (macOS emulator) - Add RTP timestamp calculation helpers ## SDP (`sdp.c`, `sdp.h`) - Parse ICE candidates from SDP answer - Handle a=setup:active/passive/actpass roles - Parse DTLS fingerprint from SDP ## Peer Connection (`peer_connection.c`) - Add data channel send/receive support - Add connection state change callbacks - Add audio send API with RTP packetization - Handle DTLS-SRTP negotiation completion - Configurable Opus frame duration (20ms default for aiortc compat) ## Other - `socket.c`: Add socket reuse and non-blocking improvements - `address.c/h`: Add address comparison helpers - `config.h`: Add CONFIG_AUDIO_DURATION, CONFIG_DTLS_USE_ECDSA options --- src/address.c | 17 +- src/address.h | 3 + src/agent.c | 949 ++++++++++++++++++++++++++++++++++++++---- src/agent.h | 57 ++- src/config.h | 4 +- src/dtls_srtp.c | 258 ++++++++++-- src/ice.c | 54 ++- src/ice.h | 4 + src/peer.c | 15 +- src/peer_connection.c | 328 ++++++++++++--- src/rtcp.c | 9 +- src/rtp.c | 29 +- src/rtp.h | 4 + src/sctp.c | 2 +- src/sdp.c | 122 ++++-- src/sdp.h | 10 +- src/socket.c | 43 +- src/stun.c | 267 +++++++++--- src/stun.h | 27 +- 19 files changed, 1894 insertions(+), 308 deletions(-) diff --git a/src/address.c b/src/address.c index 1618ede5..c321765e 100644 --- a/src/address.c +++ b/src/address.c @@ -42,7 +42,7 @@ int addr_from_string(const char* buf, Address* addr) { } int addr_to_string(const Address* addr, char* buf, size_t len) { - memset(buf, 0, sizeof(len)); + memset(buf, 0, len); switch (addr->family) { case AF_INET6: return inet_ntop(AF_INET6, &addr->sin6.sin6_addr, buf, len) != NULL; @@ -57,3 +57,18 @@ int addr_equal(const Address* a, const Address* b) { // TODO return 1; } + +int addr_cmp(const Address* a, const Address* b) { + if (a->family != b->family) return -1; + if (a->port != b->port) return -1; + switch (a->family) { + case AF_INET: + default: + if (a->sin.sin_addr.s_addr != b->sin.sin_addr.s_addr) return -1; + break; + case AF_INET6: + if (memcmp(&a->sin6.sin6_addr, &b->sin6.sin6_addr, sizeof(a->sin6.sin6_addr)) != 0) return -1; + break; + } + return 0; +} diff --git a/src/address.h b/src/address.h index 6f8eb781..ba7c05d7 100644 --- a/src/address.h +++ b/src/address.h @@ -33,4 +33,7 @@ int addr_from_string(const char* str, Address* addr); int addr_equal(const Address* a, const Address* b); +// Compare two addresses; return 0 if equal, non-zero otherwise +int addr_cmp(const Address* a, const Address* b); + #endif // ADDRESS_H_ diff --git a/src/agent.c b/src/agent.c index 57de5c95..13ed3765 100644 --- a/src/agent.c +++ b/src/agent.c @@ -1,8 +1,10 @@ +#include #include #include #include #include #include +#include #include "agent.h" #include "base64.h" @@ -12,10 +14,142 @@ #include "stun.h" #include "utils.h" -#define AGENT_POLL_TIMEOUT 1 -#define AGENT_CONNCHECK_MAX 1000 -#define AGENT_CONNCHECK_PERIOD 100 -#define AGENT_STUN_RECV_MAXTIMES 1000 +static int addr_is_private_ipv4(const Address* addr) { + if (addr->family != AF_INET) return 0; + uint32_t ip = ntohl(addr->sin.sin_addr.s_addr); + if ((ip & 0xFF000000) == 0x0A000000) return 1; // 10.0.0.0/8 + if ((ip & 0xFFF00000) == 0xAC100000) return 1; // 172.16.0.0/12 + if ((ip & 0xFFFF0000) == 0xC0A80000) return 1; // 192.168.0.0/16 + return 0; +} + +// Return an ID for the private space: 10 => 1, 172.16/12 => 2, 192.168/16 => 3, else 0 +static int addr_private_space_id(const Address* addr) { + if (addr->family != AF_INET) return 0; + uint32_t ip = ntohl(addr->sin.sin_addr.s_addr); + if ((ip & 0xFF000000) == 0x0A000000) return 1; + if ((ip & 0xFFF00000) == 0xAC100000) return 2; + if ((ip & 0xFFFF0000) == 0xC0A80000) return 3; + return 0; +} + +#define AGENT_POLL_TIMEOUT 150 // 150ms poll timeout - increased for slow TURN relay responses +#define AGENT_CONNCHECK_MAX 1 // 1 check per pair - cycle through ALL candidates quickly + // aiortc sends to all candidates within ~100ms before waiting + // Give Pipecat enough time to process SDP and start sending +#define AGENT_CONNCHECK_PERIOD 1 // Log every check for debugging +// Max receive attempts for synchronous STUN/TURN transactions. +// Each attempt waits up to AGENT_POLL_TIMEOUT (150ms) in select(). +// Keep this bounded to avoid watchdog resets when a TURN transaction never gets a response. +#define AGENT_TURN_RECV_MAX_ATTEMPTS 20 // ~2 seconds max blocking per transaction + +static uint64_t htonll_u64(uint64_t v) { + // Network byte order is big-endian. + uint32_t hi = (uint32_t)(v >> 32); + uint32_t lo = (uint32_t)(v & 0xFFFFFFFFu); + return ((uint64_t)htonl(lo) << 32) | (uint64_t)htonl(hi); +} + +// Forward declarations for STUN processing during TURN setup +void agent_process_stun_request(Agent* agent, StunMessage* stun_msg, Address* addr, ChannelBinding* via_channel); +static int agent_socket_recv(Agent* agent, Address* addr, uint8_t* buf, int len); +static int agent_socket_send(Agent* agent, Address* addr, const uint8_t* buf, int len); + +void agent_channel_init(Agent* agent) { + memset(agent->channel_bindings, 0, sizeof(agent->channel_bindings)); + agent->next_channel_number = CHANNEL_NUMBER_MIN; +} + +ChannelBinding* agent_channel_find_by_peer(Agent* agent, const Address* peer_addr) { + for (int i = 0; i < AGENT_MAX_CHANNEL_BINDINGS; i++) { + if (agent->channel_bindings[i].channel_number == 0) continue; + if (addr_cmp(&agent->channel_bindings[i].peer_addr, peer_addr) == 0) { + return &agent->channel_bindings[i]; + } + } + return NULL; +} + +ChannelBinding* agent_channel_find_by_channel(Agent* agent, uint16_t channel) { + for (int i = 0; i < AGENT_MAX_CHANNEL_BINDINGS; i++) { + if (agent->channel_bindings[i].channel_number == channel) { + return &agent->channel_bindings[i]; + } + } + return NULL; +} + +int agent_channel_allocate(Agent* agent, const Address* peer_addr, ChannelBinding** out_binding) { + ChannelBinding* existing = agent_channel_find_by_peer(agent, peer_addr); + if (existing) { + *out_binding = existing; + return 0; + } + + // Find free slot + int free_idx = -1; + for (int i = 0; i < AGENT_MAX_CHANNEL_BINDINGS; i++) { + if (agent->channel_bindings[i].channel_number == 0) { + free_idx = i; + break; + } + } + if (free_idx < 0) { + LOGE("TURN: No free channel binding slots"); + return -1; + } + + uint16_t channel = agent->next_channel_number; + if (channel < CHANNEL_NUMBER_MIN || channel > CHANNEL_NUMBER_MAX) { + channel = CHANNEL_NUMBER_MIN; + } + agent->next_channel_number = channel + 1; + if (agent->next_channel_number > CHANNEL_NUMBER_MAX) { + agent->next_channel_number = CHANNEL_NUMBER_MIN; + } + + agent->channel_bindings[free_idx].channel_number = channel; + agent->channel_bindings[free_idx].bound = 0; + memcpy(&agent->channel_bindings[free_idx].peer_addr, peer_addr, sizeof(Address)); + *out_binding = &agent->channel_bindings[free_idx]; + return 0; +} + +int agent_channel_data_encode(uint16_t channel, const uint8_t* payload, size_t payload_len, uint8_t* out, size_t out_size) { + if (channel < CHANNEL_NUMBER_MIN || channel > CHANNEL_NUMBER_MAX) { + return -1; + } + if (!payload || !out) { + return -1; + } + if (payload_len > 0xFFFF || out_size < payload_len + 4) { + return -1; + } + uint16_t net_chan = htons(channel); + uint16_t net_len = htons((uint16_t)payload_len); + memcpy(out, &net_chan, sizeof(net_chan)); + memcpy(out + 2, &net_len, sizeof(net_len)); + memcpy(out + 4, payload, payload_len); + return (int)(payload_len + 4); +} + +int agent_channel_data_decode(const uint8_t* data, size_t data_len, uint16_t* channel, const uint8_t** payload, size_t* payload_len) { + if (!data || data_len < 4 || !channel || !payload || !payload_len) { + return -1; + } + uint16_t chan = ntohs(*(uint16_t*)data); + uint16_t len = ntohs(*(uint16_t*)(data + 2)); + if (chan < CHANNEL_NUMBER_MIN || chan > CHANNEL_NUMBER_MAX) { + return -1; + } + if (data_len < 4 + len) { + return -1; + } + *channel = chan; + *payload = data + 4; + *payload_len = len; + return 0; +} void agent_clear_candidates(Agent* agent) { agent->local_candidates_count = 0; @@ -25,21 +159,40 @@ void agent_clear_candidates(Agent* agent) { int agent_create(Agent* agent) { int ret; + // Ensure unused sockets are invalid so select/FD_ISSET never touches garbage fds. + agent->udp_sockets[0].fd = -1; + agent->udp_sockets[1].fd = -1; + agent->ice_socket.fd = -1; + // Do not nominate immediately; only nominate (USE-CANDIDATE) after a successful check. + agent->use_candidate = 0; + // Default: use RFC 8445 priority order. Set prefer_relay=1 for cloud services. + agent->prefer_relay = 0; + if ((ret = udp_socket_open(&agent->udp_sockets[0], AF_INET, 0)) < 0) { LOGE("Failed to create UDP socket."); return ret; } - LOGI("create IPv4 UDP socket: %d", agent->udp_sockets[0].fd); + LOGD("UDP socket: %d", agent->udp_sockets[0].fd); #if CONFIG_IPV6 if ((ret = udp_socket_open(&agent->udp_sockets[1], AF_INET6, 0)) < 0) { LOGE("Failed to create IPv6 UDP socket."); return ret; } - LOGI("create IPv6 UDP socket: %d", agent->udp_sockets[1].fd); + LOGD("UDP6 socket: %d", agent->udp_sockets[1].fd); #endif + // Create separate socket for direct ICE connectivity checks. + // Using a different socket than TURN avoids NAT/firewall issues where + // Cloudflare may reject non-TURN traffic on the TURN-bound socket. + if ((ret = udp_socket_open(&agent->ice_socket, AF_INET, 0)) < 0) { + LOGE("Failed to create ICE socket."); + return ret; + } + LOGI("ICE socket: %d (separate from TURN socket %d)", agent->ice_socket.fd, agent->udp_sockets[0].fd); + agent_clear_candidates(agent); + agent_channel_init(agent); memset(agent->remote_ufrag, 0, sizeof(agent->remote_ufrag)); memset(agent->remote_upwd, 0, sizeof(agent->remote_upwd)); return 0; @@ -55,6 +208,10 @@ void agent_destroy(Agent* agent) { udp_socket_close(&agent->udp_sockets[1]); } #endif + + if (agent->ice_socket.fd > 0) { + udp_socket_close(&agent->ice_socket); + } } static int agent_socket_recv(Agent* agent, Address* addr, uint8_t* buf, int len) { @@ -68,12 +225,13 @@ static int agent_socket_recv(Agent* agent, Address* addr, uint8_t* buf, int len) AF_INET6, #endif }; + const int socket_count = (int)(sizeof(addr_type) / sizeof(addr_type[0])); tv.tv_sec = 0; tv.tv_usec = AGENT_POLL_TIMEOUT * 1000; FD_ZERO(&rfds); - for (i = 0; i < sizeof(addr_type) / sizeof(addr_type[0]); i++) { + for (i = 0; i < socket_count; i++) { if (agent->udp_sockets[i].fd > maxfd) { maxfd = agent->udp_sockets[i].fd; } @@ -82,17 +240,38 @@ static int agent_socket_recv(Agent* agent, Address* addr, uint8_t* buf, int len) } } + // Also poll the separate ICE socket + if (agent->ice_socket.fd >= 0) { + if (agent->ice_socket.fd > maxfd) { + maxfd = agent->ice_socket.fd; + } + FD_SET(agent->ice_socket.fd, &rfds); + } + + LOGD("socket_recv: maxfd=%d timeout=%dms", maxfd, AGENT_POLL_TIMEOUT); + ret = select(maxfd + 1, &rfds, NULL, NULL, &tv); + if (ret < 0) { - LOGE("select error"); + LOGE("select error: %d", errno); } else if (ret == 0) { - // timeout + // Timeout - no data available + LOGD("socket_recv: select timeout (no data)"); } else { - for (i = 0; i < 2; i++) { - if (FD_ISSET(agent->udp_sockets[i].fd, &rfds)) { - memset(buf, 0, len); - ret = udp_socket_recvfrom(&agent->udp_sockets[i], addr, buf, len); - break; + LOGD("socket_recv: select ready, ret=%d", ret); + // Check ICE socket first (more likely to have ICE responses) + if (agent->ice_socket.fd >= 0 && FD_ISSET(agent->ice_socket.fd, &rfds)) { + memset(buf, 0, len); + ret = udp_socket_recvfrom(&agent->ice_socket, addr, buf, len); + LOGD("socket_recv: ice_fd=%d ret=%d", agent->ice_socket.fd, ret); + } else { + for (i = 0; i < socket_count; i++) { + if (agent->udp_sockets[i].fd >= 0 && FD_ISSET(agent->udp_sockets[i].fd, &rfds)) { + memset(buf, 0, len); + ret = udp_socket_recvfrom(&agent->udp_sockets[i], addr, buf, len); + LOGD("socket_recv: fd=%d ret=%d", agent->udp_sockets[i].fd, ret); + break; + } } } } @@ -111,6 +290,52 @@ static int agent_socket_recv_attempts(Agent* agent, Address* addr, uint8_t* buf, return ret; } +// Receive data while processing STUN binding requests from peers. +// This is critical during TURN setup: Pipecat may start sending binding requests +// before we finish CreatePermission/ChannelBind. If we don't respond, Pipecat +// times out and never establishes the connection. +// Returns: >0 for non-binding-request data (TURN responses etc), 0 for timeout, <0 for error +static int agent_recv_turn_response(Agent* agent, uint8_t* buf, int len, int maxtimes) { + int ret = -1; + Address addr; + StunMessage stun_msg; + + // Check if we have ICE credentials to validate binding requests + int can_process_binding = (agent->local_upwd[0] != '\0' && agent->remote_ufrag[0] != '\0'); + + for (int i = 0; i < maxtimes; i++) { + memset(&addr, 0, sizeof(addr)); + ret = agent_socket_recv(agent, &addr, buf, len); + + if (ret <= 0) { + // Timeout or error, continue polling + continue; + } + + // Check if this is a STUN binding request from a peer (only if we can process it) + if (can_process_binding && stun_probe(buf, ret) == 0) { + memcpy(stun_msg.buf, buf, ret); + stun_msg.size = ret; + stun_parse_msg_buf(&stun_msg); + + if (stun_msg.stunclass == STUN_CLASS_REQUEST && stun_msg.stunmethod == STUN_METHOD_BINDING) { + // This is a binding request from Pipecat - process it immediately! + char addr_str[64]; + addr_to_string(&addr, addr_str, sizeof(addr_str)); + LOGI("TURN recv: Got STUN binding request from %s during TURN setup - responding inline", addr_str); + agent_process_stun_request(agent, &stun_msg, &addr, NULL); + // Continue waiting for our TURN response + continue; + } + } + + // Not a binding request - return this data to caller (likely TURN response) + return ret; + } + + return ret; // Timeout +} + static int agent_socket_send(Agent* agent, Address* addr, const uint8_t* buf, int len) { switch (addr->family) { case AF_INET6: @@ -122,6 +347,16 @@ static int agent_socket_send(Agent* agent, Address* addr, const uint8_t* buf, in return -1; } +// Send via the separate ICE socket for direct connectivity checks. +// This avoids NAT/firewall issues with the TURN-bound socket. +static int agent_ice_socket_send(Agent* agent, Address* addr, const uint8_t* buf, int len) { + if (agent->ice_socket.fd < 0) { + // Fallback to main socket if ICE socket not available + return agent_socket_send(agent, addr, buf, len); + } + return udp_socket_sendto(&agent->ice_socket, addr, buf, len); +} + static int agent_create_host_addr(Agent* agent) { int i, j; const char* iface_prefx[] = {CONFIG_IFACE_PREFIX}; @@ -135,11 +370,18 @@ static int agent_create_host_addr(Agent* agent) { for (i = 0; i < sizeof(addr_type) / sizeof(addr_type[0]); i++) { for (j = 0; j < sizeof(iface_prefx) / sizeof(iface_prefx[0]); j++) { ice_candidate = agent->local_candidates + agent->local_candidates_count; - // only copy port and family to addr of ice candidate + // CRITICAL: Use ICE socket for host candidate so all ICE traffic uses same port. + // Previously used udp_sockets[i] (TURN socket), causing port mismatch where we + // advertised one port but sent binding requests from another. + Address* base_addr = (i == 0 && agent->ice_socket.fd >= 0) + ? &agent->ice_socket.bind_addr + : &agent->udp_sockets[i].bind_addr; ice_candidate_create(ice_candidate, agent->local_candidates_count, ICE_CANDIDATE_TYPE_HOST, - &agent->udp_sockets[i].bind_addr); + base_addr); // if resolve host addr, add to local candidate if (ports_get_host_addr(&ice_candidate->addr, iface_prefx[j])) { + // For host candidates the base (raddr/rport) is itself + memcpy(&ice_candidate->raddr, &ice_candidate->addr, sizeof(Address)); agent->local_candidates_count++; } } @@ -158,14 +400,18 @@ static int agent_create_stun_addr(Agent* agent, Address* serv_addr) { stun_msg_create(&send_msg, STUN_CLASS_REQUEST | STUN_METHOD_BINDING); - ret = agent_socket_send(agent, serv_addr, send_msg.buf, send_msg.size); + // CRITICAL: Send STUN binding request FROM the ICE socket, not the TURN socket. + // This gets the reflexive address of the ICE socket, which we then advertise. + // When we later send ICE binding requests from the ICE socket, they match + // the address Pipecat has created TURN permissions for. + ret = agent_ice_socket_send(agent, serv_addr, send_msg.buf, send_msg.size); if (ret == -1) { LOGE("Failed to send STUN Binding Request."); return ret; } - ret = agent_socket_recv_attempts(agent, NULL, recv_msg.buf, sizeof(recv_msg.buf), AGENT_STUN_RECV_MAXTIMES); + ret = agent_socket_recv_attempts(agent, NULL, recv_msg.buf, sizeof(recv_msg.buf), AGENT_TURN_RECV_MAX_ATTEMPTS); if (ret <= 0) { LOGD("Failed to receive STUN Binding Response."); return ret; @@ -175,6 +421,12 @@ static int agent_create_stun_addr(Agent* agent, Address* serv_addr) { memcpy(&bind_addr, &recv_msg.mapped_addr, sizeof(Address)); IceCandidate* ice_candidate = agent->local_candidates + agent->local_candidates_count++; ice_candidate_create(ice_candidate, agent->local_candidates_count, ICE_CANDIDATE_TYPE_SRFLX, &bind_addr); + // Set base address (raddr/rport) to the first host candidate if available + if (agent->local_candidates_count > 1) { // host candidate already added + memcpy(&ice_candidate->raddr, &agent->local_candidates[0].addr, sizeof(Address)); + } else { + memcpy(&ice_candidate->raddr, &agent->udp_sockets[0].bind_addr, sizeof(Address)); + } return ret; } @@ -184,6 +436,23 @@ static int agent_create_turn_addr(Agent* agent, Address* serv_addr, const char* Address turn_addr; StunMessage send_msg; StunMessage recv_msg; + // Store TURN server addr and creds for later CreatePermission + memcpy(&agent->turn_server_addr, serv_addr, sizeof(Address)); + memset(agent->turn_username, 0, sizeof(agent->turn_username)); + memset(agent->turn_credential, 0, sizeof(agent->turn_credential)); + agent->turn_username_len = strlen(username); + agent->turn_credential_len = strlen(credential); + if (agent->turn_username_len >= sizeof(agent->turn_username)) { + agent->turn_username_len = sizeof(agent->turn_username) - 1; + } + if (agent->turn_credential_len >= sizeof(agent->turn_credential)) { + agent->turn_credential_len = sizeof(agent->turn_credential) - 1; + } + memcpy(agent->turn_username, username, agent->turn_username_len); + memcpy(agent->turn_credential, credential, agent->turn_credential_len); + agent->has_turn_allocation = 0; + agent->turn_nonce_len = 0; + agent->turn_realm_len = 0; memset(&recv_msg, 0, sizeof(recv_msg)); memset(&send_msg, 0, sizeof(send_msg)); stun_msg_create(&send_msg, STUN_METHOD_ALLOCATE); @@ -196,7 +465,7 @@ static int agent_create_turn_addr(Agent* agent, Address* serv_addr, const char* return -1; } - ret = agent_socket_recv_attempts(agent, NULL, recv_msg.buf, sizeof(recv_msg.buf), AGENT_STUN_RECV_MAXTIMES); + ret = agent_recv_turn_response(agent, recv_msg.buf, sizeof(recv_msg.buf), AGENT_TURN_RECV_MAX_ATTEMPTS); if (ret <= 0) { LOGD("Failed to receive STUN Binding Response."); return ret; @@ -205,13 +474,31 @@ static int agent_create_turn_addr(Agent* agent, Address* serv_addr, const char* stun_parse_msg_buf(&recv_msg); if (recv_msg.stunclass == STUN_CLASS_ERROR && recv_msg.stunmethod == STUN_METHOD_ALLOCATE) { + // Log the 401 challenge parameters - USE STORED LENGTHS, NOT strlen()! + LOGD("TURN 401 challenge, retrying with auth"); + // Persist nonce/realm for CreatePermission later + agent->turn_nonce_len = recv_msg.nonce_len < sizeof(agent->turn_nonce) ? recv_msg.nonce_len : sizeof(agent->turn_nonce) - 1; + agent->turn_realm_len = recv_msg.realm_len < sizeof(agent->turn_realm) ? recv_msg.realm_len : sizeof(agent->turn_realm) - 1; + memcpy(agent->turn_nonce, recv_msg.nonce, agent->turn_nonce_len); + agent->turn_nonce[agent->turn_nonce_len] = '\0'; + memcpy(agent->turn_realm, recv_msg.realm, agent->turn_realm_len); + agent->turn_realm[agent->turn_realm_len] = '\0'; + + // Sanity check: if nonce_len is 0, something went wrong in parsing + if (recv_msg.nonce_len == 0) { + LOGE("TURN 401 response missing nonce! Cannot authenticate."); + return -1; + } + memset(&send_msg, 0, sizeof(send_msg)); stun_msg_create(&send_msg, STUN_METHOD_ALLOCATE); stun_msg_write_attr(&send_msg, STUN_ATTR_TYPE_REQUESTED_TRANSPORT, sizeof(attr), (char*)&attr); // UDP stun_msg_write_attr(&send_msg, STUN_ATTR_TYPE_USERNAME, strlen(username), (char*)username); - stun_msg_write_attr(&send_msg, STUN_ATTR_TYPE_NONCE, strlen(recv_msg.nonce), recv_msg.nonce); - stun_msg_write_attr(&send_msg, STUN_ATTR_TYPE_REALM, strlen(recv_msg.realm), recv_msg.realm); + // CRITICAL: Use stored nonce_len and realm_len, NOT strlen()! + stun_msg_write_attr(&send_msg, STUN_ATTR_TYPE_NONCE, recv_msg.nonce_len, recv_msg.nonce); + stun_msg_write_attr(&send_msg, STUN_ATTR_TYPE_REALM, recv_msg.realm_len, recv_msg.realm); stun_msg_finish(&send_msg, STUN_CREDENTIAL_LONG_TERM, credential, strlen(credential)); + LOGD("Retrying TURN Allocate with MESSAGE-INTEGRITY"); } else { LOGE("Invalid TURN Binding Response."); return -1; @@ -219,23 +506,193 @@ static int agent_create_turn_addr(Agent* agent, Address* serv_addr, const char* ret = agent_socket_send(agent, serv_addr, send_msg.buf, send_msg.size); if (ret < 0) { - LOGE("Failed to send TURN Binding Request."); + LOGE("Failed to send TURN Allocate with MESSAGE-INTEGRITY."); return -1; } + LOGD("TURN Allocate retry sent"); - agent_socket_recv_attempts(agent, NULL, recv_msg.buf, sizeof(recv_msg.buf), AGENT_STUN_RECV_MAXTIMES); + // BUGFIX: Actually capture the return value from recv! + ret = agent_recv_turn_response(agent, recv_msg.buf, sizeof(recv_msg.buf), AGENT_TURN_RECV_MAX_ATTEMPTS); if (ret <= 0) { - LOGD("Failed to receive TURN Binding Response."); - return ret; + LOGE("Failed to receive TURN Allocate response (ret=%d)", ret); + return -1; } + LOGD("TURN Allocate response received"); stun_parse_msg_buf(&recv_msg); + + // Check if we got ANOTHER 401 (MI was rejected) vs success + if (recv_msg.stunclass == STUN_CLASS_ERROR) { + LOGE("TURN Allocate with MESSAGE-INTEGRITY was rejected! Check credentials/nonce."); + return -1; + } + + if (recv_msg.relayed_addr.port == 0) { + LOGE("TURN Allocate succeeded but no relay address in response!"); + return -1; + } + memcpy(&turn_addr, &recv_msg.relayed_addr, sizeof(Address)); + { + char relay_addr_str[64]; + addr_to_string(&turn_addr, relay_addr_str, sizeof(relay_addr_str)); + LOGI("TURN relay address obtained: %s:%u", relay_addr_str, (unsigned)turn_addr.port); + } IceCandidate* ice_candidate = agent->local_candidates + agent->local_candidates_count++; ice_candidate_create(ice_candidate, agent->local_candidates_count, ICE_CANDIDATE_TYPE_RELAY, &turn_addr); + // Base address for relay should point to the local host candidate + if (agent->local_candidates_count > 0) { + memcpy(&ice_candidate->raddr, &agent->local_candidates[0].addr, sizeof(Address)); + } else { + memcpy(&ice_candidate->raddr, &agent->udp_sockets[0].bind_addr, sizeof(Address)); + } + agent->has_turn_allocation = 1; return ret; } +int agent_build_channel_bind_request(Agent* agent, ChannelBinding* binding, StunMessage* out_msg) { + if (!agent || !binding || !out_msg) return -1; + if (!agent->has_turn_allocation || agent->turn_nonce_len == 0 || agent->turn_realm_len == 0) { + LOGW("TURN ChannelBind build skipped (no allocation or missing nonce/realm)"); + return -1; + } + uint8_t peer_attr[32] = {0}; + uint8_t mask[16] = {0}; + uint8_t channel_attr[4] = {0}; + int peer_len = 0; + + memset(out_msg, 0, sizeof(StunMessage)); + stun_msg_create(out_msg, STUN_CLASS_REQUEST | STUN_METHOD_CHANNEL_BIND); + channel_attr[0] = (binding->channel_number >> 8) & 0xFF; + channel_attr[1] = binding->channel_number & 0xFF; + stun_msg_write_attr(out_msg, STUN_ATTR_TYPE_CHANNEL_NUMBER, sizeof(channel_attr), (char*)channel_attr); + + *((uint32_t*)mask) = htonl(MAGIC_COOKIE); + StunHeader* header = (StunHeader*)out_msg->buf; + memcpy(mask + 4, header->transaction_id, sizeof(header->transaction_id)); + peer_len = stun_set_mapped_address((char*)peer_attr, mask, &binding->peer_addr); + stun_msg_write_attr(out_msg, STUN_ATTR_TYPE_XOR_PEER_ADDRESS, peer_len, (char*)peer_attr); + stun_msg_write_attr(out_msg, STUN_ATTR_TYPE_USERNAME, agent->turn_username_len, agent->turn_username); + stun_msg_write_attr(out_msg, STUN_ATTR_TYPE_NONCE, agent->turn_nonce_len, agent->turn_nonce); + stun_msg_write_attr(out_msg, STUN_ATTR_TYPE_REALM, agent->turn_realm_len, agent->turn_realm); + stun_msg_finish(out_msg, STUN_CREDENTIAL_LONG_TERM, agent->turn_credential, agent->turn_credential_len); + return 0; +} + +static int agent_turn_channel_bind(Agent* agent, ChannelBinding* binding) { + if (!agent || !binding) return -1; + StunMessage send_msg; + StunMessage recv_msg; + // Some TURN servers are picky and require an explicit CreatePermission even when using ChannelBind. + // RFC 5766 says ChannelBind creates/refreshes the corresponding permission, but doing an explicit + // CreatePermission here improves interoperability. + if (agent->has_turn_allocation && agent->turn_nonce_len > 0 && agent->turn_realm_len > 0) { + StunMessage perm_msg; + StunMessage perm_resp; + uint8_t peer_attr[32] = {0}; + uint8_t mask[16] = {0}; + int peer_len = 0; + + memset(&perm_msg, 0, sizeof(perm_msg)); + stun_msg_create(&perm_msg, STUN_CLASS_REQUEST | STUN_METHOD_CREATE_PERMISSION); + + *((uint32_t*)mask) = htonl(MAGIC_COOKIE); + StunHeader* perm_header = (StunHeader*)perm_msg.buf; + memcpy(mask + 4, perm_header->transaction_id, sizeof(perm_header->transaction_id)); + peer_len = stun_set_mapped_address((char*)peer_attr, mask, &binding->peer_addr); + stun_msg_write_attr(&perm_msg, STUN_ATTR_TYPE_XOR_PEER_ADDRESS, peer_len, (char*)peer_attr); + stun_msg_write_attr(&perm_msg, STUN_ATTR_TYPE_USERNAME, agent->turn_username_len, agent->turn_username); + stun_msg_write_attr(&perm_msg, STUN_ATTR_TYPE_NONCE, agent->turn_nonce_len, agent->turn_nonce); + stun_msg_write_attr(&perm_msg, STUN_ATTR_TYPE_REALM, agent->turn_realm_len, agent->turn_realm); + stun_msg_finish(&perm_msg, STUN_CREDENTIAL_LONG_TERM, agent->turn_credential, agent->turn_credential_len); + + char perm_peer_str[ADDRSTRLEN]; + addr_to_string(&binding->peer_addr, perm_peer_str, sizeof(perm_peer_str)); + LOGI("TURN: Sending CreatePermission peer=%s:%d", perm_peer_str, binding->peer_addr.port); + + int perm_sent = agent_socket_send(agent, &agent->turn_server_addr, perm_msg.buf, perm_msg.size); + if (perm_sent < 0) { + LOGE("TURN: Failed to send CreatePermission"); + return -1; + } + memset(&perm_resp, 0, sizeof(perm_resp)); + int perm_ret = agent_recv_turn_response(agent, perm_resp.buf, sizeof(perm_resp.buf), AGENT_TURN_RECV_MAX_ATTEMPTS); + if (perm_ret <= 0) { + // Best-effort: proceed to ChannelBind anyway (many servers accept ChannelBind without this). + LOGW("TURN: No CreatePermission response (ret=%d) — proceeding to ChannelBind anyway", perm_ret); + } else { + stun_parse_msg_buf(&perm_resp); + LOGI("TURN: CreatePermission response class=0x%02x method=0x%04x", perm_resp.stunclass, perm_resp.stunmethod); + if (perm_resp.stunclass == STUN_CLASS_ERROR) { + LOGW("TURN: CreatePermission rejected — proceeding to ChannelBind anyway"); + } + } + } + + int ret = agent_build_channel_bind_request(agent, binding, &send_msg); + if (ret < 0) { + return ret; + } + + char peer_str[ADDRSTRLEN]; + addr_to_string(&binding->peer_addr, peer_str, sizeof(peer_str)); + LOGI("TURN: Sending ChannelBind ch=0x%04x peer=%s:%d", binding->channel_number, peer_str, binding->peer_addr.port); + + ret = agent_socket_send(agent, &agent->turn_server_addr, send_msg.buf, send_msg.size); + if (ret < 0) { + LOGE("TURN: Failed to send ChannelBind"); + return -1; + } + + memset(&recv_msg, 0, sizeof(recv_msg)); + ret = agent_recv_turn_response(agent, recv_msg.buf, sizeof(recv_msg.buf), AGENT_TURN_RECV_MAX_ATTEMPTS); + if (ret <= 0) { + LOGE("TURN: No ChannelBind response (ret=%d)", ret); + return -1; + } + stun_parse_msg_buf(&recv_msg); + LOGI("TURN: ChannelBind response class=0x%02x method=0x%04x", recv_msg.stunclass, recv_msg.stunmethod); + + if (recv_msg.stunclass == STUN_CLASS_ERROR) { + // Retry once if nonce/realm provided (401/438) + if (recv_msg.nonce_len > 0 && recv_msg.realm_len > 0) { + agent->turn_nonce_len = recv_msg.nonce_len < sizeof(agent->turn_nonce) ? recv_msg.nonce_len : sizeof(agent->turn_nonce) - 1; + agent->turn_realm_len = recv_msg.realm_len < sizeof(agent->turn_realm) ? recv_msg.realm_len : sizeof(agent->turn_realm) - 1; + memcpy(agent->turn_nonce, recv_msg.nonce, agent->turn_nonce_len); + agent->turn_nonce[agent->turn_nonce_len] = '\0'; + memcpy(agent->turn_realm, recv_msg.realm, agent->turn_realm_len); + agent->turn_realm[agent->turn_realm_len] = '\0'; + + memset(&send_msg, 0, sizeof(send_msg)); + ret = agent_build_channel_bind_request(agent, binding, &send_msg); + if (ret < 0) { + return ret; + } + ret = agent_socket_send(agent, &agent->turn_server_addr, send_msg.buf, send_msg.size); + if (ret < 0) { + LOGE("TURN: Failed to send ChannelBind retry"); + return -1; + } + ret = agent_recv_turn_response(agent, recv_msg.buf, sizeof(recv_msg.buf), AGENT_TURN_RECV_MAX_ATTEMPTS); + if (ret <= 0) { + LOGE("TURN: No ChannelBind response after retry"); + return -1; + } + stun_parse_msg_buf(&recv_msg); + if (recv_msg.stunclass == STUN_CLASS_ERROR) { + LOGE("TURN: ChannelBind retry still error"); + return -1; + } + } else { + LOGE("TURN: ChannelBind error without nonce/realm"); + return -1; + } + } + + binding->bound = 1; + return 0; +} + void agent_gather_candidate(Agent* agent, const char* urls, const char* username, const char* credential) { char* pos; int port; @@ -268,7 +725,7 @@ void agent_gather_candidate(Agent* agent, const char* urls, const char* username if (ports_resolve_addr(hostname, &resolved_addr) == 0) { addr_set_port(&resolved_addr, port); addr_to_string(&resolved_addr, addr_string, sizeof(addr_string)); - LOGI("Resolved stun/turn server %s:%d", addr_string, port); + LOGD("Resolved %s:%d", addr_string, port); if (strncmp(urls, "stun:", 5) == 0) { LOGD("Create stun addr"); @@ -300,7 +757,27 @@ void agent_get_local_description(Agent* agent, char* description, int length) { } int agent_send(Agent* agent, const uint8_t* buf, int len) { - return agent_socket_send(agent, &agent->nominated_pair->remote->addr, buf, len); + if (!agent || !buf || len <= 0) return -1; + if (agent->nominated_pair && agent->nominated_pair->local->type == ICE_CANDIDATE_TYPE_RELAY) { + ChannelBinding* binding = NULL; + uint8_t channel_buf[1400 + 4]; + if (agent_channel_allocate(agent, &agent->nominated_pair->remote->addr, &binding) != 0) { + return -1; + } + if (!binding->bound) { + if (agent_turn_channel_bind(agent, binding) < 0) { + return -1; + } + } + int encoded = agent_channel_data_encode(binding->channel_number, buf, len, channel_buf, sizeof(channel_buf)); + if (encoded < 0) { + return -1; + } + return agent_socket_send(agent, &agent->turn_server_addr, channel_buf, encoded); + } + // For non-relay (host/srflx), use the ICE socket that was used for ICE negotiation. + // This ensures DTLS packets come from the same source address/port as ICE binding requests. + return agent_ice_socket_send(agent, &agent->nominated_pair->remote->addr, buf, len); } static void agent_create_binding_response(Agent* agent, StunMessage* msg, Address* addr) { @@ -321,34 +798,67 @@ static void agent_create_binding_response(Agent* agent, StunMessage* msg, Addres stun_msg_finish(msg, STUN_CREDENTIAL_SHORT_TERM, agent->local_upwd, strlen(agent->local_upwd)); } -static void agent_create_binding_request(Agent* agent, StunMessage* msg) { - uint64_t tie_breaker = 0; // always be controlled - // send binding request +void agent_create_binding_request(Agent* agent, StunMessage* msg, int is_heartbeat) { + // RFC 8445 §6.1.1.2: tie-breaker MUST be a random number + static uint64_t tie_breaker = 0; + if (tie_breaker == 0) { + // Generate a simple random tie-breaker using available entropy + uint32_t r1 = (uint32_t)rand(); + uint32_t r2 = (uint32_t)rand(); + tie_breaker = ((uint64_t)r1 << 32) | r2; + } stun_msg_create(msg, STUN_CLASS_REQUEST | STUN_METHOD_BINDING); + + // Both normal checks and heartbeats need full ICE credentials (RFC 7675 consent freshness). + // Without USERNAME + MESSAGE-INTEGRITY the peer rejects with 400 Bad Request. char username[584]; memset(username, 0, sizeof(username)); snprintf(username, sizeof(username), "%s:%s", agent->remote_ufrag, agent->local_ufrag); stun_msg_write_attr(msg, STUN_ATTR_TYPE_USERNAME, strlen(username), username); - stun_msg_write_attr(msg, STUN_ATTR_TYPE_PRIORITY, 4, (char*)&agent->nominated_pair->priority); + + // RFC 8445 §7.1.1: PRIORITY attribute must use peer-reflexive type preference + uint32_t prflx_priority = ice_candidate_compute_prflx_priority(agent->nominated_pair->local); + uint32_t priority_network = htonl(prflx_priority); + stun_msg_write_attr(msg, STUN_ATTR_TYPE_PRIORITY, 4, (char*)&priority_network); if (agent->mode == AGENT_MODE_CONTROLLING) { - stun_msg_write_attr(msg, STUN_ATTR_TYPE_USE_CANDIDATE, 0, NULL); - stun_msg_write_attr(msg, STUN_ATTR_TYPE_ICE_CONTROLLING, 8, (char*)&tie_breaker); + uint64_t tie_breaker_network = htonll_u64(tie_breaker); + stun_msg_write_attr(msg, STUN_ATTR_TYPE_ICE_CONTROLLING, 8, (char*)&tie_breaker_network); + // USE-CANDIDATE only for initial nomination, not heartbeat (RFC 7675 consent freshness) + if (!is_heartbeat) { + stun_msg_write_attr(msg, STUN_ATTR_TYPE_USE_CANDIDATE, 0, NULL); + } } else { - stun_msg_write_attr(msg, STUN_ATTR_TYPE_ICE_CONTROLLED, 8, (char*)&tie_breaker); + uint64_t tie_breaker_network = htonll_u64(tie_breaker); + stun_msg_write_attr(msg, STUN_ATTR_TYPE_ICE_CONTROLLED, 8, (char*)&tie_breaker_network); } stun_msg_finish(msg, STUN_CREDENTIAL_SHORT_TERM, agent->remote_upwd, strlen(agent->remote_upwd)); } -void agent_process_stun_request(Agent* agent, StunMessage* stun_msg, Address* addr) { +void agent_process_stun_request(Agent* agent, StunMessage* stun_msg, Address* addr, ChannelBinding* via_channel) { StunMessage msg; StunHeader* header; switch (stun_msg->stunmethod) { case STUN_METHOD_BINDING: - if (stun_msg_is_valid(stun_msg->buf, stun_msg->size, agent->local_upwd) == 0) { + if (stun_msg_is_valid(stun_msg->buf, stun_msg->size, agent->local_upwd, strlen(agent->local_upwd)) == 0) { header = (StunHeader*)stun_msg->buf; memcpy(agent->transaction_id, header->transaction_id, sizeof(header->transaction_id)); agent_create_binding_response(agent, &msg, addr); - agent_socket_send(agent, addr, msg.buf, msg.size); + + // Route response via TURN ChannelData if request came that way + if (via_channel && via_channel->bound) { + uint8_t channel_buf[sizeof(msg.buf) + 4]; + int encoded = agent_channel_data_encode(via_channel->channel_number, msg.buf, msg.size, channel_buf, sizeof(channel_buf)); + if (encoded > 0) { + LOGD("ICE: Sending binding response via TURN ChannelData ch=0x%04x", via_channel->channel_number); + agent_socket_send(agent, &agent->turn_server_addr, channel_buf, encoded); + } else { + LOGE("ICE: Failed to encode binding response as ChannelData"); + } + } else { + // Direct response - use ICE socket to match source address of our binding requests + // This ensures aiortc sees consistent source address for all ICE traffic + agent_ice_socket_send(agent, addr, msg.buf, msg.size); + } agent->binding_request_time = ports_get_epoch_time(); } break; @@ -358,13 +868,28 @@ void agent_process_stun_request(Agent* agent, StunMessage* stun_msg, Address* ad } void agent_process_stun_response(Agent* agent, StunMessage* stun_msg) { + LOGD("STUN response: method=0x%04x", stun_msg->stunmethod); switch (stun_msg->stunmethod) { - case STUN_METHOD_BINDING: - if (stun_msg_is_valid(stun_msg->buf, stun_msg->size, agent->remote_upwd) == 0) { - agent->nominated_pair->state = ICE_CANDIDATE_STATE_SUCCEEDED; + case STUN_METHOD_BINDING: { + int valid = stun_msg_is_valid(stun_msg->buf, stun_msg->size, agent->remote_upwd, strlen(agent->remote_upwd)); + if (valid == 0) { + if (!agent->use_candidate) { + // First successful check - connectivity confirmed. Now trigger nomination. + LOGI("ICE: Binding response valid, connectivity confirmed. Starting nomination..."); + agent->use_candidate = 1; + // Keep state as INPROGRESS to trigger another check with USE-CANDIDATE + } else { + // Nomination check succeeded - ICE complete! + LOGI("ICE: Nomination confirmed, connected"); + agent->nominated_pair->state = ICE_CANDIDATE_STATE_SUCCEEDED; + } + } else { + LOGW("ICE binding response validation FAILED"); } break; + } default: + LOGW("Unknown STUN response method: 0x%04x", stun_msg->stunmethod); break; } } @@ -373,23 +898,82 @@ int agent_recv(Agent* agent, uint8_t* buf, int len) { int ret = -1; StunMessage stun_msg; Address addr; - if ((ret = agent_socket_recv(agent, &addr, buf, len)) > 0 && stun_probe(buf, len) == 0) { - memcpy(stun_msg.buf, buf, ret); - stun_msg.size = ret; - stun_parse_msg_buf(&stun_msg); - switch (stun_msg.stunclass) { - case STUN_CLASS_REQUEST: - agent_process_stun_request(agent, &stun_msg, &addr); - break; - case STUN_CLASS_RESPONSE: - agent_process_stun_response(agent, &stun_msg); - break; - case STUN_CLASS_ERROR: - break; - default: - break; - } - ret = 0; + // During ICE checks we can see responses slightly delayed; do a small bounded wait + // instead of a single 100ms poll to avoid missing packets under load. + const int recv_attempts = 3; // 3 * AGENT_POLL_TIMEOUT (100ms) = 300ms max + ret = agent_socket_recv_attempts(agent, &addr, buf, len, recv_attempts); + if (ret > 0) { + // Log first byte to detect packet type + LOGD("agent_recv: %d bytes, first_byte=0x%02x", ret, buf[0]); + // ChannelData must be demuxed before STUN probe + if (stun_is_channel_data(buf, ret)) { + LOGD("TURN: ChannelData RECV (%d bytes)", ret); + uint16_t channel = 0; + const uint8_t* payload = NULL; + size_t payload_len = 0; + if (agent_channel_data_decode(buf, ret, &channel, &payload, &payload_len) == 0) { + LOGD("TURN: ChannelData ch=0x%04x payload_len=%zu", channel, payload_len); + ChannelBinding* binding = agent_channel_find_by_channel(agent, channel); + if (binding && payload_len > 0 && stun_probe((uint8_t*)payload, payload_len) == 0) { + StunMessage inner; + memset(&inner, 0, sizeof(inner)); + memcpy(inner.buf, payload, payload_len); + inner.size = payload_len; + stun_parse_msg_buf(&inner); + LOGD("TURN: ChannelData contains STUN class=0x%02x method=0x%04x", inner.stunclass, inner.stunmethod); + if (inner.stunclass == STUN_CLASS_RESPONSE) { + agent_process_stun_response(agent, &inner); + } else if (inner.stunclass == STUN_CLASS_REQUEST) { + // Process binding requests from peer via TURN relay + // The peer's address is the channel's bound peer address + // Pass channel binding so response goes back via TURN + agent_process_stun_request(agent, &inner, &binding->peer_addr, binding); + } + } else if (!binding) { + LOGW("TURN: ChannelData for unknown channel 0x%04x", channel); + } + } else { + LOGW("TURN: ChannelData decode failed"); + } + return 0; + } else if (stun_probe(buf, ret) == 0) { + memcpy(stun_msg.buf, buf, ret); + stun_msg.size = ret; + stun_parse_msg_buf(&stun_msg); + LOGD("STUN RECV class=0x%02x method=0x%04x size=%d", stun_msg.stunclass, stun_msg.stunmethod, ret); + // Handle TURN Data Indication: unwrap DATA payload and feed to STUN processor + if (stun_msg.stunclass == STUN_CLASS_INDICATION && stun_msg.stunmethod == STUN_METHOD_DATA) { + LOGD("TURN: Data Indication received, data_len=%zu", stun_msg.data_len); + if (stun_msg.data_len > 0 && stun_probe(stun_msg.data, stun_msg.data_len) == 0) { + StunMessage inner; + memset(&inner, 0, sizeof(inner)); + memcpy(inner.buf, stun_msg.data, stun_msg.data_len); + inner.size = stun_msg.data_len; + stun_parse_msg_buf(&inner); + if (inner.stunclass == STUN_CLASS_RESPONSE) { + agent_process_stun_response(agent, &inner); + } else if (inner.stunclass == STUN_CLASS_REQUEST) { + // Process binding request from peer via TURN Data Indication + // Look up channel binding for this peer to route response via TURN + ChannelBinding* peer_binding = agent_channel_find_by_peer(agent, &stun_msg.peer_addr); + agent_process_stun_request(agent, &inner, &stun_msg.peer_addr, peer_binding); + } + } + return 0; + } + switch (stun_msg.stunclass) { + case STUN_CLASS_REQUEST: + // Direct request (not via TURN), respond directly + agent_process_stun_request(agent, &stun_msg, &addr, NULL); + break; + case STUN_CLASS_RESPONSE: + agent_process_stun_response(agent, &stun_msg); + break; + default: + break; + } + ret = 0; + } } return ret; } @@ -430,8 +1014,11 @@ void agent_set_remote_description(Agent* agent, char* description) { line_start = line_end + 2; } - LOGD("remote ufrag: %s", agent->remote_ufrag); - LOGD("remote upwd: %s", agent->remote_upwd); + // Avoid logging ICE passwords; log lengths instead (helps debug mismatched credentials). + LOGI("ICE creds: local_ufrag=%s remote_ufrag=%s remote_pwd_len=%zu", + agent->local_ufrag, + agent->remote_ufrag, + strlen(agent->remote_upwd)); } void agent_update_candidate_pairs(Agent* agent) { @@ -439,40 +1026,144 @@ void agent_update_candidate_pairs(Agent* agent) { // Please set gather candidates before set remote description for (i = 0; i < agent->local_candidates_count; i++) { for (j = 0; j < agent->remote_candidates_count; j++) { + // NOTE: We no longer skip "unreachable" pairs. Even though sending from + // a public IP to a remote private IP seems futile, the act of sending + // establishes NAT/firewall state and triggers the remote peer's ICE agent. + // aiortc sends to ALL candidates including private host addresses, and + // this is required for some ICE implementations (like Pipecat/Daily) to respond. if (agent->local_candidates[i].addr.family == agent->remote_candidates[j].addr.family) { agent->candidate_pairs[agent->candidate_pairs_num].local = &agent->local_candidates[i]; agent->candidate_pairs[agent->candidate_pairs_num].remote = &agent->remote_candidates[j]; agent->candidate_pairs[agent->candidate_pairs_num].priority = agent->local_candidates[i].priority + agent->remote_candidates[j].priority; agent->candidate_pairs[agent->candidate_pairs_num].state = ICE_CANDIDATE_STATE_FROZEN; + // Debug: log each candidate pair with types and priority + LOGI("ICE: Pair[%d] local_type=%d remote_type=%d priority=%" PRIu64 " (local_pri=%" PRIu32 " + remote_pri=%" PRIu32 ")", + agent->candidate_pairs_num, + agent->local_candidates[i].type, + agent->remote_candidates[j].type, + agent->candidate_pairs[agent->candidate_pairs_num].priority, + agent->local_candidates[i].priority, + agent->remote_candidates[j].priority); agent->candidate_pairs_num++; } } } - LOGD("candidate pairs num: %d", agent->candidate_pairs_num); + LOGI("ICE: %d candidate pairs created", agent->candidate_pairs_num); + // Log summary by type for quick analysis + int host_pairs = 0, srflx_pairs = 0, relay_pairs = 0; + for (i = 0; i < agent->candidate_pairs_num; i++) { + if (agent->candidate_pairs[i].local->type == ICE_CANDIDATE_TYPE_RELAY || + agent->candidate_pairs[i].remote->type == ICE_CANDIDATE_TYPE_RELAY) { + relay_pairs++; + } else if (agent->candidate_pairs[i].local->type == ICE_CANDIDATE_TYPE_SRFLX || + agent->candidate_pairs[i].remote->type == ICE_CANDIDATE_TYPE_SRFLX) { + srflx_pairs++; + } else { + host_pairs++; + } + } + LOGI("ICE: Pair types: host=%d srflx=%d relay=%d", host_pairs, srflx_pairs, relay_pairs); + + // NOTE: We intentionally do NOT pre-create TURN permissions here. + // aiortc and other WebRTC implementations start ICE checks immediately without + // pre-creating permissions. Permissions are created lazily only when we need + // to send via our own TURN relay (ChannelData). For direct sends to the remote's + // relay address, no permission on our TURN server is needed. + // Pre-creating permissions adds delay and can cause Pipecat to time out. } -int agent_connectivity_check(Agent* agent) { - char addr_string[ADDRSTRLEN]; +int agent_connectivity_check(Agent* agent, int is_heartbeat) { uint8_t buf[1400]; StunMessage msg; + static int s_logged_first_binding_request = 0; - if (agent->nominated_pair->state != ICE_CANDIDATE_STATE_INPROGRESS) { - LOGI("nominated pair is not in progress"); + // For heartbeat mode, we don't need a nominated pair in progress + if (!is_heartbeat && agent->nominated_pair->state != ICE_CANDIDATE_STATE_INPROGRESS) { return -1; } memset(&msg, 0, sizeof(msg)); - if (agent->nominated_pair->conncheck % AGENT_CONNCHECK_PERIOD == 0) { - addr_to_string(&agent->nominated_pair->remote->addr, addr_string, sizeof(addr_string)); - LOGD("send binding request to remote ip: %s, port: %d", addr_string, agent->nominated_pair->remote->addr.port); - agent_create_binding_request(agent, &msg); - agent_socket_send(agent, &agent->nominated_pair->remote->addr, msg.buf, msg.size); + if (is_heartbeat || (agent->nominated_pair->conncheck % AGENT_CONNCHECK_PERIOD == 0)) { + agent_create_binding_request(agent, &msg, is_heartbeat); + + if (!s_logged_first_binding_request) { + // Log the first Binding Request we generate, to verify ICE credentials & key fields. + // Keep it compact to avoid log spam. + StunMessage dbg; + memset(&dbg, 0, sizeof(dbg)); + memcpy(dbg.buf, msg.buf, msg.size); + dbg.size = msg.size; + stun_parse_msg_buf(&dbg); + + LOGI("ICE: BindingRequest dbg: class=0x%02x method=0x%04x size=%zu username_len=%zu", + dbg.stunclass, dbg.stunmethod, dbg.size, dbg.username_len); + LOGI("ICE: BindingRequest dbg: username='%s'", dbg.username); + LOGI("ICE: BindingRequest dbg: local_candidate_priority=%" PRIu32, agent->nominated_pair->local->priority); + LOGI("ICE: BindingRequest dbg: agent_mode=%d (0=CONTROLLED,1=CONTROLLING)", agent->mode); + + // Hex dump first 48 bytes for debugging STUN message format + char hexdump[150]; + int hlen = 0; + for (size_t i = 0; i < 48 && i < msg.size; i++) { + hlen += snprintf(hexdump + hlen, sizeof(hexdump) - hlen, "%02x ", (unsigned char)msg.buf[i]); + } + LOGI("ICE: BindingRequest hex (first 48): %s", hexdump); + + s_logged_first_binding_request = 1; + } + + int sent = 0; + if (agent->nominated_pair->local->type == ICE_CANDIDATE_TYPE_RELAY) { + ChannelBinding* binding = NULL; + uint8_t channel_buf[sizeof(msg.buf) + 4]; + if (agent_channel_allocate(agent, &agent->nominated_pair->remote->addr, &binding) == 0) { + if (!binding->bound) { + if (agent_turn_channel_bind(agent, binding) < 0) { + LOGE("TURN: ChannelBind failed"); + sent = -1; + } + } + if (binding->bound) { + int encoded = agent_channel_data_encode(binding->channel_number, msg.buf, msg.size, channel_buf, sizeof(channel_buf)); + if (encoded < 0) { + LOGE("TURN: ChannelData encode failed"); + sent = -1; + } else { + sent = agent_socket_send(agent, &agent->turn_server_addr, channel_buf, encoded); + LOGD("TURN: ChannelData SEND ch=0x%04x len=%d sent=%d", binding->channel_number, encoded, sent); + } + } + } else { + sent = -1; + } + } else { + // Direct binding request (host/srflx/relay candidate) - use separate ICE socket + // Using the ICE socket (different from TURN socket) avoids NAT/firewall issues + // where Cloudflare may reject non-TURN traffic on the TURN-bound socket. + char addr_str[64]; + addr_to_string(&agent->nominated_pair->remote->addr, addr_str, sizeof(addr_str)); + LOGD("ICE: Direct SEND to %s:%u (type=%d)", + addr_str, + (unsigned)agent->nominated_pair->remote->addr.port, + agent->nominated_pair->local->type); + sent = agent_ice_socket_send(agent, &agent->nominated_pair->remote->addr, msg.buf, msg.size); + } + if (sent < 0) { + LOGE("ICE: Failed to send binding request"); + } } - agent_recv(agent, buf, sizeof(buf)); + // Only poll for binding response during initial connectivity checks. + // During heartbeat, skip agent_recv to avoid consuming RTP/RTCP/DTLS packets + // that should be processed by peer_connection_loop. The heartbeat binding + // response will be picked up by the main agent_recv in the next loop iteration. + if (!is_heartbeat) { + agent_recv(agent, buf, sizeof(buf)); + } if (agent->nominated_pair->state == ICE_CANDIDATE_STATE_SUCCEEDED) { + LOGD("ICE: Connected"); agent->selected_pair = agent->nominated_pair; return 0; } @@ -482,25 +1173,125 @@ int agent_connectivity_check(Agent* agent) { int agent_select_candidate_pair(Agent* agent) { int i; + + // If we already have a pair in progress, continue it. for (i = 0; i < agent->candidate_pairs_num; i++) { - if (agent->candidate_pairs[i].state == ICE_CANDIDATE_STATE_FROZEN) { - // nominate this pair - agent->nominated_pair = &agent->candidate_pairs[i]; - agent->candidate_pairs[i].conncheck = 0; - agent->candidate_pairs[i].state = ICE_CANDIDATE_STATE_INPROGRESS; - return 0; - } else if (agent->candidate_pairs[i].state == ICE_CANDIDATE_STATE_INPROGRESS) { + if (agent->candidate_pairs[i].state == ICE_CANDIDATE_STATE_INPROGRESS) { agent->candidate_pairs[i].conncheck++; if (agent->candidate_pairs[i].conncheck < AGENT_CONNCHECK_MAX) { return 0; } agent->candidate_pairs[i].state = ICE_CANDIDATE_STATE_FAILED; - } else if (agent->candidate_pairs[i].state == ICE_CANDIDATE_STATE_FAILED) { - } else if (agent->candidate_pairs[i].state == ICE_CANDIDATE_STATE_SUCCEEDED) { + break; + } + if (agent->candidate_pairs[i].state == ICE_CANDIDATE_STATE_SUCCEEDED) { agent->selected_pair = &agent->candidate_pairs[i]; return 0; } } + + // Choose next pair. If prefer_relay is set, try relay→relay pairs first. + // This is essential for cloud services like Pipecat where NAT/firewall rules + // often block direct connectivity but relay→relay always works. + int chosen_idx = -1; + uint64_t best_priority = 0; + + if (agent->prefer_relay) { + // For cloud services: aiortc sends to ALL candidate types including remote HOST. + // Even though remote host addresses (172.31.x.x) are private and "unreachable", + // sending to them triggers the remote peer's ICE agent to start responding. + // + // Strategy: Try host→host FIRST (triggers response), then srflx, then relay + + // First pass: host→host pairs (critical for triggering Pipecat's response) + for (i = 0; i < agent->candidate_pairs_num; i++) { + if (agent->candidate_pairs[i].state != ICE_CANDIDATE_STATE_FROZEN) continue; + if (agent->candidate_pairs[i].remote->type == ICE_CANDIDATE_TYPE_HOST && + agent->candidate_pairs[i].local->type == ICE_CANDIDATE_TYPE_HOST) { + if (chosen_idx < 0 || agent->candidate_pairs[i].priority > best_priority) { + chosen_idx = i; + best_priority = agent->candidate_pairs[i].priority; + } + } + } + if (chosen_idx >= 0) { + LOGI("ICE: prefer_relay mode - trying host→host first (triggers remote ICE)"); + } + + // Second pass: host→srflx pairs (establishes our IP in Pipecat's view) + if (chosen_idx < 0) { + for (i = 0; i < agent->candidate_pairs_num; i++) { + if (agent->candidate_pairs[i].state != ICE_CANDIDATE_STATE_FROZEN) continue; + if (agent->candidate_pairs[i].remote->type == ICE_CANDIDATE_TYPE_SRFLX && + agent->candidate_pairs[i].local->type == ICE_CANDIDATE_TYPE_HOST) { + if (chosen_idx < 0 || agent->candidate_pairs[i].priority > best_priority) { + chosen_idx = i; + best_priority = agent->candidate_pairs[i].priority; + } + } + } + if (chosen_idx >= 0) { + LOGI("ICE: prefer_relay mode - trying host→srflx (to establish path)"); + } + } + + // Second pass: host/srflx→relay pairs (direct send to remote relay) + if (chosen_idx < 0) { + for (i = 0; i < agent->candidate_pairs_num; i++) { + if (agent->candidate_pairs[i].state != ICE_CANDIDATE_STATE_FROZEN) continue; + if (agent->candidate_pairs[i].remote->type == ICE_CANDIDATE_TYPE_RELAY && + agent->candidate_pairs[i].local->type != ICE_CANDIDATE_TYPE_RELAY) { + if (chosen_idx < 0 || agent->candidate_pairs[i].priority > best_priority) { + chosen_idx = i; + best_priority = agent->candidate_pairs[i].priority; + } + } + } + if (chosen_idx >= 0) { + LOGI("ICE: prefer_relay mode - using host/srflx→relay pair (direct to remote relay)"); + } + } + + // Third pass: try relay→relay if nothing else works + if (chosen_idx < 0) { + for (i = 0; i < agent->candidate_pairs_num; i++) { + if (agent->candidate_pairs[i].state != ICE_CANDIDATE_STATE_FROZEN) continue; + if (agent->candidate_pairs[i].local->type == ICE_CANDIDATE_TYPE_RELAY && + agent->candidate_pairs[i].remote->type == ICE_CANDIDATE_TYPE_RELAY) { + if (chosen_idx < 0 || agent->candidate_pairs[i].priority > best_priority) { + chosen_idx = i; + best_priority = agent->candidate_pairs[i].priority; + } + } + } + if (chosen_idx >= 0) { + LOGI("ICE: prefer_relay mode - using relay→relay pair"); + } + } + } + + // Standard pass: choose by highest ICE priority (host > srflx > relay) + if (chosen_idx < 0) { + for (i = 0; i < agent->candidate_pairs_num; i++) { + if (agent->candidate_pairs[i].state != ICE_CANDIDATE_STATE_FROZEN) continue; + if (chosen_idx < 0 || agent->candidate_pairs[i].priority > best_priority) { + chosen_idx = i; + best_priority = agent->candidate_pairs[i].priority; + } + } + } + + if (chosen_idx >= 0) { + agent->nominated_pair = &agent->candidate_pairs[chosen_idx]; + agent->candidate_pairs[chosen_idx].conncheck = 0; + agent->candidate_pairs[chosen_idx].state = ICE_CANDIDATE_STATE_INPROGRESS; + LOGI("ICE: Checking pair %d (priority=%" PRIu64 ", local_type=%d remote_type=%d)", chosen_idx, + agent->candidate_pairs[chosen_idx].priority, + agent->candidate_pairs[chosen_idx].local->type, agent->candidate_pairs[chosen_idx].remote->type); + return 0; + } + // all candidate pairs are failed + LOGE("ICE: All %d candidate pairs FAILED! (check if any responses were received)", agent->candidate_pairs_num); return -1; } diff --git a/src/agent.h b/src/agent.h index 382d5a4e..8ad7963c 100644 --- a/src/agent.h +++ b/src/agent.h @@ -24,6 +24,13 @@ #define AGENT_MAX_CANDIDATE_PAIRS 100 #endif +#ifndef AGENT_MAX_CHANNEL_BINDINGS +#define AGENT_MAX_CHANNEL_BINDINGS 8 +#endif + +#define CHANNEL_NUMBER_MIN 0x4000 +#define CHANNEL_NUMBER_MAX 0x7FFF + typedef enum AgentState { AGENT_STATE_GATHERING_ENDED = 0, @@ -41,6 +48,12 @@ typedef enum AgentMode { typedef struct Agent Agent; +typedef struct { + uint16_t channel_number; // 0 = unused, else 0x4000-0x7FFF + Address peer_addr; + int bound; // 1 once ChannelBind succeeds +} ChannelBinding; + struct Agent { char remote_ufrag[ICE_UFRAG_LENGTH + 1]; char remote_upwd[ICE_UPWD_LENGTH + 1]; @@ -56,6 +69,11 @@ struct Agent { UdpSocket udp_sockets[2]; + // Separate socket for direct ICE connectivity checks. + // Using a different socket than TURN avoids NAT/firewall issues where + // Cloudflare may reject non-TURN traffic on the TURN-bound socket. + UdpSocket ice_socket; + Address host_addr; int b_host_addr; uint64_t binding_request_time; @@ -70,6 +88,26 @@ struct Agent { int candidate_pairs_num; int use_candidate; uint32_t transaction_id[3]; + + // TURN allocation context + Address turn_server_addr; + int has_turn_allocation; + char turn_username[256]; + size_t turn_username_len; + char turn_credential[256]; + size_t turn_credential_len; + char turn_realm[128]; + size_t turn_realm_len; + char turn_nonce[192]; + size_t turn_nonce_len; + + // TURN Channel Binding state + ChannelBinding channel_bindings[AGENT_MAX_CHANNEL_BINDINGS]; + uint16_t next_channel_number; + + // Relay-first mode: prioritize relay→relay pairs for cloud services + // When set, relay pairs are tried before host/srflx pairs + int prefer_relay; }; void agent_gather_candidate(Agent* agent, const char* urls, const char* username, const char* credential); @@ -86,7 +124,7 @@ void agent_set_remote_description(Agent* agent, char* description); int agent_select_candidate_pair(Agent* agent); -int agent_connectivity_check(Agent* agent); +int agent_connectivity_check(Agent* agent, int is_heartbeat); void agent_clear_candidates(Agent* agent); @@ -96,4 +134,21 @@ void agent_destroy(Agent* agent); void agent_update_candidate_pairs(Agent* agent); +// Exposed for unit testing ChannelData helpers +int agent_channel_data_encode(uint16_t channel, const uint8_t* payload, size_t payload_len, uint8_t* out, size_t out_size); +int agent_channel_data_decode(const uint8_t* data, size_t data_len, uint16_t* channel, const uint8_t** payload, size_t* payload_len); + +// Build a TURN ChannelBind request (no network I/O). Exposed for tests. +int agent_build_channel_bind_request(Agent* agent, ChannelBinding* binding, StunMessage* out_msg); + +// Build a STUN Binding Request (no network I/O). Exposed for tests. +// is_heartbeat=1: consent freshness check (no USE-CANDIDATE), is_heartbeat=0: nomination check. +void agent_create_binding_request(Agent* agent, StunMessage* msg, int is_heartbeat); + +// Channel state management - exposed for unit testing +void agent_channel_init(Agent* agent); +ChannelBinding* agent_channel_find_by_peer(Agent* agent, const Address* peer_addr); +ChannelBinding* agent_channel_find_by_channel(Agent* agent, uint16_t channel); +int agent_channel_allocate(Agent* agent, const Address* peer_addr, ChannelBinding** out_binding); + #endif // AGENT_H_ diff --git a/src/config.h b/src/config.h index 94905a83..c13e2a0a 100644 --- a/src/config.h +++ b/src/config.h @@ -2,7 +2,7 @@ #define CONFIG_H_ // uncomment this if you want to handshake with a aiortc -// #define CONFIG_DTLS_USE_ECDSA 1 +#define CONFIG_DTLS_USE_ECDSA 1 #define SCTP_MTU (1200) #define CONFIG_MTU (1300) @@ -66,9 +66,7 @@ #define CONFIG_IFACE_PREFIX "" // #define LOG_LEVEL LEVEL_DEBUG -#ifndef LOG_REDIRECT #define LOG_REDIRECT 0 -#endif // Disable MQTT and HTTP signaling // #define DISABLE_PEER_SIGNALING 1 diff --git a/src/dtls_srtp.c b/src/dtls_srtp.c index dd546169..9781f928 100644 --- a/src/dtls_srtp.c +++ b/src/dtls_srtp.c @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -31,9 +32,16 @@ int dtls_srtp_udp_recv(void* ctx, uint8_t* buf, size_t len) { UdpSocket* udp_socket = (UdpSocket*)dtls_srtp->user_data; int ret; + int timeout_ms = 5000; // 5 second timeout to prevent hanging + int elapsed_ms = 0; while ((ret = udp_socket_recvfrom(udp_socket, &udp_socket->bind_addr, buf, len)) <= 0) { - ports_sleep_ms(1); + ports_sleep_ms(10); + elapsed_ms += 10; + if (elapsed_ms >= timeout_ms) { + LOGW("dtls_srtp_udp_recv timeout after %d ms", elapsed_ms); + return MBEDTLS_ERR_SSL_TIMEOUT; + } } LOGD("dtls_srtp_udp_recv (%d)", ret); @@ -128,9 +136,21 @@ static int dtls_srtp_selfsign_cert(DtlsSrtp* dtls_srtp) { if (ret < 0) { LOGE("mbedtls_x509write_crt_pem failed -0x%.4x", (unsigned int)-ret); + mbedtls_x509write_crt_free(&crt); + free(cert_buf); + return ret; } - mbedtls_x509_crt_parse(&dtls_srtp->cert, cert_buf, 2 * RSA_KEY_LENGTH); + // Parse the generated PEM certificate - use strlen to get actual PEM length + size_t cert_len = strlen((char*)cert_buf) + 1; // Include null terminator for PEM + ret = mbedtls_x509_crt_parse(&dtls_srtp->cert, cert_buf, cert_len); + if (ret != 0) { + LOGE("mbedtls_x509_crt_parse failed -0x%.4x", (unsigned int)-ret); + mbedtls_x509write_crt_free(&crt); + free(cert_buf); + return ret; + } + LOGD("Self-signed certificate generated (len=%zu)", cert_len); mbedtls_x509write_crt_free(&crt); @@ -146,6 +166,7 @@ static void dtls_srtp_debug(void* ctx, int level, const char* file, int line, co #endif int dtls_srtp_init(DtlsSrtp* dtls_srtp, DtlsSrtpRole role, void* user_data) { + int ret; static const mbedtls_ssl_srtp_profile default_profiles[] = { MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80, MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_32, @@ -153,72 +174,150 @@ int dtls_srtp_init(DtlsSrtp* dtls_srtp, DtlsSrtpRole role, void* user_data) { MBEDTLS_TLS_SRTP_NULL_HMAC_SHA1_32, MBEDTLS_TLS_SRTP_UNSET}; + // Initialize libsrtp once (required before any srtp_create calls) + // Note: srtp_init() may be called multiple times if peer_init() was called first + // Error code 2 means already initialized, which is fine + static bool srtp_initialized = false; + if (!srtp_initialized) { + srtp_err_status_t srtp_err = srtp_init(); + if (srtp_err != srtp_err_status_ok && srtp_err != 2) { // 2 = already initialized + LOGE("srtp_init() failed: %d", (int)srtp_err); + return -1; + } + if (srtp_err == 2) { + LOGD("libsrtp already initialized (by peer_init)"); + } else { + LOGD("libsrtp initialized"); + } + srtp_initialized = true; + } + dtls_srtp->role = role; dtls_srtp->state = DTLS_SRTP_STATE_INIT; dtls_srtp->user_data = user_data; dtls_srtp->udp_send = dtls_srtp_udp_send; dtls_srtp->udp_recv = dtls_srtp_udp_recv; + + // Initialize SRTP pointers to NULL + dtls_srtp->srtp_in = NULL; + dtls_srtp->srtp_out = NULL; + // Initialize all mbedtls structures first mbedtls_ssl_config_init(&dtls_srtp->conf); mbedtls_ssl_init(&dtls_srtp->ssl); - mbedtls_x509_crt_init(&dtls_srtp->cert); mbedtls_pk_init(&dtls_srtp->pkey); mbedtls_entropy_init(&dtls_srtp->entropy); mbedtls_ctr_drbg_init(&dtls_srtp->ctr_drbg); + #if CONFIG_MBEDTLS_DEBUG mbedtls_debug_set_threshold(3); - mbedtls_ssl_conf_dbg(&dtls_srtp->conf, dtls_srtp_debug, NULL); #endif - dtls_srtp_selfsign_cert(dtls_srtp); - - mbedtls_ssl_conf_verify(&dtls_srtp->conf, dtls_srtp_cert_verify, NULL); - - mbedtls_ssl_conf_authmode(&dtls_srtp->conf, MBEDTLS_SSL_VERIFY_REQUIRED); - - mbedtls_ssl_conf_ca_chain(&dtls_srtp->conf, &dtls_srtp->cert, NULL); - mbedtls_ssl_conf_own_cert(&dtls_srtp->conf, &dtls_srtp->cert, &dtls_srtp->pkey); - - mbedtls_ssl_conf_rng(&dtls_srtp->conf, mbedtls_ctr_drbg_random, &dtls_srtp->ctr_drbg); - - mbedtls_ssl_conf_read_timeout(&dtls_srtp->conf, 1000); + // Generate self-signed certificate BEFORE setting up SSL config + ret = dtls_srtp_selfsign_cert(dtls_srtp); + if (ret != 0) { + LOGE("Failed to generate self-signed certificate: -0x%.4x", (unsigned int)-ret); + return ret; + } + // Set up SSL config defaults FIRST (this resets many settings) if (dtls_srtp->role == DTLS_SRTP_ROLE_SERVER) { - mbedtls_ssl_config_defaults(&dtls_srtp->conf, + ret = mbedtls_ssl_config_defaults(&dtls_srtp->conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_DATAGRAM, MBEDTLS_SSL_PRESET_DEFAULT); + if (ret != 0) { + LOGE("mbedtls_ssl_config_defaults (server) failed: -0x%.4x", (unsigned int)-ret); + return ret; + } mbedtls_ssl_cookie_init(&dtls_srtp->cookie_ctx); - mbedtls_ssl_cookie_setup(&dtls_srtp->cookie_ctx, mbedtls_ctr_drbg_random, &dtls_srtp->ctr_drbg); - mbedtls_ssl_conf_dtls_cookies(&dtls_srtp->conf, mbedtls_ssl_cookie_write, mbedtls_ssl_cookie_check, &dtls_srtp->cookie_ctx); - + LOGD("DTLS-SRTP: SERVER mode"); } else { - mbedtls_ssl_config_defaults(&dtls_srtp->conf, + ret = mbedtls_ssl_config_defaults(&dtls_srtp->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_DATAGRAM, MBEDTLS_SSL_PRESET_DEFAULT); + if (ret != 0) { + LOGE("mbedtls_ssl_config_defaults (client) failed: -0x%.4x", (unsigned int)-ret); + return ret; + } + LOGD("DTLS-SRTP: CLIENT mode"); } - dtls_srtp_x509_digest(&dtls_srtp->cert, dtls_srtp->local_fingerprint); + // Now configure SSL settings AFTER defaults are set +#if CONFIG_MBEDTLS_DEBUG + mbedtls_ssl_conf_dbg(&dtls_srtp->conf, dtls_srtp_debug, NULL); +#endif - LOGD("local fingerprint: %s", dtls_srtp->local_fingerprint); + mbedtls_ssl_conf_verify(&dtls_srtp->conf, dtls_srtp_cert_verify, NULL); + mbedtls_ssl_conf_authmode(&dtls_srtp->conf, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&dtls_srtp->conf, &dtls_srtp->cert, NULL); - mbedtls_ssl_conf_dtls_srtp_protection_profiles(&dtls_srtp->conf, default_profiles); + ret = mbedtls_ssl_conf_own_cert(&dtls_srtp->conf, &dtls_srtp->cert, &dtls_srtp->pkey); + if (ret != 0) { + LOGE("mbedtls_ssl_conf_own_cert failed: -0x%.4x", (unsigned int)-ret); + return ret; + } - mbedtls_ssl_conf_srtp_mki_value_supported(&dtls_srtp->conf, MBEDTLS_SSL_DTLS_SRTP_MKI_UNSUPPORTED); + mbedtls_ssl_conf_rng(&dtls_srtp->conf, mbedtls_ctr_drbg_random, &dtls_srtp->ctr_drbg); + mbedtls_ssl_conf_read_timeout(&dtls_srtp->conf, 1000); + // Generate fingerprint from our certificate + dtls_srtp_x509_digest(&dtls_srtp->cert, dtls_srtp->local_fingerprint); + LOGD("DTLS fingerprint: %s", dtls_srtp->local_fingerprint); + + // Configure SRTP profiles + mbedtls_ssl_conf_dtls_srtp_protection_profiles(&dtls_srtp->conf, default_profiles); + mbedtls_ssl_conf_srtp_mki_value_supported(&dtls_srtp->conf, MBEDTLS_SSL_DTLS_SRTP_MKI_UNSUPPORTED); mbedtls_ssl_conf_cert_req_ca_list(&dtls_srtp->conf, MBEDTLS_SSL_CERT_REQ_CA_LIST_DISABLED); - mbedtls_ssl_setup(&dtls_srtp->ssl, &dtls_srtp->conf); + // Configure cipher suites for aiortc compatibility + // aiortc ONLY accepts ECDHE-ECDSA-* ciphers, in this order: + // - ECDHE-ECDSA-AES128-GCM-SHA256 + // - ECDHE-ECDSA-CHACHA20-POLY1305 + // - ECDHE-ECDSA-AES128-SHA + // - ECDHE-ECDSA-AES256-SHA + static const int aiortc_ciphersuites[] = { + MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, +#ifdef MBEDTLS_TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + MBEDTLS_TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, +#endif + MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + 0 // Must be 0-terminated + }; + mbedtls_ssl_conf_ciphersuites(&dtls_srtp->conf, aiortc_ciphersuites); + LOGI("DTLS: Configured %d aiortc-compatible cipher suites", + (int)(sizeof(aiortc_ciphersuites)/sizeof(aiortc_ciphersuites[0]) - 1)); + + // Finally, set up the SSL context with our config + ret = mbedtls_ssl_setup(&dtls_srtp->ssl, &dtls_srtp->conf); + if (ret != 0) { + LOGE("mbedtls_ssl_setup failed: -0x%.4x", (unsigned int)-ret); + return ret; + } return 0; } void dtls_srtp_deinit(DtlsSrtp* dtls_srtp) { + // Send close_notify to peer if we have an established connection + // This allows the peer to cleanly close its side + if (dtls_srtp->state == DTLS_SRTP_STATE_CONNECTED) { + int ret; + // Try to send close_notify (best effort, ignore errors) + do { + ret = mbedtls_ssl_close_notify(&dtls_srtp->ssl); + } while (ret == MBEDTLS_ERR_SSL_WANT_WRITE); + + srtp_dealloc(dtls_srtp->srtp_in); + srtp_dealloc(dtls_srtp->srtp_out); + } + mbedtls_ssl_free(&dtls_srtp->ssl); mbedtls_ssl_config_free(&dtls_srtp->conf); @@ -230,17 +329,13 @@ void dtls_srtp_deinit(DtlsSrtp* dtls_srtp) { if (dtls_srtp->role == DTLS_SRTP_ROLE_SERVER) { mbedtls_ssl_cookie_free(&dtls_srtp->cookie_ctx); } - - if (dtls_srtp->state == DTLS_SRTP_STATE_CONNECTED) { - srtp_dealloc(dtls_srtp->srtp_in); - srtp_dealloc(dtls_srtp->srtp_out); - } } static int dtls_srtp_key_derivation(DtlsSrtp* dtls_srtp, const unsigned char* master_secret, size_t secret_len, const unsigned char* randbytes, size_t randbytes_len, mbedtls_tls_prf_types tls_prf_type) { int ret; const char* dtls_srtp_label = "EXTRACTOR-dtls_srtp"; uint8_t key_material[DTLS_SRTP_KEY_MATERIAL_LENGTH]; + // Export keying material if ((ret = mbedtls_ssl_tls_prf(tls_prf_type, master_secret, secret_len, dtls_srtp_label, randbytes, randbytes_len, key_material, sizeof(key_material))) != 0) { @@ -248,6 +343,8 @@ static int dtls_srtp_key_derivation(DtlsSrtp* dtls_srtp, const unsigned char* ma return ret; } + LOGI("DTLS-SRTP: Key material derived (%zu bytes from %zu byte secret)", sizeof(key_material), secret_len); + #if 0 int i, j; printf(" DTLS-SRTP key material is:"); @@ -300,12 +397,15 @@ static int dtls_srtp_key_derivation(DtlsSrtp* dtls_srtp, const unsigned char* ma dtls_srtp->remote_policy.key = dtls_srtp->remote_policy_key; dtls_srtp->remote_policy.next = NULL; - if (srtp_create(&dtls_srtp->srtp_in, &dtls_srtp->remote_policy) != srtp_err_status_ok) { - LOGD("Error creating inbound SRTP session for component"); + srtp_err_status_t srtp_err; + + srtp_err = srtp_create(&dtls_srtp->srtp_in, &dtls_srtp->remote_policy); + if (srtp_err != srtp_err_status_ok) { + LOGE("Error creating inbound SRTP session: %d", (int)srtp_err); return -1; } - LOGI("Created inbound SRTP session"); + LOGI("SRTP inbound session created (ssrc_any_inbound)"); // derive outbounds keys memset(&dtls_srtp->local_policy, 0, sizeof(dtls_srtp->local_policy)); @@ -320,12 +420,15 @@ static int dtls_srtp_key_derivation(DtlsSrtp* dtls_srtp, const unsigned char* ma dtls_srtp->local_policy.key = dtls_srtp->local_policy_key; dtls_srtp->local_policy.next = NULL; - if (srtp_create(&dtls_srtp->srtp_out, &dtls_srtp->local_policy) != srtp_err_status_ok) { - LOGE("Error creating outbound SRTP session"); + srtp_err = srtp_create(&dtls_srtp->srtp_out, &dtls_srtp->local_policy); + if (srtp_err != srtp_err_status_ok) { + LOGE("Error creating outbound SRTP session: %d", (int)srtp_err); + srtp_dealloc(dtls_srtp->srtp_in); // Clean up inbound session + dtls_srtp->srtp_in = NULL; return -1; } - LOGI("Created outbound SRTP session"); + LOGI("SRTP outbound session created (ssrc_any_outbound)"); dtls_srtp->state = DTLS_SRTP_STATE_CONNECTED; return 0; } @@ -351,6 +454,9 @@ static void dtls_srtp_key_derivation_cb(void* context, #endif DtlsSrtp* dtls_srtp = (DtlsSrtp*)context; + LOGI("DTLS key export callback invoked (secret_type=%d, secret_len=%zu)", + (int)secret_type, secret_len); + unsigned char master_secret[48]; unsigned char randbytes[64]; @@ -362,15 +468,22 @@ static void dtls_srtp_key_derivation_cb(void* context, return dtls_srtp_key_derivation(dtls_srtp, master_secret, sizeof(master_secret), randbytes, sizeof(randbytes), tls_prf_type); #else memcpy(master_secret, secret, sizeof(master_secret)); - dtls_srtp_key_derivation(dtls_srtp, master_secret, sizeof(master_secret), randbytes, sizeof(randbytes), tls_prf_type); + int ret = dtls_srtp_key_derivation(dtls_srtp, master_secret, sizeof(master_secret), randbytes, sizeof(randbytes), tls_prf_type); + if (ret != 0) { + LOGE("SRTP session creation failed: %d", ret); + } #endif } static int dtls_srtp_do_handshake(DtlsSrtp* dtls_srtp) { int ret; + int timeout_count = 0; + const int max_timeouts = 10; // Allow up to 10 timeout retries (30 seconds total at 3s each) static mbedtls_timing_delay_context timer; + LOGI("DTLS: do_handshake starting (role=%d)", dtls_srtp->role); + mbedtls_ssl_set_timer_cb(&dtls_srtp->ssl, &timer, mbedtls_timing_set_delay, mbedtls_timing_get_delay); #if CONFIG_MBEDTLS_2_X @@ -381,8 +494,21 @@ static int dtls_srtp_do_handshake(DtlsSrtp* dtls_srtp) { mbedtls_ssl_set_bio(&dtls_srtp->ssl, dtls_srtp, dtls_srtp->udp_send, dtls_srtp->udp_recv, NULL); + LOGI("DTLS: calling mbedtls_ssl_handshake..."); do { ret = mbedtls_ssl_handshake(&dtls_srtp->ssl); + LOGI("DTLS: mbedtls_ssl_handshake returned %d (0x%04x)", ret, (unsigned int)-ret); + + // Handle timeout - retry up to max_timeouts times + if (ret == MBEDTLS_ERR_SSL_TIMEOUT) { + timeout_count++; + if (timeout_count >= max_timeouts) { + LOGE("DTLS handshake timeout after %d retries", timeout_count); + break; + } + LOGD("DTLS handshake timeout, retrying (%d/%d)", timeout_count, max_timeouts); + continue; + } } while (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE); @@ -391,30 +517,40 @@ static int dtls_srtp_do_handshake(DtlsSrtp* dtls_srtp) { static int dtls_srtp_handshake_server(DtlsSrtp* dtls_srtp) { int ret; + int retry_count = 0; + const int max_retries = 5; // Limit retries to prevent infinite loops - while (1) { + while (retry_count < max_retries) { unsigned char client_ip[] = "test"; - mbedtls_ssl_session_reset(&dtls_srtp->ssl); + // Only reset session on retries after hello verify, not on first attempt + if (retry_count > 0) { + mbedtls_ssl_session_reset(&dtls_srtp->ssl); + } mbedtls_ssl_set_client_transport_id(&dtls_srtp->ssl, client_ip, sizeof(client_ip)); ret = dtls_srtp_do_handshake(dtls_srtp); if (ret == MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED) { - LOGD("DTLS hello verification requested"); + LOGD("DTLS hello verification requested, retrying (%d/%d)", retry_count + 1, max_retries); + retry_count++; + continue; } else if (ret != 0) { LOGE("failed! mbedtls_ssl_handshake returned -0x%.4x", (unsigned int)-ret); - break; } else { + LOGD("DTLS server handshake done"); break; } } - LOGD("DTLS server handshake done"); + if (retry_count >= max_retries) { + LOGE("DTLS handshake exceeded max retries"); + ret = -1; + } return ret; } @@ -507,17 +643,49 @@ int dtls_srtp_probe(uint8_t* buf) { } void dtls_srtp_decrypt_rtp_packet(DtlsSrtp* dtls_srtp, uint8_t* packet, int* bytes) { - srtp_unprotect(dtls_srtp->srtp_in, packet, bytes); + if (dtls_srtp->srtp_in == NULL) { + LOGE("Cannot decrypt RTP: SRTP session not initialized"); + *bytes = 0; + return; + } + + srtp_err_status_t status = srtp_unprotect(dtls_srtp->srtp_in, packet, bytes); + if (status != srtp_err_status_ok) { + LOGE("SRTP decrypt failed: status=%d", status); + *bytes = 0; + return; + } } void dtls_srtp_decrypt_rtcp_packet(DtlsSrtp* dtls_srtp, uint8_t* packet, int* bytes) { + if (dtls_srtp->srtp_in == NULL) { + LOGE("Cannot decrypt RTCP: SRTP session not initialized"); + *bytes = 0; + return; + } srtp_unprotect_rtcp(dtls_srtp->srtp_in, packet, bytes); } void dtls_srtp_encrypt_rtp_packet(DtlsSrtp* dtls_srtp, uint8_t* packet, int* bytes) { - srtp_protect(dtls_srtp->srtp_out, packet, bytes); + if (dtls_srtp->srtp_out == NULL) { + LOGE("Cannot encrypt RTP: SRTP session not initialized"); + *bytes = 0; + return; + } + + srtp_err_status_t status = srtp_protect(dtls_srtp->srtp_out, packet, bytes); + if (status != srtp_err_status_ok) { + LOGE("SRTP encrypt failed: status=%d", status); + *bytes = 0; + return; + } } void dtls_srtp_encrypt_rctp_packet(DtlsSrtp* dtls_srtp, uint8_t* packet, int* bytes) { + if (dtls_srtp->srtp_out == NULL) { + LOGE("Cannot encrypt RTCP: SRTP session not initialized"); + *bytes = 0; + return; + } srtp_protect_rtcp(dtls_srtp->srtp_out, packet, bytes); } diff --git a/src/ice.c b/src/ice.c index 756e961f..44520d2e 100644 --- a/src/ice.c +++ b/src/ice.c @@ -17,13 +17,31 @@ static uint8_t ice_candidate_type_preference(IceCandidateType type) { return 126; case ICE_CANDIDATE_TYPE_SRFLX: return 100; + case ICE_CANDIDATE_TYPE_PRFLX: + return 110; // RFC 8445: peer-reflexive between host and srflx case ICE_CANDIDATE_TYPE_RELAY: - return 0; + // Boosted from 0 to 50 to ensure relay candidates are tried earlier. + // RFC 8445 recommends lower priority for relay, but libpeer's sequential + // checking means relay pairs may never be reached if host/srflx all fail. + // With priority=50, relay pairs interleave with srflx (100) checks. + return 50; default: return 0; } } +// RFC 8445 §7.1.1: PRIORITY attribute in binding request must use peer-reflexive +// type preference, not the actual candidate type preference. +uint32_t ice_candidate_compute_prflx_priority(IceCandidate* candidate) { + // priority = (2^24)*(type preference) + (2^8)*(local preference) + (256 - component ID) + // Use PRFLX type preference (110) instead of actual candidate type + // Use maximum local_pref (65535) like aiortc - the prflx candidate doesn't exist yet + // so we use maximum preference for this hypothetical candidate + uint8_t prflx_type_pref = 110; + uint16_t local_pref = 65535; // aiortc uses max local preference + return (1 << 24) * prflx_type_pref + (1 << 8) * local_pref + (256 - candidate->component); +} + static uint16_t ice_candidate_local_preference(IceCandidate* candidate) { return candidate->addr.port; } @@ -43,7 +61,7 @@ void ice_candidate_create(IceCandidate* candidate, int foundation, IceCandidateT ice_candidate_priority(candidate); - snprintf(candidate->transport, sizeof(candidate->transport), "%s", "UDP"); + snprintf(candidate->transport, sizeof(candidate->transport), "%s", "udp"); } void ice_candidate_to_description(IceCandidate* candidate, char* description, int length) { @@ -60,8 +78,12 @@ void ice_candidate_to_description(IceCandidate* candidate, char* description, in case ICE_CANDIDATE_TYPE_SRFLX: snprintf(typ_raddr, sizeof(typ_raddr), "srflx raddr %s rport %d", addr_string, candidate->raddr.port); break; + case ICE_CANDIDATE_TYPE_PRFLX: + snprintf(typ_raddr, sizeof(typ_raddr), "prflx raddr %s rport %d", addr_string, candidate->raddr.port); + break; case ICE_CANDIDATE_TYPE_RELAY: snprintf(typ_raddr, sizeof(typ_raddr), "relay raddr %s rport %d", addr_string, candidate->raddr.port); + break; default: break; } @@ -118,15 +140,37 @@ int ice_candidate_from_description(IceCandidate* candidate, char* description, c return -1; } - addr_set_port(&candidate->addr, port); + // IMPORTANT: udp_socket_sendto() uses addr->sin/sin6 port from the sockaddr. + // So we must set IP/family first, then set the port via addr_set_port(). + memset(&candidate->addr, 0, sizeof(candidate->addr)); + memset(&candidate->raddr, 0, sizeof(candidate->raddr)); if (strstr(addrstring, "local") != NULL) { if (mdns_resolve_addr(addrstring, &candidate->addr) == 0) { LOGW("Failed to resolve mDNS address"); return -1; } - } else if (addr_from_string(addrstring, &candidate->addr) == 0) { - return -1; + } else { + if (addr_from_string(addrstring, &candidate->addr) == 0) { + return -1; + } + } + + addr_set_port(&candidate->addr, (uint16_t)port); + + // Parse optional base address (raddr/rport) for srflx/relay candidates. + // Example: + // a=candidate:... 1 udp ... 1.2.3.4 12345 typ srflx raddr 192.168.1.10 rport 56789 + char raddrstring[ADDRSTRLEN]; + uint32_t rport = 0; + memset(raddrstring, 0, sizeof(raddrstring)); + if (strstr(candidate_start, " raddr ") && strstr(candidate_start, " rport ")) { + if (sscanf(strstr(candidate_start, " raddr "), " raddr %s", raddrstring) == 1 && + sscanf(strstr(candidate_start, " rport "), " rport %" PRIu32, &rport) == 1) { + if (addr_from_string(raddrstring, &candidate->raddr) != 0) { + addr_set_port(&candidate->raddr, (uint16_t)rport); + } + } } return 0; diff --git a/src/ice.h b/src/ice.h index 2628a549..d804429c 100644 --- a/src/ice.h +++ b/src/ice.h @@ -60,4 +60,8 @@ int ice_candidate_from_description(IceCandidate* candidate, char* description, c int ice_candidate_get_local_address(IceCandidate* candidate, Address* address); +// RFC 8445 §7.1.1: Compute priority for PRIORITY attribute in binding requests +// Uses peer-reflexive type preference instead of actual candidate type +uint32_t ice_candidate_compute_prflx_priority(IceCandidate* candidate); + #endif // ICE_H_ diff --git a/src/peer.c b/src/peer.c index f24bf9df..88e97086 100644 --- a/src/peer.c +++ b/src/peer.c @@ -7,12 +7,21 @@ #include "sctp.h" #include "utils.h" +static int s_peer_initialized = 0; + int peer_init() { - if (srtp_init() != srtp_err_status_ok) { - LOGE("libsrtp init failed"); - return -1; + if (s_peer_initialized) { + LOGD("peer_init: already initialized"); + return 0; + } + + srtp_err_status_t srtp_ret = srtp_init(); + if (srtp_ret != srtp_err_status_ok) { + LOGE("libsrtp init failed: %d", srtp_ret); } sctp_usrsctp_init(); + s_peer_initialized = 1; + LOGI("peer_init: initialized SRTP and SCTP"); return 0; } diff --git a/src/peer_connection.c b/src/peer_connection.c index c763e806..f0a8ba6a 100644 --- a/src/peer_connection.c +++ b/src/peer_connection.c @@ -5,6 +5,7 @@ #include "agent.h" #include "config.h" +#include "ice.h" #include "dtls_srtp.h" #include "peer_connection.h" #include "ports.h" @@ -13,6 +14,31 @@ #include "sctp.h" #include "sdp.h" +// STUN heartbeat interval in seconds (from libpeer PR #205) +#define STUN_HEARTBEAT_INTERVAL 8000 // milliseconds (ports_get_epoch_time returns ms) + +// Track last heartbeat time for STUN keepalive +static uint64_t last_heartbeat_time = 0; + +// Diagnostic counters for COMPLETED state (helps debug silent audio) +static uint32_t diag_recv_calls = 0; +static uint32_t diag_recv_data = 0; +static uint32_t diag_rtp_packets = 0; +static uint32_t diag_rtcp_packets = 0; +static uint32_t diag_dtls_packets = 0; +static uint32_t diag_unknown_packets = 0; +static uint32_t diag_srtp_fail = 0; +static uint32_t diag_audio_decoded = 0; +static uint32_t diag_ssrc_mismatch = 0; +static uint64_t diag_last_log_time = 0; + +// Diagnostic counters for outgoing RTP (uplink send path) +static uint32_t diag_tx_rtp_packets = 0; +static uint32_t diag_tx_srtp_fail = 0; +static uint32_t diag_tx_send_fail = 0; +static uint32_t diag_tx_send_ok = 0; +static uint32_t diag_tx_bytes = 0; + #define STATE_CHANGED(pc, curr_state) \ if (pc->oniceconnectionstatechange && pc->state != curr_state) { \ pc->oniceconnectionstatechange(curr_state, pc->config.user_data); \ @@ -49,8 +75,29 @@ struct PeerConnection { static void peer_connection_outgoing_rtp_packet(uint8_t* data, size_t size, void* user_data) { PeerConnection* pc = (PeerConnection*)user_data; + size_t pre_encrypt_size = size; dtls_srtp_encrypt_rtp_packet(&pc->dtls_srtp, data, (int*)&size); - agent_send(&pc->agent, data, size); + diag_tx_rtp_packets++; + if (size == 0) { + diag_tx_srtp_fail++; + if (diag_tx_srtp_fail <= 5 || (diag_tx_srtp_fail % 100) == 0) { + LOGE("TX: SRTP encrypt failed (pre_size=%zu, count=%" PRIu32 ")", pre_encrypt_size, diag_tx_srtp_fail); + } + return; + } + int ret = agent_send(&pc->agent, data, size); + if (ret < 0) { + diag_tx_send_fail++; + if (diag_tx_send_fail <= 5 || (diag_tx_send_fail % 100) == 0) { + LOGE("TX: agent_send failed ret=%d size=%zu (count=%" PRIu32 ")", ret, size, diag_tx_send_fail); + } + } else { + diag_tx_send_ok++; + diag_tx_bytes += (uint32_t)size; + if (diag_tx_send_ok == 1) { + LOGI("TX: First RTP packet sent (%zu bytes, SRTP %zu->%zu)", size, pre_encrypt_size, size); + } + } } static int peer_connection_dtls_srtp_recv(void* ctx, unsigned char* buf, size_t len) { @@ -59,21 +106,40 @@ static int peer_connection_dtls_srtp_recv(void* ctx, unsigned char* buf, size_t DtlsSrtp* dtls_srtp = (DtlsSrtp*)ctx; PeerConnection* pc = (PeerConnection*)dtls_srtp->user_data; + // Check if we have buffered data from a previous recv if (pc->agent_ret > 0 && pc->agent_ret <= len) { memcpy(buf, pc->agent_buf, pc->agent_ret); return pc->agent_ret; } + // Poll for incoming data with timeout while (recv_max < CONFIG_TLS_READ_TIMEOUT && pc->state == PEER_CONNECTION_CONNECTED) { ret = agent_recv(&pc->agent, buf, len); if (ret > 0) { - break; + // Got non-STUN data (DTLS packet) + LOGD("DTLS recv: got %d bytes (first=0x%02x)", ret, buf[0]); + return ret; + } + // ret == 0 means STUN was processed, ret < 0 means no data + // Either way, keep polling + if (recv_max % 500 == 0) { + LOGD("DTLS recv: polling %d/%d", recv_max, CONFIG_TLS_READ_TIMEOUT); } recv_max++; } - return ret; + + // Timeout or state changed - return timeout error instead of 0 + // Returning 0 tells mbedtls the connection is closed, which is wrong + // Returning MBEDTLS_ERR_SSL_TIMEOUT tells it we timed out waiting for data + if (pc->state != PEER_CONNECTION_CONNECTED) { + LOGD("DTLS recv: state changed to %d, aborting", pc->state); + return MBEDTLS_ERR_SSL_CONN_EOF; + } + + LOGD("DTLS recv: timeout after %d polls", recv_max); + return MBEDTLS_ERR_SSL_TIMEOUT; } static int peer_connection_dtls_srtp_send(void* ctx, const uint8_t* buf, size_t len) { @@ -263,9 +329,6 @@ int peer_connection_create_datachannel_sid(PeerConnection* pc, DecpChannelType c uint16_t label_length = htons(strlen(label)); uint16_t protocol_length = htons(strlen(protocol)); char* msg = calloc(1, msg_size); - if (!msg) { - return rtrn; - } msg[0] = DATA_CHANNEL_OPEN; memcpy(msg + 2, &priority_big_endian, sizeof(uint16_t)); @@ -280,8 +343,20 @@ int peer_connection_create_datachannel_sid(PeerConnection* pc, DecpChannelType c return rtrn; } -static char* peer_connection_dtls_role_setup_value(DtlsSrtpRole d) { - return d == DTLS_SRTP_ROLE_SERVER ? "a=setup:passive" : "a=setup:active"; +/** + * Returns the SDP setup attribute value. + * + * Per RFC 8842: + * - Offerer MUST use "actpass" (can be either DTLS client or server) + * - Answerer picks "active" (DTLS client) or "passive" (DTLS server) + */ +static char* peer_connection_dtls_role_setup_value(DtlsSrtpRole role, SdpType sdp_type) { + if (sdp_type == SDP_TYPE_OFFER) { + // Offerer must always use actpass per RFC 8842 + return "a=setup:actpass"; + } + // Answerer picks active or passive based on role + return role == DTLS_SRTP_ROLE_SERVER ? "a=setup:passive" : "a=setup:active"; } int peer_connection_loop(PeerConnection* pc) { @@ -296,14 +371,17 @@ int peer_connection_loop(PeerConnection* pc) { case PEER_CONNECTION_CHECKING: if (agent_select_candidate_pair(&pc->agent) < 0) { STATE_CHANGED(pc, PEER_CONNECTION_FAILED); - } else if (agent_connectivity_check(&pc->agent) == 0) { + } else if (agent_connectivity_check(&pc->agent, 0) == 0) { // 0 = normal connectivity check STATE_CHANGED(pc, PEER_CONNECTION_CONNECTED); } break; - case PEER_CONNECTION_CONNECTED: - - if (dtls_srtp_handshake(&pc->dtls_srtp, NULL) == 0) { + case PEER_CONNECTION_CONNECTED: { + LOGI("DTLS: Starting handshake (role=%s)", + pc->dtls_srtp.role == DTLS_SRTP_ROLE_SERVER ? "SERVER/passive" : "CLIENT/active"); + int dtls_ret = dtls_srtp_handshake(&pc->dtls_srtp, NULL); + LOGI("DTLS: Handshake returned %d", dtls_ret); + if (dtls_ret == 0) { LOGD("DTLS-SRTP handshake done"); if (pc->config.datachannel) { @@ -312,19 +390,47 @@ int peer_connection_loop(PeerConnection* pc) { pc->sctp.userdata = pc->config.user_data; } + // Reset diagnostic counters on entering COMPLETED + diag_recv_calls = 0; + diag_recv_data = 0; + diag_rtp_packets = 0; + diag_rtcp_packets = 0; + diag_dtls_packets = 0; + diag_unknown_packets = 0; + diag_last_log_time = ports_get_epoch_time(); + LOGI("Entering COMPLETED: ice_fd=%d udp_fd=%d remote_assrc=%" PRIu32 " audio_codec=%d", + pc->agent.ice_socket.fd, pc->agent.udp_sockets[0].fd, + pc->remote_assrc, pc->config.audio_codec); STATE_CHANGED(pc, PEER_CONNECTION_COMPLETED); + } else { + LOGE("DTLS-SRTP handshake failed: %d, transitioning to FAILED", dtls_ret); + STATE_CHANGED(pc, PEER_CONNECTION_FAILED); } break; - case PEER_CONNECTION_COMPLETED: + } + case PEER_CONNECTION_COMPLETED: { + // Send STUN heartbeat every STUN_HEARTBEAT_INTERVAL seconds (from libpeer PR #205) + // This keeps the TURN allocation and ICE binding alive during long sessions + uint64_t current_time = ports_get_epoch_time(); + if (current_time - last_heartbeat_time >= STUN_HEARTBEAT_INTERVAL) { + agent_connectivity_check(&pc->agent, 1); // 1 = heartbeat mode + LOGD("STUN heartbeat sent"); + last_heartbeat_time = current_time; + } + + diag_recv_calls++; if ((pc->agent_ret = agent_recv(&pc->agent, pc->agent_buf, sizeof(pc->agent_buf))) > 0) { + diag_recv_data++; LOGD("agent_recv %d", pc->agent_ret); if (rtcp_probe(pc->agent_buf, pc->agent_ret)) { + diag_rtcp_packets++; LOGD("Got RTCP packet"); dtls_srtp_decrypt_rtcp_packet(&pc->dtls_srtp, pc->agent_buf, &pc->agent_ret); peer_connection_incoming_rtcp(pc, pc->agent_buf, pc->agent_ret); } else if (dtls_srtp_probe(pc->agent_buf)) { + diag_dtls_packets++; int ret = dtls_srtp_read(&pc->dtls_srtp, pc->temp_buf, sizeof(pc->temp_buf)); LOGD("Got DTLS data %d", ret); @@ -333,28 +439,61 @@ int peer_connection_loop(PeerConnection* pc) { } } else if (rtp_packet_validate(pc->agent_buf, pc->agent_ret)) { + diag_rtp_packets++; LOGD("Got RTP packet"); + int pre_decrypt_size = pc->agent_ret; dtls_srtp_decrypt_rtp_packet(&pc->dtls_srtp, pc->agent_buf, &pc->agent_ret); - ssrc = rtp_get_ssrc(pc->agent_buf); - if (ssrc == pc->remote_assrc) { - rtp_decoder_decode(&pc->artp_decoder, pc->agent_buf, pc->agent_ret); - } else if (ssrc == pc->remote_vssrc) { - rtp_decoder_decode(&pc->vrtp_decoder, pc->agent_buf, pc->agent_ret); + if (pc->agent_ret == 0) { + diag_srtp_fail++; + if (diag_srtp_fail <= 5 || (diag_srtp_fail % 100) == 0) { + LOGW("SRTP decrypt failed (pre_size=%d fail_count=%u)", pre_decrypt_size, (unsigned)diag_srtp_fail); + } + } else { + ssrc = rtp_get_ssrc(pc->agent_buf); + // SSRC late binding: if remote SDP omitted a=ssrc, learn from first packet + if (ssrc != 0 && pc->remote_assrc == 0 && pc->config.audio_codec != CODEC_NONE) { + pc->remote_assrc = ssrc; + LOGI("Audio SSRC learned from RTP: %" PRIu32, ssrc); + } + if (ssrc == pc->remote_assrc) { + diag_audio_decoded++; + rtp_decoder_decode(&pc->artp_decoder, pc->agent_buf, pc->agent_ret); + } else if (ssrc == pc->remote_vssrc) { + rtp_decoder_decode(&pc->vrtp_decoder, pc->agent_buf, pc->agent_ret); + } else { + diag_ssrc_mismatch++; + } } } else { - LOGW("Unknown data"); + diag_unknown_packets++; + LOGW("Unknown data (len=%d first_byte=0x%02x)", pc->agent_ret, pc->agent_buf[0]); } } + // Periodic diagnostic log every 10 seconds to help debug silent audio + if (current_time - diag_last_log_time >= 10000) { + LOGI("DIAG: recv=%u data=%u rtp=%u decoded=%u srtp_fail=%u ssrc_miss=%u rtcp=%u dtls=%u unk=%u", + (unsigned)diag_recv_calls, (unsigned)diag_recv_data, + (unsigned)diag_rtp_packets, (unsigned)diag_audio_decoded, + (unsigned)diag_srtp_fail, (unsigned)diag_ssrc_mismatch, + (unsigned)diag_rtcp_packets, (unsigned)diag_dtls_packets, + (unsigned)diag_unknown_packets); + LOGI("DIAG TX: sent=%u srtp_fail=%u send_fail=%u bytes=%u", + (unsigned)diag_tx_send_ok, (unsigned)diag_tx_srtp_fail, + (unsigned)diag_tx_send_fail, (unsigned)diag_tx_bytes); + diag_last_log_time = current_time; + } + if (CONFIG_KEEPALIVE_TIMEOUT > 0 && (ports_get_epoch_time() - pc->agent.binding_request_time) > CONFIG_KEEPALIVE_TIMEOUT) { LOGI("binding request timeout"); STATE_CHANGED(pc, PEER_CONNECTION_CLOSED); } break; + } case PEER_CONNECTION_FAILED: break; case PEER_CONNECTION_DISCONNECTED: @@ -384,11 +523,24 @@ void peer_connection_set_remote_description(PeerConnection* pc, const char* sdp, buf[line - start] = '\0'; if (strstr(buf, "a=setup:passive")) { + LOGI("SDP: Remote has setup:passive, we are DTLS CLIENT (active)"); role = DTLS_SRTP_ROLE_CLIENT; + } else if (strstr(buf, "a=setup:active")) { + LOGI("SDP: Remote has setup:active, we are DTLS SERVER (passive)"); + // Default is SERVER, so no change needed } - if (strstr(buf, "a=fingerprint")) { - strncpy(pc->dtls_srtp.remote_fingerprint, buf + 22, DTLS_SRTP_FINGERPRINT_LENGTH); + // Only match SHA-256 fingerprints (libpeer uses SHA-256 for DTLS certificate verification) + if (strstr(buf, "a=fingerprint:sha-256 ")) { + char *fp_start = strstr(buf, "sha-256 "); + if (fp_start) { + fp_start += 8; + size_t fp_len = strlen(fp_start); + if (fp_len >= 95) fp_len = 95; + strncpy(pc->dtls_srtp.remote_fingerprint, fp_start, fp_len); + pc->dtls_srtp.remote_fingerprint[fp_len] = '\0'; + LOGI("SDP: Parsed remote fingerprint: %s", pc->dtls_srtp.remote_fingerprint); + } } if (strstr(buf, "a=ice-ufrag") && @@ -417,16 +569,67 @@ void peer_connection_set_remote_description(PeerConnection* pc, const char* sdp, agent_set_remote_description(&pc->agent, (char*)sdp); if (type == SDP_TYPE_ANSWER) { + // Only reinitialize DTLS if the role changed from what was set during offer creation. + // If we reinitialize unnecessarily, we generate a NEW certificate with a DIFFERENT + // fingerprint, which breaks the fingerprint matching with the remote peer. + // The offer was created with SERVER role. Only change if remote sent a=setup:passive. + if (role != pc->dtls_srtp.role) { + LOGD("DTLS: Role changed, reinitializing"); + dtls_srtp_deinit(&pc->dtls_srtp); + dtls_srtp_init(&pc->dtls_srtp, role, pc); + pc->dtls_srtp.udp_recv = peer_connection_dtls_srtp_recv; + pc->dtls_srtp.udp_send = peer_connection_dtls_srtp_send; + } + agent_update_candidate_pairs(&pc->agent); STATE_CHANGED(pc, PEER_CONNECTION_CHECKING); } } -static const char* peer_connection_create_sdp(PeerConnection* pc, SdpType sdp_type) { - char* description = (char*)pc->temp_buf; +/** + * Helper to append per-media trailing attributes (aiortc format) + * These go at the end of each media section: candidates, end-of-candidates, ice creds, fingerprint, setup + */ +static void peer_connection_append_media_tail(PeerConnection* pc, SdpType sdp_type, DtlsSrtpRole role) { + // Candidates + for (int i = 0; i < pc->agent.local_candidates_count; i++) { + char candidate_line[256]; + memset(candidate_line, 0, sizeof(candidate_line)); + ice_candidate_to_description(&pc->agent.local_candidates[i], candidate_line, sizeof(candidate_line)); + sdp_append(pc->sdp, "%s", candidate_line); + } + + // End of candidates marker + sdp_append(pc->sdp, "a=end-of-candidates"); - memset(pc->temp_buf, 0, sizeof(pc->temp_buf)); + // ICE credentials + sdp_append(pc->sdp, "a=ice-ufrag:%s", pc->agent.local_ufrag); + sdp_append(pc->sdp, "a=ice-pwd:%s", pc->agent.local_upwd); + + // Fingerprint + sdp_append(pc->sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); + + // Setup + sdp_append(pc->sdp, peer_connection_dtls_role_setup_value(role, sdp_type)); +} + +/** + * Generate SDP in aiortc-compatible format + * + * Structure (matches aiortc 1:1): + * - Session level: v=, o=, s=, t=, a=group:BUNDLE 0 1, a=msid-semantic:WMS * + * - Per media section: + * - m= line, c= line, media-specific attributes + * - a=mid:N (numeric) + * - a=candidate:... (all candidates) + * - a=end-of-candidates + * - a=ice-ufrag:, a=ice-pwd: + * - a=fingerprint:sha-256 + * - a=setup:actpass (or active/passive for answers) + */ +static const char* peer_connection_create_sdp(PeerConnection* pc, SdpType sdp_type) { DtlsSrtpRole role = DTLS_SRTP_ROLE_SERVER; + int mid = 0; pc->sctp.connected = 0; @@ -445,57 +648,70 @@ static const char* peer_connection_create_sdp(PeerConnection* pc, SdpType sdp_ty } dtls_srtp_reset_session(&pc->dtls_srtp); - dtls_srtp_init(&pc->dtls_srtp, role, pc); + int dtls_ret = dtls_srtp_init(&pc->dtls_srtp, role, pc); + if (dtls_ret != 0) { + LOGE("dtls_srtp_init failed: %d (fingerprint will be empty!)", dtls_ret); + } else { + LOGI("DTLS initialized, fingerprint: %s", pc->dtls_srtp.local_fingerprint); + } pc->dtls_srtp.udp_recv = peer_connection_dtls_srtp_recv; pc->dtls_srtp.udp_send = peer_connection_dtls_srtp_send; + // Generate ICE credentials (same for all media sections in BUNDLE) + agent_create_ice_credential(&pc->agent); + + // Gather candidates before building SDP (need them for each media section) + agent_gather_candidate(&pc->agent, NULL, NULL, NULL); // host address + for (int i = 0; i < sizeof(pc->config.ice_servers) / sizeof(pc->config.ice_servers[0]); ++i) { + if (pc->config.ice_servers[i].urls) { + LOGI("ice server: %s", pc->config.ice_servers[i].urls); + agent_gather_candidate(&pc->agent, pc->config.ice_servers[i].urls, + pc->config.ice_servers[i].username, + pc->config.ice_servers[i].credential); + } + } + + // === Build SDP === memset(pc->sdp, 0, sizeof(pc->sdp)); - // TODO: check if we have video or audio codecs + + // Session-level header (only these attributes at session level) sdp_create(pc->sdp, pc->config.video_codec != CODEC_NONE, pc->config.audio_codec != CODEC_NONE, pc->config.datachannel); - agent_create_ice_credential(&pc->agent); - sdp_append(pc->sdp, "a=ice-ufrag:%s", pc->agent.local_ufrag); - sdp_append(pc->sdp, "a=ice-pwd:%s", pc->agent.local_upwd); - sdp_append(pc->sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); - sdp_append(pc->sdp, peer_connection_dtls_role_setup_value(role)); - + // === Video media section (if enabled) === if (pc->config.video_codec == CODEC_H264) { - sdp_append_h264(pc->sdp); + sdp_append_h264(pc->sdp, mid++); + peer_connection_append_media_tail(pc, sdp_type, role); } - switch (pc->config.audio_codec) { - case CODEC_PCMA: - sdp_append_pcma(pc->sdp); - break; - case CODEC_PCMU: - sdp_append_pcmu(pc->sdp); - break; - case CODEC_OPUS: - sdp_append_opus(pc->sdp); - default: - break; + // === Audio media section (if enabled) === + if (pc->config.audio_codec != CODEC_NONE) { + switch (pc->config.audio_codec) { + case CODEC_PCMA: + sdp_append_pcma(pc->sdp, mid++); + break; + case CODEC_PCMU: + sdp_append_pcmu(pc->sdp, mid++); + break; + case CODEC_OPUS: + sdp_append_opus(pc->sdp, mid++); + break; + default: + break; + } + peer_connection_append_media_tail(pc, sdp_type, role); } + // === Datachannel media section (if enabled) === if (pc->config.datachannel) { - sdp_append_datachannel(pc->sdp); + sdp_append_datachannel(pc->sdp, mid++); + peer_connection_append_media_tail(pc, sdp_type, role); } pc->b_local_description_created = 1; - agent_gather_candidate(&pc->agent, NULL, NULL, NULL); // host address - for (int i = 0; i < sizeof(pc->config.ice_servers) / sizeof(pc->config.ice_servers[0]); ++i) { - if (pc->config.ice_servers[i].urls) { - LOGI("ice server: %s", pc->config.ice_servers[i].urls); - agent_gather_candidate(&pc->agent, pc->config.ice_servers[i].urls, pc->config.ice_servers[i].username, pc->config.ice_servers[i].credential); - } - } - - agent_get_local_description(&pc->agent, description, sizeof(pc->temp_buf)); - sdp_append(pc->sdp, description); - if (pc->onicecandidate) { pc->onicecandidate(pc->sdp, pc->config.user_data); } diff --git a/src/rtcp.c b/src/rtcp.c index 117b58c5..1bd84f2e 100644 --- a/src/rtcp.c +++ b/src/rtcp.c @@ -9,8 +9,13 @@ int rtcp_probe(uint8_t* packet, size_t size) { if (size < 8) return -1; - RtpHeader* header = (RtpHeader*)packet; - return ((header->type >= 64) && (header->type < 96)); + // RTP/RTCP packet format (network byte order): + // Byte 0: V(2) | P(1) | X/RC(5) + // Byte 1: M(1) | PT(7) + // Per RFC 5761 demux rule: PT 64-95 goes to RTCP path + // Note: Don't use RtpHeader bitfield - it doesn't handle network byte order correctly + uint8_t pt = packet[1] & 0x7F; // Mask off marker bit to get payload type + return ((pt >= 64) && (pt < 96)); } int rtcp_get_pli(uint8_t* packet, int len, uint32_t ssrc) { diff --git a/src/rtp.c b/src/rtp.c index 0a3136ad..2a17a43b 100644 --- a/src/rtp.c +++ b/src/rtp.c @@ -34,8 +34,13 @@ int rtp_packet_validate(uint8_t* packet, size_t size) { if (size < 12) return 0; - RtpHeader* rtp_header = (RtpHeader*)packet; - return ((rtp_header->type < 64) || (rtp_header->type >= 96)); + // RTP packet format (network byte order): + // Byte 0: V(2) | P(1) | X(1) | CC(4) + // Byte 1: M(1) | PT(7) + // Per RFC 5761 demux rule: PT outside 64-95 goes to RTP path (PT < 64 or PT >= 96) + // Note: Don't use RtpHeader bitfield - it doesn't handle network byte order correctly + uint8_t pt = packet[1] & 0x7F; // Mask off marker bit to get payload type + return ((pt < 64) || (pt >= 96)); } uint32_t rtp_get_ssrc(uint8_t* packet) { @@ -268,9 +273,25 @@ static int rtp_decode_h264(RtpDecoder* rtp_decoder, uint8_t* buf, size_t size) { static int rtp_decode_generic(RtpDecoder* rtp_decoder, uint8_t* buf, size_t size) { RtpPacket* rtp_packet = (RtpPacket*)buf; + + // Calculate payload offset: skip fixed header, CSRC entries, and extensions + size_t offset = sizeof(RtpHeader); + + // Skip CSRC entries (each 4 bytes, count in CC field) + offset += rtp_packet->header.csrccount * sizeof(uint32_t); + + // Skip header extension if present (RFC 3550 Section 5.3.1) + if (rtp_packet->header.extension && offset + 4 <= size) { + // Extension header: 2 bytes profile + 2 bytes length (in 32-bit words) + uint16_t ext_length = (uint16_t)((buf[offset + 2] << 8) | buf[offset + 3]); + offset += 4 + ext_length * 4; + } + + if (offset > size) + return -1; // malformed packet + if (rtp_decoder->on_packet != NULL) - rtp_decoder->on_packet(rtp_packet->payload, size - sizeof(RtpHeader), rtp_decoder->user_data); - // even if there is no callback set, assume everything is ok for caller and do not return an error + rtp_decoder->on_packet(buf + offset, size - offset, rtp_decoder->user_data); return (int)size; } diff --git a/src/rtp.h b/src/rtp.h index 4f946145..4fc0065b 100644 --- a/src/rtp.h +++ b/src/rtp.h @@ -8,6 +8,10 @@ #define __LITTLE_ENDIAN 1234 #elif __APPLE__ #include +// macOS defines BYTE_ORDER, not __BYTE_ORDER +#define __BYTE_ORDER BYTE_ORDER +#define __BIG_ENDIAN BIG_ENDIAN +#define __LITTLE_ENDIAN LITTLE_ENDIAN #else #include #endif diff --git a/src/sctp.c b/src/sctp.c index 644f8d8e..99758563 100644 --- a/src/sctp.c +++ b/src/sctp.c @@ -271,7 +271,7 @@ void sctp_incoming_data(Sctp* sctp, char* buf, size_t len) { data_chunk->length = htons(1 + sizeof(SctpDataChunk)); data_chunk->data[0] = DATA_CHANNEL_ACK; length += ntohs(data_chunk->length); - } else if (ntohl(data_chunk->ppid) == DATA_CHANNEL_PPID_DOMSTRING || ntohl(data_chunk->ppid) == DATA_CHANNEL_PPID_BINARY) { + } else if (ntohl(data_chunk->ppid) == DATA_CHANNEL_PPID_DOMSTRING) { if (sctp->onmessage) { sctp->onmessage((char*)data_chunk->data, ntohs(data_chunk->length) - sizeof(SctpDataChunk), sctp->userdata, ntohs(data_chunk->sid)); diff --git a/src/sdp.c b/src/sdp.c index b7661a4c..d3f3d241 100644 --- a/src/sdp.c +++ b/src/sdp.c @@ -1,5 +1,16 @@ +/** + * SDP Generation - aiortc-compatible format + * + * This generates SDP that matches aiortc 1:1 for Pipecat interoperability. + * + * Structure: + * - Session level: v=, o=, s=, t=, a=group:BUNDLE, a=msid-semantic:WMS * + * - Per media section: m=, c=, attributes, candidates, ice-ufrag/pwd, fingerprint, setup + */ + #include #include +#include #include "sdp.h" @@ -26,82 +37,121 @@ void sdp_reset(char* sdp) { memset(sdp, 0, CONFIG_SDP_BUFFER_SIZE); } -void sdp_append_h264(char* sdp) { +/** + * Append H264 video media section with aiortc-compatible attribute order + * Order: m=, c=, direction, mid, rtcp, rtcp-mux, ssrc, rtpmap, rtcp-fb, fmtp + */ +void sdp_append_h264(char* sdp, int mid) { sdp_append(sdp, "m=video 9 UDP/TLS/RTP/SAVPF 96"); sdp_append(sdp, "c=IN IP4 0.0.0.0"); + sdp_append(sdp, "a=sendrecv"); + sdp_append(sdp, "a=mid:%d", mid); + sdp_append(sdp, "a=rtcp:9 IN IP4 0.0.0.0"); + sdp_append(sdp, "a=rtcp-mux"); + sdp_append(sdp, "a=ssrc:1 cname:webrtc-h264"); + sdp_append(sdp, "a=rtpmap:96 H264/90000"); sdp_append(sdp, "a=rtcp-fb:96 nack"); sdp_append(sdp, "a=rtcp-fb:96 nack pli"); sdp_append(sdp, "a=fmtp:96 profile-level-id=42e01f;level-asymmetry-allowed=1"); - sdp_append(sdp, "a=rtpmap:96 H264/90000"); - sdp_append(sdp, "a=ssrc:1 cname:webrtc-h264"); - sdp_append(sdp, "a=sendrecv"); - sdp_append(sdp, "a=mid:video"); - sdp_append(sdp, "a=rtcp-mux"); } -void sdp_append_pcma(char* sdp) { - sdp_append(sdp, "m=audio 9 UDP/TLS/RTP/SAVP 8"); +/** + * Append PCMA audio media section with aiortc-compatible attribute order + * Order: m=, c=, direction, mid, rtcp, rtcp-mux, ssrc, rtpmap + */ +void sdp_append_pcma(char* sdp, int mid) { + sdp_append(sdp, "m=audio 9 UDP/TLS/RTP/SAVPF 8"); sdp_append(sdp, "c=IN IP4 0.0.0.0"); - sdp_append(sdp, "a=rtpmap:8 PCMA/8000"); - sdp_append(sdp, "a=ssrc:4 cname:webrtc-pcma"); sdp_append(sdp, "a=sendrecv"); - sdp_append(sdp, "a=mid:audio"); + sdp_append(sdp, "a=mid:%d", mid); + sdp_append(sdp, "a=rtcp:9 IN IP4 0.0.0.0"); sdp_append(sdp, "a=rtcp-mux"); + sdp_append(sdp, "a=ssrc:4 cname:webrtc-pcma"); + sdp_append(sdp, "a=rtpmap:8 PCMA/8000"); } -void sdp_append_pcmu(char* sdp) { - sdp_append(sdp, "m=audio 9 UDP/TLS/RTP/SAVP 0"); +/** + * Append PCMU audio media section with aiortc-compatible attribute order + * Order: m=, c=, direction, mid, rtcp, rtcp-mux, ssrc, rtpmap + */ +void sdp_append_pcmu(char* sdp, int mid) { + sdp_append(sdp, "m=audio 9 UDP/TLS/RTP/SAVPF 0"); sdp_append(sdp, "c=IN IP4 0.0.0.0"); - sdp_append(sdp, "a=rtpmap:0 PCMU/8000"); - sdp_append(sdp, "a=ssrc:5 cname:webrtc-pcmu"); sdp_append(sdp, "a=sendrecv"); - sdp_append(sdp, "a=mid:audio"); + sdp_append(sdp, "a=mid:%d", mid); + sdp_append(sdp, "a=rtcp:9 IN IP4 0.0.0.0"); sdp_append(sdp, "a=rtcp-mux"); + sdp_append(sdp, "a=ssrc:5 cname:webrtc-pcmu"); + sdp_append(sdp, "a=rtpmap:0 PCMU/8000"); } -void sdp_append_opus(char* sdp) { - sdp_append(sdp, "m=audio 9 UDP/TLS/RTP/SAVP 111"); +/** + * Append Opus audio media section with aiortc-compatible attribute order + * Order: m=, c=, direction, mid, rtcp, rtcp-mux, ssrc, rtpmap, rtcp-fb, fmtp + */ +void sdp_append_opus(char* sdp, int mid) { + sdp_append(sdp, "m=audio 9 UDP/TLS/RTP/SAVPF 111"); sdp_append(sdp, "c=IN IP4 0.0.0.0"); - sdp_append(sdp, "a=rtpmap:111 opus/48000/2"); - sdp_append(sdp, "a=ssrc:6 cname:webrtc-opus"); sdp_append(sdp, "a=sendrecv"); - sdp_append(sdp, "a=mid:audio"); + sdp_append(sdp, "a=mid:%d", mid); + sdp_append(sdp, "a=rtcp:9 IN IP4 0.0.0.0"); sdp_append(sdp, "a=rtcp-mux"); + sdp_append(sdp, "a=ssrc:6 cname:webrtc-opus"); + sdp_append(sdp, "a=rtpmap:111 opus/48000/2"); } -void sdp_append_datachannel(char* sdp) { - sdp_append(sdp, "m=application 50712 UDP/DTLS/SCTP webrtc-datachannel"); +/** + * Append datachannel media section with aiortc-compatible attribute order + */ +void sdp_append_datachannel(char* sdp, int mid) { + sdp_append(sdp, "m=application 9 UDP/DTLS/SCTP webrtc-datachannel"); sdp_append(sdp, "c=IN IP4 0.0.0.0"); - sdp_append(sdp, "a=mid:datachannel"); + sdp_append(sdp, "a=mid:%d", mid); sdp_append(sdp, "a=sctp-port:5000"); - sdp_append(sdp, "a=max-message-size:262144"); + sdp_append(sdp, "a=max-message-size:65536"); } +/** + * Create SDP session-level header with numeric BUNDLE mids + * + * @param sdp Output buffer + * @param b_video Include video in BUNDLE + * @param b_audio Include audio in BUNDLE + * @param b_datachannel Include datachannel in BUNDLE + */ void sdp_create(char* sdp, int b_video, int b_audio, int b_datachannel) { - char bundle[64]; sdp_append(sdp, "v=0"); sdp_append(sdp, "o=- 1495799811084970 1495799811084970 IN IP4 0.0.0.0"); sdp_append(sdp, "s=-"); sdp_append(sdp, "t=0 0"); - sdp_append(sdp, "a=msid-semantic: iot"); -#if ICE_LITE - sdp_append(sdp, "a=ice-lite"); -#endif - memset(bundle, 0, sizeof(bundle)); + // Build BUNDLE with numeric mids (like aiortc) + // Mid assignment: video=0 (if present), audio=0 or 1, datachannel=last + char bundle[64]; + memset(bundle, 0, sizeof(bundle)); strcat(bundle, "a=group:BUNDLE"); + int mid = 0; if (b_video) { - strcat(bundle, " video"); + char num[8]; + snprintf(num, sizeof(num), " %d", mid++); + strcat(bundle, num); } - if (b_audio) { - strcat(bundle, " audio"); + char num[8]; + snprintf(num, sizeof(num), " %d", mid++); + strcat(bundle, num); } - if (b_datachannel) { - strcat(bundle, " datachannel"); + char num[8]; + snprintf(num, sizeof(num), " %d", mid++); + strcat(bundle, num); } sdp_append(sdp, bundle); + sdp_append(sdp, "a=msid-semantic:WMS *"); + +#if ICE_LITE + sdp_append(sdp, "a=ice-lite"); +#endif } diff --git a/src/sdp.h b/src/sdp.h index ba85d71d..73307716 100644 --- a/src/sdp.h +++ b/src/sdp.h @@ -10,15 +10,15 @@ #define ICE_LITE 0 #endif -void sdp_append_h264(char* sdp); +void sdp_append_h264(char* sdp, int mid); -void sdp_append_pcma(char* sdp); +void sdp_append_pcma(char* sdp, int mid); -void sdp_append_pcmu(char* sdp); +void sdp_append_pcmu(char* sdp, int mid); -void sdp_append_opus(char* sdp); +void sdp_append_opus(char* sdp, int mid); -void sdp_append_datachannel(char* sdp); +void sdp_append_datachannel(char* sdp, int mid); void sdp_create(char* sdp, int b_video, int b_audio, int b_datachannel); diff --git a/src/socket.c b/src/socket.c index 6f7d8ef2..eb3e0931 100644 --- a/src/socket.c +++ b/src/socket.c @@ -129,6 +129,14 @@ int udp_socket_sendto(UdpSocket* udp_socket, Address* addr, const uint8_t* buf, return -1; } +#if LOG_LEVEL >= LEVEL_DEBUG + { + char addr_string[ADDRSTRLEN]; + addr_to_string(addr, addr_string, sizeof(addr_string)); + LOGD("udp_socket_sendto: fd=%d bytes=%d to=%s:%u", udp_socket->fd, ret, addr_string, (unsigned)addr->port); + } +#endif + return ret; } @@ -167,18 +175,49 @@ int udp_socket_recvfrom(UdpSocket* udp_socket, Address* addr, uint8_t* buf, int switch (udp_socket->bind_addr.family) { case AF_INET6: addr->family = AF_INET6; - addr->port = htons(sin6.sin6_port); + // sin6_port is already in network byte order; store host-order port in addr->port + addr->port = ntohs(sin6.sin6_port); memcpy(&addr->sin6, &sin6, sizeof(struct sockaddr_in6)); break; case AF_INET: default: addr->family = AF_INET; - addr->port = htons(sin.sin_port); + // sin_port is already in network byte order; store host-order port in addr->port + addr->port = ntohs(sin.sin_port); memcpy(&addr->sin, &sin, sizeof(struct sockaddr_in)); break; } } +#if LOG_LEVEL >= LEVEL_DEBUG + { + Address src_addr; + memset(&src_addr, 0, sizeof(src_addr)); + switch (udp_socket->bind_addr.family) { + case AF_INET6: + src_addr.family = AF_INET6; + src_addr.port = ntohs(sin6.sin6_port); + memcpy(&src_addr.sin6, &sin6, sizeof(struct sockaddr_in6)); + break; + case AF_INET: + default: + src_addr.family = AF_INET; + src_addr.port = ntohs(sin.sin_port); + memcpy(&src_addr.sin, &sin, sizeof(struct sockaddr_in)); + break; + } + char addr_string[ADDRSTRLEN]; + addr_to_string(&src_addr, addr_string, sizeof(addr_string)); + const char* kind = "udp"; + if (ret >= 1) { + if ((buf[0] & 0xC0) == 0x00) kind = "stun?"; + else if ((buf[0] & 0xC0) == 0x40) kind = "chan"; + } + LOGD("udp_socket_recvfrom: fd=%d bytes=%d from=%s:%u kind=%s", + udp_socket->fd, ret, addr_string, (unsigned)src_addr.port, kind); + } +#endif + return ret; } diff --git a/src/stun.c b/src/stun.c index d3872410..0eb42bd8 100644 --- a/src/stun.c +++ b/src/stun.c @@ -51,9 +51,11 @@ void stun_msg_create(StunMessage* msg, uint16_t type) { header->type = htons(type); header->length = 0; header->magic_cookie = htonl(MAGIC_COOKIE); - header->transaction_id[0] = htonl(CRC32_TABLE[1]); - header->transaction_id[1] = htonl(CRC32_TABLE[2]); - header->transaction_id[2] = htonl(CRC32_TABLE[3]); + // RFC 5389: Transaction ID MUST be uniformly and randomly chosen + // Use rand() to generate unique IDs for each message + header->transaction_id[0] = htonl((uint32_t)rand()); + header->transaction_id[1] = htonl((uint32_t)rand()); + header->transaction_id[2] = htonl((uint32_t)rand()); msg->size = sizeof(StunHeader); } @@ -132,23 +134,19 @@ void stun_parse_msg_buf(StunMessage* msg) { uint8_t mask[16]; - msg->stunclass = ntohs(header->type); - if ((msg->stunclass & STUN_CLASS_ERROR) == STUN_CLASS_ERROR) { - msg->stunclass = STUN_CLASS_ERROR; - } else if ((msg->stunclass & STUN_CLASS_INDICATION) == STUN_CLASS_INDICATION) { - msg->stunclass = STUN_CLASS_INDICATION; - } else if ((msg->stunclass & STUN_CLASS_RESPONSE) == STUN_CLASS_RESPONSE) { - msg->stunclass = STUN_CLASS_RESPONSE; - } else if ((msg->stunclass & STUN_CLASS_REQUEST) == STUN_CLASS_REQUEST) { - msg->stunclass = STUN_CLASS_REQUEST; - } - - msg->stunmethod = ntohs(header->type) & 0x0FFF; - if ((msg->stunmethod & STUN_METHOD_ALLOCATE) == STUN_METHOD_ALLOCATE) { - msg->stunmethod = STUN_METHOD_ALLOCATE; - } else if ((msg->stunmethod & STUN_METHOD_BINDING) == STUN_METHOD_BINDING) { - msg->stunmethod = STUN_METHOD_BINDING; - } + // Extract class and method according to RFC 5389 + // Type field encoding: M11 M10 M9 M8 M7 C1 M6 M5 M4 C0 M3 M2 M1 M0 + // where M0-M11 are method bits and C0-C1 are class bits + uint16_t raw_type = ntohs(header->type); + + // Extract class: C0 at bit 4, C1 at bit 8 + // STUN_CLASS constants use these same bit positions + msg->stunclass = raw_type & 0x0110; + + // Extract method: M0-M3 from bits 0-3, M4-M6 from bits 5-7, M7-M11 from bits 9-13 + msg->stunmethod = (raw_type & 0x000F) | // M0-M3 stay in bits 0-3 + ((raw_type & 0x00E0) >> 1) | // M4-M6 from bits 5-7 to bits 4-6 + ((raw_type & 0x3E00) >> 2); // M7-M11 from bits 9-13 to bits 7-11 while (pos < length) { StunAttribute* attr = (StunAttribute*)(msg->buf + pos); @@ -160,11 +158,18 @@ void stun_parse_msg_buf(StunMessage* msg) { case STUN_ATTR_TYPE_MAPPED_ADDRESS: stun_get_mapped_address(attr->value, mask, &msg->mapped_addr); break; - case STUN_ATTR_TYPE_USERNAME: + case STUN_ATTR_TYPE_USERNAME: { + uint16_t attr_len = ntohs(attr->length); memset(msg->username, 0, sizeof(msg->username)); - memcpy(msg->username, attr->value, ntohs(attr->length)); - // LOGD("length = %d, Username %s", ntohs(attr->length), msg->username); + if (attr_len >= sizeof(msg->username)) { + LOGE("USERNAME truncated! attr_len=%u, buf_size=%zu", attr_len, sizeof(msg->username)); + attr_len = sizeof(msg->username) - 1; + } + memcpy(msg->username, attr->value, attr_len); + msg->username_len = attr_len; + // LOGD("length = %d, Username %s", attr_len, msg->username); break; + } case STUN_ATTR_TYPE_MESSAGE_INTEGRITY: memcpy(msg->message_integrity, attr->value, ntohs(attr->length)); @@ -177,22 +182,54 @@ void stun_parse_msg_buf(StunMessage* msg) { break; case STUN_ATTR_TYPE_LIFETIME: break; - case STUN_ATTR_TYPE_REALM: + case STUN_ATTR_TYPE_REALM: { + uint16_t attr_len = ntohs(attr->length); memset(msg->realm, 0, sizeof(msg->realm)); - memcpy(msg->realm, attr->value, ntohs(attr->length)); - LOGD("Realm %s", msg->realm); + if (attr_len >= sizeof(msg->realm)) { + LOGE("REALM truncated! attr_len=%u, buf_size=%zu", attr_len, sizeof(msg->realm)); + attr_len = sizeof(msg->realm) - 1; + } + memcpy(msg->realm, attr->value, attr_len); + msg->realm_len = attr_len; + LOGD("Realm (len=%zu): %s", msg->realm_len, msg->realm); break; - case STUN_ATTR_TYPE_NONCE: + } + case STUN_ATTR_TYPE_NONCE: { + uint16_t attr_len = ntohs(attr->length); memset(msg->nonce, 0, sizeof(msg->nonce)); - memcpy(msg->nonce, attr->value, ntohs(attr->length)); - LOGD("Nonce %s", msg->nonce); + if (attr_len >= sizeof(msg->nonce)) { + LOGE("NONCE truncated! attr_len=%u, buf_size=%zu -- THIS BREAKS TURN AUTH!", attr_len, sizeof(msg->nonce)); + attr_len = sizeof(msg->nonce) - 1; + } + memcpy(msg->nonce, attr->value, attr_len); + msg->nonce_len = attr_len; + break; + } + case STUN_ATTR_TYPE_XOR_PEER_ADDRESS: { + *((uint32_t*)mask) = htonl(MAGIC_COOKIE); + memcpy(mask + 4, header->transaction_id, sizeof(header->transaction_id)); + stun_get_mapped_address(attr->value, mask, &msg->peer_addr); + break; + } + case STUN_ATTR_TYPE_DATA: { + size_t copy_len = ntohs(attr->length); + if (copy_len > sizeof(msg->data)) { + LOGW("STUN DATA truncated from %zu to %zu", copy_len, sizeof(msg->data)); + copy_len = sizeof(msg->data); + } + memcpy(msg->data, attr->value, copy_len); + msg->data_len = copy_len; break; + } case STUN_ATTR_TYPE_XOR_RELAYED_ADDRESS: *((uint32_t*)mask) = htonl(MAGIC_COOKIE); memcpy(mask + 4, header->transaction_id, sizeof(header->transaction_id)); LOGD("XOR Relayed Address"); stun_get_mapped_address(attr->value, mask, &msg->relayed_addr); break; + case STUN_ATTR_TYPE_CHANNEL_NUMBER: + // TURN ChannelBind CHANNEL-NUMBER attribute (2-byte channel, 2-byte RFFU). No-op parse. + break; case STUN_ATTR_TYPE_XOR_MAPPED_ADDRESS: *((uint32_t*)mask) = htonl(MAGIC_COOKIE); memcpy(mask + 4, header->transaction_id, sizeof(header->transaction_id)); @@ -207,6 +244,30 @@ void stun_parse_msg_buf(StunMessage* msg) { memcpy(&msg->fingerprint, attr->value, ntohs(attr->length)); // LOGD("Fingerprint: 0x%.4x", msg->fingerprint); break; + case STUN_ATTR_TYPE_ERROR_CODE: { + // Parse ERROR-CODE attribute to see why TURN failed + // Format: 21-bit padding, 3-bit class, 8-bit number + // Followed by UTF-8 reason phrase + if (ntohs(attr->length) >= 4) { + uint8_t *err_data = (uint8_t *)(attr + 1); + uint8_t err_class = err_data[2] & 0x07; // Bits 0-2 of byte 2 + uint8_t err_number = err_data[3]; // Byte 3 + uint16_t err_code = err_class * 100 + err_number; + + // Reason phrase starts at byte 4 + int reason_len = ntohs(attr->length) - 4; + char reason[128] = {0}; + if (reason_len > 0 && reason_len < sizeof(reason)) { + memcpy(reason, &err_data[4], reason_len); + reason[reason_len] = '\0'; + } + + LOGE("TURN ERROR-CODE %d: %s", err_code, reason[0] ? reason : "(no reason)"); + } else { + LOGE("Malformed ERROR-CODE attribute (len=%d)", ntohs(attr->length)); + } + break; + } case STUN_ATTR_TYPE_ICE_CONTROLLED: case STUN_ATTR_TYPE_ICE_CONTROLLING: case STUN_ATTR_TYPE_NETWORK_COST: @@ -237,25 +298,45 @@ int stun_msg_write_attr(StunMessage* msg, StunAttrType type, uint16_t length, ch StunAttribute* stun_attr = (StunAttribute*)(msg->buf + msg->size); + uint16_t padded_len = 4 * ((length + 3) / 4); + if (msg->size + sizeof(StunAttribute) + padded_len > STUN_ATTR_BUF_SIZE) { + LOGE("stun_msg_write_attr: overflow (need %u, have %zu)", (unsigned)(msg->size + sizeof(StunAttribute) + padded_len), (size_t)STUN_ATTR_BUF_SIZE); + return -1; + } + stun_attr->type = htons(type); stun_attr->length = htons(length); if (value) memcpy(stun_attr->value, value, length); - length = 4 * ((length + 3) / 4); + length = padded_len; header->length = htons(ntohs(header->length) + sizeof(StunAttribute) + length); msg->size += length + sizeof(StunAttribute); + // Store attribute values and their lengths for MESSAGE-INTEGRITY computation + uint16_t orig_len = ntohs(stun_attr->length); // Use original length before padding switch (type) { case STUN_ATTR_TYPE_REALM: - memcpy(msg->realm, value, length); + if (orig_len < sizeof(msg->realm)) { + memset(msg->realm, 0, sizeof(msg->realm)); + memcpy(msg->realm, value, orig_len); + msg->realm_len = orig_len; + } break; case STUN_ATTR_TYPE_NONCE: - memcpy(msg->nonce, value, length); + if (orig_len < sizeof(msg->nonce)) { + memset(msg->nonce, 0, sizeof(msg->nonce)); + memcpy(msg->nonce, value, orig_len); + msg->nonce_len = orig_len; + } break; case STUN_ATTR_TYPE_USERNAME: - memcpy(msg->username, value, length); + if (orig_len < sizeof(msg->username)) { + memset(msg->username, 0, sizeof(msg->username)); + memcpy(msg->username, value, orig_len); + msg->username_len = orig_len; + } break; default: break; @@ -269,7 +350,7 @@ int stun_msg_finish(StunMessage* msg, StunCredential credential, const char* pas StunAttribute* stun_attr; uint16_t header_length = ntohs(header->length); - char key[256]; + char key[512]; // Increased: username(256) + realm(128) + password + separators char hash_key[17]; memset(key, 0, sizeof(key)); memset(hash_key, 0, sizeof(hash_key)); @@ -277,7 +358,6 @@ int stun_msg_finish(StunMessage* msg, StunCredential credential, const char* pas switch (credential) { case STUN_CREDENTIAL_LONG_TERM: snprintf(key, sizeof(key), "%s:%s:%s", msg->username, msg->realm, password); - LOGD("key: %s", key); utils_get_md5(key, strlen(key), (unsigned char*)hash_key); password = hash_key; password_len = 16; @@ -286,23 +366,58 @@ int stun_msg_finish(StunMessage* msg, StunCredential credential, const char* pas break; } + // MESSAGE-INTEGRITY: + // Many deployed TURN servers (including Cloudflare) expect the HMAC to be computed + // over the STUN message up to (but excluding) the MESSAGE-INTEGRITY attribute, + // with the header length field including the MESSAGE-INTEGRITY attribute (24 bytes) + // and excluding any later FINGERPRINT attribute. + if (msg->size + sizeof(StunAttribute) + 20 > STUN_ATTR_BUF_SIZE) { + LOGE("stun_msg_finish: overflow adding MESSAGE-INTEGRITY (need %zu, have %d)", + msg->size + sizeof(StunAttribute) + 20, STUN_ATTR_BUF_SIZE); + return -1; + } stun_attr = (StunAttribute*)(msg->buf + msg->size); - header->length = htons(header_length + 24); /* HMAC-SHA1 */ stun_attr->type = htons(STUN_ATTR_TYPE_MESSAGE_INTEGRITY); stun_attr->length = htons(20); - utils_get_hmac_sha1((char*)msg->buf, msg->size, password, password_len, (unsigned char*)stun_attr->value); + header->length = htons(header_length + 24); /* header length INCLUDES MI */ + if (credential == STUN_CREDENTIAL_SHORT_TERM) { + // RFC 5389 §15.4: For short-term credentials, the HMAC input is the message + // UP TO (but not including) the MESSAGE-INTEGRITY attribute. The header length + // is adjusted to include MI BEFORE computing the HMAC. + utils_get_hmac_sha1((char*)msg->buf, msg->size, password, password_len, (unsigned char*)stun_attr->value); + } else { + // TURN long-term: same computation (HMAC excludes MI attribute). + utils_get_hmac_sha1((char*)msg->buf, msg->size, password, password_len, (unsigned char*)stun_attr->value); + } msg->size += sizeof(StunAttribute) + 20; - // FINGERPRINT + // FINGERPRINT: RFC 5389 §15.5 says optional, but aiortc/aioice ALWAYS includes it. + // Pipecat Cloud (aiortc-based) may silently reject binding requests without FINGERPRINT. + // Always add FINGERPRINT for maximum interoperability. + if (msg->size + sizeof(StunAttribute) + 4 > STUN_ATTR_BUF_SIZE) { + LOGE("stun_msg_finish: overflow adding FINGERPRINT (need %zu, have %d)", msg->size + sizeof(StunAttribute) + 4, STUN_ATTR_BUF_SIZE); + return -1; + } stun_attr = (StunAttribute*)(msg->buf + msg->size); - header->length = htons(header_length + 24 /* HMAC-SHA1 */ + 8 /* FINGERPRINT */); + // Header length must include the FINGERPRINT attribute (8 bytes total) + header_length = ntohs(header->length); + header->length = htons(header_length + 8); stun_attr->type = htons(STUN_ATTR_TYPE_FINGERPRINT); stun_attr->length = htons(4); stun_calculate_fingerprint((char*)msg->buf, msg->size, (uint32_t*)stun_attr->value); msg->size += sizeof(StunAttribute) + 4; + return 0; } +// Detect TURN ChannelData packets per RFC 5766 §11: first two bits must be 01 +int stun_is_channel_data(const uint8_t* data, size_t len) { + if (data == NULL || len < 1) { + return 0; + } + return (data[0] & 0xC0) == 0x40; +} + int stun_probe(uint8_t* buf, size_t size) { StunHeader* header; if (size < sizeof(StunHeader)) { @@ -351,7 +466,7 @@ StunMsgType stun_is_stun_msg(uint8_t *buf, size_t size) { return 0; } #endif -int stun_msg_is_valid(uint8_t* buf, size_t size, char* password) { +int stun_msg_is_valid(uint8_t* buf, size_t size, const char* password, size_t password_len) { StunMessage msg; memcpy(msg.buf, buf, size); @@ -360,32 +475,64 @@ int stun_msg_is_valid(uint8_t* buf, size_t size, char* password) { StunHeader* header = (StunHeader*)msg.buf; - // FINGERPRINT - uint32_t fingerprint = 0; - size_t length = size - 4 - sizeof(StunAttribute); - stun_calculate_fingerprint((char*)msg.buf, length, &fingerprint); - // LOGD("Fingerprint: 0x%08x", fingerprint); - - if (fingerprint != msg.fingerprint) { - // LOGE("Fingerprint does not match."); - return -1; - } else { - // LOGD("Fingerprint matches."); + // FINGERPRINT (optional). + // If absent, msg.fingerprint will be 0; skip validation in that case. + if (msg.fingerprint != 0) { + uint32_t fingerprint = 0; + size_t length = size - 4 - sizeof(StunAttribute); + stun_calculate_fingerprint((char*)msg.buf, length, &fingerprint); + if (fingerprint != msg.fingerprint) { + return -1; + } } // MESSAGE-INTEGRITY - unsigned char message_integrity_hex[41]; - unsigned char message_integrity[20]; - header->length = htons(ntohs(header->length) - 4 - sizeof(StunAttribute)); - length = length - 20 - sizeof(StunAttribute); - utils_get_hmac_sha1((char*)msg.buf, length, password, strlen(password), message_integrity); - for (int i = 0; i < 20; i++) { - sprintf((char*)&message_integrity_hex[2 * i], "%02x", (uint8_t)message_integrity[i]); + // Validate MESSAGE-INTEGRITY per RFC 5389 semantics used by ICE short-term credentials: + // compute HMAC over the message up to and including the MESSAGE-INTEGRITY attribute, + // with the MI value bytes treated as zero during computation, and with the header + // length excluding the FINGERPRINT attribute. + unsigned char computed_mi[20]; + memset(computed_mi, 0, sizeof(computed_mi)); + + // Copy message excluding FINGERPRINT (assumed last). + size_t msg_no_fp_size = size - (sizeof(StunAttribute) + 4); + if (msg_no_fp_size > STUN_ATTR_BUF_SIZE) { + return -1; + } + uint8_t tmp[STUN_ATTR_BUF_SIZE]; + memcpy(tmp, msg.buf, msg_no_fp_size); + + // Adjust header length to exclude fingerprint. + StunHeader* tmp_header = (StunHeader*)tmp; + tmp_header->length = htons(ntohs(tmp_header->length) - (uint16_t)(sizeof(StunAttribute) + 4)); + + // Find MESSAGE-INTEGRITY offset. + size_t pos = sizeof(StunHeader); + size_t mi_attr_offset = 0; + while (pos + sizeof(StunAttribute) <= msg_no_fp_size) { + StunAttribute* attr = (StunAttribute*)(tmp + pos); + uint16_t attr_type = ntohs(attr->type); + uint16_t attr_len = ntohs(attr->length); + size_t padded_len = 4 * ((attr_len + 3) / 4); + if (pos + sizeof(StunAttribute) + padded_len > msg_no_fp_size) { + break; + } + if (attr_type == STUN_ATTR_TYPE_MESSAGE_INTEGRITY && attr_len == 20) { + mi_attr_offset = pos; + break; + } + pos += sizeof(StunAttribute) + padded_len; + } + if (mi_attr_offset == 0) { + return -1; } - // LOGD("message_integrity: 0x%s", message_integrity_hex); + // RFC 5389 §15.4: HMAC input is up to (but NOT including) the MESSAGE-INTEGRITY attribute. + // The header length should already be adjusted to include MI in the received message. + // We compute HMAC over the bytes up to the MI attribute offset. + utils_get_hmac_sha1((char*)tmp, mi_attr_offset, password, password_len, computed_mi); - if (memcmp(message_integrity, msg.message_integrity, 20) != 0) { + if (memcmp(computed_mi, msg.message_integrity, 20) != 0) { // LOGE("Message Integrity does not match."); return -1; } else { diff --git a/src/stun.h b/src/stun.h index 801133b6..6d2db198 100644 --- a/src/stun.h +++ b/src/stun.h @@ -13,7 +13,7 @@ typedef struct StunAttribute StunAttribute; typedef struct StunMessage StunMessage; -#define STUN_ATTR_BUF_SIZE 256 +#define STUN_ATTR_BUF_SIZE 1024 // Increased to avoid overflow with TURN attrs (nonce/realm/MI) #define MAGIC_COOKIE 0x2112A442 #define STUN_FINGERPRINT_XOR 0x5354554e @@ -30,6 +30,10 @@ typedef enum StunMethod { STUN_METHOD_BINDING = 0x0001, STUN_METHOD_ALLOCATE = 0x0003, + STUN_METHOD_SEND = 0x0006, // TURN Send Indication + STUN_METHOD_DATA = 0x0007, // TURN Data Indication + STUN_METHOD_CREATE_PERMISSION = 0x0008, + STUN_METHOD_CHANNEL_BIND = 0x0009, // TURN ChannelBind } StunMethod; @@ -38,12 +42,16 @@ typedef enum StunAttrType { STUN_ATTR_TYPE_MAPPED_ADDRESS = 0x0001, STUN_ATTR_TYPE_USERNAME = 0x0006, STUN_ATTR_TYPE_MESSAGE_INTEGRITY = 0x0008, + STUN_ATTR_TYPE_ERROR_CODE = 0x0009, // TURN error responses STUN_ATTR_TYPE_LIFETIME = 0x000d, + STUN_ATTR_TYPE_CHANNEL_NUMBER = 0x000c, STUN_ATTR_TYPE_REALM = 0x0014, STUN_ATTR_TYPE_NONCE = 0x0015, STUN_ATTR_TYPE_XOR_RELAYED_ADDRESS = 0x0016, + STUN_ATTR_TYPE_DATA = 0x0013, STUN_ATTR_TYPE_REQUESTED_TRANSPORT = 0x0019, STUN_ATTR_TYPE_XOR_MAPPED_ADDRESS = 0x0020, + STUN_ATTR_TYPE_XOR_PEER_ADDRESS = 0x0012, STUN_ATTR_TYPE_PRIORITY = 0x0024, STUN_ATTR_TYPE_USE_CANDIDATE = 0x0025, STUN_ATTR_TYPE_FINGERPRINT = 0x8028, @@ -87,11 +95,17 @@ struct StunMessage { StunMethod stunmethod; uint32_t fingerprint; char message_integrity[20]; - char username[128]; - char realm[64]; - char nonce[64]; + char username[256]; // Increased: TURN usernames can be long + char realm[128]; // Increased: safe margin + char nonce[192]; // Increased from 64: Cloudflare sends 80-byte nonces! + size_t nonce_len; // Store actual nonce length (not null-terminated) + size_t realm_len; // Store actual realm length + size_t username_len; // Store actual username length Address mapped_addr; Address relayed_addr; + Address peer_addr; // For TURN Send/Data indications + uint8_t data[STUN_ATTR_BUF_SIZE]; + size_t data_len; uint8_t buf[STUN_ATTR_BUF_SIZE]; size_t size; }; @@ -112,8 +126,11 @@ int stun_msg_write_attr(StunMessage* msg, StunAttrType type, uint16_t length, ch int stun_probe(uint8_t* buf, size_t size); -int stun_msg_is_valid(uint8_t* buf, size_t len, char* password); +int stun_msg_is_valid(uint8_t* buf, size_t len, const char* password, size_t password_len); int stun_msg_finish(StunMessage* msg, StunCredential credential, const char* password, size_t password_len); +// ChannelData detector (RFC 5766 §11) - true when leading bits are 01xxxxxx +int stun_is_channel_data(const uint8_t* data, size_t len); + #endif // STUN_H_ From 22edc324fae1d44e394366010f0b7febad7ebc7a Mon Sep 17 00:00:00 2001 From: AJ Keller Date: Mon, 30 Mar 2026 15:51:04 -0700 Subject: [PATCH 2/3] fix: suppress mbedtls doc warnings on newer clang --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 83789d06..22a7fdcb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,7 +81,7 @@ ExternalProject_Add(cjson ExternalProject_Add(mbedtls SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/mbedtls CMAKE_ARGS - -DCMAKE_C_FLAGS="-fPIC" + "-DCMAKE_C_FLAGS=-fPIC -Wno-error=documentation -Wno-error" -DENABLE_TESTING=off -DENABLE_PROGRAMS=off -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/dist From a2693de133d56c4ef36d2dda5edac8dd604cd515 Mon Sep 17 00:00:00 2001 From: AJ Keller Date: Mon, 30 Mar 2026 15:52:01 -0700 Subject: [PATCH 3/3] fix: add CMAKE_POLICY_VERSION_MINIMUM for CMake 4.x compat --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 22a7fdcb..c98688cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,7 @@ ExternalProject_Add(cjson -DENABLE_CJSON_TEST=off -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/dist -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 ) ExternalProject_Add(mbedtls @@ -86,6 +87,7 @@ ExternalProject_Add(mbedtls -DENABLE_PROGRAMS=off -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/dist -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 ) file(READ ${CMAKE_CURRENT_SOURCE_DIR}/third_party/mbedtls/include/mbedtls/mbedtls_config.h INPUT_CONTENT) string(REPLACE "//#define MBEDTLS_SSL_DTLS_SRTP" "#define MBEDTLS_SSL_DTLS_SRTP" MODIFIED_CONTENT ${INPUT_CONTENT}) @@ -98,6 +100,7 @@ ExternalProject_Add(srtp2 -DTEST_APPS=off -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/dist -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 ) ExternalProject_Add(usrsctp @@ -107,4 +110,5 @@ ExternalProject_Add(usrsctp -Dsctp_build_programs=off -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/dist -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 )