This is the public API for the Sneaker++ encrypted peer-to-peer mesh networking library. Everything is accessed through a single header:
#include <sneaker.hpp>Link against the sneakerpp CMake target to pull in all dependencies.
No crypto types or platform headers are exposed — the public API uses only
standard library types (std::vector, std::string, std::function,
std::chrono, unsigned char[32] wrappers).
| Section | What It Covers |
|---|---|
| How It Works (ELI5) | The library in plain English |
| Getting Started | Creating a node, connecting, sending messages |
| Key Types | PublicKeyBytes, SecretKeyBytes, Keypair, PeerId |
| Capabilities | Peer capability advertisement |
| Enums | PeerEvent, Error, StartResult, LogLevel |
| PeerInfo | Per-peer connection metadata |
| Metrics | Runtime counters and statistics |
| Config | Node configuration options |
| Node | The main API surface — lifecycle, messaging, callbacks |
| Internal Module Guides | Links to per-layer documentation |
| References | Protocol specs and further reading |
Imagine you want to set up a walkie-talkie network where:
- Anyone can join by turning on their radio
- Every conversation is encrypted end-to-end
- Nobody can tell who is talking to whom, or even whether a message is real data or just static
- Radios automatically find each other, even through walls (NAT)
- If someone starts jamming or misbehaving, the network kicks them out
THE SNEAKER++ NETWORK
┌──────────┐ ┌──────────┐
│ Node A │◄────────►│ Node B │
│ (you) │ Noise │ │
└────┬─────┘ XX └────┬─────┘
│ encrypted │
│ UDP channel │
│ │
┌────▼─────┐ ┌────▼─────┐
│ Node C │◄────────►│ Node D │
│ │ │ (behind │
└──────────┘ │ NAT) │
└──────────┘
Here's what happens when your application starts a node:
1. You create a Config with your protocol ID and preferences.
The protocol ID is like a radio frequency — only nodes with the
same ID can talk to each other.
2. You call Node::create(config) and get back a Node object.
This allocates all internal state but doesn't touch the network yet.
3. You register callbacks: on_message, on_peer_event, on_error.
These are your "event handlers" — the library calls them when
something interesting happens.
4. You call node.start(). The node:
- Binds a UDP socket on your chosen port (or a random one)
- Launches IO, timer, and discovery threads
- Starts looking for peers via multicast, DGA/DNS, and manual list
- Begins the Noise XX handshake with each discovered peer
5. Once a handshake completes, the peer is "connected":
- All traffic is encrypted with ChaCha20-Poly1305
- When padding is enabled (default), every packet is padded to the current path MTU
- A fresh rekey happens every 2 minutes
- Chaff packets fill the gaps between real messages
- Each channel is a reliable ordered byte stream with congestion control
6. You send and receive messages through the Node API:
- send() — direct message to one peer on a channel
- broadcast() — send to all connected peers on a channel
All sends are reliable and ordered within their channel.
7. When you're done, call node.shutdown(). All threads stop,
all connections close gracefully, all state is discarded.
There is no persistent state — clean start every boot.
#include <sneaker.hpp>
#include <iostream>
int main()
{
sneaker::Config config;
config.protocol_id = "my-chat-app";
config.listen_port = 9000;
auto node = sneaker::Node::create(config);
// Handle incoming messages
node.on_message([](const sneaker::PeerId &from, uint8_t channel,
const uint8_t *data, size_t len) {
std::cout << "Received " << len << " bytes on channel " << (int)channel << "\n";
});
// Handle peer lifecycle
node.on_peer_event([](const sneaker::PeerId &peer,
sneaker::PeerEvent event) {
if (event == sneaker::PeerEvent::CONNECTED)
std::cout << "New peer connected\n";
});
auto result = node.start();
if (result != sneaker::StartResult::OK)
return 1;
// ... your application logic here ...
node.shutdown();
}FUNCTION lan_discovery():
// Alice and Bob are on the same LAN subnet. Neither knows the
// other's IP address. Both start nodes with the same protocol_id
// and local multicast enabled (the default).
// --- Step 1: Both nodes start ---
// Alice binds UDP on port 9000. Bob binds on port 9001.
// Both join multicast group 239.255.77.77:7777.
// --- Step 2: Multicast announcement ---
// Alice's discovery thread sends a 10-byte multicast packet:
// magic "SNKM" (4B) + protocol_hash (4B) + port 9000 (2B)
//
// Bob's discovery thread receives it. The protocol_hash matches,
// so Bob now knows Alice is at 192.168.1.10:9000.
// --- Step 3: Noise XX handshake ---
// Bob sends Noise msg1 (ephemeral public key) to Alice.
// Alice replies with msg2 (her ephemeral + encrypted static key).
// Bob sends msg3 (his encrypted static key + optional payload).
// After 3 messages, both have authenticated each other and
// derived send/receive CipherState pairs with forward secrecy.
// --- Step 4: Connected ---
// Both nodes fire PeerEvent::CONNECTED.
// Alice can now call:
// alice.send(bob_id, data, len) // channel 0 (default)
// alice.send(bob_id, data, len, 1) // channel 1
// alice.broadcast(data, len) // all peers, channel 0
//
// Every packet is encrypted, padded to the current path MTU, and tagged
// with a 16-byte Poly1305 MAC. Congestion control, loss detection,
// and retransmission are handled automatically per channel.
Header: sneaker.hpp
Namespace: sneaker
A 32-byte wrapper around a Noise static public key. This is the peer's identity — it never changes across connections (unless the peer generates a new keypair).
struct PublicKeyBytes {
unsigned char data[32];
bool operator==(const PublicKeyBytes &other) const;
bool operator!=(const PublicKeyBytes &other) const;
bool operator<(const PublicKeyBytes &other) const;
};A 32-byte wrapper around a Noise static secret key. Never transmitted over the wire — used only for handshake signing.
struct SecretKeyBytes {
unsigned char data[32];
};A matched public/secret key pair. Generate one with sneaker::generate_keypair():
struct Keypair {
PublicKeyBytes public_key;
SecretKeyBytes secret_key;
};
// Generate a random keypair (CSPRNG-backed)
sneaker::Keypair kp = sneaker::generate_keypair();Type alias for PublicKeyBytes. Every connected peer is identified by
their Noise static public key:
using PeerId = PublicKeyBytes;Type alias for std::vector<uint8_t>. Used for message payloads:
using Bytes = std::vector<uint8_t>;Namespace: sneaker
An 8-bit flag set for advertising what a node supports. Used to filter peer selection — a node only connects to peers with matching capabilities.
struct Capabilities {
uint8_t bits = 0;
void set(uint8_t flag); // Set a single capability bit (0-7)
bool supports(uint8_t flag); // Check if a capability is set
void set_all(); // Set all 8 bits
};sneaker::Config config;
// This node supports capabilities 0 (chat) and 2 (file transfer)
config.capabilities.set(0);
config.capabilities.set(2);
// Map channel 0 to capability bit 0 (chat)
// Map channel 1 to capability bit 2 (file transfer)
config.channel_to_capability[0] = 0;
config.channel_to_capability[1] = 2;Peer lifecycle events delivered through the on_peer_event callback:
| Value | Meaning |
|---|---|
CONNECTED |
Noise handshake completed, peer is ready for messaging |
DISCONNECTED_GRACEFUL |
Peer sent a DISCONNECT message |
DISCONNECTED_TIMEOUT |
Peer failed to respond to keepalive pings |
DISCONNECTED_EVICTED |
Peer removed due to low reputation score |
DISCONNECTED_BANNED |
Peer banned for protocol violations |
HANDSHAKE_FAILED |
Noise handshake did not complete (bad keys, timeout, etc.) |
BACKPRESSURED |
Peer's send window is full — slow down |
BACKPRESSURE_CLEARED |
Peer's send window has space — safe to resume |
Fatal or significant errors delivered through the on_error callback:
| Value | Meaning |
|---|---|
BIND_FAILED |
Could not bind the UDP socket to the configured port |
BOOTSTRAP_FAILED |
All discovery mechanisms failed to find any peers |
THREAD_POOL_EXHAUSTED |
Worker thread pool rejected a task (queue full) |
Return value from Node::start():
| Value | Meaning |
|---|---|
OK |
Node started successfully |
BIND_FAILED |
UDP socket bind failed (port in use, permissions, etc.) |
INVALID_CONFIG |
Configuration validation failed |
Severity levels for the on_log callback:
| Value | Typical Content |
|---|---|
TRACE |
Per-packet details, nonce values, timer ticks |
DEBUG |
Handshake steps, peer scoring changes, congestion events |
INFO |
Peer connections/disconnections, bootstrap results |
WARN |
MAC failures, congestion events, PTO expirations |
ERROR |
Bind failures, thread pool exhaustion |
Per-peer connection metadata returned by Node::connected_peers():
| Field | Type | Description |
|---|---|---|
id |
PeerId |
Peer's Noise static public key |
observed_endpoint |
std::string |
IP:port as seen by this node |
is_public |
bool |
True if peer is directly reachable (not behind NAT) |
is_inbound |
bool |
True if this peer initiated the connection |
capabilities |
Capabilities |
Capability bits advertised by this peer |
avg_rtt |
milliseconds |
Average round-trip time from RTT estimator |
score |
float |
Reputation score (higher = better) |
bytes_sent |
uint64_t |
Total bytes sent to this peer |
bytes_received |
uint64_t |
Total bytes received from this peer |
connected_since |
milliseconds |
Timestamp when handshake completed |
cwnd |
size_t |
Current congestion window size (bytes) |
bytes_in_flight |
size_t |
Unacknowledged bytes currently in flight |
smoothed_rtt_ms |
float |
Smoothed round-trip time (ms) |
current_mtu |
size_t |
Current path MTU from PLPMTUD |
Runtime counters exposed via Node::metrics(). All counters start at zero
and are never reset — use deltas for rate calculation.
const sneaker::Metrics &m = node.metrics();
std::cout << "Packets sent: " << m.packets_sent << "\n";
std::cout << "Congestion events: " << m.congestion_events << "\n";| Category | Counters |
|---|---|
| Connections | handshakes_initiated, handshakes_completed, handshakes_failed, handshakes_rejected |
| Traffic | bytes_sent, bytes_received, packets_sent, packets_received |
| Stream Transport | messages_received, bytes_queued, stream_bytes_sent, stream_bytes_delivered, packets_retransmitted, packets_acked, congestion_events, pto_expirations |
| Chaff | chaff_packets_sent |
| Security | invalid_macs, protocol_violations |
| Peers | connected_inbound, connected_outbound, banned_count, peers_evicted |
| Rekeys | rekeys_completed |
The Config struct controls all aspects of a node's behavior. Every field
has a sensible default — you only need to set what you want to change.
| Field | Type | Default | Description |
|---|---|---|---|
protocol_id |
string |
"sneaker-default" |
Network identifier — nodes with different IDs cannot talk |
static_identity |
Keypair |
— | Persistent identity keypair |
has_static_identity |
bool |
false |
If false, generates an ephemeral keypair each boot |
listen_port |
uint16_t |
0 |
UDP port to bind (0 = OS assigns) |
| Field | Type | Default | Description |
|---|---|---|---|
target_peer_count |
size_t |
12 |
Target number of outbound connections |
max_inbound |
size_t |
32 |
Maximum inbound connections |
max_outbound |
size_t |
12 |
Maximum outbound connections |
max_per_ip |
size_t |
3 |
Maximum connections from a single IP address |
| Field | Type | Default | Description |
|---|---|---|---|
keepalive_interval |
Duration |
25s | Ping interval for liveness detection |
dead_peer_timeout |
Duration |
120s | Disconnect after this long without a pong |
rekey_interval |
Duration |
2min | In-tunnel rekey frequency |
hole_punch_timeout |
Duration |
5s | Give up hole punching after this long |
peer_evaluate_interval |
Duration |
30s | Scoring and eviction check frequency |
| Field | Type | Default | Description |
|---|---|---|---|
max_connection_data |
size_t |
4 MB | Per-peer total send window across all channels |
max_stream_data |
size_t |
1 MB | Per-channel send window |
max_ack_delay |
Duration |
25ms | Maximum delay before forcing an ACK |
max_write_queue_bytes |
size_t |
16 MB | Per-peer app-to-IO queue cap |
| Field | Type | Default | Description |
|---|---|---|---|
max_relayed_connections |
size_t |
3 |
Max simultaneous relay sessions |
max_concurrent_handshakes |
size_t |
20 |
Max simultaneous in-progress handshakes |
socket_recv_buffer_size |
size_t |
4 MB | OS socket receive buffer size (0 = OS default) |
socket_send_buffer_size |
size_t |
1 MB | OS socket send buffer size (0 = OS default) |
uniform_packet_size |
bool |
true |
Pad all packets to the current path MTU for traffic analysis resistance |
| Field | Type | Default | Description |
|---|---|---|---|
enable_dga_discovery |
bool |
true |
Use DGA + DNS TXT for bootstrap |
enable_local_multicast |
bool |
true |
Use LAN multicast for local discovery |
manual_peers |
vector<string> |
{} |
Explicit peer endpoints ("ip:port") |
bootstrap_signers |
vector<PublicKeyBytes> |
{} |
Trusted keys for DNS bootstrap record verification |
dga_tlds |
vector<string> |
.com .net .org |
TLDs for DGA domain generation |
dns_resolvers |
vector<string> |
Cloudflare, Google, Quad9 | DNS resolvers for TXT queries |
| Field | Type | Default | Description |
|---|---|---|---|
chaff_interval |
Duration |
200ms | Chaff packet interval (0 = disable) |
eviction_threshold |
float |
30.0 |
Score below which peers are evicted |
initial_ban_duration |
Duration |
5min | How long a banned peer stays banned |
worker_threads |
size_t |
0 |
Worker thread count (0 = hardware_concurrency - 1) |
capabilities |
Capabilities |
all zeros | Capability bits this node advertises |
min_log_level |
LogLevel |
INFO |
Minimum severity for log callbacks |
Header: sneaker.hpp
Namespace: sneaker
Pattern: Pimpl (pointer-to-implementation) — NodeImpl is defined in src/mesh/node.hpp
The Node class is the entire public interface. It is move-only (no copy).
// Create a node (does not touch the network)
auto node = sneaker::Node::create(config);
// Start the node (binds socket, launches threads, begins discovery)
sneaker::StartResult result = node.start();
// Stop the node (closes connections, joins threads, discards state)
node.shutdown();// Get this node's Noise static public key (its PeerId)
sneaker::PeerId my_id = node.local_id();All sends are reliable and ordered within a channel. The QUIC-inspired stream transport handles congestion control, loss detection, and retransmission automatically.
// Direct message to one peer (returns false if flow control limit reached
// or peer unknown/disconnected)
bool ok = node.send(peer_id, data, len); // channel 0 (default)
bool ok = node.send(peer_id, data, len, channel); // specific channel (0-127)
// Send to all connected peers (returns false if no peers accepted the write)
bool ok = node.broadcast(data, len); // channel 0 (default)
bool ok = node.broadcast(data, len, channel); // specific channel
// Check if all send buffers have drained
bool empty = node.send_queues_empty();Channels: Application messages use channels 0-127. Each channel is an independent reliable ordered stream with its own flow control window.
// Incoming message handler
node.on_message([](const sneaker::PeerId &from, uint8_t channel,
const uint8_t *data, size_t len) {
// Called for every received application message
});
// Peer lifecycle events
node.on_peer_event([](const sneaker::PeerId &peer, sneaker::PeerEvent event) {
// CONNECTED, DISCONNECTED_*, HANDSHAKE_FAILED, BACKPRESSURED, etc.
});
// Fatal/significant errors
node.on_error([](sneaker::Error err) {
// BIND_FAILED, BOOTSTRAP_FAILED, THREAD_POOL_EXHAUSTED
});
// Per-peer backpressure signals
node.on_backpressure([](const sneaker::PeerId &peer, bool pressured) {
// true = slow down sending to this peer
// false = safe to resume
});
// Structured logging
node.on_log([](sneaker::LogLevel level, const std::string &msg) {
std::cerr << "[" << (int)level << "] " << msg << "\n";
});// List all connected peers with metadata
std::vector<sneaker::PeerInfo> peers = node.connected_peers();
// Runtime counters
const sneaker::Metrics &m = node.metrics();The implementation is organized into six layers, each with its own README documenting internal types, state machines, and design rationale:
| Layer | Directory | README | Role |
|---|---|---|---|
| Platform | src/platform/ |
README | OS-agnostic UDP, threading, CSPRNG, multicast |
| Noise | src/noise/ |
README | Noise XX handshake, symmetric encryption |
| Transport | src/transport/ |
README | QUIC-style streams, framing, congestion, loss detection, chaff, rekey |
| Mesh | src/mesh/ |
README | Node lifecycle, peer management, peer exchange |
| Discovery | src/discovery/ |
README | DGA, DNS bootstrap, multicast, orchestration |
| NAT | src/nat/ |
README | Introduction, hole punching, relay |
The full design specification is in plan.md.
| Topic | Link |
|---|---|
| Noise Protocol Framework | Noise Protocol Specification, Revision 34 |
| Noise XX pattern | Noise Explorer — XX pattern analysis |
| X25519 key exchange | RFC 7748 — Elliptic Curves for Security |
| ChaCha20-Poly1305 | RFC 8439 — ChaCha20 and Poly1305 for IETF Protocols |
| BLAKE2b | RFC 7693 — The BLAKE2 Cryptographic Hash |
| QUIC Transport | RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport |
| QUIC Loss Detection | RFC 9002 — QUIC Loss Detection and Congestion Control |
| UDP hole punching | Ford, Srisuresh, Kegel, 2005 — Peer-to-Peer Communication Across NATs |