mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-05-16 12:25:06 +00:00
a83739c1cf
Link proof signature, TCP proof routing, and split-packet handling were causing intermittent message failures with Python LXMF clients. Route >254B messages through link delivery, add proof retry for LoRa.
351 lines
13 KiB
C++
351 lines
13 KiB
C++
// Direct port from Ratputer — microReticulum integration
|
|
#include "ReticulumManager.h"
|
|
#include "config/Config.h"
|
|
#include <LittleFS.h>
|
|
#include <Preferences.h>
|
|
#include <unordered_map>
|
|
#include <string>
|
|
|
|
uint32_t ReticulumManager::_announceFilterCount = 0;
|
|
|
|
bool LittleFSFileSystem::init() { return true; }
|
|
bool LittleFSFileSystem::file_exists(const char* p) { return LittleFS.exists(p); }
|
|
|
|
size_t LittleFSFileSystem::read_file(const char* p, RNS::Bytes& data) {
|
|
File f = LittleFS.open(p, "r");
|
|
if (!f) return 0;
|
|
size_t s = f.size();
|
|
data = RNS::Bytes(s);
|
|
f.readBytes((char*)data.writable(s), s);
|
|
f.close();
|
|
return s;
|
|
}
|
|
|
|
size_t LittleFSFileSystem::write_file(const char* p, const RNS::Bytes& data) {
|
|
String path = String(p);
|
|
int lastSlash = path.lastIndexOf('/');
|
|
if (lastSlash > 0) {
|
|
String dir = path.substring(0, lastSlash);
|
|
if (!LittleFS.exists(dir.c_str())) { LittleFS.mkdir(dir.c_str()); }
|
|
}
|
|
File f = LittleFS.open(p, "w");
|
|
if (!f) return 0;
|
|
size_t w = f.write(data.data(), data.size());
|
|
f.close();
|
|
return w;
|
|
}
|
|
|
|
RNS::FileStream LittleFSFileSystem::open_file(const char*, RNS::FileStream::MODE) { return {RNS::Type::NONE}; }
|
|
bool LittleFSFileSystem::remove_file(const char* p) { return LittleFS.remove(p); }
|
|
bool LittleFSFileSystem::rename_file(const char* f, const char* t) { return LittleFS.rename(f, t); }
|
|
bool LittleFSFileSystem::directory_exists(const char* p) { return LittleFS.exists(p); }
|
|
bool LittleFSFileSystem::create_directory(const char* p) { return LittleFS.mkdir(p); }
|
|
bool LittleFSFileSystem::remove_directory(const char* p) { return LittleFS.rmdir(p); }
|
|
|
|
std::list<std::string> LittleFSFileSystem::list_directory(const char* p, Callbacks::DirectoryListing callback) {
|
|
std::list<std::string> entries;
|
|
File dir = LittleFS.open(p);
|
|
if (!dir || !dir.isDirectory()) return entries;
|
|
File f = dir.openNextFile();
|
|
while (f) {
|
|
const char* name = f.name();
|
|
entries.push_back(name);
|
|
if (callback) callback(name);
|
|
f = dir.openNextFile();
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
size_t LittleFSFileSystem::storage_size() { return LittleFS.totalBytes(); }
|
|
size_t LittleFSFileSystem::storage_available() { return LittleFS.totalBytes() - LittleFS.usedBytes(); }
|
|
|
|
bool ReticulumManager::begin(SX1262* radio, FlashStore* flash) {
|
|
_flash = flash;
|
|
|
|
LittleFSFileSystem* fsImpl = new LittleFSFileSystem();
|
|
RNS::FileSystem fs(fsImpl);
|
|
fs.init();
|
|
RNS::Utilities::OS::register_filesystem(fs);
|
|
Serial.println("[RNS] Filesystem registered");
|
|
|
|
// Restore routing tables and known destinations from SD if missing on flash
|
|
if (_sd && _sd->isReady()) {
|
|
static const char* files[] = {"/destination_table", "/packet_hashlist", "/known_destinations"};
|
|
for (const char* name : files) {
|
|
if (!LittleFS.exists(name)) {
|
|
char sdPath[64];
|
|
snprintf(sdPath, sizeof(sdPath), "/ratputer/transport%s", name);
|
|
uint8_t buf[4096];
|
|
size_t len = 0;
|
|
if (_sd->readFile(sdPath, buf, sizeof(buf), len) && len > 0) {
|
|
File f = LittleFS.open(name, "w");
|
|
if (f) { f.write(buf, len); f.close(); }
|
|
Serial.printf("[RNS] Restored %s from SD (%d bytes)\n", name, (int)len);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_loraImpl = new LoRaInterface(radio, "LoRa.915");
|
|
_loraIface = _loraImpl;
|
|
_loraIface.mode(RNS::Type::Interface::MODE_GATEWAY);
|
|
RNS::Transport::register_interface(_loraIface);
|
|
if (!_loraImpl->start()) {
|
|
Serial.println("[RNS] WARNING: LoRa interface failed to start");
|
|
}
|
|
|
|
_reticulum = RNS::Reticulum();
|
|
// Suppress verbose microReticulum logging — LOG_TRACE floods serial at 115200 baud,
|
|
// blocking the CPU for hundreds of ms. Change to LOG_TRACE or LOG_DEBUG for protocol debugging.
|
|
RNS::loglevel(RNS::LOG_WARNING);
|
|
RNS::Reticulum::transport_enabled(false);
|
|
RNS::Reticulum::probe_destination_enabled(true);
|
|
RNS::Transport::path_table_maxsize(256);
|
|
RNS::Transport::announce_table_maxsize(128);
|
|
_reticulum.start();
|
|
Serial.println("[RNS] Reticulum started (Endpoint)");
|
|
|
|
// Layer 1: Transport-level announce filter — runs BEFORE Ed25519 verify
|
|
RNS::Transport::set_filter_packet_callback([](const RNS::Packet& packet) -> bool {
|
|
if (packet.packet_type() != RNS::Type::Packet::ANNOUNCE) return true;
|
|
|
|
unsigned long now = millis();
|
|
|
|
// Rate limit window (per-second)
|
|
static unsigned long windowStart = 0;
|
|
static unsigned int count = 0;
|
|
if (now - windowStart >= 1000) { windowStart = now; count = 0; }
|
|
|
|
// Adaptive rate: tighter during first 60s boot flood, then normal
|
|
unsigned int maxRate = (now < 60000) ? 3 : RATDECK_MAX_ANNOUNCES_PER_SEC;
|
|
if (++count > maxRate) { ReticulumManager::_announceFilterCount++; return false; }
|
|
|
|
// Skip re-validation of known paths (saves ~100ms Ed25519 per announce)
|
|
// Allow through if better path discovered, or once per 5 min for name/ratchet updates.
|
|
// Uses raw hash bytes as key (avoids expensive toHex per packet).
|
|
if (RNS::Transport::has_path(packet.destination_hash())) {
|
|
// Always allow announces with a better (shorter) path through
|
|
uint8_t existingHops = RNS::Transport::hops_to(packet.destination_hash());
|
|
if (packet.hops() < existingHops) return true;
|
|
|
|
static std::unordered_map<std::string, unsigned long> lastRevalidate;
|
|
std::string key((const char*)packet.destination_hash().data(),
|
|
packet.destination_hash().size());
|
|
|
|
auto it = lastRevalidate.find(key);
|
|
if (it != lastRevalidate.end() && (now - it->second) < 300000) {
|
|
ReticulumManager::_announceFilterCount++;
|
|
return false;
|
|
}
|
|
lastRevalidate[key] = now;
|
|
|
|
if (lastRevalidate.size() > 300) lastRevalidate.clear();
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// Load persisted known destinations so Identity::recall() works
|
|
// immediately after reboot for previously-seen nodes.
|
|
RNS::Identity::load_known_destinations();
|
|
|
|
if (!loadOrCreateIdentity()) {
|
|
Serial.println("[RNS] ERROR: Identity creation failed!");
|
|
return false;
|
|
}
|
|
|
|
_destination = RNS::Destination(
|
|
_identity,
|
|
RNS::Type::Destination::IN,
|
|
RNS::Type::Destination::SINGLE,
|
|
"lxmf",
|
|
"delivery"
|
|
);
|
|
_destination.set_proof_strategy(RNS::Type::Destination::PROVE_ALL);
|
|
_destination.accepts_links(true);
|
|
|
|
_transportActive = true;
|
|
Serial.println("[RNS] Endpoint active");
|
|
return true;
|
|
}
|
|
|
|
bool ReticulumManager::loadOrCreateIdentity() {
|
|
// Tier 1: Flash (LittleFS)
|
|
if (_flash->exists(PATH_IDENTITY)) {
|
|
RNS::Bytes keyData;
|
|
if (RNS::Utilities::OS::read_file(PATH_IDENTITY, keyData) > 0) {
|
|
_identity = RNS::Identity(false);
|
|
if (_identity.load_private_key(keyData)) {
|
|
Serial.printf("[RNS] Identity loaded from flash: %s\n", _identity.hexhash().c_str());
|
|
saveIdentityToAll(keyData);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tier 2: SD card
|
|
if (_sd && _sd->isReady() && _sd->exists(SD_PATH_IDENTITY)) {
|
|
uint8_t keyBuf[128];
|
|
size_t keyLen = 0;
|
|
if (_sd->readFile(SD_PATH_IDENTITY, keyBuf, sizeof(keyBuf), keyLen) && keyLen > 0) {
|
|
RNS::Bytes keyData(keyBuf, keyLen);
|
|
_identity = RNS::Identity(false);
|
|
if (_identity.load_private_key(keyData)) {
|
|
Serial.printf("[RNS] Identity restored from SD: %s\n", _identity.hexhash().c_str());
|
|
saveIdentityToAll(keyData);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tier 3: NVS (ESP32 Preferences — always available)
|
|
{
|
|
Preferences prefs;
|
|
if (prefs.begin("ratdeck_id", true)) {
|
|
size_t keyLen = prefs.getBytesLength("privkey");
|
|
if (keyLen > 0 && keyLen <= 128) {
|
|
uint8_t keyBuf[128];
|
|
prefs.getBytes("privkey", keyBuf, keyLen);
|
|
prefs.end();
|
|
RNS::Bytes keyData(keyBuf, keyLen);
|
|
_identity = RNS::Identity(false);
|
|
if (_identity.load_private_key(keyData)) {
|
|
Serial.printf("[RNS] Identity restored from NVS: %s\n", _identity.hexhash().c_str());
|
|
saveIdentityToAll(keyData);
|
|
return true;
|
|
}
|
|
} else {
|
|
prefs.end();
|
|
}
|
|
}
|
|
}
|
|
|
|
// No identity found anywhere — create new
|
|
_identity = RNS::Identity();
|
|
Serial.printf("[RNS] New identity created: %s\n", _identity.hexhash().c_str());
|
|
|
|
RNS::Bytes privKey = _identity.get_private_key();
|
|
if (privKey.size() > 0) {
|
|
saveIdentityToAll(privKey);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ReticulumManager::saveIdentityToAll(const RNS::Bytes& keyData) {
|
|
// Flash
|
|
_flash->writeAtomic(PATH_IDENTITY, keyData.data(), keyData.size());
|
|
// SD
|
|
if (_sd && _sd->isReady()) {
|
|
_sd->ensureDir("/ratputer/identity");
|
|
_sd->writeAtomic(SD_PATH_IDENTITY, keyData.data(), keyData.size());
|
|
}
|
|
// NVS (always available, survives flash/SD failures)
|
|
Preferences prefs;
|
|
if (prefs.begin("ratdeck_id", false)) {
|
|
prefs.putBytes("privkey", keyData.data(), keyData.size());
|
|
prefs.end();
|
|
Serial.println("[RNS] Identity saved to NVS");
|
|
}
|
|
}
|
|
|
|
void ReticulumManager::loop() {
|
|
if (!_transportActive) return;
|
|
_reticulum.loop();
|
|
if (_loraImpl) { _loraImpl->loop(); }
|
|
unsigned long now = millis();
|
|
if (now - _lastPersist >= PATH_PERSIST_INTERVAL_MS) {
|
|
_lastPersist = now;
|
|
persistData();
|
|
}
|
|
}
|
|
|
|
// Synchronous persist — one cycle per call to spread I/O across intervals.
|
|
// Runs on core 1 (main loop) to avoid data races with microReticulum's
|
|
// single-threaded transport state.
|
|
void ReticulumManager::persistData() {
|
|
unsigned long start = millis();
|
|
switch (_persistCycle) {
|
|
case 0:
|
|
RNS::Transport::persist_data();
|
|
break;
|
|
case 1:
|
|
RNS::Identity::persist_data();
|
|
break;
|
|
case 2:
|
|
if (_sd && _sd->isReady()) {
|
|
static const char* files[] = {"/destination_table", "/packet_hashlist", "/known_destinations"};
|
|
for (const char* name : files) {
|
|
File f = LittleFS.open(name, "r");
|
|
if (f && f.size() > 0) {
|
|
size_t sz = f.size();
|
|
uint8_t* buf = (uint8_t*)malloc(sz);
|
|
if (buf) {
|
|
f.readBytes((char*)buf, sz);
|
|
char sdPath[64];
|
|
snprintf(sdPath, sizeof(sdPath), "/ratputer/transport%s", name);
|
|
_sd->ensureDir("/ratputer/transport");
|
|
_sd->writeSimple(sdPath, buf, sz);
|
|
free(buf);
|
|
}
|
|
}
|
|
if (f) f.close();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
unsigned long dur = millis() - start;
|
|
Serial.printf("[PERSIST] Cycle %d done (%lums)\n", _persistCycle, dur);
|
|
if (dur > 500) {
|
|
Serial.printf("[PERSIST] WARNING: Cycle %d blocked for %lums!\n", _persistCycle, dur);
|
|
}
|
|
_persistCycle = (_persistCycle + 1) % 3;
|
|
}
|
|
|
|
String ReticulumManager::identityHash() const {
|
|
if (!_identity) return "unknown";
|
|
std::string hex = _identity.hexhash();
|
|
if (hex.length() >= 12) {
|
|
return String((hex.substr(0, 4) + ":" + hex.substr(4, 4) + ":" + hex.substr(8, 4)).c_str());
|
|
}
|
|
return String(hex.c_str());
|
|
}
|
|
|
|
String ReticulumManager::destinationHashHex() const {
|
|
if (!_destination) return "unknown";
|
|
return String(_destination.hash().toHex().c_str());
|
|
}
|
|
|
|
String ReticulumManager::destinationHashStr() const {
|
|
if (!_destination) return "unknown";
|
|
std::string hex = _destination.hash().toHex();
|
|
if (hex.length() >= 12) {
|
|
return String((hex.substr(0, 4) + ":" + hex.substr(4, 4) + ":" + hex.substr(8, 4)).c_str());
|
|
}
|
|
return String(hex.c_str());
|
|
}
|
|
|
|
size_t ReticulumManager::pathCount() const { return _reticulum.get_path_table().size(); }
|
|
size_t ReticulumManager::linkCount() const { return _reticulum.get_link_count(); }
|
|
|
|
void ReticulumManager::announce(const RNS::Bytes& appData) {
|
|
if (!_transportActive) return;
|
|
Serial.println("[ANNOUNCE-TX] === Starting ===");
|
|
Serial.printf("[ANNOUNCE-TX] dest_hash: %s\n", _destination.hash().toHex().c_str());
|
|
Serial.printf("[ANNOUNCE-TX] identity_hash: %s\n", _identity.hexhash().c_str());
|
|
Serial.printf("[ANNOUNCE-TX] app_data size: %d bytes\n", (int)appData.size());
|
|
if (appData.size() > 0) {
|
|
Serial.printf("[ANNOUNCE-TX] app_data hex: %s\n", appData.toHex().c_str());
|
|
}
|
|
// Log registered interfaces
|
|
auto& ifaces = RNS::Transport::get_interfaces();
|
|
Serial.printf("[ANNOUNCE-TX] registered interfaces: %d\n", (int)ifaces.size());
|
|
for (const auto& [hash, iface] : ifaces) {
|
|
Serial.printf("[ANNOUNCE-TX] iface: %s OUT=%d online=%d mode=%d\n",
|
|
iface.toString().c_str(), iface.OUT(), iface.online(), (int)iface.mode());
|
|
}
|
|
unsigned long startMs = millis();
|
|
_destination.announce(appData);
|
|
_lastAnnounceTime = millis();
|
|
Serial.printf("[ANNOUNCE-TX] === Complete === (%lu ms)\n", millis() - startMs);
|
|
}
|