Open Source / MIT

p2p-messaging

Direct P2P messaging. E2E encrypted. Crash-safe. NAT hole-punch built in.

Download Binary View on GitHub

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

Use Cases

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–3MsgIDuint32Monotonic sender-assigned ID
4–5MsgTypeuint16DATA / ACK / NACK / PING / PONG
6–7ContentTypeuint16JSON / BINARY / TEXT / PROTO / AVRO / RAW
8Flagsuint8Compressed bit, Fragmented bit
9Priorityuint80 (low) to 255 (high)
10–11FragIndexuint16Position of this fragment
12–13FragTotaluint16Total fragments in message
14–17PayloadLenuint32Bytes 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 KB9,008 msg/s74 Mbps
8 KB5,290 msg/s346 Mbps
64 KB1,791 msg/s938 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

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