Skip to content
DiegoSandivalPublic

About

Libreria P2P minima sobre go-libp2p (4 verbos, zero-config)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

p2plite

CI

Librería P2P mínima en Go, construida sobre go-libp2p pero envuelta en una superficie diminuta: la App solo maneja seis primitivas.

La librería solo mueve bytes y encuentra peers; archivos, chunks, integridad y reintentos son responsabilidad de la App. Identidad criptográfica persistente, DHT Kademlia auto-organizada, transporte QUIC autenticado y redes aisladas por NetworkID. El objetivo: correr un nodo sin configuración y que la red se mantenga sola.

El diseño y el porqué de cada decisión están en docs/diseno-red-p2p.md, que es el documento canónico.

Las seis primitivas

node, _ := p2plite.New(ctx, p2plite.Config{})   // construye (defaults horneados)

// El handler va ANTES de Start: después, un stream podría llegar antes que él.
node.SetStreamHandler(func(from p2plite.PeerID, s p2plite.Stream) { /* … */ })
node.Start(ctx)                                 // escucha y entra a la red

node.Announce(ctx, "mi-recurso")                // publico que tengo algo (y se mantiene)
node.StopAnnounce("mi-recurso")                 // dejo de publicarlo
ch, err := node.FindProviders(ctx, "mi-recurso") // descubro quién lo tiene
node.Connect(ctx, peerID)                       // conecto (opcional: OpenStream ya lo hace)
s, err := node.OpenStream(ctx, peerID)          // le abro un stream (bytes opacos)

Announce se mantiene solo: republica en segundo plano hasta StopAnnounce o Close, porque los provider records de la DHT caducan.

FindProviders no tiene límite de resultados — cancela el ctx cuando tengas bastantes. Su error es un fallo de nodo (DHT caída, nodo parado); "cero proveedores" se señala cerrando el canal sin emitir nada.

Logs: silencioso por defecto

La librería no escribe nada salvo que le pases un Logger. Fiel a que no se apropia de la salida de la App: tú decides si quieres diagnósticos y a dónde van.

p2plite.New(ctx, p2plite.Config{Logger: log.Default()}) // stdlib log → stderr

Logger es una interfaz de un solo método (Printf(string, ...any)) que *log.Logger de la stdlib satisface tal cual; slog o zap se adaptan con un envoltorio de una línea.

Dos clases de error, nunca mezcladas

Clase De dónde sale Qué hacer
Fallo de nodo error devuelto por un método de Node Raro y fatal: reiniciar
Fallo de stream error del Stream al leer/escribir Normal: cerrar y seguir

Los de nodo son ErrNotStarted, ErrClosed, ErrDHTNotReady, ErrNoAddrs.

Diagnóstico: ¿por dónde conectó?

node.ConnKind(peerID)   // direct | hole-punched | relayed | none  — el escalón
node.ConnIP(peerID)     // ipv6 | ipv4 | ""                        — la familia

La escalera de conectividad es automática y no se configura, pero sí se puede mirar. relayed estable en un peer significa que la perforación falló y esa conexión está a punto de morir por los límites de Circuit Relay v2. ConnIP distingue el escalón 1 (IPv6, sin NAT) del 2 (IPv4): direct ipv6 es el caso ideal, hole-punched ipv4 es NAT perforado.

Para verlo con detalle, libp2p lo loguea:

GOLOG_LOG_LEVEL="holepunch=debug,autonat=info" ./node -find hola

⚠️ Requisito de compilación: Go 1.25.7 (no 1.26)

quic-go v0.60 (que arrastra go-libp2p) todavía no soporta Go 1.26: compilado con 1.26 panica en el handshake (crypto/tls bug: where's my session ticket?). Hay que compilar con Go 1.25.7. El toolchain go1.25.7 del go.mod no basta si tienes 1.26 instalado; fuérzalo con la variable de entorno:

GOTOOLCHAIN=go1.25.7 go build ./...

El script build.sh ya lo hace y cross-compila todo:

./build.sh        # genera dist/seed-linux-amd64, dist/node-windows-amd64.exe, dist/node-linux-amd64

(Cuando quic-go publique soporte de Go 1.26, se podrá subir la versión y quitar este pin.)

Tests

GOTOOLCHAIN=go1.25.7 go test -short ./...   # unidad + ciclo de vida (rápido, sin red)
GOTOOLCHAIN=go1.25.7 go test ./...          # + integración announce→find→stream local

El suite rápido no toca la red y corre en menos de un segundo. La integración levanta una semilla y dos nodos en localhost; se salta con -short. El detector de carreras (-race) necesita cgo/gcc, así que va en CI (Linux), no en el Windows de desarrollo.

Puesta en marcha (una sola vez)

  1. Compila todo: ./build.sh.
  2. Abre el puerto UDP 4001 de entrada en tu servidor. Es UDP, no TCP: QUIC va sobre UDP y que el TCP del mismo número responda no sirve de nada. Hay que abrirlo tanto en el firewall del proveedor (IONOS, AWS…) como en el del sistema (ufw/firewalld).
  3. Arranca la semilla. Para probar rápido, a mano:
    ./seed-linux-amd64 -datadir /var/lib/p2plite -port 4001
    En su primer arranque imprime su PeerID y sus multiaddrs. Para dejarla en producción, instálala como servicio endurecido (usuario sin privilegios + sandbox de systemd): ver deploy/.
  4. Hornea la multiaddr con la IP pública del servidor en DefaultBootstrap (en bootstrap.go) y recompila: ./build.sh.
  5. A partir de aquí, los nodos entran a la red sin flags, y los servidores nuevos se añaden solos vía EntryKey sin recompilar a nadie.

Para usar /dns4/<nombre> en vez de la IP hace falta un registro A en modo DNS-only (nube gris): un proxy tipo Cloudflare no pasa UDP/QUIC.

Releases

Empujar un tag vX.Y.Z dispara el workflow de release, que compila los binarios y publica un GitHub Release con ellos y un SHA256SUMS:

git tag v0.1.0 && git push origin v0.1.0

Quien descargue verifica con sha256sum -c SHA256SUMS --ignore-missing.

¿Quieres sostener la red con un seed propio? Ver deploy/RUN-A-SEED.md: cualquiera con un servidor público puede correr un seed y se une solo.

Uso de un nodo (casa / trabajo)

./node-windows-amd64.exe                       # solo unirse a la red y mantenerse
./node-windows-amd64.exe -announce hola        # publicar "hola" y servir a quien lo pida
./node-windows-amd64.exe -find hola -msg ping  # buscar "hola", conectar al proveedor y enviar

Las flags son overrides opcionales para pruebas (-bootstrap, -datadir, -port, -network); nunca obligatorias.

-version imprime qué build corre (útil para saber qué máquina tiene el fix):

./node-linux-amd64 -version   # p2plite-node <git describe> (go1.25.7)

La versión sale de git describe --tags --always --dirty al compilar con build.sh: sin tags es el SHA corto; márcalos (git tag v0.1.0) para versiones legibles.

Prueba de la red (validada en local)

El flujo objetivo funciona de punta a punta:

casa:    node -announce hola          → anuncia el recurso en la DHT (vía la semilla)
trabajo: node -find hola -msg "..."   → descubre a casa, conecta y envía el mensaje
         ← "recibido por <casa>: ..."  (respuesta de casa)
✓ descubrimiento + conexión + mensaje ida y vuelta

Estructura

Ruta Qué es
docs/diseno-red-p2p.md El diseño canónico: las decisiones y su porqué
p2plite.go La librería: las seis primitivas + identidad + host libp2p + DHT
bootstrap.go Valores por defecto horneados (semilla, red, puerto, datadir, EntryKey)
cmd/seed Nodo semilla siempre-activo (DHT server + relay de handshake)
cmd/node Nodo de la App (casa/trabajo), zero-config
build.sh Cross-compila forzando Go 1.25.7

Limitaciones (por diseño)

  • Sin relay de datos. El relay solo coordina el hole punching (DCUtR) y se sale; lo garantizan los límites por defecto de Circuit Relay v2 (128 KB / 2 min). Dos peers que ambos fallen la perforación (NAT simétrico, CGNAT) no se conectan, y es un trade-off aceptado a propósito.
  • El descubrimiento y almacenamiento DHT dependen de que haya al menos una entrada viva.
  • IPv6: el transporte ya escucha en IPv6; en una red IPv6 nativa la conexión directa es el caso normal y no hace falta perforar casi nunca.

About

Libreria P2P minima sobre go-libp2p (4 verbos, zero-config)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

⌂ 换网址