Expand description
peashape — a traffic-shaping middleware for pea2pea
nodes.
§What it does
peashape is a thin layer on top of pea2pea that
enforces two simple rules on every outbound frame:
- The frame size is constant. Every byte the node
writes to the wire is padded (or covered) to exactly
frame_sizebytes, so an observer cannot distinguish “what” is being sent by length. - The frame timing is constant (or Poisson). A background scheduler ticks at a configured rate; on every tick it pulls the next real message from a bounded priority lane, or generates a cover message if the lanes are empty. Because real and cover messages share the same code path, and because the timing of the ticks is independent of application activity, the resulting outbound traffic is observationally indistinguishable from a stream with no real content at all.
The two rules together defeat a passive global network observer who can see every byte sent between every pair of nodes, and the timing of every byte, but cannot break the cryptographic primitives protecting the link. The observer learns nothing about the existence, frequency, or destination of user activity beyond the rate the node has been configured for.
§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 _local_addr = node.spawn().await?;
// subscribe to incoming frames
let mut rx = node.subscribe();
// submit a real message to a peer (call `node.connect(addr)` first)
// node.send_shaped(peer, b"hello, peashape")?;
// or broadcast it: the scheduler will fan it out to `fanout` peers
// node.broadcast_shaped(b"hello, world")?;
node.shutdown().await;§When to use peashape
- Standalone: any
pea2peaapplication that needs a constant-rate or Poisson outbound stream with cover traffic — for example, a private RPC system that wants to hide when a user makes a request. - As a substrate: a higher-level protocol (e.g. a
gossip layer, a publish-subscribe system, a private
membership protocol) that wants to inherit the
metadata-privacy property for free. The application calls
Node::send_shaped/Node::broadcast_shapedinstead of rawunicast/broadcast, and the rest of the protocol — framing, scheduling, cover — is taken care of. - Stacked: running
peashapeon 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.
§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
pea2peaHandshake).
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.
The one caveat is destination under sustained unicast
traffic in ShapingScope::Global: a peer that is the
steady recipient of real unicast frames receives them at a
marginally higher long-run rate than its cover-only share,
a residual aggregate signal inherent to carrying unicast
over a gossip-style fanout (see Scheduler). Broadcast
traffic spreads uniformly and is unaffected, and
ShapingScope::PerConnection removes the signal entirely
— a real frame merely occupies the cover slot the
recipient’s link was going to emit anyway. Use
PerConnection for unicast-heavy workloads.
peashape does not attempt to defeat:
- an observer that can compromise the node itself;
- side channels outside the network (e.g. a screen-snooping adversary, or a process that visibly burns CPU only when the user is active);
- an observer that controls a non-trivial fraction of the network’s nodes and can correlate across them;
- traffic content analysis: cover hides when messages
are sent, not what they say. End-to-end payload
confidentiality is the application’s responsibility (or
can be layered on via a
pea2peaHandshake).
§Architecture
Nodewraps apea2pea::Nodeand adds three pieces of bookkeeping: a bounded high-priority lane (FIFO; always drained first), a bounded low-priority lane (LIFO with drop-oldest), a broadcast channel of incoming frames, and a backgroundSchedulertask that drains the lanes at the configured rate.- The length-delimited
Codecforces every frame on the wire to a single fixed size (configurable viaShapeConfig::frame_size), so the length of a frame is never a tell. - The first
ID_SIZEbytes of every frame are a random message identifier (per the convention, butpeashapeitself does not interpret them). Real messages receive a random ID at submission time; cover messages receive a fresh random ID per emission. The receiver’ssubscribechannel yields every frame it sees on the wire; the application filters for real traffic (typically by authenticating-decrypting with a recognizable structure that random cover bytes won’t match). - When a tick fires, the scheduler either pulls from the
high-priority lane, then the low-priority lane, or
generates a cover frame — and ships it to the right
peer(s) according to the configured
ShapingScope. Every tick emits exactly the same number of frames of the same on-the-wire size, so the metadata-privacy property is preserved regardless offanout.
§Composing with other protocols
peashape is designed to be a building block. The most
common composition is to wrap it inside a higher-level
protocol: the higher-level layer is in charge of the
application semantics (e.g. gossip, pub/sub, RPC), and
uses peashape’s Node::send_shaped /
Node::broadcast_shaped to actually put bytes on the
wire. Because the wire format is “constant-size
length-delimited frames at a constant (or Poisson) rate,”
the higher-level protocol inherits the metadata-privacy
property for free.
For multi-hop networks, run peashape on every node in the
network; each hop independently adds its own cover traffic,
so a passive observer who can only watch a single link still
cannot correlate traffic across hops. This is the
“cover on top of cover” property.
§Choosing the shaping rate
The shaping rate is the only knob that controls the privacy / bandwidth trade-off. As a rule of thumb:
- the application should submit no faster than the configured rate, otherwise its messages accumulate in the high-priority lane;
high_lane_capacityshould be sized torate * burst_secondsto keep submission latency low under reasonable bursts;low_lane_capacityshould be sized to the expected burst of relay traffic (e.g.fanout * cover_rate * drain_secondsfor a gossip layer on top ofpeashape).
Re-exports§
pub use pea2pea;
Structs§
- Codec
- A
pea2peacodec that constrains every frame on the wire to a single, fixed payload size, regardless of the actual payload contents. - Node
- A single peer in a
peashapenetwork. - Pending
Frame - A frame that has been queued for transmission, together with its intended delivery target.
- Scheduler
- The background task that drains a
Shaperat the configured rate, picking one frame per tick (real if any are queued, cover otherwise) and shipping it to the right peer(s). - Shape
Config - The set of parameters that govern a
Node. - Shaper
- The shared, node-wide state that backs every clone of a
Node.
Enums§
- Error
- All errors that can be surfaced to the application through the
public
peashapeAPI. - Lane
- Which priority lane to enqueue a frame into.
- Shaping
Scope - Whether the scheduler ticks once for the whole node, or once per connection.
- Shaping
Strategy - How the node generates outbound traffic.
- Target
- The on-the-wire delivery mode of a queued frame.
- Topology
- Re-exported so that callers can wire up a topology in tests
without adding
pea2peaas a direct dependency. The way in which nodes are connected to each other; used inconnect_nodes.
Constants§
- ID_SIZE
- Size, in bytes, of the message-identifier field that prefixes
every frame in protocols built on top of
peashape. - SUBSCRIBER_
CAPACITY - Capacity of the broadcast channel used to deliver received
messages to application subscribers. When the channel is full,
the oldest message is dropped and receivers observe
RecvError::Lagged.
Traits§
- Cover
Generator - Generates cover frames for a
Nodewhen its priority lanes are empty at a shaping tick.
Functions§
- build_
frame - Pads
payloadtoconfig.frame_sizewith random bytes and returns the freshly-assembled frame along with the random ID written into its firstID_SIZEbytes. - connect_
nodes - Re-exported so that callers can wire up a topology in tests
without adding
pea2peaas a direct dependency. Connects the provided list of nodes in order to form the givenTopology. - random_
cover - Returns a freshly-generated cover frame: a random
ID_SIZE-byte identifier followed byframe_size - ID_SIZErandom bytes. Indistinguishable on the wire from a real application message.