peaboard 0.1.0

A private bulletin board on the pea* stack: discovery (peaveil), gossip (peasub), and metadata-private shaping (peashape) in one small CLI.
peaboard-0.1.0 is not a library.

peaboard

A private bulletin board you run from the terminal — built to demonstrate the Peapod stack end to end.

[!WARNING] This is a demonstration, not a product. It exists to show how the Peapod crates fit together in the smallest honest amount of code. It ships a hard-coded encryption key (so anyone with the source can read every message), keeps no history across restarts, and has had no security review. Do not use it for anything you actually want kept private. See Just a demo at the bottom.

┌─ peaboard ───────────────────────────────────────────────┐
│ a private bulletin board on the Peapod stack — DEMO ONLY │
└──────────────────────────────────────────────────────────┘
discovery : 127.0.0.1:9000   board : 127.0.0.1:9001
nick      : alice

Boards:
    rust
    privacy
    memes

> /join rust
— now on #rust (0 message(s)) —
[net] peers on the network: 2
[15:45 #b8de5d] bob: has anyone benchmarked locktick?
[15:45 #df4fb8] carol: yep, see message #b8de5d

How the pea* crates interact

The whole point of this example. Four crates stack up, and each one does only its own job — the layer below never knows what the layer above is for, and the layer above never re-implements what the layer below already does:

Layer Crate Job What it does not do
transport pea2pea open TCP connections, run protocols
shaping peashape pad every frame to one size, emit at a constant rate, fill gaps with cover traffic decide what to send
discovery peaveil gossip peer addresses, maintain a "view" of the network open connections
gossip peasub spread messages to the whole overlay, de-dup what it has seen open connections; encrypt
the app peaboard connection policy + payload encryption + UI anything a lower layer already handles

peaveil and peasub are siblings: each is an independent protocol that sits on its own peashape node (and peashape sits on its own pea2pea node). peaboard runs one of each and bridges them.

Independent Nodes

Unlike many peer-to-peer frameworks, peaboard intentionally does not multiplex all protocols over a single networking stack. Each component owns its own pea2pea::Node, along with its own peashape instance. At first glance this may appear redundant. In practice, it creates clean operational boundaries.

Each component independently controls:

  • connection establishment and teardown,
  • maximum number of peers,
  • admission policy,
  • backpressure strategy,
  • traffic shaping profile,
  • reconnect behaviour,
  • resource limits,
  • exposure to different networks or interfaces.

This separation allows discovery and dissemination to be optimized independently. For example, a deployment may expose peaveil to a broader set of peers while keeping peasub considerably more selective. Likewise, each component may employ different traffic shaping strategies or bandwidth budgets without affecting the others.

Keeping the networking stacks separate also isolates failure domains. Congestion, policy changes, or implementation bugs in one component do not automatically propagate to the others.

Finally, multiplexing remains available as an application-level optimization, not as a fundamental architectural assumption. Components that benefit from sharing a connection may do so, while those that benefit from isolation can continue to operate independently.

This design deliberately favors explicit composition over implicit coordination. The application owns the policy; the libraries remain small, self-contained, and independently reusable.

The life of a discovery

How a node that knows only one peer ends up connected to the whole network:

  1. peaboard hands peaveil a bootstrap address at startup. peaveil puts it in its view but does not dial it — opening connections isn't a discovery library's job.
  2. peaboard's reconcile loop reads peaveil.known_peers() and calls peaveil.connect(addr). The application owns the connection.
  3. Now connected, the two peaveil nodes gossip address samples (shaped by peashape, so the exchange is invisible). Each learns the peers the other knows.
  4. Next reconcile tick, the newly-learned addresses are in known_peers(), so peaboard dials them too — and bridges each onto the board overlay at addr.port() + 1. Discovery is transitive: bootstrap to one node, reach them all.

The life of a post

What happens when you type a line and press enter:

  1. peaboard builds a Post, seals it with ChaCha20-Poly1305 (proto::seal), and calls peasub.publish(sealed). The board name is inside the ciphertext.
  2. peasub assigns a random 32-byte id, and queues the frame.
  3. peashape is already emitting a frame on every cover tick. On the next tick it sends your frame instead of a cover one — same size, same timing — to fanout connected peers.
  4. pea2pea writes the bytes to the wire. To an observer they are indistinguishable from the cover frames flowing the rest of the time.
  5. At each peer, pea2pea reads the frame, peasub de-dups it by id, delivers it to subscribers, and re-gossips it to its peers (so it fans out across the overlay).
  6. peaboard receives it via peasub.subscribe(), calls proto::open; if it decrypts under the shared key it's a real post (cover frames just fail to open), and it's displayed.
        peaveil overlay (discovery)            peasub overlay (the board)
   alice:9000 ─ bob:9002 ─ carol:9004     alice:9001 ─ bob:9003 ─ carol:9005
        └────────────┬───────────┘             └────────────┬──────────┘
          "who is out there?"                     posts gossip hop-by-hop
                     │                                       ▲
                     └──── peaboard's reconcile loop dials ──┘
              (the app is the only thing that opens a socket)

Connections and the peer count

A subtlety worth understanding, because the raw connection count is not what you'd expect.

peaboard's reconcile loop dials every peer it discovers. TCP connections are bidirectional, but each dial opens its own connection — so when both ends dial each other (which they do, since both discover each other), a pair of nodes ends up sharing two connections, one opened from each side. In a 3-node network every node therefore holds four board connections: two it dialed, two dialed into it.

Why doesn't peaboard just collapse those into "one peer"? Because it can't tell they're the same peer. An outbound connection goes to the peer's known listener address (bob:9003), but the matching inbound connection arrives from the peer's ephemeral source port (bob:51324), and nothing in the frames says "I am bob". The pea* stack deliberately carries no peer identity — that's an application concern, like encryption.

A real overlay closes this gap with a handshake: right after connecting, each side announces its listener address (and usually a public key), so the receiver can recognize the ephemeral connection as "bob", find the connection it already has to bob, and drop the redundant one. pea2pea supports exactly this via a custom Handshake protocol — peaboard skips it to stay minimal.

So instead of counting connections, peaboard reports the number of distinct peers peaveil has discovered. peaveil keys peers by listener address, so each appears once regardless of how many TCP connections back it — which is why a healthy 3-node network reports 2 peers, not 4.

Run it

Three terminals, one machine. Bob and Carol each know only Alice; they find each other through her.

# terminal 1 — the entry point
cargo run -- --port 9000 --nick alice

# terminal 2
cargo run -- --port 9002 --bootstrap 127.0.0.1:9000 --nick bob

# terminal 3 — only knows alice, still sees bob's posts
cargo run -- --port 9004 --bootstrap 127.0.0.1:9000 --nick carol

In each window: /join rust, then type to post. A post from any node reaches every node, even ones it never directly dialed.

Commands

/join <board>   enter a board (replays its history)
/boards         list known boards and message counts
/peers          how many peers are on the network
/help           command list
/quit           leave
<text>          post to the current board

The code

Two short files, kept deliberately small:

  • src/main.rs — CLI, the reconcile loop (the connection manager), and the incoming-post task.
  • src/proto.rs — the Post wire format and the AEAD seal / open that make a real post indistinguishable from peasub cover.

Just a demo

What a real application built on this stack would need, and peaboard deliberately skips:

  • Real key management. peaboard uses one hard-coded key (proto::board_key) shared by every node — that is what makes them one "private" board, but it means the encryption keeps out nobody who has the source. A real deployment does key agreement (a per-board key, or a pea2pea Noise handshake). The pea* crates stay out of the crypto business by design; supplying it is the application's job, and here it's only a stub.
  • Connection de-duplication. peaboard opens one connection per direction and runs no identity handshake to collapse them — see Connections and the peer count.
  • Persistence. History lives in memory and is gone on exit.
  • Sanity limits. No rate limiting, no message-size policy beyond the single-frame cap, no moderation, no abuse handling.
  • Identity & threads. Nicknames are unauthenticated free text; there are no replies, attachments, or message ordering guarantees.
  • A serious threat-model review. The metadata-privacy property is inherited from peashape (constant rate/size); the caveats in peashape's and peasub's own docs all apply, and peaboard adds no analysis of its own.

It is a readable map of how the pieces connect — start there, not here, if you're building something real.

License

MIT OR CC0-1.0.

🫛 Peapod

This library is part of the Peapod: a collection of small, composable Rust libraries for building robust peer-to-peer systems.

Library Purpose
pea2pea Lightweight P2P networking primitive
peashape Traffic shaping
peaveil Privacy-oriented peer discovery
peasub Metadata-private dissemination
peaplex Optional stream multiplexing
peaboard Reference application

Each library does one thing well and composes naturally with the others.