
Mensajero cifrado peer-to-peer en Rust con Noise IK y UDP NAT hole punching
Chat peer-to-peer cifrado de extremo a extremo sobre UDP. Sin cuentas, sin servidor central que retransmita/almacene mensajes, sin intermediarios. Solo dos pares, una conexión directa y cifrado mediante el protocolo Noise.
https://github.com/user-attachments/assets/939e96d3-45e3-4484-9a27-28c3a0457b05
Dos personas ejecutan punchline connect <peer> en sus máquinas. Punchline atraviesa sus NATs, realiza un handshake cifrado y los introduce en un chat privado, todo en milisegundos. Los servidores STUN y de señalización incluidos manejan el descubrimiento y luego se apartan.

cargo build --release
Inicia los servidores (en una máquina a la que ambos pares puedan acceder), o usa los públicos que tengo alojados en 64.225.107.28 (STUN: puerto 3478, señalización: puerto 8743):
punchline-stund # STUN server - tells peers their public IP
punchline-signald # Signal server - matches peers who want to talk
En la máquina de cada par:
# Generate your identity (X25519 keypair)
punchline keygen
# Share your public key with your peer
punchline pubkey
# Save their key
punchline peers add alice a1b2c3d4...64_hex_chars
# Connect (both peers run this, targeting each other)
punchline connect alice --stun <server>:3478 --signal <server>:8743
La TUI se inicia con una vista en vivo del progreso de conexión:
Descubrimiento STUN - resolviendo tu dirección externa mediante punchline-stund
Servidor de señalización - conectándose a punchline-signald
Esperando al par - el servidor de señalización empareja a ambos pares
Perforación de NAT - estableciendo la ruta UDP directa
Handshake Noise - intercambio de claves cifrado
Una vez completado, estás en el chat. Escribe y presiona Enter. Presiona Esc para salir.
El sistema completo consta de tres binarios, todos incluidos en este repositorio:
| Binario | Rol | Cuándo se usa |
|---|
Después de la configuración inicial, ya no se contacta a los servidores STUN y de señalización. Todo fluye directamente entre pares.
punchlineBanderas globales:
| Bandera | Descripción |
|---|---|
-v | Aumenta la verbosidad del registro (-v = debug, -vv = trace). |
-q, --quiet | Suprime toda la salida de registro. |
punchline-stundpunchline-signaldEn lugar de pasar --stun y --signal cada vez, crea ~/.config/punchline/config.toml:
stun_server = "203.0.113.10:3478"
signal_server = "203.0.113.10:8743"
punchline peers # list all
punchline peers add alice a1b2c3d4... # add
punchline peers remove alice # remove
Los alias se almacenan en ~/.punchline/known_peers.toml. También puedes conectarte directamente con una clave hexadecimal de 64 caracteres.
punchline status
Muestra tu identidad, configuración, accesibilidad del servidor (envía una sonda STUN real y una conexión TCP) y número de pares.
Ambos servidores soportan -v (debug), -vv (trace), -q (quieto), --address y --port:
punchline-stund -v --port 3478
punchline-signald -v --port 8743
Personaliza la TUI a través de ~/.config/punchline/style.toml
Estilos usados en el vídeo:
[colors]
my_text = "#ebdbb2"
peer_text = "#bdae93"
input_text = "#ebdbb2"
border = "#ebdbb2"
sidebar_key = "#ebdbb2"
sidebar_value = "#bdae93"
[padding]
chat_horizontal = 2
chat_vertical = 1
Todos los colores son RGB hexadecimal. Si el archivo no existe, se usan los colores predeterminados del terminal.
punchline completions bash > ~/.local/share/bash-completion/completions/punchline
punchline completions zsh > ~/.zfunc/_punchline
punchline completions fish > ~/.config/fish/completions/punchline.fish
Nombre completo del protocolo: Noise_IK_25519_ChaChaPoly_SHA256
| Componente | Rol |
|---|
El patrón IK significa que el iniciador conoce la clave pública estática del respondedor antes de que comience el handshake. Ambos pares ya tienen las claves del otro (intercambiadas fuera de banda o a través del registro de pares), por lo que no se requiere confianza en el primer uso.
Punchline selecciona deterministamente al iniciador comparando los primeros 8 bytes de la clave pública de cada par como un u64 en big-endian. El par con el valor más pequeño se convierte en el iniciador. Ambos lados calculan esto de forma independiente.
La identidad es una clave secreta X25519 de 32 bytes en ~/.punchline/id_x25519 con permisos Unix 0600. La clave pública se deriva al cargar. La generación de claves usa x25519-dalek con OsRng.
El primer byte de cada paquete UDP identifica su tipo:
Ambos pares ejecutan el mismo algoritmo simultáneamente:
PROBE (0x00) cada 200ms a la dirección externa del par.PROBE, cambiar a enviar ACK (0x01).ACK, enviar un ACK final y declarar éxito.Los mensajes (0x02) transportan cargas útiles UTF-8 cifradas con Noise. Los keepalives (0x03) son cargas útiles vacías cifradas enviadas cada 10 segundos para mantener la sincronización del nonce del cifrado. 30 segundos sin ningún paquete provoca la desconexión.
JSON sobre WebSocket:
// PairRequest (client -> server)
{ "external_addr": "203.0.113.5:48291", "public_key": "a1b2...", "target_public_key": "d4e5..." }
// PairResponse (server -> client)
{ "target_external_addr": "198.51.100.7:51003", "target_public_key": "d4e5..." }
Sigue RFC 5389 (simplificado): solicitud/respuesta de enlace con XOR-MAPPED-ADDRESS. Solo IPv4.
Workspace de Cargo con cuatro crates:
crates/
├── proto/ # Biblioteca compartida: criptografía, tipos STUN y de señalización, trait de transporte
├── client/ # Cliente P2P: CLI, TUI, lógica de conexión, gestión de pares
├── signald/ # Servidor de señalización: emparejamiento de pares vía WebSocket
└── stund/ # Servidor STUN: descubrimiento de dirección externa
cargo install punchline # TUI client
cargo install punchline-signald # Signal server
cargo install punchline-stund # STUN server
Requisitos previos: Rust edición 2024 (rustc 1.85+)
git clone https://github.com/michal-pielka/punchline.git
cd punchline
cargo build --release
Los binarios se colocan en target/release/:
punchlinepunchline-signaldpunchline-stundcargo test
Las pruebas cubren operaciones criptográficas, codificación/decodificación STUN, serialización del protocolo de señalización, análisis de configuración, gestión de pares, tematización de estilos y el handshake Noise IK.
MIT - consulta LICENSE.
punchline-stund | Servidor STUN (UDP) - responde con la IP:puerto externa del cliente | Solo durante la configuración |
punchline-signald | Servidor de señalización (WebSocket) - empareja pares e intercambia direcciones | Solo durante la configuración |
punchline | El mensajero en sí - CLI, TUI, criptografía, perforación de NAT | Siempre |
| Comando | Descripción |
|---|
keygen [--force] [-i path] | Genera un nuevo par de claves de identidad X25519. Usa --force para sobrescribir sin preguntar. Usa -i para especificar la ruta de salida. |
pubkey [-i path] | Imprime tu clave pública (64 caracteres hexadecimales). Usa -i para derivar de un archivo de clave específico. |
connect <peer> [-i path] [--stun addr] [--signal addr] | Conéctate a un par por alias o clave hexadecimal directa. Usa -i para especificar la clave de identidad. Inicia la TUI. |
peers | Lista todos los pares conocidos. |
peers add <name> <key> | Guarda la clave pública de un par bajo un apodo. |
peers remove <name> | Elimina un par por apodo. |
config path | Imprime la ruta del archivo de configuración. |
config show | Muestra los valores de configuración actuales. |
status | Muestra identidad, configuración, accesibilidad del servidor y número de pares. |
completions <shell> | Genera completados de shell (bash, zsh o fish). |
| Bandera | Descripción |
|---|
--address <addr> | Dirección de enlace (por defecto: 0.0.0.0). |
--port <port> | Puerto de enlace (por defecto: 3478). |
-v / -vv | Registro debug / trace. |
-q | Modo silencioso. |
| Bandera | Descripción |
|---|
--address <addr> | Dirección de enlace (por defecto: 0.0.0.0). |
--port <port> | Puerto de enlace (por defecto: 8743). |
-v / -vv | Registro debug / trace. |
-q | Modo silencioso. |
| Noise IK | Patrón de handshake - el iniciador conoce la clave pública del respondedor. Se completa en 2 mensajes. |
| X25519 | Intercambio de claves Diffie-Hellman de curva elíptica (RFC 7748). Seguridad de 128 bits, tiempo constante. |
| ChaCha20-Poly1305 | Cifrado AEAD para el cifrado de mensajes (RFC 8439). El mismo cifrado usado en TLS 1.3 y WireGuard. |
| SHA-256 | Usado internamente por Noise para la derivación de claves y el hash del handshake. |
| Prefijo | Tipo | Fase | Descripción |
|---|
0x00 | PROBE | Perforación de NAT | Enviado cada 200ms para abrir un agujero en la NAT |
0x01 | ACK | Perforación de NAT | Confirma la recepción de un PROBE |
| (ninguno) | Handshake | Handshake | Payload de handshake cifrado con Noise (sin prefijo) |
0x02 | Mensaje | Transporte | Mensaje de chat cifrado |
0x03 | Keepalive | Transporte | Payload vacío cifrado (heartbeat) |
| Crate | Propósito |
|---|
snow | Framework del protocolo Noise (handshake + cifrado de transporte) |
x25519-dalek | Generación y derivación de claves X25519 |
ratatui | Framework de interfaz de terminal |
crossterm | Manejo de eventos de terminal |
clap | Análisis de argumentos CLI + completado de shell |
tungstenite | Cliente/servidor WebSocket |
tracing | Registro estructurado |