feat: implement RNS transport (Smol Mail 1.2) over microReticulum
This commit is contained in:
parent
af1d31be83
commit
7203556186
19 changed files with 1939 additions and 1141 deletions
178
shim/smolmail_rns.cpp
Normal file
178
shim/smolmail_rns.cpp
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue