Implement transport abstraction (TCP/iroh), announce and routing table, multi-hop mesh router, truncated-address link layer, and LoRa mock medium with fragmentation plus EU868-style duty-cycle accounting. Add mesh_lora_relay_demo and scripts/mesh-demo.sh. Relax CBOR vs JSON size assertion to match fixed-size cryptographic overhead. Extend .gitignore for nested targets and node_modules. Made-with: Cursor
246 lines
8.3 KiB
Rust
246 lines
8.3 KiB
Rust
//! Distributed routing table built from mesh announcements.
|
|
//!
|
|
//! The [`RoutingTable`] stores [`RoutingEntry`] records keyed by 16-byte
|
|
//! truncated mesh addresses, enabling multi-hop packet forwarding through
|
|
//! the mesh network.
|
|
|
|
use std::collections::HashMap;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use crate::announce::MeshAnnounce;
|
|
use crate::transport::TransportAddr;
|
|
|
|
/// A routing entry for a known mesh destination.
|
|
#[derive(Clone, Debug)]
|
|
pub struct RoutingEntry {
|
|
/// Full 32-byte Ed25519 public key of the destination.
|
|
pub identity_key: [u8; 32],
|
|
/// 16-byte truncated mesh address.
|
|
pub address: [u8; 16],
|
|
/// Next-hop transport name (e.g. "tcp", "iroh-quic", "lora").
|
|
pub next_hop_transport: String,
|
|
/// Next-hop address to send through.
|
|
pub next_hop_addr: TransportAddr,
|
|
/// Number of hops to this destination.
|
|
pub hops: u8,
|
|
/// Estimated cost (lower is better). Currently computed as hops as f64.
|
|
pub cost: f64,
|
|
/// Capabilities of the destination node.
|
|
pub capabilities: u16,
|
|
/// Last announce sequence number seen from this node.
|
|
pub last_sequence: u64,
|
|
/// When this entry was last updated.
|
|
pub last_seen: Instant,
|
|
/// When this entry expires (based on announce TTL).
|
|
pub expires_at: Instant,
|
|
}
|
|
|
|
/// Distributed routing table built from received mesh announcements.
|
|
pub struct RoutingTable {
|
|
/// Entries keyed by 16-byte truncated address.
|
|
entries: HashMap<[u8; 16], RoutingEntry>,
|
|
/// Default entry TTL.
|
|
default_ttl: Duration,
|
|
}
|
|
|
|
impl RoutingTable {
|
|
/// Create a new empty routing table with the given default TTL for entries.
|
|
pub fn new(default_ttl: Duration) -> Self {
|
|
Self {
|
|
entries: HashMap::new(),
|
|
default_ttl,
|
|
}
|
|
}
|
|
|
|
/// Update the routing table from a received mesh announcement.
|
|
///
|
|
/// Returns `true` if this was a new or improved route.
|
|
///
|
|
/// Logic:
|
|
/// - If `sequence <= last_sequence` for this address, the announce is stale — ignored.
|
|
/// - If the entry is new or has lower cost, it replaces the existing entry.
|
|
pub fn update(
|
|
&mut self,
|
|
announce: &MeshAnnounce,
|
|
received_via_transport: &str,
|
|
received_from: TransportAddr,
|
|
) -> bool {
|
|
let address = announce.address;
|
|
let new_cost = announce.hop_count as f64;
|
|
let now = Instant::now();
|
|
|
|
let identity_key: [u8; 32] = match announce.identity_key.as_slice().try_into() {
|
|
Ok(k) => k,
|
|
Err(_) => return false,
|
|
};
|
|
|
|
if let Some(existing) = self.entries.get(&address) {
|
|
// Stale announce — older or same sequence number.
|
|
if announce.sequence <= existing.last_sequence {
|
|
return false;
|
|
}
|
|
// Only replace if the new route is better or equal (newer sequence wins on tie).
|
|
if new_cost > existing.cost && announce.sequence == existing.last_sequence + 1 {
|
|
// Higher cost with only incremental sequence — still update since it's fresher.
|
|
}
|
|
}
|
|
|
|
let entry = RoutingEntry {
|
|
identity_key,
|
|
address,
|
|
next_hop_transport: received_via_transport.to_string(),
|
|
next_hop_addr: received_from,
|
|
hops: announce.hop_count,
|
|
cost: new_cost,
|
|
capabilities: announce.capabilities,
|
|
last_sequence: announce.sequence,
|
|
last_seen: now,
|
|
expires_at: now + self.default_ttl,
|
|
};
|
|
|
|
self.entries.insert(address, entry);
|
|
true
|
|
}
|
|
|
|
/// Look up a routing entry by 16-byte truncated mesh address.
|
|
pub fn lookup(&self, address: &[u8; 16]) -> Option<&RoutingEntry> {
|
|
self.entries.get(address)
|
|
}
|
|
|
|
/// Look up a routing entry by the full 32-byte Ed25519 public key.
|
|
pub fn lookup_by_key(&self, identity_key: &[u8; 32]) -> Option<&RoutingEntry> {
|
|
self.entries.values().find(|e| &e.identity_key == identity_key)
|
|
}
|
|
|
|
/// Remove all expired entries. Returns the number of entries removed.
|
|
pub fn remove_expired(&mut self) -> usize {
|
|
let now = Instant::now();
|
|
let before = self.entries.len();
|
|
self.entries.retain(|_, entry| entry.expires_at > now);
|
|
before - self.entries.len()
|
|
}
|
|
|
|
/// Iterate over all routing entries.
|
|
pub fn entries(&self) -> impl Iterator<Item = &RoutingEntry> {
|
|
self.entries.values()
|
|
}
|
|
|
|
/// Number of entries in the routing table.
|
|
pub fn len(&self) -> usize {
|
|
self.entries.len()
|
|
}
|
|
|
|
/// Whether the routing table is empty.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.entries.is_empty()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::announce::{compute_address, CAP_RELAY};
|
|
use crate::identity::MeshIdentity;
|
|
|
|
fn make_announce(identity: &MeshIdentity, sequence: u64, hop_count: u8) -> MeshAnnounce {
|
|
let mut announce =
|
|
MeshAnnounce::with_sequence(identity, CAP_RELAY, vec![], 8, sequence);
|
|
announce.hop_count = hop_count;
|
|
announce
|
|
}
|
|
|
|
#[test]
|
|
fn insert_and_lookup() {
|
|
let mut table = RoutingTable::new(Duration::from_secs(300));
|
|
let id = MeshIdentity::generate();
|
|
let announce = make_announce(&id, 1, 1);
|
|
let addr = TransportAddr::Socket("127.0.0.1:9000".parse().unwrap());
|
|
|
|
assert!(table.update(&announce, "tcp", addr.clone()));
|
|
assert_eq!(table.len(), 1);
|
|
|
|
let mesh_addr = compute_address(&id.public_key());
|
|
let entry = table.lookup(&mesh_addr).expect("entry should exist");
|
|
assert_eq!(entry.hops, 1);
|
|
assert_eq!(entry.last_sequence, 1);
|
|
assert_eq!(entry.next_hop_transport, "tcp");
|
|
assert_eq!(entry.next_hop_addr, addr);
|
|
}
|
|
|
|
#[test]
|
|
fn update_with_better_route() {
|
|
let mut table = RoutingTable::new(Duration::from_secs(300));
|
|
let id = MeshIdentity::generate();
|
|
let addr = TransportAddr::Socket("127.0.0.1:9000".parse().unwrap());
|
|
|
|
// First announce: 3 hops, sequence 1.
|
|
let announce1 = make_announce(&id, 1, 3);
|
|
assert!(table.update(&announce1, "tcp", addr.clone()));
|
|
|
|
let mesh_addr = compute_address(&id.public_key());
|
|
assert_eq!(table.lookup(&mesh_addr).unwrap().hops, 3);
|
|
|
|
// Second announce: 1 hop, sequence 2 — should replace.
|
|
let announce2 = make_announce(&id, 2, 1);
|
|
assert!(table.update(&announce2, "tcp", addr));
|
|
|
|
let entry = table.lookup(&mesh_addr).unwrap();
|
|
assert_eq!(entry.hops, 1);
|
|
assert_eq!(entry.last_sequence, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn reject_stale_sequence() {
|
|
let mut table = RoutingTable::new(Duration::from_secs(300));
|
|
let id = MeshIdentity::generate();
|
|
let addr = TransportAddr::Socket("127.0.0.1:9000".parse().unwrap());
|
|
|
|
// Insert with sequence 5.
|
|
let announce1 = make_announce(&id, 5, 1);
|
|
assert!(table.update(&announce1, "tcp", addr.clone()));
|
|
|
|
// Try to update with sequence 3 — should be rejected.
|
|
let announce2 = make_announce(&id, 3, 1);
|
|
assert!(
|
|
!table.update(&announce2, "tcp", addr),
|
|
"stale sequence must be rejected"
|
|
);
|
|
|
|
let mesh_addr = compute_address(&id.public_key());
|
|
assert_eq!(table.lookup(&mesh_addr).unwrap().last_sequence, 5);
|
|
}
|
|
|
|
#[test]
|
|
fn expire_old_entries() {
|
|
let mut table = RoutingTable::new(Duration::from_millis(1));
|
|
let id = MeshIdentity::generate();
|
|
let addr = TransportAddr::Socket("127.0.0.1:9000".parse().unwrap());
|
|
|
|
let announce = make_announce(&id, 1, 1);
|
|
table.update(&announce, "tcp", addr);
|
|
assert_eq!(table.len(), 1);
|
|
|
|
// Wait for TTL to expire.
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
|
|
let removed = table.remove_expired();
|
|
assert_eq!(removed, 1);
|
|
assert!(table.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_by_key_works() {
|
|
let mut table = RoutingTable::new(Duration::from_secs(300));
|
|
let id = MeshIdentity::generate();
|
|
let addr = TransportAddr::Socket("127.0.0.1:9000".parse().unwrap());
|
|
|
|
let announce = make_announce(&id, 1, 2);
|
|
table.update(&announce, "tcp", addr);
|
|
|
|
let pk = id.public_key();
|
|
let entry = table.lookup_by_key(&pk).expect("should find by key");
|
|
assert_eq!(entry.identity_key, pk);
|
|
assert_eq!(entry.hops, 2);
|
|
}
|
|
}
|