πŸ€ librats

August 29, 2026 Β· View on GitHub

MIT License C++17 Build Status Release npm vcpkg

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

librats is a modern P2P networking library written in C++17, with bindings for C, Node.js, Java, Python, React Native, and Android, plus an iOS build of the core. It's designed to be fast and light enough for low-power and embedded devices, while staying simple to build on: you start with a tiny core and add exactly the features you need β€” nothing more.

Official Website: https://librats.com

Used By

Projects and companies building on librats:

rats-search Β Β  uvnc Β Β  rasync

Using librats in production? Open a PR to add your logo here.

✨ Key Features

Core

  • Native C++17 implementation for maximum performance
  • Cross-platform support (Windows, Linux, macOS, Android, iOS)
  • Shared-nothing reactor transport β€” connections are sharded across reactor threads with no cross-thread locking on the hot path
  • TCP and UDP as equals β€” the same encrypted protocol over either wire. The UDP transport is a full ordered/reliable stream (sequencing, selective acks, RTO, congestion + flow control) on one socket shared by all peers, which is what keeps a single NAT mapping open and makes hole punching possible. A dial tries UDP first and races TCP as fallback, so a UDP-hostile network still connects
  • 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: binds dual-stack by default, so a single node accepts both IPv4 and IPv6 peers
  • 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 (for TCP and UDP, which share it) 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().
  • UDP hole punching: when no port forwarding is possible, HolePunch reaches a peer behind a NAT by arranging β€” through a peer both sides already have β€” that the two dial each other at the same instant, so each side's outbound packet opens the mapping the other's needs. The rendezvous is timed from the round trip itself (no clock synchronisation), and the node learns the external endpoint to advertise from its own mesh rather than from a STUN server
  • Relaying: for the pairs a punch cannot reach β€” a symmetric NAT, a network that drops UDP and blocks inbound TCP β€” Relay carries the connection itself through a node both ends already reach. It is a byte stream that is relayed, so the Noise handshake still runs end to end and the relay moves ciphertext it cannot read; a circuit that comes up then tries to upgrade itself to a direct link, and the swap costs the application nothing
  • NAT awareness: several peers' independent views of the node's shared UDP socket say whether its mapping is stable enough to punch through at all (endpoint-independent) or per-destination (symmetric, where punching cannot work) β€” see node.nat_status()
  • 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: N-API native addon (RatsNode) with TypeScript definitions (npm package)
  • Java/Android: JNI wrapper with a high-level Java API (com.librats.RatsNode)
  • Python: ctypes package with a Pythonic RatsNode
  • React Native: react-native/ β€” a Nitro Modules HybridObject implemented once in C++ and shared by iOS and Android, so there is no JNI bridge and no Swift wrapper to keep in sync. Covers messaging, peer events, file transfer and pub/sub
  • iOS: ios/ β€” the core cross-compiles to an XCFramework (device + simulator) and Swift imports the C ABI directly as import LibRats, no shim needed

πŸš€ Quick Start

Everything in librats revolves around one idea: a small, predictable core (Node) plus opt-in subsystems you attach explicitly. A bare Node is just the secure transport β€” an encrypted 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();

Two transports, one API

A connection runs over TCP or UDP, and nothing above the transport can tell the difference: same framing, same Noise handshake, same guarantees. UDP is not a lossy shortcut here β€” librats implements ordered, reliable delivery with congestion and flow control on top of datagrams (sequencing, cumulative + selective acks, RFC 6298 retransmission timing, Reno congestion control).

Both are enabled by default and bind the same port, so one advertised address is dialable either way. A dial tries UDP first and races TCP alongside it if UDP has not come up within transport_fallback_ms β€” the first handshake to complete wins and the other is dropped.

UDP is the default first choice because it fits peer-to-peer better: every peer shares one socket, so a NAT holds one mapping instead of one per peer; the source port a peer sees is the port it can dial back, which is what makes hole punching possible at all; and no middlebox holds per-connection state that can be exhausted or timed out. TCP remains a first-class equal, and is what the fallback exists for β€” some networks block or throttle UDP outright.

librats::NodeConfig config;
config.enable_tcp            = true;                          // both on by default
config.enable_udp            = true;
config.preferred_transport   = librats::TransportKind::Udp;   // tried first
config.transport_fallback_ms = 1200;                          // 0 = never fall back

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

node.transports();                 // bitmask of what is actually running
node.peers()[0].transport;         // which wire this peer's link uses

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

1. Basic P2P connection

#include <librats/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 <librats/node/node.h>
#include <librats/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 librats::Json& data) {
    std::cout << "[chat] " << from.short_hex() << ": " << data.value("text", "") << "\n";
});

node.start();

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

4. GossipSub publish-subscribe

#include <librats/node/node.h>
#include <librats/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 <librats/node/node.h>
#include <librats/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 mappings are refreshed automatically and removed on stop().

The port is forwarded under both protocols the node is listening on β€” TCP and UDP, since both transports share one port. UDP matters most: it is the dialer's first choice, so a TCP-only mapping would leave inbound peers on the TCP fallback. A node configured with only one transport gets only that one mapped.

#include <librats/node/node.h>
#include <librats/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).
// Each protocol is mapped independently; a router may accept one and refuse the other.
if (auto pub = portmap->mapped_public_address(PortMapProtocol::TCP))
    std::cout << "public tcp: " << pub->first << ":" << pub->second << "\n";
if (auto pub = portmap->mapped_public_address(PortMapProtocol::UDP))
    std::cout << "public udp: " << pub->first << ":" << pub->second << "\n";

Where the router forwards nothing β€” carrier-grade NAT, a locked-down office network, a router with UPnP off β€” HolePunch is the other half. Two peers that cannot be dialed can still reach each other if they dial at the same moment: each side's outbound packet opens the mapping the other side's needs. The moment is agreed through a peer both already have, and timed from that relayed round trip rather than from any clock.

#include <librats/node/node.h>
#include <librats/subsystems/hole_punch.h>

Node node(config);
auto* punch = node.add_subsystem(std::make_unique<HolePunch>());
node.start();

// ... once connected to at least one datagram peer (that is where the node
// learns the external endpoint to advertise) ...

// What the mesh says about our own side of the NAT.
switch (node.nat_status().udp_mapping()) {
    case NatMapping::Open:                break;  // directly dialable; no punch needed
    case NatMapping::EndpointIndependent: break;  // punchable
    case NatMapping::EndpointDependent:   break;  // symmetric NAT β€” a relay is the only way
    case NatMapping::Unknown:             break;  // not enough independent observations yet
}

punch->punch(peer_id);   // non-blocking; success arrives as an ordinary peer-connected event

Every node involved must have the subsystem attached β€” including the one carrying the rendezvous, which forwards a few dozen bytes per punch and only ever to peers it already holds. The C ABI mirrors this as rats_enable_hole_punch() / rats_punch_peer() / rats_nat_mapping().

Calling punch() by hand assumes you know which peer is unreachable β€” which is the discovery half of the problem, not the traversal half. Attach PeerExchange alongside and it answers that on its own: PEX learns peers as an id plus an address, and when that address will not dial (exactly what a NATed peer looks like) it hands the id to HolePunchService, the capability HolePunch publishes. Nothing to wire up β€” attach both and unreachable peers start getting punched:

node.add_subsystem(std::make_unique<HolePunch>());     // provides HolePunchService
node.add_subsystem(std::make_unique<PeerExchange>());  // resolves it, if present

Some pairs cannot be punched at all: a symmetric NAT gives a fresh mapping per destination, so no endpoint either side can advertise is the one the other's packets would arrive on. Relay is the last rung β€” it routes the connection itself through a node both ends already reach.

The relayed thing is a byte stream, not a message, so it becomes an ordinary Connection: the Noise handshake runs end to end and the relay moves ciphertext it cannot read, cannot forge and cannot replay. Every subsystem β€” pub/sub, file transfer, PEX β€” works over a relayed peer without a line of its own, and PeerInfo::transport is the only thing that says the path is not direct.

#include <librats/subsystems/relay.h>

Relay::Config relay_config;
relay_config.serve = true;   // ALSO carry other peers' connections β€” off by default

node.add_subsystem(std::make_unique<Relay>(relay_config));  // provides RelayService
node.add_subsystem(std::make_unique<HolePunch>());          // escalates to it on failure

Attach both and the ladder runs itself: a punch that cannot work hands the target to RelayService, and a circuit that comes up asks HolePunchService to try again β€” now that the two ends are peers they can arrange a punch over the very circuit carrying them. If it lands, the peer table prefers the direct link at both ends and swaps the route with no disconnect event, so the application never sees the seam.

Serving as a relay is opt-in because, unlike a hole-punch rendezvous, it spends real bandwidth on somebody else's traffic. A serving node is protected by an end-to-end credit window that bounds what one circuit can make it hold, by forwarding only between peers it already holds (it never dials and never resolves an address, so it cannot become an open reflector), by refusing to chain circuits, and by per-circuit byte and duration caps. The C ABI mirrors this as rats_enable_relay() / rats_connect_via_relay().

8. Peer discovery (DHT + mDNS) and reconnection

#include <librats/node/node.h>
#include <librats/subsystems/dht_discovery.h>
#include <librats/subsystems/mdns_discovery.h>
#include <librats/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 <librats/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 <librats/storage/storage.h>

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

storage->put("greeting", "hello");   // replicated to connected peers (Last-Write-Wins)
if (auto v = storage->get_string("greeting")) std::cout << *v << "\n";

On connect the two sides exchange a full snapshot, streamed in bounded chunks paced against each link's send queue, so a database far larger than that queue syncs without the peer being dropped as a slow consumer. StorageConfig tunes the two sizes that matter β€” max_value_size (1 MiB) and sync_batch_bytes (256 KiB); both must stay under a quarter of NodeConfig::send_queue_limit, and both are clamped to that assumption for the default 8 MiB queue.

πŸ“– 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). The bool is backpressure: false means
// "queued, but stop and wait for on_peer_writable" β€” see Backpressure below.
bool send(const PeerId& to, std::string_view channel, ByteView payload);
bool broadcast(std::string_view channel, ByteView payload);
bool peer_writable(const PeerId& id) const;      // the same question, without sending

// Events (additive; run on a reactor thread)
void on_peer_connected(PeerEventHandler cb);     // (const Peer&)
void on_peer_disconnected(PeerDisconnectHandler cb);  // (const PeerId&, CloseReason)
void on_peer_writable(PeerEventHandler cb);      // (const Peer&) β€” room again
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

Backpressure

Sending is non-blocking, so a peer that reads slower than you write is answered by its queue growing. Three things bound that, and they are the whole contract:

  • send() (and Peer::send()) returns whether there is still room. false means this message was queued like any other, nothing was dropped β€” but stop. It is the queue passing its low-water mark (a quarter of NodeConfig::send_queue_limit, so 2 MiB by default).
  • on_peer_writable says the room is back, and peer_writable() asks the same question without sending β€” for a caller that must wait on a thread of its own.
  • Keep sending regardless and the peer is dropped, with CloseReason::SlowConsumer handed to on_peer_disconnected so you can tell that apart from a peer that simply left. It takes offering another message while the queue is still over send_queue_limit; the mark is never charged against the message that crosses it, so a single message of any size is always queued β€” one large frame on a healthy connection is not a slow consumer. The only hard limit on one message is the 64 MiB wire block, past which send() refuses outright (returning false, connection untouched) because no amount of waiting would ever make it fit.

The same three are in the C ABI: rats_peer_writable(), rats_on_peer_writable(), and the rats_close_reason_t handed to rats_on_peer_disconnected().

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
HolePunchsubsystems/hole_punch.hUDP hole punching: two NATed peers dial each other at the same instant, arranged through a peer they share
Relaysubsystems/relay.hLast-resort connectivity: the connection itself is carried through a peer both ends reach, still encrypted end to end
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 <librats/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_on_peer_writable, rats_peer_writable / rats_close_reason_str, 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/librats/core, src/librats/util, src/librats/wire, src/librats/transport, src/librats/peer, src/librats/security, src/librats/node, src/librats/subsystems, src/librats/dht, src/librats/mdns, src/librats/nat, src/librats/crypto, src/librats/bittorrent, src/librats/storage, src/librats/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 or UDP (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 connections used for communication β€” over TCP or over the reliable UDP transport, whichever the dial settled on.
  • 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 the authenticated channel (UDP first, TCP as fallback) 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
iOSXcode + CMakeClang 14+πŸ”Ά In Development

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.jsN-API Addonβœ… Fully SupportedRatsNode + TypeScript definitions (npm)
Pythonctypes Packageβœ… Fully SupportedRatsNode with context-manager lifecycle
React NativeNitro Modules (C++)πŸ”Ά In DevelopmentOne C++ HybridObject for both platforms (react-native/). Messaging, peer events, file transfer, pub/sub; no discovery yet. Verified on simulator + emulator
iOS / SwiftC ABI via modulemapπŸ”Ά In DevelopmentXCFramework build of the core (ios/); import LibRats reaches the C ABI directly. No idiomatic Swift wrapper yet
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_CLIENTONBuild the rats-client reference/demo application
RATS_BUILD_EXAMPLESOFFBuild the examples/ programs
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, client or examples
cmake .. -DRATS_SHARED_LIBRARY=ON -DRATS_STATIC_LIBRARY=OFF \
         -DRATS_BUILD_TESTS=OFF -DRATS_BUILD_CLIENT=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

# Build for iOS -- device + simulator slices and the XCFramework.
# Driven by its own script, not by `cmake ..`, so run it from the repository root.
cd .. && ios/build-xcframework.sh     # -> build/ios/LibRats.xcframework

See ios/README.md for how to consume that from Xcode, and react-native/README.md for the React Native package.

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_CLIENT 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_CLIENT 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 <librats/node/node.h>).

Method 3: vcpkg

librats is in the official vcpkg registry:

vcpkg install librats
# optional features:
vcpkg install librats[bindings,search-features,storage]
find_package(rats CONFIG REQUIRED)
target_link_libraries(my_p2p_app PRIVATE rats::rats)

(The port lives in ports/librats/ in this repository and can also be used as an overlay port with vcpkg install librats --overlay-ports=<path-to-librats>/ports.)

Required System Libraries

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

PlatformRequired Libraries
Windowsws2_32, iphlpapi, bcrypt
Linuxpthread
macOSpthread
Androidlog
iOSβ€” (the XCFramework carries what it needs)

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, built by default; disable with RATS_BUILD_CLIENT=OFF)
  • Examples: build/bin/examples/* (the examples/ programs, if RATS_BUILD_EXAMPLES=ON)
  • 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, …).

Runnable examples

The examples/ directory holds small, focused programs β€” one capability each, built on the public Node API. They are off by default; enable them with -DRATS_BUILD_EXAMPLES=ON (add -DRATS_SEARCH_FEATURES=ON for the BitTorrent one). Binaries land in build/bin/examples/.

ProgramShows
chatA bare Node: encrypted transport + raw channel messaging, manual dialing
pubsubThe PubSub (GossipSub) subsystem β€” a topic mesh that relays across hops
typed_messagingMessageJson typed JSON messages, keyed by the authenticated sender
file_transferFileTransfer β€” streaming a file with CRC32 / SHA-256 integrity + progress
dht_discoveryDhtDiscovery β€” automatic peer discovery over the Kademlia DHT
full_chat"Batteries-included" chat: DHT + mDNS + PEX discovery, reconnection, ping and pub/sub β€” peers find each other with no addresses typed in
bittorrent_downloadDownloading a magnet link (requires RATS_SEARCH_FEATURES)
cmake -B build -DRATS_BUILD_EXAMPLES=ON && cmake --build build -j

# find each other automatically in the "lobby" room (LAN via mDNS, WAN via DHT):
./build/bin/examples/full_chat 9000 lobby
./build/bin/examples/full_chat 9001 lobby

See examples/README.md for the full list and per-example usage.

Minimal chat

#include <librats/node/node.h>
#include <librats/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 librats::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", librats::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, which makes it a good fit for low-power devices, edge computing and embedded systems where memory and CPU are the scarce things.

vs js-libp2p

Environment: Intel Core Ultra 7 265KF (16 cores), 36 GB RAM, Linux, GCC 15.2 -O3, Node.js v24.18.0, js-libp2p 3.3.8. Both sides run TCP + Noise_XX over loopback, 3 runs per cell, median reported.

Metriclibrats (JS)js-libp2p (JS)librats advantage
Memory, node started5.0 MB110.9 MB22x less
Memory per connected peer8.1 KB475.1 KB59x less
Memory, 100 peers5.8 MB158.4 MB27x less
Cold start21 ms188 ms9x faster
Connection setup1 746 /s317 /s5.5x faster
CPU per handshake1.61 ms9.19 ms5.7x less
Small messages (256 B)863 517 msg/s92 711 msg/s9.3x faster
CPU per small message2.27 Β΅s23.38 Β΅s10x less
Bulk throughput (64 KiB)600 MB/s603 MB/sparity
CPU per GB3.27 s/GB4.68 s/GB1.4x less
Runtime dependenciesnone139 npm packages, 66 MBβ€”

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.

librats also embeds a few adapted third-party cryptographic and platform-compatibility sources under BSD terms; see THIRD_PARTY_NOTICES.md for the component list and full notices.

πŸ™ Acknowledgments

  • libtorrent: a huge source of inspiration for librats' DHT and BitTorrent stacks. Many algorithmic ideas and improvements β€” the traversal/lookup algorithm, the ordered-bucket routing table, IP-diversity admission and other hardening details β€” are borrowed from its battle-tested design. Big thanks to the libtorrent team for their outstanding work.
  • noise-c by Rhys Weatherley / Southern Storm Software: the reference librats' Noise Protocol implementation was written against. The ChaCha20, SHA-256/512 and BLAKE2b/BLAKE2s primitives in src/librats/crypto/ are derived from it (MIT), as are β€” through it β€” curve25519-donna by Adam Langley (BSD-3-Clause) and poly1305-donna by Andrew Moon (MIT). See THIRD_PARTY_NOTICES.md for provenance and the full notices.
  • nlohmann/json: inspiration for the API surface of librats' own self-contained librats::Json type
  • Contributors: everyone who has helped make librats better