ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
src/discovery/session.c
Go to the documentation of this file.
1
7#include "session.h"
8
9#include <ascii-chat/common.h>
10#include <ascii-chat/options/options.h>
11#include <ascii-chat/log/logging.h>
12#include <ascii-chat/buffer_pool.h>
13#include <ascii-chat/network/acip/acds.h>
14#include <ascii-chat/network/acip/acds_client.h>
15#include <ascii-chat/network/acip/send.h>
16#include <ascii-chat/network/packet.h>
17#include <ascii-chat/network/webrtc/stun.h>
18#include <ascii-chat/network/webrtc/peer_manager.h>
19#include <ascii-chat/discovery/identity.h>
20#include "ascii-chat/common/error_codes.h"
21#include "negotiate.h"
22#include "nat.h"
23#include <ascii-chat/platform/abstraction.h>
24#include <ascii-chat/platform/socket.h>
25#include <ascii-chat/util/time.h>
26#include <ascii-chat/util/endian.h>
27#include <ascii-chat/crypto/keys.h>
28
29#ifdef _WIN32
30#include <ws2tcpip.h> // For getaddrinfo on Windows
31#include <time.h>
32#else
33#include <netdb.h> // For getaddrinfo on POSIX
34#include <sys/time.h>
35#include <time.h>
36#endif
37
38#include <string.h>
39
43static uint64_t session_get_current_time_ms(void) {
44 uint64_t current_time_ns = time_get_ns();
45 return time_ns_to_ms(current_time_ns);
46}
47
49 if (!config) {
50 SET_ERRNO(ERROR_INVALID_PARAM, "config is NULL");
51 return NULL;
52 }
53
54 discovery_session_t *session = SAFE_CALLOC(1, sizeof(discovery_session_t), discovery_session_t *);
55 if (!session) {
56 SET_ERRNO(ERROR_MEMORY, "Failed to allocate discovery session");
57 return NULL;
58 }
59
60 session->state = DISCOVERY_STATE_INIT;
61 session->acds_socket = INVALID_SOCKET_VALUE;
62 session->peer_manager = NULL;
63 session->webrtc_transport_ready = false;
64 session->webrtc_connection_initiated = false;
65 session->webrtc_retry_attempt = 0;
66 session->webrtc_last_attempt_time_ms = 0;
67 session->stun_servers = NULL;
68 session->stun_count = 0;
69 session->turn_servers = NULL;
70 session->turn_count = 0;
71
72 // Initialize host liveness detection
73 session->liveness.last_ping_sent_ms = 0;
75 session->liveness.consecutive_failures = 0;
76 session->liveness.max_failures = 3;
77 session->liveness.ping_interval_ms = 3 * MS_PER_SEC_INT; // Ping every 3 seconds
78 session->liveness.timeout_ms = 10 * MS_PER_SEC_INT; // 10 second timeout
79 session->liveness.ping_in_flight = false;
80
81 log_info("discovery_session_create: After CALLOC - participant_ctx=%p, host_ctx=%p", session->participant_ctx,
82 session->host_ctx);
83
84 // Load or generate identity key
85 char identity_path[256];
86 asciichat_error_t id_result = acds_identity_default_path(identity_path, sizeof(identity_path));
87 if (id_result == ASCIICHAT_OK) {
88 id_result = acds_identity_load(identity_path, session->identity_pubkey, session->identity_seckey);
89 if (id_result != ASCIICHAT_OK) {
90 // File doesn't exist or is corrupted - generate new key
91 log_info("Identity key not found, generating new key");
92 id_result = acds_identity_generate(session->identity_pubkey, session->identity_seckey);
93 if (id_result == ASCIICHAT_OK) {
94 // Save for future use
95 acds_identity_save(identity_path, session->identity_pubkey, session->identity_seckey);
96 }
97 }
98 }
99
100 if (id_result != ASCIICHAT_OK) {
101 log_warn("Failed to load/generate identity key, using zero key");
102 memset(session->identity_pubkey, 0, sizeof(session->identity_pubkey));
103 memset(session->identity_seckey, 0, sizeof(session->identity_seckey));
104 } else {
105 char fingerprint[65];
106 acds_identity_fingerprint(session->identity_pubkey, fingerprint);
107 log_info("Using identity key with fingerprint: %.16s...", fingerprint);
108 }
109
110 // ACDS connection info
111 if (config->acds_address && config->acds_address[0]) {
112 SAFE_STRNCPY(session->acds_address, config->acds_address, sizeof(session->acds_address));
113 } else {
114 SAFE_STRNCPY(session->acds_address, "127.0.0.1", sizeof(session->acds_address));
115 }
116 session->acds_port = config->acds_port > 0 ? config->acds_port : ACIP_DISCOVERY_DEFAULT_PORT;
117
118 // Session string (if joining)
119 if (config->session_string && config->session_string[0]) {
120 SAFE_STRNCPY(session->session_string, config->session_string, sizeof(session->session_string));
121 session->is_initiator = false;
122 } else {
123 session->is_initiator = true;
124 }
125
126 // Callbacks
127 session->on_state_change = config->on_state_change;
128 session->on_session_ready = config->on_session_ready;
129 session->on_error = config->on_error;
130 session->callback_user_data = config->callback_user_data;
132 session->exit_callback_data = config->exit_callback_data;
133
134 log_debug("Discovery session created (initiator=%d, acds=%s:%u)", session->is_initiator, session->acds_address,
135 session->acds_port);
136
137 return session;
138}
139
141 if (!session) {
142 SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
143 return;
144 }
145
146 // Close ACDS connection
147 if (session->acds_socket != INVALID_SOCKET_VALUE) {
148 socket_close(session->acds_socket);
149 session->acds_socket = INVALID_SOCKET_VALUE;
150 }
151
152 // Destroy host context if active
153 if (session->host_ctx) {
155 session->host_ctx = NULL;
156 }
157
158 // Destroy participant context if active
159 if (session->participant_ctx) {
161 session->participant_ctx = NULL;
162 }
163
164 // Clean up WebRTC peer manager (NEW)
165 if (session->peer_manager) {
167 session->peer_manager = NULL;
168 }
169
170 // Clean up STUN/TURN server arrays (NEW)
171 if (session->stun_servers) {
172 SAFE_FREE(session->stun_servers);
173 session->stun_servers = NULL;
174 session->stun_count = 0;
175 }
176
177 if (session->turn_servers) {
178 SAFE_FREE(session->turn_servers);
179 session->turn_servers = NULL;
180 session->turn_count = 0;
181 }
182
183 SAFE_FREE(session);
184}
185
186static void set_state(discovery_session_t *session, discovery_state_t new_state) {
187 if (!session) {
188 SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
189 return;
190 }
191
192 discovery_state_t old_state = session->state;
193 session->state = new_state;
194
195 log_debug("Discovery state: %d -> %d", old_state, new_state);
196
197 if (session->on_state_change) {
198 session->on_state_change(new_state, session->callback_user_data);
199 }
200}
201
202static void set_error(discovery_session_t *session, asciichat_error_t error, const char *message) {
203 if (!session) {
204 SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
205 return;
206 }
207
208 session->error = error;
209 set_state(session, DISCOVERY_STATE_FAILED);
210
211 log_error("Discovery error: %s", message ? message : "Unknown error");
212
213 if (session->on_error) {
214 session->on_error(error, message, session->callback_user_data);
215 }
216}
217
226static asciichat_error_t gather_nat_quality(nat_quality_t *quality) {
227 if (!quality) {
228 return SET_ERRNO(ERROR_INVALID_PARAM, "quality is NULL");
229 }
230
231 // Initialize with defaults
232 nat_quality_init(quality);
233
234 // Parse STUN servers from options (use fallback endpoint for NAT probe)
235 // Extract just the hostname:port without the "stun:" prefix for nat_detect_quality
236 // If custom servers are configured, use the first one; otherwise use fallback
237 const char *stun_servers_option = GET_OPTION(stun_servers);
238 const char *stun_server_for_probe = OPT_ENDPOINT_STUN_FALLBACK; // Default fallback
239
240 if (stun_servers_option && stun_servers_option[0] != '\0') {
241 // Use the first configured server
242 // nat_detect_quality expects just "host:port" without "stun:" prefix
243 // Use the fallback server, ACDS will provide custom servers in SESSION_CREATED
244 stun_server_for_probe = OPT_ENDPOINT_STUN_FALLBACK;
245 }
246
247 // Strip "stun:" prefix if present
248 const char *probe_host = stun_server_for_probe;
249 if (strncmp(probe_host, "stun:", 5) == 0) {
250 probe_host = stun_server_for_probe + 5;
251 }
252
253 // Run NAT detection (timeout 2 seconds)
254 // This will probe STUN, check UPnP, etc.
255 asciichat_error_t result = nat_detect_quality(quality, probe_host, 0);
256 if (result != ASCIICHAT_OK) {
257 log_warn("NAT detection had issues, using partial data");
258 // Don't fail - we have partial data that's better than nothing
259 }
260
261 return ASCIICHAT_OK;
262}
263
271static asciichat_error_t send_network_quality_to_acds(discovery_session_t *session) {
272 if (!session || session->acds_socket == INVALID_SOCKET_VALUE) {
273 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session or ACDS socket");
274 }
275
276 // Gather our NAT quality
277 nat_quality_t our_quality;
278 asciichat_error_t result = gather_nat_quality(&our_quality);
279 if (result != ASCIICHAT_OK) {
280 log_warn("Failed to gather NAT quality: %d", result);
281 return result;
282 }
283
284 // Convert to wire format
285 acip_nat_quality_t wire_quality;
286 nat_quality_to_acip(&our_quality, session->session_id, session->participant_id, &wire_quality);
287
288 // Send via ACDS
289 asciichat_error_t send_result =
290 packet_send(session->acds_socket, PACKET_TYPE_ACIP_NETWORK_QUALITY, &wire_quality, sizeof(wire_quality));
291
292 if (send_result == ASCIICHAT_OK) {
293 log_debug("Sent NETWORK_QUALITY to ACDS (NAT tier: %d, upload: %u Kbps)", nat_compute_tier(&our_quality),
294 our_quality.upload_kbps);
295 } else {
296 log_error("Failed to send NETWORK_QUALITY: %d", send_result);
297 }
298
299 return send_result;
300}
301
311static asciichat_error_t receive_network_quality_from_acds(discovery_session_t *session) {
312 if (!session || session->acds_socket == INVALID_SOCKET_VALUE) {
313 return SET_ERRNO(ERROR_INVALID_PARAM, "invalid session or ACDS socket");
314 }
315
316 // Receive packet
317 packet_type_t ptype;
318 void *data = NULL;
319 size_t len = 0;
320 asciichat_error_t result = packet_receive(session->acds_socket, &ptype, &data, &len);
321
322 if (result != ASCIICHAT_OK) {
323 if (data)
324 SAFE_FREE(data);
325 return result;
326 }
327
328 // Check if it's a NETWORK_QUALITY packet
329 if (ptype != PACKET_TYPE_ACIP_NETWORK_QUALITY) {
330 log_debug("Received packet type %u (expected NETWORK_QUALITY)", ptype);
331 if (data)
332 SAFE_FREE(data);
333 return ERROR_NETWORK_PROTOCOL;
334 }
335
336 // Parse NETWORK_QUALITY
337 if (len < sizeof(acip_nat_quality_t)) {
338 log_error("NETWORK_QUALITY packet too small: %zu bytes", len);
339 if (data)
340 SAFE_FREE(data);
341 return ERROR_NETWORK_SIZE;
342 }
343
344 acip_nat_quality_t *wire_quality = (acip_nat_quality_t *)data;
345
346 // Convert from wire format
347 nat_quality_from_acip(wire_quality, &session->negotiate.peer_quality);
348 session->negotiate.peer_quality_received = true;
349
350 log_debug("Received NETWORK_QUALITY from peer (NAT tier: %d, upload: %u Kbps)",
352
353 if (data)
354 SAFE_FREE(data);
355 return ASCIICHAT_OK;
356}
357
358static asciichat_error_t connect_to_acds(discovery_session_t *session) {
359 if (!session) {
360 return SET_ERRNO(ERROR_INVALID_PARAM, "null session");
361 }
362
363 set_state(session, DISCOVERY_STATE_CONNECTING_ACDS);
364 log_info("Connecting to ACDS at %s:%u...", session->acds_address, session->acds_port);
365
366 // Resolve hostname using getaddrinfo (supports both IP addresses and hostnames)
367 struct addrinfo hints, *result = NULL;
368 memset(&hints, 0, sizeof(hints));
369 hints.ai_family = AF_UNSPEC; // IPv4 and IPv6
370 hints.ai_socktype = SOCK_STREAM;
371 hints.ai_protocol = IPPROTO_TCP;
372
373 char port_str[8];
374 safe_snprintf(port_str, sizeof(port_str), "%u", session->acds_port);
375
376 int gai_err = getaddrinfo(session->acds_address, port_str, &hints, &result);
377 if (gai_err != 0) {
378 set_error(session, ERROR_NETWORK_CONNECT, "Failed to resolve ACDS address");
379 log_error("getaddrinfo failed: %s", gai_strerror(gai_err));
380 return ERROR_NETWORK_CONNECT;
381 }
382
383 // Create socket
384 session->acds_socket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
385 if (session->acds_socket == INVALID_SOCKET_VALUE) {
386 set_error(session, ERROR_NETWORK, "Failed to create socket");
387 freeaddrinfo(result);
388 return ERROR_NETWORK;
389 }
390
391 // Check exit flag before attempting connect
392 if (session->should_exit_callback && session->should_exit_callback(session->exit_callback_data)) {
393 socket_close(session->acds_socket);
394 session->acds_socket = INVALID_SOCKET_VALUE;
395 freeaddrinfo(result);
396 return ERROR_NETWORK_CONNECT;
397 }
398
399 // Set socket to non-blocking mode for timeout-aware connect
400 if (socket_set_nonblocking(session->acds_socket, true) != 0) {
401 set_error(session, ERROR_NETWORK, "Failed to set non-blocking mode");
402 socket_close(session->acds_socket);
403 session->acds_socket = INVALID_SOCKET_VALUE;
404 freeaddrinfo(result);
405 return ERROR_NETWORK;
406 }
407
408 // Attempt non-blocking connect
409 int connect_result = connect(session->acds_socket, result->ai_addr, (socklen_t)result->ai_addrlen);
410
411 if (connect_result == 0) {
412 // Connected immediately (unlikely but possible for localhost)
413 socket_set_blocking(session->acds_socket);
414 freeaddrinfo(result);
415 log_info("Connected to ACDS");
416 return ASCIICHAT_OK;
417 }
418
419 // Check for EINPROGRESS (non-blocking connect in progress)
420 int last_error = socket_get_last_error();
421 if (!socket_is_in_progress_error(last_error)) {
422 set_error(session, ERROR_NETWORK_CONNECT, "Failed to connect to ACDS");
423 socket_close(session->acds_socket);
424 session->acds_socket = INVALID_SOCKET_VALUE;
425 freeaddrinfo(result);
426 return ERROR_NETWORK_CONNECT;
427 }
428
429 // Poll for connection with 100ms timeout intervals, checking exit flag each iteration
430 const uint64_t start_time_ms = session_get_current_time_ms();
431 const uint64_t max_connect_timeout_ms = 10000; // 10 second hard timeout
432
433 while (1) {
434 // Check if we should exit
435 if (session->should_exit_callback && session->should_exit_callback(session->exit_callback_data)) {
436 log_debug("ACDS connection interrupted by exit signal");
437 socket_close(session->acds_socket);
438 session->acds_socket = INVALID_SOCKET_VALUE;
439 freeaddrinfo(result);
440 return ERROR_NETWORK_CONNECT;
441 }
442
443 // Check if hard timeout exceeded
444 uint64_t elapsed_ms = session_get_current_time_ms() - start_time_ms;
445 if (elapsed_ms >= max_connect_timeout_ms) {
446 log_debug("ACDS connection timeout after %lu ms", elapsed_ms);
447 set_error(session, ERROR_NETWORK_CONNECT, "Connection to ACDS timed out");
448 socket_close(session->acds_socket);
449 session->acds_socket = INVALID_SOCKET_VALUE;
450 freeaddrinfo(result);
451 return ERROR_NETWORK_CONNECT;
452 }
453
454 // Set up select() timeout (100ms)
455 struct timeval tv;
456 tv.tv_sec = 0;
457 tv.tv_usec = 100000; // 100ms
458
459 fd_set writefds, exceptfds;
460 socket_fd_zero(&writefds);
461 socket_fd_zero(&exceptfds);
462 socket_fd_set(session->acds_socket, &writefds);
463 socket_fd_set(session->acds_socket, &exceptfds);
464
465 int select_result = socket_select(session->acds_socket + 1, NULL, &writefds, &exceptfds, &tv);
466
467 if (select_result < 0) {
468 set_error(session, ERROR_NETWORK, "select() failed");
469 socket_close(session->acds_socket);
470 session->acds_socket = INVALID_SOCKET_VALUE;
471 freeaddrinfo(result);
472 return ERROR_NETWORK;
473 }
474
475 if (select_result == 0) {
476 // Timeout, loop again to check exit flag
477 continue;
478 }
479
480 // Socket is ready - check if connection succeeded or failed
481 if (socket_fd_isset(session->acds_socket, &exceptfds)) {
482 // Exception on socket means connection failed
483 int socket_errno = socket_get_error(session->acds_socket);
484 log_debug("ACDS connection failed with error: %d", socket_errno);
485 set_error(session, ERROR_NETWORK_CONNECT, "Failed to connect to ACDS");
486 socket_close(session->acds_socket);
487 session->acds_socket = INVALID_SOCKET_VALUE;
488 freeaddrinfo(result);
489 return ERROR_NETWORK_CONNECT;
490 }
491
492 if (socket_fd_isset(session->acds_socket, &writefds)) {
493 // Verify connection succeeded by checking SO_ERROR
494 int socket_errno = socket_get_error(session->acds_socket);
495 if (socket_errno != 0) {
496 log_debug("ACDS connection failed with SO_ERROR: %d", socket_errno);
497 set_error(session, ERROR_NETWORK_CONNECT, "Failed to connect to ACDS");
498 socket_close(session->acds_socket);
499 session->acds_socket = INVALID_SOCKET_VALUE;
500 freeaddrinfo(result);
501 return ERROR_NETWORK_CONNECT;
502 }
503
504 // Connection succeeded - restore blocking mode for subsequent operations
505 socket_set_blocking(session->acds_socket);
506 freeaddrinfo(result);
507 log_info("Connected to ACDS");
508 return ASCIICHAT_OK;
509 }
510 }
511}
512
513static asciichat_error_t create_session(discovery_session_t *session) {
514 if (!session) {
515 return SET_ERRNO(ERROR_INVALID_PARAM, "null session");
516 }
517
518 set_state(session, DISCOVERY_STATE_CREATING_SESSION);
519 log_info("Creating new discovery session...");
520
521 // Build SESSION_CREATE message
522 acip_session_create_t create_msg;
523 memset(&create_msg, 0, sizeof(create_msg));
524
525 // Set identity public key
526 memcpy(create_msg.identity_pubkey, session->identity_pubkey, 32);
527
528 // Set timestamp for signature
529 create_msg.timestamp = session_get_current_time_ms();
530
531 // Set capabilities
532 create_msg.capabilities = 0x03; // Video + Audio
533 create_msg.max_participants = 8;
534
535 // Generate signature (signs: type || timestamp || capabilities || max_participants)
536 asciichat_error_t sig_result =
537 acds_sign_session_create(session->identity_seckey, create_msg.timestamp, create_msg.capabilities,
538 create_msg.max_participants, create_msg.signature);
539 if (sig_result != ASCIICHAT_OK) {
540 set_error(session, sig_result, "Failed to sign SESSION_CREATE");
541 return sig_result;
542 }
543
544 // Check if WebRTC is preferred
545 const options_t *opts = options_get();
546 if (opts && opts->prefer_webrtc) {
547 log_info("DISCOVERY: WebRTC preferred, using SESSION_TYPE_WEBRTC");
548 create_msg.session_type = SESSION_TYPE_WEBRTC;
549 } else {
550 log_info("DISCOVERY: Using direct TCP (SESSION_TYPE_DIRECT_TCP)");
551 create_msg.session_type = SESSION_TYPE_DIRECT_TCP;
552 }
553
554 create_msg.has_password = 0;
555 create_msg.expose_ip_publicly = 0;
556 create_msg.reserved_string_len = 0;
557
558 // Set server address (will be updated after NAT detection)
559 SAFE_STRNCPY(create_msg.server_address, "127.0.0.1", sizeof(create_msg.server_address));
560 create_msg.server_port = ACIP_HOST_DEFAULT_PORT;
561
562 // Send SESSION_CREATE
563 asciichat_error_t result =
564 packet_send(session->acds_socket, PACKET_TYPE_ACIP_SESSION_CREATE, &create_msg, sizeof(create_msg));
565 if (result != ASCIICHAT_OK) {
566 set_error(session, result, "Failed to send SESSION_CREATE");
567 return result;
568 }
569
570 // Receive SESSION_CREATED response
571 packet_type_t type;
572 void *data = NULL;
573 size_t len = 0;
574
575 result = packet_receive(session->acds_socket, &type, &data, &len);
576 if (result != ASCIICHAT_OK) {
577 set_error(session, result, "Failed to receive SESSION_CREATED");
578 return result;
579 }
580
581 if (type == PACKET_TYPE_ACIP_ERROR) {
582 acip_error_t *error = (acip_error_t *)data;
583 log_error("ACDS error: %s", error->error_message);
584 set_error(session, ERROR_NETWORK_PROTOCOL, error->error_message);
585 POOL_FREE(data, len);
586 return ERROR_NETWORK_PROTOCOL;
587 }
588
589 if (type != PACKET_TYPE_ACIP_SESSION_CREATED || len < sizeof(acip_session_created_t)) {
590 set_error(session, ERROR_NETWORK_PROTOCOL, "Unexpected response to SESSION_CREATE");
591 POOL_FREE(data, len);
592 return ERROR_NETWORK_PROTOCOL;
593 }
594
595 acip_session_created_t *created = (acip_session_created_t *)data;
596
597 // Store session info
598 memcpy(session->session_id, created->session_id, 16);
599 SAFE_STRNCPY(session->session_string, created->session_string, sizeof(session->session_string));
600
601 // We're also participant 0 (initiator)
602 memcpy(session->participant_id, created->session_id, 16); // Use session ID as participant ID for initiator
603 memcpy(session->initiator_id, session->participant_id, 16);
604
605 log_info("Session created: %s", session->session_string);
606
607 POOL_FREE(data, len);
608
609 // Notify that session is ready
610 if (session->on_session_ready) {
611 session->on_session_ready(session->session_string, session->callback_user_data);
612 }
613
614 // Wait for peer to join
615 set_state(session, DISCOVERY_STATE_WAITING_PEER);
616
617 return ASCIICHAT_OK;
618}
619
620// ============================================================================
621// WebRTC Support Functions (NEW)
622// ============================================================================
623
627static asciichat_error_t discovery_send_sdp_via_acds(const uint8_t session_id[16], const uint8_t recipient_id[16],
628 const char *sdp_type, const char *sdp, void *user_data) {
629 discovery_session_t *session = (discovery_session_t *)user_data;
630
631 if (!session || session->acds_socket == INVALID_SOCKET_VALUE) {
632 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session or ACDS socket");
633 }
634
635 // Build ACIP SDP packet
636 size_t sdp_len = strlen(sdp);
637 size_t total_len = sizeof(acip_webrtc_sdp_t) + sdp_len;
638
639 uint8_t *packet_data = SAFE_MALLOC(total_len, uint8_t *);
640 if (!packet_data) {
641 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate SDP packet");
642 }
643 acip_webrtc_sdp_t *sdp_msg = (acip_webrtc_sdp_t *)packet_data;
644
645 memcpy(sdp_msg->session_id, session_id, 16);
646 memcpy(sdp_msg->sender_id, session->participant_id, 16);
647 memcpy(sdp_msg->recipient_id, recipient_id, 16);
648 sdp_msg->sdp_type = (strcmp(sdp_type, "offer") == 0) ? 0 : 1;
649 sdp_msg->sdp_len = HOST_TO_NET_U16(sdp_len);
650 memcpy(packet_data + sizeof(acip_webrtc_sdp_t), sdp, sdp_len);
651
652 // Send to ACDS
653 asciichat_error_t result = packet_send(session->acds_socket, PACKET_TYPE_ACIP_WEBRTC_SDP, packet_data, total_len);
654
655 SAFE_FREE(packet_data);
656
657 if (result != ASCIICHAT_OK) {
658 return SET_ERRNO(ERROR_NETWORK, "Failed to send SDP to ACDS");
659 }
660
661 log_info("Sent SDP %s to ACDS (len=%zu)", sdp_type, sdp_len);
662 return ASCIICHAT_OK;
663}
664
668static asciichat_error_t discovery_send_ice_via_acds(const uint8_t session_id[16], const uint8_t recipient_id[16],
669 const char *candidate, const char *mid, void *user_data) {
670 discovery_session_t *session = (discovery_session_t *)user_data;
671
672 if (!session || session->acds_socket == INVALID_SOCKET_VALUE) {
673 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session or ACDS socket");
674 }
675
676 // Calculate payload length (candidate + null + mid + null)
677 size_t candidate_len = strlen(candidate);
678 size_t mid_len = strlen(mid);
679 size_t payload_len = candidate_len + 1 + mid_len + 1;
680
681 // Allocate packet buffer (header + payload)
682 size_t total_len = sizeof(acip_webrtc_ice_t) + payload_len;
683 uint8_t *packet_data = SAFE_MALLOC(total_len, uint8_t *);
684 if (!packet_data) {
685 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ICE packet");
686 }
687
688 // Fill header
689 acip_webrtc_ice_t *ice_msg = (acip_webrtc_ice_t *)packet_data;
690 memcpy(ice_msg->session_id, session_id, 16);
691 memcpy(ice_msg->sender_id, session->participant_id, 16);
692 memcpy(ice_msg->recipient_id, recipient_id, 16);
693 ice_msg->candidate_len = HOST_TO_NET_U16((uint16_t)candidate_len);
694
695 // Copy candidate and mid after header
696 uint8_t *payload = packet_data + sizeof(acip_webrtc_ice_t);
697 memcpy(payload, candidate, candidate_len);
698 payload[candidate_len] = '\0';
699 memcpy(payload + candidate_len + 1, mid, mid_len);
700 payload[candidate_len + 1 + mid_len] = '\0';
701
702 log_debug("Sending ICE candidate to ACDS (candidate_len=%zu, mid=%s)", candidate_len, mid);
703
704 // Send to ACDS
705 asciichat_error_t result = packet_send(session->acds_socket, PACKET_TYPE_ACIP_WEBRTC_ICE, packet_data, total_len);
706
707 SAFE_FREE(packet_data);
708
709 if (result != ASCIICHAT_OK) {
710 return SET_ERRNO(ERROR_NETWORK, "Failed to send ICE to ACDS");
711 }
712
713 log_debug("Sent ICE candidate to ACDS (candidate_len=%zu, mid=%s)", candidate_len, mid);
714 return ASCIICHAT_OK;
715}
716
725static uint32_t calculate_backoff_delay_ms(int attempt) {
726 // Base delay: 2^attempt seconds
727 uint32_t base_delay_ms = 1000U << attempt; // 1s, 2s, 4s, 8s, 16s...
728
729 // Cap at 30 seconds
730 if (base_delay_ms > 30 * MS_PER_SEC_INT) {
731 base_delay_ms = 30 * MS_PER_SEC_INT;
732 }
733
734 // Add jitter (0-1000ms) to prevent thundering herd
735 // Use monotonic time as pseudo-random seed
736 uint32_t jitter_ms = (uint32_t)(platform_get_monotonic_time_us() % 1000);
737
738 return base_delay_ms + jitter_ms;
739}
740
748static void discovery_on_gathering_timeout(const uint8_t participant_id[16], uint32_t timeout_ms, uint64_t elapsed_ms,
749 void *user_data) {
750 (void)user_data; // Session context not needed for logging
751
752 log_warn("Peer connection failed to gather ICE candidates");
753 log_debug(" Participant: %02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", participant_id[0],
757 log_error(" Timeout: %u ms", timeout_ms);
758 log_error(" Elapsed: %llu ms", (unsigned long long)elapsed_ms);
759 log_error("===========================================");
760 log_warn("Possible causes: STUN/TURN servers unreachable, firewall blocking UDP, network issues");
761}
762
777static void discovery_on_transport_ready(acip_transport_t *transport, const uint8_t participant_id[16],
778 void *user_data) {
779 discovery_session_t *session = (discovery_session_t *)user_data;
780
781 if (!session) {
782 SET_ERRNO(ERROR_INVALID_STATE, "discovery_on_transport_ready: session is NULL");
783 return;
784 }
785
786 log_info("========== WebRTC DataChannel READY ==========");
787 log_info("WebRTC DataChannel successfully established and wrapped in ACIP transport");
788 log_debug(" Remote participant: %02x%02x%02x%02x-...", participant_id[0], participant_id[1], participant_id[2],
789 participant_id[3]);
790 log_info(" Transport status: READY for media transmission");
791 log_info(" Encryption: DTLS (automatic) + optional ACIP layer encryption");
792
793 // Mark transport as ready
794 session->webrtc_transport_ready = true;
795
796 if (session->session_type != SESSION_TYPE_WEBRTC) {
797 log_warn("WebRTC transport ready but session type is not WebRTC!");
798 return;
799 }
800
801 log_info("========== WebRTC Media Channel ACTIVE ==========");
802 log_info("Switching to WebRTC transport for media transmission...");
803
804 // Implement transport switching for participant/host
805 if (session->is_host && session->host_ctx) {
806 // We are the host - set transport for the remote client
807 // participant_id contains the client's ID
808 uint32_t client_id = 0;
809 // Extract client ID from participant_id (use first 4 bytes as uint32)
810 memcpy(&client_id, participant_id, sizeof(uint32_t));
811
812 asciichat_error_t result = session_host_set_client_transport(session->host_ctx, client_id, transport);
813 if (result == ASCIICHAT_OK) {
814 log_info("✓ Host: Successfully switched client %u to WebRTC transport", client_id);
815 } else {
816 log_error("✗ Host: Failed to set WebRTC transport for client %u: %d", client_id, result);
817 }
818 } else if (session->participant_ctx) {
819 // We are a participant - set transport for ourselves
820 asciichat_error_t result = session_participant_set_transport(session->participant_ctx, transport);
821 if (result == ASCIICHAT_OK) {
822 log_info("✓ Participant: Successfully switched to WebRTC transport");
823 log_info(" TCP connection remains open as control/signaling channel");
824 log_info(" Media frames will now be transmitted over WebRTC DataChannel");
825 } else {
826 log_error("✗ Participant: Failed to set WebRTC transport: %d", result);
827 }
828 } else {
829 log_warn("transport_ready: No participant or host context available for transport switching");
830 }
831}
832
839static asciichat_error_t initialize_webrtc_peer_manager(discovery_session_t *session) {
840 if (!session) {
841 return SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
842 }
843
844 // Initialize WebRTC library (idempotent)
845 asciichat_error_t init_result = webrtc_init();
846 if (init_result != ASCIICHAT_OK) {
847 return SET_ERRNO(ERROR_NETWORK, "Failed to initialize WebRTC library");
848 }
849
850 // Set up STUN servers (unless --webrtc-skip-stun is set)
851 if (GET_OPTION(webrtc_skip_stun)) {
852 log_info("Skipping STUN (--webrtc-skip-stun) - will use TURN relay only");
853 session->stun_servers = NULL;
854 session->stun_count = 0;
855 } else {
856 // Parse STUN servers from options (supports up to 4 servers for redundancy)
857 const int max_stun_servers = 4;
858 session->stun_servers = SAFE_MALLOC(max_stun_servers * sizeof(stun_server_t), stun_server_t *);
859 if (!session->stun_servers) {
860 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate STUN server array");
861 }
862
863 // Parse comma-separated STUN server list from options
864 session->stun_count = stun_servers_parse(GET_OPTION(stun_servers), OPT_ENDPOINT_STUN_SERVERS_DEFAULT,
865 session->stun_servers, max_stun_servers);
866
867 if (session->stun_count == 0) {
868 log_warn("No STUN servers configured, WebRTC may fail with symmetric NAT");
869 SAFE_FREE(session->stun_servers);
870 session->stun_servers = NULL;
871 } else {
872 log_info("Configured %d STUN server(s) for WebRTC", session->stun_count);
873 }
874 }
875
876 // Set up TURN servers (if credentials available from ACDS and not disabled)
877 if (GET_OPTION(webrtc_disable_turn)) {
878 log_info("TURN disabled (--webrtc-disable-turn) - will use direct P2P + STUN only");
879 session->turn_servers = NULL;
880 session->turn_count = 0;
881 } else if (session->turn_username[0] != '\0' && session->turn_password[0] != '\0') {
882 // Parse TURN servers from options and apply ACDS credentials to each
883 const char *turn_servers_str = GET_OPTION(turn_servers);
884 if (!turn_servers_str || turn_servers_str[0] == '\0') {
885 turn_servers_str = OPT_ENDPOINT_TURN_SERVERS_DEFAULT;
886 }
887
888 // Allocate array for up to 4 TURN servers
889 const int max_turn_servers = 4;
890 session->turn_servers = SAFE_MALLOC(max_turn_servers * sizeof(turn_server_t), turn_server_t *);
891 if (!session->turn_servers) {
892 SAFE_FREE(session->stun_servers);
893 session->stun_servers = NULL;
894 session->stun_count = 0;
895 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate TURN server array");
896 }
897
898 // Parse comma-separated TURN server URLs
899 char turn_copy[OPTIONS_BUFF_SIZE];
900 SAFE_STRNCPY(turn_copy, turn_servers_str, sizeof(turn_copy));
901 session->turn_count = 0;
902
903 char *saveptr = NULL;
904 char *token = platform_strtok_r(turn_copy, ",", &saveptr);
905 while (token && session->turn_count < max_turn_servers) {
906 // Trim whitespace
907 while (*token == ' ' || *token == '\t')
908 token++;
909 size_t len = strlen(token);
910 while (len > 0 && (token[len - 1] == ' ' || token[len - 1] == '\t')) {
911 token[--len] = '\0';
912 }
913
914 if (len > 0 && len < sizeof(session->turn_servers[0].url)) {
915 // Set TURN server URL
916 session->turn_servers[session->turn_count].url_len = (uint8_t)len;
917 SAFE_STRNCPY(session->turn_servers[session->turn_count].url, token,
918 sizeof(session->turn_servers[session->turn_count].url));
919
920 // Apply ACDS-provided credentials to this TURN server
921 session->turn_servers[session->turn_count].username_len = strlen(session->turn_username);
922 SAFE_STRNCPY(session->turn_servers[session->turn_count].username, session->turn_username,
923 sizeof(session->turn_servers[session->turn_count].username));
924
925 session->turn_servers[session->turn_count].credential_len = strlen(session->turn_password);
926 SAFE_STRNCPY(session->turn_servers[session->turn_count].credential, session->turn_password,
927 sizeof(session->turn_servers[session->turn_count].credential));
928
929 session->turn_count++;
930 } else if (len > 0) {
931 log_warn("TURN server URL too long (max 63 chars): %s", token);
932 }
933
934 token = platform_strtok_r(NULL, ",", &saveptr);
935 }
936
937 if (session->turn_count > 0) {
938 log_info("Configured %d TURN server(s) with ACDS credentials for symmetric NAT relay", session->turn_count);
939 } else {
940 log_warn("No valid TURN servers configured");
941 SAFE_FREE(session->turn_servers);
942 session->turn_servers = NULL;
943 }
944 } else {
945 log_info("No TURN credentials from ACDS - will rely on direct P2P + STUN (sufficient for most NAT scenarios)");
946 session->turn_servers = NULL;
947 session->turn_count = 0;
948 }
949
950 // Determine our role for WebRTC peer connection
951 // Participants always use JOINER role (they initiate connections)
952 webrtc_peer_role_t role = WEBRTC_ROLE_JOINER;
953
954 log_info("Initializing WebRTC peer manager (role=%s, stun_count=%zu, turn_count=%zu)",
955 role == WEBRTC_ROLE_JOINER ? "JOINER" : "CREATOR", session->stun_count, session->turn_count);
956
957 // Create peer manager configuration
958 webrtc_peer_manager_config_t pm_config = {
959 .role = role,
960 .stun_servers = session->stun_servers,
961 .stun_count = session->stun_count,
962 .turn_servers = session->turn_servers,
963 .turn_count = session->turn_count,
964 .on_transport_ready = discovery_on_transport_ready,
965 .on_gathering_timeout = discovery_on_gathering_timeout,
966 .user_data = session,
967 .crypto_ctx = NULL // Crypto happens at ACIP layer
968 };
969
970 // Create signaling callbacks
971 webrtc_signaling_callbacks_t signaling = {
972 .send_sdp = discovery_send_sdp_via_acds, .send_ice = discovery_send_ice_via_acds, .user_data = session};
973
974 // Create peer manager
975 asciichat_error_t result = webrtc_peer_manager_create(&pm_config, &signaling, &session->peer_manager);
976 if (result != ASCIICHAT_OK) {
977 // Clean up on error
978 SAFE_FREE(session->stun_servers);
979 session->stun_servers = NULL;
980 session->stun_count = 0;
981 if (session->turn_servers) {
982 SAFE_FREE(session->turn_servers);
983 session->turn_servers = NULL;
984 }
985 session->turn_count = 0;
986 }
987
988 return result;
989}
990
994static void handle_discovery_webrtc_sdp(discovery_session_t *session, void *data, size_t len) {
995 if (len < sizeof(acip_webrtc_sdp_t)) {
996 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP packet size: %zu", len);
997 return;
998 }
999
1000 const acip_webrtc_sdp_t *sdp_msg = (const acip_webrtc_sdp_t *)data;
1001 uint16_t sdp_len = NET_TO_HOST_U16(sdp_msg->sdp_len);
1002
1003 if (len != sizeof(acip_webrtc_sdp_t) + sdp_len) {
1004 log_error("SDP packet size mismatch: expected %zu, got %zu", sizeof(acip_webrtc_sdp_t) + sdp_len, len);
1005 return;
1006 }
1007
1008 // Extract SDP string
1009 char *sdp_str = SAFE_MALLOC(sdp_len + 1, char *);
1010 if (!sdp_str) {
1011 log_error("Failed to allocate SDP string");
1012 return;
1013 }
1014 memcpy(sdp_str, (const uint8_t *)data + sizeof(acip_webrtc_sdp_t), sdp_len);
1015 sdp_str[sdp_len] = '\0';
1016
1017 const char *sdp_type = (sdp_msg->sdp_type == 0) ? "offer" : "answer";
1018 log_info("Received SDP %s from ACDS (len=%u)", sdp_type, sdp_len);
1019
1020 // Forward to peer manager
1021 if (session->peer_manager) {
1022 asciichat_error_t result = webrtc_peer_manager_handle_sdp(session->peer_manager, sdp_msg);
1023 if (result != ASCIICHAT_OK) {
1024 log_error("Failed to handle SDP: %d", result);
1025 }
1026 } else {
1027 log_error("Received SDP but peer manager not initialized");
1028 }
1029
1030 SAFE_FREE(sdp_str);
1031}
1032
1036static void handle_discovery_webrtc_ice(discovery_session_t *session, void *data, size_t len) {
1037 if (len < sizeof(acip_webrtc_ice_t)) {
1038 log_error("Invalid ICE packet size: %zu", len);
1039 return;
1040 }
1041
1042 const acip_webrtc_ice_t *ice_msg = (const acip_webrtc_ice_t *)data;
1043 uint16_t candidate_len = NET_TO_HOST_U16(ice_msg->candidate_len);
1044
1045 log_debug("★ ICE VALIDATION: len=%zu, sizeof(header)=%zu, candidate_len=%u, expected=%zu", len,
1046 sizeof(acip_webrtc_ice_t), candidate_len, sizeof(acip_webrtc_ice_t) + candidate_len);
1047
1048 // FIXED: The payload contains candidate + null + mid + null, not just candidate
1049 // So we can't validate the exact size without parsing the strings first
1050 if (len < sizeof(acip_webrtc_ice_t) + candidate_len) {
1051 log_error("ICE packet too small: expected at least %zu, got %zu", sizeof(acip_webrtc_ice_t) + candidate_len, len);
1052 return;
1053 }
1054
1055 log_debug("Received ICE candidate from ACDS (len=%u)", candidate_len);
1056
1057 // Forward to peer manager
1058 if (session->peer_manager) {
1059 asciichat_error_t result = webrtc_peer_manager_handle_ice(session->peer_manager, ice_msg);
1060 if (result != ASCIICHAT_OK) {
1061 log_error("Failed to handle ICE candidate: %d", result);
1062 }
1063 } else {
1064 log_error("Received ICE but peer manager not initialized");
1065 }
1066}
1067
1071static void handle_acds_webrtc_packet(discovery_session_t *session, packet_type_t type, void *data, size_t len) {
1072 switch (type) {
1073 case PACKET_TYPE_ACIP_WEBRTC_SDP:
1074 handle_discovery_webrtc_sdp(session, data, len);
1075 break;
1076 case PACKET_TYPE_ACIP_WEBRTC_ICE:
1077 handle_discovery_webrtc_ice(session, data, len);
1078 break;
1079 default:
1080 log_debug("Unhandled WebRTC packet type: 0x%04x", type);
1081 }
1082}
1083
1087static asciichat_error_t join_session(discovery_session_t *session) {
1088 log_info("join_session: START - participant_ctx=%p", session->participant_ctx);
1089
1090 if (!session)
1091 return ERROR_INVALID_PARAM;
1092
1093 set_state(session, DISCOVERY_STATE_JOINING_SESSION);
1094 log_info("Joining session: %s (participant_ctx still=%p)", session->session_string, session->participant_ctx);
1095
1096 // Build SESSION_JOIN message
1097 acip_session_join_t join_msg;
1098 memset(&join_msg, 0, sizeof(join_msg));
1099
1100 join_msg.session_string_len = (uint8_t)strlen(session->session_string);
1101 SAFE_STRNCPY(join_msg.session_string, session->session_string, sizeof(join_msg.session_string));
1102
1103 // Set identity public key
1104 memcpy(join_msg.identity_pubkey, session->identity_pubkey, 32);
1105
1106 // Set timestamp for signature
1107 join_msg.timestamp = session_get_current_time_ms();
1108
1109 // Generate signature (signs: type || timestamp || session_string)
1110 asciichat_error_t sig_result =
1111 acds_sign_session_join(session->identity_seckey, join_msg.timestamp, session->session_string, join_msg.signature);
1112 if (sig_result != ASCIICHAT_OK) {
1113 set_error(session, sig_result, "Failed to sign SESSION_JOIN");
1114 return sig_result;
1115 }
1116
1117 join_msg.has_password = 0;
1118
1119 // Send SESSION_JOIN
1120 asciichat_error_t result =
1121 packet_send(session->acds_socket, PACKET_TYPE_ACIP_SESSION_JOIN, &join_msg, sizeof(join_msg));
1122 if (result != ASCIICHAT_OK) {
1123 set_error(session, result, "Failed to send SESSION_JOIN");
1124 return result;
1125 }
1126
1127 // Receive SESSION_JOINED response
1128 packet_type_t type;
1129 void *data = NULL;
1130 size_t len = 0;
1131
1132 result = packet_receive(session->acds_socket, &type, &data, &len);
1133 if (result != ASCIICHAT_OK) {
1134 set_error(session, result, "Failed to receive SESSION_JOINED");
1135 return result;
1136 }
1137
1138 if (type == PACKET_TYPE_ACIP_ERROR) {
1139 acip_error_t *error = (acip_error_t *)data;
1140 log_error("ACDS error: %s", error->error_message);
1141 set_error(session, ERROR_NETWORK_PROTOCOL, error->error_message);
1142 POOL_FREE(data, len);
1143 return ERROR_NETWORK_PROTOCOL;
1144 }
1145
1146 if (type != PACKET_TYPE_ACIP_SESSION_JOINED || len < sizeof(acip_session_joined_t)) {
1147 set_error(session, ERROR_NETWORK_PROTOCOL, "Unexpected response to SESSION_JOIN");
1148 POOL_FREE(data, len);
1149 return ERROR_NETWORK_PROTOCOL;
1150 }
1151
1152 acip_session_joined_t *joined = (acip_session_joined_t *)data;
1153
1154 if (!joined->success) {
1155 log_error("Failed to join session: %s", joined->error_message);
1156 set_error(session, ERROR_NETWORK_PROTOCOL, joined->error_message);
1157 POOL_FREE(data, len);
1158 return ERROR_NETWORK_PROTOCOL;
1159 }
1160
1161 // Store session info
1162 memcpy(session->session_id, joined->session_id, 16);
1163 memcpy(session->participant_id, joined->participant_id, 16);
1164
1165 // Store session type and WebRTC credentials (if WebRTC)
1166 session->session_type = joined->session_type;
1167 if (joined->session_type == 1) { // SESSION_TYPE_WEBRTC
1168 SAFE_STRNCPY(session->turn_username, joined->turn_username, sizeof(session->turn_username));
1169 SAFE_STRNCPY(session->turn_password, joined->turn_password, sizeof(session->turn_password));
1170 log_info("WebRTC session detected - TURN username: %s", session->turn_username);
1171
1172 // Initialize WebRTC peer manager for WebRTC sessions (NEW)
1173 asciichat_error_t webrtc_result = initialize_webrtc_peer_manager(session);
1174 if (webrtc_result != ASCIICHAT_OK) {
1175 log_error("Failed to initialize WebRTC peer manager: %d", webrtc_result);
1176 POOL_FREE(data, len);
1177 return webrtc_result;
1178 }
1179 log_info("WebRTC peer manager initialized successfully");
1180 }
1181
1182 // Check if host is already established
1183 if (joined->server_address[0] && joined->server_port > 0) {
1184 // Host exists - connect directly
1185 SAFE_STRNCPY(session->host_address, joined->server_address, sizeof(session->host_address));
1186 session->host_port = joined->server_port;
1187 session->is_host = false;
1188
1189 log_info("Host already established: %s:%u (session_type=%u, participant_ctx=%p before transition)",
1190 session->host_address, session->host_port, session->session_type, session->participant_ctx);
1191 POOL_FREE(data, len);
1192
1193 log_info("join_session: About to transition to CONNECTING_HOST - participant_ctx=%p", session->participant_ctx);
1194 set_state(session, DISCOVERY_STATE_CONNECTING_HOST);
1195 log_info("join_session: Transitioned to CONNECTING_HOST - participant_ctx=%p (returning ASCIICHAT_OK)",
1196 session->participant_ctx);
1197 return ASCIICHAT_OK;
1198 }
1199
1200 // No host yet - need to negotiate
1201 log_info("No host established (server_address='%s' port=%u), starting negotiation...", joined->server_address,
1202 joined->server_port);
1203 POOL_FREE(data, len);
1204
1205 set_state(session, DISCOVERY_STATE_NEGOTIATING);
1206 return ASCIICHAT_OK;
1207}
1208
1210 log_info("discovery_session_start: ENTRY - session=%p", session);
1211
1212 if (!session) {
1213 return SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
1214 }
1215
1216 log_info("discovery_session_start: is_initiator=%d, session_string='%s'", session->is_initiator,
1217 session->session_string);
1218
1219 // Check if we should exit before starting blocking operations
1220 if (session->should_exit_callback && session->should_exit_callback(session->exit_callback_data)) {
1221 log_info("discovery_session_start: Exiting early due to should_exit_callback (before ACDS connect)");
1222 return ASCIICHAT_OK; // Return success to allow clean shutdown
1223 }
1224
1225 log_info("discovery_session_start: Calling connect_to_acds...");
1226 // Connect to ACDS
1227 asciichat_error_t result = connect_to_acds(session);
1228 if (result != ASCIICHAT_OK) {
1229 log_error("discovery_session_start: connect_to_acds FAILED - result=%d", result);
1230 return result;
1231 }
1232 log_info("discovery_session_start: connect_to_acds succeeded");
1233
1234 // Check again after connection
1235 if (session->should_exit_callback && session->should_exit_callback(session->exit_callback_data)) {
1236 log_info("discovery_session_start: Exiting early due to should_exit_callback (after ACDS connect)");
1237 return ASCIICHAT_OK; // Return success to allow clean shutdown
1238 }
1239
1240 // Create or join session
1241 if (session->is_initiator) {
1242 log_info("discovery_session_start: Calling create_session - participant_ctx before=%p", session->participant_ctx);
1243 asciichat_error_t result = create_session(session);
1244 log_info("discovery_session_start: create_session returned - participant_ctx after=%p, result=%d",
1245 session->participant_ctx, result);
1246 return result;
1247 } else {
1248 log_info("discovery_session_start: Calling join_session - participant_ctx before=%p", session->participant_ctx);
1249 asciichat_error_t result = join_session(session);
1250 log_info("discovery_session_start: join_session returned - participant_ctx after=%p, state=%d, result=%d",
1251 session->participant_ctx, session->state, result);
1252 return result;
1253 }
1254}
1255
1256asciichat_error_t discovery_session_process(discovery_session_t *session, int64_t timeout_ns) {
1257 if (!session) {
1258 return SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
1259 }
1260
1261 // Handle state-specific processing
1262 switch (session->state) {
1264 // Wait for PARTICIPANT_JOINED notification from ACDS
1265 if (session->acds_socket != INVALID_SOCKET_VALUE && timeout_ns > 0) {
1266 // Set socket timeout
1267 if (socket_set_timeout(session->acds_socket, timeout_ns) == 0) {
1268 packet_type_t type;
1269 void *data = NULL;
1270 size_t len = 0;
1271
1272 // Try to receive a packet (non-blocking with timeout)
1273 asciichat_error_t recv_result = packet_receive(session->acds_socket, &type, &data, &len);
1274 if (recv_result == ASCIICHAT_OK) {
1275 if (type == PACKET_TYPE_ACIP_PARTICIPANT_JOINED) {
1276 log_info("Peer joined session, transitioning to negotiation");
1277 set_state(session, DISCOVERY_STATE_NEGOTIATING);
1278 POOL_FREE(data, len);
1279 } else {
1280 // Got some other packet, handle it or ignore
1281 log_debug("Received packet type %d while waiting for peer", type);
1282 POOL_FREE(data, len);
1283 }
1284 } else if (recv_result == ERROR_NETWORK_TIMEOUT) {
1285 // Timeout is normal, just return
1286 } else {
1287 log_debug("Packet receive error while waiting for peer: %d", recv_result);
1288 }
1289 }
1290 } else {
1291 // Fallback to sleep if no timeout or invalid socket
1292 platform_sleep_us(timeout_ns / 1000);
1293 }
1294 break;
1295 }
1296
1298 // Handle NAT negotiation
1299 if (session->negotiate.state == NEGOTIATE_STATE_INIT) {
1300 // Initialize negotiation
1301 negotiate_init(&session->negotiate, session->session_id, session->participant_id, session->is_initiator);
1302 log_info("Starting NAT negotiation (initiator=%d)", session->is_initiator);
1303
1304 // Send our NETWORK_QUALITY to ACDS immediately (let peer receive it)
1305 asciichat_error_t send_result = send_network_quality_to_acds(session);
1306 if (send_result != ASCIICHAT_OK) {
1307 log_warn("Failed to send initial NETWORK_QUALITY: %d", send_result);
1308 // Don't fail - we'll try again next iteration
1309 }
1310 }
1311
1312 // Try to receive peer's NETWORK_QUALITY from ACDS (non-blocking with short timeout)
1313 if (!session->negotiate.peer_quality_received) {
1314 asciichat_error_t recv_result = receive_network_quality_from_acds(session);
1315 if (recv_result == ASCIICHAT_OK) {
1316 // Got peer quality - proceed to election
1317 log_info("Received peer NETWORK_QUALITY, proceeding to host election");
1318 } else if (recv_result != ERROR_NETWORK_TIMEOUT) {
1319 // Real error (not just timeout)
1320 log_debug("Error receiving NETWORK_QUALITY: %d (this might be normal if peer not ready)", recv_result);
1321 }
1322 }
1323
1324 // Check if we have both qualities and can determine host
1326 // Both sides have exchanged NAT quality - determine the winner
1327 log_info("Both NAT qualities received, determining host...");
1328 asciichat_error_t result = negotiate_determine_result(&session->negotiate);
1329 if (result == ASCIICHAT_OK) {
1330 // Store result
1331 session->is_host = session->negotiate.we_are_host;
1332
1333 if (session->is_host) {
1334 // We won the negotiation - become the host
1335 log_info("🏠 NAT negotiation complete: WE ARE HOST (tier:%d vs tier:%d)",
1338 memcpy(session->host_id, session->participant_id, 16);
1339 SAFE_STRNCPY(session->host_address, session->negotiate.host_address, sizeof(session->host_address));
1340 session->host_port = session->negotiate.host_port;
1341 set_state(session, DISCOVERY_STATE_STARTING_HOST);
1342 } else {
1343 // They won - we'll be a participant
1344 log_info("👤 NAT negotiation complete: THEY ARE HOST at %s:%u (tier:%d vs tier:%d)",
1345 session->negotiate.host_address, session->negotiate.host_port,
1348 SAFE_STRNCPY(session->host_address, session->negotiate.host_address, sizeof(session->host_address));
1349 session->host_port = session->negotiate.host_port;
1350 set_state(session, DISCOVERY_STATE_CONNECTING_HOST);
1351 }
1352 } else {
1353 log_error("Failed to determine negotiation result: %d", result);
1354 set_error(session, result, "Host election failed");
1355 }
1356 } else {
1357 // Still waiting for peer quality - sleep briefly to avoid busy loop
1358 platform_sleep_us(timeout_ns / 1000);
1359 }
1360 break;
1361 }
1362
1364 // Start hosting
1365 if (!session->host_ctx) {
1366 // Create host context with configured port
1367 int host_port = GET_OPTION(port);
1368
1369 session_host_config_t hconfig = {
1370 .port = host_port,
1371 .ipv4_address = "0.0.0.0",
1372 .max_clients = 32,
1373 .encryption_enabled = true,
1374 };
1375
1376 session->host_ctx = session_host_create(&hconfig);
1377 if (!session->host_ctx) {
1378 log_error("Failed to create host context");
1379 set_error(session, ERROR_MEMORY, "Failed to create host context");
1380 break;
1381 }
1382
1383 // Start listening
1384 asciichat_error_t hstart = session_host_start(session->host_ctx);
1385 if (hstart != ASCIICHAT_OK) {
1386 log_error("Failed to start host: %d", hstart);
1387 set_error(session, hstart, "Failed to start host");
1388 break;
1389 }
1390
1391 log_info("Host started, listening for connections");
1392 }
1393
1394 // Notify listeners that we're ready
1395 if (session->on_session_ready) {
1396 session->on_session_ready(session->session_string, session->callback_user_data);
1397 }
1398
1399 set_state(session, DISCOVERY_STATE_ACTIVE);
1400 break;
1401 }
1402
1404 // Connect to host as participant
1405
1406 // For WebRTC sessions, wait for DataChannel to open
1407 if (session->session_type == SESSION_TYPE_WEBRTC) {
1408 // Check if transport is ready (DataChannel opened)
1409 if (session->webrtc_transport_ready) {
1410 log_info("WebRTC DataChannel established, transitioning to ACTIVE state");
1411 set_state(session, DISCOVERY_STATE_ACTIVE);
1412 break;
1413 }
1414
1415 // If not ready and we haven't initiated connection yet, initiate it
1416 if (session->peer_manager && !session->webrtc_connection_initiated) {
1417 log_info("Initiating WebRTC connection to host (attempt %d/%d)...", session->webrtc_retry_attempt + 1,
1418 GET_OPTION(webrtc_reconnect_attempts) + 1);
1419
1420 // Initiate connection (generates offer, triggers SDP exchange)
1421 asciichat_error_t conn_result =
1422 webrtc_peer_manager_connect(session->peer_manager, session->session_id, session->host_id);
1423
1424 if (conn_result != ASCIICHAT_OK) {
1425 log_error("Failed to initiate WebRTC connection: %d", conn_result);
1426 set_error(session, conn_result, "Failed to initiate WebRTC connection");
1427 break;
1428 }
1429
1430 log_info("WebRTC connection initiated, waiting for DataChannel...");
1431 session->webrtc_connection_initiated = true;
1433 }
1434
1435 // Check for ICE gathering timeout on all peers
1436 if (session->webrtc_connection_initiated && session->peer_manager) {
1437 int timeout_ms = GET_OPTION(webrtc_ice_timeout_ms);
1438 int timed_out_count = webrtc_peer_manager_check_gathering_timeouts(session->peer_manager, timeout_ms);
1439
1440 if (timed_out_count > 0) {
1441 log_error("ICE gathering timeout: %d peer(s) failed to gather candidates within %dms", timed_out_count,
1442 timeout_ms);
1443
1444 // Check if we have retries remaining
1445 int max_attempts = GET_OPTION(webrtc_reconnect_attempts);
1446 if (session->webrtc_retry_attempt < max_attempts) {
1447 // Calculate exponential backoff delay
1448 uint32_t backoff_ms = calculate_backoff_delay_ms(session->webrtc_retry_attempt);
1449
1450 log_warn("WebRTC connection attempt %d/%d failed, retrying in %ums...", session->webrtc_retry_attempt + 1,
1451 max_attempts + 1, backoff_ms);
1452
1453 // Wait for backoff period
1454 platform_sleep_ms(backoff_ms);
1455
1456 // Destroy old peer manager
1458 session->peer_manager = NULL;
1459
1460 // Re-initialize peer manager for retry
1461 asciichat_error_t reinit_result = initialize_webrtc_peer_manager(session);
1462 if (reinit_result != ASCIICHAT_OK) {
1463 log_error("Failed to re-initialize WebRTC for retry: %d", reinit_result);
1464 set_error(session, reinit_result, "Failed to re-initialize WebRTC for retry");
1465
1466 // If --prefer-webrtc is set, this is a fatal error
1467 if (GET_OPTION(prefer_webrtc)) {
1468 log_fatal("WebRTC connection failed and --prefer-webrtc is set - exiting");
1469 set_state(session, DISCOVERY_STATE_FAILED);
1470 return ERROR_NETWORK_TIMEOUT;
1471 }
1472
1473 break; // Exit this case to allow fallback to TCP
1474 }
1475
1476 // Reset connection state for retry
1477 session->webrtc_connection_initiated = false;
1478 session->webrtc_retry_attempt++;
1479
1480 log_info("WebRTC peer manager re-initialized for attempt %d/%d", session->webrtc_retry_attempt + 1,
1481 max_attempts + 1);
1482 } else {
1483 // No more retries - connection failed
1484 log_error("WebRTC connection failed after %d attempts (timeout: %dms)", max_attempts + 1, timeout_ms);
1485
1486 // Set error state - WebRTC connection failed
1487 char error_msg[256];
1488 snprintf(error_msg, sizeof(error_msg), "WebRTC connection failed after %d attempts (%dms timeout)",
1489 max_attempts + 1, timeout_ms);
1490 set_error(session, ERROR_NETWORK_TIMEOUT, error_msg);
1491
1492 // If --prefer-webrtc is set, this is a fatal error (no TCP fallback)
1493 if (GET_OPTION(prefer_webrtc)) {
1494 log_fatal("WebRTC connection failed and --prefer-webrtc is set - exiting");
1495 set_state(session, DISCOVERY_STATE_FAILED);
1496 return ERROR_NETWORK_TIMEOUT;
1497 }
1498
1499 break; // Exit this case to allow fallback to TCP
1500 }
1501 }
1502 }
1503
1504 // Poll ACDS for WebRTC signaling messages (SDP answer, ICE candidates)
1505 // while waiting for DataChannel to open
1506 if (session->acds_socket != INVALID_SOCKET_VALUE) {
1507 // Check if data available (non-blocking with timeout_ns)
1508 struct timeval tv_timeout;
1509 tv_timeout.tv_sec = timeout_ns / NS_PER_SEC_INT;
1510 tv_timeout.tv_usec = (timeout_ns % NS_PER_SEC_INT) / 1000;
1511
1512 fd_set readfds;
1513 FD_ZERO(&readfds);
1514 FD_SET(session->acds_socket, &readfds);
1515
1516 int select_result = select(session->acds_socket + 1, &readfds, NULL, NULL, &tv_timeout);
1517 if (select_result > 0 && FD_ISSET(session->acds_socket, &readfds)) {
1518 // Data available to read
1519 packet_type_t type;
1520 void *data = NULL;
1521 size_t len = 0;
1522
1523 asciichat_error_t recv_result = packet_receive(session->acds_socket, &type, &data, &len);
1524
1525 if (recv_result == ASCIICHAT_OK) {
1526 // Dispatch to appropriate handler
1527 handle_acds_webrtc_packet(session, type, data, len);
1528 POOL_FREE(data, len);
1529 } else {
1530 log_debug("Failed to receive ACDS packet: %d", recv_result);
1531 }
1532 }
1533 }
1534
1535 // Stay in CONNECTING_HOST until DataChannel opens (webrtc_transport_ready becomes true)
1536 // The discovery_on_transport_ready callback will set webrtc_transport_ready=true
1537 break;
1538 }
1539
1540 if (!session->participant_ctx) {
1541 // Create participant context for this session
1542 session_participant_config_t pconfig = {
1543 .address = session->host_address,
1544 .port = session->host_port,
1545 .enable_audio = true,
1546 .enable_video = true,
1547 .encryption_enabled = true,
1548 };
1549
1550 session->participant_ctx = session_participant_create(&pconfig);
1551 if (!session->participant_ctx) {
1552 log_error("Failed to create participant context");
1553 set_error(session, ERROR_MEMORY, "Failed to create participant context");
1554 break;
1555 }
1556
1557 // Attempt connection
1558 asciichat_error_t pconn = session_participant_connect(session->participant_ctx);
1559 if (pconn != ASCIICHAT_OK) {
1560 log_error("Failed to connect as participant: %d", pconn);
1561 // Stay in this state, will retry on next process() call
1562 break;
1563 }
1564
1565 log_info("Connected to host, performing crypto handshake...");
1566
1567 // Perform crypto handshake with server
1568 // Get the socket from participant context
1569 socket_t participant_socket = session_participant_get_socket(session->participant_ctx);
1570 if (participant_socket == INVALID_SOCKET_VALUE) {
1571 log_error("Failed to get socket from participant context");
1572 set_error(session, ERROR_NETWORK, "Failed to get participant socket");
1574 break;
1575 }
1576
1577 // Step 1: Send client protocol version
1578 log_debug("Sending protocol version to server...");
1579 protocol_version_packet_t client_version = {0};
1580 client_version.protocol_version = HOST_TO_NET_U16(1); // Protocol version 1
1581 client_version.protocol_revision = HOST_TO_NET_U16(0); // Revision 0
1582 client_version.supports_encryption = 1; // We support encryption
1583 client_version.compression_algorithms = 0;
1584 client_version.compression_threshold = 0;
1585 client_version.feature_flags = 0;
1586
1587 int result = send_protocol_version_packet(participant_socket, &client_version);
1588 if (result != 0) {
1589 log_error("Failed to send protocol version to server");
1590 set_error(session, ERROR_NETWORK, "Failed to send protocol version");
1592 break;
1593 }
1594
1595 // Step 2: Receive server protocol version
1596 log_debug("Receiving server protocol version...");
1597 packet_type_t packet_type;
1598 void *payload = NULL;
1599 size_t payload_len = 0;
1600
1601 int recv_result = receive_packet(participant_socket, &packet_type, &payload, &payload_len);
1602 if (recv_result != ASCIICHAT_OK || packet_type != PACKET_TYPE_PROTOCOL_VERSION) {
1603 log_error("Failed to receive server protocol version (got type 0x%x)", packet_type);
1604 if (payload) {
1605 buffer_pool_free(NULL, payload, payload_len);
1606 }
1607 set_error(session, ERROR_NETWORK, "Failed to receive protocol version from server");
1609 break;
1610 }
1611
1612 if (payload_len != sizeof(protocol_version_packet_t)) {
1613 log_error("Invalid protocol version packet size: %zu", payload_len);
1614 buffer_pool_free(NULL, payload, payload_len);
1615 set_error(session, ERROR_NETWORK, "Invalid protocol version packet");
1617 break;
1618 }
1619
1620 protocol_version_packet_t server_version;
1621 memcpy(&server_version, payload, sizeof(protocol_version_packet_t));
1622 buffer_pool_free(NULL, payload, payload_len);
1623
1624 if (!server_version.supports_encryption) {
1625 log_error("Server does not support encryption");
1626 set_error(session, ERROR_NETWORK, "Server does not support encryption");
1628 break;
1629 }
1630
1631 log_info("Server protocol version: %u.%u (encryption: yes)", NET_TO_HOST_U16(server_version.protocol_version),
1632 NET_TO_HOST_U16(server_version.protocol_revision));
1633
1634 // Step 3: Send crypto capabilities
1635 log_debug("Sending crypto capabilities...");
1636 crypto_capabilities_packet_t client_caps = {0};
1637 client_caps.supported_kex_algorithms = HOST_TO_NET_U16(0x0001); // KEX_ALGO_X25519
1638 client_caps.supported_auth_algorithms = HOST_TO_NET_U16(0x0003); // AUTH_ALGO_ED25519 | AUTH_ALGO_NONE
1639 client_caps.supported_cipher_algorithms = HOST_TO_NET_U16(0x0001); // CIPHER_ALGO_XSALSA20_POLY1305
1640 client_caps.requires_verification = 0;
1641 client_caps.preferred_kex = 0x0001; // KEX_ALGO_X25519
1642 client_caps.preferred_auth = 0x0001; // AUTH_ALGO_ED25519
1643 client_caps.preferred_cipher = 0x0001; // CIPHER_ALGO_XSALSA20_POLY1305
1644
1645 result = send_crypto_capabilities_packet(participant_socket, &client_caps);
1646 if (result != 0) {
1647 log_error("Failed to send crypto capabilities");
1648 set_error(session, ERROR_NETWORK, "Failed to send crypto capabilities");
1650 break;
1651 }
1652
1653 log_info("Crypto handshake initiated successfully");
1654 log_warn("*** Connected to host as participant - transitioning to ACTIVE ***");
1655 }
1656
1657 // Check if connection is established
1658 bool ctx_valid = session->participant_ctx != NULL;
1659 bool is_connected = ctx_valid ? session_participant_is_connected(session->participant_ctx) : false;
1660 log_debug("discovery_session_process: CONNECTING_HOST - participant_ctx=%p, ctx_valid=%d, is_connected=%d",
1661 session->participant_ctx, ctx_valid, is_connected);
1662
1663 if (ctx_valid && !is_connected) {
1664 // Log more details about why it's not connected
1665 log_debug("discovery_session_process: CONNECTING_HOST - context exists but not connected yet");
1666 }
1667
1668 if (is_connected) {
1669 log_info("Participant connection confirmed, transitioning to ACTIVE state");
1670 set_state(session, DISCOVERY_STATE_ACTIVE);
1671 } else {
1672 log_debug("discovery_session_process: CONNECTING_HOST - awaiting connection establishment (ctx_valid=%d, "
1673 "is_connected=%d)",
1674 ctx_valid, is_connected);
1675 }
1676 break;
1677 }
1678
1680 // Receive and handle ACDS packets for WebRTC signaling (NEW)
1681 if (session->session_type == SESSION_TYPE_WEBRTC && session->acds_socket != INVALID_SOCKET_VALUE) {
1682 // Use non-blocking polling to check for incoming ACDS packets
1683 struct pollfd pfd = {.fd = session->acds_socket, .events = POLLIN, .revents = 0};
1684
1685 int poll_result = socket_poll(&pfd, 1, 0); // Non-blocking poll (timeout=0)
1686
1687 if (poll_result > 0 && (pfd.revents & POLLIN)) {
1688 // Data available to read
1689 packet_type_t type;
1690 void *data = NULL;
1691 size_t len = 0;
1692
1693 asciichat_error_t recv_result = packet_receive(session->acds_socket, &type, &data, &len);
1694
1695 if (recv_result == ASCIICHAT_OK) {
1696 // Dispatch to appropriate handler
1697 handle_acds_webrtc_packet(session, type, data, len);
1698 POOL_FREE(data, len);
1699 }
1700 }
1701 }
1702
1703 // Session is active - check for host disconnect
1704 if (!session->is_host) {
1705 // We are a participant - check if host is still alive
1706 asciichat_error_t host_check = discovery_session_check_host_alive(session);
1707 if (host_check != ASCIICHAT_OK) {
1708 // Host is down! Trigger automatic failover to pre-elected future host
1709 log_warn("Host disconnect detected during ACTIVE state");
1710
1711 // If --prefer-webrtc is set, WebRTC failure is fatal (no TCP fallback)
1712 const options_t *opts = options_get();
1713 if (opts && opts->prefer_webrtc) {
1714 log_fatal("WebRTC connection failed and --prefer-webrtc is set - exiting");
1715 set_state(session, DISCOVERY_STATE_FAILED);
1716 session->error = ERROR_NETWORK;
1717 return ERROR_NETWORK;
1718 }
1719
1720 set_state(session, DISCOVERY_STATE_MIGRATING);
1721 // Reason code: 1 = timeout/disconnect detected during ACTIVE session
1723 }
1724 }
1725 platform_sleep_us(timeout_ns / 1000);
1726 break;
1727
1729 // Host migration in progress
1730 // Check if migration has timed out (if it takes >30 seconds, give up)
1731 if (session->migration.state == MIGRATION_STATE_COMPLETE) {
1732 // Migration completed successfully
1733 set_state(session, DISCOVERY_STATE_ACTIVE);
1734 log_info("Migration complete, session resumed");
1735 } else if (session_get_current_time_ms() - session->migration.detection_time_ms > 30 * MS_PER_SEC_INT) {
1736 // Migration timed out after 30 seconds
1737 log_error("Host migration timeout - session cannot recover");
1738 set_state(session, DISCOVERY_STATE_FAILED);
1739 session->error = ERROR_NETWORK;
1740 }
1741 platform_sleep_us(timeout_ns / 1000);
1742 break;
1743
1744 default:
1745 SET_ERRNO(ERROR_INVALID_STATE, "Invalid session state");
1746 break;
1747 }
1748
1749 return ASCIICHAT_OK;
1750}
1751
1753 if (!session) {
1754 SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
1755 return;
1756 }
1757
1758 log_info("Stopping discovery session...");
1759
1760 // Send SESSION_LEAVE if connected
1761 if (session->acds_socket != INVALID_SOCKET_VALUE && session->session_id[0] != 0) {
1762 acip_session_leave_t leave_msg;
1763 memset(&leave_msg, 0, sizeof(leave_msg));
1764 memcpy(leave_msg.session_id, session->session_id, 16);
1765 memcpy(leave_msg.participant_id, session->participant_id, 16);
1766
1767 packet_send(session->acds_socket, PACKET_TYPE_ACIP_SESSION_LEAVE, &leave_msg, sizeof(leave_msg));
1768 }
1769
1770 set_state(session, DISCOVERY_STATE_ENDED);
1771}
1772
1774 if (!session) {
1775 SET_ERRNO(ERROR_INVALID_PARAM, "null session");
1777 }
1778 return session->state;
1779}
1780
1782 if (!session) {
1783 SET_ERRNO(ERROR_INVALID_PARAM, "null session");
1784 return false;
1785 }
1786 return session->state == DISCOVERY_STATE_ACTIVE;
1787}
1788
1790 if (!session || session->session_string[0] == '\0') {
1791 SET_ERRNO(ERROR_INVALID_PARAM, "null session or session string is empty");
1792 return NULL;
1793 }
1794 return session->session_string;
1795}
1796
1798 if (!session) {
1799 SET_ERRNO(ERROR_INVALID_PARAM, "null session");
1800 return false;
1801 }
1802 return session->is_host;
1803}
1804
1806 if (!session) {
1807 SET_ERRNO(ERROR_INVALID_PARAM, "null session");
1808 return NULL;
1809 }
1810 return session->host_ctx;
1811}
1812
1814 if (!session) {
1815 SET_ERRNO(ERROR_INVALID_PARAM, "null session");
1816 return NULL;
1817 }
1818 return session->participant_ctx;
1819}
1820
1821// ============================================================================
1822// Ring Consensus & Migration Implementation (Host-Mediated Proactive Election)
1823// ============================================================================
1824
1840static asciichat_error_t discovery_session_run_election(discovery_session_t *session) {
1841 if (!session || !session->is_host) {
1842 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session or not a host");
1843 }
1844
1845 log_debug("Running host election with collected NETWORK_QUALITY");
1846
1847 // TODO: Request fresh NETWORK_QUALITY from all participants
1848 // Implementation steps:
1849 // 1. Get list of connected participants from session_host_ctx
1850 // 2. Send RING_COLLECT or broadcast request via ACDS to each participant
1851 // 3. Wait for NETWORK_QUALITY responses (with timeout)
1852 // 4. Collect responses into array: nat_quality_t qualities[MAX_PARTICIPANTS]
1853 // 5. Run election: negotiate_elect_future_host(qualities, participant_ids, count, &future_host_id)
1854 //
1855 // For now: Use host's own quality only (single-participant session)
1856
1857 // Measure host's own NAT quality (fresh measurement)
1858 nat_quality_t host_quality = {0};
1859 nat_quality_init(&host_quality);
1860
1861 // Re-measure host's NAT quality for accurate election
1862 const options_t *opts = options_get();
1863 const char *stun_server = opts ? opts->stun_servers : NULL;
1864 if (stun_server && stun_server[0] != '\0') {
1865 log_debug("Re-measuring host NAT quality for election");
1866 nat_detect_quality(&host_quality, stun_server, 0);
1867
1868 // Measure bandwidth to ACDS
1869 if (session->acds_socket != INVALID_SOCKET_VALUE) {
1870 nat_measure_bandwidth(&host_quality, session->acds_socket);
1871 }
1872 } else {
1873 // Fallback: use placeholder values
1874 host_quality.has_public_ip = true;
1875 host_quality.upload_kbps = 100000; // Placeholder: 100 Mbps
1876 host_quality.detection_complete = true;
1877 }
1878
1879 // Run election (currently only host quality, will include participants later)
1880 // TODO: Pass collected participant qualities to election algorithm
1881 uint8_t future_host_id[16];
1882 memcpy(future_host_id, session->participant_id, 16); // Host stays host for now
1883
1884 log_info("Election result: Future host elected (round %llu)",
1885 (unsigned long long)(session->ring.future_host_elected_round + 1));
1886
1887 // Store locally
1889 memcpy(session->ring.future_host_id, future_host_id, 16);
1890 session->ring.am_future_host = (memcmp(future_host_id, session->participant_id, 16) == 0);
1891 memcpy(session->ring.future_host_address, session->host_address, 64);
1892 session->ring.future_host_port = session->host_port;
1893 session->ring.future_host_connection_type = 0; // DIRECT_PUBLIC (we're the host)
1894
1895 // Broadcast FUTURE_HOST_ELECTED to all participants via ACDS
1896 acip_future_host_elected_t msg = {0};
1897 memcpy(msg.session_id, session->session_id, 16);
1898 memcpy(msg.future_host_id, future_host_id, 16);
1899 memcpy(msg.future_host_address, session->ring.future_host_address, 64);
1900 msg.future_host_port = session->ring.future_host_port;
1901 msg.connection_type = session->ring.future_host_connection_type;
1902 msg.elected_at_round = session->ring.future_host_elected_round;
1903
1904 packet_send(session->acds_socket, PACKET_TYPE_ACIP_FUTURE_HOST_ELECTED, &msg, sizeof(msg));
1905
1906 log_info("Broadcasted FUTURE_HOST_ELECTED: participant_id=%.*s, round=%llu", 16, (char *)future_host_id,
1907 (unsigned long long)msg.elected_at_round);
1908
1909 return ASCIICHAT_OK;
1910}
1911
1913 if (!session) {
1914 SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
1915 return ERROR_INVALID_PARAM;
1916 }
1917
1918 // Simplified for new architecture: just initialize timing
1919 // No participant list needed - host runs the election
1920 session->ring.last_ring_round_ms = session_get_current_time_ms();
1921 session->ring.am_future_host = false;
1922 memset(session->ring.future_host_id, 0, 16);
1923
1924 log_info("Ring consensus initialized (host-mediated model)");
1925 return ASCIICHAT_OK;
1926}
1927
1929 if (!session) {
1930 SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
1931 return ERROR_INVALID_PARAM;
1932 }
1933
1934 session->ring.last_ring_round_ms = session_get_current_time_ms();
1935
1936 if (session->is_host) {
1937 log_info("Starting 5-minute proactive election round (collecting NETWORK_QUALITY from participants)");
1938
1939 // HOST: Run proactive host election
1940 // 1. Collect NETWORK_QUALITY from all connected participants (TODO: request fresh data)
1941 // 2. Measure the host's own NAT quality
1942 // 3. Run deterministic election to determine future host
1943 // 4. Broadcast FUTURE_HOST_ELECTED to all participants via ACDS
1944 asciichat_error_t result = discovery_session_run_election(session);
1945 if (result != ASCIICHAT_OK) {
1946 log_error("Host election failed: %d", result);
1947 return result;
1948 }
1949
1950 return ASCIICHAT_OK;
1951 } else {
1952 log_debug("Ring round timer triggered (waiting for host to broadcast FUTURE_HOST_ELECTED)");
1953 return ASCIICHAT_OK;
1954 }
1955}
1956
1958 if (!session || session->is_host) {
1959 // We are the host, so "host" is always alive
1960 return ASCIICHAT_OK;
1961 }
1962
1963 // For WebRTC sessions, check the WebRTC transport instead of participant_ctx
1964 if (session->session_type == SESSION_TYPE_WEBRTC) {
1965 // If we have a WebRTC transport and it's marked as ready, host is alive
1966 if (session->webrtc_transport_ready) {
1967 return ASCIICHAT_OK;
1968 }
1969 // WebRTC transport not ready yet - might still be connecting
1970 return ERROR_NETWORK;
1971 }
1972
1973 // For TCP sessions, check participant context
1974 // If we don't have a participant context yet, we're not connected
1975 if (!session->participant_ctx) {
1976 return ERROR_NETWORK;
1977 }
1978
1979 // Check migration state - if we're already migrating, host is definitely not alive
1980 if (session->migration.state != MIGRATION_STATE_NONE) {
1981 return ERROR_NETWORK; // Already detected disconnect
1982 }
1983
1984 uint64_t now_ms = session_get_current_time_ms();
1985
1986 // Check if we have a ping in flight that timed out
1987 if (session->liveness.ping_in_flight) {
1988 uint64_t ping_age_ms = now_ms - session->liveness.last_ping_sent_ms;
1989 if (ping_age_ms > session->liveness.timeout_ms) {
1990 // Ping timed out
1991 session->liveness.consecutive_failures++;
1992 session->liveness.ping_in_flight = false;
1993 log_warn("Host ping timeout (attempt %u/%u, age=%llu ms)", session->liveness.consecutive_failures,
1994 session->liveness.max_failures, (unsigned long long)ping_age_ms);
1995
1996 // Check if we've exceeded failure threshold
1997 if (session->liveness.consecutive_failures >= session->liveness.max_failures) {
1998 log_error("Host declared dead after %u consecutive ping failures", session->liveness.consecutive_failures);
1999 return ERROR_NETWORK;
2000 }
2001 }
2002 }
2003
2004 // Check if it's time to send a new ping
2005 uint64_t time_since_last_ping = now_ms - session->liveness.last_ping_sent_ms;
2006 if (!session->liveness.ping_in_flight && time_since_last_ping >= session->liveness.ping_interval_ms) {
2007 // Send ping to host via participant connection
2009 if (host_socket != INVALID_SOCKET_VALUE) {
2010 asciichat_error_t result = packet_send(host_socket, PACKET_TYPE_PING, NULL, 0);
2011 if (result == ASCIICHAT_OK) {
2012 session->liveness.last_ping_sent_ms = now_ms;
2013 session->liveness.ping_in_flight = true;
2014 log_debug("Sent ping to host (attempt %u/%u)", session->liveness.consecutive_failures + 1,
2015 session->liveness.max_failures);
2016 } else {
2017 log_warn("Failed to send ping to host: %d", result);
2018 session->liveness.consecutive_failures++;
2019 if (session->liveness.consecutive_failures >= session->liveness.max_failures) {
2020 log_error("Host declared dead after %u consecutive send failures", session->liveness.consecutive_failures);
2021 return ERROR_NETWORK;
2022 }
2023 }
2024 }
2025 }
2026
2027 return ASCIICHAT_OK;
2028}
2029
2030asciichat_error_t discovery_session_handle_host_disconnect(discovery_session_t *session, uint32_t disconnect_reason) {
2031 if (!session) {
2032 SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
2033 return ERROR_INVALID_PARAM;
2034 }
2035
2036 log_warn("Host disconnect detected: reason=%u", disconnect_reason);
2037
2038 // Initialize migration context
2040 session->migration.detection_time_ms = session_get_current_time_ms();
2041 memcpy(session->migration.last_host_id, session->host_id, 16);
2042 session->migration.disconnect_reason = disconnect_reason;
2043
2044 // Notify ACDS of host loss (lightweight notification, no NAT data exchanged!)
2045 acip_host_lost_t host_lost = {0};
2046 memcpy(host_lost.session_id, session->session_id, 16);
2047 memcpy(host_lost.participant_id, session->participant_id, 16);
2048 memcpy(host_lost.last_host_id, session->host_id, 16);
2049 host_lost.disconnect_reason = disconnect_reason;
2050 host_lost.disconnect_time_ms = session->migration.detection_time_ms;
2051
2052 packet_send(session->acds_socket, PACKET_TYPE_ACIP_HOST_LOST, &host_lost, sizeof(host_lost));
2053
2054 // Check if we have a pre-elected future host (from last 5-minute ring round)
2055 if (session->ring.future_host_id[0] == 0) {
2056 // No future host known! This shouldn't happen if we just received FUTURE_HOST_ELECTED
2057 // within the last 5 minutes. Fall back to treating this as fatal.
2058 log_error("No future host pre-elected! Session cannot recover from host disconnect.");
2060 return ERROR_NETWORK; // Session should end
2061 }
2062
2063 // **SIMPLIFIED**: No metrics exchange needed! Future host was already elected 5 minutes ago.
2064 // Just failover to the pre-elected host.
2066
2067 // Check if I am the future host
2068 if (session->ring.am_future_host) {
2069 // I become the new host immediately! (no election delay!)
2070 log_info("Becoming host (I'm the pre-elected future host)");
2071 return discovery_session_become_host(session);
2072 } else {
2073 // Connect to pre-elected future host (address already stored!)
2074 log_info("Connecting to pre-elected future host: %s:%u", session->ring.future_host_address,
2075 session->ring.future_host_port);
2077 }
2078}
2079
2081 if (!session) {
2082 SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
2083 return ERROR_INVALID_PARAM;
2084 }
2085
2086 log_info("Starting as new host after migration (participant ID: %02x%02x...)", session->participant_id[0],
2087 session->participant_id[1]);
2088
2089 // Get configured port (or use default)
2090 int host_port = GET_OPTION(port);
2091
2092 // Mark ourselves as host
2093 session->is_host = true;
2094
2095 // Create host context if needed with configured port
2096 if (!session->host_ctx) {
2097 session_host_config_t hconfig = {
2098 .port = host_port,
2099 .ipv4_address = "0.0.0.0",
2100 .max_clients = 32,
2101 .encryption_enabled = true,
2102 };
2103
2104 session->host_ctx = session_host_create(&hconfig);
2105 if (!session->host_ctx) {
2106 log_error("Failed to create host context for migration");
2107 return ERROR_MEMORY;
2108 }
2109
2110 // Start listening for new participant connections
2111 asciichat_error_t hstart = session_host_start(session->host_ctx);
2112 if (hstart != ASCIICHAT_OK) {
2113 log_error("Failed to start host after migration: %d", hstart);
2114 return hstart;
2115 }
2116
2117 log_info("Host restarted after migration, listening on port %d", host_port);
2118 }
2119
2120 // Send HOST_ANNOUNCEMENT to ACDS so other participants can reconnect
2121 acip_host_announcement_t announcement = {0};
2122 memcpy(announcement.session_id, session->session_id, 16);
2123 memcpy(announcement.host_id, session->participant_id, 16);
2124 SAFE_STRNCPY(announcement.host_address, "127.0.0.1", sizeof(announcement.host_address));
2125 // TODO: Use actual determined host address instead of 127.0.0.1
2126 announcement.host_port = host_port;
2127 announcement.connection_type = ACIP_CONNECTION_TYPE_DIRECT_PUBLIC;
2128 // TODO: Use actual connection type based on NAT quality
2129
2130 if (session->acds_socket != INVALID_SOCKET_VALUE) {
2131 packet_send(session->acds_socket, PACKET_TYPE_ACIP_HOST_ANNOUNCEMENT, &announcement, sizeof(announcement));
2132 log_info("Sent HOST_ANNOUNCEMENT to ACDS for new host %02x%02x... at %s:%u", announcement.host_id[0],
2133 announcement.host_id[1], announcement.host_address, announcement.host_port);
2134 }
2135
2136 // Mark migration complete
2138
2139 // Transition to ACTIVE state (host is ready)
2140 set_state(session, DISCOVERY_STATE_ACTIVE);
2141
2142 return ASCIICHAT_OK;
2143}
2144
2146 if (!session) {
2147 SET_ERRNO(ERROR_INVALID_PARAM, "session is NULL");
2148 return ERROR_INVALID_PARAM;
2149 }
2150
2151 log_info("Reconnecting to future host: %s:%u (connection type: %u)", session->ring.future_host_address,
2153
2154 // Destroy old participant context if present
2155 if (session->participant_ctx) {
2157 session->participant_ctx = NULL;
2158 }
2159
2160 // Create new participant context for the future host
2161 session_participant_config_t pconfig = {
2162 .address = session->ring.future_host_address,
2163 .port = session->ring.future_host_port,
2164 .enable_audio = true,
2165 .enable_video = true,
2166 .encryption_enabled = true,
2167 };
2168
2169 session->participant_ctx = session_participant_create(&pconfig);
2170 if (!session->participant_ctx) {
2171 log_error("Failed to create participant context for future host");
2172 return ERROR_MEMORY;
2173 }
2174
2175 // Attempt connection to future host
2176 asciichat_error_t pconn = session_participant_connect(session->participant_ctx);
2177 if (pconn != ASCIICHAT_OK) {
2178 log_error("Failed to connect to future host: %d", pconn);
2179 return pconn;
2180 }
2181
2182 log_info("Connected to future host after migration");
2183
2184 // Update our host info with the new future host details
2185 memcpy(session->host_id, session->ring.future_host_id, 16);
2186 SAFE_STRNCPY(session->host_address, session->ring.future_host_address, sizeof(session->host_address));
2187 session->host_port = session->ring.future_host_port;
2188
2189 log_info("Updated host info to: %s:%u (id: %02x%02x...)", session->host_address, session->host_port,
2190 session->host_id[0], session->host_id[1]);
2191
2192 // Mark migration complete (in real implementation, this would be done after successful connection)
2194
2195 // Transition to ACTIVE state (participant is ready)
2196 set_state(session, DISCOVERY_STATE_ACTIVE);
2197
2198 return ASCIICHAT_OK;
2199}
2200
2201asciichat_error_t discovery_session_get_future_host(const discovery_session_t *session, uint8_t out_id[16],
2202 char out_address[64], uint16_t *out_port,
2203 uint8_t *out_connection_type) {
2204 if (!session || !out_id || !out_address || !out_port || !out_connection_type) {
2205 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid output parameters");
2206 return ERROR_INVALID_PARAM;
2207 }
2208
2209 if (session->ring.future_host_id[0] == 0) {
2210 SET_ERRNO(ERROR_NOT_FOUND, "Future host not elected yet");
2211 return ERROR_NOT_FOUND;
2212 }
2213
2214 memcpy(out_id, session->ring.future_host_id, 16);
2215 SAFE_STRNCPY(out_address, session->ring.future_host_address, 64);
2216 *out_port = session->ring.future_host_port;
2217 *out_connection_type = session->ring.future_host_connection_type;
2218
2219 return ASCIICHAT_OK;
2220}
2221
2223 if (!session) {
2224 SET_ERRNO(ERROR_INVALID_PARAM, "null session");
2225 return false;
2226 }
2227 return session->ring.am_future_host;
2228}
asciichat_error_t acds_sign_session_join(const uint8_t identity_seckey[64], uint64_t timestamp, const char *session_string, uint8_t signature_out[64])
asciichat_error_t acds_sign_session_create(const uint8_t identity_seckey[64], uint64_t timestamp, uint8_t capabilities, uint8_t max_participants, uint8_t signature_out[64])
void buffer_pool_free(buffer_pool_t *pool, void *data, size_t size)
asciichat_error_t session_host_set_client_transport(session_host_t *host, uint32_t client_id, acip_transport_t *transport)
Definition host.c:1409
asciichat_error_t session_host_start(session_host_t *host)
Definition host.c:797
void session_host_destroy(session_host_t *host)
Definition host.c:222
session_host_t * session_host_create(const session_host_config_t *config)
Definition host.c:166
int socket_t
asciichat_error_t acds_identity_load(const char *path, uint8_t public_key[32], uint8_t secret_key[64])
Definition identity.c:32
asciichat_error_t acds_identity_default_path(char *path_out, size_t path_size)
Definition identity.c:121
asciichat_error_t acds_identity_save(const char *path, const uint8_t public_key[32], const uint8_t secret_key[64])
Definition identity.c:61
void acds_identity_fingerprint(const uint8_t public_key[32], char fingerprint[65])
Definition identity.c:104
asciichat_error_t acds_identity_generate(uint8_t public_key[32], uint8_t secret_key[64])
Definition identity.c:18
asciichat_error_t webrtc_init(void)
void nat_quality_to_acip(const nat_quality_t *quality, const uint8_t session_id[16], const uint8_t participant_id[16], acip_nat_quality_t *out)
Convert nat_quality_t to acip_nat_quality_t for network transmission.
Definition nat.c:449
asciichat_error_t nat_measure_bandwidth(nat_quality_t *quality, socket_t acds_socket)
Measure upload bandwidth to ACDS server.
Definition nat.c:365
void nat_quality_init(nat_quality_t *quality)
Initialize NAT quality structure with defaults.
Definition nat.c:30
asciichat_error_t nat_detect_quality(nat_quality_t *quality, const char *stun_server, uint16_t local_port)
Detect NAT quality using all available methods.
Definition nat.c:283
int nat_compute_tier(const nat_quality_t *quality)
Compute NAT tier for host selection (0=best, 4=worst)
Definition nat.c:37
void nat_quality_from_acip(const acip_nat_quality_t *acip, nat_quality_t *out)
Convert acip_nat_quality_t to nat_quality_t.
Definition nat.c:487
NAT quality detection for discovery mode host selection.
void negotiate_init(negotiate_ctx_t *ctx, const uint8_t session_id[16], const uint8_t participant_id[16], bool is_initiator)
Initialize negotiation context.
Definition negotiate.c:17
asciichat_error_t negotiate_determine_result(negotiate_ctx_t *ctx)
Determine negotiation result.
Definition negotiate.c:111
Host negotiation logic for discovery mode.
@ NEGOTIATE_STATE_INIT
Initial state.
Definition negotiate.h:27
@ NEGOTIATE_STATE_COMPARING
Comparing qualities.
Definition negotiate.h:30
int send_protocol_version_packet(socket_t sockfd, const protocol_version_packet_t *version)
Send protocol version packet.
Definition packet.c:1013
asciichat_error_t packet_receive(socket_t sockfd, packet_type_t *type, void **data, size_t *len)
Receive a packet with proper header validation and CRC32 checking.
Definition packet.c:348
int receive_packet(socket_t sockfd, packet_type_t *type, void **data, size_t *len)
Receive a basic packet without encryption.
Definition packet.c:766
asciichat_error_t packet_send(socket_t sockfd, packet_type_t type, const void *data, size_t len)
Send a packet with proper header and CRC32.
Definition packet.c:288
int send_crypto_capabilities_packet(socket_t sockfd, const crypto_capabilities_packet_t *caps)
Send crypto capabilities packet.
Definition packet.c:1027
asciichat_error_t session_participant_connect(session_participant_t *p)
void session_participant_destroy(session_participant_t *p)
socket_t session_participant_get_socket(session_participant_t *p)
session_participant_t * session_participant_create(const session_participant_config_t *config)
bool session_participant_is_connected(session_participant_t *p)
asciichat_error_t session_participant_set_transport(session_participant_t *p, acip_transport_t *transport)
void session_participant_disconnect(session_participant_t *p)
asciichat_error_t webrtc_peer_manager_create(const webrtc_peer_manager_config_t *config, const webrtc_signaling_callbacks_t *signaling_callbacks, webrtc_peer_manager_t **manager_out)
asciichat_error_t webrtc_peer_manager_handle_ice(webrtc_peer_manager_t *manager, const acip_webrtc_ice_t *ice)
asciichat_error_t webrtc_peer_manager_handle_sdp(webrtc_peer_manager_t *manager, const acip_webrtc_sdp_t *sdp)
void webrtc_peer_manager_destroy(webrtc_peer_manager_t *manager)
asciichat_error_t webrtc_peer_manager_connect(webrtc_peer_manager_t *manager, const uint8_t session_id[16], const uint8_t participant_id[16])
int webrtc_peer_manager_check_gathering_timeouts(webrtc_peer_manager_t *manager, uint32_t timeout_ms)
void platform_sleep_us(unsigned int us)
uint64_t platform_get_monotonic_time_us(void)
void platform_sleep_ms(unsigned int ms)
const options_t * options_get(void)
Definition rcu.c:347
Discovery session flow management.
@ MIGRATION_STATE_DETECTED
Host disconnect detected.
Definition session.h:87
@ MIGRATION_STATE_COMPLETE
Failover complete, call resumed.
Definition session.h:89
@ MIGRATION_STATE_NONE
No migration in progress.
Definition session.h:86
@ MIGRATION_STATE_FAILOVER
Failing over to pre-elected future host.
Definition session.h:88
discovery_state_t
Discovery session state.
Definition session.h:116
@ DISCOVERY_STATE_ENDED
Session ended.
Definition session.h:128
@ DISCOVERY_STATE_JOINING_SESSION
Joining existing session.
Definition session.h:120
@ DISCOVERY_STATE_INIT
Initial state.
Definition session.h:117
@ DISCOVERY_STATE_ACTIVE
Session active (call in progress)
Definition session.h:125
@ DISCOVERY_STATE_MIGRATING
Host migration in progress.
Definition session.h:126
@ DISCOVERY_STATE_NEGOTIATING
NAT negotiation in progress.
Definition session.h:122
@ DISCOVERY_STATE_CONNECTING_HOST
Connecting to host as participant.
Definition session.h:124
@ DISCOVERY_STATE_CREATING_SESSION
Creating new session.
Definition session.h:119
@ DISCOVERY_STATE_CONNECTING_ACDS
Connecting to ACDS.
Definition session.h:118
@ DISCOVERY_STATE_WAITING_PEER
Waiting for peer to join (initiator)
Definition session.h:121
@ DISCOVERY_STATE_FAILED
Session failed.
Definition session.h:127
@ DISCOVERY_STATE_STARTING_HOST
Starting as host.
Definition session.h:123
int socket_set_timeout(socket_t sock, uint64_t timeout_ns)
Set socket receive and send timeouts.
Definition socket.c:82
uint8_t session_id[16]
uint8_t participant_id[16]
bool discovery_session_is_active(const discovery_session_t *session)
Check if session is active (call in progress)
bool discovery_session_is_future_host(const discovery_session_t *session)
Check if we are the future host.
void discovery_session_stop(discovery_session_t *session)
Stop the discovery session.
bool discovery_session_is_host(const discovery_session_t *session)
Check if we are the host.
discovery_session_t * discovery_session_create(const discovery_config_t *config)
Create a new discovery session.
asciichat_error_t discovery_session_start(discovery_session_t *session)
Start the discovery session.
asciichat_error_t discovery_session_init_ring(discovery_session_t *session)
Initialize ring consensus state.
discovery_state_t discovery_session_get_state(const discovery_session_t *session)
Get current session state.
asciichat_error_t discovery_session_check_host_alive(discovery_session_t *session)
Detect host disconnect (check connection status)
session_host_t * discovery_session_get_host(discovery_session_t *session)
Get host context (if we are host)
asciichat_error_t discovery_session_get_future_host(const discovery_session_t *session, uint8_t out_id[16], char out_address[64], uint16_t *out_port, uint8_t *out_connection_type)
Get future host information.
asciichat_error_t discovery_session_process(discovery_session_t *session, int64_t timeout_ns)
Process session events (call in main loop)
asciichat_error_t discovery_session_start_ring_round(discovery_session_t *session)
Start a new ring consensus round (every 5 minutes or on new joiner)
const char * discovery_session_get_string(const discovery_session_t *session)
Get session string.
asciichat_error_t discovery_session_handle_host_disconnect(discovery_session_t *session, uint32_t disconnect_reason)
Handle host disconnect with automatic failover to future host.
asciichat_error_t discovery_session_connect_to_future_host(discovery_session_t *session)
Connect to pre-elected future host (called when NOT future host)
void discovery_session_destroy(discovery_session_t *session)
Destroy discovery session and free resources.
session_participant_t * discovery_session_get_participant(discovery_session_t *session)
Get participant context (if we are participant)
asciichat_error_t discovery_session_become_host(discovery_session_t *session)
Become the host (called when elected as future host)
Configuration for discovery session.
Definition session.h:211
void * callback_user_data
Definition session.h:226
const char * acds_address
ACDS address (default: "127.0.0.1")
Definition session.h:213
void(* on_error)(asciichat_error_t error, const char *message, void *user_data)
Definition session.h:225
uint16_t acds_port
ACDS port (default: 27225)
Definition session.h:214
discovery_should_exit_fn should_exit_callback
Definition session.h:229
void * exit_callback_data
Definition session.h:230
void(* on_state_change)(discovery_state_t new_state, void *user_data)
Definition session.h:223
const char * session_string
Session string to join (or NULL to create)
Definition session.h:217
void(* on_session_ready)(const char *session_string, void *user_data)
Definition session.h:224
Discovery session context.
Definition session.h:134
void * callback_user_data
Definition session.h:201
asciichat_error_t error
Definition session.h:137
host_liveness_t liveness
Definition session.h:173
uint64_t webrtc_last_attempt_time_ms
Timestamp of last connection attempt (monotonic time)
Definition session.h:183
ring_consensus_t ring
Definition session.h:170
uint8_t session_type
0 = DIRECT_TCP, 1 = WEBRTC
Definition session.h:160
uint16_t acds_port
Definition session.h:154
session_host_t * host_ctx
Definition session.h:194
uint16_t host_port
Definition session.h:159
char acds_address[64]
Definition session.h:153
socket_t acds_socket
Definition session.h:152
uint8_t participant_id[16]
Definition session.h:141
int webrtc_retry_attempt
Current retry attempt number (0 = initial, 1+ = retries)
Definition session.h:182
char turn_password[128]
Definition session.h:164
discovery_state_t state
Definition session.h:136
void * exit_callback_data
Definition session.h:205
uint8_t identity_pubkey[32]
Ed25519 public key for this participant.
Definition session.h:148
char turn_username[128]
Definition session.h:163
bool webrtc_transport_ready
True when DataChannel is open and transport created.
Definition session.h:180
uint8_t host_id[16]
Definition session.h:157
turn_server_t * turn_servers
TURN servers from ACDS.
Definition session.h:190
uint8_t initiator_id[16]
Definition session.h:142
size_t stun_count
Number of STUN servers.
Definition session.h:189
void(* on_error)(asciichat_error_t error, const char *message, void *user_data)
Definition session.h:200
uint8_t identity_seckey[64]
Ed25519 secret key for signing.
Definition session.h:149
migration_ctx_t migration
Definition session.h:176
stun_server_t * stun_servers
STUN servers from ACDS.
Definition session.h:188
void(* on_session_ready)(const char *session_string, void *user_data)
Definition session.h:199
char session_string[SESSION_STRING_BUFFER_SIZE]
Definition session.h:143
discovery_should_exit_fn should_exit_callback
Definition session.h:204
size_t turn_count
Number of TURN servers.
Definition session.h:191
bool webrtc_connection_initiated
True when we've called webrtc_peer_manager_connect()
Definition session.h:181
session_participant_t * participant_ctx
Definition session.h:195
char host_address[64]
Definition session.h:158
uint8_t session_id[16]
Definition session.h:140
void(* on_state_change)(discovery_state_t new_state, void *user_data)
Definition session.h:198
struct webrtc_peer_manager * peer_manager
WebRTC peer connection manager (NULL for TCP sessions)
Definition session.h:179
negotiate_ctx_t negotiate
Definition session.h:167
uint32_t consecutive_failures
Number of consecutive ping failures.
Definition session.h:71
uint32_t max_failures
Threshold for triggering migration (default: 3)
Definition session.h:72
uint64_t ping_interval_ms
Time between pings (default: 3000ms)
Definition session.h:73
uint64_t last_pong_received_ms
Timestamp of last pong received (monotonic)
Definition session.h:70
uint64_t last_ping_sent_ms
Timestamp of last ping sent (monotonic)
Definition session.h:69
bool ping_in_flight
True if waiting for pong.
Definition session.h:75
uint64_t timeout_ms
Timeout for ping response (default: 10000ms)
Definition session.h:74
uint64_t detection_time_ms
When host disconnect detected (Unix ms)
Definition session.h:97
migration_state_t state
Current migration state.
Definition session.h:96
uint32_t disconnect_reason
Reason for disconnect (from HOST_LOST packet)
Definition session.h:99
uint8_t last_host_id[16]
The host that died.
Definition session.h:98
NAT quality assessment result.
Definition nat.h:27
bool detection_complete
All probes finished.
Definition nat.h:51
uint32_t upload_kbps
Upload bandwidth in Kbps.
Definition nat.h:39
bool has_public_ip
STUN reflexive == local IP.
Definition nat.h:29
bool we_are_host
True if we should become host.
Definition negotiate.h:56
char host_address[64]
Host's address (ours if we_are_host)
Definition negotiate.h:57
nat_quality_t our_quality
Our NAT quality.
Definition negotiate.h:47
bool peer_quality_received
Have we received peer's quality?
Definition negotiate.h:49
nat_quality_t peer_quality
Peer's NAT quality (when received)
Definition negotiate.h:48
negotiate_state_t state
Definition negotiate.h:52
uint16_t host_port
Host's port.
Definition negotiate.h:58
uint8_t future_host_id[16]
Who will host if current host dies.
Definition session.h:51
uint8_t future_host_connection_type
acip_connection_type_t (DIRECT, UPNP, STUN, TURN)
Definition session.h:54
uint16_t future_host_port
Port number.
Definition session.h:53
uint64_t future_host_elected_round
Which 5-minute round this was elected in.
Definition session.h:56
uint64_t last_ring_round_ms
When host last ran election (for 5-min timer)
Definition session.h:59
char future_host_address[64]
Where to connect.
Definition session.h:52
bool am_future_host
Am I the elected future host?
Definition session.h:55
Internal session host structure.
Definition host.c:82
Internal session participant structure.
Definition participant.c:43
char address[BUFFER_SIZE_SMALL]
Server address.
Definition participant.c:45
int stun_servers_parse(const char *csv_servers, const char *default_csv, stun_server_t *out_servers, int max_count)
Parse comma-separated STUN server URLs into stun_server_t array.
Definition stun.c:51
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
uint64_t time_get_ns(void)
Definition util/time.c:48