ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
upnp.h File Reference

UPnP/NAT-PMP port mapping for TCP and UDP connectivity. More...

Go to the source code of this file.

Data Structures

struct  nat_upnp_context
 Handle to UPnP context. More...

Macros

#define NETWORK_NAT_UPNP_EXTERNAL_IP_BUFFER_SIZE   16
#define NETWORK_NAT_UPNP_DEVICE_DESCRIPTION_BUFFER_SIZE   256
#define NETWORK_NAT_UPNP_INTERNAL_IP_BUFFER_SIZE   16
#define NETWORK_NAT_UPNP_SERVICE_TYPE_BUFFER_SIZE   128
#define NETWORK_NAT_UPNP_DESCRIPTION_BUFFER_SIZE   128

Typedefs

typedef struct nat_upnp_context nat_upnp_context_t
 Handle to UPnP context.

Enumerations

enum  nat_upnp_protocol_t { NAT_UPNP_TCP , NAT_UPNP_UDP }

Functions

asciichat_error_t nat_upnp_open (uint16_t internal_port, const char *description, nat_upnp_context_t **ctx)
 Discover and open port via UPnP.
asciichat_error_t nat_upnp_open_protocol (uint16_t internal_port, const char *description, nat_upnp_protocol_t protocol, nat_upnp_context_t **ctx)
 Open an owned TCP or UDP mapping; close with nat_upnp_close().
void nat_upnp_close (nat_upnp_context_t **ctx)
 Close port mapping and clean up.
bool nat_upnp_is_active (const nat_upnp_context_t *ctx)
 Check if port mapping is still active.
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.
asciichat_error_t nat_upnp_get_endpoint (const nat_upnp_context_t *ctx, char *ip, size_t ip_len, uint16_t *port)
 Copy the unexpired mapped IP and external port for discovery advertisement.
asciichat_error_t nat_upnp_refresh (nat_upnp_context_t *ctx)
 Refresh port mapping (e.g., for long-running servers).
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.

Detailed Description

UPnP/NAT-PMP port mapping for TCP and UDP connectivity.

Requests router mappings for TCP listeners and UDP transports. Mapping success does not prove Internet reachability (for example, behind another NAT). The owner must refresh the lease and close the mapping on shutdown.

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

Definition in file upnp.h.

Macro Definition Documentation

◆ NETWORK_NAT_UPNP_DESCRIPTION_BUFFER_SIZE

#define NETWORK_NAT_UPNP_DESCRIPTION_BUFFER_SIZE   128

Definition at line 21 of file upnp.h.

◆ NETWORK_NAT_UPNP_DEVICE_DESCRIPTION_BUFFER_SIZE

#define NETWORK_NAT_UPNP_DEVICE_DESCRIPTION_BUFFER_SIZE   256

Definition at line 18 of file upnp.h.

◆ NETWORK_NAT_UPNP_EXTERNAL_IP_BUFFER_SIZE

#define NETWORK_NAT_UPNP_EXTERNAL_IP_BUFFER_SIZE   16

Definition at line 17 of file upnp.h.

◆ NETWORK_NAT_UPNP_INTERNAL_IP_BUFFER_SIZE

#define NETWORK_NAT_UPNP_INTERNAL_IP_BUFFER_SIZE   16

Definition at line 19 of file upnp.h.

◆ NETWORK_NAT_UPNP_SERVICE_TYPE_BUFFER_SIZE

#define NETWORK_NAT_UPNP_SERVICE_TYPE_BUFFER_SIZE   128

Definition at line 20 of file upnp.h.

Typedef Documentation

◆ nat_upnp_context_t

Handle to UPnP context.

Enumeration Type Documentation

◆ nat_upnp_protocol_t

Enumerator
NAT_UPNP_TCP 
NAT_UPNP_UDP 

Definition at line 27 of file upnp.h.

nat_upnp_protocol_t
Definition upnp.h:27
@ NAT_UPNP_TCP
Definition upnp.h:27
@ NAT_UPNP_UDP
Definition upnp.h:27

Function Documentation

◆ nat_upnp_close()

void nat_upnp_close ( nat_upnp_context_t ** ctx)

Close port mapping and clean up.

Removes the port mapping from the gateway and frees resources. Safe to call with NULL.

Parameters
ctxContext handle (will be set to NULL on return)

Definition at line 251 of file upnp.c.

251 {
252 ASCII_OTEL_SCOPE(NAT_CLOSE);
253
254 if (!ctx || !*ctx) {
255 return;
256 }
257 if ((*ctx)->is_mapped) {
259#if defined(__APPLE__) && defined(HAVE_MINIUPNPC)
260 if ((*ctx)->is_natpmp) {
261 error = natpmp_map(*ctx, 0, false);
262 } else
263#endif
264 {
265#ifdef HAVE_MINIUPNPC
266 char port[PORT_STRING_BUFFER_SIZE];
267 safe_snprintf(port, sizeof(port), "%u", (*ctx)->mapped_port);
268 int result =
269 UPNP_DeletePortMapping((*ctx)->control_url, (*ctx)->service_type, port, mapping_protocol(*ctx), NULL);
270 // Fios gateways require an explicit wildcard when deleting an unrestricted mapping.
271 if (result == 402) {
272 result = UPNP_DeletePortMapping((*ctx)->control_url, (*ctx)->service_type, port, mapping_protocol(*ctx), "*");
273 }
274 if (result != UPNPCOMMAND_SUCCESS && result != 714) {
275 log_debug("UPnP: deletion failed: %s (%d)", strupnperror(result), result);
276 }
277 // An already expired mapping needs no further cleanup.
278 error = (result == UPNPCOMMAND_SUCCESS || result == 714) ? ASCIICHAT_OK : ERROR_NETWORK;
279#endif
280 }
281 if (error != ASCIICHAT_OK) {
282 log_warn("NAT: could not remove mapping for port %u; its lease will expire", (*ctx)->mapped_port);
283 } else {
284 log_info("NAT: removed mapping for port %u", (*ctx)->mapped_port);
285 }
286 }
287 SAFE_FREE((*ctx)->control_url);
288 SAFE_FREE(*ctx);
289 *ctx = NULL;
290}
#define SAFE_FREE(ptr)
Definition common.h:377
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_NETWORK
Definition error_codes.h:72
@ ASCIICHAT_OK
Definition error_codes.h:46
#define PORT_STRING_BUFFER_SIZE
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72

References ASCII_OTEL_SCOPE, ASCIICHAT_OK, ERROR_NETWORK, log_debug, log_info, log_warn, PORT_STRING_BUFFER_SIZE, SAFE_FREE, and safe_snprintf().

Referenced by mcp_http_run(), and session_server_like_run().

◆ nat_upnp_get_address()

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.

Useful for ACDS registration where we need to advertise the public endpoint for P2P connections.

Parameters
ctxContext handle
[out]addrBuffer to write "IP:port" format (must be at least 22 bytes)
addr_lenSize of addr buffer
Returns
ASCIICHAT_OK if successfully written
ERROR_INVALID_PARAM if addr is too small

Definition at line 343 of file upnp.c.

343 {
344 if (!ctx || !addr || addr_len < 22) {
345 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: invalid arguments for get_address");
346 }
347 if (!nat_upnp_is_active(ctx)) {
348 return SET_ERRNO(ERROR_NETWORK, "NAT: no unexpired mapping to advertise");
349 }
350 int written = safe_snprintf(addr, addr_len, "%s:%u", ctx->external_ip, ctx->mapped_port);
351 if (written < 0 || (size_t)written >= addr_len) {
352 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: address buffer too small");
353 }
354 return ASCIICHAT_OK;
355}
#define SET_ERRNO(code,...)
@ ERROR_INVALID_PARAM
uint16_t mapped_port
External port that was mapped (may differ from internal).
Definition upnp.h:32
char external_ip[16]
Detected external/public IP (e.g., "203.0.113.42").
Definition upnp.h:31
bool nat_upnp_is_active(const nat_upnp_context_t *ctx)
Check if port mapping is still active.
Definition upnp.c:292

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_NETWORK, nat_upnp_context::external_ip, nat_upnp_context::mapped_port, nat_upnp_is_active(), safe_snprintf(), and SET_ERRNO.

Referenced by session_server_like_run().

◆ nat_upnp_get_endpoint()

asciichat_error_t nat_upnp_get_endpoint ( const nat_upnp_context_t * ctx,
char * ip,
size_t ip_len,
uint16_t * port )

Copy the unexpired mapped IP and external port for discovery advertisement.

Definition at line 304 of file upnp.c.

304 {
305 if (!ctx || !ip || !port || ip_len < sizeof(ctx->external_ip)) {
306 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: invalid endpoint output");
307 }
308 if (!nat_upnp_is_active(ctx)) {
309 return SET_ERRNO(ERROR_NETWORK, "NAT: no unexpired mapping to advertise");
310 }
311 SAFE_STRNCPY(ip, ctx->external_ip, ip_len);
312 *port = ctx->mapped_port;
313 return ASCIICHAT_OK;
314}
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_NETWORK, nat_upnp_context::external_ip, nat_upnp_context::mapped_port, nat_upnp_is_active(), SAFE_STRNCPY, and SET_ERRNO.

◆ nat_upnp_is_active()

bool nat_upnp_is_active ( const nat_upnp_context_t * ctx)

Check if port mapping is still active.

Checks the locally tracked lease; does not probe Internet reachability.

Parameters
ctxContext handle
Returns
true if the locally tracked mapping lease has not expired

Definition at line 292 of file upnp.c.

292 {
293 return ctx && ctx->is_mapped && ctx->external_ip[0] && time_get_ns() < ctx->expires_at_ns;
294}
bool is_mapped
true if port mapping is currently active
Definition upnp.h:45
uint64_t expires_at_ns
Monotonic lease expiry.
Definition upnp.h:43

References nat_upnp_context::expires_at_ns, nat_upnp_context::external_ip, and nat_upnp_context::is_mapped.

Referenced by nat_upnp_get_address(), nat_upnp_get_endpoint(), nat_upnp_refresh(), and session_server_like_run().

◆ nat_upnp_matches_bind_address()

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 at line 296 of file upnp.c.

296 {
297 if (!ctx) {
298 return false;
299 }
300 return !bind_address || !bind_address[0] || strcmp(bind_address, "0.0.0.0") == 0 ||
301 (ctx->internal_ip[0] && strcmp(bind_address, ctx->internal_ip) == 0);
302}
char internal_ip[16]
LAN address used by the gateway to reach us.
Definition upnp.h:37

References nat_upnp_context::internal_ip.

Referenced by session_server_like_run().

◆ nat_upnp_open()

asciichat_error_t nat_upnp_open ( uint16_t internal_port,
const char * description,
nat_upnp_context_t ** ctx )

Discover and open port via UPnP.

Attempts to find an UPnP-enabled gateway and request port mapping. On success, fills in external_ip and mapped_port.

Parameters
internal_portLocal TCP port to map (e.g., 27224 for ACDS)
descriptionDescription for port mapping (e.g., "ascii-chat Server")
[out]ctxContext handle (must be freed with nat_upnp_close())
Returns
ASCIICHAT_OK if port was successfully mapped
ERROR_NETWORK_* if discovery or mapping failed (does not determine listener reachability)

Example:

nat_upnp_context_t *ctx = NULL;
asciichat_error_t result = nat_upnp_open(27224, "ascii-chat Server", &ctx);
if (result == ASCIICHAT_OK && ctx) {
printf("Public IP: %s\n", ctx->external_ip);
// Advertise ctx->external_ip:ctx->mapped_port to ACDS
}
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
struct nat_upnp_context nat_upnp_context_t
Handle to UPnP context.

Definition at line 203 of file upnp.c.

203 {
204 return nat_upnp_open_protocol(internal_port, description, NAT_UPNP_TCP, ctx);
205}
asciichat_error_t nat_upnp_open_protocol(uint16_t internal_port, const char *description, nat_upnp_protocol_t protocol, nat_upnp_context_t **ctx)
Open an owned TCP or UDP mapping; close with nat_upnp_close().
Definition upnp.c:207

References nat_upnp_open_protocol(), and NAT_UPNP_TCP.

Referenced by mcp_http_run(), and session_server_like_run().

◆ nat_upnp_open_protocol()

asciichat_error_t nat_upnp_open_protocol ( uint16_t internal_port,
const char * description,
nat_upnp_protocol_t protocol,
nat_upnp_context_t ** ctx )

Open an owned TCP or UDP mapping; close with nat_upnp_close().

Definition at line 207 of file upnp.c.

208 {
209 ASCII_OTEL_SCOPE(NAT_MAP);
210 ASCII_OTEL_ATTR_STRING("network.transport", protocol == NAT_UPNP_UDP ? "udp" : "tcp");
211 ASCII_OTEL_ATTR_INT("network.local.port", internal_port);
212 if (!ctx) {
213 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: missing mapping output");
214 }
215 *ctx = NULL;
216 if (!internal_port || !description || (protocol != NAT_UPNP_TCP && protocol != NAT_UPNP_UDP)) {
217 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: a listening port and description are required");
218 }
220 if (!*ctx) {
221 return SET_ERRNO(ERROR_MEMORY, "NAT: could not allocate mapping context");
222 }
223 (*ctx)->protocol = protocol;
224 (*ctx)->internal_port = internal_port;
225 (*ctx)->mapped_port = internal_port;
226 SAFE_STRNCPY((*ctx)->description, description, sizeof((*ctx)->description));
227 log_info("UPnP: discovering a gateway for %s port %u", protocol == NAT_UPNP_UDP ? "UDP" : "TCP", internal_port);
229 asciichat_error_t result = upnp_try_map_port(*ctx);
230#if defined(__APPLE__) && defined(HAVE_MINIUPNPC)
231 if (result != ASCIICHAT_OK) {
232 log_debug("UPnP unavailable; trying NAT-PMP");
233 result = natpmp_map(*ctx, MAPPING_LEASE_SECONDS, true);
234 }
235#endif
236 if (result == ASCIICHAT_OK) {
237 ascii_otel_metric(ASCII_OTEL_METRIC_NAT_MAP_SUCCESS, 1);
239 log_info("NAT: mapping created for %s:%u (external reachability unverified)", (*ctx)->external_ip,
240 (*ctx)->mapped_port);
241 return ASCIICHAT_OK;
242 }
243 ascii_otel_metric(ASCII_OTEL_METRIC_NAT_MAP_FAILURE, 1);
244 LOG_ERRNO_IF_SET("Automatic router mapping unavailable");
245 SAFE_FREE((*ctx)->control_url);
246 SAFE_FREE(*ctx);
247 *ctx = NULL;
248 return SET_ERRNO(ERROR_NETWORK, "NAT: automatic mapping unavailable; direct connectivity may still work");
249}
void errno_scope_end(asciichat_errno_scope_t scope, asciichat_errno_outcome_t outcome)
asciichat_errno_scope_t errno_scope_begin(void)
@ ASCIICHAT_ERRNO_HANDLED
#define LOG_ERRNO_IF_SET(message)
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
@ ERROR_MEMORY
Definition error_codes.h:51
void ascii_otel_metric(ascii_otel_metric_t metric, double value)
Definition otel.c:161
#define ASCII_OTEL_ATTR_INT(key, value)
Definition otel.h:67
#define ASCII_OTEL_ATTR_STRING(key, value)
Definition otel.h:66
When code needs to work with asciichat errno snapshot, it keeps total, copied here instead of scatter...
#define MAPPING_LEASE_SECONDS
Definition upnp.c:32

References ASCII_OTEL_ATTR_INT, ASCII_OTEL_ATTR_STRING, ascii_otel_metric(), ASCII_OTEL_SCOPE, ASCIICHAT_ERRNO_HANDLED, ASCIICHAT_OK, errno_scope_begin(), errno_scope_end(), ERROR_INVALID_PARAM, ERROR_MEMORY, ERROR_NETWORK, log_debug, LOG_ERRNO_IF_SET, log_info, MAPPING_LEASE_SECONDS, NAT_UPNP_TCP, NAT_UPNP_UDP, SAFE_CALLOC, SAFE_FREE, SAFE_STRNCPY, and SET_ERRNO.

Referenced by nat_upnp_open().

◆ nat_upnp_refresh()

asciichat_error_t nat_upnp_refresh ( nat_upnp_context_t * ctx)

Refresh port mapping (e.g., for long-running servers).

Call from one owning thread when refresh_at_ns is reached. Renewal is scheduled halfway through the lease; failures schedule a bounded retry.

Parameters
ctxContext handle
Returns
ASCIICHAT_OK if refresh succeeded

Definition at line 316 of file upnp.c.

316 {
317 ASCII_OTEL_SCOPE(NAT_REFRESH);
318
319 if (!ctx || !ctx->is_mapped) {
320 return SET_ERRNO(ERROR_INVALID_PARAM, "NAT: cannot refresh without a mapping");
321 }
323#if defined(__APPLE__) && defined(HAVE_MINIUPNPC)
324 if (ctx->is_natpmp) {
325 result = natpmp_map(ctx, MAPPING_LEASE_SECONDS, false);
326 } else
327#endif
328 {
329#ifdef HAVE_MINIUPNPC
330 result = upnp_map(ctx);
331#endif
332 }
333 if (result != ASCIICHAT_OK) {
334 ctx->refresh_at_ns = time_get_ns() + MAPPING_RETRY_NS;
335 log_warn("NAT: mapping renewal failed; %s; retrying in 30 seconds",
336 nat_upnp_is_active(ctx) ? "previous lease has not expired" : "mapping lease expired");
337 return result;
338 }
339 log_info("NAT: mapping renewed for %s:%u", ctx->external_ip, ctx->mapped_port);
340 return ASCIICHAT_OK;
341}
bool is_natpmp
true if NAT-PMP was used, false if UPnP
Definition upnp.h:36
uint64_t refresh_at_ns
Next renewal or retry deadline.
Definition upnp.h:44
#define MAPPING_RETRY_NS
Definition upnp.c:33

References ASCII_OTEL_SCOPE, ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_NETWORK, nat_upnp_context::external_ip, nat_upnp_context::is_mapped, nat_upnp_context::is_natpmp, log_info, log_warn, nat_upnp_context::mapped_port, MAPPING_LEASE_SECONDS, MAPPING_RETRY_NS, nat_upnp_is_active(), nat_upnp_context::refresh_at_ns, and SET_ERRNO.

Referenced by mcp_http_run().