C++17 • Open Source • MIT License

Small, Fast P2P Networking for Real Applications

A peer-to-peer networking library in C++17: encrypted connections over TCP or UDP, peer discovery, NAT traversal, publish-subscribe and file transfer. A tiny core plus the subsystems you choose, with bindings for C, Node.js, Java, Python, React Native, Android and iOS.

5 MB
Memory, node started
8 KB
Per connected peer
0
Runtime dependencies

Used by

Using librats in production? Add your logo here

What's in the box

A small core you start with, and the pieces you attach to it

A Small Core, Opt-in Everything

A bare Node is just an encrypted transport with channel messaging. Discovery, pub/sub, file transfer, NAT traversal and reconnection are subsystems you attach before start(). You pay only for what you use.

Composable

TCP and UDP, One Port

The same encrypted protocol runs over either wire. The UDP side is a full reliable stream (sequencing, selective acks, congestion control) on one socket shared by all peers. A dial tries UDP first and races TCP as fallback.

Reliable UDP

Encrypted and Authenticated

Noise_XX (Curve25519 + ChaCha20-Poly1305) on every connection, on by default. A peer's id is derived from its public key, so peers authenticate each other with no certificates and no central authority.

Noise Protocol

NAT Traversal That Escalates

UPnP and NAT-PMP open the port on the router. Where that fails, UDP hole punching connects two peers behind NATs directly. Where even that cannot work, a relay carries the connection, still encrypted end to end.

Port map → Punch → Relay

Peer Discovery

Wide-area discovery over the BitTorrent Mainline DHT (IPv4 and IPv6), mDNS on the local network, and peer exchange to grow the mesh. Automatic reconnection with backoff.

DHT + mDNS + PEX

GossipSub Pub/Sub

Topic-based publish-subscribe over a gossip mesh: messages reach every subscriber, even several hops away. Per-topic validators decide what gets delivered and forwarded.

Pub/Sub

File and Directory Transfer

Files streamed in order with bounded memory, whole directories as one transfer. Per-chunk CRC32 and a whole-file SHA-256 are verified before the file lands. Offer/accept, progress, pause, resume and cancel.

Verified

One C ABI, Many Languages

Every binding sits on the same C API: Node.js, Java/Android, Python and React Native ship today; Swift imports the C ABI directly on iOS. Anything with a C FFI can bind it the same way.

C · Node.js · Java · Python · RN · Swift

Quick Start

Get up and running in minutes

Prefer runnable code? The examples/ directory has focused, self-contained programs — chat, pub/sub, typed messaging, file transfer, DHT discovery, and a batteries-included full_chat that finds its peers automatically over DHT + mDNS + PEX. Build them with -DRATS_BUILD_EXAMPLES=ON.

Basic P2P Connection
#include <librats/node/node.h>
#include <iostream>
#include <thread>
#include <chrono>

using namespace librats;

int main() {
    // A bare Node: encrypted transport (Noise_XX) + raw channel messaging.
    Node node(NodeConfig{/*listen_port=*/8080});

    // Callbacks run on a reactor thread; register them BEFORE start().
    node.on_peer_connected([](const Peer& peer) {
        std::cout << "✅ New peer connected: " << peer.id().short_hex() << std::endl;
    });

    node.on("chat", [](const Peer& peer, ByteView data) {
        std::cout << "💬 Message from " << peer.id().short_hex() << ": "
                  << std::string(reinterpret_cast<const char*>(data.data()), data.size()) << std::endl;
    });

    // Start the node
    if (!node.start()) {
        std::cerr << "Failed to start node" << std::endl;
        return 1;
    }

    std::cout << "🐀 librats node running on port " << node.listen_port() << std::endl;

    // Dial another peer (optional) — a bare Node never discovers peers on its own.
    // node.connect("127.0.0.1", 8081);

    // Send raw bytes to all connected peers on the "chat" channel.
    std::string msg = "Hello from librats!";
    node.broadcast("chat", ByteView(msg));

    // Keep running
    std::this_thread::sleep_for(std::chrono::minutes(1));

    node.stop();
    return 0;
}
Custom Protocol Setup
#include <librats/node/node.h>
#include <librats/subsystems/dht_discovery.h>
#include <iostream>
#include <thread>
#include <chrono>

using namespace librats;

int main() {
    NodeConfig config;
    config.listen_port = 8080;
    // The protocol is bound into the handshake: only peers with the exact same
    // (name/version) can complete a connection.
    config.protocol = "my_app/1.0";

    Node node(config);

    // DHT discovery announces + searches under a discovery key and dials any
    // peers it finds. An empty key defaults to config.protocol, so peers of the
    // same app/version discover each other automatically.
    node.add_subsystem(std::make_unique<DhtDiscovery>(DhtDiscovery::Config{}));

    node.on_peer_connected([](const Peer& peer) {
        std::cout << "Discovered peer: " << peer.id().short_hex() << std::endl;
    });

    std::cout << "Protocol: " << node.protocol() << std::endl;

    // start() brings up the DHT; announcing + searching runs on its own thread.
    node.start();

    std::this_thread::sleep_for(std::chrono::minutes(5));  // let discovery run
    node.stop();
    return 0;
}
Chat Application with Message Exchange API
#include <librats/node/node.h>
#include <librats/subsystems/message_json.h>
#include <iostream>
#include <string>

using namespace librats;

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

    // Typed JSON handlers. The sender is the AUTHENTICATED PeerId from the
    // handshake — it cannot be spoofed by a field in the payload.
    node.json()->on("chat", [](const PeerId& from, const librats::Json& data) {
        std::cout << "[CHAT] " << from.short_hex() << ": "
                  << data.value("message", "") << std::endl;
    });

    node.json()->on("user_join", [](const PeerId& from, const librats::Json& data) {
        std::cout << "[JOIN] " << data.value("username", "") << " joined" << std::endl;
    });

    // Announce ourselves whenever a peer connects.
    node.on_peer_connected([&](const Peer& peer) {
        std::cout << "✅ Peer connected: " << peer.id().short_hex() << std::endl;
        node.json()->send("user_join",
            librats::Json{{"username", "User_" + node.local_id().short_hex()}});
    });

    node.start();
    node.connect("127.0.0.1", 8081);   // or let a discovery subsystem find peers

    // Every line typed becomes a chat message to all connected peers.
    std::string line;
    while (std::getline(std::cin, line))
        node.json()->send("chat", librats::Json{{"message", line}});

    node.stop();
    return 0;
}
GossipSub Messaging
#include <librats/node/node.h>
#include <librats/subsystems/pubsub.h>
#include <librats/subsystems/dht_discovery.h>
#include <iostream>

using namespace librats;

int main() {
    Node node(NodeConfig{/*listen_port=*/8080});
    auto* pubsub = node.add_subsystem(std::make_unique<PubSub>());
    node.add_subsystem(std::make_unique<DhtDiscovery>(DhtDiscovery::Config{}));  // find peers to gossip with

    // Optional: validate inbound messages before they are delivered or forwarded.
    pubsub->set_validator("chat", [](const PeerId& from, const std::string& topic, ByteView data) {
        // Only accept messages shorter than 1000 bytes.
        return data.size() <= 1000 ? ValidationResult::Accept : ValidationResult::Reject;
    });

    // Subscribe: installs the delivery handler and joins the topic mesh.
    pubsub->subscribe("chat", [](const PeerId& from, const std::string& topic, ByteView data) {
        std::cout << "Chat from " << from.short_hex() << ": "
                  << std::string(reinterpret_cast<const char*>(data.data()), data.size()) << std::endl;
    });

    node.start();

    // Publish along the mesh — relayed to every subscriber, even several hops away.
    std::string line;
    while (std::getline(std::cin, line))
        pubsub->publish("chat", ByteView(line));

    node.stop();
    return 0;
}
File Transfer
#include <librats/node/node.h>
#include <librats/subsystems/file_transfer.h>
#include <iostream>

using namespace librats;

int main() {
    Node node(NodeConfig{/*listen_port=*/8080});

    // Stage in-progress downloads in ./downloads; chunk size, window and
    // integrity verification are set through FileTransfer::Config if needed.
    auto* files = node.add_subsystem(std::make_unique<FileTransfer>("./downloads"));

    files->on_progress([](const FileTransfer::Progress& p) {
        std::cout << "📁 Transfer " << p.id
                  << ": " << (int)p.percent() << "% complete"
                  << " (" << (p.transfer_rate_bps / 1024) << " KB/s)" << std::endl;
    });

    files->on_complete([](uint64_t id, bool success, const std::string& path) {
        if (success) {
            std::cout << "✅ Transfer completed: " << path << std::endl;
        } else {
            std::cout << "❌ Transfer failed: " << path << std::endl;
        }
    });

    // Auto-accept incoming offers into ./downloads. Per-chunk CRC32 and a
    // whole-file SHA-256 are verified before each file is moved into place.
    files->on_offer([&](const FileTransfer::Offer& offer) {
        std::cout << "📥 Incoming: " << offer.name
                  << " from " << offer.from.short_hex() << std::endl;
        files->accept(offer.from, offer.id, "./downloads/" + offer.name);
    });

    node.start();
    node.connect("127.0.0.1", 8081);

    // Once a peer is connected, push a file or a whole directory to it.
    std::cout << "type a path to send it to every peer\n";
    std::string path;
    while (std::getline(std::cin, path))
        for (const auto& p : node.peers())
            files->send_file(p.id, path);          // or files->send_directory(p.id, path)

    node.stop();
    return 0;
}
NAT Traversal: port mapping, hole punching, relay
#include <librats/node/node.h>
#include <librats/subsystems/dht_discovery.h>
#include <librats/subsystems/peer_exchange.h>
#include <librats/subsystems/port_mapping_service.h>
#include <librats/subsystems/hole_punch.h>
#include <librats/subsystems/relay.h>
#include <iostream>
#include <thread>
#include <chrono>

using namespace librats;

int main() {
    NodeConfig config;
    config.listen_port = 8080;
    config.protocol    = "my_app/1.0";
    Node node(config);

    // Find peers: wide-area over the DHT, then grow the mesh by peer exchange.
    node.add_subsystem(std::make_unique<DhtDiscovery>(DhtDiscovery::Config{}));
    node.add_subsystem(std::make_unique<PeerExchange>());

    // Rung 1: ask the router to forward the port (UPnP IGD + NAT-PMP, in parallel).
    node.add_subsystem(std::make_unique<PortMappingService>());

    // Rung 2: UDP hole punching. A peer that PEX learned but could not dial is
    // punched automatically; the rendezvous goes through a peer both sides share.
    node.add_subsystem(std::make_unique<HolePunch>());

    // Rung 3: relay. Carries the connection through a common peer when a punch
    // cannot work (e.g. symmetric NAT). Still Noise-encrypted end to end; the
    // relay only moves ciphertext. Serving *other* peers' circuits is opt-in.
    Relay::Config relay;
    relay.serve = false;
    node.add_subsystem(std::make_unique<Relay>(relay));

    node.start();

    // Peers arrive through on_peer_connected() whichever rung got them there;
    // PeerInfo::transport says whether a link is direct or relayed.
    std::this_thread::sleep_for(std::chrono::seconds(30));

    // What the mesh has learned about our own side of the NAT:
    // Open, EndpointIndependent (punchable), EndpointDependent (needs a relay), Unknown.
    if (node.nat_status().udp_mapping() == NatMapping::EndpointDependent)
        std::cout << "symmetric NAT: direct links to other NATed peers will go via relay\n";

    node.stop();
    return 0;
}
React Native (iOS + Android)
import { createNode, encodeUtf8, decodeUtf8 } from 'react-native-librats'

// One C++ implementation drives both iOS and Android -- no JNI bridge,
// no Swift wrapper. `protocol` is bound into the Noise handshake, so it
// must be identical on every platform of your app.
const node = createNode({
  listenPort: 8080,
  protocol: 'myapp/1.0',
  dataDir: appWritablePath,   // keeps a stable identity across restarts
})

// Handlers go on before start(): librats stores them without a lock and
// reads them from reactor threads, so registering later throws.
node.onPeerConnected((peerId) => console.log('connected', peerId))
node.onMessage('chat', (peerId, data) => {
  console.log(peerId, decodeUtf8(data))   // Hermes has no TextDecoder
})

node.start()
node.connect('192.168.1.42', 8080)
node.broadcast('chat', encodeUtf8('hello from React Native'))

// A node owns live sockets and threads, and iOS tears them down when the
// app suspends. Stop on background, start on resume.
AppState.addEventListener('change', (state) => {
  if (state === 'active') node.start()
  else node.stop()
})

Performance

Built to be light enough for low-power and embedded devices

librats (Node.js binding) vs js-libp2p 3.3.8

Intel Core Ultra 7 265KF, Linux, GCC 15.2 -O3, Node.js v24. Both sides TCP + Noise_XX over loopback; median of 3 runs.

Memory, node started
5.0 MB
vs
110.9 MB
22x less
Memory per connected peer
8.1 KB
vs
475 KB
59x less
Connection setup
1 746 /s
vs
317 /s
5.5x faster, 5.7x less CPU per handshake
Small messages (256 B)
863 K/s
vs
93 K/s
9.3x faster, 10x less CPU per message

The numbers above are the same measurements listed in the README. The repository's bench/ directory holds the micro-benchmarks behind the library's own internals: receive/send paths, the poller, crypto primitives against the noise-c reference, the DHT keyspace, and the reliable-UDP transport on loopback, over a real path and against a modelled lossy path.

NAT Traversal

Three rungs, each one for the networks the previous cannot reach

Port mapping asks the router (UPnP IGD or NAT-PMP) to forward the listen port. Hole punching has two NATed peers dial each other at the same instant, arranged through a peer they share; the node learns its own external endpoint from the mesh, no STUN server needed. Relay carries the connection through a common peer as a byte stream, so the Noise handshake still runs end to end and the relay only sees ciphertext. A relayed link keeps trying to punch and swaps to a direct one when it can.

Your side of the NAT Direct dial Port mapping Hole punch Relay Result
Public address direct
Home router with UPnP / NAT-PMP direct
Endpoint-independent NAT (most home / mobile NATs, CGNAT) direct, over UDP
Symmetric NAT (fresh mapping per destination) via relay, encrypted end to end
UDP blocked TCP only direct if mapped, else relay

node.nat_status() tells you which row you are on: several peers' independent views of the node's shared UDP socket say whether its mapping is stable (punchable) or per-destination (symmetric). Hole punching and relaying are subsystems like everything else (HolePunch, Relay); attach both and the ladder runs itself.

Built with librats

Real-world applications powered by high-performance P2P networking

More Projects

Other applications built on librats

UltraVNC

Remote desktop over librats: UltraVNC sessions carried over a peer-to-peer connection instead of a directly exposed port.

rasync

Peer-to-peer file synchronization built on librats' discovery, encrypted transport and file transfer.

Built something on librats? Tell us and we will list it here.

Submit Your Project

Download librats

Get the latest version for your platform

Latest Release v2.3.7

Each archive holds the static library, the headers and the rats-client reference binary, built by CI from the tagged release.

Windows

MSVC build, static library

Link with ws2_32, iphlpapi, bcrypt, advapi32

macOS

Apple Silicon, static library

Intel Macs: build from source (a few seconds with CMake)

Linux

GCC build, static library

Built on Ubuntu 24.04; older glibc: build from source

Android

AAR with the Java API + native libs

Also: arm32 / arm64 NDK archives on the release page

Build from Source

Compile librats yourself for maximum compatibility

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

# Optional: build the runnable examples (off by default)
# cmake .. -DRATS_BUILD_EXAMPLES=ON && make -j$(nproc)
# ./bin/examples/full_chat 9000 lobby   # auto-discovers peers

Package Managers

vcpkg

vcpkg install librats

npm (Node.js)

npm install librats

CMake FetchContent

FetchContent_Declare(librats GIT_REPOSITORY https://github.com/DEgITx/librats.git)

Supported Platforms & Language Bindings

Cross-platform support with bindings for multiple programming languages

Native C++ Support

Production-ready implementations across all major platforms

Platform Build Environment Compiler Status
Windows
MinGW-w64 GCC 7+ Fully Supported
Windows
Visual Studio MSVC 2017+ Fully Supported
Linux
Native GCC 7+, Clang 5+ Fully Supported
macOS
Xcode/Native Clang 10+ Fully Supported
iOS
Xcode + CMake Clang 14+ In Development

Language Bindings

Every binding is a thin layer over the same C ABI (librats/bindings/rats.h)

Language/Platform Binding Type Status Notes
C / C++
Native library + C ABI Fully Supported The C++ Node API has the full feature set; the C ABI (rats_*) is what every other binding builds on
Node.js
N-API addon Fully Supported RatsNode with TypeScript definitions, callback-based events. npm install librats
Java / Android
JNI wrapper Fully Supported com.librats.RatsNode: messaging, discovery, pub/sub, file transfer, hole punching and relay. Gradle module from source or the release AAR (android/)
Python
ctypes package Fully Supported Pythonic RatsNode with a context-manager lifecycle; installed from the checkout (python/)
React Native
Nitro Modules (C++) In Development One C++ HybridObject shared by iOS and Android, no JNI bridge, no Swift wrapper. Messaging, peer events, file transfer and pub/sub; no discovery yet. Verified on simulator and emulator (react-native/)
iOS / Swift
C ABI via modulemap In Development The core builds as an XCFramework (device + simulator); Swift reaches the C ABI directly with import LibRats. No idiomatic Swift wrapper yet (ios/)

Need another language? Anything with a C FFI (Rust, Go, C#, Zig, Swift on other platforms) binds rats.h the same way the shipped bindings do: one opaque pointer, ~75 functions, no C++ across the boundary.

Status Legend

Fully Supported Complete API over the C ABI; built and packaged with every release
In Development Working and usable from the repository, but the surface is still moving

Documentation

Start with the README, read the architecture guide when you want to know why it is built this way