mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-11-11 14:47:39 +00:00
233 lines
12 KiB
C++
233 lines
12 KiB
C++
#include "chat/infra/mesh_peer_directory_core.h"
|
|
#include "chat/linux_reticulum_ingress.h"
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <unistd.h>
|
|
#include <vector>
|
|
|
|
namespace rt = chat::reticulum;
|
|
using Ingress = trailmate::linux_app::LinuxReticulumIngress;
|
|
using Identity = platform::linux_runtime::LinuxReticulumIdentity;
|
|
struct MemoryBlob final : chat::IMeshPeerDirectoryBlobStore
|
|
{
|
|
chat::MeshPeerDirectoryBlobLoadResult loadBlob(std::vector<uint8_t>&) override { return chat::MeshPeerDirectoryBlobLoadResult::Missing; }
|
|
bool saveBlob(const uint8_t*, size_t) override { return true; }
|
|
void clearBlob() override {}
|
|
};
|
|
std::vector<uint8_t> unhex(const char* text)
|
|
{
|
|
std::vector<uint8_t> bytes;
|
|
while (*text)
|
|
{
|
|
const auto digit = [](char c)
|
|
{ return c <= '9' ? c - '0' : c - 'a' + 10; };
|
|
bytes.push_back(static_cast<uint8_t>((digit(text[0]) << 4) | digit(text[1])));
|
|
text += 2;
|
|
}
|
|
return bytes;
|
|
}
|
|
std::vector<uint8_t> announce(Identity& identity, uint8_t nonce, bool ratchet = false)
|
|
{
|
|
uint8_t name[10];
|
|
rt::computeNameHash("lxmf", "delivery", name);
|
|
std::vector<uint8_t> payload(identity.publicKey().begin(), identity.publicKey().end());
|
|
payload.insert(payload.end(), name, name + sizeof(name));
|
|
payload.insert(payload.end(), {nonce, 0, 0, 0, 0, 0, 0, 0, 0, nonce});
|
|
if (ratchet) payload.insert(payload.end(), identity.publicKey().begin(), identity.publicKey().begin() + 32);
|
|
std::vector<uint8_t> signed_data(identity.destinationHash().begin(), identity.destinationHash().end());
|
|
signed_data.insert(signed_data.end(), payload.begin(), payload.end());
|
|
signed_data.push_back('A');
|
|
uint8_t signature[64];
|
|
assert(identity.sign(signed_data.data(), signed_data.size(), signature));
|
|
payload.insert(payload.end(), signature, signature + sizeof(signature));
|
|
payload.push_back('A');
|
|
std::vector<uint8_t> raw(rt::kReticulumMtu);
|
|
size_t size = raw.size();
|
|
assert(rt::buildHeader1Packet(rt::PacketType::Announce, rt::DestinationType::Single, rt::PacketContext::None,
|
|
ratchet, identity.destinationHash().data(), payload.data(), payload.size(), raw.data(), &size));
|
|
raw.resize(size);
|
|
return raw;
|
|
}
|
|
void requests(Identity& local, Identity& remote)
|
|
{
|
|
Ingress::Options options;
|
|
options.request_interval_ms = 20;
|
|
options.request_ttl_ms = 45;
|
|
options.pending_paths = 2;
|
|
Ingress ingress(local, options);
|
|
struct Sender
|
|
{
|
|
bool accepted = true;
|
|
unsigned count = 0;
|
|
std::vector<uint8_t> bytes;
|
|
} sender;
|
|
const auto send = [](void* context, const uint8_t* bytes, size_t size)
|
|
{
|
|
auto& sender = *static_cast<Sender*>(context);
|
|
++sender.count;
|
|
sender.bytes.assign(bytes, bytes + size);
|
|
return sender.accepted;
|
|
};
|
|
std::array<uint8_t, 16> tag{};
|
|
tag[0] = 7;
|
|
using Result = Ingress::RequestResult;
|
|
assert(ingress.requestPath(remote.destinationHash().data(), tag.data(), 0, send, &sender) == Result::Sent);
|
|
assert(sender.count == 1 && ingress.pendingPath(remote.destinationHash().data()));
|
|
assert(ingress.requestPath(remote.destinationHash().data(), tag.data(), 1, send, &sender) == Result::CoolingDown && sender.count == 1);
|
|
auto raw = announce(remote, 1);
|
|
assert(ingress.receive(raw.data(), raw.size(), 2, 100));
|
|
assert(!ingress.pendingPath(remote.destinationHash().data()));
|
|
const auto updated = ingress.path(remote.destinationHash().data(), 2)->updated_ms;
|
|
assert(ingress.requestPath(remote.destinationHash().data(), tag.data(), 3, send, &sender) == Result::Sent);
|
|
raw[18] = static_cast<uint8_t>(rt::PacketContext::PathResponse);
|
|
auto tampered = raw;
|
|
tampered.back() ^= 1;
|
|
assert(!ingress.receive(tampered.data(), tampered.size(), 4, 100) && ingress.pendingPath(remote.destinationHash().data()));
|
|
assert(ingress.receive(raw.data(), raw.size(), 5, 100));
|
|
assert(!ingress.pendingPath(remote.destinationHash().data()) && ingress.statistics().responses == 1);
|
|
assert(ingress.path(remote.destinationHash().data(), 5)->updated_ms == updated);
|
|
assert(!ingress.receive(raw.data(), raw.size(), 6, 100));
|
|
sender.accepted = false;
|
|
auto other = remote.destinationHash();
|
|
other[0] ^= 1;
|
|
assert(ingress.requestPath(other.data(), tag.data(), 7, send, &sender) == Result::TxFailed);
|
|
assert(!ingress.pendingPath(other.data()) && ingress.statistics().request_errors == 1);
|
|
sender.accepted = true;
|
|
assert(ingress.requestPath(other.data(), tag.data(), 8, send, &sender) == Result::Sent);
|
|
ingress.tick(54);
|
|
assert(!ingress.pendingPath(other.data()));
|
|
std::array<std::array<uint8_t, 16>, 3> targets{};
|
|
for (unsigned i = 0; i < 3; ++i)
|
|
{
|
|
targets[i][0] = i + 1;
|
|
assert(ingress.requestPath(targets[i].data(), tag.data(), 60 + i, send, &sender) == Result::Sent);
|
|
}
|
|
assert(!ingress.pendingPath(targets[0].data()) && ingress.pendingPath(targets[2].data()));
|
|
Ingress wrapped(local, options);
|
|
assert(wrapped.requestPath(other.data(), tag.data(), UINT32_MAX - 5, send, &sender) == Result::Sent);
|
|
assert(wrapped.requestPath(other.data(), tag.data(), 3, send, &sender) == Result::CoolingDown);
|
|
wrapped.tick(50);
|
|
assert(!wrapped.pendingPath(other.data()));
|
|
|
|
// An early verified announce can learn a route before the asynchronous
|
|
// directory is ready. A solicited cached response must retry its projection.
|
|
MemoryBlob blob;
|
|
chat::MeshPeerDirectoryCore directory(blob);
|
|
directory.setAutoSaveEnabled(false);
|
|
Ingress recovering(local, options, &directory);
|
|
raw = announce(remote, 9, true);
|
|
assert(recovering.receive(raw.data(), raw.size(), 100, 200));
|
|
assert(recovering.statistics().directory_errors == 1);
|
|
const auto route_time = recovering.path(remote.destinationHash().data(), 100)->updated_ms;
|
|
assert(directory.beginEmpty().succeeded());
|
|
assert(recovering.requestPath(remote.destinationHash().data(), tag.data(), 101, send, &sender) == Result::Sent);
|
|
raw[18] = static_cast<uint8_t>(rt::PacketContext::PathResponse);
|
|
tampered = raw;
|
|
tampered.back() ^= 1;
|
|
assert(!recovering.receive(tampered.data(), tampered.size(), 102, 201));
|
|
assert(recovering.pendingPath(remote.destinationHash().data()));
|
|
assert(recovering.receive(raw.data(), raw.size(), 103, 201));
|
|
const auto key = chat::makeMeshPeerReticulumIdentity(chat::makeReticulumPeerIdentity(remote.destinationHash().data(), remote.identityHash().data()));
|
|
chat::MeshPeerRecord peer;
|
|
assert(directory.find(key, peer).succeeded() && peer.reticulum.has_public_keys);
|
|
assert(peer.reticulum.has_ratchet && !peer.flags.trusted && !peer.flags.favorite);
|
|
assert(!recovering.pendingPath(remote.destinationHash().data()) && recovering.statistics().responses == 1);
|
|
assert(recovering.path(remote.destinationHash().data(), 103)->updated_ms == route_time);
|
|
assert(recovering.statistics().accepted == 1);
|
|
assert(!recovering.receive(raw.data(), raw.size(), 104, 201));
|
|
chat::MeshPeerUserFlags flags;
|
|
flags.ignored = flags.trusted = flags.favorite = true;
|
|
assert(directory.setUserFlags(key, flags).succeeded() && directory.setUserAlias(key, "Local alias").succeeded());
|
|
assert(recovering.requestPath(remote.destinationHash().data(), tag.data(), 105, send, &sender) == Result::Sent);
|
|
auto stale = announce(remote, 8, true);
|
|
stale[18] = static_cast<uint8_t>(rt::PacketContext::PathResponse);
|
|
assert(!recovering.receive(stale.data(), stale.size(), 106, 202));
|
|
assert(recovering.pendingPath(remote.destinationHash().data()));
|
|
assert(recovering.receive(raw.data(), raw.size(), 107, 202));
|
|
assert(directory.find(key, peer).succeeded() && peer.flags.ignored && peer.flags.trusted && peer.flags.favorite);
|
|
assert(!std::strcmp(peer.user_alias, "Local alias"));
|
|
assert(recovering.path(remote.destinationHash().data(), 107)->updated_ms == route_time);
|
|
auto latest = announce(remote, 10, true);
|
|
assert(recovering.receive(latest.data(), latest.size(), 110, 203));
|
|
assert(recovering.requestPath(remote.destinationHash().data(), tag.data(), 111, send, &sender) == Result::Sent);
|
|
// A previously seen blob is a replay too, but it must not restore older
|
|
// peer metadata after a newer authenticated announce has been admitted.
|
|
assert(!recovering.receive(raw.data(), raw.size(), 112, 204));
|
|
assert(recovering.pendingPath(remote.destinationHash().data()));
|
|
latest[18] = static_cast<uint8_t>(rt::PacketContext::PathResponse);
|
|
assert(recovering.receive(latest.data(), latest.size(), 113, 204));
|
|
assert(!recovering.pendingPath(remote.destinationHash().data()));
|
|
assert(recovering.path(remote.destinationHash().data(), 113)->updated_ms == 110);
|
|
}
|
|
int main()
|
|
{
|
|
#if !defined(TRAIL_MATE_HAS_OPENSSL)
|
|
return 77;
|
|
#else
|
|
char root[] = "/tmp/trailmate-reticulum-ingress-XXXXXX";
|
|
assert(mkdtemp(root));
|
|
Identity local(std::filesystem::path(root) / "local.db"), remote(std::filesystem::path(root) / "remote.db");
|
|
assert(local.begin() && remote.begin());
|
|
requests(local, remote);
|
|
const char* references[] = {
|
|
#include "reticulum_announce_vectors.inc"
|
|
};
|
|
// Checked-in external direct/rebroadcast and ratchet vectors, verified by
|
|
// the same core component used by the embedded client.
|
|
for (const auto* encoded : references)
|
|
{
|
|
const auto raw = unhex(encoded);
|
|
Ingress ingress(local);
|
|
assert(ingress.receive(raw.data(), raw.size(), 10, 100));
|
|
rt::ParsedPacket parsed;
|
|
assert(rt::parsePacket(raw.data(), raw.size(), &parsed));
|
|
const auto* path = ingress.path(parsed.destination_hash, 10);
|
|
assert(path && path->hops == parsed.hops + 1 && path->cached_announce_len == raw.size());
|
|
assert(path->direct == (parsed.transport_id == nullptr));
|
|
if (parsed.transport_id) assert(!std::memcmp(path->next_hop_transport, parsed.transport_id, 16));
|
|
assert(!ingress.receive(raw.data(), raw.size(), 11, 100) && ingress.statistics().replays == 1);
|
|
}
|
|
Ingress::Options options;
|
|
options.paths = 2;
|
|
options.path_ttl_ms = 100;
|
|
options.packet_ttl_ms = 10;
|
|
options.interface_id = 7;
|
|
Ingress ingress(local, options);
|
|
auto raw = announce(remote, 1);
|
|
assert(ingress.receive(raw.data(), raw.size(), 10, 100));
|
|
assert(ingress.path(remote.destinationHash().data(), 10)->interface_id == 7);
|
|
assert(!ingress.receive(raw.data(), raw.size(), 30, 100)); // Random-blob history survives packet-filter expiry.
|
|
auto newer = announce(remote, 2, true);
|
|
assert(ingress.receive(newer.data(), newer.size(), 31, 100));
|
|
auto stale = announce(remote, 1, true);
|
|
assert(!ingress.receive(stale.data(), stale.size(), 32, 100));
|
|
auto invalid = announce(remote, 3);
|
|
invalid.back() ^= 1;
|
|
assert(!ingress.receive(invalid.data(), invalid.size(), 33, 100));
|
|
assert(ingress.path(remote.destinationHash().data(), 33)->updated_ms == 31);
|
|
invalid = announce(remote, 3);
|
|
invalid[2] ^= 1;
|
|
assert(!ingress.receive(invalid.data(), invalid.size(), 34, 100));
|
|
auto own = announce(local, 1);
|
|
assert(!ingress.receive(own.data(), own.size(), 35, 100) && !ingress.path(local.destinationHash().data(), 35));
|
|
auto too_many_hops = announce(remote, 4);
|
|
too_many_hops[1] = 128;
|
|
assert(!ingress.receive(too_many_hops.data(), too_many_hops.size(), 36, 100));
|
|
assert(!ingress.receive(nullptr, 0, 37, 100));
|
|
ingress.tick(132);
|
|
assert(!ingress.path(remote.destinationHash().data(), 132) && ingress.statistics().paths == 0);
|
|
Identity other(std::filesystem::path(root) / "other.db"), third(std::filesystem::path(root) / "third.db");
|
|
assert(other.begin() && third.begin());
|
|
for (auto* identity : {&remote, &other, &third})
|
|
{
|
|
auto frame = announce(*identity, 7);
|
|
assert(ingress.receive(frame.data(), frame.size(), 150, 100));
|
|
}
|
|
assert(ingress.statistics().paths == 2);
|
|
ingress.reset();
|
|
assert(ingress.statistics().paths == 0 && !ingress.statistics().accepted);
|
|
std::filesystem::remove_all(root);
|
|
#endif
|
|
}
|