ascii-chat 0.8.38
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 <ascii-chat/network/nat/upnp.h>
20#include <ascii-chat/common.h>
21#include <ascii-chat/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
44#ifdef HAVE_MINIUPNPC
45static asciichat_error_t upnp_try_map_port(uint16_t internal_port, const char *description, nat_upnp_context_t *ctx) {
46 struct UPNPDev *device_list = NULL;
47 struct UPNPUrls urls;
48 struct IGDdatas data;
49 int upnp_result = 0;
50 char port_str[10];
51 char external_addr[40];
52
53 memset(&urls, 0, sizeof(urls));
54 memset(&data, 0, sizeof(data));
55
56 // Step 1: Discover UPnP devices (2 second timeout for faster fallback)
57 log_debug("UPnP: Starting discovery (2 second timeout)...");
58 device_list = upnpDiscover(2000, // timeout in milliseconds
59 NULL, // multicast interface
60 NULL, // minissdpdpath
61 0, // sameport
62 0, // ipv6
63 2, // ttl
64 NULL); // error pointer
65
66 if (!device_list) {
67 SET_ERRNO(ERROR_NETWORK, "UPnP: No devices found (router may not support UPnP)");
68 return ERROR_NETWORK;
69 }
70
71 log_debug("UPnP: Found %d device(s)", 1); // device_list is a linked list, just log 1 for now
72
73 // Step 2: Find the Internet Gateway Device (IGD)
74 // Note: UPNP_GetValidIGD signature changed in miniupnpc API version 14
75 // API < 14: UPNP_GetValidIGD(devlist, urls, data, external_addr, len) - 5 args
76 // API >= 14: UPNP_GetValidIGD(devlist, urls, data, external_addr, len, lanaddr, lanaddr_len) - 7 args
77 // MINIUPNPC_GETVALIDIGD_7ARG is set by CMake's check_c_source_compiles to detect the actual signature
78#ifdef MINIUPNPC_GETVALIDIGD_7ARG
79 upnp_result = UPNP_GetValidIGD(device_list, &urls, &data, external_addr, sizeof(external_addr), NULL, 0);
80#else
81 upnp_result = UPNP_GetValidIGD(device_list, &urls, &data, external_addr, sizeof(external_addr));
82#endif
83
84 if (upnp_result != 1) { // 1 = UPNP_IGD_VALID_CONNECTED (value may vary between versions)
85 SET_ERRNO(ERROR_NETWORK, "UPnP: No valid Internet Gateway found");
86 freeUPNPDevlist(device_list);
87 FreeUPNPUrls(&urls);
88 return ERROR_NETWORK;
89 }
90
91 log_debug("UPnP: Found valid IGD, external address: %s", external_addr);
92
93 // Step 3: Get external IP
94 upnp_result = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, external_addr);
95
96 if (upnp_result != UPNPCOMMAND_SUCCESS) {
97 SET_ERRNO(ERROR_NETWORK, "UPnP: Failed to get external IP: %s", strupnperror(upnp_result));
98 freeUPNPDevlist(device_list);
99 FreeUPNPUrls(&urls);
100 return ERROR_NETWORK;
101 }
102
103 SAFE_STRNCPY(ctx->external_ip, external_addr, sizeof(ctx->external_ip));
104 log_info("UPnP: External IP detected: %s", ctx->external_ip);
105
106 // Step 4: Request port mapping
107 safe_snprintf(port_str, sizeof(port_str), "%u", internal_port);
108
109 log_debug("UPnP: Requesting port mapping for port %u (%s)...", internal_port, description);
110
111 upnp_result = UPNP_AddPortMapping(urls.controlURL, // controlURL
112 data.first.servicetype, // servicetype
113 port_str, // extPort (external port, same as internal for now)
114 port_str, // inPort (internal port)
115 "127.0.0.1", // inClient (internal IP - gets resolved by router)
116 description, // description
117 "TCP", // protocol
118 NULL, // remoteHost (any)
119 "3600"); // leaseDuration (1 hour)
120
121 if (upnp_result != UPNPCOMMAND_SUCCESS) {
122 SET_ERRNO(ERROR_NETWORK, "UPnP: Failed to add port mapping: %s", strupnperror(upnp_result));
123 freeUPNPDevlist(device_list);
124 FreeUPNPUrls(&urls);
125 return ERROR_NETWORK;
126 }
127
128 log_info("UPnP: ✓ Port %u successfully mapped on %s", internal_port, urls.controlURL);
129
130 // Store device description for logging
131 SAFE_STRNCPY(ctx->device_description, urls.controlURL, sizeof(ctx->device_description));
132 ctx->internal_port = internal_port;
133 ctx->mapped_port = internal_port;
134 ctx->is_natpmp = false;
135 ctx->is_mapped = true;
136
137 // Cleanup UPnP structures
138 freeUPNPDevlist(device_list);
139 FreeUPNPUrls(&urls);
140
141 return ASCIICHAT_OK;
142}
143#else
144// Stub implementation when miniupnpc is not available
145static asciichat_error_t upnp_try_map_port(uint16_t internal_port, const char *description, nat_upnp_context_t *ctx) {
146 (void)internal_port;
147 (void)description;
148 (void)ctx;
149 SET_ERRNO(ERROR_NETWORK, "miniupnpc not installed (UPnP disabled)");
150 return ERROR_NETWORK;
151}
152#endif
153
159static asciichat_error_t natpmp_try_map_port(uint16_t internal_port, nat_upnp_context_t *ctx) {
160#if !defined(__APPLE__) || !defined(HAVE_MINIUPNPC)
161 (void)internal_port;
162 (void)ctx;
163#ifndef __APPLE__
164 SET_ERRNO(ERROR_NETWORK, "NAT-PMP: Not available on this platform (Apple only)");
165#else
166 SET_ERRNO(ERROR_NETWORK, "NAT-PMP: libnatpmp not available (install miniupnpc)");
167#endif
168 return ERROR_NETWORK;
169#else // __APPLE__ && HAVE_MINIUPNPC
170 natpmp_t natpmp;
171 natpmpresp_t response;
172 int result;
173 char external_ip_str[16];
174
175 log_debug("NAT-PMP: Initializing (fallback)...");
176
177 // Initialize NAT-PMP
178 result = initnatpmp(&natpmp, 0, 0);
179 if (result < 0) {
180 SET_ERRNO(ERROR_NETWORK, "NAT-PMP: Failed to initialize (%d)", result);
181 return ERROR_NETWORK;
182 }
183
184 // Get external IP
185 result = sendpublicaddressrequest(&natpmp);
186 if (result < 0) {
187 closenatpmp(&natpmp);
188 SET_ERRNO(ERROR_NETWORK, "NAT-PMP: Failed to request public address");
189 return ERROR_NETWORK;
190 }
191
192 // Wait for response
193 memset(&response, 0, sizeof(response));
194 result = readnatpmpresponseorretry(&natpmp, &response);
195 if (result != NATPMP_TRYAGAIN && response.type == NATPMP_RESPTYPE_PUBLICADDRESS) {
196 unsigned char *ipv4 = (unsigned char *)&response.pnu.publicaddress.addr;
197 safe_snprintf(external_ip_str, sizeof(external_ip_str), "%u.%u.%u.%u", ipv4[0], ipv4[1], ipv4[2], ipv4[3]);
198 SAFE_STRNCPY(ctx->external_ip, external_ip_str, sizeof(ctx->external_ip));
199 log_info("NAT-PMP: External IP detected: %s", ctx->external_ip);
200 }
201
202 // Request port mapping
203 result = sendnewportmappingrequest(&natpmp, NATPMP_PROTOCOL_TCP, internal_port, internal_port,
204 3600); // 1 hour lease
205 if (result < 0) {
206 closenatpmp(&natpmp);
207 SET_ERRNO(ERROR_NETWORK, "NAT-PMP: Failed to send port mapping request");
208 return ERROR_NETWORK;
209 }
210
211 // Wait for mapping response
212 memset(&response, 0, sizeof(response));
213 result = readnatpmpresponseorretry(&natpmp, &response);
214 if (result != NATPMP_TRYAGAIN && response.type == NATPMP_RESPTYPE_TCPPORTMAPPING) {
215 log_info("NAT-PMP: ✓ Port %u successfully mapped", internal_port);
216 ctx->internal_port = internal_port;
217 ctx->mapped_port = response.pnu.newportmapping.mappedpublicport;
218 ctx->is_natpmp = true;
219 ctx->is_mapped = true;
220 SAFE_STRNCPY(ctx->device_description, "Time Capsule/Apple AirPort", sizeof(ctx->device_description));
221 } else {
222 closenatpmp(&natpmp);
223 SET_ERRNO(ERROR_NETWORK, "NAT-PMP: Failed to map port");
224 return ERROR_NETWORK;
225 }
226
227 closenatpmp(&natpmp);
228 return ASCIICHAT_OK;
229#endif // __APPLE__ && HAVE_MINIUPNPC
230}
231
232// ============================================================================
233// Public API Implementation
234// ============================================================================
235
236asciichat_error_t nat_upnp_open(uint16_t internal_port, const char *description, nat_upnp_context_t **ctx) {
237 if (!ctx || !description) {
238 return SET_ERRNO(ERROR_INVALID_PARAM, "nat_upnp_open: Invalid arguments");
239 }
240
241 // Allocate context
242 *ctx = SAFE_MALLOC(sizeof(nat_upnp_context_t), nat_upnp_context_t *);
243 if (!(*ctx)) {
244 return ERROR_MEMORY;
245 }
246
247 memset(*ctx, 0, sizeof(nat_upnp_context_t));
248
249 // Try UPnP first (works on ~90% of home routers)
250 log_info("NAT: Attempting UPnP port mapping for port %u...", internal_port);
251 asciichat_error_t result = upnp_try_map_port(internal_port, description, *ctx);
252
253 if (result == ASCIICHAT_OK) {
254 log_info("NAT: ✓ UPnP port mapping successful!");
255 return ASCIICHAT_OK;
256 }
257
258 log_info("NAT: UPnP failed, trying NAT-PMP fallback...");
259 result = natpmp_try_map_port(internal_port, *ctx);
260
261 if (result == ASCIICHAT_OK) {
262 log_info("NAT: ✓ NAT-PMP port mapping successful!");
263 return ASCIICHAT_OK;
264 }
265
266 // Both UPnP and NAT-PMP failed - this is OK, not fatal
267 log_warn("NAT: Both UPnP and NAT-PMP failed. Direct TCP won't work, will use ACDS + WebRTC.");
268 log_warn("NAT: This is normal for strict NATs. No action required.");
269
270 SAFE_FREE(*ctx);
271 *ctx = NULL;
272
273 return SET_ERRNO(ERROR_NETWORK, "NAT: No automatic port mapping available (will use WebRTC)");
274}
275
276void nat_upnp_close(nat_upnp_context_t **ctx) {
277 if (!ctx || !(*ctx)) {
278 return;
279 }
280
281 if ((*ctx)->is_mapped) {
282 // Note: In a real implementation, we'd remove the port mapping from the gateway.
283 // For MVP, we just log and let the lease expire naturally (typically 1 hour).
284 log_debug("NAT: Port mapping will expire in ~1 hour (cleanup handled by router)");
285 }
286
287 SAFE_FREE(*ctx);
288 *ctx = NULL;
289}
290
291bool nat_upnp_is_active(const nat_upnp_context_t *ctx) {
292 if (!ctx) {
293 return false;
294 }
295 return ctx->is_mapped && ctx->external_ip[0] != '\0';
296}
297
298asciichat_error_t nat_upnp_refresh(nat_upnp_context_t *ctx) {
299 if (!ctx || !ctx->is_mapped) {
300 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: Cannot refresh - no active mapping");
301 }
302
303 log_debug("NAT: Refreshing port mapping (would extend lease in full implementation)");
304
305 // In a real implementation, we'd re-register the port mapping to extend the lease.
306 // For now, we just return success since the lease is 1 hour anyway.
307 return ASCIICHAT_OK;
308}
309
310asciichat_error_t nat_upnp_get_address(const nat_upnp_context_t *ctx, char *addr, size_t addr_len) {
311 if (!ctx || !addr || addr_len < 22) {
312 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: Invalid arguments for get_address");
313 }
314
315 if (!ctx->is_mapped || ctx->external_ip[0] == '\0') {
316 return SET_ERRNO(ERROR_NETWORK, "NAT: No active mapping to advertise");
317 }
318
319 // Format as "IP:port" (e.g., "203.0.113.42:27224")
320 int written = safe_snprintf(addr, addr_len, "%s:%u", ctx->external_ip, ctx->mapped_port);
321
322 if (written < 0 || (size_t)written >= addr_len) {
323 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: Address buffer too small");
324 }
325
326 return ASCIICHAT_OK;
327}
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
asciichat_error_t nat_upnp_refresh(nat_upnp_context_t *ctx)
Definition upnp.c:298
bool nat_upnp_is_active(const nat_upnp_context_t *ctx)
Definition upnp.c:291
void nat_upnp_close(nat_upnp_context_t **ctx)
Definition upnp.c:276
asciichat_error_t nat_upnp_open(uint16_t internal_port, const char *description, nat_upnp_context_t **ctx)
Definition upnp.c:236
asciichat_error_t nat_upnp_get_address(const nat_upnp_context_t *ctx, char *addr, size_t addr_len)
Definition upnp.c:310