mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
SessionDatagram redesign: add src_addr, reclassify error signals
Add src_addr to SessionDatagram envelope (34-byte header: msg_type + src_addr + dest_addr + hop_limit) so transit routers can route error signals back to the packet's originator. Reclassify CoordsRequired/PathBroken as link-layer error signals (plaintext inside SessionDatagram) rather than e2e encrypted session messages. Transit routers generate these when forwarding fails and route them to src_addr; if source is also unreachable, drop silently. Remove redundant src_addr/dest_addr/hop_limit from SessionSetup, SessionAck, and DataPacket (now in envelope). DataPacket header reduced from 36 to 4 bytes. Remove PathBroken.original_src. Fix routing loop vulnerability: gate bloom filter path on having cached dest_coords to prevent blind forwarding between peers. Simplify select_best_candidate() to require coordinates. Fix gossip protocol type codes (0x11->0x20, 0x12->0x30, 0x13->0x31) for consistency across all design docs. All 5 design docs updated and cross-checked for consistency. 335 tests pass, zero warnings.
This commit is contained in:
+28
-39
@@ -713,16 +713,19 @@ impl Node {
|
||||
/// Routing priority:
|
||||
/// 1. Destination is self → `None` (local delivery)
|
||||
/// 2. Destination is a direct peer → that peer
|
||||
/// 3. Bloom filter candidates + greedy tree routing → among peers whose
|
||||
/// 3. Bloom filter candidates with cached dest coords → among peers whose
|
||||
/// bloom filter contains the destination, pick the one that minimizes
|
||||
/// tree distance to the destination (if dest coords are cached), with
|
||||
/// tree distance to the destination, with
|
||||
/// `(link_cost, tree_distance_to_dest, node_addr)` tie-breaking.
|
||||
/// Falls back to greedy tree routing if no bloom filter hits.
|
||||
/// 4. No route → `None`
|
||||
/// The self-distance check ensures only peers strictly closer to the
|
||||
/// destination than us are considered (prevents routing loops).
|
||||
/// 4. Greedy tree routing fallback (requires cached dest coords)
|
||||
/// 5. No route → `None`
|
||||
///
|
||||
/// The self-distance check from greedy routing also applies to bloom
|
||||
/// filter candidates: a peer is only selected if it is strictly closer
|
||||
/// to the destination than we are (prevents routing loops).
|
||||
/// Both the bloom filter and tree routing paths require cached destination
|
||||
/// coordinates. Without coordinates, the node cannot make loop-free
|
||||
/// forwarding decisions. The caller should signal `CoordsRequired` back
|
||||
/// to the source when `None` is returned for a non-local destination.
|
||||
pub fn find_next_hop(&self, dest_node_addr: &NodeAddr) -> Option<&ActivePeer> {
|
||||
// 1. Local delivery
|
||||
if dest_node_addr == self.node_addr() {
|
||||
@@ -736,21 +739,20 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
// Look up destination coords (used by both bloom and tree paths)
|
||||
// Look up destination coords (required by both bloom and tree paths)
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
let dest_coords = self.coord_cache.get(dest_node_addr, now_ms);
|
||||
let dest_coords = self.coord_cache.get(dest_node_addr, now_ms)?;
|
||||
|
||||
// 3. Bloom filter candidates, scored by tree distance to dest
|
||||
// 3. Bloom filter candidates — requires dest_coords for loop-free selection
|
||||
let candidates: Vec<&ActivePeer> = self.destination_in_filters(dest_node_addr);
|
||||
if !candidates.is_empty() {
|
||||
return self.select_best_candidate(&candidates, dest_coords);
|
||||
}
|
||||
|
||||
// 4. Greedy tree routing fallback
|
||||
let dest_coords = dest_coords?;
|
||||
let next_hop_id = self.tree_state.find_next_hop(dest_coords)?;
|
||||
|
||||
self.peers.get(&next_hop_id).filter(|p| p.can_send())
|
||||
@@ -758,22 +760,18 @@ impl Node {
|
||||
|
||||
/// Select the best peer from a set of bloom filter candidates.
|
||||
///
|
||||
/// When dest_coords are available, uses distance from each candidate's
|
||||
/// coordinates to the destination as the primary metric (after link_cost).
|
||||
/// Only selects peers that are strictly closer to the destination than
|
||||
/// we are (self-distance check prevents loops).
|
||||
///
|
||||
/// When dest_coords are not available, falls back to distance from us
|
||||
/// to the candidate peer (a weaker heuristic — prefers closer peers on
|
||||
/// the theory that shorter paths are better).
|
||||
/// Uses distance from each candidate's tree coordinates to the destination
|
||||
/// as the primary metric (after link_cost). Only selects peers that are
|
||||
/// strictly closer to the destination than we are (self-distance check
|
||||
/// prevents routing loops).
|
||||
///
|
||||
/// Ordering: `(link_cost, distance_to_dest, node_addr)`.
|
||||
fn select_best_candidate<'a>(
|
||||
&'a self,
|
||||
candidates: &[&'a ActivePeer],
|
||||
dest_coords: Option<&crate::tree::TreeCoordinate>,
|
||||
dest_coords: &crate::tree::TreeCoordinate,
|
||||
) -> Option<&'a ActivePeer> {
|
||||
let my_distance = dest_coords.map(|dc| self.tree_state.my_coords().distance_to(dc));
|
||||
let my_distance = self.tree_state.my_coords().distance_to(dest_coords);
|
||||
|
||||
let mut best: Option<(&ActivePeer, f64, usize)> = None;
|
||||
|
||||
@@ -784,25 +782,16 @@ impl Node {
|
||||
|
||||
let cost = candidate.link_cost();
|
||||
|
||||
// Compute distance: peer→dest if coords available, else us→peer
|
||||
let dist = match dest_coords {
|
||||
Some(dc) => self
|
||||
.tree_state
|
||||
.peer_coords(candidate.node_addr())
|
||||
.map(|pc| pc.distance_to(dc))
|
||||
.unwrap_or(usize::MAX),
|
||||
None => self
|
||||
.tree_state
|
||||
.distance_to_peer(candidate.node_addr())
|
||||
.unwrap_or(usize::MAX),
|
||||
};
|
||||
let dist = self
|
||||
.tree_state
|
||||
.peer_coords(candidate.node_addr())
|
||||
.map(|pc| pc.distance_to(dest_coords))
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
// Self-distance check: when dest coords are available,
|
||||
// only consider peers that are strictly closer than us
|
||||
if let Some(my_dist) = my_distance {
|
||||
if dist >= my_dist {
|
||||
continue;
|
||||
}
|
||||
// Self-distance check: only consider peers strictly closer
|
||||
// to the destination than we are (prevents routing loops)
|
||||
if dist >= my_distance {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dominated = match &best {
|
||||
|
||||
+101
-12
@@ -54,6 +54,7 @@ fn test_routing_unknown_destination() {
|
||||
fn test_routing_bloom_filter_hit() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
let my_addr = *node.node_addr();
|
||||
|
||||
// Create two peers
|
||||
let link_id1 = LinkId::new(1);
|
||||
@@ -68,8 +69,27 @@ fn test_routing_bloom_filter_hit() {
|
||||
node.add_connection(conn2).unwrap();
|
||||
node.promote_connection(link_id2, id2, 2000).unwrap();
|
||||
|
||||
// Destination not directly connected
|
||||
// Set up tree: we are root, both peers are our children
|
||||
let peer1_coords = TreeCoordinate::from_addrs(vec![peer1_addr, my_addr]).unwrap();
|
||||
node.tree_state_mut().update_peer(
|
||||
ParentDeclaration::new(peer1_addr, my_addr, 1, 1000),
|
||||
peer1_coords,
|
||||
);
|
||||
let peer2_coords = TreeCoordinate::from_addrs(vec![peer2_addr, my_addr]).unwrap();
|
||||
node.tree_state_mut().update_peer(
|
||||
ParentDeclaration::new(peer2_addr, my_addr, 1, 1000),
|
||||
peer2_coords,
|
||||
);
|
||||
|
||||
// Destination not directly connected — placed under peer1 in the tree
|
||||
let dest = make_node_addr(99);
|
||||
let dest_coords =
|
||||
TreeCoordinate::from_addrs(vec![dest, peer1_addr, my_addr]).unwrap();
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
node.coord_cache_mut().insert(dest, dest_coords, now_ms);
|
||||
|
||||
// Add dest to peer1's bloom filter only
|
||||
let peer1 = node.get_peer_mut(&peer1_addr).unwrap();
|
||||
@@ -77,7 +97,7 @@ fn test_routing_bloom_filter_hit() {
|
||||
filter.insert(&dest);
|
||||
peer1.update_filter(filter, 1, 3000);
|
||||
|
||||
// Should route through peer1 (bloom filter hit)
|
||||
// Should route through peer1 (bloom filter hit, closer to dest)
|
||||
let result = node.find_next_hop(&dest);
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap().node_addr(), &peer1_addr);
|
||||
@@ -90,6 +110,7 @@ fn test_routing_bloom_filter_hit() {
|
||||
fn test_routing_bloom_filter_multiple_hits_tiebreak() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
let my_addr = *node.node_addr();
|
||||
|
||||
// Create three peers
|
||||
let mut peer_addrs = Vec::new();
|
||||
@@ -102,7 +123,25 @@ fn test_routing_bloom_filter_multiple_hits_tiebreak() {
|
||||
node.promote_connection(link_id, id, 2000).unwrap();
|
||||
}
|
||||
|
||||
// Set up tree: we are root, all peers are our children (equidistant)
|
||||
for &addr in &peer_addrs {
|
||||
let coords = TreeCoordinate::from_addrs(vec![addr, my_addr]).unwrap();
|
||||
node.tree_state_mut().update_peer(
|
||||
ParentDeclaration::new(addr, my_addr, 1, 1000),
|
||||
coords,
|
||||
);
|
||||
}
|
||||
|
||||
// Destination placed under the first peer (arbitrary — all peers are
|
||||
// equidistant from dest since dest is 2 hops from root via any child)
|
||||
let dest = make_node_addr(99);
|
||||
let dest_coords =
|
||||
TreeCoordinate::from_addrs(vec![dest, peer_addrs[0], my_addr]).unwrap();
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
node.coord_cache_mut().insert(dest, dest_coords, now_ms);
|
||||
|
||||
// Add dest to ALL peers' bloom filters
|
||||
for &addr in &peer_addrs {
|
||||
@@ -112,13 +151,13 @@ fn test_routing_bloom_filter_multiple_hits_tiebreak() {
|
||||
peer.update_filter(filter, 1, 3000);
|
||||
}
|
||||
|
||||
// All peers have equal link_cost (1.0) and no tree coords set,
|
||||
// so tree distance is usize::MAX for all. Tie-break by smallest node_addr.
|
||||
// All peers have equal link_cost (1.0). peer_addrs[0] is closest to dest
|
||||
// (distance 1 vs distance 3 for the others). Self-distance check filters
|
||||
// peers that aren't strictly closer than us (our distance = 2).
|
||||
// peer_addrs[0] has distance 1 (passes), others have distance 3 (filtered).
|
||||
let result = node.find_next_hop(&dest);
|
||||
assert!(result.is_some());
|
||||
|
||||
let smallest_addr = peer_addrs.iter().min().unwrap();
|
||||
assert_eq!(result.unwrap().node_addr(), smallest_addr);
|
||||
assert_eq!(result.unwrap().node_addr(), &peer_addrs[0]);
|
||||
}
|
||||
|
||||
// === Greedy tree routing ===
|
||||
@@ -183,6 +222,42 @@ fn test_routing_tree_no_coords_in_cache() {
|
||||
assert!(node.find_next_hop(&dest).is_none());
|
||||
}
|
||||
|
||||
// === Bloom filter without coords → no route (loop prevention) ===
|
||||
|
||||
#[test]
|
||||
fn test_routing_bloom_hit_without_coords_returns_none() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
|
||||
// Create two peers
|
||||
let link_id1 = LinkId::new(1);
|
||||
let (conn1, id1) = make_completed_connection(&mut node, link_id1, transport_id, 1000);
|
||||
let peer1_addr = *id1.node_addr();
|
||||
node.add_connection(conn1).unwrap();
|
||||
node.promote_connection(link_id1, id1, 2000).unwrap();
|
||||
|
||||
let link_id2 = LinkId::new(2);
|
||||
let (conn2, id2) = make_completed_connection(&mut node, link_id2, transport_id, 1000);
|
||||
let peer2_addr = *id2.node_addr();
|
||||
node.add_connection(conn2).unwrap();
|
||||
node.promote_connection(link_id2, id2, 2000).unwrap();
|
||||
|
||||
let dest = make_node_addr(99);
|
||||
|
||||
// Add dest to BOTH peers' bloom filters
|
||||
for &addr in &[peer1_addr, peer2_addr] {
|
||||
let peer = node.get_peer_mut(&addr).unwrap();
|
||||
let mut filter = BloomFilter::new();
|
||||
filter.insert(&dest);
|
||||
peer.update_filter(filter, 1, 3000);
|
||||
}
|
||||
|
||||
// Bloom filter candidates exist, but dest coords are NOT cached.
|
||||
// find_next_hop must return None to prevent routing loops.
|
||||
// The caller should signal CoordsRequired back to the source.
|
||||
assert!(node.find_next_hop(&dest).is_none());
|
||||
}
|
||||
|
||||
// === Integration: converged network ===
|
||||
|
||||
#[tokio::test]
|
||||
@@ -270,18 +345,31 @@ async fn test_routing_bloom_preferred_over_tree() {
|
||||
|
||||
drain_all_packets(&mut nodes, false).await;
|
||||
|
||||
// Create a destination beyond the network
|
||||
// Create a destination beyond the network and cache its coords.
|
||||
// Place dest as a child of peer2 in the converged tree so bloom
|
||||
// filter routing selects peer2 (strictly closer to dest than us).
|
||||
let dest = make_node_addr(99);
|
||||
let peer2_addr = *nodes[2].node.node_addr();
|
||||
let mut dest_path: Vec<NodeAddr> =
|
||||
nodes[2].node.tree_state().my_coords().node_addrs().copied().collect();
|
||||
dest_path.insert(0, dest);
|
||||
let dest_coords = TreeCoordinate::from_addrs(dest_path).unwrap();
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
nodes[0]
|
||||
.node
|
||||
.coord_cache_mut()
|
||||
.insert(dest, dest_coords, now_ms);
|
||||
|
||||
// Add dest to peer 2's bloom filter (from node 0's perspective)
|
||||
let peer2_addr = *nodes[2].node.node_addr();
|
||||
let peer2 = nodes[0].node.get_peer_mut(&peer2_addr).unwrap();
|
||||
let mut filter = BloomFilter::new();
|
||||
filter.insert(&dest);
|
||||
peer2.update_filter(filter, 100, 50000);
|
||||
|
||||
// Even though we could use tree routing (if coords were cached),
|
||||
// the bloom filter hit should be preferred.
|
||||
// Bloom filter hit with cached coords should route via peer 2.
|
||||
let hop = nodes[0].node.find_next_hop(&dest);
|
||||
assert!(hop.is_some(), "Should route via bloom filter");
|
||||
assert_eq!(
|
||||
@@ -401,7 +489,8 @@ async fn test_routing_reachability_100_nodes() {
|
||||
|
||||
// Populate coord caches: every node learns every other node's coordinates.
|
||||
// In production this happens via SessionSetup/LookupResponse; here we
|
||||
// inject them directly so routing can make progress-based decisions.
|
||||
// inject them directly. Bloom filter routing requires cached dest_coords
|
||||
// for loop-free forwarding — without coords, find_next_hop returns None.
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
|
||||
Reference in New Issue
Block a user