zcatp2p/src/connection.zig
reugenio 7e5b16ee15 Inicial: biblioteca zcatp2p para comunicación P2P segura
- Especificación completa del protocolo (PROTOCOL.md)
- Referencia de API (API.md)
- Implementación crypto: SHA256, ChaCha20-Poly1305
- Device ID con Base32 y verificación Luhn32
- Framing de mensajes (HELLO, PING, DATA, etc.)
- Discovery local UDP broadcast
- Estructura de conexiones y estados
- Build system para Zig 0.15.2

Pendiente: TLS 1.3, STUN, Global Discovery HTTPS, Relay

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 01:06:30 +01:00

369 lines
9.9 KiB
Zig

//! Módulo de conexión - Gestión de conexiones P2P
//!
//! Maneja conexiones TLS, keepalives, y multiplexación de mensajes.
const std = @import("std");
const identity = @import("identity.zig");
const protocol = @import("protocol.zig");
const discovery = @import("discovery.zig");
pub const DeviceId = identity.DeviceId;
/// Configuración del nodo P2P
pub const Config = struct {
/// Nombre del dispositivo
device_name: []const u8 = "zcatp2p",
/// Puerto para conexiones entrantes
listen_port: u16 = 22000,
/// Habilitar discovery local
local_discovery: bool = true,
/// Servidores de discovery global
global_discovery_servers: []const []const u8 = &.{},
/// Servidores STUN
stun_servers: []const []const u8 = &.{
"stun.l.google.com:19302",
"stun.syncthing.net:3478",
},
/// Servidores relay
relay_servers: []const []const u8 = &.{},
/// Directorio de datos
data_dir: []const u8,
/// Compresión habilitada
compression: bool = true,
/// Callbacks
on_device_discovered: ?*const fn (DeviceId, []const []const u8) void = null,
on_message_received: ?*const fn (*Connection, protocol.Message) void = null,
on_connection_state_changed: ?*const fn (*Connection, ConnectionState) void = null,
};
/// Estado de conexión
pub const ConnectionState = enum {
connecting,
connected,
disconnecting,
disconnected,
@"error",
};
/// Tipo de NAT detectado
pub const NatType = enum {
unknown,
none,
full_cone,
restricted,
port_restricted,
symmetric,
blocked,
};
/// Errores del módulo
pub const Error = error{
AlreadyInitialized,
NotInitialized,
InvalidDeviceId,
ConnectionFailed,
ConnectionTimeout,
ConnectionClosed,
PeerNotFound,
CertificateError,
TlsError,
ProtocolError,
CompressionError,
OutOfMemory,
IoError,
InvalidConfig,
};
/// Información de un peer
pub const PeerInfo = struct {
device_id: DeviceId,
device_name: []const u8,
client_name: []const u8,
client_version: []const u8,
addresses: []const []const u8,
connected_at: i64,
is_local: bool,
bytes_sent: u64,
bytes_received: u64,
};
/// Conexión con un peer
pub const Connection = struct {
allocator: std.mem.Allocator,
device_id: DeviceId,
state: ConnectionState,
peer_info: ?PeerInfo,
socket: ?std.posix.socket_t,
next_message_id: u32,
bytes_sent: u64,
bytes_received: u64,
connected_at: i64,
pub fn init(allocator: std.mem.Allocator, device_id: DeviceId) Connection {
return .{
.allocator = allocator,
.device_id = device_id,
.state = .disconnected,
.peer_info = null,
.socket = null,
.next_message_id = 1,
.bytes_sent = 0,
.bytes_received = 0,
.connected_at = 0,
};
}
pub fn deinit(self: *Connection) void {
if (self.socket) |sock| {
std.posix.close(sock);
}
}
pub fn getDeviceId(self: *Connection) DeviceId {
return self.device_id;
}
pub fn getState(self: *Connection) ConnectionState {
return self.state;
}
pub fn getPeerInfo(self: *Connection) ?PeerInfo {
return self.peer_info;
}
pub fn isConnected(self: *Connection) bool {
return self.state == .connected;
}
/// Envía datos al peer
pub fn send(
self: *Connection,
content_type: []const u8,
data: []const u8,
) Error!u32 {
if (self.state != .connected) return Error.ConnectionClosed;
const msg_id = self.next_message_id;
self.next_message_id += 1;
const msg = protocol.DataMessage{
.message_id = msg_id,
.content_type = content_type,
.data = data,
};
const encoded = msg.encode(self.allocator) catch return Error.OutOfMemory;
defer self.allocator.free(encoded);
// TODO: Enviar por socket con TLS
self.bytes_sent += encoded.len;
return msg_id;
}
/// Cierra la conexión
pub fn close(self: *Connection) void {
self.closeWithReason("closed by user");
}
/// Cierra la conexión con motivo
pub fn closeWithReason(self: *Connection, reason: []const u8) void {
_ = reason;
if (self.socket) |sock| {
std.posix.close(sock);
self.socket = null;
}
self.state = .disconnected;
}
/// Espera hasta que la conexión esté establecida
pub fn waitConnected(self: *Connection, timeout_ms: u32) Error!void {
_ = timeout_ms;
if (self.state == .connected) return;
if (self.state == .@"error" or self.state == .disconnected) {
return Error.ConnectionFailed;
}
// TODO: Implementar espera con timeout
return Error.ConnectionTimeout;
}
};
/// Instancia principal P2P
pub const P2P = struct {
allocator: std.mem.Allocator,
config: Config,
device_id: DeviceId,
connections: std.AutoHashMapUnmanaged(DeviceId, *Connection),
discovery_manager: discovery.DiscoveryManager,
listener_socket: ?std.posix.socket_t,
external_address: ?[]const u8,
nat_type: NatType,
pub fn init(allocator: std.mem.Allocator, config: Config) Error!*P2P {
const self = allocator.create(P2P) catch return Error.OutOfMemory;
errdefer allocator.destroy(self);
// Generar o cargar Device ID
// TODO: Implementar carga de certificado
var device_id: DeviceId = undefined;
std.crypto.random.bytes(&device_id);
self.* = .{
.allocator = allocator,
.config = config,
.device_id = device_id,
.connections = .{},
.discovery_manager = discovery.DiscoveryManager.init(allocator, device_id),
.listener_socket = null,
.external_address = null,
.nat_type = .unknown,
};
return self;
}
pub fn deinit(self: *P2P) void {
// Cerrar todas las conexiones
var iter = self.connections.iterator();
while (iter.next()) |entry| {
entry.value_ptr.*.deinit();
self.allocator.destroy(entry.value_ptr.*);
}
self.connections.deinit(self.allocator);
// Cerrar listener
if (self.listener_socket) |sock| {
std.posix.close(sock);
}
// Limpiar discovery
self.discovery_manager.deinit();
// Limpiar external address
if (self.external_address) |addr| {
self.allocator.free(addr);
}
self.allocator.destroy(self);
}
/// Obtiene el Device ID local
pub fn getDeviceId(self: *P2P) DeviceId {
return self.device_id;
}
/// Obtiene el Device ID como string
pub fn getDeviceIdString(self: *P2P, buf: []u8) []const u8 {
return identity.deviceIdToString(self.device_id, buf);
}
/// Parsea un Device ID desde string
pub fn parseDeviceId(str: []const u8) Error!DeviceId {
return identity.stringToDeviceId(str) catch Error.InvalidDeviceId;
}
/// Compara dos Device IDs
pub fn deviceIdEquals(a: DeviceId, b: DeviceId) bool {
return identity.deviceIdEquals(a, b);
}
/// Obtiene el Short ID
pub fn getShortId(device_id: DeviceId) identity.ShortId {
return identity.getShortId(device_id);
}
/// Conecta a un dispositivo
pub fn connect(self: *P2P, device_id: DeviceId) Error!*Connection {
// Verificar si ya existe conexión
if (self.connections.get(device_id)) |conn| {
if (conn.isConnected()) return conn;
}
// Crear nueva conexión
const conn = self.allocator.create(Connection) catch return Error.OutOfMemory;
conn.* = Connection.init(self.allocator, device_id);
conn.state = .connecting;
self.connections.put(self.allocator, device_id, conn) catch {
self.allocator.destroy(conn);
return Error.OutOfMemory;
};
// Buscar direcciones del peer
const addresses = self.discovery_manager.lookup(device_id) catch null;
if (addresses == null) {
conn.state = .@"error";
return Error.PeerNotFound;
}
// TODO: Intentar conectar a las direcciones
return conn;
}
/// Desconecta de un peer
pub fn disconnect(self: *P2P, device_id: DeviceId) void {
if (self.connections.get(device_id)) |conn| {
conn.close();
}
}
/// Obtiene una conexión existente
pub fn getConnection(self: *P2P, device_id: DeviceId) ?*Connection {
return self.connections.get(device_id);
}
/// Número de conexiones activas
pub fn connectionCount(self: *P2P) usize {
var count: usize = 0;
var iter = self.connections.iterator();
while (iter.next()) |entry| {
if (entry.value_ptr.*.isConnected()) count += 1;
}
return count;
}
/// Obtiene la IP externa
pub fn getExternalAddress(self: *P2P) ?[]const u8 {
return self.external_address;
}
/// Obtiene el tipo de NAT
pub fn getNatType(self: *P2P) NatType {
return self.nat_type;
}
};
// =============================================================================
// Tests
// =============================================================================
test "p2p init/deinit" {
const allocator = std.testing.allocator;
const p2p = try P2P.init(allocator, .{
.data_dir = "/tmp/zcatp2p-test",
});
defer p2p.deinit();
try std.testing.expect(p2p.connectionCount() == 0);
}
test "connection init" {
const allocator = std.testing.allocator;
const device_id = [_]u8{0xab} ** 32;
var conn = Connection.init(allocator, device_id);
defer conn.deinit();
try std.testing.expect(conn.state == .disconnected);
try std.testing.expect(!conn.isConnected());
}