bunshin/shim/smolmail_rns.cpp

178 lines
5.9 KiB
C++

/*
* microReticulum bridge for the bunshin RNS carrier (RNS.md sec 7).
*
* Owns the Reticulum instance, the smolmail.server IN/SINGLE destination
* with its request handler, and the loop thread that drives
* Reticulum::loop(). Every Rust callback fires on that thread; per-link
* teardowns decided inside a callback are deferred until after the current
* loop iteration so they never re-enter transport processing.
*/
#include "smolmail_rns.h"
#include "udp_interface.h"
#include <microStore/FileSystem.h>
#include <microStore/Adapters/UniversalFileSystem.h>
#include <MsgPack.h>
#include <microReticulum.h>
#include <cstring>
#include <mutex>
#include <thread>
#include <vector>
// Anchored in src/rns.rs.
static const char* APP_NAME = "smolmail";
static const char* APP_ASPECT = "server";
static const char* REQ_PATH = "smolmail/1";
// Interfaces rate-limit announces to roughly one an hour (RNS.md sec 1);
// every two hours stays well under that ceiling (upstream spec 13.1).
static const double ANNOUNCE_INTERVAL_SECS = 2.0 * 3600.0;
static const double LOOP_SLEEP_SECS = 0.01;
static RNS::Reticulum reticulum({RNS::Type::NONE});
static RNS::Interface udp_interface({RNS::Type::NONE});
static RNS::Destination destination({RNS::Type::NONE});
static microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
static std::mutex teardown_mutex;
static std::vector<RNS::Link> pending_teardowns;
static volatile bool running = false;
static void on_link_closed(RNS::Link& link) {
const RNS::Bytes& id = link.link_id();
smolmail_rns_on_link_closed(id.data());
}
static void on_link_established(RNS::Link& link) {
// link_id is the map key on the Rust side; the closed callback evicts.
link.set_link_closed_callback(on_link_closed);
const RNS::Bytes& id = link.link_id();
if (smolmail_rns_on_link_opened(id.data()) != 0) {
std::lock_guard<std::mutex> lock(teardown_mutex);
pending_teardowns.push_back(link);
}
}
// The request data arrives msgpack-bin-wrapped: Link::handle_request splices
// the generator's return value verbatim into the response envelope, so both
// directions must carry the smolmail payload as a msgpack binary.
static RNS::Bytes handle_smolmail_request(const RNS::Bytes& path, const RNS::Bytes& data,
const RNS::Bytes& request_id, const RNS::Bytes& link_id,
const RNS::Identity& remote_identity, double requested_at) {
(void)path; (void)request_id; (void)remote_identity; (void)requested_at;
RNS::Bytes request;
{
MsgPack::Unpacker u;
u.feed(data.data(), data.size());
if (u.isBin()) {
MsgPack::bin_t<uint8_t> bin;
u.deserialize(bin);
request = RNS::Bytes(bin.data(), bin.size());
}
}
size_t len = 0;
if (request) {
len = smolmail_rns_on_request(request.data(), request.size(), link_id.data());
}
RNS::Bytes response(len);
if (len > 0) {
smolmail_rns_take_response(response.writable(len), len);
}
else {
// The request was not a msgpack binary, or Rust produced nothing;
// always answer (upstream spec 13.5) with MALFORMED.
response = RNS::Bytes(1);
response.writable(1)[0] = 1;
}
MsgPack::Packer p;
p.packBinary(response.data(), response.size());
return RNS::Bytes(p.data(), p.size());
}
static void loop_thread_main() {
double last_announce = RNS::Utilities::OS::time();
destination.announce();
while (running) {
reticulum.loop();
std::vector<RNS::Link> teardowns;
{
std::lock_guard<std::mutex> lock(teardown_mutex);
teardowns.swap(pending_teardowns);
}
for (RNS::Link& link : teardowns) {
link.teardown();
}
double now = RNS::Utilities::OS::time();
if (now - last_announce >= ANNOUNCE_INTERVAL_SECS) {
destination.announce();
last_announce = now;
}
RNS::Utilities::OS::sleep(LOOP_SLEEP_SECS);
}
}
extern "C" int smolmail_rns_destination_hash(const uint8_t* identity,
uint8_t* destination_hash_out) {
RNS::Identity rns_identity(false);
if (!rns_identity.load_private_key(RNS::Bytes(identity, 64))) {
return -1;
}
const RNS::Bytes& hash = RNS::Destination::hash(rns_identity, APP_NAME, APP_ASPECT);
memcpy(destination_hash_out, hash.data(), 16);
return 0;
}
extern "C" int smolmail_rns_start(const uint8_t* identity,
const char* storage_dir,
const char* udp_listen_host, uint16_t udp_listen_port,
const char* udp_forward_host, uint16_t udp_forward_port,
uint8_t* destination_hash_out) {
if (running) {
return -1;
}
// Registered before anything else, as the interop examples do, so
// persistence and identity writes have a filesystem to go through.
filesystem.init();
RNS::Utilities::OS::register_filesystem(filesystem);
RNS::Reticulum::storagepath(storage_dir);
udp_interface = new UDPInterface("smolmail_rns_udp",
udp_listen_host, udp_listen_port,
udp_forward_host ? udp_forward_host : "",
udp_forward_port);
udp_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
RNS::Transport::register_interface(udp_interface);
if (!udp_interface.start()) {
return -2;
}
reticulum = RNS::Reticulum();
reticulum.transport_enabled(false);
reticulum.start();
RNS::Identity rns_identity(false);
if (!rns_identity.load_private_key(RNS::Bytes(identity, 64))) {
return -3;
}
destination = RNS::Destination(rns_identity, RNS::Type::Destination::IN,
RNS::Type::Destination::SINGLE, APP_NAME, APP_ASPECT);
destination.accepts_links(true);
destination.register_request_handler(RNS::Bytes(REQ_PATH), handle_smolmail_request,
RNS::Type::Destination::ALLOW_ALL);
destination.set_link_established_callback(on_link_established);
memcpy(destination_hash_out, destination.hash().data(), 16);
running = true;
std::thread(loop_thread_main).detach();
return 0;
}