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:
parent
1dba570368
commit
f5663644ea
3 changed files with 935 additions and 1 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
932
src/nat.zig
Normal file
932
src/nat.zig
Normal 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);
|
||||
}
|
||||
Loading…
Reference in a new issue