/* * 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 #include #include #include #include #include #include #include // 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 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 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 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 teardowns; { std::lock_guard 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; }