πŸ€ librats

June 23, 2026 Β· View on GitHub

MIT License C++17 Build Status Release npm

A high-performance, lightweight peer-to-peer networking library with C++, C, Node.js, Java, Python, and Android support

librats is a modern P2P networking library designed for superior performance and simplicity. Built from the ground up in C++17 with comprehensive language bindings, it provides enterprise-grade P2P networking with minimal overhead and maximum efficiency.

Official Website: https://librats.com

🧱 Design at a glance

librats is built around a small, predictable core (Node) and a set of opt-in subsystems you attach explicitly. A bare Node is just the secure transport: an encrypted TCP channel (Noise_XX) with a self-certifying peer identity, manual dialing, and raw channel messaging. Everything else β€” discovery, pub/sub, typed messaging, file transfer, liveness, NAT port mapping, reconnection β€” is a Subsystem you add before start(). You pay only for what you attach, and the core stays small and easy to reason about.

librats::NodeConfig config;
config.listen_port = 8080;
librats::Node node(config);

// attach only the capabilities you need
node.add_subsystem(std::make_unique<librats::PubSub>());
node.add_subsystem(std::make_unique<librats::DhtDiscovery>(dht_config));

node.start();

✨ Key Features

Core

  • Native C++17 implementation for maximum performance
  • Cross-platform support (Windows, Linux, macOS, Android)
  • Shared-nothing reactor transport β€” connections are sharded across reactor threads with no cross-thread locking on the hot path
  • Self-certifying identity: every node has a Curve25519 keypair; its PeerId is its public key, so peers authenticate each other with no PKI or central authority
  • Stable identity persistence: point a node at a data_dir and its keypair (and therefore its PeerId) survives restarts
  • Composable subsystems: opt-in plugins attached to a Node; a bare node neither discovers peers nor reconnects on its own

Discovery & Networking

  • DHT Discovery: peer discovery over a Kademlia DHT, fully compatible with the BitTorrent Mainline DHT β€” the largest distributed hash table in the world, with millions of active nodes (IPv4 + IPv6 / BEP 32)
  • mDNS Discovery: automatic local-network peer discovery with service advertisement
  • IPv4/IPv6 Dual Stack: bind dual-stack by default; full support for modern internet protocols
  • Peer Exchange (PEX): peers gossip known addresses to grow the mesh
  • Automatic Reconnection: re-dials dropped peers with exponential backoff; targets persist to disk when a data_dir is set
  • Network-change awareness: an optional monitor detects interface/route changes and notifies subsystems so they can re-announce and renew port mappings

Pub/Sub (GossipSub)

  • Scalable publish-subscribe with mesh networking
  • Topic-based communication with per-topic subscriptions
  • Message validation: configurable per-topic validators to accept/reject/ignore messages

I/O Multiplexing

  • Platform-optimal polling behind one abstraction:
    • Linux β€” epoll (O(1) per event)
    • macOS/BSD β€” kqueue (O(1) per event)
    • Windows β€” IOCP (true async completion, O(1) per event)

File Transfer

  • Streaming transfers: files streamed in order over the reliable peer connection β€” bounded memory regardless of file size
  • Directory transfer: whole directory trees sent recursively as one transfer
  • Backpressure: windowed flow control keeps the sender from outrunning the receiver
  • Integrity: per-chunk CRC32 plus a whole-file SHA-256 verified before delivery
  • Atomic delivery: data lands in a temp file and is renamed to its destination only after verification
  • Transfer control: pause, resume, and cancel from either side, with real-time progress callbacks
  • Offer/Accept model: incoming transfers are offered to the application, which accepts (with a destination) or rejects

Security

  • Noise Protocol encryption (Noise_XX): Curve25519 key exchange + ChaCha20-Poly1305 AEAD on every connection by default
  • Mutual authentication: both peers prove possession of the private key behind their PeerId
  • Perfect forward secrecy: per-session ephemeral keys
  • Protocol binding: your app's protocol id (e.g. "myapp/1.0") is bound into the handshake prologue, so nodes from different apps cryptographically cannot cross-connect
  • Plaintext option: select Security::Plaintext for local debugging or trusted networks

NAT Traversal

  • Automatic port forwarding: built-in UPnP IGD and NAT-PMP β€” the PortMappingService asks the router to forward the listen port on startup (both backends run in parallel; whichever the router supports wins), so peers behind a NAT can accept inbound connections with zero manual configuration. Mappings are refreshed automatically and removed on stop().
  • STUN: public-IP discovery used by the DHT (BEP-42 node-id derivation and external-address reporting)

Distributed Storage (optional, requires RATS_STORAGE)

  • Key-value storage: typed string / int64 / double / binary / JSON values
  • Automatic P2P synchronization across connected peers via GossipSub
  • Last-Write-Wins (LWW) conflict resolution based on timestamps
  • Disk persistence with an efficient binary format
  • Change notifications for local and remote updates

Multi-Language Support

  • Native C++17: core implementation with the full feature set
  • C API (bindings/rats.h): clean opaque-pointer C ABI β€” the foundation for all FFI bindings
  • Node.js: native addon with async/await and TypeScript definitions (npm package)
  • Java/Android: JNI wrapper with a high-level Java API
  • Python: ctypes-based package with asyncio support

πŸš€ Quick Start

Examples use the C++ Node API. The equivalent C API (rats_*) is shown in the C API section.

1. Basic P2P connection

#include "node/node.h"
#include <iostream>

using namespace librats;

int main() {
    NodeConfig config;
    config.listen_port = 8080;       // 0 = ephemeral
    config.bind_address = "::";      // dual-stack (IPv6 + IPv4-mapped); the default

    Node node(config);

    // Register events BEFORE start(). They run on a reactor thread.
    node.on_peer_connected([](const Peer& peer) {
        std::cout << "[+] peer connected: " << peer.id().short_hex() << "\n";
    });
    node.on("chat", [](const Peer& peer, ByteView data) {
        std::cout << peer.id().short_hex() << ": "
                  << std::string(reinterpret_cast<const char*>(data.data()), data.size()) << "\n";
    });

    if (!node.start()) {
        std::cerr << "failed to start node\n";
        return 1;
    }
    std::cout << "node " << node.local_id().short_hex()
              << " listening on " << node.listen_port() << "\n";

    // Dial another peer (non-blocking; connects asynchronously).
    node.connect("127.0.0.1", 8081);

    // Send raw bytes on a named channel to every connected peer.
    node.broadcast("chat", ByteView(std::string("Hello from librats!")));

    std::string line;
    while (std::getline(std::cin, line)) node.broadcast("chat", ByteView(line));

    node.stop();
    return 0;
}

2. Custom protocol & stable identity

NodeConfig config;
config.listen_port = 8080;
config.protocol = "my_app/1.0";       // bound into the handshake β€” only peers with
                                      // the same protocol id can connect
config.data_dir = "./node-data";      // persist identity.key β†’ stable PeerId across restarts

Node node(config);
node.start();

std::cout << "protocol: " << node.protocol() << "\n";
std::cout << "peer id:  " << node.local_id().to_hex() << "\n";

Two nodes whose protocol id differs cannot complete a handshake β€” a cheap, cryptographically-enforced way to keep separate apps (or app versions) from cross-connecting. The id is an opaque string compared for exact equality; by convention "<name>/<version>". See Private Network Formation.

3. Typed JSON messaging

Attach the MessageJson subsystem and reach it through node.json().

#include "node/node.h"
#include "subsystems/message_json.h"

Node node(NodeConfig{/*listen_port=*/8080});
node.add_subsystem(std::make_unique<MessageJson>());

// Handlers are additive and keyed by message type. `from` is the authenticated PeerId.
node.json()->on("chat", [](const PeerId& from, const nlohmann::json& data) {
    std::cout << "[chat] " << from.short_hex() << ": " << data.value("text", "") << "\n";
});

node.start();

// Broadcast / direct send.
node.json()->send("chat", nlohmann::json{{"text", "Hello, P2P chat!"}});
node.json()->send(some_peer_id, "chat", nlohmann::json{{"text", "private hi"}});

4. GossipSub publish-subscribe

#include "node/node.h"
#include "subsystems/pubsub.h"

Node node(NodeConfig{8080});
auto* pubsub = node.add_subsystem(std::make_unique<PubSub>());

pubsub->subscribe("news", [](const PeerId& from, const std::string& topic, ByteView data) {
    std::cout << "[" << topic << "] " << from.short_hex() << ": "
              << std::string(reinterpret_cast<const char*>(data.data()), data.size()) << "\n";
});

node.start();

pubsub->publish("news", ByteView(std::string("Breaking: librats is awesome!")));

std::cout << "subscribers in 'news': " << pubsub->peers_for_topic("news").size() << "\n";

5. File and directory transfer

#include "node/node.h"
#include "subsystems/file_transfer.h"

Node node(NodeConfig{8080});
auto* files = node.add_subsystem(std::make_unique<FileTransfer>("./downloads"));  // temp dir

// Incoming offers must be accepted (with a destination) or rejected.
files->on_offer([&](const FileTransfer::Offer& offer) {
    std::cout << "[file] offer from " << offer.from.short_hex() << ": " << offer.name
              << " (" << offer.size << " bytes)\n";
    if (offer.size < 100 * 1024 * 1024)
        files->accept(offer.from, offer.id, "./downloads/" + offer.name);
    else
        files->reject(offer.from, offer.id);
});
files->on_progress([](const FileTransfer::Progress& p) { /* p.bytes_transferred / p.total_bytes */ });
files->on_complete([](uint64_t id, bool ok, const std::string& path) {
    std::cout << "[file] transfer " << id << (ok ? " complete: " : " FAILED: ") << path << "\n";
});

node.start();

// Push a file / directory to a connected peer (returns a transfer id, 0 on failure).
uint64_t id  = files->send_file(peer_id, "my_file.txt");
uint64_t dir = files->send_directory(peer_id, "./my_folder");
// Control either side: files->pause(peer, id) / resume(...) / cancel(...)

6. Security

Encryption is on by default β€” every connection runs Noise_XX (Curve25519 + ChaCha20-Poly1305) with mutual authentication. There is nothing to enable.

NodeConfig config;
config.listen_port = 8080;
config.security = NodeConfig::Security::Noise;   // default; Plaintext for trusted/debug nets
config.data_dir = "./node-data";                 // persist the Noise keypair β†’ stable PeerId

Node node(config);
node.start();
// node.local_id() is the node's static public key β€” peers authenticate it during the handshake.

7. NAT traversal (UPnP / NAT-PMP)

Attach PortMappingService to forward the listen port automatically on startup. Both UPnP IGD and NAT-PMP are attempted in parallel; whichever the router supports wins. The mapping is refreshed automatically and removed on stop().

#include "node/node.h"
#include "subsystems/port_mapping_service.h"

Node node(NodeConfig{8080});
auto* portmap = node.add_subsystem(std::make_unique<PortMappingService>());
node.start();

// Public endpoint as seen from outside the NAT (if a mapping succeeded).
if (auto pub = portmap->mapped_public_address())
    std::cout << "public: " << pub->first << ":" << pub->second << "\n";

8. Peer discovery (DHT + mDNS) and reconnection

#include "node/node.h"
#include "subsystems/dht_discovery.h"
#include "subsystems/mdns_discovery.h"
#include "subsystems/reconnection.h"

NodeConfig config;
config.listen_port = 8080;
config.data_dir = "./node-data";
Node node(config);

// Wide-area discovery via the BitTorrent Mainline DHT (IPv4 + IPv6).
DhtDiscovery::Config dc;
dc.data_dir = config.data_dir;          // co-locate the routing tables with identity + peers
node.add_subsystem(std::make_unique<DhtDiscovery>(std::move(dc)));

// Local-network discovery.
node.add_subsystem(std::make_unique<MdnsDiscovery>());

// Auto-reconnect dropped peers with exponential backoff; persist targets to disk.
ReconnectionService::Config rc;
rc.store_path = config.data_dir + "/peers.txt";
rc.max_attempts = 10;
auto* reconnect = node.add_subsystem(std::make_unique<ReconnectionService>(rc));

node.start();
reconnect->add(Address{"203.0.113.7", 8080});   // keep this target connected

9. Liveness (RTT probing)

#include "subsystems/ping_service.h"

auto* ping = node.add_subsystem(std::make_unique<PingService>());
node.start();
// ...later:
if (auto rtt = ping->last_rtt(peer_id))
    std::cout << "rtt = " << rtt->count() << "ms\n";

10. Distributed storage (requires RATS_STORAGE)

#include "storage/storage.h"

auto* storage = node.add_subsystem(std::make_unique<StorageManager>());
node.start();

storage->put("greeting", "hello");                       // syncs to connected peers via GossipSub
if (auto v = storage->get_string("greeting")) std::cout << *v << "\n";

πŸ“– API Documentation

Node β€” the entry point

Node (in node/node.h) owns the reactor pool, the security provider, the peer directory and the message router. connect/send/broadcast are non-blocking and thread-safe; event callbacks run on a reactor thread, so register them before start().

// Construction
explicit Node(NodeConfig config);

// Lifecycle
bool start();                  // open listener + reactors + subsystems; false if bind fails
void stop();                   // stop subsystems (reverse order), close connections, join

// Identity & protocol
const PeerId&      local_id() const;          // our self-certifying id (== public key)
uint16_t           listen_port() const;       // actual bound port (when config requested 0)
const std::string& protocol() const;          // app protocol id bound into the handshake

// Subsystems (attach BEFORE start(); the node owns them and returns a non-owning pointer)
template <class T> T* add_subsystem(std::unique_ptr<T> subsystem);
template <class T> T* subsystem();            // typed lookup, nullptr if not attached
MessageJson*          json();                 // shortcut for subsystem<MessageJson>()

// Connections
void   connect(const Address& address);
void   connect(const std::string& host, uint16_t port);
size_t peer_count() const;
std::vector<PeerInfo> peers() const;          // snapshot: id, addresses, direction
std::optional<Peer>   peer(const PeerId& id);
std::vector<Address>  observed_addresses() const;  // our addresses as peers report them

// Peer admission limit (0 = unlimited; guards inbound, not our own dials)
size_t max_peers() const;
void   set_max_peers(size_t n);
bool   peer_limit_reached() const;

// Messaging (raw bytes on a named channel)
void send(const PeerId& to, std::string_view channel, ByteView payload);
void broadcast(std::string_view channel, ByteView payload);

// Events (additive; run on a reactor thread)
void on_peer_connected(PeerEventHandler cb);     // (const Peer&)
void on_peer_disconnected(PeerDisconnectHandler cb);  // (const PeerId&)
void on(std::string_view channel, MessageRouter::Handler cb);  // (const Peer&, ByteView)

// Node-scoped coordination shared with subsystems
EventBus&        events();      // fire-and-forget, one→many (e.g. NetworkChanged)
ServiceRegistry& services();    // targeted capability lookup, one→one

NodeConfig

struct NodeConfig {
    uint16_t    listen_port = 0;            // 0 = ephemeral; ignored if !enable_listen
    bool        enable_listen = true;       // false = dial-only (no listener)
    std::string bind_address = "";          // "" / "::" dual-stack, "0.0.0.0", or an IP literal
    size_t      reactor_threads = 1;        // 1 handles thousands of peers; more shards cores
    size_t      max_peers = 0;              // 0 = unlimited (guards inbound only)
    enum class Security { Noise, Plaintext };
    Security    security = Security::Noise; // Noise_XX by default
    std::string protocol = "librats/1.0";   // app id bound into the handshake; must match to connect
    std::string data_dir = "";              // "" = ephemeral identity; else identity.key persists
    bool        enable_network_monitor = true;  // watch host network changes β†’ NetworkChanged
};

Subsystems

Each subsystem is attached with node.add_subsystem(std::make_unique<T>(...)) before start(). A bare node has none of these.

SubsystemHeaderWhat it adds
PubSubsubsystems/pubsub.hGossipSub topics: subscribe / unsubscribe / publish, per-topic validators
MessageJsonsubsystems/message_json.hTyped JSON messaging: on / once / off / send; reached via node.json()
FileTransfersubsystems/file_transfer.hPush file/dir transfer: send_file / send_directory / accept / reject / pause / resume / cancel
DhtDiscoverysubsystems/dht_discovery.hWide-area discovery over the BitTorrent Mainline DHT (IPv4 + IPv6)
MdnsDiscoverysubsystems/mdns_discovery.hLocal-network discovery + advertisement
PingServicesubsystems/ping_service.hPeriodic liveness ping/pong + last_rtt(id)
ReconnectionServicesubsystems/reconnection.hAuto-reconnect with exponential backoff; persistent targets
PortMappingServicesubsystems/port_mapping_service.hUPnP IGD + NAT-PMP automatic port forwarding
PeerExchangesubsystems/peer_exchange.hPEX: gossip known peer addresses to grow the mesh
StorageManagerstorage/storage.hDistributed key-value store (requires RATS_STORAGE)

C API (bindings/rats.h)

The canonical opaque-pointer C ABI β€” the foundation for every language binding. A rats_t wraps a Node. Fallible calls return rats_error_t (RATS_OK == 0); pure getters return their value directly. Subsystems are opt-in: enable each with the matching rats_enable_*() before rats_start(). Strings returned by the library are heap-allocated β€” free them with rats_string_free().

#include "bindings/rats.h"
#include <stdio.h>

static void on_connected(void* user, const char* peer_id_hex) {
    printf("[+] connected: %s\n", peer_id_hex);
}
static void on_chat(void* user, const char* peer_id_hex, const void* data, size_t len) {
    printf("%s: %.*s\n", peer_id_hex, (int)len, (const char*)data);
}

int main(void) {
    rats_t node = rats_create(8080);

    rats_on_peer_connected(node, on_connected, NULL);
    rats_on(node, "chat", on_chat, NULL);

    rats_enable_pubsub(node);          // before start
    rats_enable_dht(node, 0, NULL);

    if (rats_start(node) != RATS_OK) return 1;

    rats_connect(node, "127.0.0.1", 8081);
    rats_broadcast(node, "chat", "hello", 5);

    /* ... run ... */
    rats_stop(node);
    rats_destroy(node);
    return 0;
}

Key entry points: rats_create / rats_create_config / rats_config_default / rats_destroy, rats_start / rats_stop, rats_connect, rats_send / rats_broadcast, rats_on / rats_on_peer_connected / rats_on_peer_disconnected, rats_enable_{dht,mdns,pubsub,json,file_transfer,ping,reconnect,port_mapping}, rats_subscribe / rats_publish, rats_on_json / rats_send_json, rats_send_file / rats_accept_file, rats_peer_ids, rats_local_id, rats_protocol, rats_version / rats_version_string / rats_git_describe / rats_abi, rats_set_log_level / rats_set_log_file.

🏒 Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Application                                                        β”‚
β”‚   composes a Node + exactly the subsystems it needs               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Subsystems (opt-in plugins attached to a Node)                    β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚ β”‚  PubSub    β”‚ β”‚ MessageJsonβ”‚ β”‚ FileTransferβ”‚ β”‚ Reconnect  β”‚      β”‚
β”‚ β”‚ (GossipSub)β”‚ β”‚ (typed JSON)β”‚ β”‚  (push)    β”‚ β”‚  Service   β”‚      β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚ β”‚ DhtDiscoveryβ”‚ β”‚MdnsDiscoveryβ”‚ β”‚PingService β”‚ β”‚PortMapping β”‚      β”‚
β”‚ β”‚  (Mainline)β”‚ β”‚  (local)   β”‚ β”‚ (liveness) β”‚ β”‚UPnP/NAT-PMP β”‚      β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚ β”‚ PeerExchange (PEX)       β”‚ β”‚ StorageManager (RATS_STORAGE)β”‚     β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Node core                                                         β”‚
β”‚   peer directory Β· message router Β· EventBus Β· ServiceRegistry    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Security  β€” Noise_XX (Curve25519 + ChaCha20-Poly1305) / plaintext β”‚
β”‚             over a self-certifying PeerId                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Transport β€” shared-nothing reactor pool, per-connection state      β”‚
β”‚             machine, length-prefixed wire framing                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ I/O multiplexing β€” epoll (Linux) Β· kqueue (macOS/BSD) Β· IOCP (Win) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Platform β€” WinSock2/bcrypt (Windows) Β· BSD sockets (Linux/macOS)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The source tree mirrors these layers: src/core, src/util, src/wire, src/transport, src/peer, src/security, src/node, src/subsystems, src/dht, src/mdns, src/nat, src/crypto, src/bittorrent, src/storage, src/bindings.

Frequently Asked Questions (FAQ)

Understanding DHT vs peer connections

librats has two distinct peer systems that serve different purposes:

LayerProtocolPurposeWhere
DHT layerUDP (Kademlia)Peer discovery onlyDhtDiscovery subsystem
Peer connection layerTCP (Noise)Message exchangeNode core: peers(), send, broadcast

Key points:

  • The DHT routing table is NOT your connected peers. It holds DHT nodes (often from the global BitTorrent Mainline DHT) that help you discover peers.
  • Peer connections (node.peers(), node.peer_count()) are the actual authenticated TCP connections used for communication.
  • The DHT is for discovery, not message routing. For messaging, use the Node core (channels), MessageJson, or PubSub.

Private Network Formation

To create a private overlay limited to your application's peers:

  1. Set a unique protocol id before starting:
NodeConfig config;
config.protocol = "my_private_app/1.0";
Node node(config);
node.add_subsystem(std::make_unique<DhtDiscovery>(dht_config));
node.start();   // discovery uses a hash derived from your protocol identity
  1. How it works:

    • DhtDiscovery derives a discovery hash from your protocol identity and announces under it in the global DHT.
    • Only peers with the same protocol id discover each other β€” and even if a stranger dials you, the protocol identity is bound into the Noise handshake, so the connection cannot complete.
    • Once discovered, peers connect over authenticated TCP and grow the mesh via Peer Exchange.
  2. Discovery timing:

    • DHT discovery is asynchronous β€” initial peers typically appear in 1–30 seconds.
    • For fast local testing, attach MdnsDiscovery instead (or as well).

πŸ› οΈ Building

Supported Platforms & Language Bindings

Native C++ Support

PlatformBuild EnvironmentCompilerStatus
WindowsMinGW-w64GCC 7+βœ… Fully Supported
WindowsVisual StudioMSVC 2017+βœ… Fully Supported
LinuxNativeGCC 7+, Clang 5+βœ… Fully Supported
macOSXcode/NativeClang 10+βœ… Fully Supported

Language Bindings & Wrappers

Language/PlatformBinding TypeStatusNotes
C/C++Native Libraryβœ… Fully SupportedCore implementation with the full feature set
Android (NDK)Native C++βœ… Fully SupportedAndroid NDK integration with JNI bindings
Android (Java)JNI Wrapperβœ… Fully SupportedHigh-level Java API for Android apps
Node.jsNative Addonβœ… Fully Supportedasync/await support (npm)
PythonC Extensionβœ… Fully SupportedCPython extension with asyncio integration
RustFFI BindingsπŸ“‹ PlannedSafe bindings with tokio async support
GoCGO BindingsπŸ“‹ FutureCGO wrapper for Go applications
C#/.NETP/InvokeπŸ“‹ Future.NET bindings for Windows/Linux/macOS

Legend: βœ… Fully Supported Β· πŸ”Ά In Development Β· πŸ“‹ Planned/Future/Research

Prerequisites

  • CMake 3.10+
  • C++17 compatible compiler:
    • GCC 7+ (Linux, MinGW)
    • Clang 5+ (macOS, Linux)
    • MSVC 2017+ (Windows)
  • Git (for dependency management)

Building on Linux/macOS

git clone https://github.com/DEgITx/librats.git
cd librats
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)

Building on Windows

git clone https://github.com/DEgITx/librats.git
cd librats
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . --config Release

Build Options

# Disable tests
cmake .. -DRATS_BUILD_TESTS=OFF

# Debug build with full logging
cmake .. -DCMAKE_BUILD_TYPE=Debug

# Release build optimized for performance
cmake .. -DCMAKE_BUILD_TYPE=Release

Complete Build Configuration Options

OptionDefaultDescription
RATS_BUILD_TESTSONBuild unit tests with GoogleTest
RATS_BUILD_EXAMPLESONBuild the rats-client demo application
RATS_ENABLE_ASANOFFEnable AddressSanitizer for memory debugging
RATS_ENABLE_TSANOFFEnable ThreadSanitizer for data-race debugging
RATS_BINDINGSONBuild the C API bindings for FFI support
RATS_CROSSCOMPILINGOFFForce cross-compilation flags
RATS_SHARED_LIBRARYOFFBuild as shared library (.dll/.so/.dylib)
RATS_STATIC_LIBRARYONBuild as static library (.a/.lib)
RATS_SEARCH_FEATURESOFFEnable Rats Search features (BitTorrent / DHT spider)
RATS_STORAGEOFFEnable the distributed key-value storage subsystem

Examples:

# Build as shared library without tests or examples
cmake .. -DRATS_SHARED_LIBRARY=ON -DRATS_STATIC_LIBRARY=OFF \
         -DRATS_BUILD_TESTS=OFF -DRATS_BUILD_EXAMPLES=OFF

# Build with BitTorrent support and debug symbols
cmake .. -DRATS_SEARCH_FEATURES=ON -DCMAKE_BUILD_TYPE=Debug

# Build with distributed storage support
cmake .. -DRATS_STORAGE=ON -DCMAKE_BUILD_TYPE=Release

# Build with all optional features enabled
cmake .. -DRATS_STORAGE=ON -DRATS_SEARCH_FEATURES=ON -DCMAKE_BUILD_TYPE=Release

# Cross-compile for Android (requires NDK)
cmake .. -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
         -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-21 \
         -DRATS_CROSSCOMPILING=ON -DRATS_BUILD_TESTS=OFF

Integrating librats Into Your Application

cmake_minimum_required(VERSION 3.10)
project(MyP2PApp)
set(CMAKE_CXX_STANDARD 17)

include(FetchContent)
FetchContent_Declare(
    librats
    GIT_REPOSITORY https://github.com/DEgITx/librats.git
    GIT_TAG master  # or a specific version/tag
)
set(RATS_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(RATS_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(librats)

add_executable(my_p2p_app main.cpp)
target_link_libraries(my_p2p_app PRIVATE rats)

Method 2: CMake add_subdirectory

# As a git submodule
git submodule add https://github.com/DEgITx/librats.git external/librats
set(RATS_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(RATS_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(external/librats)

add_executable(my_p2p_app main.cpp)
target_link_libraries(my_p2p_app PRIVATE rats)
# Include directories are propagated automatically (use #include "node/node.h").

Required System Libraries

When linking against a pre-built librats, add these system libraries:

PlatformRequired Libraries
Windowsws2_32, iphlpapi, bcrypt
Linuxpthread
macOSpthread
Androidlog

Running Tests

# In the build directory
ctest -j$(nproc) --output-on-failure

# Or run the test binary directly
./bin/librats_tests

Output Files

After building, you'll find:

  • Library: build/lib/librats.a (static library)
  • Executable: build/bin/rats-client (reference/demo application)
  • Tests: build/bin/librats_tests (if RATS_BUILD_TESTS=ON)

🎯 Usage Examples

The reference application

rats-client (built from src/main.cpp) wires up the full set of subsystems so every capability can be exercised from one binary:

# Terminal 1: start a node on port 8080 with DHT + mDNS discovery
./build/bin/rats-client 8080 --dht --mdns

# Terminal 2: start a second node and dial the first
./build/bin/rats-client 8081 --connect 127.0.0.1 8080

Options: --bind <addr>, --data <dir> (stable identity + reconnect store), --connect <host> <port> (repeatable), --dht, --mdns, --upnp, --reconnect, --no-ping. Pub/sub, typed JSON messaging and file transfer are always on. Type /help once running for the interactive command list (/peers, /connect, /sub, /pub, /msg, /file, …).

Minimal chat

#include "node/node.h"
#include "subsystems/message_json.h"
#include <iostream>

using namespace librats;

int main() {
    Node node(NodeConfig{/*listen_port=*/8080});
    node.add_subsystem(std::make_unique<MessageJson>());

    node.json()->on("chat", [](const PeerId& from, const nlohmann::json& d) {
        std::cout << "[" << d.value("user", "?") << "]: " << d.value("text", "") << "\n";
    });

    node.start();

    const std::string user = "User_" + node.local_id().short_hex();
    std::cout << "πŸ€ librats chat β€” type messages, 'quit' to exit\n";

    std::string line;
    while (std::getline(std::cin, line) && line != "quit") {
        if (!line.empty())
            node.json()->send("chat", nlohmann::json{{"user", user}, {"text", line}});
    }
    node.stop();
    return 0;
}

πŸ”§ Persistent State

When a node is given a data_dir, it co-locates its persistent state there:

  • identity.key β€” the node's Noise/Curve25519 private key. Loaded on startup (or generated and saved on first run), giving a stable PeerId across restarts. An empty data_dir means a fresh random identity each run.
  • peers.txt β€” reconnection targets, written by ReconnectionService when configured with a store_path (typically <data_dir>/peers.txt).
  • DHT routing tables β€” persisted by DhtDiscovery when its Config::data_dir is set, so the DHT warm-starts on the next run.

There is no central config.json: configuration is supplied programmatically via NodeConfig and each subsystem's Config.

πŸš€ Benchmark Performance

librats is engineered for resource efficiency, making it ideal for low-power devices, edge computing, and embedded systems where memory and CPU are precious.

Performance Comparison vs libp2p (JavaScript)

Test Environment: AMD Ryzen 7 5700U, 16GB RAM

Metriclibrats (C++17)libp2p (JavaScript)Improvement
Startup Memory~1.6 MB~50-80 MB31-50x less
Memory per Peer~80 KB~4-6 MB50-75x less
Peak Memory (100 peers)~9.4 MB400-600 MB42-64x less
CPU Usage (idle)0-1%15-25%15-25x less
CPU Usage (peak)1-2%80-100%5-16x less

Network Traffic (DHT Discovery)

MetricTraffic
DHT Discovery (idle)~350-450 bytes/sec

DHT discovery uses minimal bandwidth β€” only 350-450 bytes per second during continuous peer discovery β€” making librats ideal for bandwidth-constrained environments and mobile devices.

Why Choose librats?

Performance

  • Native C++17: maximum performance with minimal overhead
  • Shared-nothing reactor: no cross-thread locking on the connection hot path
  • Platform-optimal I/O: epoll / kqueue / IOCP behind one abstraction

Reliability

  • Comprehensive testing: unit and integration tests across all components
  • Memory safety: RAII and smart pointers throughout
  • Cross-platform: consistent behaviour across Windows, Linux, and macOS

Developer Experience

  • Small, predictable core: a bare Node does exactly one thing β€” secure transport
  • Composable subsystems: attach only the capabilities you need
  • Self-certifying identity: authentication with no PKI or central authority
  • Modern C++: takes advantage of C++17 features

Contributing

We welcome contributions! Please see our Contributing Guide for guidelines on code style, development setup, running tests, and submitting pull requests.

Quick Start for Contributors

git clone https://github.com/DEgITx/librats.git
cd librats
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug -DRATS_BUILD_TESTS=ON
make -j$(nproc)
./bin/librats_tests

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

  • nlohmann/json: for the excellent JSON library integration
  • Contributors: everyone who has helped make librats better