peashape 0.3.0

A traffic-shaping middleware for pea2pea nodes: constant-rate or Poisson outbound traffic, automatic frame padding, and priority lanes for cover traffic.
Documentation

peashape

Crates.io Documentation dependency status

A traffic-shaping middleware for pea2pea nodes. It enforces two simple rules on every outbound frame:

  1. Constant size. Every frame on the wire is padded (or covered) to a single fixed frame_size, so an observer cannot distinguish "what" is being sent by length.
  2. Constant (or Poisson) timing. A background scheduler ticks at a configured rate; on every tick it pulls the next real message from a priority lane, or generates a cover message if the lanes are empty.

Real and cover messages share the same code path, so the resulting outbound traffic is observationally indistinguishable from a stream with no real content at all.

peashape is a building block: use it as a substrate for a private RPC system, a private pub/sub layer, a private membership protocol, or — in conjunction with peasub — a metadata-private gossip network. Running peashape on every hop of a multi-hop network gives "cover on top of cover": every frame is re-padded and re-timed by the local shaper as it passes through, so a passive observer who can only watch one link still cannot correlate traffic across hops.

📖 Table of Contents


Quick start

use std::time::Duration;
use peashape::{Node, ShapeConfig, ShapingStrategy};

let node = Node::new(ShapeConfig {
    name: Some("alice".into()),
    listener_addr: Some("127.0.0.1:0".parse()?),
    strategy: ShapingStrategy::Constant {
        interval: Duration::from_millis(100),
    },
    ..Default::default()
});

let _addr = node.spawn().await?;

// Subscribe to incoming frames (real and cover alike).
let mut rx = node.subscribe();

// Send a real message to a specific peer (after `connect(addr)`).
node.send_shaped(peer, b"hello, peashape")?;

// Or broadcast it; the scheduler fans it out to `fanout` peers.
node.broadcast_shaped(b"hello, world")?;

# drop(rx);
node.shutdown().await;
# Ok::<_, Box<dyn std::error::Error>>(())

Run cargo run --example demo to see the traffic-analysis-resistance property visualized, or cargo run --example two_nodes for the minimal end-to-end usage pattern.

Priority lanes

peashape exposes two priority lanes for application traffic:

  • Lane::High — FIFO, always drained first. The natural choice for application-submitted traffic: a fresh broadcast_shaped(...) is sent on the very next shaping tick regardless of how much other traffic has piled up. Bounded; once full, further enqueues return Error::LaneFull.
  • Lane::Low — LIFO with drop-oldest eviction. The natural choice for relay traffic: a freshly-enqueued frame is pushed to the front and the very next shaping tick pops it without waiting in line behind older relays.

The shaping scheduler always drains the high lane before the low lane, and falls back to a freshly-generated cover frame when both are empty.

Shaping strategies

pub enum ShapingStrategy {
    /// One frame per fixed `interval`.
    Constant { interval: Duration },
    /// Inter-arrival times drawn from Exp(rate) - Poisson process.
    Poisson { rate: f64 },
}

Constant is the simplest and most predictable. Poisson makes the inter-arrival times themselves random, so an observer who can only see the node's traffic cannot statistically distinguish the Poisson-scheduled cover stream from one whose timing is influenced by user activity — the strictest form of metadata privacy.

Scopes: global vs. per-connection

peashape supports two scheduling scopes:

  • ShapingScope::Global (default) — one global ticker; every tick broadcasts the next frame to fanout random connected peers. This is the natural choice for gossip-style protocols where a single message goes to many destinations.
  • ShapingScope::PerConnection — one ticker per connection; every tick sends one frame to a single round-robin peer. fanout is ignored in this mode.

Unicast and broadcast

Two complementary submit methods are provided:

  • Node::send_shaped(peer, payload) — sends to one specific peer (or silently drops the frame if the peer is no longer connected at tick time).
  • Node::broadcast_shaped(payload) — fanout-based broadcast (in Global mode) or round-robin (in PerConnection).

The same pair of methods is available for the low-priority lane: Node::send_shaped_low(peer, payload) and Node::broadcast_shaped_low(payload). And for re-broadcasting a pre-built frame byte-for-byte (e.g. a frame received from a peer that you want to relay unchanged): Node::relay_shaped(frame).

Threat model

peashape is designed to defeat a passive global network observer who can:

  • observe every byte sent between every pair of nodes;
  • observe the timing of every byte;
  • but cannot break the cryptographic primitives protecting the link (e.g. TLS via a pea2pea Handshake).

Against such an observer, the shaping schedule ensures that the timing distribution and size distribution of a node's outbound traffic are independent of whether the application is submitting messages or not. The observer learns nothing about the existence, frequency, or destination of user activity beyond the rate the node has been configured for.

peashape does not attempt to defeat an observer that can compromise the node itself, that can correlate application-level events with coarse traffic features, or that controls a non-trivial fraction of the network's nodes.

For the full threat model, see the crate-level documentation.

Choosing the shaping rate

The shaping rate is the only knob that controls the privacy / bandwidth trade-off:

  • Higher shaping rate → tighter privacy, more bandwidth.
  • Lower shaping rate → looser privacy, less bandwidth.

The application must submit no faster than the shaping rate; otherwise its messages accumulate in the high-priority lane. The high_lane_capacity should be sized to shaping_rate * burst_seconds to keep submission latency low under reasonable bursts.

Custom cover traffic

By default, cover frames are uniform random bytes. To make the whole stream — real frames and cover alike — mimic a specific wire profile (so a passive observer sees a recognizable protocol rather than random padding), supply a CoverGenerator via ShapeConfig::cover_generator:

pub trait CoverGenerator: Send + Sync {
    fn cover(&self, config: &ShapeConfig) -> BytesMut;
}

A bare Fn(&ShapeConfig) -> BytesMut closure implements the trait, and the generator must return exactly frame_size bytes. Because it is shared across threads and called via &self, any per-frame state (e.g. a sequence counter) needs interior mutability. The trait also has a finalize_real hook, called on each real frame as it drains from a priority lane, so real traffic and cover can be folded into one coherently-sequenced stream. Two worked examples:

  • cargo run --example webrtc_masquerade — a 1:1 (PerConnection) link shaped to look like a WebRTC/RTP call tunneled over TCP.
  • cargo run --example mpegts_broadcast — a one-to-many (Global fan-out) stream shaped to look like a scrambled MPEG-TS / IPTV multicast.

Composition with peasub

peasub is a metadata-private gossip protocol built on top of peashape. The peasub::Node wraps a peashape::Node and adds:

  • an LRU of recently-seen message identifiers (for dedup);
  • a background task that subscribes to the peashape incoming broadcast, dedups each frame against the LRU, and re-enqueues novel frames into peashape's low-priority lane for fanout (LIFO discipline forwards a fresh relay on the next tick).

peasub uses peashape's broadcast_shaped to put bytes on the wire, and peashape's subscribe channel to receive them. The result: every byte that hits the wire is shaped by peashape, so peasub inherits the metadata-privacy property for free.

License

Dual-licensed under MIT or CC0-1.0, at your option.

🫛 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.