ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
server_like.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define SESSION_SERVER_LIKE_PUBLIC_ADDR_BUFFER_SIZE 22
4
14
17#include "server_like.h"
18#include "quic_listeners.h"
20#include <ascii-chat/common.h>
23#include <ascii-chat/log/log.h>
25#include <ascii-chat/buffer.h>
30#include <libplatform/compiler.h>
31#include <libplatform/system.h>
32#include <libplatform/terminal.h>
33#include <libplatform/systemd.h>
35#include <libplatform/system.h>
36#include <libplatform/terminal.h>
37#include <ascii-chat/util/ip.h>
39#include <signal.h>
40#include <string.h>
41
42/* ============================================================================
43 * Static State
44 * ============================================================================ */
45
46static network_server_t *g_network_server;
47static quic_endpoint_t *g_quic_endpoint;
48static quic_endpoint_t *g_webtransport_endpoint;
49
50static asciichat_mdns_t *g_mdns_ctx = NULL;
51static nat_upnp_context_t *g_upnp_ctx = NULL;
52static nat_upnp_context_t *g_ws_upnp_ctx = NULL;
53static platform_thread_t g_upnp_thread;
54static bool g_upnp_thread_started = false;
55static atomic_t g_upnp_stop = {0};
56static atomic_t g_upnp_ready = {0};
57
58// The worker is the sole mapping writer after mode initialization. Shutdown
59// joins it before freeing the context or issuing the delete request.
60static void *upnp_renewal_thread(void *unused) {
61 (void)unused;
62 while (!atomic_load_bool(&g_upnp_stop)) {
63 if (atomic_load_bool(&g_upnp_ready)) {
64 nat_upnp_context_t *mappings[] = {g_upnp_ctx, g_ws_upnp_ctx};
65 for (size_t i = 0; i < sizeof(mappings) / sizeof(mappings[0]); i++) {
66 if (mappings[i] && time_get_ns() >= mappings[i]->refresh_at_ns) {
68 if (nat_upnp_refresh(mappings[i]) != ASCIICHAT_OK) {
69 LOG_ERRNO_IF_SET("Router mapping renewal failed");
70 }
72 }
73 }
74 }
76 }
77 return NULL;
78}
79
80static platform_thread_t g_status_screen_thread;
81static bool g_status_screen_thread_started = false;
82
84static session_server_like_status_fn g_status_fn = NULL;
85static void *g_status_user_data = NULL;
86
87/* ============================================================================
88 * Signal Handler
89 * ============================================================================ */
90
92static atomic_t g_shutdown_requested = {0};
93
94static session_server_like_interrupt_fn g_interrupt_fn = NULL;
95
96static void server_like_signal_handler(int sig) {
97 atomic_store_u64(&g_shutdown_requested, 1);
98 if (g_interrupt_fn) {
99 g_interrupt_fn(sig);
100 }
101}
102
103/* ============================================================================
104 * Keyboard Queue (lock-free SPSC ring buffer)
105 * ============================================================================ */
106
107/* ============================================================================
108 * Status Screen Thread
109 * ============================================================================ */
110
112static atomic_t g_status_should_exit = {0};
113
114static void *status_screen_thread_func(void *arg) {
115 (void)arg;
116
117 uint32_t fps = GET_OPTION(fps);
118 if (fps == 0) {
119 fps = 60;
120 }
121 uint64_t frame_interval_us = US_PER_SEC_INT / fps;
122
123 log_debug("Status screen thread started (target %u FPS)", fps);
124
125 int output_fd = platform_isatty(STDOUT_FILENO) ? STDOUT_FILENO : STDERR_FILENO;
126
127 // Stderr is the status output when stdout is redirected; retain it in that
128 // case. The presentation controller suppresses competing terminal log writes.
129 platform_stderr_redirect_handle_t stderr_redirect = {.original_fd = -1, .devnull_fd = -1};
130 if (output_fd != STDERR_FILENO) {
131 stderr_redirect = platform_stderr_redirect_to_null();
132 }
133
134 while (!atomic_load_bool(&g_status_should_exit)) {
138
139 // Call mode's status callback to populate status data
140 // Callbacks outlive the borrowed run configuration until the thread stops.
141 if (g_status_fn) {
142 ui_status_t status;
143 memset(&status, 0, sizeof(status));
144 g_status_fn(g_status_user_data, &status);
146 }
147
148 // Maintain frame rate
150 uint64_t frame_time = frame_end - frame_start;
151 if (frame_time < frame_interval_us) {
152 platform_sleep_us(frame_interval_us - frame_time);
153 }
154 }
155
157
158 platform_stderr_restore(stderr_redirect);
159
160 log_debug("Status screen thread exiting");
161 return NULL;
162}
163
164/* ============================================================================
165 * WebSocket Server Thread
166 * ============================================================================ */
167
169 return g_network_server;
170}
171
173 return g_mdns_ctx;
174}
175
177 return g_upnp_ctx;
178}
179
180/* ============================================================================
181 * Helpers
182 * ============================================================================ */
183
184asciichat_error_t session_server_like_mdns_advertise(const char *name, const char *type, uint16_t port,
185 const char **txt, size_t txt_count) {
186 if (!g_mdns_ctx) {
187 return SET_ERRNO(ERROR_INVALID_STATE, "mDNS context not initialized");
188 }
189
190 char hostname[HOSTNAME_BUFFER_SIZE] = {0};
191 gethostname(hostname, sizeof(hostname) - 1);
192
193 asciichat_mdns_service_t service = {
194 .name = name,
195 .type = type,
196 .host = hostname,
197 .port = port,
198 .txt_records = txt,
199 .txt_count = txt_count,
200 };
201
202 asciichat_error_t result = asciichat_mdns_advertise(g_mdns_ctx, &service);
203 if (result != ASCIICHAT_OK) {
204 LOG_ERRNO_IF_SET("Failed to advertise mDNS service");
205 log_warn("mDNS advertising failed - LAN discovery disabled");
206 } else {
207 log_info("mDNS: Service advertised as '%s' type '%s' (port=%d)", name, type, port);
208 }
209
210 return result;
211}
212
213/* ============================================================================
214 * Accessors (shutdown state and network resources)
215 * ============================================================================ */
216
218 server_like_signal_handler(SIGINT);
219}
220
222 return (bool)atomic_load_u64(&g_shutdown_requested);
223}
224
225/* ============================================================================
226 * Shared Handshake
227 * ============================================================================ */
228
230 if (!config) {
231 return SET_ERRNO(ERROR_INVALID_PARAM, "config is NULL");
232 }
233 if (!config->init_fn) {
234 return SET_ERRNO(ERROR_INVALID_PARAM, "init_fn is NULL");
235 }
236 if (!config->interrupt_fn) {
237 return SET_ERRNO(ERROR_INVALID_PARAM, "interrupt_fn is NULL");
238 }
239 if (!config->connections.packet) {
240 return SET_ERRNO(ERROR_INVALID_PARAM, "connection packet callback is NULL");
241 }
242
244
245 /* === 1. Keepawake === */
246
247 bool keepawake_enabled = GET_OPTION(enable_keepawake);
248 bool keepawake_disabled = GET_OPTION(disable_keepawake);
249 if (keepawake_enabled && keepawake_disabled) {
250 return SET_ERRNO(ERROR_INVALID_PARAM, "Cannot specify both --keepawake and --no-keepawake");
251 }
252 if (keepawake_enabled) {
254 bool suppressed = errno_notifications_suppress(true);
255 asciichat_error_t keepawake_result = platform_enable_keepawake();
257 if (keepawake_result != ASCIICHAT_OK) {
258 log_warn("Unable to inhibit system sleep: %s", asciichat_error_string(keepawake_result));
259 }
261 }
262
263 /* === 2. File descriptor limit === */
264
265 if (config->raise_fd_limit) {
267 }
268
269 /* === 3. Signal handlers === */
270
271 atomic_store_u64(&g_shutdown_requested, 0);
272 g_interrupt_fn = config->interrupt_fn;
273 platform_signal(SIGINT, server_like_signal_handler);
274 platform_signal(SIGTERM, server_like_signal_handler);
275#ifndef _WIN32
276 platform_signal(SIGPIPE, SIG_IGN);
277#endif
278
279 /* === 4. TCP server init === */
280
281 int port = GET_OPTION(port);
282 const char *address = GET_OPTION(address);
283 const char *address6 = GET_OPTION(address6);
284
285 const char *effective_address = (address && strlen(address) > 0) ? address : "127.0.0.1";
286 const char *effective_address6 = (address6 && strlen(address6) > 0) ? address6 : "::1";
287 bool ipv4_has_value = true;
288
289 result = session_quic_listeners_open(port, &g_quic_endpoint, &g_webtransport_endpoint);
290 if (result != ASCIICHAT_OK)
291 goto cleanup;
292
293 network_server_config_t tcp_config = {
294 .port = port,
295 .ipv4_address = effective_address,
296 .ipv6_address = effective_address6,
297 .bind_ipv4 = true,
298 .bind_ipv6 = true,
299 .callbacks = config->connections,
300 .max_payload = config->max_payload,
301 .partial_timeout_ms = config->partial_timeout_ms,
302 .idle_timeout_ms = config->idle_timeout_ms,
303 .websocket_enabled = config->websocket_enabled,
304 .websocket_port = GET_OPTION(websocket_port),
305 .tls_cert_path = GET_OPTION(tls_cert),
306 .tls_key_path = GET_OPTION(tls_key),
307 .tick = config->tick_fn,
308 .tick_context = config->tick_user_data,
309 .user_data = config->connection_user_data,
310 .quic_endpoint = g_quic_endpoint,
311 .webtransport_endpoint = g_webtransport_endpoint,
312 };
313
314 result = network_server_create(&tcp_config, &g_network_server);
315 if (result != ASCIICHAT_OK) {
316 log_error("Failed to initialize TCP server on port %d", port);
317 goto cleanup;
318 }
319 log_info("TCP server initialized on port %d", port);
320
321 /* === 5. UPnP port mapping === */
322
323 bool upnp_requested = config->upnp.enabled && GET_OPTION(enable_upnp);
324 bool ipv4_loopback = ipv4_has_value && is_localhost_ipv4(address);
325 bool ipv4_listener = tcp_config.bind_ipv4 && !ipv4_loopback;
326 if (upnp_requested && ipv4_listener) {
327 log_info("UPnP status: discovering (TCP port %d)", port);
329 asciichat_error_t upnp_result = nat_upnp_open(port, config->upnp.description, &g_upnp_ctx);
330 if (upnp_result == ASCIICHAT_OK && !nat_upnp_matches_bind_address(g_upnp_ctx, tcp_config.ipv4_address)) {
331 log_warn("NAT: gateway mapping does not target the bound IPv4 listener; removing it");
332 nat_upnp_close(&g_upnp_ctx);
333 upnp_result = ERROR_NETWORK;
334 }
335 if (upnp_result == ASCIICHAT_OK && nat_upnp_is_active(g_upnp_ctx)) {
337 if (nat_upnp_get_address(g_upnp_ctx, public_addr, sizeof(public_addr)) == ASCIICHAT_OK) {
338 log_info("UPnP status: mapped at %s (external reachability unverified)", public_addr);
339 }
340 } else {
341 log_warn("UPnP status: unavailable. Listener remains enabled; for Internet access, "
342 "check router support or manually forward TCP port %d. External reachability is unverified.",
343 port);
344 }
346 } else if (upnp_requested) {
347 log_info("UPnP status: disabled (requires a non-loopback IPv4 listener)");
348 } else {
349 log_info("UPnP status: disabled");
350 if (ipv4_listener && (!ipv4_has_value || strcmp(address, "0.0.0.0") == 0 || is_lan_ipv4(address))) {
351 log_info("For connections from outside your LAN, try --port-forwarding or forward TCP port %d on your router.",
352 port);
353 }
354 }
355
356 /* === 6. mDNS === */
357
358 if (config->mdns.enabled) {
359 g_mdns_ctx = asciichat_mdns_init();
360 if (!g_mdns_ctx) {
361 LOG_ERRNO_IF_SET("Failed to initialize mDNS (non-fatal, LAN discovery disabled)");
362 log_warn("mDNS disabled - LAN discovery will not be available");
363 } else if (config->mdns.service_name) {
364 // Immediate advertisement if service_name is provided
366 }
367 }
368
369 /* === 8. Mode-specific init === */
370
371 result = config->init_fn(config->init_user_data);
372 if (result != ASCIICHAT_OK) {
373 log_error("Mode initialization failed");
374 goto cleanup;
375 }
376
377 /* === 8b. WebSocket event loop thread start (after mode init succeeds) === */
378
379 if (!atomic_load_u64(&g_shutdown_requested)) {
380 result = network_server_start(g_network_server);
381 if (result != ASCIICHAT_OK)
382 goto cleanup;
383 platform_systemd_notify_status("Listener started");
384 platform_systemd_notify_ready();
385 } else {
386 goto cleanup;
387 }
388
389 if (upnp_requested && network_server_websocket_enabled(g_network_server)) {
390 // WebSocket binds independently of the raw TCP listener's address.
391 asciichat_errno_scope_t ws_mapping_scope = errno_scope_begin();
392 if (nat_upnp_open((uint16_t)GET_OPTION(websocket_port), "ascii-chat WebSocket", &g_ws_upnp_ctx) != ASCIICHAT_OK) {
393 log_warn("WebSocket mapping unavailable; manually forward TCP port %d if needed", GET_OPTION(websocket_port));
394 }
395 errno_scope_end(ws_mapping_scope, ASCIICHAT_ERRNO_HANDLED);
396 }
397
398 if (g_upnp_ctx || g_ws_upnp_ctx) {
399 atomic_store_bool(&g_upnp_stop, false);
400 atomic_store_bool(&g_upnp_ready, false);
401 if (platform_thread_create(&g_upnp_thread, "upnp_renewal", upnp_renewal_thread, NULL) == 0) {
402 g_upnp_thread_started = true;
403 } else {
404 log_warn("NAT: could not start lease renewal; removing the temporary mapping");
405 nat_upnp_close(&g_upnp_ctx);
406 nat_upnp_close(&g_ws_upnp_ctx);
407 }
408 }
409
410 // Keep bounded log capture available even when only the stats overlay is used.
412 /* === 9. Status screen === */
413
414 if (config->status_fn) {
415 bool status_opt = GET_OPTION(status_screen);
416 bool status_explicit = GET_OPTION(status_screen_explicitly_set);
417 if ((status_opt && terminal_is_interactive()) || (status_explicit && status_opt)) {
418 atomic_store_bool(&g_status_should_exit, false);
419
420 // Store callback and user data for the lifetime of the status thread.
421 g_status_fn = config->status_fn;
422 g_status_user_data = config->status_user_data;
423
424 if (platform_thread_create(&g_status_screen_thread, "status_screen", status_screen_thread_func, NULL) == 0) {
425 g_status_screen_thread_started = true;
426 log_info("Status screen thread started");
427 } else {
428 log_error("Failed to start status screen thread");
430 g_status_fn = NULL;
431 g_status_user_data = NULL;
432 }
433 }
434 }
435
436 atomic_store_bool(&g_upnp_ready, true);
437
438 /* === 10. TCP accept loop (blocks) === */
439
440 log_info("Server accepting connections on port %d", port);
442 result = network_server_run(g_network_server);
443 if (result != ASCIICHAT_OK) {
444 log_error("TCP server exited with error");
445 }
446
447 /* === CLEANUP === */
448
449cleanup:
450 if (g_upnp_thread_started) {
451 atomic_store_bool(&g_upnp_stop, true);
452 thread_join(&g_upnp_thread, NULL);
453 g_upnp_thread_started = false;
454 }
455
456 // Remove the mapping before mode teardown, which can wait on other services.
457 if (g_upnp_ctx) {
458 nat_upnp_close(&g_upnp_ctx);
459 log_debug("UPnP port mapping closed");
460 }
461
462 nat_upnp_close(&g_ws_upnp_ctx);
463
464 /* 11. Stop status screen */
465 if (g_status_screen_thread_started) {
466 atomic_store_bool(&g_status_should_exit, true);
467 int join_result = thread_join_timeout(&g_status_screen_thread, NULL, 2000 * NS_PER_MS_INT);
468 if (join_result != 0) {
469 log_warn("Status screen thread did not exit within 2s");
470 }
471 g_status_screen_thread_started = false;
472 g_status_fn = NULL;
473 g_status_user_data = NULL;
474 }
476
477 asciichat_error_t stopped = network_server_stop(g_network_server);
478 if (stopped != ASCIICHAT_OK)
479 return stopped;
480
481 /* 13. Mode-specific cleanup (only call if config provided) */
482 if (config && config->cleanup_fn) {
483 config->cleanup_fn(config->cleanup_user_data);
484 }
485
486 network_server_destroy(&g_network_server);
487 stopped = session_quic_listeners_close(&g_quic_endpoint, &g_webtransport_endpoint);
488 if (stopped != ASCIICHAT_OK)
489 return stopped;
490
491 /* 17. mDNS cleanup */
492 if (g_mdns_ctx) {
493 asciichat_mdns_destroy(g_mdns_ctx);
494 g_mdns_ctx = NULL;
495 log_debug("mDNS context destroyed");
496 }
497
498 /* 18. Keepawake */
499 if (keepawake_enabled) {
501 }
502
503 return result;
504}
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
bool errno_notifications_suppress(bool suppress)
asciichat_errno_scope_t errno_scope_begin(void)
Synchronized per-thread pending error stacks and value snapshots. SET_ERRNO pushes,...
#define SET_ERRNO(code,...)
@ ASCIICHAT_ERRNO_HANDLED
#define LOG_ERRNO_IF_SET(message)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
unsigned short uint16_t
Definition common.h:61
unsigned int uint32_t
Definition common.h:62
unsigned long long uint64_t
Definition common.h:63
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_INVALID_STATE
@ ERROR_NETWORK
Definition error_codes.h:72
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
#define HOSTNAME_BUFFER_SIZE
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define NS_PER_MS_INT
Definition time.h:168
void time_sleep_ns(uint64_t ns)
Configuration for adaptive sleep behavior.
Definition time.c:113
#define US_PER_SEC_INT
Definition time.h:173
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
signal_handler_t platform_signal(int sig, signal_handler_t handler)
Set a signal handler.
platform_stderr_redirect_handle_t platform_stderr_redirect_to_null(void)
Redirect stderr to /dev/null temporarily.
void platform_stderr_restore(platform_stderr_redirect_handle_t handle)
Restore stderr from a redirect handle.
void platform_disable_keepawake(void)
Disable system sleep prevention (allow OS to sleep).
uint64_t platform_get_monotonic_time_us(void)
Get monotonic time in microseconds.
void platform_raise_fd_limit(unsigned int limit)
Raise the file descriptor limit for the current process.
asciichat_error_t platform_enable_keepawake(void)
Enable system sleep prevention (keepawake mode).
int is_lan_ipv4(const char *ip)
Check if IPv4 address is a private/LAN address.
Definition ip.c:1104
int is_localhost_ipv4(const char *ip)
Check if IPv4 address is localhost.
Definition ip.c:1218
Server-side handshake functions.
struct quic_endpoint quic_endpoint_t
struct network_server network_server_t
🌍 IP Address Parsing and Formatting Utilities
asciichat_error_t network_server_create(const network_server_config_t *config, network_server_t **out)
asciichat_error_t network_server_start(network_server_t *s)
void network_server_destroy(network_server_t **owner)
asciichat_error_t network_server_stop(network_server_t *s)
asciichat_error_t network_server_run(network_server_t *s)
bool network_server_websocket_enabled(network_server_t *s)
📝 Logging API with multiple log levels and terminal output control
void asciichat_mdns_destroy(asciichat_mdns_t *mdns)
Shutdown mDNS context and cleanup.
asciichat_mdns_t * asciichat_mdns_init(void)
Initialize mDNS context.
asciichat_error_t asciichat_mdns_advertise(asciichat_mdns_t *mdns, const asciichat_mdns_service_t *service)
Advertise a service on the local network.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
asciichat_error_t session_quic_listeners_close(quic_endpoint_t **native, quic_endpoint_t **webtransport)
asciichat_error_t session_quic_listeners_open(int port, quic_endpoint_t **native, quic_endpoint_t **webtransport)
bool log_search_check_signal_cancel(void)
Check and clear the signal-cancel flag.
Definition search.c:403
void log_search_exit_mode(bool accept)
Exit search mode.
Definition search.c:303
Interactive search filtering for terminal screens.
ACIP shared/bidirectional packet sending functions.
void session_server_like_request_shutdown(void)
#define SESSION_SERVER_LIKE_PUBLIC_ADDR_BUFFER_SIZE
Definition server_like.c:3
bool session_server_like_shutdown_requested(void)
asciichat_mdns_t * session_server_like_get_mdns_ctx(void)
asciichat_error_t session_server_like_mdns_advertise(const char *name, const char *type, uint16_t port, const char **txt, size_t txt_count)
asciichat_error_t session_server_like_run(const session_server_like_config_t *config)
network_server_t * session_server_like_get_network_server(void)
nat_upnp_context_t * session_server_like_get_upnp_ctx(void)
struct session_server_like_config session_server_like_config_t
The useful part of session server like config is in [struct].mdns, [struct].upnp, init_fn,...
void(* session_server_like_status_fn)(void *user_data, ui_status_t *out_status)
Definition server_like.h:25
void(* session_server_like_interrupt_fn)(int sig)
Definition server_like.h:21
void ui_status_log_destroy(void)
Cleanup status screen log capture system.
Definition status.c:55
void ui_status_log_init(void)
Initialize status screen log capture system.
Definition status.c:47
bool ui_status_display_interactive(const ui_status_t *status)
Display status screen with keyboard input support Returns true if status screen should continue,...
Definition status.c:156
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
Service information for advertisement.
Owns one mDNS browse or advertisement session, including its socket, event-loop registration,...
asciichat_error_t(* packet)(network_connection_t *, packet_type_t, const void *, size_t, void *)
Handle for temporary stderr redirection.
Definition system.h:379
void(* tick_fn)(void *user_data)
Definition server_like.h:39
struct session_server_like_config::@331157361304265235031377013321160306024244316160 mdns
session_server_like_interrupt_fn interrupt_fn
Definition server_like.h:37
struct session_server_like_config::@022167030105260237161361274212172160377140006150 upnp
network_connection_callbacks_t connections
Definition server_like.h:48
session_server_like_init_fn init_fn
Definition server_like.h:34
session_server_like_cleanup_fn cleanup_fn
Definition server_like.h:42
session_server_like_status_fn status_fn
Definition server_like.h:45
Status information for server/discovery service modes.
Definition status.h:38
⏱️ High-precision timing utilities using sokol_time.h and uthash
void ui_controller_remove(ui_screen_t screen)
void ui_controller_title_ready(void)
@ UI_SCREEN_STATUS
asciichat_error_t nat_upnp_refresh(nat_upnp_context_t *ctx)
Refresh port mapping (e.g., for long-running servers).
Definition upnp.c:316
bool nat_upnp_matches_bind_address(const nat_upnp_context_t *ctx, const char *bind_address)
Whether the mapping targets a specific IPv4 bind address, or any wildcard listener.
Definition upnp.c:296
bool nat_upnp_is_active(const nat_upnp_context_t *ctx)
Check if port mapping is still active.
Definition upnp.c:292
void nat_upnp_close(nat_upnp_context_t **ctx)
Close port mapping and clean up.
Definition upnp.c:251
asciichat_error_t nat_upnp_open(uint16_t internal_port, const char *description, nat_upnp_context_t **ctx)
Discover and open port via UPnP.
Definition upnp.c:203
asciichat_error_t nat_upnp_get_address(const nat_upnp_context_t *ctx, char *addr, size_t addr_len)
Get the public address (IP:port) for advertising to clients.
Definition upnp.c:343
struct nat_upnp_context nat_upnp_context_t
Handle to UPnP context.