Skip to content

Latest commit

 

History

History
584 lines (444 loc) · 20.8 KB

File metadata and controls

584 lines (444 loc) · 20.8 KB

Sneaker++ Public API Reference

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


Table of Contents

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

How It Works (ELI5)

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.

Getting Started

Minimal Node

#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();
}

Scenario: Two Nodes Find Each Other on a LAN

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.

Key Types

Header: sneaker.hpp Namespace: sneaker

PublicKeyBytes

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;
};

SecretKeyBytes

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];
};

Keypair

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();

PeerId

Type alias for PublicKeyBytes. Every connected peer is identified by their Noise static public key:

using PeerId = PublicKeyBytes;

Bytes

Type alias for std::vector<uint8_t>. Used for message payloads:

using Bytes = std::vector<uint8_t>;

Capabilities

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
};

Usage

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;

Enums

PeerEvent

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

Error

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)

StartResult

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

LogLevel

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

PeerInfo

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

Metrics

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

Config

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.

Identity & Network

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)

Peer Limits

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

Timing

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

Stream Transport

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

Limits

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

Discovery

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

Other

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

Node

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

Lifecycle

// 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();

Identity

// Get this node's Noise static public key (its PeerId)
sneaker::PeerId my_id = node.local_id();

Messaging

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.

Callbacks

// 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";
});

Queries

// List all connected peers with metadata
std::vector<sneaker::PeerInfo> peers = node.connected_peers();

// Runtime counters
const sneaker::Metrics &m = node.metrics();

Internal Module Guides

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.


References

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