feat: implementar UPnP IGD y NAT-PMP port mapping

Nuevo módulo nat.zig con:
- NatPmpClient: cliente NAT-PMP (RFC 6886) con detección de gateway,
  obtención de IP externa y mapeo de puertos UDP/TCP
- UpnpClient: cliente UPnP IGD con SSDP discovery, SOAP control,
  AddPortMapping y DeletePortMapping
- NatManager: interfaz unificada que intenta ambos protocolos

Tests incluidos para inicialización y tipos básicos.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-15 10:52:52 +01:00
parent 1dba570368
commit f5663644ea
3 changed files with 935 additions and 1 deletions

View file

@ -65,6 +65,7 @@ zcatp2p/
├── discovery.zig # Local + global discovery ├── discovery.zig # Local + global discovery
├── stun.zig # STUN client ├── stun.zig # STUN client
├── relay.zig # Relay protocol ├── relay.zig # Relay protocol
├── nat.zig # UPnP IGD / NAT-PMP port mapping
└── connection.zig # Connection management └── connection.zig # Connection management
``` ```
@ -86,8 +87,8 @@ zcatp2p/
- [x] Implementación relay client - [x] Implementación relay client
- [x] Tests unitarios (36 tests) - [x] Tests unitarios (36 tests)
- [x] Discovery global (HTTPS API) - [x] Discovery global (HTTPS API)
- [x] UPnP/NAT-PMP port mapping
- [ ] Integración completa de red - [ ] Integración completa de red
- [ ] UPnP/NAT-PMP port mapping
## Comandos ## Comandos

View file

@ -16,6 +16,7 @@ pub const tls = @import("tls.zig");
pub const stun = @import("stun.zig"); pub const stun = @import("stun.zig");
pub const relay = @import("relay.zig"); pub const relay = @import("relay.zig");
pub const http = @import("http.zig"); pub const http = @import("http.zig");
pub const nat = @import("nat.zig");
// Re-exports principales // Re-exports principales
pub const DeviceId = identity.DeviceId; pub const DeviceId = identity.DeviceId;

932
src/nat.zig Normal file
View file

@ -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: <serviceType>...</serviceType> ... <controlURL>...</controlURL>
var pos: usize = 0;
while (pos < xml.len) {
// Buscar serviceType
const st_start = std.mem.indexOfPos(u8, xml, pos, "<serviceType>") orelse break;
const st_end = std.mem.indexOfPos(u8, xml, st_start, "</serviceType>") 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, "<controlURL>") orelse break;
const ctrl_end = std.mem.indexOfPos(u8, xml, ctrl_start, "</controlURL>") 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 =
\\<?xml version="1.0"?>
\\<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
\\<s:Body>
\\<u:GetExternalIPAddress xmlns:u="{s}">
\\</u:GetExternalIPAddress>
\\</s:Body>
\\</s:Envelope>
;
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, "<NewExternalIPAddress>")) |start| {
const ip_start = start + 22;
if (std.mem.indexOfPos(u8, response, ip_start, "</NewExternalIPAddress>")) |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 =
\\<?xml version="1.0"?>
\\<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
\\<s:Body>
\\<u:AddPortMapping xmlns:u="{s}">
\\<NewRemoteHost></NewRemoteHost>
\\<NewExternalPort>{d}</NewExternalPort>
\\<NewProtocol>{s}</NewProtocol>
\\<NewInternalPort>{d}</NewInternalPort>
\\<NewInternalClient>{s}</NewInternalClient>
\\<NewEnabled>1</NewEnabled>
\\<NewPortMappingDescription>{s}</NewPortMappingDescription>
\\<NewLeaseDuration>{d}</NewLeaseDuration>
\\</u:AddPortMapping>
\\</s:Body>
\\</s:Envelope>
;
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 =
\\<?xml version="1.0"?>
\\<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
\\<s:Body>
\\<u:DeletePortMapping xmlns:u="{s}">
\\<NewRemoteHost></NewRemoteHost>
\\<NewExternalPort>{d}</NewExternalPort>
\\<NewProtocol>{s}</NewProtocol>
\\</u:DeletePortMapping>
\\</s:Body>
\\</s:Envelope>
;
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);
}