ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
ice.c
Go to the documentation of this file.
1
11#include <ascii-chat/network/webrtc/ice.h>
12#include <ascii-chat/network/webrtc/webrtc.h>
13#include <ascii-chat/log/logging.h>
14#include <ascii-chat/platform/util.h>
15#include <ascii-chat/util/pcre2.h>
16#include <ascii-chat/util/ip.h>
17#include <string.h>
18#include <stdio.h>
19#include <stdlib.h>
20#define PCRE2_CODE_UNIT_WIDTH 8
21#include <pcre2.h>
22
23/* ============================================================================
24 * Utility Functions
25 * ============================================================================ */
26
27const char *ice_candidate_type_name(ice_candidate_type_t type) {
28 switch (type) {
29 case ICE_CANDIDATE_HOST:
30 return "host";
31 case ICE_CANDIDATE_SRFLX:
32 return "srflx";
33 case ICE_CANDIDATE_PRFLX:
34 return "prflx";
35 case ICE_CANDIDATE_RELAY:
36 return "relay";
37 default:
38 return "unknown";
39 }
40}
41
42const char *ice_protocol_name(ice_protocol_t protocol) {
43 switch (protocol) {
44 case ICE_PROTOCOL_UDP:
45 return "udp";
46 case ICE_PROTOCOL_TCP:
47 return "tcp";
48 default:
49 return "unknown";
50 }
51}
52
53/* ============================================================================
54 * ICE Candidate Priority
55 * ============================================================================ */
56
73static uint16_t ice_calculate_local_preference_by_ip(const char *ip_address) {
74 if (!ip_address || ip_address[0] == '\0') {
75 return 0; // Invalid/empty
76 }
77
78 // Check for wildcard addresses (should not appear in candidates, but handle gracefully)
79 if (strcmp(ip_address, "0.0.0.0") == 0 || strcmp(ip_address, "::") == 0) {
80 return 0; // Invalid candidate
81 }
82
83 // LAN addresses: Highest priority (same network = lowest latency)
84 if (is_lan_ipv4(ip_address) || is_lan_ipv6(ip_address)) {
85 return 65535;
86 }
87
88 // Localhost: High priority but less useful than LAN for peer-to-peer
89 if (is_localhost_ipv4(ip_address) || is_localhost_ipv6(ip_address)) {
90 return 65000;
91 }
92
93 // Internet (public): Medium priority
94 if (is_internet_ipv4(ip_address) || is_internet_ipv6(ip_address)) {
95 return 32768;
96 }
97
98 // Unknown/unclassified: Lowest priority
99 return 0;
100}
101
102uint32_t ice_calculate_priority(ice_candidate_type_t type, uint16_t local_preference, uint8_t component_id) {
103 // RFC 5245: priority = (2^24 * typePreference) + (2^8 * localPreference) + (256 - componentID)
104
105 // Type preference (higher is better):
106 // - Host: 126 (most preferred, lowest latency)
107 // - SRFLX: 100 (STUN-discovered address)
108 // - PRFLX: 110 (discovered during connectivity checks)
109 // - Relay: 0 (least preferred, uses bandwidth)
110
111 uint8_t type_preference = 0;
112 switch (type) {
113 case ICE_CANDIDATE_HOST:
114 type_preference = 126;
115 break;
116 case ICE_CANDIDATE_SRFLX:
117 type_preference = 100;
118 break;
119 case ICE_CANDIDATE_PRFLX:
120 type_preference = 110;
121 break;
122 case ICE_CANDIDATE_RELAY:
123 type_preference = 0;
124 break;
125 default:
126 type_preference = 0;
127 break;
128 }
129
130 uint32_t priority =
131 (((uint32_t)type_preference) << 24) | (((uint32_t)local_preference) << 8) | (256 - (uint32_t)component_id);
132
133 return priority;
134}
135
136uint32_t ice_calculate_priority_for_candidate(const ice_candidate_t *candidate) {
137 if (!candidate) {
138 return 0;
139 }
140
141 // Calculate local preference based on IP classification
142 uint16_t local_pref = ice_calculate_local_preference_by_ip(candidate->ip_address);
143
144 // Use standard RFC 5245 priority formula with IP-aware local preference
145 return ice_calculate_priority(candidate->type, local_pref, (uint8_t)candidate->component_id);
146}
147
148/* ============================================================================
149 * ICE Candidate Parsing with PCRE2
150 * ============================================================================ */
151
159static const char *ICE_CANDIDATE_PATTERN = "^([^ ]+)\\s+" // 1: foundation
160 "(\\d+)\\s+" // 2: component_id
161 "(udp|tcp)\\s+" // 3: protocol
162 "(\\d+)\\s+" // 4: priority
163 "([a-fA-F0-9.:]+)\\s+" // 5: ip_address (IPv4 or IPv6)
164 "(\\d+)\\s+" // 6: port
165 "typ\\s+" // literal "typ"
166 "(host|srflx|prflx|relay)" // 7: candidate_type
167 "(?:\\s+raddr\\s+([a-fA-F0-9.:]+))?" // 8: optional raddr
168 "(?:\\s+rport\\s+(\\d+))?" // 9: optional rport
169 "(?:\\s+tcptype\\s+(active|passive|so))?" // 10: optional tcptype
170 "\\s*$";
171
172static pcre2_singleton_t *g_ice_candidate_regex = NULL;
173
178static pcre2_code *ice_candidate_regex_get(void) {
179 if (g_ice_candidate_regex == NULL) {
180 g_ice_candidate_regex = asciichat_pcre2_singleton_compile(ICE_CANDIDATE_PATTERN, PCRE2_CASELESS);
181 }
182 return asciichat_pcre2_singleton_get_code(g_ice_candidate_regex);
183}
184
195static asciichat_error_t ice_parse_candidate_pcre2(const char *line, ice_candidate_t *candidate) {
196 if (!line || !candidate) {
197 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid candidate parameters");
198 }
199
200 // Get compiled regex (lazy initialization)
201 pcre2_code *regex = ice_candidate_regex_get();
202
203 // If PCRE2 not available, return error to fall back to manual parser
204 if (!regex) {
205 return ERROR_MEMORY; // Signal to use fallback
206 }
207
208 pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(regex, NULL);
209 if (!match_data) {
210 return ERROR_MEMORY;
211 }
212
213 int rc = pcre2_jit_match(regex, (PCRE2_SPTR8)line, strlen(line), 0, 0, match_data, NULL);
214
215 if (rc < 7) {
216 // Must have at least foundation, component, protocol, priority, ip, port, type (7 groups)
217 pcre2_match_data_free(match_data);
218 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid ICE candidate format");
219 }
220
221 memset(candidate, 0, sizeof(ice_candidate_t));
222
223 // Extract foundation (group 1)
224 char *foundation = asciichat_pcre2_extract_group(match_data, 1, line);
225 if (foundation && strlen(foundation) < sizeof(candidate->foundation)) {
226 SAFE_STRNCPY(candidate->foundation, foundation, sizeof(candidate->foundation));
227 }
228 SAFE_FREE(foundation);
229
230 // Extract component_id (group 2)
231 unsigned long component_id;
232 if (asciichat_pcre2_extract_group_ulong(match_data, 2, line, &component_id)) {
233 candidate->component_id = (uint32_t)component_id;
234 }
235
236 // Extract protocol (group 3)
237 char *protocol_str = asciichat_pcre2_extract_group(match_data, 3, line);
238 if (protocol_str) {
239 if (strcmp(protocol_str, "udp") == 0) {
240 candidate->protocol = ICE_PROTOCOL_UDP;
241 } else if (strcmp(protocol_str, "tcp") == 0) {
242 candidate->protocol = ICE_PROTOCOL_TCP;
243 } else {
244 SAFE_FREE(protocol_str);
245 pcre2_match_data_free(match_data);
246 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid protocol");
247 }
248 SAFE_FREE(protocol_str);
249 }
250
251 // Extract priority (group 4)
252 unsigned long priority;
253 if (asciichat_pcre2_extract_group_ulong(match_data, 4, line, &priority)) {
254 candidate->priority = (uint32_t)priority;
255 }
256
257 // Extract IP address (group 5)
258 char *ip_address = asciichat_pcre2_extract_group(match_data, 5, line);
259 if (ip_address && strlen(ip_address) < sizeof(candidate->ip_address)) {
260 SAFE_STRNCPY(candidate->ip_address, ip_address, sizeof(candidate->ip_address));
261 }
262 SAFE_FREE(ip_address);
263
264 // Extract port (group 6)
265 unsigned long port;
266 if (asciichat_pcre2_extract_group_ulong(match_data, 6, line, &port)) {
267 candidate->port = (uint16_t)port;
268 }
269
270 // Extract candidate type (group 7)
271 char *type_str = asciichat_pcre2_extract_group(match_data, 7, line);
272 if (type_str) {
273 if (strcmp(type_str, "host") == 0) {
274 candidate->type = ICE_CANDIDATE_HOST;
275 } else if (strcmp(type_str, "srflx") == 0) {
276 candidate->type = ICE_CANDIDATE_SRFLX;
277 } else if (strcmp(type_str, "prflx") == 0) {
278 candidate->type = ICE_CANDIDATE_PRFLX;
279 } else if (strcmp(type_str, "relay") == 0) {
280 candidate->type = ICE_CANDIDATE_RELAY;
281 } else {
282 SAFE_FREE(type_str);
283 pcre2_match_data_free(match_data);
284 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid candidate type");
285 }
286 SAFE_FREE(type_str);
287 }
288
289 // Extract optional raddr (group 8)
290 char *raddr = asciichat_pcre2_extract_group(match_data, 8, line);
291 if (raddr && strlen(raddr) < sizeof(candidate->raddr)) {
292 SAFE_STRNCPY(candidate->raddr, raddr, sizeof(candidate->raddr));
293 }
294 SAFE_FREE(raddr);
295
296 // Extract optional rport (group 9)
297 unsigned long rport;
298 if (asciichat_pcre2_extract_group_ulong(match_data, 9, line, &rport)) {
299 candidate->rport = (uint16_t)rport;
300 }
301
302 // Extract optional tcptype (group 10)
303 char *tcptype_str = asciichat_pcre2_extract_group(match_data, 10, line);
304 if (tcptype_str) {
305 if (strcmp(tcptype_str, "active") == 0) {
306 candidate->tcp_type = ICE_TCP_TYPE_ACTIVE;
307 } else if (strcmp(tcptype_str, "passive") == 0) {
308 candidate->tcp_type = ICE_TCP_TYPE_PASSIVE;
309 } else if (strcmp(tcptype_str, "so") == 0) {
310 candidate->tcp_type = ICE_TCP_TYPE_SO;
311 }
312 SAFE_FREE(tcptype_str);
313 }
314
315 pcre2_match_data_free(match_data);
316 return ASCIICHAT_OK;
317}
318
319asciichat_error_t ice_parse_candidate(const char *line, ice_candidate_t *candidate) {
320 return ice_parse_candidate_pcre2(line, candidate);
321}
322
323asciichat_error_t ice_format_candidate(const ice_candidate_t *candidate, char *line, size_t line_size) {
324 if (!candidate || !line || line_size == 0) {
325 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid candidate format parameters");
326 }
327
328 // Format: foundation component protocol priority ip port typ type [raddr rport] [tcptype type]
329 // Example: 1 1 udp 2130706431 192.168.1.1 54321 typ host
330 // Example with raddr: 1 1 udp 2130706431 203.0.113.45 54321 typ srflx raddr 192.168.1.1 rport 12345
331
332 int written = safe_snprintf(line, line_size, "%s %u %s %u %s %u typ %s", candidate->foundation,
333 candidate->component_id, ice_protocol_name(candidate->protocol), candidate->priority,
334 candidate->ip_address, candidate->port, ice_candidate_type_name(candidate->type));
335
336 if (written < 0 || (size_t)written >= line_size) {
337 return SET_ERRNO(ERROR_BUFFER_FULL, "Candidate line too large for buffer");
338 }
339
340 size_t remaining = line_size - (size_t)written;
341 char *pos = line + written;
342
343 // 2. If srflx/prflx/relay: append raddr and rport
344 if (candidate->type == ICE_CANDIDATE_SRFLX || candidate->type == ICE_CANDIDATE_PRFLX ||
345 candidate->type == ICE_CANDIDATE_RELAY) {
346 if (candidate->raddr[0] != '\0') {
347 int appended = safe_snprintf(pos, remaining, " raddr %s rport %u", candidate->raddr, candidate->rport);
348 if (appended < 0 || (size_t)appended >= remaining) {
349 return SET_ERRNO(ERROR_BUFFER_FULL, "Cannot append raddr/rport to candidate line");
350 }
351 pos += appended;
352 remaining -= (size_t)appended;
353 }
354 }
355
356 // 3. If TCP: append tcptype
357 if (candidate->protocol == ICE_PROTOCOL_TCP) {
358 const char *tcptype_str = "passive"; // default
359 if (candidate->tcp_type == ICE_TCP_TYPE_ACTIVE) {
360 tcptype_str = "active";
361 } else if (candidate->tcp_type == ICE_TCP_TYPE_SO) {
362 tcptype_str = "so";
363 }
364
365 int appended = safe_snprintf(pos, remaining, " tcptype %s", tcptype_str);
366 if (appended < 0 || (size_t)appended >= remaining) {
367 return SET_ERRNO(ERROR_BUFFER_FULL, "Cannot append tcptype to candidate line");
368 }
369 pos += appended;
370 remaining -= (size_t)appended;
371 }
372
373 // 4. Append any extensions
374 if (candidate->extensions[0] != '\0') {
375 int appended = safe_snprintf(pos, remaining, " %s", candidate->extensions);
376 if (appended < 0 || (size_t)appended >= remaining) {
377 return SET_ERRNO(ERROR_BUFFER_FULL, "Cannot append extensions to candidate line");
378 }
379 }
380
381 // 5. Ensure line is null-terminated (snprintf already does this)
382 return ASCIICHAT_OK;
383}
384
385/* ============================================================================
386 * ICE Candidate Gathering
387 * ============================================================================ */
388
389asciichat_error_t ice_gather_candidates(const ice_config_t *config) {
390 if (!config) {
391 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid ICE config");
392 }
393
394 if (!config->send_callback) {
395 return SET_ERRNO(ERROR_INVALID_PARAM, "ICE config missing send_callback");
396 }
397
398 log_debug("ICE: Starting candidate gathering (ufrag=%s, pwd=%s)", config->ufrag, config->pwd);
399
400 // NOTE: libdatachannel (juice) handles ICE candidate gathering internally.
401 // This function is a placeholder for the callback-based API.
402 // The actual gathering happens in the peer_manager when you:
403 // 1. Create peer connection with rtcCreatePeerConnection()
404 // 2. Set ice candidate callback with rtcSetIceCandidateCallback()
405 // 3. libdatachannel automatically gathers candidates from:
406 // - Local interfaces (host candidates)
407 // - STUN servers (server-reflexive candidates)
408 // - TURN servers (relay candidates)
409 // 4. When a candidate is found, rtcSetIceCandidateCallback() is called
410 // 5. Application (webrtc.c) formats and sends via ACDS signaling
411 //
412 // This ice_gather_candidates() function may not be called directly -
413 // the gathering happens automatically when peer connection is created.
414 // Keeping this for API compatibility.
415
416 return ASCIICHAT_OK;
417}
418
419/* ============================================================================
420 * ICE Connectivity
421 * ============================================================================ */
422
423asciichat_error_t ice_add_remote_candidate(webrtc_peer_connection_t *pc, const ice_candidate_t *candidate,
424 const char *mid) {
425 if (!pc || !candidate || !mid) {
426 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid remote candidate parameters");
427 }
428
429 // Validate candidate has required fields
430 if (candidate->ip_address[0] == '\0' || candidate->port == 0) {
431 return SET_ERRNO(ERROR_INVALID_PARAM, "Candidate missing IP address or port");
432 }
433
434 log_debug("ICE: Adding remote candidate %s:%u (type=%s, foundation=%s) on mid=%s", candidate->ip_address,
435 candidate->port, ice_candidate_type_name(candidate->type), candidate->foundation, mid);
436
437 // Format candidate to SDP attribute line for libdatachannel
438 char candidate_line[512];
439 asciichat_error_t err = ice_format_candidate(candidate, candidate_line, sizeof(candidate_line));
440 if (err != ASCIICHAT_OK) {
441 return err;
442 }
443
444 // Delegate to WebRTC layer
445 return webrtc_add_remote_candidate(pc, candidate_line, mid);
446}
447
448bool ice_is_connected(webrtc_peer_connection_t *pc) {
449 if (!pc) {
450 return false;
451 }
452
453 // Check peer connection state
454 webrtc_state_t state = webrtc_get_state(pc);
455 return (state == WEBRTC_STATE_CONNECTED);
456}
457
458// Forward declaration of C++ implementation (in ice_selected_pair.cpp)
459asciichat_error_t ice_get_selected_pair_impl(webrtc_peer_connection_t *pc, ice_candidate_t *local_candidate,
460 ice_candidate_t *remote_candidate);
461
462asciichat_error_t ice_get_selected_pair(webrtc_peer_connection_t *pc, ice_candidate_t *local_candidate,
463 ice_candidate_t *remote_candidate) {
464 // Delegate to C++ implementation which uses libdatachannel's C++ API
465 return ice_get_selected_pair_impl(pc, local_candidate, remote_candidate);
466}
uint32_t ice_calculate_priority_for_candidate(const ice_candidate_t *candidate)
Definition ice.c:136
const char * ice_protocol_name(ice_protocol_t protocol)
Definition ice.c:42
asciichat_error_t ice_get_selected_pair(webrtc_peer_connection_t *pc, ice_candidate_t *local_candidate, ice_candidate_t *remote_candidate)
Definition ice.c:462
uint32_t ice_calculate_priority(ice_candidate_type_t type, uint16_t local_preference, uint8_t component_id)
Definition ice.c:102
bool ice_is_connected(webrtc_peer_connection_t *pc)
Definition ice.c:448
asciichat_error_t ice_format_candidate(const ice_candidate_t *candidate, char *line, size_t line_size)
Definition ice.c:323
asciichat_error_t ice_parse_candidate(const char *line, ice_candidate_t *candidate)
Definition ice.c:319
asciichat_error_t ice_get_selected_pair_impl(webrtc_peer_connection_t *pc, ice_candidate_t *local_candidate, ice_candidate_t *remote_candidate)
Get selected ICE candidate pair (C++ implementation)
const char * ice_candidate_type_name(ice_candidate_type_t type)
Definition ice.c:27
asciichat_error_t ice_add_remote_candidate(webrtc_peer_connection_t *pc, const ice_candidate_t *candidate, const char *mid)
Definition ice.c:423
asciichat_error_t ice_gather_candidates(const ice_config_t *config)
Definition ice.c:389
int is_lan_ipv6(const char *ip)
Definition ip.c:1216
int is_localhost_ipv6(const char *ip)
Definition ip.c:1320
int is_internet_ipv4(const char *ip)
Definition ip.c:1408
int is_lan_ipv4(const char *ip)
Definition ip.c:1185
int is_localhost_ipv4(const char *ip)
Definition ip.c:1299
int is_internet_ipv6(const char *ip)
Definition ip.c:1490
asciichat_error_t webrtc_add_remote_candidate(webrtc_peer_connection_t *pc, const char *candidate, const char *mid)
webrtc_state_t webrtc_get_state(webrtc_peer_connection_t *pc)
pcre2_code * asciichat_pcre2_singleton_get_code(pcre2_singleton_t *singleton)
Get the compiled pcre2_code from a singleton handle.
Definition pcre2.c:95
char * asciichat_pcre2_extract_group(pcre2_match_data *match_data, int group_num, const char *subject)
Extract numbered capture group as allocated string.
Definition pcre2.c:291
bool asciichat_pcre2_extract_group_ulong(pcre2_match_data *match_data, int group_num, const char *subject, unsigned long *out_value)
Extract numbered capture group and convert to unsigned long.
Definition pcre2.c:344
Represents a thread-safe compiled PCRE2 regex singleton.
Definition pcre2.c:21
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456