Why p2p-messaging
Fast
23,059 msg/s plaintext. 7,053 msg/s encrypted (Noise + AES-256-GCM). Measured on real public internet.
Reliable
ACK/NACK per message. Exponential backoff retry. WAL replay across crashes. No message silently dropped.
Simple
Single Go library. No external broker process. One relay binary for NAT traversal. Binary-only dependency.
Secure
Noise Protocol Framework (WireGuard-grade). Ed25519 identity keys. PAKE bootstrap. TOFU key pinning.
Benefits
- No broker in hot path — direct P2P, no middleman cost
- E2E encrypted — Noise XX + AES-256-GCM by default, plaintext optional
- NAT-friendly — TCP hole-punch with relay fallback (works behind home/corp networks)
- At-least-once delivery — ACK/NACK + retry + WAL persistence across crashes
- SSH-style identity — Ed25519 keys, known_peers like SSH known_hosts
- Industry-standard fast — 2.4× speedup with batching, near-line-rate for large msgs
- Zero external runtime — single Go binary, no broker/DB/sidecar to deploy
- MIT licensed — use anywhere
Use Cases
- Microservice ↔ microservice — direct messaging without Kafka/RabbitMQ/NATS. Each service holds long-lived P2P connections.
- Edge → cloud telemetry — IoT devices stream metrics over encrypted P2P with WAL fallback for offline buffering.
- Trading / market data — low-latency binary messages with ACK guarantees, persistent across reconnects.
- Multi-region replication — encrypted P2P across data centers without VPN setup.
- Distributed agents — autonomous workers coordinate via direct messages, no central broker.
- File transfer (croc-style) — chunker handles GB+ files over parallel streams + zstd compression.
- Gaming / real-time control — TCP_NODELAY + small msgs + NAT hole-punch through residential networks.
- CLI tool ↔ tool — bench, peer, relay binaries can be composed into pipelines.
- Chat / messaging — text content type + sentinel + persistence = at-least-once messaging without a central server.
- Embedded systems — single Go binary, small footprint (~5 MB), runs on ARM/x86/anywhere Go runs.
Getting Started
Install in one line (no Go needed)
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/jdp5949/p2p-messaging/main/install.sh | sh
# Windows (PowerShell)
irm https://raw.githubusercontent.com/jdp5949/p2p-messaging/main/install.ps1 | iex
Auto-detects OS/arch, installs the p2p binary on your PATH. Prefer Go? go install github.com/jdp5949/p2p-messaging/cmd/p2p@latest
Use it — that's the whole setup
# Machine A — start a chat, or send a file / folder
p2p send
p2p send movie.mp4 # 8 parallel streams by default (-streams N)
p2p send ./my-folder
# → prints a code like: 4-brave-tiger-comet
# Machine B — join with the code
p2p 4-brave-tiger-comet
Encrypted (Noise + AES-256-GCM) end-to-end, NAT hole-punched, with a relay fallback. Files are SHA-256 verified.
Two Nodes, Different Networks — One Config
The only shared config is the one-time code. A hosted rendezvous relay (baked-in default) matches the two peers, punches a direct P2P link through both NATs, and drops out. If a direct punch can't form, traffic is bridged through the relay — either way it's end-to-end encrypted.
Machine A (home Wi-Fi, NAT) Machine B (cloud VM, different NAT)
│ │
│ p2p send report.pdf │ p2p 4-brave-tiger-comet
│ ╲ ╱
│ ╲ 1. both reach relay ╱
│ ▼ (rendezvous by code) ▼
│ ┌─────────────────────────────────┐
│ │ relay relay.p2pmsg.duckdns.org │ 2. NAT hole-punch
│ └─────────────────────────────────┘
│ ╲ 3. direct P2P link ╱
▼ ╲___________________╱ ▼
A ◀═══════ encrypted, direct ═══════▶ B
(relay bridges if punch fails)
Real example — your laptop ↔ a cloud server
# On the cloud server (any network, behind NAT or public)
p2p 4-brave-tiger-comet # receives + saves report.pdf, sha256 verified
# On your laptop (home Wi-Fi)
p2p send report.pdf
# ✓ Connected — peer online (direct P2P)
# ✓ sent 2.9 MB in 0.7s (4.3 MB/s)
No port-forwarding, no public IP, no firewall rules. Override the relay with -relay host:9009; force the relay path on hostile NATs with P2P_FORCE_RELAY=1.
Measure the link (connect time, latency, throughput)
p2p bench # machine A → prints a code
p2p bench 4-brave-tiger-comet # machine B
# Connected : 372ms (direct P2P)
# Latency : 28ms (round-trip)
# SIZE TIME THROUGHPUT
# 1.0 MB 410ms 2.5 MB/s
# 10.0 MB 3.6s 2.8 MB/s
Persistent & Long-Running P2P
For always-on nodes (a server that should stay reachable, or transfers that must survive blips), run the relay as a service and lean on built-in reconnect + write-ahead-log recovery.
1. Host your own always-on relay (systemd)
# build a linux relay and run it under systemd on a public host
GOOS=linux GOARCH=amd64 go build -o relay ./cmd/relay
./relay -addr :9009 -tls -tls-cert fullchain.pem -tls-key privkey.pem
# point peers at it
p2p -relay your-host.example.com:9009 send
A ready-made systemd unit lives in deploy/ in the repo.
2. Auto-reconnect on drops
# A dropped link retries (direct, then relay) for 60s before giving up.
# Pinned Ed25519 keys mean reconnects skip the code (fast KK handshake).
p2p 4-brave-tiger-comet
# ⚠ peer offline — reconnecting (up to 60s)…
# ✓ peer back online (direct P2P)
3. Crash-safe delivery (library, WAL)
// unacked messages are journaled and replayed after a restart
b, _ := broker.New(broker.Config{
Conn: c,
WAL: wal, // open with wal.Open("/var/lib/p2p/out.wal", true)
})
Run a long-lived node as a service (systemd / a container) and it will keep rendezvousing under the same code; the relay re-matches peers whenever they reconnect.
Use From Any Language
p2p is a single self-contained binary. Any language that can spawn a
process can drive it — no bindings required. Generate/parse the code on one
side, pass it to the other out-of-band, and stream files or messages.
Python
import subprocess, re
# sender: capture the code, then it transfers movie.mp4
p = subprocess.Popen(["p2p", "send", "movie.mp4"],
stdout=subprocess.PIPE, text=True)
for line in p.stdout:
m = re.search(r"\d+-\w+-\w+-\w+", line)
if m:
code = m.group(0)
print("share this code:", code)
break
p.wait()
# receiver elsewhere: subprocess.run(["p2p", code])
Node.js
const { spawn } = require("child_process");
// receiver: save whatever the peer sends into the current dir
const p = spawn("p2p", ["4-brave-tiger-comet"], { stdio: "inherit" });
p.on("exit", code => console.log("done", code));
Shell / cron (any OS)
# pipe arbitrary program output to a peer over an interactive chat session
echo "backup finished at $(date)" | p2p 4-brave-tiger-comet
# scheduled file push
p2p send /var/backups/db.dump.gz
Prefer a native API? Import the Go packages directly (pkg/broker, pkg/conn, pkg/crypto, pkg/transfer) — see “Using as a Library”.
Using as a Library
package main
import (
"log"
"net"
"time"
"github.com/jdp5949/p2p-messaging/pkg/broker"
"github.com/jdp5949/p2p-messaging/pkg/conn"
"github.com/jdp5949/p2p-messaging/pkg/crypto"
"github.com/jdp5949/p2p-messaging/pkg/protocol"
)
func main() {
// Load or generate identity key (like ~/.ssh/id_ed25519)
id, _ := crypto.LoadOrGenerateIdentity("~/.p2p/id_ed25519")
kp, _ := crypto.LoadKnownPeers("~/.p2p/known_peers")
// Dial remote
c, _ := conn.New(conn.Config{
DialFunc: func() (net.Conn, error) {
return net.Dial("tcp", "peer-b.example.com:9000")
},
BatchSize: 65536, // 64 KB batching for throughput
BatchTimeout: 5 * time.Millisecond,
HandshakeCfg: &crypto.HandshakeConfig{
Identity: id,
KnownPeers: kp,
PeerName: "b",
PAKECode: "snow-tiger-72", // empty for repeat connects
Initiator: true,
},
})
// Create broker
b, _ := broker.New(broker.Config{
Conn: c,
OnInbound: func(m broker.InboundMsg) {
log.Printf("received: %s", m.Payload)
},
})
defer b.Close()
// Send
b.Send(protocol.ContentText, protocol.PriorityNormal, []byte("hello!"))
}
Security NEW
Built on the Noise Protocol Framework — the same foundation as WireGuard and the Lightning Network. Ed25519 identity keys. X25519 ECDH key exchange. AES-256-GCM session encryption.
First Connect — Pattern XX + PAKE
Alice Bob
|-- code "test-2026" --|
| (out-of-band) |
| |
|--> e |
|<-- e, ee, s, es |
|--> s, se |
| |
| X25519 ECDH + PAKE |
| derive session key |
| pin remote pub key |
| |
|<== AES-256-GCM ===>|
Subsequent Connects — Pattern KK
Alice Bob
| (both pubkeys known) |
| |
|--> e, es, ss |
|<-- e, ee, se |
| |
| No PAKE needed. |
| Faster handshake. |
| Full mutual auth. |
| |
|<== AES-256-GCM ===>|
Identity keys are Ed25519, stored at ~/.p2p/id_ed25519 (mode 0600).
Known peers are pinned in ~/.p2p/known_peers using SSH known_hosts format.
First connection uses a one-time PAKE code shared out-of-band; subsequent connections skip the code entirely.
Persistence (WAL) NEW
Append-only write-ahead log for at-least-once delivery across process crashes.
Binary frame format (big-endian)
[Op : 1 byte][Length : 4 bytes][MsgID : 8 bytes][Payload : N bytes]
Op values:
0x01 OpSend — written before each network write
0x02 OpAck — written when ACK received (eligible for compaction)
WAL.Append() before send →
WAL.Ack() on ACK →
WAL.Replay() on startup →
WAL.Compact() every 60 s.
Semantics: at-least-once. Use MsgID for receiver-side deduplication.
NAT Traversal UPGRADED
TCP simultaneous-open via SO_REUSEPORT. Relay coordinates timing; direct path wins. Falls back to relay byte-bridge after 5 s.
Peer A Relay Peer B
|-- register -------->|<------ register --|
|<-- Info(B addrs) ---|--- Info(A addrs) ->|
| | |
|<======= START signal (simultaneous) ====>|
| |
|-------- TCP simultaneous-open (SO_REUSEPORT) -------->|
|<================ direct TCP link ===================>|
| (PUNCH_OK sent to both) |
| |
| if timeout after 5 s: relay byte-bridge |
Wire Format
Every frame starts with a 20-byte binary header (big-endian). Zero parsing ambiguity. Noise ciphertext wraps entire frames when encryption is enabled.
| Bytes | Field | Type | Description |
|---|---|---|---|
| 0–3 | MsgID | uint32 | Monotonic sender-assigned ID |
| 4–5 | MsgType | uint16 | DATA / ACK / NACK / PING / PONG |
| 6–7 | ContentType | uint16 | JSON / BINARY / TEXT / PROTO / AVRO / RAW |
| 8 | Flags | uint8 | Compressed bit, Fragmented bit |
| 9 | Priority | uint8 | 0 (low) to 255 (high) |
| 10–11 | FragIndex | uint16 | Position of this fragment |
| 12–13 | FragTotal | uint16 | Total fragments in message |
| 14–17 | PayloadLen | uint32 | Bytes that follow this header |
Benchmarks
Public internet, Mac → Oracle Cloud, 50,000 × 1 KB messages
| Mode | Throughput | E2E time | Wire BW |
|---|---|---|---|
| Plaintext, no batching | 7,805 msg/s | 6.4 s | 64 Mbps |
| Plaintext + 64 KB batch (5 ms flush) | 18,793 msg/s | 2.7 s | 154 Mbps |
Sustained transfers (no batching)
| Msg size | Throughput | Wire BW |
|---|---|---|
| 1 KB | 9,008 msg/s | 74 Mbps |
| 8 KB | 5,290 msg/s | 346 Mbps |
| 64 KB | 1,791 msg/s | 938 Mbps |
Numbers measured against Oracle Cloud free-tier VM. Code ceiling on localhost is ~70K msg/s for 1 KB — bottleneck is per-msg overhead, not wire.
Download
Easiest: the one-line installer grabs the right p2p binary for your OS/arch and puts it on PATH.
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/jdp5949/p2p-messaging/main/install.sh | sh
# Windows (PowerShell)
irm https://raw.githubusercontent.com/jdp5949/p2p-messaging/main/install.ps1 | iex
Prebuilt binaries
With Go
- go install github.com/jdp5949/p2p-messaging/cmd/p2p@latest
Each release also ships peer, relay, and bench for the same platforms.
Build from Source
Requires Go 1.21+
- git clone https://github.com/jdp5949/p2p-messaging
- go build ./cmd/p2p