p2p-chat is designed to be as simple as possible: two peers connect directly over TCP, exchange ephemeral encryption keys, and then send encrypted messages back and forth. There is no broker, relay, or server involved at any stage. This page describes how the pieces fit together — from the momentDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/Project516/p2p-chat/llms.txt
Use this file to discover all available pages before exploring further.
listen binds a port to the moment a message lands on screen.
High-level design
p2p-chat is a single-binary Go program. It supports exactly one connection at a time: the listener accepts one incoming TCP connection and then stops accepting. Both sides are symmetric once the session is established — each peer can send and receive freely.Package layout
crypto/
Key exchange and per-message encryption/decryption using NaCl box (X25519 + XSalsa20-Poly1305).
internal/chat/
Session management: reading stdin, dispatching slash commands, and driving the message loop.
internal/transport/
Length-prefixed framing. Writes and reads wire frames — a 2-byte big-endian length header followed by the payload.
internal/version/
Reads
assets/version.txt at runtime and returns the version string.main.go parses the CLI arguments and delegates to either chat.Listen or chat.Connect. Both functions follow the same lifecycle once a TCP connection is established.
Connection lifecycle
Bind or dial
The host calls
chat.Listen(address), which binds a TCP socket and calls Accept() to block until one peer connects. The connecting peer calls chat.Connect(address), which dials the given TCP address. The underlying transport is a plain TCP stream — there is no TLS handshake at this layer.Key exchange
Immediately after the TCP connection is established, both sides call
crypto.ExchangeKeys. Each peer generates an ephemeral X25519 key pair, sends its public key to the other side over the raw TCP connection, and reads the other peer’s public key in return. The shared NaCl box key is derived from these two public keys.No key material is written to disk. Keys exist only in memory for the duration of the session.Bidirectional message loop
Once keys are exchanged, the session enters the message loop. Incoming messages are handled in a goroutine that reads frames from the TCP connection, decrypts them, and prints them to stdout. Outgoing messages are handled in the main goroutine, which reads lines from stdin, encrypts them, and writes frames to the TCP connection.
Transport layer: length-prefixed framing
All data on the wire passes throughinternal/transport, which implements a simple length-prefix framing protocol. Every frame consists of a 2-byte big-endian uint16 length header followed immediately by the payload bytes.
SendFrame writes a frame:
ReceiveFrame reads a frame:
Crypto layer
Encryption is handled bycrypto/crypto.go, which wraps the NaCl box primitive.
ExchangeKeys— exchanges public keys over the open TCP connection and returns a shared 32-byte key derived via X25519 Diffie-Hellman.Encrypt(plaintext, sharedKey)— generates a random 24-byte nonce, encrypts the plaintext with XSalsa20-Poly1305, and returnsnonce || ciphertext.Decrypt(encrypted, sharedKey)— splits the first 24 bytes as the nonce, decrypts the remainder, and returns the plaintext.
Wire message format
After framing is stripped, each payload has the following internal layout:uint16 length covers the nonce and ciphertext together.
Concurrency model
p2p-chat uses two goroutines per session:| Goroutine | Responsibility |
|---|---|
| Main goroutine | Reads lines from stdin, processes slash commands, encrypts and writes outgoing frames |
| Receiver goroutine | Reads incoming frames from the TCP connection, decrypts them, and prints to stdout |
Version
The version string is stored inassets/version.txt and read at runtime by internal/version/version.go:
1.0.0. Both the go run . version subcommand and the /version slash command call this function. The file must be present at the working directory from which you run the program.