diff --git a/CLAUDE.md b/CLAUDE.md
index 8d93986..426b2c0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -65,6 +65,7 @@ zcatp2p/
├── discovery.zig # Local + global discovery
├── stun.zig # STUN client
├── relay.zig # Relay protocol
+ ├── nat.zig # UPnP IGD / NAT-PMP port mapping
└── connection.zig # Connection management
```
@@ -86,8 +87,8 @@ zcatp2p/
- [x] Implementación relay client
- [x] Tests unitarios (36 tests)
- [x] Discovery global (HTTPS API)
+- [x] UPnP/NAT-PMP port mapping
- [ ] Integración completa de red
-- [ ] UPnP/NAT-PMP port mapping
## Comandos
diff --git a/src/main.zig b/src/main.zig
index fd506c8..6de93e4 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -16,6 +16,7 @@ pub const tls = @import("tls.zig");
pub const stun = @import("stun.zig");
pub const relay = @import("relay.zig");
pub const http = @import("http.zig");
+pub const nat = @import("nat.zig");
// Re-exports principales
pub const DeviceId = identity.DeviceId;
diff --git a/src/nat.zig b/src/nat.zig
new file mode 100644
index 0000000..7383a5e
--- /dev/null
+++ b/src/nat.zig
@@ -0,0 +1,932 @@
+//! Módulo NAT - UPnP IGD y NAT-PMP port mapping
+//!
+//! Implementa apertura automática de puertos en routers NAT.
+//! Soporta UPnP IGD (Internet Gateway Device) y NAT-PMP/PCP.
+
+const std = @import("std");
+const http = @import("http.zig");
+
+// =============================================================================
+// Tipos comunes
+// =============================================================================
+
+/// Protocolo de transporte
+pub const Protocol = enum {
+ TCP,
+ UDP,
+
+ pub fn toString(self: Protocol) []const u8 {
+ return switch (self) {
+ .TCP => "TCP",
+ .UDP => "UDP",
+ };
+ }
+};
+
+/// Mapeo de puerto
+pub const PortMapping = struct {
+ internal_port: u16,
+ external_port: u16,
+ protocol: Protocol,
+ description: []const u8,
+ lifetime: u32, // segundos, 0 = permanente
+ external_ip: ?[4]u8,
+};
+
+/// Resultado de operación NAT
+pub const NatResult = union(enum) {
+ success: PortMapping,
+ gateway_not_found,
+ mapping_failed: []const u8,
+ not_supported,
+ timeout,
+};
+
+/// Tipo de gateway NAT detectado
+pub const GatewayType = enum {
+ unknown,
+ upnp,
+ nat_pmp,
+ pcp,
+ none,
+};
+
+// =============================================================================
+// NAT-PMP Client (RFC 6886)
+// =============================================================================
+
+/// Puerto NAT-PMP del gateway
+pub const NATPMP_PORT: u16 = 5351;
+
+/// Opcodes NAT-PMP
+const NatPmpOpcode = enum(u8) {
+ external_address = 0,
+ map_udp = 1,
+ map_tcp = 2,
+};
+
+/// Códigos de resultado NAT-PMP
+const NatPmpResult = enum(u16) {
+ success = 0,
+ unsupported_version = 1,
+ not_authorized = 2,
+ network_failure = 3,
+ out_of_resources = 4,
+ unsupported_opcode = 5,
+ _,
+};
+
+/// Cliente NAT-PMP
+pub const NatPmpClient = struct {
+ allocator: std.mem.Allocator,
+ socket: ?std.posix.socket_t,
+ gateway_ip: [4]u8,
+ external_ip: ?[4]u8,
+ epoch: u32,
+
+ pub fn init(allocator: std.mem.Allocator) NatPmpClient {
+ return .{
+ .allocator = allocator,
+ .socket = null,
+ .gateway_ip = .{ 0, 0, 0, 0 },
+ .external_ip = null,
+ .epoch = 0,
+ };
+ }
+
+ pub fn deinit(self: *NatPmpClient) void {
+ if (self.socket) |sock| {
+ std.posix.close(sock);
+ }
+ }
+
+ /// Detecta el gateway por defecto
+ pub fn detectGateway(self: *NatPmpClient) !void {
+ // En la mayoría de redes, el gateway es x.x.x.1
+ // Método más robusto: leer de /proc/net/route en Linux
+ const gateway = try self.readDefaultGateway();
+ self.gateway_ip = gateway;
+ }
+
+ fn readDefaultGateway(self: *NatPmpClient) ![4]u8 {
+ _ = self;
+
+ // Intentar leer de /proc/net/route (Linux)
+ const file = std.fs.openFileAbsolute("/proc/net/route", .{}) catch {
+ // Fallback: asumir 192.168.1.1
+ return .{ 192, 168, 1, 1 };
+ };
+ defer file.close();
+
+ var buf: [4096]u8 = undefined;
+ const bytes_read = file.readAll(&buf) catch return .{ 192, 168, 1, 1 };
+
+ // Parsear tabla de rutas
+ var lines = std.mem.splitSequence(u8, buf[0..bytes_read], "\n");
+ _ = lines.next(); // Skip header
+
+ while (lines.next()) |line| {
+ var fields = std.mem.splitSequence(u8, line, "\t");
+ _ = fields.next(); // Interface
+ const dest = fields.next() orelse continue;
+ const gateway_hex = fields.next() orelse continue;
+
+ // Buscar ruta por defecto (destino 00000000)
+ if (std.mem.eql(u8, dest, "00000000")) {
+ // Gateway está en formato hex little-endian
+ const gw = std.fmt.parseInt(u32, gateway_hex, 16) catch continue;
+ return .{
+ @truncate(gw),
+ @truncate(gw >> 8),
+ @truncate(gw >> 16),
+ @truncate(gw >> 24),
+ };
+ }
+ }
+
+ return .{ 192, 168, 1, 1 };
+ }
+
+ /// Crea el socket UDP
+ pub fn createSocket(self: *NatPmpClient) !void {
+ if (self.socket != null) return;
+
+ self.socket = try std.posix.socket(
+ std.posix.AF.INET,
+ std.posix.SOCK.DGRAM,
+ 0,
+ );
+
+ // Timeout de 250ms (NAT-PMP spec)
+ const tv = std.posix.timeval{
+ .sec = 0,
+ .usec = 250000,
+ };
+ try std.posix.setsockopt(
+ self.socket.?,
+ std.posix.SOL.SOCKET,
+ std.posix.SO.RCVTIMEO,
+ std.mem.asBytes(&tv),
+ );
+ }
+
+ /// Obtiene la dirección IP externa
+ pub fn getExternalAddress(self: *NatPmpClient) !?[4]u8 {
+ try self.createSocket();
+
+ // Construir request
+ var request: [2]u8 = .{ 0, @intFromEnum(NatPmpOpcode.external_address) };
+
+ const gateway_addr = std.net.Address.initIp4(self.gateway_ip, NATPMP_PORT);
+
+ // Enviar con retries exponenciales
+ var timeout_ms: u32 = 250;
+ for (0..9) |_| {
+ _ = std.posix.sendto(
+ self.socket.?,
+ &request,
+ 0,
+ &gateway_addr.any,
+ gateway_addr.getOsSockLen(),
+ ) catch continue;
+
+ // Recibir respuesta
+ var response: [12]u8 = undefined;
+ const len = std.posix.recvfrom(self.socket.?, &response, 0, null, null) catch {
+ timeout_ms *= 2;
+ continue;
+ };
+
+ if (len >= 12) {
+ // Verificar versión y opcode
+ if (response[0] != 0) continue; // Versión incorrecta
+ if (response[1] != 128) continue; // No es respuesta
+
+ const result: NatPmpResult = @enumFromInt(std.mem.readInt(u16, response[2..4], .big));
+ if (result != .success) continue;
+
+ self.epoch = std.mem.readInt(u32, response[4..8], .big);
+ self.external_ip = response[8..12].*;
+
+ return self.external_ip;
+ }
+ }
+
+ return null;
+ }
+
+ /// Mapea un puerto
+ pub fn mapPort(
+ self: *NatPmpClient,
+ internal_port: u16,
+ external_port: u16,
+ protocol: Protocol,
+ lifetime: u32,
+ ) !?PortMapping {
+ try self.createSocket();
+
+ // Construir request
+ var request: [12]u8 = undefined;
+ request[0] = 0; // Versión
+ request[1] = switch (protocol) {
+ .UDP => @intFromEnum(NatPmpOpcode.map_udp),
+ .TCP => @intFromEnum(NatPmpOpcode.map_tcp),
+ };
+ request[2] = 0; // Reserved
+ request[3] = 0;
+ std.mem.writeInt(u16, request[4..6], internal_port, .big);
+ std.mem.writeInt(u16, request[6..8], external_port, .big);
+ std.mem.writeInt(u32, request[8..12], lifetime, .big);
+
+ const gateway_addr = std.net.Address.initIp4(self.gateway_ip, NATPMP_PORT);
+
+ // Enviar con retries
+ var timeout_ms: u32 = 250;
+ for (0..9) |_| {
+ _ = std.posix.sendto(
+ self.socket.?,
+ &request,
+ 0,
+ &gateway_addr.any,
+ gateway_addr.getOsSockLen(),
+ ) catch continue;
+
+ var response: [16]u8 = undefined;
+ const len = std.posix.recvfrom(self.socket.?, &response, 0, null, null) catch {
+ timeout_ms *= 2;
+ continue;
+ };
+
+ if (len >= 16) {
+ if (response[0] != 0) continue;
+ if (response[1] != 128 + request[1]) continue;
+
+ const result: NatPmpResult = @enumFromInt(std.mem.readInt(u16, response[2..4], .big));
+ if (result != .success) {
+ return null;
+ }
+
+ self.epoch = std.mem.readInt(u32, response[4..8], .big);
+ const mapped_internal = std.mem.readInt(u16, response[8..10], .big);
+ const mapped_external = std.mem.readInt(u16, response[10..12], .big);
+ const mapped_lifetime = std.mem.readInt(u32, response[12..16], .big);
+
+ return PortMapping{
+ .internal_port = mapped_internal,
+ .external_port = mapped_external,
+ .protocol = protocol,
+ .description = "NAT-PMP",
+ .lifetime = mapped_lifetime,
+ .external_ip = self.external_ip,
+ };
+ }
+ }
+
+ return null;
+ }
+
+ /// Elimina un mapeo de puerto
+ pub fn unmapPort(
+ self: *NatPmpClient,
+ internal_port: u16,
+ protocol: Protocol,
+ ) !void {
+ // Lifetime 0 elimina el mapeo
+ _ = try self.mapPort(internal_port, 0, protocol, 0);
+ }
+};
+
+// =============================================================================
+// UPnP IGD Client
+// =============================================================================
+
+/// Dirección multicast SSDP
+const SSDP_MULTICAST_ADDR: [4]u8 = .{ 239, 255, 255, 250 };
+const SSDP_PORT: u16 = 1900;
+
+/// Timeout de discovery SSDP
+const SSDP_TIMEOUT_MS: u32 = 3000;
+
+/// Dispositivo UPnP descubierto
+pub const UpnpDevice = struct {
+ location: []const u8,
+ server: []const u8,
+ usn: []const u8,
+ control_url: []const u8,
+ service_type: []const u8,
+ allocator: std.mem.Allocator,
+
+ pub fn deinit(self: *UpnpDevice) void {
+ self.allocator.free(self.location);
+ if (self.server.len > 0) self.allocator.free(self.server);
+ if (self.usn.len > 0) self.allocator.free(self.usn);
+ if (self.control_url.len > 0) self.allocator.free(self.control_url);
+ if (self.service_type.len > 0) self.allocator.free(self.service_type);
+ }
+};
+
+/// Cliente UPnP IGD
+pub const UpnpClient = struct {
+ allocator: std.mem.Allocator,
+ socket: ?std.posix.socket_t,
+ device: ?UpnpDevice,
+ local_ip: ?[4]u8,
+
+ /// Tipos de servicio IGD
+ const SERVICE_TYPES: []const []const u8 = &.{
+ "urn:schemas-upnp-org:service:WANIPConnection:1",
+ "urn:schemas-upnp-org:service:WANIPConnection:2",
+ "urn:schemas-upnp-org:service:WANPPPConnection:1",
+ };
+
+ pub fn init(allocator: std.mem.Allocator) UpnpClient {
+ return .{
+ .allocator = allocator,
+ .socket = null,
+ .device = null,
+ .local_ip = null,
+ };
+ }
+
+ pub fn deinit(self: *UpnpClient) void {
+ if (self.socket) |sock| {
+ std.posix.close(sock);
+ }
+ if (self.device) |*dev| {
+ dev.deinit();
+ }
+ }
+
+ /// Descubre dispositivos IGD mediante SSDP
+ pub fn discover(self: *UpnpClient) !bool {
+ // Crear socket UDP
+ self.socket = try std.posix.socket(
+ std.posix.AF.INET,
+ std.posix.SOCK.DGRAM,
+ 0,
+ );
+ errdefer {
+ if (self.socket) |sock| std.posix.close(sock);
+ self.socket = null;
+ }
+
+ // Timeout
+ const tv = std.posix.timeval{
+ .sec = @intCast(SSDP_TIMEOUT_MS / 1000),
+ .usec = @intCast((SSDP_TIMEOUT_MS % 1000) * 1000),
+ };
+ try std.posix.setsockopt(
+ self.socket.?,
+ std.posix.SOL.SOCKET,
+ std.posix.SO.RCVTIMEO,
+ std.mem.asBytes(&tv),
+ );
+
+ // Enviar M-SEARCH para cada tipo de servicio
+ for (SERVICE_TYPES) |service_type| {
+ if (try self.sendMSearch(service_type)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ fn sendMSearch(self: *UpnpClient, service_type: []const u8) !bool {
+ // Construir mensaje M-SEARCH
+ var request_buf: [512]u8 = undefined;
+ const request = std.fmt.bufPrint(&request_buf,
+ \\M-SEARCH * HTTP/1.1
+ \\HOST: 239.255.255.250:1900
+ \\MAN: "ssdp:discover"
+ \\MX: 3
+ \\ST: {s}
+ \\
+ \\
+ , .{service_type}) catch return false;
+
+ const multicast_addr = std.net.Address.initIp4(SSDP_MULTICAST_ADDR, SSDP_PORT);
+
+ // Enviar
+ _ = try std.posix.sendto(
+ self.socket.?,
+ request,
+ 0,
+ &multicast_addr.any,
+ multicast_addr.getOsSockLen(),
+ );
+
+ // Recibir respuestas
+ var response_buf: [2048]u8 = undefined;
+ while (true) {
+ var src_addr: std.posix.sockaddr = undefined;
+ var src_len: std.posix.socklen_t = @sizeOf(std.posix.sockaddr);
+
+ const len = std.posix.recvfrom(
+ self.socket.?,
+ &response_buf,
+ 0,
+ &src_addr,
+ &src_len,
+ ) catch break;
+
+ if (len == 0) break;
+
+ // Parsear respuesta SSDP
+ if (try self.parseSsdpResponse(response_buf[0..len], service_type)) {
+ // Obtener IP local desde la respuesta
+ if (src_addr.family == std.posix.AF.INET) {
+ const addr4: *std.posix.sockaddr.in = @ptrCast(&src_addr);
+ _ = addr4;
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ fn parseSsdpResponse(self: *UpnpClient, response: []const u8, service_type: []const u8) !bool {
+ // Verificar que es respuesta HTTP 200
+ if (!std.mem.startsWith(u8, response, "HTTP/1.1 200")) {
+ return false;
+ }
+
+ var location: ?[]const u8 = null;
+ var server: ?[]const u8 = null;
+ var usn: ?[]const u8 = null;
+
+ // Parsear headers
+ var lines = std.mem.splitSequence(u8, response, "\r\n");
+ while (lines.next()) |line| {
+ if (std.ascii.startsWithIgnoreCase(line, "LOCATION:")) {
+ location = std.mem.trim(u8, line[9..], " \t");
+ } else if (std.ascii.startsWithIgnoreCase(line, "SERVER:")) {
+ server = std.mem.trim(u8, line[7..], " \t");
+ } else if (std.ascii.startsWithIgnoreCase(line, "USN:")) {
+ usn = std.mem.trim(u8, line[4..], " \t");
+ }
+ }
+
+ if (location == null) return false;
+
+ // Obtener descripción del dispositivo
+ const control_url = try self.getControlUrl(location.?, service_type);
+ if (control_url == null) return false;
+
+ self.device = .{
+ .location = try self.allocator.dupe(u8, location.?),
+ .server = if (server) |s| try self.allocator.dupe(u8, s) else "",
+ .usn = if (usn) |u| try self.allocator.dupe(u8, u) else "",
+ .control_url = control_url.?,
+ .service_type = try self.allocator.dupe(u8, service_type),
+ .allocator = self.allocator,
+ };
+
+ return true;
+ }
+
+ fn getControlUrl(self: *UpnpClient, location: []const u8, service_type: []const u8) !?[]const u8 {
+ // Hacer GET al location para obtener XML de descripción
+ var client = http.HttpClient.init(self.allocator);
+ defer client.deinit();
+
+ var response = client.get(location, null) catch return null;
+ defer response.deinit();
+
+ if (!response.status_code.isSuccess()) return null;
+
+ // Parsear XML para encontrar controlURL del servicio
+ return self.parseDeviceDescription(response.body, service_type, location);
+ }
+
+ fn parseDeviceDescription(self: *UpnpClient, xml: []const u8, service_type: []const u8, base_url: []const u8) !?[]const u8 {
+ // Buscar el servicio en el XML
+ // Formato: ... ... ...
+
+ var pos: usize = 0;
+ while (pos < xml.len) {
+ // Buscar serviceType
+ const st_start = std.mem.indexOfPos(u8, xml, pos, "") orelse break;
+ const st_end = std.mem.indexOfPos(u8, xml, st_start, "") orelse break;
+ const found_type = xml[st_start + 13 .. st_end];
+
+ if (std.mem.indexOf(u8, found_type, service_type) != null) {
+ // Encontrado - buscar controlURL
+ const ctrl_start = std.mem.indexOfPos(u8, xml, st_end, "") orelse break;
+ const ctrl_end = std.mem.indexOfPos(u8, xml, ctrl_start, "") orelse break;
+ const control_path = xml[ctrl_start + 12 .. ctrl_end];
+
+ // Construir URL completa
+ return try self.buildControlUrl(base_url, control_path);
+ }
+
+ pos = st_end;
+ }
+
+ return null;
+ }
+
+ fn buildControlUrl(self: *UpnpClient, base_url: []const u8, control_path: []const u8) ![]const u8 {
+ // Si control_path es absoluto, usarlo directamente
+ if (std.mem.startsWith(u8, control_path, "http://") or
+ std.mem.startsWith(u8, control_path, "https://"))
+ {
+ return try self.allocator.dupe(u8, control_path);
+ }
+
+ // Extraer base del location URL
+ const url = try http.Url.parse(base_url);
+
+ var buf: [512]u8 = undefined;
+ const full_url = std.fmt.bufPrint(&buf, "{s}://{s}:{d}{s}", .{
+ url.scheme,
+ url.host,
+ url.port,
+ control_path,
+ }) catch return error.UrlTooLong;
+
+ return try self.allocator.dupe(u8, full_url);
+ }
+
+ /// Obtiene la dirección IP externa
+ pub fn getExternalIPAddress(self: *UpnpClient) !?[]const u8 {
+ const device = self.device orelse return null;
+
+ const soap_body =
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ ;
+
+ var body_buf: [1024]u8 = undefined;
+ const body = std.fmt.bufPrint(&body_buf, soap_body, .{device.service_type}) catch return null;
+
+ const response = try self.sendSoapRequest("GetExternalIPAddress", body);
+ defer self.allocator.free(response);
+
+ // Parsear respuesta para extraer NewExternalIPAddress
+ if (std.mem.indexOf(u8, response, "")) |start| {
+ const ip_start = start + 22;
+ if (std.mem.indexOfPos(u8, response, ip_start, "")) |end| {
+ return try self.allocator.dupe(u8, response[ip_start..end]);
+ }
+ }
+
+ return null;
+ }
+
+ /// Añade un mapeo de puerto
+ pub fn addPortMapping(
+ self: *UpnpClient,
+ external_port: u16,
+ internal_port: u16,
+ protocol: Protocol,
+ description: []const u8,
+ lease_duration: u32,
+ ) !bool {
+ const device = self.device orelse return false;
+
+ // Obtener IP local
+ const local_ip = try self.getLocalIP();
+
+ const soap_template =
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\{d}
+ \\{s}
+ \\{d}
+ \\{s}
+ \\1
+ \\{s}
+ \\{d}
+ \\
+ \\
+ \\
+ ;
+
+ var body_buf: [2048]u8 = undefined;
+ const body = std.fmt.bufPrint(&body_buf, soap_template, .{
+ device.service_type,
+ external_port,
+ protocol.toString(),
+ internal_port,
+ local_ip,
+ description,
+ lease_duration,
+ }) catch return false;
+
+ const response = self.sendSoapRequest("AddPortMapping", body) catch return false;
+ defer self.allocator.free(response);
+
+ // Verificar éxito
+ return std.mem.indexOf(u8, response, "AddPortMappingResponse") != null;
+ }
+
+ /// Elimina un mapeo de puerto
+ pub fn deletePortMapping(
+ self: *UpnpClient,
+ external_port: u16,
+ protocol: Protocol,
+ ) !bool {
+ const device = self.device orelse return false;
+
+ const soap_template =
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\{d}
+ \\{s}
+ \\
+ \\
+ \\
+ ;
+
+ var body_buf: [1024]u8 = undefined;
+ const body = std.fmt.bufPrint(&body_buf, soap_template, .{
+ device.service_type,
+ external_port,
+ protocol.toString(),
+ }) catch return false;
+
+ const response = self.sendSoapRequest("DeletePortMapping", body) catch return false;
+ defer self.allocator.free(response);
+
+ return std.mem.indexOf(u8, response, "DeletePortMappingResponse") != null;
+ }
+
+ fn sendSoapRequest(self: *UpnpClient, action: []const u8, body: []const u8) ![]const u8 {
+ const device = self.device orelse return error.NoDevice;
+
+ var client = http.HttpClient.init(self.allocator);
+ defer client.deinit();
+
+ // Headers SOAP
+ var soap_action_buf: [256]u8 = undefined;
+ const soap_action = std.fmt.bufPrint(&soap_action_buf, "\"{s}#{s}\"", .{
+ device.service_type,
+ action,
+ }) catch return error.BufferTooSmall;
+
+ const headers = [_]http.Header{
+ .{ .name = "Content-Type", .value = "text/xml; charset=\"utf-8\"" },
+ .{ .name = "SOAPAction", .value = soap_action },
+ };
+
+ var response = try client.post(device.control_url, &headers, body);
+ defer response.deinit();
+
+ return try self.allocator.dupe(u8, response.body);
+ }
+
+ fn getLocalIP(self: *UpnpClient) ![]const u8 {
+ if (self.local_ip) |ip| {
+ var buf: [16]u8 = undefined;
+ return std.fmt.bufPrint(&buf, "{d}.{d}.{d}.{d}", .{ ip[0], ip[1], ip[2], ip[3] }) catch "0.0.0.0";
+ }
+
+ // Obtener IP local conectando a una dirección externa
+ const sock = try std.posix.socket(std.posix.AF.INET, std.posix.SOCK.DGRAM, 0);
+ defer std.posix.close(sock);
+
+ const addr = std.net.Address.initIp4(.{ 8, 8, 8, 8 }, 53);
+ std.posix.connect(sock, &addr.any, addr.getOsSockLen()) catch return "0.0.0.0";
+
+ var local_addr: std.posix.sockaddr = undefined;
+ var local_len: std.posix.socklen_t = @sizeOf(std.posix.sockaddr);
+ std.posix.getsockname(sock, &local_addr, &local_len) catch return "0.0.0.0";
+
+ if (local_addr.family == std.posix.AF.INET) {
+ const addr4: *std.posix.sockaddr.in = @ptrCast(&local_addr);
+ const ip = addr4.addr;
+ self.local_ip = .{
+ @truncate(ip),
+ @truncate(ip >> 8),
+ @truncate(ip >> 16),
+ @truncate(ip >> 24),
+ };
+
+ var buf: [16]u8 = undefined;
+ return std.fmt.bufPrint(&buf, "{d}.{d}.{d}.{d}", .{
+ self.local_ip.?[0],
+ self.local_ip.?[1],
+ self.local_ip.?[2],
+ self.local_ip.?[3],
+ }) catch "0.0.0.0";
+ }
+
+ return "0.0.0.0";
+ }
+};
+
+// =============================================================================
+// NAT Manager - Interfaz unificada
+// =============================================================================
+
+/// Gestor NAT unificado
+pub const NatManager = struct {
+ allocator: std.mem.Allocator,
+ upnp: UpnpClient,
+ nat_pmp: NatPmpClient,
+ gateway_type: GatewayType,
+ mappings: std.ArrayListUnmanaged(PortMapping),
+
+ pub fn init(allocator: std.mem.Allocator) NatManager {
+ return .{
+ .allocator = allocator,
+ .upnp = UpnpClient.init(allocator),
+ .nat_pmp = NatPmpClient.init(allocator),
+ .gateway_type = .unknown,
+ .mappings = .{},
+ };
+ }
+
+ pub fn deinit(self: *NatManager) void {
+ self.upnp.deinit();
+ self.nat_pmp.deinit();
+ self.mappings.deinit(self.allocator);
+ }
+
+ /// Descubre el gateway y el protocolo soportado
+ pub fn discover(self: *NatManager) !GatewayType {
+ // Intentar NAT-PMP primero (más rápido)
+ self.nat_pmp.detectGateway() catch {};
+ if (self.nat_pmp.getExternalAddress() catch null) |_| {
+ self.gateway_type = .nat_pmp;
+ return .nat_pmp;
+ }
+
+ // Intentar UPnP
+ if (self.upnp.discover() catch false) {
+ self.gateway_type = .upnp;
+ return .upnp;
+ }
+
+ self.gateway_type = .none;
+ return .none;
+ }
+
+ /// Obtiene la dirección IP externa
+ pub fn getExternalIP(self: *NatManager) !?[]const u8 {
+ switch (self.gateway_type) {
+ .nat_pmp => {
+ if (self.nat_pmp.external_ip) |ip| {
+ var buf: [16]u8 = undefined;
+ const result = std.fmt.bufPrint(&buf, "{d}.{d}.{d}.{d}", .{
+ ip[0], ip[1], ip[2], ip[3],
+ }) catch return null;
+ return try self.allocator.dupe(u8, result);
+ }
+ return null;
+ },
+ .upnp => {
+ return self.upnp.getExternalIPAddress();
+ },
+ else => return null,
+ }
+ }
+
+ /// Mapea un puerto (intenta ambos protocolos)
+ pub fn mapPort(
+ self: *NatManager,
+ internal_port: u16,
+ external_port: u16,
+ protocol: Protocol,
+ description: []const u8,
+ lifetime: u32,
+ ) !NatResult {
+ // Si no se ha descubierto, intentar
+ if (self.gateway_type == .unknown) {
+ _ = try self.discover();
+ }
+
+ switch (self.gateway_type) {
+ .nat_pmp => {
+ if (try self.nat_pmp.mapPort(internal_port, external_port, protocol, lifetime)) |mapping| {
+ try self.mappings.append(self.allocator, mapping);
+ return .{ .success = mapping };
+ }
+ return .{ .mapping_failed = "NAT-PMP mapping failed" };
+ },
+ .upnp => {
+ if (try self.upnp.addPortMapping(external_port, internal_port, protocol, description, lifetime)) {
+ const mapping = PortMapping{
+ .internal_port = internal_port,
+ .external_port = external_port,
+ .protocol = protocol,
+ .description = description,
+ .lifetime = lifetime,
+ .external_ip = null,
+ };
+ try self.mappings.append(self.allocator, mapping);
+ return .{ .success = mapping };
+ }
+ return .{ .mapping_failed = "UPnP mapping failed" };
+ },
+ .none => return .gateway_not_found,
+ else => return .not_supported,
+ }
+ }
+
+ /// Elimina un mapeo de puerto
+ pub fn unmapPort(self: *NatManager, external_port: u16, protocol: Protocol) !void {
+ switch (self.gateway_type) {
+ .nat_pmp => {
+ // NAT-PMP usa puerto interno para eliminar
+ for (self.mappings.items) |mapping| {
+ if (mapping.external_port == external_port and mapping.protocol == protocol) {
+ try self.nat_pmp.unmapPort(mapping.internal_port, protocol);
+ break;
+ }
+ }
+ },
+ .upnp => {
+ _ = try self.upnp.deletePortMapping(external_port, protocol);
+ },
+ else => {},
+ }
+
+ // Remover de la lista local
+ var i: usize = 0;
+ while (i < self.mappings.items.len) {
+ if (self.mappings.items[i].external_port == external_port and
+ self.mappings.items[i].protocol == protocol)
+ {
+ _ = self.mappings.orderedRemove(i);
+ } else {
+ i += 1;
+ }
+ }
+ }
+
+ /// Renueva todos los mapeos
+ pub fn renewMappings(self: *NatManager) !void {
+ for (self.mappings.items) |mapping| {
+ _ = try self.mapPort(
+ mapping.internal_port,
+ mapping.external_port,
+ mapping.protocol,
+ mapping.description,
+ mapping.lifetime,
+ );
+ }
+ }
+
+ /// Elimina todos los mapeos
+ pub fn unmapAll(self: *NatManager) !void {
+ while (self.mappings.items.len > 0) {
+ const mapping = self.mappings.items[0];
+ try self.unmapPort(mapping.external_port, mapping.protocol);
+ }
+ }
+};
+
+// =============================================================================
+// Tests
+// =============================================================================
+
+test "nat pmp client init" {
+ const allocator = std.testing.allocator;
+ var client = NatPmpClient.init(allocator);
+ defer client.deinit();
+
+ try std.testing.expect(client.socket == null);
+ try std.testing.expect(client.external_ip == null);
+}
+
+test "upnp client init" {
+ const allocator = std.testing.allocator;
+ var client = UpnpClient.init(allocator);
+ defer client.deinit();
+
+ try std.testing.expect(client.socket == null);
+ try std.testing.expect(client.device == null);
+}
+
+test "nat manager init" {
+ const allocator = std.testing.allocator;
+ var manager = NatManager.init(allocator);
+ defer manager.deinit();
+
+ try std.testing.expect(manager.gateway_type == .unknown);
+ try std.testing.expect(manager.mappings.items.len == 0);
+}
+
+test "protocol to string" {
+ try std.testing.expectEqualStrings("TCP", Protocol.TCP.toString());
+ try std.testing.expectEqualStrings("UDP", Protocol.UDP.toString());
+}
+
+test "gateway type" {
+ try std.testing.expect(GatewayType.unknown != GatewayType.upnp);
+ try std.testing.expect(GatewayType.nat_pmp != GatewayType.none);
+}