ACDS (discovery-service mode) entry point.
Expects options already parsed and shared initialization complete. Called by main.c mode dispatcher after options_init() and asciichat_shared_init().
99 {
100 asciichat_error_t result;
101
102
103
104 if (GET_OPTION(enable_keepawake) && GET_OPTION(disable_keepawake)) {
105 log_error("--keepawake and --no-keepawake are mutually exclusive");
106 return ERROR_INVALID_PARAM;
107 }
108 if (GET_OPTION(enable_keepawake)) {
110 }
111
112
113#ifdef __linux__
114 struct rlimit rl = {.rlim_cur = 65536, .rlim_max = 65536};
115 if (setrlimit(RLIMIT_NOFILE, &rl) < 0) {
116 log_warn("couldn't raise fd limit: %s", strerror(errno));
117 }
118#elif defined(__APPLE__)
119 struct rlimit rl = {.rlim_cur = 10240, .rlim_max = RLIM_INFINITY};
120 setrlimit(RLIMIT_NOFILE, &rl);
121#endif
122
123
125
126 log_info("ascii-chat Discovery Service (acds) starting...");
127 log_info("Version: %s (%s, %s)", ASCII_CHAT_VERSION_FULL, ASCII_CHAT_BUILD_TYPE, ASCII_CHAT_BUILD_DATE);
128
129
131 if (result != ASCIICHAT_OK) {
132 log_error("Failed to initialize session string generator");
133 return result;
134 }
135
136
137 uint8_t public_key[32];
138 uint8_t secret_key[64];
139
140 const char *acds_key_path = GET_OPTION(encrypt_key);
141
143 if (!acds_key_path || acds_key_path[0] == '\0') {
145 if (config_dir) {
146 safe_snprintf(default_key_path,
sizeof(default_key_path),
"%sdiscovery_identity", config_dir);
147 acds_key_path = default_key_path;
148 }
149 }
150 log_info("Loading identity key from %s", acds_key_path);
152
153 if (result != ASCIICHAT_OK) {
154 log_info("Identity key not found, generating new key...");
155
157 if (result != ASCIICHAT_OK) {
158 log_error("Failed to generate identity key");
159 return result;
160 }
161
163 if (result != ASCIICHAT_OK) {
164 log_error("Failed to save identity key to %s", acds_key_path);
165 return result;
166 }
167
168 log_info("Saved new identity key to %s", acds_key_path);
169 }
170
171
172 char fingerprint[65];
174 log_info("Discovery server identity: SHA256:%s", fingerprint);
175 char msg[256];
176 safe_snprintf(msg,
sizeof(msg),
"🔑 Server fingerprint: SHA256:%s", fingerprint);
177 log_console(LOG_INFO, msg);
178
179
181
182 int port_num = GET_OPTION(port);
183 if (port_num < 1 || port_num > 65535) {
184 log_error("Invalid port: %d (must be 1-65535)", port_num);
185 return ERROR_INVALID_PARAM;
186 }
187 config.
port = port_num;
188
189 const char *address = opts && opts->address[0] != '\0' ? opts->address : "127.0.0.1";
190 const char *address6 = opts && opts->address6[0] != '\0' ? opts->address6 : "::1";
191 const char *log_file = opts && opts->log_file[0] != '\0' ? opts->log_file : "acds.log";
197 config.
log_level = GET_OPTION(log_level);
200
201
203 log_info("Security: Requiring signed identity from servers creating sessions");
204 }
206 log_info("Security: Requiring signed identity from clients joining sessions");
207 }
208
209
212 const char *stun_servers_str = GET_OPTION(stun_servers);
213 if (stun_servers_str && stun_servers_str[0] != '\0') {
214 char stun_copy[OPTIONS_BUFF_SIZE];
215 SAFE_STRNCPY(stun_copy, stun_servers_str, sizeof(stun_copy));
216
217 char *saveptr = NULL;
218 char *token = platform_strtok_r(stun_copy, ",", &saveptr);
220
221 while (*token == ' ' || *token == '\t')
222 token++;
223 size_t len = strlen(token);
224 while (len > 0 && (token[len - 1] == ' ' || token[len - 1] == '\t')) {
225 token[--len] = '\0';
226 }
227
228 if (len > 0 && len <
sizeof(config.
stun_servers[0].host)) {
232 log_info("Added STUN server: %s", token);
234 } else if (len > 0) {
235 log_warn("STUN server URL too long (max 63 chars): %s", token);
236 }
237
238 token = platform_strtok_r(NULL, ",", &saveptr);
239 }
240 }
241
242
245 const char *turn_servers_str = GET_OPTION(turn_servers);
246 const char *turn_username_str = GET_OPTION(turn_username);
247 const char *turn_credential_str = GET_OPTION(turn_credential);
248
249 if (turn_servers_str && turn_servers_str[0] != '\0') {
250 char turn_copy[OPTIONS_BUFF_SIZE];
251 SAFE_STRNCPY(turn_copy, turn_servers_str, sizeof(turn_copy));
252
253 char *saveptr = NULL;
254 char *token = platform_strtok_r(turn_copy, ",", &saveptr);
256
257 while (*token == ' ' || *token == '\t')
258 token++;
259 size_t len = strlen(token);
260 while (len > 0 && (token[len - 1] == ' ' || token[len - 1] == '\t')) {
261 token[--len] = '\0';
262 }
263
264 if (len > 0 && len <
sizeof(config.
turn_servers[0].url)) {
268
269
270 if (turn_username_str && turn_username_str[0] != '\0') {
271 size_t username_len = strlen(turn_username_str);
272 if (username_len <
sizeof(config.
turn_servers[0].username)) {
276 }
277 }
278
279
280 if (turn_credential_str && turn_credential_str[0] != '\0') {
281 size_t credential_len = strlen(turn_credential_str);
282 if (credential_len <
sizeof(config.
turn_servers[0].credential)) {
286 }
287 }
288
289 log_info("Added TURN server: %s (username: %s)", token,
290 turn_username_str && turn_username_str[0] ? turn_username_str : "<none>");
292 } else if (len > 0) {
293 log_warn("TURN server URL too long (max 63 chars): %s", token);
294 }
295
296 token = platform_strtok_r(NULL, ",", &saveptr);
297 }
298 }
299
300
301 const char *turn_secret_str = GET_OPTION(turn_secret);
302 if (turn_secret_str && turn_secret_str[0] != '\0') {
304 log_info("TURN dynamic credential generation enabled");
305 } else {
307 }
308
309
311 memset(&server, 0, sizeof(server));
312 g_server = &server;
313
315 if (result != ASCIICHAT_OK) {
316 log_error("Server initialization failed");
317 g_server = NULL;
318 return result;
319 }
320
321
322 websocket_server_config_t ws_config = {
323 .port = GET_OPTION(websocket_port),
324 .client_handler = NULL,
325 .user_data = &server,
326 };
327
328 memset(&g_websocket_server, 0, sizeof(g_websocket_server));
330 if (ws_init_result != ASCIICHAT_OK) {
331 log_warn("Failed to initialize WebSocket server - browser clients will not be supported");
332 } else {
333 log_info("WebSocket server initialized on port %d", GET_OPTION(websocket_port));
334 }
335
336
337 g_discovery_start_time = time(NULL);
338 g_last_status_update = g_discovery_start_time;
339 if (GET_OPTION(status_screen)) {
340 server.
tcp_server.config.status_update_fn = discovery_status_update_callback;
341 server.
tcp_server.config.status_update_data = &server;
342 }
343
344
345 if (acds_should_exit()) {
346 log_info("Shutdown signal received during initialization, skipping server startup");
347 return 0;
348 }
349
350
351
352
353
354
355
356
357
358
359
360
361
362 if (acds_should_exit()) {
363 log_info("Shutdown signal received before UPnP initialization");
364 result = ASCIICHAT_OK;
365 goto cleanup_resources;
366 }
367
368 if (GET_OPTION(enable_upnp)) {
369 asciichat_error_t upnp_result =
nat_upnp_open(config.
port,
"ascii-chat ACDS", &g_upnp_ctx);
370
371 if (upnp_result == ASCIICHAT_OK && g_upnp_ctx) {
372 char public_addr[22];
374 char msg[256];
375 safe_snprintf(msg,
sizeof(msg),
"🌐 Public endpoint: %s (direct TCP)", public_addr);
376 log_console(LOG_INFO, msg);
377 log_info("UPnP: Port mapping successful, public endpoint: %s", public_addr);
378 }
379 } else {
380 log_info("UPnP: Port mapping unavailable or failed - will use WebRTC fallback");
381 log_console(LOG_INFO, "📡 Clients behind strict NATs will use WebRTC fallback");
382 }
383 } else {
384 log_debug("UPnP: Disabled (use --upnp to enable automatic port mapping)");
385 }
386
387
388 if (acds_should_exit()) {
389 log_info("Shutdown signal received before mDNS initialization");
390 result = ASCIICHAT_OK;
391 goto cleanup_resources;
392 }
393
394
395
396
397 log_debug("Initializing mDNS for ACDS LAN service discovery...");
399 if (!g_mdns_ctx) {
400 LOG_ERRNO_IF_SET("Failed to initialize mDNS (non-fatal, LAN discovery disabled)");
401 log_warn("mDNS disabled for ACDS - LAN discovery of discovery service will not be available");
402 } else {
403
404
405 char hostname[256] = {0};
406 gethostname(hostname, sizeof(hostname) - 1);
407
408 asciichat_mdns_service_t service = {
409 .name = "ascii-chat-Discovery-Service",
410 .type = "_ascii-chat-discovery-service._tcp",
411 .host = hostname,
413 .txt_records = NULL,
414 .txt_count = 0,
415 };
416
418 if (mdns_advertise_result != ASCIICHAT_OK) {
419 LOG_ERRNO_IF_SET("Failed to advertise ACDS mDNS service");
420 log_warn("mDNS advertising failed for ACDS - LAN discovery disabled");
422 g_mdns_ctx = NULL;
423 } else {
424 log_console(LOG_INFO, "🌐 mDNS: ACDS advertised as '_ascii-chat-discovery-service._tcp.local' on LAN");
425 log_info(
"mDNS: ACDS advertised as '_ascii-chat-discovery-service._tcp.local' (port=%d)", config.
port);
426 }
427 }
428
429
430 signal(SIGINT, acds_handle_signal);
431 signal(SIGTERM, acds_handle_signal);
432
433
434 if (GET_OPTION(status_screen)) {
435 discovery_status_t status;
437 config.
port, g_discovery_start_time, &status) == ASCIICHAT_OK) {
439 g_last_status_update = g_discovery_start_time;
440 }
441 }
442
443
445 if (result != ASCIICHAT_OK) {
446 log_error("Server run failed");
447 }
448
449cleanup_resources:
450
451
453
454 log_info("Shutting down discovery server...");
456 g_server = NULL;
457
458
460 log_debug("WebSocket server destroyed");
461
462
463 if (g_upnp_ctx) {
465 log_debug("UPnP port mapping closed");
466 }
467
468
469 if (g_mdns_ctx) {
471 g_mdns_ctx = NULL;
472 log_debug("mDNS context shut down");
473 }
474
475 log_info("Discovery server stopped");
476 return result;
477}
asciichat_error_t acds_string_init(void)
void discovery_status_display(const discovery_status_t *status)
asciichat_error_t discovery_status_gather(tcp_server_t *server, discovery_database_t *db, const char *ipv4_address, const char *ipv6_address, uint16_t port, time_t start_time, discovery_status_t *out_status)
asciichat_error_t acds_identity_load(const char *path, uint8_t public_key[32], uint8_t secret_key[64])
asciichat_error_t acds_identity_save(const char *path, const uint8_t public_key[32], const uint8_t secret_key[64])
void acds_identity_fingerprint(const uint8_t public_key[32], char fingerprint[65])
asciichat_error_t acds_identity_generate(uint8_t public_key[32], uint8_t secret_key[64])
asciichat_error_t websocket_server_init(websocket_server_t *server, const websocket_server_config_t *config)
void websocket_server_destroy(websocket_server_t *server)
void platform_disable_keepawake(void)
asciichat_error_t platform_enable_keepawake(void)
void asciichat_mdns_destroy(asciichat_mdns_t *mdns)
asciichat_mdns_t * asciichat_mdns_init(void)
asciichat_error_t asciichat_mdns_advertise(asciichat_mdns_t *mdns, const asciichat_mdns_service_t *service)
char * get_config_dir(void)
const options_t * options_get(void)
void acds_server_shutdown(acds_server_t *server)
Shutdown discovery server.
asciichat_error_t acds_server_init(acds_server_t *server, const acds_config_t *config)
Initialize discovery server.
asciichat_error_t acds_server_run(acds_server_t *server)
Run discovery server main loop.
Discovery server configuration.
char address[256]
IPv4 bind address (empty = all interfaces)
turn_server_t turn_servers[4]
TURN server configurations.
char database_path[512]
SQLite database path.
bool require_server_identity
Require servers to provide signed identity when creating sessions.
uint8_t turn_count
Number of configured TURN servers (0-4)
int port
TCP listen port (default 27225)
char turn_secret[256]
Shared secret for TURN credential generation (HMAC-SHA1)
uint8_t stun_count
Number of configured STUN servers (0-4)
char key_path[512]
Ed25519 identity key file path.
char log_file[512]
Log file path (empty = stderr)
char address6[256]
IPv6 bind address (empty = all interfaces)
bool require_client_identity
Require clients to provide signed identity when joining sessions.
stun_server_t stun_servers[4]
STUN server configurations.
log_level_t log_level
Logging verbosity level.
tcp_server_t tcp_server
TCP server abstraction.
sqlite3 * db
SQLite database handle.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
#define PLATFORM_MAX_PATH_LENGTH
void nat_upnp_close(nat_upnp_context_t **ctx)
asciichat_error_t nat_upnp_open(uint16_t internal_port, const char *description, nat_upnp_context_t **ctx)
asciichat_error_t nat_upnp_get_address(const nat_upnp_context_t *ctx, char *addr, size_t addr_len)