Construyendo un mensajero descentralizado: Integrando libp2p en Flutter mediante Go
Las aplicaciones de mensajería actuales exigen privacidad y descentralización. Este artículo desglosa la implementación de un mensajero P2P con cifrado de extremo a extremo basado en libp2p. La solución utiliza Go para la lógica de red, Flutter para la interfaz de usuario y FFI para la interacción entre componentes. La arquitectura elimina los servidores centrales, garantizando la seguridad incluso al sortear NAT y firewalls.
Arquitectura básica: Flutter, Go y FFI
El principal desafío es integrar libp2p en una app móvil sin comprometer el rendimiento. Flutter se encarga de la interfaz, mientras que la lógica de red se traslada a un binario de Go compilado en:
.dylibpara macOS.sopara Android- Biblioteca estática
.apara iOS
Dart y Go interactúan mediante FFI (Foreign Function Interface) utilizando funciones compatibles con C. Esto evita la sobrecarga de serialización y mantiene las velocidades nativas de intercambio de datos. La gestión de memoria es crucial: los datos de cadena pasados de Go a Dart requieren liberación manual mediante FreeString.
Compilando la biblioteca de Go para plataformas móviles
El proceso de compilación requiere configuraciones específicas por plataforma. Para Android, usa NDK:
CGO_ENABLED=1 GOOS=android GOARCH=arm64 \
CC=$ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang \
go build -buildmode=c-shared \
-o libp2p_network.so \
./main.go
Para iOS:
CGO_ENABLED=1 GOOS=ios GOARCH=arm64 \
CC=$(xcrun --sdk iphoneos --find clang) \
CGO_CFLAGS="-isysroot $(xcrun --sdk iphoneos --show-sdk-path) -arch arm64 -miphoneos-version-min=12.0" \
CGO_LDFLAGS="-isysroot $(xcrun --sdk iphoneos --show-sdk-path) -arch arm64 -miphoneos-version-min=12.0" \
go build -buildmode=c-archive \
-o libp2p_network.a \
./main.go
En Android, la biblioteca va en jniLibs/arm64-v8a/. En iOS, se enlaza mediante Xcode. Para el simulador, compila para x86_64 y fusiona archivos usando lipo -create.
Puente FFI: Interacción entre Go y Dart
Exportar funciones desde Go requiere reglas estrictas:
- Todas las funciones se marcan con
//export - Incluir una
func main() {}vacía - Gestión de memoria:
C.CString()asigna en el montón de C, que Dart debe liberar medianteFreeString
Ejemplo de exportación en Go:
//export StartNode
func StartNode(storagePath *C.char) *C.char {
path := C.GoString(storagePath)
node, err := p2p.NewNode(path)
// ...
return C.CString(node.GetPeerID())
}
En Dart, carga la biblioteca y enlaza funciones:
_loadLibrary() {
if (Platform.isAndroid) {
_lib = DynamicLibrary.open('libp2p_network.so');
}
_startNode = _lib!.lookupFunction<
Pointer<Utf8> Function(Pointer<Utf8>),
Pointer<Utf8> Function(Pointer<Utf8>)
>('StartNode');
}
Llamar a la función implica conversión de cadenas y gestión manual de memoria:
final pathPtr = dir.path.toNativeUtf8();
final resultPtr = _startNode(pathPtr);
final peerId = resultPtr.toDartString();
_freeString(resultPtr);
calloc.free(pathPtr);
Trabajando con libp2p: Creando un nodo e intercambiando mensajes
La inicialización del nodo configura transportes y protocolos:
func NewNode(storagePath string) (*Node, error) {
h, err := libp2p.New(
libp2p.Identity(priv),
libp2p.ListenAddrStrings(
"/ip4/0.0.0.0/tcp/0",
"/ip4/0.0.0.0/udp/0/quic-v1",
),
libp2p.EnableNATService(),
libp2p.EnableRelay(),
libp2p.NATPortMap(),
)
// ...
}
Cada dispositivo obtiene un PeerID único: un hash de la clave pública Ed25519. El envío de mensajes usa intercambio basado en streams:
func (n *Node) SendMessage(peerIDStr, content, msgType, id string) error {
peerID, _ := peer.Decode(peerIDStr)
s, err := n.host.NewStream(ctx, peerID, "/messaging/1.0.0")
s.Write(data)
s.Close()
return nil
}
Los mensajes se entregan directamente cuando están en línea o mediante un nodo relé cuando están desconectados. Los datos cifrados se almacenan temporalmente hasta que el destinatario se conecta.
Garantizando conectividad en condiciones de red difíciles
Para llamadas de voz, se implementa un sistema de transporte de tres niveles:
- Transporte UDP directo (Pion ICE)—canal principal con latencia mínima. Los candidatos ICE se intercambian mediante mensajes libp2p.
- DCUtR (Perforación de agujeros)—recorrido de NAT usando relé.
- Respaldo por relé—relé de tráfico a través de un nodo servidor cuando falla la conexión directa.
El código de manejo de flujo de audio muestra un formato de paquete personalizado:
func (n *Node) SendAudio(data []byte) error {
packet := make([]byte, 1+2+len(data))
packet[0] = 0xFE
binary.BigEndian.PutUint16(packet[1:], uint16(len(data)))
copy(packet[3:], data)
call.Stream.Write(packet)
return nil
}
La recepción de datos usa un bucle para detectar bytes de sincronización y procesar tramas Opus.
Cifrado y seguridad: Double Ratchet en acción
Para chats privados, el protocolo Double Ratchet proporciona secreto de reenvío perfecto y seguridad post-compromiso. Características clave:
- Cada mensaje usa una clave única
- Rotación periódica de claves
- Protección contra compromiso de claves a largo plazo
El sistema garantiza cifrado de extremo a extremo sin que las claves pasen por nodos intermedios. Todas las operaciones criptográficas se ejecutan en el dispositivo del usuario, evitando fugas incluso en nodos relé.
Manejando escenarios sin conexión sin comprometer la privacidad
Las notificaciones push sirven solo como disparadores de activación de la app. Importante:
- Los mensajes push no contienen datos del mensaje
- Sin información del remitente
- El disparador solo contiene una bandera de servicio
Flujo de trabajo:
- Llega una notificación push silenciosa
- El SO activa brevemente la app
- El nodo Go se conecta y recupera paquetes cifrados
- La desencriptación ocurre localmente
- Se genera una notificación local para el usuario
Esto preserva el E2EE incluso con servicios push centralizados.
Puntos clave
- El puente FFI exige una gestión estricta de memoria: se producen fugas si se omite
FreeString - Circuit Relay v2 es crítico para el recorrido de NAT pero añade un 15-20% de latencia
- Double Ratchet proporciona PFS pero requiere sincronización de estado entre dispositivos
- Los disparadores push no comprometen la privacidad ya que no llevan carga útil
- Las compilaciones para iOS necesitan fusión manual de archivos para simulador y dispositivo
— Editorial Team
Aún no hay comentarios.