- 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>
369 lines
9.9 KiB
Zig
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());
|
|
}
|