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.
A small core you start with, and the pieces you attach to it
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.
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.
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.
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.
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.
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.
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.
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.
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.
#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;
}
#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;
}
#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;
}
#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;
}
#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;
}
#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;
}
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()
})
Built to be light enough for low-power and embedded devices
Intel Core Ultra 7 265KF, Linux, GCC 15.2 -O3, Node.js v24. Both sides TCP + Noise_XX over loopback; median of 3 runs.
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.
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.
Real-world applications powered by high-performance P2P networking
Rats Search
A high-performance BitTorrent search program for desktop. Collects and indexes torrents from the DHT network, enabling powerful full-text search through millions of torrents with advanced filtering and P2P communication between clients.
Other applications built on librats
Remote desktop over librats: UltraVNC sessions carried over a peer-to-peer connection instead of a directly exposed port.
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 ProjectGet the latest version for your platform
Each archive holds the static library, the headers and the rats-client reference binary, built by CI from the tagged release.
MSVC build, static library
ws2_32, iphlpapi, bcrypt, advapi32
Apple Silicon, static library
GCC build, static library
AAR with the Java API + native libs
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
vcpkg install librats
npm install librats
FetchContent_Declare(librats GIT_REPOSITORY https://github.com/DEgITx/librats.git)
Cross-platform support with bindings for multiple programming languages
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 |
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.
Start with the README, read the architecture guide when you want to know why it is built this way
Generated from the headers: Node, NodeConfig, every subsystem and the C ABI
Feature tour, a worked example per subsystem, the Node API, build options and how to integrate librats into your project
The layers, the subsystem contract, the wire protocol, the threading model and how to add a feature the way the design expects
Chat, pub/sub, typed messaging, file transfer, DHT discovery, backpressure, and a batteries-included chat with the full NAT ladder
Install from npm, the RatsNode API, TypeScript definitions and examples
Gradle integration and the Java API. The React Native and iOS guides live next to it in react-native/ and ios/
Development setup, code style, how tests are organised and what a good pull request looks like