ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
upnp.c
Go to the documentation of this file.
1
14#include <stdio.h>
15#include <stdlib.h>
16#include <string.h>
17#include <time.h>
18
19#include "upnp.h"
20#include "common.h"
21#include "log/logging.h"
22
23#ifdef __APPLE__
24// Only include natpmp if we're on Apple and miniupnpc is available
25#ifdef HAVE_MINIUPNPC
26#include <natpmp.h>
27#endif
28#endif
29
30// miniupnpc is conditionally included based on CMake detection
31// If not found, HAVE_MINIUPNPC will not be defined
32// The build system defines HAVE_MINIUPNPC=1 in compile definitions if miniupnpc is found
33#ifdef HAVE_MINIUPNPC
34#include <miniupnpc/miniupnpc.h>
35#include <miniupnpc/upnpcommands.h>
36#include <miniupnpc/upnperrors.h>
37#endif
38
43#ifdef HAVE_MINIUPNPC
44static asciichat_error_t upnp_error_to_asciichat(int upnp_result) {
45 switch (upnp_result) {
46 case UPNPCOMMAND_SUCCESS:
47 return ASCIICHAT_OK;
48 case UPNPCOMMAND_UNKNOWN_ERROR:
49 case UPNPCOMMAND_INVALID_ARGS:
50 case UPNPCOMMAND_HTTP_ERROR:
51 SET_ERRNO(ERROR_NETWORK, "UPnP error: %d", upnp_result);
52 return ERROR_NETWORK;
53 default:
54 SET_ERRNO(ERROR_NETWORK, "UPnP unknown error: %d", upnp_result);
55 return ERROR_NETWORK;
56 }
57}
58#endif
59
65#ifdef HAVE_MINIUPNPC
66static asciichat_error_t upnp_try_map_port(uint16_t internal_port, const char *description, nat_upnp_context_t *ctx) {
67 struct UPNPDev *device_list = NULL;
68 struct UPNPUrls urls;
69 struct IGDdatas data;
70 int upnp_result = 0;
71 char port_str[10];
72 char external_addr[40];
73
74 memset(&urls, 0, sizeof(urls));
75 memset(&data, 0, sizeof(data));
76
77 // Step 1: Discover UPnP devices (2 second timeout for faster fallback)
78 log_debug("UPnP: Starting discovery (2 second timeout)...");
79 device_list = upnpDiscover(2000, // timeout in milliseconds
80 NULL, // multicast interface
81 NULL, // minissdpdpath
82 0, // sameport
83 0, // ipv6
84 2, // ttl
85 NULL); // error pointer
86
87 if (!device_list) {
88 SET_ERRNO(ERROR_NETWORK, "UPnP: No devices found (router may not support UPnP)");
89 return ERROR_NETWORK;
90 }
91
92 log_debug("UPnP: Found %d device(s)", 1); // device_list is a linked list, just log 1 for now
93
94 // Step 2: Find the Internet Gateway Device (IGD)
95 // Note: UPNP_GetValidIGD signature changed in newer miniupnpc versions
96 // Older: UPNP_GetValidIGD(devlist, urls, data, external_addr, len)
97 // Newer: UPNP_GetValidIGD(devlist, urls, data, external_addr, len, lanaddr, lanaddr_len)
98 upnp_result = UPNP_GetValidIGD(device_list, &urls, &data, external_addr, sizeof(external_addr), NULL, 0);
99
100 if (upnp_result != 1) { // 1 = UPNP_IGD_VALID_CONNECTED (value may vary between versions)
101 SET_ERRNO(ERROR_NETWORK, "UPnP: No valid Internet Gateway found");
102 freeUPNPDevlist(device_list);
103 FreeUPNPUrls(&urls);
104 return ERROR_NETWORK;
105 }
106
107 log_debug("UPnP: Found valid IGD, external address: %s", external_addr);
108
109 // Step 3: Get external IP
110 upnp_result = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, external_addr);
111
112 if (upnp_result != UPNPCOMMAND_SUCCESS) {
113 SET_ERRNO(ERROR_NETWORK, "UPnP: Failed to get external IP: %s", strupnperror(upnp_result));
114 freeUPNPDevlist(device_list);
115 FreeUPNPUrls(&urls);
116 return ERROR_NETWORK;
117 }
118
119 SAFE_STRNCPY(ctx->external_ip, external_addr, sizeof(ctx->external_ip));
120 log_info("UPnP: External IP detected: %s", ctx->external_ip);
121
122 // Step 4: Request port mapping
123 snprintf(port_str, sizeof(port_str), "%u", internal_port);
124
125 log_debug("UPnP: Requesting port mapping for port %u (%s)...", internal_port, description);
126
127 upnp_result = UPNP_AddPortMapping(urls.controlURL, // controlURL
128 data.first.servicetype, // servicetype
129 port_str, // extPort (external port, same as internal for now)
130 port_str, // inPort (internal port)
131 "127.0.0.1", // inClient (internal IP - gets resolved by router)
132 description, // description
133 "TCP", // protocol
134 NULL, // remoteHost (any)
135 "3600"); // leaseDuration (1 hour)
136
137 if (upnp_result != UPNPCOMMAND_SUCCESS) {
138 SET_ERRNO(ERROR_NETWORK, "UPnP: Failed to add port mapping: %s", strupnperror(upnp_result));
139 freeUPNPDevlist(device_list);
140 FreeUPNPUrls(&urls);
141 return ERROR_NETWORK;
142 }
143
144 log_info("UPnP: ✓ Port %u successfully mapped on %s", internal_port, urls.controlURL);
145
146 // Store device description for logging
147 SAFE_STRNCPY(ctx->device_description, urls.controlURL, sizeof(ctx->device_description));
148 ctx->internal_port = internal_port;
149 ctx->mapped_port = internal_port;
150 ctx->is_natpmp = false;
151 ctx->is_mapped = true;
152
153 // Cleanup UPnP structures
154 freeUPNPDevlist(device_list);
155 FreeUPNPUrls(&urls);
156
157 return ASCIICHAT_OK;
158}
159#else
160// Stub implementation when miniupnpc is not available
161static asciichat_error_t upnp_try_map_port(uint16_t internal_port, const char *description, nat_upnp_context_t *ctx) {
162 (void)internal_port;
163 (void)description;
164 (void)ctx;
165 SET_ERRNO(ERROR_NETWORK, "miniupnpc not installed (UPnP disabled)");
166 return ERROR_NETWORK;
167}
168#endif
169
175static asciichat_error_t natpmp_try_map_port(uint16_t internal_port, const char *description, nat_upnp_context_t *ctx) {
176#ifndef __APPLE__
177 (void)internal_port;
178 (void)description;
179 (void)ctx;
180 SET_ERRNO(ERROR_NETWORK, "NAT-PMP: Not available on this platform (Apple only)");
181 return ERROR_NETWORK;
182#else
183 natpmp_t natpmp;
184 natpmp_response_t response;
185 int result;
186 char external_ip_str[16];
187
188 log_debug("NAT-PMP: Initializing (fallback)...");
189
190 // Initialize NAT-PMP
191 result = initnatpmp(&natpmp, 0, 0);
192 if (result < 0) {
193 SET_ERRNO(ERROR_NETWORK, "NAT-PMP: Failed to initialize (%d)", result);
194 return ERROR_NETWORK;
195 }
196
197 // Get external IP
198 result = sendpublicaddressrequest(&natpmp);
199 if (result < 0) {
200 closenatpmp(&natpmp);
201 SET_ERRNO(ERROR_NETWORK, "NAT-PMP: Failed to request public address");
202 return ERROR_NETWORK;
203 }
204
205 // Wait for response
206 memset(&response, 0, sizeof(response));
207 result = getnatpmpresponseorretry(&natpmp, &response);
208 if (result != NATPMP_TRYAGAIN && response.type == NATPMP_RESPTYPE_PUBLICADDRESS) {
209 unsigned char *ipv4 = (unsigned char *)&response.pnu.publicaddress.publicaddress;
210 snprintf(external_ip_str, sizeof(external_ip_str), "%u.%u.%u.%u", ipv4[0], ipv4[1], ipv4[2], ipv4[3]);
211 SAFE_STRNCPY(ctx->external_ip, external_ip_str, sizeof(ctx->external_ip));
212 log_info("NAT-PMP: External IP detected: %s", ctx->external_ip);
213 }
214
215 // Request port mapping
216 result = sendnewportmappingrequest(&natpmp, NATPMP_PROTOCOL_TCP, internal_port, internal_port,
217 3600); // 1 hour lease
218 if (result < 0) {
219 closenatpmp(&natpmp);
220 SET_ERRNO(ERROR_NETWORK, "NAT-PMP: Failed to send port mapping request");
221 return ERROR_NETWORK;
222 }
223
224 // Wait for mapping response
225 memset(&response, 0, sizeof(response));
226 result = getnatpmpresponseorretry(&natpmp, &response);
227 if (result != NATPMP_TRYAGAIN && response.type == NATPMP_RESPTYPE_TCPPORTMAPPING) {
228 log_info("NAT-PMP: ✓ Port %u successfully mapped", internal_port);
229 ctx->internal_port = internal_port;
230 ctx->mapped_port = response.pnu.newportmapping.mappedexternalport;
231 ctx->is_natpmp = true;
232 ctx->is_mapped = true;
233 SAFE_STRNCPY(ctx->device_description, "Time Capsule/Apple AirPort", sizeof(ctx->device_description));
234 } else {
235 closenatpmp(&natpmp);
236 SET_ERRNO(ERROR_NETWORK, "NAT-PMP: Failed to map port");
237 return ERROR_NETWORK;
238 }
239
240 closenatpmp(&natpmp);
241 return ASCIICHAT_OK;
242#endif
243}
244
245// ============================================================================
246// Public API Implementation
247// ============================================================================
248
249asciichat_error_t nat_upnp_open(uint16_t internal_port, const char *description, nat_upnp_context_t **ctx) {
250 if (!ctx || !description) {
251 return SET_ERRNO(ERROR_INVALID_PARAM, "nat_upnp_open: Invalid arguments");
252 }
253
254 // Allocate context
256 if (!(*ctx)) {
257 return ERROR_MEMORY;
258 }
259
260 memset(*ctx, 0, sizeof(nat_upnp_context_t));
261
262 // Try UPnP first (works on ~90% of home routers)
263 log_info("NAT: Attempting UPnP port mapping for port %u...", internal_port);
264 asciichat_error_t result = upnp_try_map_port(internal_port, description, *ctx);
265
266 if (result == ASCIICHAT_OK) {
267 log_info("NAT: ✓ UPnP port mapping successful!");
268 return ASCIICHAT_OK;
269 }
270
271 log_info("NAT: UPnP failed, trying NAT-PMP fallback...");
272 result = natpmp_try_map_port(internal_port, description, *ctx);
273
274 if (result == ASCIICHAT_OK) {
275 log_info("NAT: ✓ NAT-PMP port mapping successful!");
276 return ASCIICHAT_OK;
277 }
278
279 // Both UPnP and NAT-PMP failed - this is OK, not fatal
280 log_warn("NAT: Both UPnP and NAT-PMP failed. Direct TCP won't work, will use ACDS + WebRTC.");
281 log_warn("NAT: This is normal for strict NATs. No action required.");
282
283 SAFE_FREE(*ctx);
284 *ctx = NULL;
285
286 return SET_ERRNO(ERROR_NETWORK, "NAT: No automatic port mapping available (will use WebRTC)");
287}
288
290 if (!ctx || !(*ctx)) {
291 return;
292 }
293
294 if ((*ctx)->is_mapped) {
295 // Note: In a real implementation, we'd remove the port mapping from the gateway.
296 // For MVP, we just log and let the lease expire naturally (typically 1 hour).
297 log_debug("NAT: Port mapping will expire in ~1 hour (cleanup handled by router)");
298 }
299
300 SAFE_FREE(*ctx);
301 *ctx = NULL;
302}
303
305 if (!ctx) {
306 return false;
307 }
308 return ctx->is_mapped && ctx->external_ip[0] != '\0';
309}
310
312 if (!ctx || !ctx->is_mapped) {
313 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: Cannot refresh - no active mapping");
314 }
315
316 log_debug("NAT: Refreshing port mapping (would extend lease in full implementation)");
317
318 // In a real implementation, we'd re-register the port mapping to extend the lease.
319 // For now, we just return success since the lease is 1 hour anyway.
320 return ASCIICHAT_OK;
321}
322
323asciichat_error_t nat_upnp_get_address(const nat_upnp_context_t *ctx, char *addr, size_t addr_len) {
324 if (!ctx || !addr || addr_len < 22) {
325 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: Invalid arguments for get_address");
326 }
327
328 if (!ctx->is_mapped || ctx->external_ip[0] == '\0') {
329 return SET_ERRNO(ERROR_NETWORK, "NAT: No active mapping to advertise");
330 }
331
332 // Format as "IP:port" (e.g., "203.0.113.42:27224")
333 int written = snprintf(addr, addr_len, "%s:%u", ctx->external_ip, ctx->mapped_port);
334
335 if (written < 0 || (size_t)written >= addr_len) {
336 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: Address buffer too small");
337 }
338
339 return ASCIICHAT_OK;
340}
unsigned short uint16_t
Definition common.h:57
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ERROR_NETWORK
Definition error_codes.h:69
@ ERROR_MEMORY
Definition error_codes.h:53
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
📝 Logging API with multiple log levels and terminal output control
Handle to UPnP context.
Definition upnp.h:25
uint16_t internal_port
Internal port we're binding to.
Definition upnp.h:28
uint16_t mapped_port
External port that was mapped (may differ from internal)
Definition upnp.h:27
bool is_natpmp
true if NAT-PMP was used, false if UPnP
Definition upnp.h:30
bool is_mapped
true if port mapping is currently active
Definition upnp.h:31
char device_description[256]
Device name for logging (e.g., "TP-Link Archer C7")
Definition upnp.h:29
char external_ip[16]
Detected external/public IP (e.g., "203.0.113.42")
Definition upnp.h:26
⏱️ High-precision timing utilities using sokol_time.h and uthash
asciichat_error_t nat_upnp_refresh(nat_upnp_context_t *ctx)
Refresh port mapping (e.g., for long-running servers)
Definition upnp.c:311
bool nat_upnp_is_active(const nat_upnp_context_t *ctx)
Check if port mapping is still active.
Definition upnp.c:304
void nat_upnp_close(nat_upnp_context_t **ctx)
Close port mapping and clean up.
Definition upnp.c:289
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:249
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:323
UPnP/NAT-PMP port mapping for direct TCP connectivity.