Merge branch 'maint'

# Conflicts:
#	src/bin/fips.rs
#	src/bin/fipstop/app.rs
#	src/config/mod.rs
#	src/config/node.rs
#	src/config/transport.rs
#	src/mmp/receiver.rs
#	src/mmp/sender.rs
#	src/node/handlers/handshake.rs
#	src/node/handlers/rekey.rs
#	src/node/lifecycle.rs
#	src/node/mod.rs
#	src/transport/ethernet/socket.rs
#	src/transport/mod.rs
#	src/upper/tun.rs
This commit is contained in:
Johnathan Corgan
2026-04-10 08:46:54 +00:00
90 changed files with 2900 additions and 1894 deletions
+24 -17
View File
@@ -16,10 +16,7 @@ fn get_tree_edges(nodes: &[TestNode]) -> Vec<(usize, usize)> {
let ts = tn.node.tree_state();
if !ts.is_root() {
let parent_addr = ts.my_declaration().parent_id();
if let Some(j) = nodes
.iter()
.position(|n| n.node.node_addr() == parent_addr)
{
if let Some(j) = nodes.iter().position(|n| n.node.node_addr() == parent_addr) {
edges.push((i, j));
}
}
@@ -174,8 +171,7 @@ async fn test_bloom_filter_star() {
/// entries, and so on. Both endpoints should see all other nodes.
#[tokio::test]
async fn test_bloom_filter_chain_propagation() {
let edges: Vec<(usize, usize)> =
vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7)];
let edges: Vec<(usize, usize)> = vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7)];
let mut nodes = run_tree_test(8, &edges, false).await;
verify_tree_convergence(&nodes);
verify_filter_exchange(&nodes, &edges);
@@ -315,8 +311,7 @@ fn collect_subtree(
#[tokio::test]
async fn test_bloom_filter_split_horizon() {
// Pure tree: 7 nodes, 6 edges
let edges: Vec<(usize, usize)> =
vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (5, 6)];
let edges: Vec<(usize, usize)> = vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (5, 6)];
let mut nodes = run_tree_test(7, &edges, false).await;
verify_tree_convergence(&nodes);
verify_filter_exchange(&nodes, &edges);
@@ -340,9 +335,7 @@ async fn test_bloom_filter_split_horizon() {
// - parent's filter to child contains the complement only
for &(child_idx, parent_idx) in &tree_edges {
let child_subtree = collect_subtree(child_idx, Some(parent_idx), &tree_adj);
let complement: Vec<usize> = (0..n)
.filter(|i| !child_subtree.contains(i))
.collect();
let complement: Vec<usize> = (0..n).filter(|i| !child_subtree.contains(i)).collect();
// --- Upward filter: child → parent ---
// This is stored as parent's inbound filter from child
@@ -358,7 +351,9 @@ async fn test_bloom_filter_split_horizon() {
assert!(
filter_up.contains(&addrs[idx]),
"Upward filter (n{}→n{}): should contain subtree member n{} but doesn't",
child_idx, parent_idx, idx
child_idx,
parent_idx,
idx
);
}
@@ -367,7 +362,9 @@ async fn test_bloom_filter_split_horizon() {
assert!(
!filter_up.contains(&addrs[idx]),
"Upward filter (n{}→n{}): should NOT contain complement member n{} but does",
child_idx, parent_idx, idx
child_idx,
parent_idx,
idx
);
}
@@ -376,7 +373,10 @@ async fn test_bloom_filter_split_horizon() {
assert!(
(up_est - child_subtree.len() as f64).abs() < 1.5,
"Upward filter (n{}→n{}): expected ~{} entries, got {:.1}",
child_idx, parent_idx, child_subtree.len(), up_est
child_idx,
parent_idx,
child_subtree.len(),
up_est
);
// --- Downward filter: parent → child ---
@@ -393,7 +393,9 @@ async fn test_bloom_filter_split_horizon() {
assert!(
filter_down.contains(&addrs[idx]),
"Downward filter (n{}→n{}): should contain complement member n{} but doesn't",
parent_idx, child_idx, idx
parent_idx,
child_idx,
idx
);
}
@@ -405,7 +407,9 @@ async fn test_bloom_filter_split_horizon() {
assert!(
!filter_down.contains(&addrs[idx]),
"Downward filter (n{}→n{}): should NOT contain subtree member n{} but does",
parent_idx, child_idx, idx
parent_idx,
child_idx,
idx
);
}
@@ -414,7 +418,10 @@ async fn test_bloom_filter_split_horizon() {
assert!(
(down_est - complement.len() as f64).abs() < 1.5,
"Downward filter (n{}→n{}): expected ~{} entries, got {:.1}",
parent_idx, child_idx, complement.len(), down_est
parent_idx,
child_idx,
complement.len(),
down_est
);
// Together, subtree + complement = all nodes
+16 -8
View File
@@ -258,10 +258,8 @@ async fn test_disconnect_clears_session() {
{
let our_identity = nodes[1].node.identity();
let mut initiator = HandshakeState::new_initiator(
our_identity.keypair(),
remote_identity.pubkey_full(),
);
let mut initiator =
HandshakeState::new_initiator(our_identity.keypair(), remote_identity.pubkey_full());
let mut responder = HandshakeState::new_responder(remote_identity.keypair());
let mut init_epoch = [0u8; 8];
rand::Rng::fill_bytes(&mut rand::rng(), &mut init_epoch);
@@ -285,8 +283,16 @@ async fn test_disconnect_clears_session() {
nodes[1].node.sessions.insert(node0_addr, entry);
}
assert_eq!(nodes[1].node.session_count(), 1, "Session should exist before disconnect");
assert_eq!(nodes[1].node.peer_count(), 1, "Peer should exist before disconnect");
assert_eq!(
nodes[1].node.session_count(),
1,
"Session should exist before disconnect"
);
assert_eq!(
nodes[1].node.peer_count(),
1,
"Peer should exist before disconnect"
);
// Node 0 sends Disconnect to node 1.
let disconnect = crate::protocol::Disconnect::new(DisconnectReason::Shutdown);
@@ -301,7 +307,8 @@ async fn test_disconnect_clears_session() {
// Peer must be gone.
assert_eq!(
nodes[1].node.peer_count(), 0,
nodes[1].node.peer_count(),
0,
"Peer should be removed after disconnect"
);
@@ -309,7 +316,8 @@ async fn test_disconnect_clears_session() {
// Before the fix, session_count() would still be 1 here because
// remove_active_peer didn't remove self.sessions[node0_addr].
assert_eq!(
nodes[1].node.session_count(), 0,
nodes[1].node.session_count(),
0,
"Session must be cleaned up when peer is removed (regression: issue #5)"
);
+57 -47
View File
@@ -149,10 +149,8 @@ async fn test_response_transit_needs_recent_request() {
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
node.recent_requests.insert(
444,
RecentRequest::new(make_node_addr(0xDD), now_ms),
);
node.recent_requests
.insert(444, RecentRequest::new(make_node_addr(0xDD), now_ms));
// Handle response — should try to reverse-path forward to 0xDD
// (will fail silently since 0xDD is not an actual peer)
@@ -282,11 +280,7 @@ async fn test_response_coord_substitution_detected() {
let target = *target_identity.node_addr();
let root = make_node_addr(0xF0);
let real_coords = TreeCoordinate::from_addrs(vec![target, root]).unwrap();
let fake_coords = TreeCoordinate::from_addrs(vec![
target,
make_node_addr(0xEE),
root,
]).unwrap();
let fake_coords = TreeCoordinate::from_addrs(vec![target, make_node_addr(0xEE), root]).unwrap();
// Register target in identity_cache
node.register_identity(target, target_identity.pubkey_full());
@@ -325,16 +319,12 @@ async fn test_recent_request_expiry() {
.as_millis() as u64;
// Insert an old request (11 seconds ago)
node.recent_requests.insert(
123,
RecentRequest::new(make_node_addr(1), now_ms - 11_000),
);
node.recent_requests
.insert(123, RecentRequest::new(make_node_addr(1), now_ms - 11_000));
// Insert a recent request
node.recent_requests.insert(
456,
RecentRequest::new(make_node_addr(2), now_ms),
);
node.recent_requests
.insert(456, RecentRequest::new(make_node_addr(2), now_ms));
assert_eq!(node.recent_requests.len(), 2);
@@ -344,7 +334,8 @@ async fn test_recent_request_expiry() {
let coords = TreeCoordinate::from_addrs(vec![origin, make_node_addr(0)]).unwrap();
let request = LookupRequest::new(789, target, origin, coords, 3, 0);
let payload = &request.encode()[1..];
node.handle_lookup_request(&make_node_addr(0xAA), payload).await;
node.handle_lookup_request(&make_node_addr(0xAA), payload)
.await;
// Old entry (123) should be purged, recent entry (456) and new entry (789) kept
assert!(!node.recent_requests.contains_key(&123));
@@ -381,7 +372,10 @@ async fn test_request_forwarding_two_node() {
// Process packets — node1 should receive the forwarded request
tokio::time::sleep(Duration::from_millis(50)).await;
let count = process_available_packets(&mut nodes).await;
assert!(count > 0, "Expected forwarded LookupRequest to arrive at node 1");
assert!(
count > 0,
"Expected forwarded LookupRequest to arrive at node 1"
);
// Node1 should have recorded the request
assert!(
@@ -546,10 +540,7 @@ async fn test_discovery_100_nodes() {
}
// Collect all node addresses and public keys for lookup targets
let all_addrs: Vec<NodeAddr> = nodes
.iter()
.map(|tn| *tn.node.node_addr())
.collect();
let all_addrs: Vec<NodeAddr> = nodes.iter().map(|tn| *tn.node.node_addr()).collect();
let all_pubkeys: Vec<secp256k1::PublicKey> = nodes
.iter()
.map(|tn| tn.node.identity().pubkey_full())
@@ -563,7 +554,8 @@ async fn test_discovery_100_nodes() {
if src == dst {
continue;
}
node.node.register_identity(all_addrs[dst], all_pubkeys[dst]);
node.node
.register_identity(all_addrs[dst], all_pubkeys[dst]);
}
}
@@ -588,10 +580,7 @@ async fn test_discovery_100_nodes() {
let mut initiated = false;
for &(s, dst) in &lookup_pairs {
if s == src {
nodes[src]
.node
.initiate_lookup(&all_addrs[dst], TTL)
.await;
nodes[src].node.initiate_lookup(&all_addrs[dst], TTL).await;
initiated = true;
}
}
@@ -628,7 +617,11 @@ async fn test_discovery_100_nodes() {
let mut failed_pairs: Vec<(usize, usize)> = Vec::new();
for &(src, dst) in &lookup_pairs {
if nodes[src].node.coord_cache().contains(&all_addrs[dst], now_ms) {
if nodes[src]
.node
.coord_cache()
.contains(&all_addrs[dst], now_ms)
{
resolved += 1;
} else {
failed += 1;
@@ -638,9 +631,7 @@ async fn test_discovery_100_nodes() {
}
}
eprintln!(
"\n === Discovery 100-Node Test ===",
);
eprintln!("\n === Discovery 100-Node Test ===",);
eprintln!(
" Lookups: {} | Resolved: {} | Failed: {} | Success rate: {:.1}%",
total_lookups,
@@ -651,8 +642,16 @@ async fn test_discovery_100_nodes() {
// Report coord_cache stats across all nodes
let total_cached: usize = nodes.iter().map(|tn| tn.node.coord_cache().len()).sum();
let min_cached = nodes.iter().map(|tn| tn.node.coord_cache().len()).min().unwrap();
let max_cached = nodes.iter().map(|tn| tn.node.coord_cache().len()).max().unwrap();
let min_cached = nodes
.iter()
.map(|tn| tn.node.coord_cache().len())
.min()
.unwrap();
let max_cached = nodes
.iter()
.map(|tn| tn.node.coord_cache().len())
.max()
.unwrap();
eprintln!(
" Coord cache entries: total={} min={} max={} avg={:.1}",
total_cached,
@@ -663,21 +662,32 @@ async fn test_discovery_100_nodes() {
// Detailed diagnostics for failures (to aid future debugging)
if !failed_pairs.is_empty() {
eprintln!(" --- Failure Diagnostics ({} failures) ---", failed_pairs.len());
eprintln!(
" --- Failure Diagnostics ({} failures) ---",
failed_pairs.len()
);
for &(src, dst) in &failed_pairs {
let src_coords = nodes[src].node.tree_state().my_coords().clone();
let dst_coords = nodes[dst].node.tree_state().my_coords().clone();
let tree_dist = src_coords.distance_to(&dst_coords);
let reverse_cached = nodes[dst].node.coord_cache().contains(&all_addrs[src], now_ms);
let reverse_cached = nodes[dst]
.node
.coord_cache()
.contains(&all_addrs[src], now_ms);
let src_peers = nodes[src].node.peers.len();
let dst_peers = nodes[dst].node.peers.len();
eprintln!(
" node {} -> node {}: tree_dist={} src_depth={} dst_depth={} \
src_peers={} dst_peers={} reverse_cached={}",
src, dst, tree_dist,
src_coords.depth(), dst_coords.depth(),
src_peers, dst_peers, reverse_cached
src,
dst,
tree_dist,
src_coords.depth(),
dst_coords.depth(),
src_peers,
dst_peers,
reverse_cached
);
}
}
@@ -730,7 +740,9 @@ async fn test_response_path_mtu_two_node() {
// Check that path_mtu was stored in the cache entry
let entry = nodes[0].node.coord_cache().get_entry(&node1_addr).unwrap();
let path_mtu = entry.path_mtu().expect("path_mtu should be set from discovery");
let path_mtu = entry
.path_mtu()
.expect("path_mtu should be set from discovery");
// In a 2-node setup, no transit node applies the min() so path_mtu stays u16::MAX
assert_eq!(
path_mtu,
@@ -774,7 +786,9 @@ async fn test_response_path_mtu_three_node_chain() {
// Node1 is transit and applies min(u16::MAX, 1280) = 1280
let entry = nodes[0].node.coord_cache().get_entry(&node2_addr).unwrap();
let path_mtu = entry.path_mtu().expect("path_mtu should be set from discovery");
let path_mtu = entry
.path_mtu()
.expect("path_mtu should be set from discovery");
assert_eq!(
path_mtu, 1280,
"Three-node chain path_mtu should reflect transit node's transport MTU (1280)"
@@ -796,12 +810,8 @@ async fn test_cache_entry_path_mtu_stored() {
let coords = TreeCoordinate::from_addrs(vec![target, make_node_addr(0)]).unwrap();
let now_ms = 1000u64;
node.coord_cache_mut().insert_with_path_mtu(
target,
coords,
now_ms,
1280,
);
node.coord_cache_mut()
.insert_with_path_mtu(target, coords, now_ms, 1280);
let entry = node.coord_cache().get_entry(&target).unwrap();
assert_eq!(entry.path_mtu(), Some(1280));
+8 -4
View File
@@ -6,8 +6,8 @@
use super::*;
use crate::config::EthernetConfig;
use crate::transport::ethernet::EthernetTransport;
use crate::transport::{packet_channel, TransportAddr, TransportHandle, TransportId};
use spanning_tree::{cleanup_nodes, drain_all_packets, initiate_handshake, TestNode};
use crate::transport::{TransportAddr, TransportHandle, TransportId, packet_channel};
use spanning_tree::{TestNode, cleanup_nodes, drain_all_packets, initiate_handshake};
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
@@ -40,7 +40,9 @@ impl VethPair {
// Create veth pair
let status = Command::new("ip")
.args(["link", "add", &name_a, "type", "veth", "peer", "name", &name_b])
.args([
"link", "add", &name_a, "type", "veth", "peer", "name", &name_b,
])
.status()
.expect("failed to run 'ip link add'");
assert!(status.success(), "failed to create veth pair");
@@ -91,7 +93,9 @@ async fn make_test_node_ethernet(interface: &str) -> TestNode {
let mut transport = EthernetTransport::new(transport_id, None, config, packet_tx);
transport.start_async().await.unwrap();
let mac = transport.local_mac().expect("transport should have MAC after start");
let mac = transport
.local_mac()
.expect("transport should have MAC after start");
let addr = TransportAddr::from_bytes(&mac);
node.transports
+58 -31
View File
@@ -5,12 +5,11 @@
//! multi-hop forwarding through live node topologies.
use super::*;
use crate::node::session_wire::{build_fsp_header, FSP_FLAG_CP};
use crate::node::session_wire::{FSP_FLAG_CP, build_fsp_header};
use crate::protocol::{SessionAck, SessionDatagram, SessionSetup, encode_coords};
use crate::tree::TreeCoordinate;
use spanning_tree::{
cleanup_nodes, process_available_packets, run_tree_test, verify_tree_convergence,
TestNode,
TestNode, cleanup_nodes, process_available_packets, run_tree_test, verify_tree_convergence,
};
// ============================================================================
@@ -35,11 +34,11 @@ async fn test_forwarding_hop_limit_exhausted() {
let from = make_node_addr(0xAA);
let src = make_node_addr(0x01);
let dest = make_node_addr(0x02);
let dg = SessionDatagram::new(src, dest, vec![0x10, 0x00, 0x00, 0x00])
.with_ttl(0);
let dg = SessionDatagram::new(src, dest, vec![0x10, 0x00, 0x00, 0x00]).with_ttl(0);
let encoded = dg.encode();
// Dispatch with payload after msg_type byte
node.handle_session_datagram(&from, &encoded[1..], false).await;
node.handle_session_datagram(&from, &encoded[1..], false)
.await;
// No panic, no send (node has no peers)
}
@@ -52,11 +51,11 @@ async fn test_forwarding_hop_limit_one_drops_at_transit() {
let from = make_node_addr(0xAA);
let my_addr = *node.node_addr();
let src = make_node_addr(0x01);
let dg = SessionDatagram::new(src, my_addr, vec![0x10, 0x00, 0x00, 0x00])
.with_ttl(1);
let dg = SessionDatagram::new(src, my_addr, vec![0x10, 0x00, 0x00, 0x00]).with_ttl(1);
let encoded = dg.encode();
// Should succeed — ttl=1 decrements to 0 but packet is still processed
node.handle_session_datagram(&from, &encoded[1..], false).await;
node.handle_session_datagram(&from, &encoded[1..], false)
.await;
}
// --- Local delivery ---
@@ -69,7 +68,8 @@ async fn test_forwarding_local_delivery() {
let dg = SessionDatagram::new(from, my_addr, vec![0x10, 0x00, 0x00, 0x00]);
let encoded = dg.encode();
// Should detect local delivery and return without forwarding
node.handle_session_datagram(&from, &encoded[1..], false).await;
node.handle_session_datagram(&from, &encoded[1..], false)
.await;
}
// --- Direct peer forwarding ---
@@ -135,7 +135,8 @@ async fn test_coord_cache_warming_session_setup() {
// Handle the datagram (will be local delivery or no-route, but cache warming
// happens before routing decision)
node.handle_session_datagram(&from, &encoded[1..], false).await;
node.handle_session_datagram(&from, &encoded[1..], false)
.await;
// After: both src and dest coords should be cached
let cached_src = node.coord_cache().get(&src_addr, now_ms);
@@ -175,15 +176,22 @@ async fn test_coord_cache_warming_session_ack() {
assert!(node.coord_cache().get(&src_addr, now_ms).is_none());
assert!(node.coord_cache().get(&dest_addr, now_ms).is_none());
node.handle_session_datagram(&from, &encoded[1..], false).await;
node.handle_session_datagram(&from, &encoded[1..], false)
.await;
// SessionAck caches both src_coords and dest_coords
let cached_src = node.coord_cache().get(&src_addr, now_ms);
assert!(cached_src.is_some(), "src_addr coords not cached from SessionAck");
assert!(
cached_src.is_some(),
"src_addr coords not cached from SessionAck"
);
assert_eq!(cached_src.unwrap().root_id(), &root_addr);
let cached_dest = node.coord_cache().get(&dest_addr, now_ms);
assert!(cached_dest.is_some(), "dest_addr coords not cached from SessionAck");
assert!(
cached_dest.is_some(),
"dest_addr coords not cached from SessionAck"
);
assert_eq!(cached_dest.unwrap().root_id(), &root_addr);
}
@@ -217,7 +225,8 @@ async fn test_coord_cache_warming_encrypted_msg_with_coords() {
assert!(node.coord_cache().get(&src_addr, now_ms).is_none());
assert!(node.coord_cache().get(&dest_addr, now_ms).is_none());
node.handle_session_datagram(&from, &encoded[1..], false).await;
node.handle_session_datagram(&from, &encoded[1..], false)
.await;
assert!(
node.coord_cache().get(&src_addr, now_ms).is_some(),
@@ -250,7 +259,8 @@ async fn test_coord_cache_warming_encrypted_msg_no_coords() {
.unwrap()
.as_millis() as u64;
node.handle_session_datagram(&from, &encoded[1..], false).await;
node.handle_session_datagram(&from, &encoded[1..], false)
.await;
assert!(
node.coord_cache().get(&src_addr, now_ms).is_none(),
@@ -512,8 +522,16 @@ async fn test_forwarding_with_cache_warming_enables_routing() {
// Give each node coords for its direct peers only
let j_addr = *nodes[j].node.node_addr();
if nodes[i].node.get_peer(&j_addr).is_some() {
let coords = all_coords.iter().find(|(a, _)| a == &j_addr).unwrap().1.clone();
nodes[i].node.coord_cache_mut().insert(j_addr, coords, now_ms);
let coords = all_coords
.iter()
.find(|(a, _)| a == &j_addr)
.unwrap()
.1
.clone();
nodes[i]
.node
.coord_cache_mut()
.insert(j_addr, coords, now_ms);
}
}
}
@@ -572,8 +590,8 @@ async fn test_forwarding_with_cache_warming_enables_routing() {
// ECN Tests
// ============================================================================
use crate::node::handlers::session::mark_ipv6_ecn_ce;
use crate::node::TransportDropState;
use crate::node::handlers::session::mark_ipv6_ecn_ce;
use crate::transport::TransportId;
/// Build a minimal IPv6 header (40 bytes) with specified ECN bits.
@@ -721,10 +739,13 @@ fn test_detect_congestion_with_transport_drops() {
// Simulate transport kernel drops
let tid = TransportId::new(1);
node.transport_drops.insert(tid, TransportDropState {
prev_drops: 100,
dropping: true,
});
node.transport_drops.insert(
tid,
TransportDropState {
prev_drops: 100,
dropping: true,
},
);
// Now detect_congestion should return true (local transport congestion)
assert!(node.detect_congestion(&fake_addr));
@@ -741,10 +762,13 @@ fn test_detect_congestion_disabled_ecn() {
// Even with transport drops, disabled ECN should return false
let tid = TransportId::new(1);
node.transport_drops.insert(tid, TransportDropState {
prev_drops: 50,
dropping: true,
});
node.transport_drops.insert(
tid,
TransportDropState {
prev_drops: 50,
dropping: true,
},
);
let fake_addr = NodeAddr::from_bytes([1; 16]);
assert!(!node.detect_congestion(&fake_addr));
@@ -756,10 +780,13 @@ fn test_sample_transport_congestion() {
// Insert a transport drop state with a baseline
let tid = TransportId::new(1);
node.transport_drops.insert(tid, TransportDropState {
prev_drops: 0,
dropping: false,
});
node.transport_drops.insert(
tid,
TransportDropState {
prev_drops: 0,
dropping: false,
},
);
// No transports registered — sample_transport_congestion is a no-op
// (transport_drops entry stays unchanged)
+215 -107
View File
@@ -5,9 +5,11 @@ use super::*;
#[tokio::test]
async fn test_two_node_handshake_udp() {
use crate::config::UdpConfig;
use crate::node::wire::{
build_encrypted, build_established_header, build_msg1, prepend_inner_header,
};
use crate::transport::udp::UdpTransport;
use crate::node::wire::{build_encrypted, build_established_header, build_msg1, prepend_inner_header};
use tokio::time::{timeout, Duration};
use tokio::time::{Duration, timeout};
// === Setup: Two nodes with UDP transports on localhost ===
@@ -26,10 +28,8 @@ async fn test_two_node_handshake_udp() {
let (packet_tx_a, mut packet_rx_a) = packet_channel(64);
let (packet_tx_b, mut packet_rx_b) = packet_channel(64);
let mut transport_a =
UdpTransport::new(transport_id_a, None, udp_config.clone(), packet_tx_a);
let mut transport_b =
UdpTransport::new(transport_id_b, None, udp_config, packet_tx_b);
let mut transport_a = UdpTransport::new(transport_id_a, None, udp_config.clone(), packet_tx_a);
let mut transport_b = UdpTransport::new(transport_id_b, None, udp_config, packet_tx_b);
transport_a.start_async().await.unwrap();
transport_b.start_async().await.unwrap();
@@ -49,23 +49,20 @@ async fn test_two_node_handshake_udp() {
// === Phase 1: Node A initiates handshake to Node B ===
// Create peer identity for B (must use full key for ECDH parity)
let peer_b_identity =
PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_node_addr = *peer_b_identity.node_addr();
let link_id_a = node_a.allocate_link_id();
let mut conn_a = PeerConnection::outbound(
link_id_a,
peer_b_identity,
1000,
);
let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000);
// Allocate session index for A's outbound
let our_index_a = node_a.index_allocator.allocate().unwrap();
// Start handshake (generates Noise IK msg1)
let our_keypair_a = node_a.identity.keypair();
let noise_msg1 = conn_a.start_handshake(our_keypair_a, node_a.startup_epoch, 1000).unwrap();
let noise_msg1 = conn_a
.start_handshake(our_keypair_a, node_a.startup_epoch, 1000)
.unwrap();
conn_a.set_our_index(our_index_a);
conn_a.set_transport_id(transport_id_a);
conn_a.set_source_addr(remote_addr_b.clone());
@@ -82,10 +79,9 @@ async fn test_two_node_handshake_udp() {
);
node_a.links.insert(link_id_a, link_a);
node_a.connections.insert(link_id_a, conn_a);
node_a.pending_outbound.insert(
(transport_id_a, our_index_a.as_u32()),
link_id_a,
);
node_a
.pending_outbound
.insert((transport_id_a, our_index_a.as_u32()), link_id_a);
// Send msg1 from A to B over UDP
let transport = node_a.transports.get(&transport_id_a).unwrap();
@@ -104,11 +100,13 @@ async fn test_two_node_handshake_udp() {
node_b.handle_msg1(packet_b).await;
// Verify B promoted the inbound connection
let peer_a_node_addr = *PeerIdentity::from_pubkey_full(
node_a.identity.pubkey_full(),
)
.node_addr();
assert_eq!(node_b.peer_count(), 1, "Node B should have 1 peer after msg1");
let peer_a_node_addr =
*PeerIdentity::from_pubkey_full(node_a.identity.pubkey_full()).node_addr();
assert_eq!(
node_b.peer_count(),
1,
"Node B should have 1 peer after msg1"
);
let peer_a_on_b = node_b
.get_peer(&peer_a_node_addr)
.expect("Node B should have peer A");
@@ -134,7 +132,11 @@ async fn test_two_node_handshake_udp() {
node_a.handle_msg2(packet_a).await;
// Verify A promoted the outbound connection
assert_eq!(node_a.peer_count(), 1, "Node A should have 1 peer after msg2");
assert_eq!(
node_a.peer_count(),
1,
"Node A should have 1 peer after msg2"
);
let peer_b_on_a = node_a
.get_peer(&peer_b_node_addr)
.expect("Node A should have peer B");
@@ -241,8 +243,8 @@ async fn test_two_node_handshake_udp() {
#[tokio::test]
async fn test_run_rx_loop_handshake() {
use crate::config::UdpConfig;
use crate::transport::udp::UdpTransport;
use crate::node::wire::build_msg1;
use crate::transport::udp::UdpTransport;
use tokio::time::Duration;
// === Setup: Two nodes with UDP transports on localhost ===
@@ -262,10 +264,8 @@ async fn test_run_rx_loop_handshake() {
let (packet_tx_a, packet_rx_a) = packet_channel(64);
let (packet_tx_b, packet_rx_b) = packet_channel(64);
let mut transport_a =
UdpTransport::new(transport_id_a, None, udp_config.clone(), packet_tx_a);
let mut transport_b =
UdpTransport::new(transport_id_b, None, udp_config, packet_tx_b);
let mut transport_a = UdpTransport::new(transport_id_a, None, udp_config.clone(), packet_tx_a);
let mut transport_b = UdpTransport::new(transport_id_b, None, udp_config, packet_tx_b);
transport_a.start_async().await.unwrap();
transport_b.start_async().await.unwrap();
@@ -290,20 +290,17 @@ async fn test_run_rx_loop_handshake() {
// === Phase 1: Node A initiates handshake to Node B ===
let peer_b_identity =
PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_node_addr = *peer_b_identity.node_addr();
let link_id_a = node_a.allocate_link_id();
let mut conn_a = PeerConnection::outbound(
link_id_a,
peer_b_identity,
1000,
);
let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000);
let our_index_a = node_a.index_allocator.allocate().unwrap();
let our_keypair_a = node_a.identity.keypair();
let noise_msg1 = conn_a.start_handshake(our_keypair_a, node_a.startup_epoch, 1000).unwrap();
let noise_msg1 = conn_a
.start_handshake(our_keypair_a, node_a.startup_epoch, 1000)
.unwrap();
conn_a.set_our_index(our_index_a);
conn_a.set_transport_id(transport_id_a);
conn_a.set_source_addr(remote_addr_b.clone());
@@ -319,10 +316,9 @@ async fn test_run_rx_loop_handshake() {
);
node_a.links.insert(link_id_a, link_a);
node_a.connections.insert(link_id_a, conn_a);
node_a.pending_outbound.insert(
(transport_id_a, our_index_a.as_u32()),
link_id_a,
);
node_a
.pending_outbound
.insert((transport_id_a, our_index_a.as_u32()), link_id_a);
// Send msg1 from A to B over real UDP
let transport = node_a.transports.get(&transport_id_a).unwrap();
@@ -350,12 +346,14 @@ async fn test_run_rx_loop_handshake() {
}
// Verify Node B promoted the inbound connection via rx loop dispatch
let peer_a_node_addr = *PeerIdentity::from_pubkey_full(
node_a.identity.pubkey_full(),
)
.node_addr();
let peer_a_node_addr =
*PeerIdentity::from_pubkey_full(node_a.identity.pubkey_full()).node_addr();
assert_eq!(node_b.peer_count(), 1, "Node B should have 1 peer after rx loop processed msg1");
assert_eq!(
node_b.peer_count(),
1,
"Node B should have 1 peer after rx loop processed msg1"
);
let peer_a_on_b = node_b
.get_peer(&peer_a_node_addr)
.expect("Node B should have peer A");
@@ -390,7 +388,11 @@ async fn test_run_rx_loop_handshake() {
}
// Verify Node A promoted the outbound connection via rx loop dispatch
assert_eq!(node_a.peer_count(), 1, "Node A should have 1 peer after rx loop processed msg2");
assert_eq!(
node_a.peer_count(),
1,
"Node A should have 1 peer after rx loop processed msg2"
);
let peer_b_on_a = node_a
.get_peer(&peer_b_node_addr)
.expect("Node A should have peer B");
@@ -432,9 +434,9 @@ async fn test_run_rx_loop_handshake() {
#[tokio::test]
async fn test_cross_connection_both_initiate() {
use crate::config::UdpConfig;
use crate::transport::udp::UdpTransport;
use crate::node::wire::build_msg1;
use tokio::time::{timeout, Duration};
use crate::transport::udp::UdpTransport;
use tokio::time::{Duration, timeout};
// === Setup: Two nodes with UDP transports on localhost ===
@@ -453,10 +455,8 @@ async fn test_cross_connection_both_initiate() {
let (packet_tx_a, mut packet_rx_a) = packet_channel(64);
let (packet_tx_b, mut packet_rx_b) = packet_channel(64);
let mut transport_a =
UdpTransport::new(transport_id_a, None, udp_config.clone(), packet_tx_a);
let mut transport_b =
UdpTransport::new(transport_id_b, None, udp_config, packet_tx_b);
let mut transport_a = UdpTransport::new(transport_id_a, None, udp_config.clone(), packet_tx_a);
let mut transport_b = UdpTransport::new(transport_id_b, None, udp_config, packet_tx_b);
transport_a.start_async().await.unwrap();
transport_b.start_async().await.unwrap();
@@ -474,11 +474,9 @@ async fn test_cross_connection_both_initiate() {
.insert(transport_id_b, TransportHandle::Udp(transport_b));
// Peer identities (must use full key for ECDH parity)
let peer_b_identity =
PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_node_addr = *peer_b_identity.node_addr();
let peer_a_identity =
PeerIdentity::from_pubkey_full(node_a.identity.pubkey_full());
let peer_a_identity = PeerIdentity::from_pubkey_full(node_a.identity.pubkey_full());
let peer_a_node_addr = *peer_a_identity.node_addr();
// === Phase 1: Both nodes initiate handshakes (simulate auto_connect) ===
@@ -488,7 +486,9 @@ async fn test_cross_connection_both_initiate() {
let mut conn_a = PeerConnection::outbound(link_id_a_out, peer_b_identity, 1000);
let our_index_a = node_a.index_allocator.allocate().unwrap();
let our_keypair_a = node_a.identity.keypair();
let noise_msg1_a = conn_a.start_handshake(our_keypair_a, node_a.startup_epoch, 1000).unwrap();
let noise_msg1_a = conn_a
.start_handshake(our_keypair_a, node_a.startup_epoch, 1000)
.unwrap();
conn_a.set_our_index(our_index_a);
conn_a.set_transport_id(transport_id_a);
conn_a.set_source_addr(remote_addr_b.clone());
@@ -496,20 +496,29 @@ async fn test_cross_connection_both_initiate() {
let wire_msg1_a = build_msg1(our_index_a, &noise_msg1_a);
let link_a_out = Link::connectionless(
link_id_a_out, transport_id_a, remote_addr_b.clone(),
LinkDirection::Outbound, Duration::from_millis(100),
link_id_a_out,
transport_id_a,
remote_addr_b.clone(),
LinkDirection::Outbound,
Duration::from_millis(100),
);
node_a.links.insert(link_id_a_out, link_a_out);
node_a.addr_to_link.insert((transport_id_a, remote_addr_b.clone()), link_id_a_out);
node_a
.addr_to_link
.insert((transport_id_a, remote_addr_b.clone()), link_id_a_out);
node_a.connections.insert(link_id_a_out, conn_a);
node_a.pending_outbound.insert((transport_id_a, our_index_a.as_u32()), link_id_a_out);
node_a
.pending_outbound
.insert((transport_id_a, our_index_a.as_u32()), link_id_a_out);
// Node B initiates to Node A
let link_id_b_out = node_b.allocate_link_id();
let mut conn_b = PeerConnection::outbound(link_id_b_out, peer_a_identity, 1000);
let our_index_b = node_b.index_allocator.allocate().unwrap();
let our_keypair_b = node_b.identity.keypair();
let noise_msg1_b = conn_b.start_handshake(our_keypair_b, node_b.startup_epoch, 1000).unwrap();
let noise_msg1_b = conn_b
.start_handshake(our_keypair_b, node_b.startup_epoch, 1000)
.unwrap();
conn_b.set_our_index(our_index_b);
conn_b.set_transport_id(transport_id_b);
conn_b.set_source_addr(remote_addr_a.clone());
@@ -517,20 +526,33 @@ async fn test_cross_connection_both_initiate() {
let wire_msg1_b = build_msg1(our_index_b, &noise_msg1_b);
let link_b_out = Link::connectionless(
link_id_b_out, transport_id_b, remote_addr_a.clone(),
LinkDirection::Outbound, Duration::from_millis(100),
link_id_b_out,
transport_id_b,
remote_addr_a.clone(),
LinkDirection::Outbound,
Duration::from_millis(100),
);
node_b.links.insert(link_id_b_out, link_b_out);
node_b.addr_to_link.insert((transport_id_b, remote_addr_a.clone()), link_id_b_out);
node_b
.addr_to_link
.insert((transport_id_b, remote_addr_a.clone()), link_id_b_out);
node_b.connections.insert(link_id_b_out, conn_b);
node_b.pending_outbound.insert((transport_id_b, our_index_b.as_u32()), link_id_b_out);
node_b
.pending_outbound
.insert((transport_id_b, our_index_b.as_u32()), link_id_b_out);
// Both send msg1 over UDP
let transport = node_a.transports.get(&transport_id_a).unwrap();
transport.send(&remote_addr_b, &wire_msg1_a).await.expect("A send msg1");
transport
.send(&remote_addr_b, &wire_msg1_a)
.await
.expect("A send msg1");
let transport = node_b.transports.get(&transport_id_b).unwrap();
transport.send(&remote_addr_a, &wire_msg1_b).await.expect("B send msg1");
transport
.send(&remote_addr_a, &wire_msg1_b)
.await
.expect("B send msg1");
// === Phase 2: Both nodes receive the other's msg1 ===
// Before the fix, addr_to_link would reject these because outbound links
@@ -538,21 +560,39 @@ async fn test_cross_connection_both_initiate() {
// B receives A's msg1
let packet_at_b = timeout(Duration::from_secs(1), packet_rx_b.recv())
.await.expect("Timeout").expect("Channel closed");
.await
.expect("Timeout")
.expect("Channel closed");
node_b.handle_msg1(packet_at_b).await;
// B should have promoted the inbound connection
assert_eq!(node_b.peer_count(), 1, "Node B should have 1 peer after processing A's msg1");
assert!(node_b.get_peer(&peer_a_node_addr).is_some(), "Node B should have peer A");
assert_eq!(
node_b.peer_count(),
1,
"Node B should have 1 peer after processing A's msg1"
);
assert!(
node_b.get_peer(&peer_a_node_addr).is_some(),
"Node B should have peer A"
);
// A receives B's msg1
let packet_at_a = timeout(Duration::from_secs(1), packet_rx_a.recv())
.await.expect("Timeout").expect("Channel closed");
.await
.expect("Timeout")
.expect("Channel closed");
node_a.handle_msg1(packet_at_a).await;
// A should have promoted the inbound connection
assert_eq!(node_a.peer_count(), 1, "Node A should have 1 peer after processing B's msg1");
assert!(node_a.get_peer(&peer_b_node_addr).is_some(), "Node A should have peer B");
assert_eq!(
node_a.peer_count(),
1,
"Node A should have 1 peer after processing B's msg1"
);
assert!(
node_a.get_peer(&peer_b_node_addr).is_some(),
"Node A should have peer B"
);
// === Phase 3: Both nodes receive msg2 responses ===
// The msg2 was sent during handle_msg1 processing. When handle_msg2
@@ -560,21 +600,37 @@ async fn test_cross_connection_both_initiate() {
// A receives B's msg2 (response to A's original msg1)
let msg2_at_a = timeout(Duration::from_secs(1), packet_rx_a.recv())
.await.expect("Timeout waiting for msg2 at A").expect("Channel closed");
.await
.expect("Timeout waiting for msg2 at A")
.expect("Channel closed");
node_a.handle_msg2(msg2_at_a).await;
// B receives A's msg2 (response to B's original msg1)
let msg2_at_b = timeout(Duration::from_secs(1), packet_rx_b.recv())
.await.expect("Timeout waiting for msg2 at B").expect("Channel closed");
.await
.expect("Timeout waiting for msg2 at B")
.expect("Channel closed");
node_b.handle_msg2(msg2_at_b).await;
// === Verification ===
// Both nodes should have exactly 1 peer each after cross-connection resolution
assert_eq!(node_a.peer_count(), 1, "Node A should have exactly 1 peer after cross-connection");
assert_eq!(node_b.peer_count(), 1, "Node B should have exactly 1 peer after cross-connection");
assert_eq!(
node_a.peer_count(),
1,
"Node A should have exactly 1 peer after cross-connection"
);
assert_eq!(
node_b.peer_count(),
1,
"Node B should have exactly 1 peer after cross-connection"
);
let peer_b_on_a = node_a.get_peer(&peer_b_node_addr).expect("A should have peer B");
let peer_a_on_b = node_b.get_peer(&peer_a_node_addr).expect("B should have peer A");
let peer_b_on_a = node_a
.get_peer(&peer_b_node_addr)
.expect("A should have peer B");
let peer_a_on_b = node_b
.get_peer(&peer_a_node_addr)
.expect("B should have peer A");
assert!(peer_b_on_a.has_session(), "Peer B on A should have session");
assert!(peer_a_on_b.has_session(), "Peer A on B should have session");
@@ -611,25 +667,35 @@ async fn test_stale_connection_cleanup() {
// Allocate session index and set transport info
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let _noise_msg1 = conn.start_handshake(our_keypair, node.startup_epoch, past_time_ms).unwrap();
let _noise_msg1 = conn
.start_handshake(our_keypair, node.startup_epoch, past_time_ms)
.unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
conn.set_source_addr(remote_addr.clone());
// Set up all the state that initiate_peer_connection would create
let link = Link::connectionless(
link_id, transport_id, remote_addr.clone(),
LinkDirection::Outbound, Duration::from_millis(100),
link_id,
transport_id,
remote_addr.clone(),
LinkDirection::Outbound,
Duration::from_millis(100),
);
node.links.insert(link_id, link);
node.addr_to_link.insert((transport_id, remote_addr.clone()), link_id);
node.addr_to_link
.insert((transport_id, remote_addr.clone()), link_id);
node.connections.insert(link_id, conn);
node.pending_outbound.insert((transport_id, our_index.as_u32()), link_id);
node.pending_outbound
.insert((transport_id, our_index.as_u32()), link_id);
// Verify state before timeout check
assert_eq!(node.connection_count(), 1);
assert_eq!(node.link_count(), 1);
assert!(node.pending_outbound.contains_key(&(transport_id, our_index.as_u32())));
assert!(
node.pending_outbound
.contains_key(&(transport_id, our_index.as_u32()))
);
assert_eq!(node.index_allocator.count(), 1);
// Connection was created at time 1000ms. check_timeouts uses SystemTime::now(),
@@ -637,13 +703,27 @@ async fn test_stale_connection_cleanup() {
node.check_timeouts();
// Verify everything was cleaned up
assert_eq!(node.connection_count(), 0, "Stale connection should be removed");
assert_eq!(
node.connection_count(),
0,
"Stale connection should be removed"
);
assert_eq!(node.link_count(), 0, "Stale link should be removed");
assert!(!node.pending_outbound.contains_key(&(transport_id, our_index.as_u32())),
"pending_outbound should be cleaned up");
assert_eq!(node.index_allocator.count(), 0, "Session index should be freed");
assert!(!node.addr_to_link.contains_key(&(transport_id, remote_addr)),
"addr_to_link should be cleaned up");
assert!(
!node
.pending_outbound
.contains_key(&(transport_id, our_index.as_u32())),
"pending_outbound should be cleaned up"
);
assert_eq!(
node.index_allocator.count(),
0,
"Session index should be freed"
);
assert!(
!node.addr_to_link.contains_key(&(transport_id, remote_addr)),
"addr_to_link should be cleaned up"
);
}
/// Test that failed connections are cleaned up by check_timeouts().
@@ -665,29 +745,44 @@ async fn test_failed_connection_cleanup() {
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let _noise_msg1 = conn.start_handshake(our_keypair, node.startup_epoch, now_ms).unwrap();
let _noise_msg1 = conn
.start_handshake(our_keypair, node.startup_epoch, now_ms)
.unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
conn.set_source_addr(remote_addr.clone());
conn.mark_failed(); // Simulate send failure
let link = Link::connectionless(
link_id, transport_id, remote_addr.clone(),
LinkDirection::Outbound, Duration::from_millis(100),
link_id,
transport_id,
remote_addr.clone(),
LinkDirection::Outbound,
Duration::from_millis(100),
);
node.links.insert(link_id, link);
node.addr_to_link.insert((transport_id, remote_addr.clone()), link_id);
node.addr_to_link
.insert((transport_id, remote_addr.clone()), link_id);
node.connections.insert(link_id, conn);
node.pending_outbound.insert((transport_id, our_index.as_u32()), link_id);
node.pending_outbound
.insert((transport_id, our_index.as_u32()), link_id);
assert_eq!(node.connection_count(), 1);
// Failed connections should be cleaned up immediately regardless of age
node.check_timeouts();
assert_eq!(node.connection_count(), 0, "Failed connection should be removed");
assert_eq!(
node.connection_count(),
0,
"Failed connection should be removed"
);
assert_eq!(node.link_count(), 0, "Failed link should be removed");
assert_eq!(node.index_allocator.count(), 0, "Session index should be freed");
assert_eq!(
node.index_allocator.count(),
0,
"Session index should be freed"
);
}
/// Test that msg1 bytes are stored on connection for resend.
@@ -710,7 +805,9 @@ async fn test_msg1_stored_for_resend() {
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let noise_msg1 = conn.start_handshake(our_keypair, node.startup_epoch, now_ms).unwrap();
let noise_msg1 = conn
.start_handshake(our_keypair, node.startup_epoch, now_ms)
.unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
conn.set_source_addr(remote_addr.clone());
@@ -741,7 +838,9 @@ async fn test_resend_scheduling() {
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let noise_msg1 = conn.start_handshake(our_keypair, node.startup_epoch, now_ms).unwrap();
let noise_msg1 = conn
.start_handshake(our_keypair, node.startup_epoch, now_ms)
.unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
conn.set_source_addr(remote_addr.clone());
@@ -751,12 +850,17 @@ async fn test_resend_scheduling() {
conn.set_handshake_msg1(wire_msg1, now_ms + 1000);
let link = Link::connectionless(
link_id, transport_id, remote_addr.clone(),
LinkDirection::Outbound, Duration::from_millis(100),
link_id,
transport_id,
remote_addr.clone(),
LinkDirection::Outbound,
Duration::from_millis(100),
);
node.links.insert(link_id, link);
node.addr_to_link.insert((transport_id, remote_addr), link_id);
node.pending_outbound.insert((transport_id, our_index.as_u32()), link_id);
node.addr_to_link
.insert((transport_id, remote_addr), link_id);
node.pending_outbound
.insert((transport_id, our_index.as_u32()), link_id);
node.connections.insert(link_id, conn);
// Before resend time: nothing should happen (no transport = can't send,
@@ -772,7 +876,11 @@ async fn test_resend_scheduling() {
// No transport registered, so send fails — count stays 0.
// That's the expected behavior (transport absence is a transient condition).
let conn = node.connections.get(&link_id).unwrap();
assert_eq!(conn.resend_count(), 0, "No transport means no resend recorded");
assert_eq!(
conn.resend_count(),
0,
"No transport means no resend recorded"
);
}
/// Test that msg2 is stored on PeerConnection for responder resend.
+5 -3
View File
@@ -1,7 +1,7 @@
use super::*;
use crate::utils::index::SessionIndex;
use crate::transport::{packet_channel, LinkDirection, TransportAddr};
use crate::PeerIdentity;
use crate::transport::{LinkDirection, TransportAddr, packet_channel};
use crate::utils::index::SessionIndex;
use std::time::Duration;
mod bloom;
@@ -54,7 +54,9 @@ pub(super) fn make_completed_connection(
// Run initiator side of handshake
let our_keypair = node.identity.keypair();
let msg1 = conn.start_handshake(our_keypair, node.startup_epoch, current_time_ms).unwrap();
let msg1 = conn
.start_handshake(our_keypair, node.startup_epoch, current_time_ms)
.unwrap();
// Run responder side to generate msg2
let mut resp_conn = PeerConnection::inbound(LinkId::new(999), current_time_ms);
+58 -41
View File
@@ -7,8 +7,8 @@ use super::*;
use crate::bloom::BloomFilter;
use crate::tree::{ParentDeclaration, TreeCoordinate};
use spanning_tree::{
cleanup_nodes, drain_all_packets, generate_random_edges, initiate_handshake, make_test_node,
run_tree_test, verify_tree_convergence, TestNode,
TestNode, cleanup_nodes, drain_all_packets, generate_random_edges, initiate_handshake,
make_test_node, run_tree_test, verify_tree_convergence,
};
use std::collections::HashSet;
@@ -83,8 +83,7 @@ fn test_routing_bloom_filter_hit() {
// 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 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)
@@ -126,17 +125,14 @@ fn test_routing_bloom_filter_multiple_hits_tiebreak() {
// 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,
);
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 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)
@@ -187,8 +183,7 @@ fn test_routing_tree_fallback() {
// Destination: a node under our peer in the tree
let dest = make_node_addr(99);
let dest_coords =
TreeCoordinate::from_addrs(vec![dest, peer_addr, my_addr]).unwrap();
let dest_coords = TreeCoordinate::from_addrs(vec![dest, peer_addr, my_addr]).unwrap();
// Put dest coords in the cache
let now_ms = std::time::SystemTime::now()
@@ -239,8 +234,7 @@ fn test_routing_refreshes_coord_cache_ttl() {
// Set up tree coordinates
let dest = make_node_addr(99);
let dest_coords =
TreeCoordinate::from_addrs(vec![dest, peer_addr, my_addr]).unwrap();
let dest_coords = TreeCoordinate::from_addrs(vec![dest, peer_addr, my_addr]).unwrap();
node.tree_state_mut().update_peer(
ParentDeclaration::new(peer_addr, my_addr, 1, 1000),
TreeCoordinate::from_addrs(vec![peer_addr, my_addr]).unwrap(),
@@ -252,7 +246,8 @@ fn test_routing_refreshes_coord_cache_ttl() {
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let short_ttl = 10_000; // 10 seconds
node.coord_cache_mut().insert_with_ttl(dest, dest_coords, now_ms, short_ttl);
node.coord_cache_mut()
.insert_with_ttl(dest, dest_coords, now_ms, short_ttl);
let original_expiry = node.coord_cache().get_entry(&dest).unwrap().expires_at();
// find_next_hop should succeed and refresh TTL to now + default_ttl (300s)
@@ -263,7 +258,8 @@ fn test_routing_refreshes_coord_cache_ttl() {
assert!(
new_expiry > original_expiry,
"find_next_hop should refresh the coord_cache TTL: original={}, new={}",
original_expiry, new_expiry,
original_expiry,
new_expiry,
);
}
@@ -384,11 +380,7 @@ async fn test_routing_chain_topology() {
// Verify tree convergence
let root = nodes.iter().map(|n| *n.node.node_addr()).min().unwrap();
for tn in &nodes {
assert_eq!(
*tn.node.tree_state().root(),
root,
"Tree not converged"
);
assert_eq!(*tn.node.tree_state().root(), root, "Tree not converged");
}
// Populate coord caches: each node caches the far-end node's coords
@@ -453,8 +445,13 @@ async fn test_routing_bloom_preferred_over_tree() {
// 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();
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()
@@ -602,13 +599,20 @@ async fn test_routing_reachability_100_nodes() {
// Collect all (addr, coords) pairs first to avoid borrow issues
let all_coords: Vec<(NodeAddr, TreeCoordinate)> = nodes
.iter()
.map(|tn| (*tn.node.node_addr(), tn.node.tree_state().my_coords().clone()))
.map(|tn| {
(
*tn.node.node_addr(),
tn.node.tree_state().my_coords().clone(),
)
})
.collect();
for node in &mut nodes {
for (addr, coords) in &all_coords {
if addr != node.node.node_addr() {
node.node.coord_cache_mut().insert(*addr, coords.clone(), now_ms);
node.node
.coord_cache_mut()
.insert(*addr, coords.clone(), now_ms);
}
}
}
@@ -654,10 +658,7 @@ async fn test_routing_reachability_100_nodes() {
0.0
};
eprintln!(
"\n === Routing Reachability ({} nodes) ===",
NUM_NODES
);
eprintln!("\n === Routing Reachability ({} nodes) ===", NUM_NODES);
eprintln!(
" Pairs tested: {} | Delivered: {} | Failed: {} | Loops: {}",
total_pairs,
@@ -665,10 +666,7 @@ async fn test_routing_reachability_100_nodes() {
failures.len(),
loops.len()
);
eprintln!(
" Hops: avg={:.1} max={}",
avg_hops, max_hops
);
eprintln!(" Hops: avg={:.1} max={}", avg_hops, max_hops);
if !failures.is_empty() {
let show = failures.len().min(10);
@@ -736,13 +734,20 @@ async fn test_routing_stops_after_peer_removal() {
let all_coords: Vec<(NodeAddr, crate::tree::TreeCoordinate)> = nodes
.iter()
.map(|tn| (*tn.node.node_addr(), tn.node.tree_state().my_coords().clone()))
.map(|tn| {
(
*tn.node.node_addr(),
tn.node.tree_state().my_coords().clone(),
)
})
.collect();
for node in &mut nodes {
for (addr, coords) in &all_coords {
if addr != node.node.node_addr() {
node.node.coord_cache_mut().insert(*addr, coords.clone(), now_ms);
node.node
.coord_cache_mut()
.insert(*addr, coords.clone(), now_ms);
}
}
}
@@ -792,9 +797,12 @@ async fn test_routing_stops_after_peer_removal() {
// matters is that delivery does NOT succeed.
match simulate_forwarding(&mut nodes, &addr_index, 0, 3) {
ForwardResult::NoRoute { .. } => {} // Expected: can't reach node 3
ForwardResult::Loop { .. } => {} // Also acceptable: stale coords cause loop detection
ForwardResult::Loop { .. } => {} // Also acceptable: stale coords cause loop detection
ForwardResult::Delivered(hops) => {
panic!("Should NOT deliver after partition, but got delivery in {} hops", hops);
panic!(
"Should NOT deliver after partition, but got delivery in {} hops",
hops
);
}
}
@@ -905,7 +913,12 @@ async fn test_routing_source_only_coords_100_nodes() {
// Collect all coords for injection
let all_coords: Vec<(NodeAddr, crate::tree::TreeCoordinate)> = nodes
.iter()
.map(|tn| (*tn.node.node_addr(), tn.node.tree_state().my_coords().clone()))
.map(|tn| {
(
*tn.node.node_addr(),
tn.node.tree_state().my_coords().clone(),
)
})
.collect();
let addr_index = build_addr_index(&nodes);
@@ -946,7 +959,10 @@ async fn test_routing_source_only_coords_100_nodes() {
ForwardResult::Delivered(_) => source_only_delivered += 1,
ForwardResult::NoRoute { .. } => source_only_failed += 1,
ForwardResult::Loop { .. } => {
panic!("Routing loop detected with source-only coords: {} -> {}", src, dst);
panic!(
"Routing loop detected with source-only coords: {} -> {}",
src, dst
);
}
}
}
@@ -977,7 +993,9 @@ async fn test_routing_source_only_coords_100_nodes() {
for node in &mut nodes {
for (addr, coords) in &all_coords {
if addr != node.node.node_addr() {
node.node.coord_cache_mut().insert(*addr, coords.clone(), now_ms);
node.node
.coord_cache_mut()
.insert(*addr, coords.clone(), now_ms);
}
}
}
@@ -996,4 +1014,3 @@ async fn test_routing_source_only_coords_100_nodes() {
cleanup_nodes(&mut nodes).await;
}
+337 -190
View File
@@ -3,8 +3,8 @@
use super::*;
use crate::node::session::EndToEndState;
use crate::node::tests::spanning_tree::{
cleanup_nodes, generate_random_edges, process_available_packets, run_tree_test,
run_tree_test_with_mtus, verify_tree_convergence, TestNode,
TestNode, cleanup_nodes, generate_random_edges, process_available_packets, run_tree_test,
run_tree_test_with_mtus, verify_tree_convergence,
};
use crate::protocol::{SessionAck, SessionDatagram};
@@ -50,10 +50,7 @@ fn test_session_entry_new_initiating() {
let identity_a = Identity::generate();
let identity_b = Identity::generate();
let handshake = HandshakeState::new_initiator(
identity_a.keypair(),
identity_b.pubkey_full(),
);
let handshake = HandshakeState::new_initiator(identity_a.keypair(), identity_b.pubkey_full());
let entry = crate::node::session::SessionEntry::new(
*identity_b.node_addr(),
@@ -77,10 +74,7 @@ fn test_session_entry_touch() {
let identity_a = Identity::generate();
let identity_b = Identity::generate();
let handshake = HandshakeState::new_initiator(
identity_a.keypair(),
identity_b.pubkey_full(),
);
let handshake = HandshakeState::new_initiator(identity_a.keypair(), identity_b.pubkey_full());
let mut entry = crate::node::session::SessionEntry::new(
*identity_b.node_addr(),
@@ -102,10 +96,8 @@ fn test_session_table_operations() {
let mut node = make_node();
let identity_b = Identity::generate();
let handshake = HandshakeState::new_initiator(
node.identity().keypair(),
identity_b.pubkey_full(),
);
let handshake =
HandshakeState::new_initiator(node.identity().keypair(), identity_b.pubkey_full());
let dest_addr = *identity_b.node_addr();
let entry = crate::node::session::SessionEntry::new(
@@ -151,12 +143,14 @@ async fn test_session_direct_peer_handshake() {
// Node 0 should have a session in Initiating state
assert_eq!(nodes[0].node.session_count(), 1);
assert!(nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_initiating());
assert!(
nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_initiating()
);
// Process packets: SessionSetup arrives at Node 1
tokio::time::sleep(Duration::from_millis(20)).await;
@@ -165,12 +159,14 @@ async fn test_session_direct_peer_handshake() {
// Node 1 should now have a session in AwaitingMsg3 state (XK: identity not yet known)
assert_eq!(nodes[1].node.session_count(), 1);
assert!(nodes[1]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_awaiting_msg3());
assert!(
nodes[1]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_awaiting_msg3()
);
// Process packets: SessionAck arrives at Node 0, Node 0 sends SessionMsg3
tokio::time::sleep(Duration::from_millis(20)).await;
@@ -178,12 +174,14 @@ async fn test_session_direct_peer_handshake() {
assert!(count > 0, "Expected SessionAck packet to arrive");
// Node 0 should now be Established (transitions after sending msg3)
assert!(nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_established());
assert!(
nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_established()
);
// Process packets: SessionMsg3 arrives at Node 1
tokio::time::sleep(Duration::from_millis(20)).await;
@@ -191,12 +189,14 @@ async fn test_session_direct_peer_handshake() {
assert!(count > 0, "Expected SessionMsg3 packet to arrive");
// Node 1 should now be Established (transitions after processing msg3)
assert!(nodes[1]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_established());
assert!(
nodes[1]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_established()
);
cleanup_nodes(&mut nodes).await;
}
@@ -226,18 +226,22 @@ async fn test_session_direct_peer_data_transfer() {
tokio::time::sleep(Duration::from_millis(20)).await;
process_available_packets(&mut nodes).await; // Msg3 → Node 1
assert!(nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_established());
assert!(nodes[1]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_established());
assert!(
nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_established()
);
assert!(
nodes[1]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_established()
);
// Send data from Node 0 to Node 1
let test_data = b"Hello, FIPS session!";
@@ -291,12 +295,14 @@ async fn test_session_3node_forwarded_handshake() {
nodes[2].node.get_session(&node0_addr).is_some(),
"Node 2 should have a session entry for Node 0"
);
assert!(nodes[2]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_awaiting_msg3());
assert!(
nodes[2]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_awaiting_msg3()
);
// Process: SessionAck: 2→1 (forwarded by transit B)
tokio::time::sleep(Duration::from_millis(20)).await;
@@ -307,12 +313,14 @@ async fn test_session_3node_forwarded_handshake() {
process_available_packets(&mut nodes).await;
// Node 0 should now be Established (transitions after sending msg3)
assert!(nodes[0]
.node
.get_session(&node2_addr)
.unwrap()
.state()
.is_established());
assert!(
nodes[0]
.node
.get_session(&node2_addr)
.unwrap()
.state()
.is_established()
);
// Process: SessionMsg3: 0→1 (forwarded by transit B)
tokio::time::sleep(Duration::from_millis(20)).await;
@@ -323,12 +331,14 @@ async fn test_session_3node_forwarded_handshake() {
process_available_packets(&mut nodes).await;
// Node 2 should now be Established (transitions after processing msg3)
assert!(nodes[2]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_established());
assert!(
nodes[2]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_established()
);
// Transit node B should NOT have a session
assert_eq!(
@@ -389,12 +399,14 @@ async fn test_session_3node_forwarded_data() {
}
// Node 2 should be Established (transitioned during XK handshake msg3)
assert!(nodes[2]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_established());
assert!(
nodes[2]
.node
.get_session(&node0_addr)
.unwrap()
.state()
.is_established()
);
cleanup_nodes(&mut nodes).await;
}
@@ -520,12 +532,7 @@ async fn test_session_100_nodes() {
// Collect identities: (node_addr, pubkey) for all nodes
let all_info: Vec<(NodeAddr, secp256k1::PublicKey)> = nodes
.iter()
.map(|tn| {
(
*tn.node.node_addr(),
tn.node.identity().pubkey_full(),
)
})
.map(|tn| (*tn.node.node_addr(), tn.node.identity().pubkey_full()))
.collect();
// Each node picks one random target for its outbound session.
@@ -640,11 +647,7 @@ async fn test_session_100_nodes() {
// (Responder should already be Established after XK msg3)
let rev_payload = format!("rev-{}", pair_idx).into_bytes();
let rev_ipv6 = build_ipv6_packet(&dst_fips, &src_fips, &rev_payload);
match nodes[dst]
.node
.send_ipv6_packet(&src_addr, &rev_ipv6)
.await
{
match nodes[dst].node.send_ipv6_packet(&src_addr, &rev_ipv6).await {
Ok(()) => send_reverse_ok += 1,
Err(_) => send_reverse_err += 1,
}
@@ -723,10 +726,7 @@ async fn test_session_100_nodes() {
}
}
let session_counts: Vec<usize> = nodes
.iter()
.map(|tn| tn.node.session_count())
.collect();
let session_counts: Vec<usize> = nodes.iter().map(|tn| tn.node.session_count()).collect();
let total_sessions: usize = session_counts.iter().sum();
let min_sessions = *session_counts.iter().min().unwrap();
let max_sessions = *session_counts.iter().max().unwrap();
@@ -770,10 +770,8 @@ async fn test_session_100_nodes() {
};
// Coord cache stats
let coord_cache_sizes: Vec<usize> = nodes
.iter()
.map(|tn| tn.node.coord_cache().len())
.collect();
let coord_cache_sizes: Vec<usize> =
nodes.iter().map(|tn| tn.node.coord_cache().len()).collect();
let total_coord_entries: usize = coord_cache_sizes.iter().sum();
let min_coord = *coord_cache_sizes.iter().min().unwrap();
let max_coord = *coord_cache_sizes.iter().max().unwrap();
@@ -884,10 +882,7 @@ async fn test_session_100_nodes() {
// === Assertions ===
assert_eq!(
send_forward_err, 0,
"All forward sends should succeed"
);
assert_eq!(send_forward_err, 0, "All forward sends should succeed");
assert_eq!(
send_reverse_err, 0,
"All reverse sends should succeed (responder Established after XK msg3)"
@@ -915,7 +910,11 @@ async fn test_session_100_nodes() {
// ============================================================================
/// Build a minimal valid IPv6 packet with given source and destination addresses.
fn build_ipv6_packet(src: &crate::FipsAddress, dst: &crate::FipsAddress, payload: &[u8]) -> Vec<u8> {
fn build_ipv6_packet(
src: &crate::FipsAddress,
dst: &crate::FipsAddress,
payload: &[u8],
) -> Vec<u8> {
let payload_len = payload.len() as u16;
let mut packet = vec![0u8; 40 + payload.len()];
// Version (6) + traffic class high nibble
@@ -944,17 +943,14 @@ fn test_identity_cache_populated_on_promote() {
let transport_id = TransportId::new(1);
let link_id = LinkId::new(1);
let (conn, peer_identity) = make_completed_connection(
&mut node,
link_id,
transport_id,
1000,
);
let (conn, peer_identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
node.add_connection(conn).unwrap();
// Promote
let result = node.promote_connection(link_id, peer_identity, 2000).unwrap();
let result = node
.promote_connection(link_id, peer_identity, 2000)
.unwrap();
assert!(matches!(result, PromotionResult::Promoted(_)));
// Identity cache should contain the peer
@@ -962,7 +958,10 @@ fn test_identity_cache_populated_on_promote() {
let mut prefix = [0u8; 15];
prefix.copy_from_slice(&peer_addr.as_bytes()[0..15]);
let cached = node.lookup_by_fips_prefix(&prefix);
assert!(cached.is_some(), "Identity cache should contain promoted peer");
assert!(
cached.is_some(),
"Identity cache should contain promoted peer"
);
let (cached_addr, cached_pk) = cached.unwrap();
assert_eq!(cached_addr, peer_addr);
assert_eq!(cached_pk, peer_identity.pubkey_full());
@@ -986,7 +985,11 @@ async fn test_tun_outbound_established_session() {
let dst_fips = crate::FipsAddress::from_node_addr(&node1_addr);
// Establish session (XK: 3 messages — Setup, Ack, Msg3)
nodes[0].node.initiate_session(node1_addr, node1_pubkey).await.unwrap();
nodes[0]
.node
.initiate_session(node1_addr, node1_pubkey)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(20)).await;
process_available_packets(&mut nodes).await; // Setup → Node 1
tokio::time::sleep(Duration::from_millis(20)).await;
@@ -994,7 +997,14 @@ async fn test_tun_outbound_established_session() {
tokio::time::sleep(Duration::from_millis(20)).await;
process_available_packets(&mut nodes).await; // Msg3 → Node 1
assert!(nodes[0].node.get_session(&node1_addr).unwrap().state().is_established());
assert!(
nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_established()
);
// Install TUN receiver on Node 1
let (tun_tx, tun_rx) = std::sync::mpsc::channel();
@@ -1013,7 +1023,10 @@ async fn test_tun_outbound_established_session() {
// Verify plaintext arrived at Node 1's TUN
let delivered: Vec<Vec<u8>> = std::iter::from_fn(|| tun_rx.try_recv().ok()).collect();
assert_eq!(delivered.len(), 1, "Exactly one packet should be delivered");
assert_eq!(delivered[0], ipv6_packet, "Delivered packet should match original");
assert_eq!(
delivered[0], ipv6_packet,
"Delivered packet should match original"
);
cleanup_nodes(&mut nodes).await;
}
@@ -1049,17 +1062,35 @@ async fn test_tun_outbound_triggers_session_initiation() {
// Session should now be initiating
assert_eq!(nodes[0].node.session_count(), 1);
assert!(nodes[0].node.get_session(&node1_addr).unwrap().state().is_initiating());
assert!(
nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_initiating()
);
// Drain packets until session established and queued packet delivered
drain_to_quiescence(&mut nodes).await;
// Session should be established on Node 0
assert!(nodes[0].node.get_session(&node1_addr).unwrap().state().is_established());
assert!(
nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_established()
);
// Verify the queued packet was delivered to Node 1
let delivered: Vec<Vec<u8>> = std::iter::from_fn(|| tun_rx.try_recv().ok()).collect();
assert_eq!(delivered.len(), 1, "Queued packet should be delivered after handshake");
assert_eq!(
delivered.len(),
1,
"Queued packet should be delivered after handshake"
);
assert_eq!(delivered[0], ipv6_packet);
cleanup_nodes(&mut nodes).await;
@@ -1087,12 +1118,19 @@ async fn test_tun_outbound_unknown_destination() {
// Should receive ICMPv6 Destination Unreachable back on TUN
let delivered: Vec<Vec<u8>> = std::iter::from_fn(|| tun_rx.try_recv().ok()).collect();
assert_eq!(delivered.len(), 1, "Should receive ICMPv6 Destination Unreachable");
assert_eq!(
delivered.len(),
1,
"Should receive ICMPv6 Destination Unreachable"
);
// Verify it's an ICMPv6 Destination Unreachable (type 1, code 0)
// ICMPv6 header starts at byte 40, type at byte 40, code at byte 41
assert!(delivered[0].len() >= 48, "ICMPv6 response too short");
assert_eq!(delivered[0][6], 58, "Next header should be ICMPv6 (58)");
assert_eq!(delivered[0][40], 1, "ICMPv6 type should be Destination Unreachable (1)");
assert_eq!(
delivered[0][40], 1,
"ICMPv6 type should be Destination Unreachable (1)"
);
assert_eq!(delivered[0][41], 0, "ICMPv6 code should be No Route (0)");
cleanup_nodes(&mut nodes).await;
@@ -1131,7 +1169,14 @@ async fn test_tun_outbound_3node_forwarded() {
drain_to_quiescence(&mut nodes).await;
// Session should be established
assert!(nodes[0].node.get_session(&node2_addr).unwrap().state().is_established());
assert!(
nodes[0]
.node
.get_session(&node2_addr)
.unwrap()
.state()
.is_established()
);
// Verify packet delivered to Node 2
let delivered: Vec<Vec<u8>> = std::iter::from_fn(|| tun_rx.try_recv().ok()).collect();
@@ -1170,16 +1215,34 @@ async fn test_tun_outbound_pending_queue_flush() {
// First packet triggers session initiation, rest are queued
assert_eq!(nodes[0].node.session_count(), 1);
assert!(nodes[0].node.get_session(&node1_addr).unwrap().state().is_initiating());
assert!(
nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_initiating()
);
// Drain until session established and queued packets flushed
drain_to_quiescence(&mut nodes).await;
assert!(nodes[0].node.get_session(&node1_addr).unwrap().state().is_established());
assert!(
nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_established()
);
// All 5 packets should have been delivered
let delivered: Vec<Vec<u8>> = std::iter::from_fn(|| tun_rx.try_recv().ok()).collect();
assert_eq!(delivered.len(), 5, "All 5 queued packets should be delivered");
assert_eq!(
delivered.len(),
5,
"All 5 queued packets should be delivered"
);
for (i, pkt) in delivered.iter().enumerate() {
assert_eq!(*pkt, packets[i], "Packet {} should match", i);
}
@@ -1198,10 +1261,8 @@ fn make_noise_session(
) -> crate::noise::NoiseSession {
use crate::noise::HandshakeState;
let mut initiator = HandshakeState::new_initiator(
our_identity.keypair(),
remote_identity.pubkey_full(),
);
let mut initiator =
HandshakeState::new_initiator(our_identity.keypair(), remote_identity.pubkey_full());
let mut responder = HandshakeState::new_responder(remote_identity.keypair());
// Set epochs for both sides (required for handshake message encryption)
@@ -1270,7 +1331,11 @@ fn test_purge_idle_sessions_keeps_active() {
let now_ms = 92_000;
node.purge_idle_sessions(now_ms);
assert_eq!(node.session_count(), 1, "Active session should survive purge");
assert_eq!(
node.session_count(),
1,
"Active session should survive purge"
);
}
#[test]
@@ -1281,10 +1346,7 @@ fn test_purge_idle_sessions_ignores_initiating() {
let remote = Identity::generate();
let remote_addr = *remote.node_addr();
let handshake = HandshakeState::new_initiator(
node.identity().keypair(),
remote.pubkey_full(),
);
let handshake = HandshakeState::new_initiator(node.identity().keypair(), remote.pubkey_full());
let entry = crate::node::session::SessionEntry::new(
remote_addr,
remote.pubkey_full(),
@@ -1299,7 +1361,11 @@ fn test_purge_idle_sessions_ignores_initiating() {
let now_ms = 1000 + 200_000;
node.purge_idle_sessions(now_ms);
assert_eq!(node.session_count(), 1, "Initiating session should not be purged by idle timeout");
assert_eq!(
node.session_count(),
1,
"Initiating session should not be purged by idle timeout"
);
}
#[test]
@@ -1330,8 +1396,10 @@ fn test_purge_idle_sessions_cleans_pending_packets() {
node.purge_idle_sessions(now_ms);
assert_eq!(node.session_count(), 0);
assert!(!node.pending_tun_packets.contains_key(&remote_addr),
"Pending packets should be cleaned up with idle session");
assert!(
!node.pending_tun_packets.contains_key(&remote_addr),
"Pending packets should be cleaned up with idle session"
);
}
#[test]
@@ -1357,7 +1425,11 @@ fn test_purge_idle_sessions_disabled_when_zero() {
let now_ms = 1000 + 1_000_000;
node.purge_idle_sessions(now_ms);
assert_eq!(node.session_count(), 1, "Sessions should not be purged when idle timeout is disabled");
assert_eq!(
node.session_count(),
1,
"Sessions should not be purged when idle timeout is disabled"
);
}
#[test]
@@ -1386,8 +1458,11 @@ fn test_purge_idle_sessions_mmp_activity_does_not_prevent_purge() {
let now_ms = 92_000;
node.purge_idle_sessions(now_ms);
assert_eq!(node.session_count(), 0,
"Session with MMP-only activity should be purged");
assert_eq!(
node.session_count(),
0,
"Session with MMP-only activity should be purged"
);
}
// ============================================================================
@@ -1401,10 +1476,7 @@ fn test_coords_warmup_counter_default_zero_on_new() {
let identity_a = Identity::generate();
let identity_b = Identity::generate();
let handshake = HandshakeState::new_initiator(
identity_a.keypair(),
identity_b.pubkey_full(),
);
let handshake = HandshakeState::new_initiator(identity_a.keypair(), identity_b.pubkey_full());
let entry = crate::node::session::SessionEntry::new(
*identity_b.node_addr(),
@@ -1414,8 +1486,11 @@ fn test_coords_warmup_counter_default_zero_on_new() {
true,
);
assert_eq!(entry.coords_warmup_remaining(), 0,
"Counter should be 0 for non-Established sessions");
assert_eq!(
entry.coords_warmup_remaining(),
0,
"Counter should be 0 for non-Established sessions"
);
}
#[test]
@@ -1466,15 +1541,20 @@ fn test_coords_warmup_counter_decrement() {
assert_eq!(entry.coords_warmup_remaining(), expected);
}
assert_eq!(entry.coords_warmup_remaining(), 0,
"Counter should reach 0 after N decrements");
assert_eq!(
entry.coords_warmup_remaining(),
0,
"Counter should reach 0 after N decrements"
);
}
#[test]
fn test_coords_warmup_config_default() {
let config = crate::config::Config::new();
assert_eq!(config.node.session.coords_warmup_packets, 5,
"Default coords_warmup_packets should be 5");
assert_eq!(
config.node.session.coords_warmup_packets, 5,
"Default coords_warmup_packets should be 5"
);
}
// ============================================================================
@@ -1493,11 +1573,13 @@ fn test_identity_cache_lru_eviction() {
// Insert first two with explicit timestamps to ensure deterministic ordering
let mut prefix1 = [0u8; 15];
prefix1.copy_from_slice(&id1.node_addr().as_bytes()[0..15]);
node.identity_cache.insert(prefix1, (*id1.node_addr(), id1.pubkey_full(), 1000));
node.identity_cache
.insert(prefix1, (*id1.node_addr(), id1.pubkey_full(), 1000));
let mut prefix2 = [0u8; 15];
prefix2.copy_from_slice(&id2.node_addr().as_bytes()[0..15]);
node.identity_cache.insert(prefix2, (*id2.node_addr(), id2.pubkey_full(), 2000));
node.identity_cache
.insert(prefix2, (*id2.node_addr(), id2.pubkey_full(), 2000));
assert_eq!(node.identity_cache_len(), 2);
@@ -1505,13 +1587,17 @@ fn test_identity_cache_lru_eviction() {
node.register_identity(*id3.node_addr(), id3.pubkey_full());
assert_eq!(node.identity_cache_len(), 2);
assert!(node.lookup_by_fips_prefix(&prefix1).is_none(),
"Oldest entry should have been evicted");
assert!(
node.lookup_by_fips_prefix(&prefix1).is_none(),
"Oldest entry should have been evicted"
);
let mut prefix3 = [0u8; 15];
prefix3.copy_from_slice(&id3.node_addr().as_bytes()[0..15]);
assert!(node.lookup_by_fips_prefix(&prefix3).is_some(),
"Newest entry should be present");
assert!(
node.lookup_by_fips_prefix(&prefix3).is_some(),
"Newest entry should be present"
);
}
#[test]
@@ -1546,10 +1632,7 @@ fn test_session_entry_handshake_payload_storage() {
let identity_a = Identity::generate();
let identity_b = Identity::generate();
let handshake = HandshakeState::new_initiator(
identity_a.keypair(),
identity_b.pubkey_full(),
);
let handshake = HandshakeState::new_initiator(identity_a.keypair(), identity_b.pubkey_full());
let mut entry = crate::node::session::SessionEntry::new(
*identity_b.node_addr(),
@@ -1581,10 +1664,7 @@ fn test_session_entry_resend_tracking() {
let identity_a = Identity::generate();
let identity_b = Identity::generate();
let handshake = HandshakeState::new_initiator(
identity_a.keypair(),
identity_b.pubkey_full(),
);
let handshake = HandshakeState::new_initiator(identity_a.keypair(), identity_b.pubkey_full());
let mut entry = crate::node::session::SessionEntry::new(
*identity_b.node_addr(),
@@ -1615,10 +1695,7 @@ fn test_session_entry_clear_handshake_payload() {
let identity_a = Identity::generate();
let identity_b = Identity::generate();
let handshake = HandshakeState::new_initiator(
identity_a.keypair(),
identity_b.pubkey_full(),
);
let handshake = HandshakeState::new_initiator(identity_a.keypair(), identity_b.pubkey_full());
let mut entry = crate::node::session::SessionEntry::new(
*identity_b.node_addr(),
@@ -1649,10 +1726,8 @@ async fn test_session_handshake_timeout() {
let mut node = make_node();
let identity_b = Identity::generate();
let handshake = HandshakeState::new_initiator(
node.identity.keypair(),
identity_b.pubkey_full(),
);
let handshake =
HandshakeState::new_initiator(node.identity.keypair(), identity_b.pubkey_full());
let dest_addr = *identity_b.node_addr();
@@ -1672,12 +1747,18 @@ async fn test_session_handshake_timeout() {
let timeout_secs = node.config.node.rate_limit.handshake_timeout_secs;
let before_timeout = 1000 + timeout_secs * 1000 - 1;
node.resend_pending_session_handshakes(before_timeout).await;
assert!(node.sessions.contains_key(&dest_addr), "Session should survive before timeout");
assert!(
node.sessions.contains_key(&dest_addr),
"Session should survive before timeout"
);
// After timeout: session should be removed
let after_timeout = 1000 + timeout_secs * 1000 + 1;
node.resend_pending_session_handshakes(after_timeout).await;
assert!(!node.sessions.contains_key(&dest_addr), "Timed-out session should be removed");
assert!(
!node.sessions.contains_key(&dest_addr),
"Timed-out session should be removed"
);
}
/// Test that session handshake timeout removes stale AwaitingMsg3 sessions.
@@ -1690,9 +1771,7 @@ async fn test_session_awaiting_msg3_timeout() {
let identity_a = Identity::generate();
let identity_b = Identity::generate();
let handshake = HandshakeState::new_xk_responder(
identity_b.keypair(),
);
let handshake = HandshakeState::new_xk_responder(identity_b.keypair());
let src_addr = *identity_a.node_addr();
@@ -1712,7 +1791,10 @@ async fn test_session_awaiting_msg3_timeout() {
let timeout_secs = node.config.node.rate_limit.handshake_timeout_secs;
let after_timeout = 1000 + timeout_secs * 1000 + 1;
node.resend_pending_session_handshakes(after_timeout).await;
assert!(!node.sessions.contains_key(&src_addr), "Timed-out AwaitingMsg3 session should be removed");
assert!(
!node.sessions.contains_key(&src_addr),
"Timed-out AwaitingMsg3 session should be removed"
);
}
#[tokio::test]
@@ -1734,7 +1816,11 @@ async fn test_tun_outbound_path_mtu_generates_ptb() {
let dst_fips = crate::FipsAddress::from_node_addr(&node1_addr);
// Establish session (XK: 3 messages — Setup, Ack, Msg3)
nodes[0].node.initiate_session(node1_addr, node1_pubkey).await.unwrap();
nodes[0]
.node
.initiate_session(node1_addr, node1_pubkey)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(20)).await;
process_available_packets(&mut nodes).await;
tokio::time::sleep(Duration::from_millis(20)).await;
@@ -1742,7 +1828,14 @@ async fn test_tun_outbound_path_mtu_generates_ptb() {
tokio::time::sleep(Duration::from_millis(20)).await;
process_available_packets(&mut nodes).await;
assert!(nodes[0].node.get_session(&node1_addr).unwrap().state().is_established());
assert!(
nodes[0]
.node
.get_session(&node1_addr)
.unwrap()
.state()
.is_established()
);
// Simulate receipt of MtuExceeded by reducing PathMtuState to a value
// lower than the local transport MTU.
@@ -1751,7 +1844,8 @@ async fn test_tun_outbound_path_mtu_generates_ptb() {
{
let entry = nodes[0].node.get_session_mut(&node1_addr).unwrap();
let mmp = entry.mmp_mut().unwrap();
mmp.path_mtu.apply_notification(reduced_mtu, std::time::Instant::now());
mmp.path_mtu
.apply_notification(reduced_mtu, std::time::Instant::now());
assert_eq!(mmp.path_mtu.current_mtu(), reduced_mtu);
}
@@ -1764,14 +1858,24 @@ async fn test_tun_outbound_path_mtu_generates_ptb() {
let local_ipv6_mtu = nodes[0].node.effective_ipv6_mtu() as usize;
let oversized_payload = vec![0u8; reduced_ipv6_mtu - 39]; // 40-byte hdr + payload > reduced MTU
let ipv6_packet = build_ipv6_packet(&src_fips, &dst_fips, &oversized_payload);
assert!(ipv6_packet.len() > reduced_ipv6_mtu, "packet must exceed path MTU");
assert!(ipv6_packet.len() <= local_ipv6_mtu, "packet must fit local MTU");
assert!(
ipv6_packet.len() > reduced_ipv6_mtu,
"packet must exceed path MTU"
);
assert!(
ipv6_packet.len() <= local_ipv6_mtu,
"packet must fit local MTU"
);
nodes[0].node.handle_tun_outbound(ipv6_packet).await;
// Verify ICMPv6 Packet Too Big was generated
let ptb_messages: Vec<Vec<u8>> = std::iter::from_fn(|| tun_rx.try_recv().ok()).collect();
assert_eq!(ptb_messages.len(), 1, "Should generate exactly one ICMPv6 PTB");
assert_eq!(
ptb_messages.len(),
1,
"Should generate exactly one ICMPv6 PTB"
);
let ptb = &ptb_messages[0];
assert_eq!(ptb[0] >> 4, 6, "Should be IPv6");
@@ -1784,12 +1888,23 @@ async fn test_tun_outbound_path_mtu_generates_ptb() {
// address, causing a PMTUD blackhole.
let ptb_src = std::net::Ipv6Addr::from(<[u8; 16]>::try_from(&ptb[8..24]).unwrap());
let ptb_dst = std::net::Ipv6Addr::from(<[u8; 16]>::try_from(&ptb[24..40]).unwrap());
assert_eq!(ptb_src, dst_fips.to_ipv6(), "PTB source must be remote peer (original dst), not local node");
assert_eq!(ptb_dst, src_fips.to_ipv6(), "PTB destination must be local node (original src)");
assert_eq!(
ptb_src,
dst_fips.to_ipv6(),
"PTB source must be remote peer (original dst), not local node"
);
assert_eq!(
ptb_dst,
src_fips.to_ipv6(),
"PTB destination must be local node (original src)"
);
// Verify reported MTU (32-bit field at ICMPv6 header bytes 4-7)
let reported_mtu = u32::from_be_bytes([ptb[44], ptb[45], ptb[46], ptb[47]]);
assert_eq!(reported_mtu, reduced_ipv6_mtu as u32, "Reported MTU should match path IPv6 MTU");
assert_eq!(
reported_mtu, reduced_ipv6_mtu as u32,
"Reported MTU should match path IPv6 MTU"
);
// Verify a packet that fits within path MTU passes through (no PTB)
let (tun_tx2, tun_rx2) = std::sync::mpsc::channel();
@@ -1802,7 +1917,11 @@ async fn test_tun_outbound_path_mtu_generates_ptb() {
// No PTB should be generated for a fitting packet
let ptb_messages2: Vec<Vec<u8>> = std::iter::from_fn(|| tun_rx2.try_recv().ok()).collect();
assert_eq!(ptb_messages2.len(), 0, "Should not generate PTB for fitting packet");
assert_eq!(
ptb_messages2.len(),
0,
"Should not generate PTB for fitting packet"
);
cleanup_nodes(&mut nodes).await;
}
@@ -1845,10 +1964,19 @@ async fn test_multihop_pmtud_heterogeneous_mtu() {
nodes[0].node.register_identity(node2_addr, node2_pubkey);
// Establish session A→C via B (triggers routing through tree)
nodes[0].node.initiate_session(node2_addr, node2_pubkey).await.unwrap();
nodes[0]
.node
.initiate_session(node2_addr, node2_pubkey)
.await
.unwrap();
drain_to_quiescence(&mut nodes).await;
assert!(
nodes[0].node.get_session(&node2_addr).unwrap().state().is_established(),
nodes[0]
.node
.get_session(&node2_addr)
.unwrap()
.state()
.is_established(),
"Session A→C should be established"
);
@@ -1858,7 +1986,11 @@ async fn test_multihop_pmtud_heterogeneous_mtu() {
// With coords (~66 extra), the wire could exceed B's recv buffer.
for _ in 0..5 {
let small = build_ipv6_packet(&src_fips, &dst_fips, &[0u8; 10]);
nodes[0].node.send_ipv6_packet(&node2_addr, &small).await.unwrap();
nodes[0]
.node
.send_ipv6_packet(&node2_addr, &small)
.await
.unwrap();
}
drain_to_quiescence(&mut nodes).await;
@@ -1872,12 +2004,17 @@ async fn test_multihop_pmtud_heterogeneous_mtu() {
assert!(
ipv6_packet.len() <= local_effective_mtu,
"packet ({}) must fit A's local MTU ({})",
ipv6_packet.len(), local_effective_mtu
ipv6_packet.len(),
local_effective_mtu
);
// Send the oversized packet — B should fail to forward and send
// MtuExceeded signal back.
nodes[0].node.send_ipv6_packet(&node2_addr, &ipv6_packet).await.unwrap();
nodes[0]
.node
.send_ipv6_packet(&node2_addr, &ipv6_packet)
.await
.unwrap();
drain_to_quiescence(&mut nodes).await;
// Verify PathMtuState was updated on A
@@ -1902,7 +2039,8 @@ async fn test_multihop_pmtud_heterogeneous_mtu() {
let ptb_messages: Vec<Vec<u8>> = std::iter::from_fn(|| tun_rx2.try_recv().ok()).collect();
assert_eq!(
ptb_messages.len(), 1,
ptb_messages.len(),
1,
"Should generate ICMPv6 PTB for oversized packet after PathMtuState update"
);
@@ -1917,8 +2055,16 @@ async fn test_multihop_pmtud_heterogeneous_mtu() {
// address, causing a PMTUD blackhole.
let ptb_src = std::net::Ipv6Addr::from(<[u8; 16]>::try_from(&ptb[8..24]).unwrap());
let ptb_dst = std::net::Ipv6Addr::from(<[u8; 16]>::try_from(&ptb[24..40]).unwrap());
assert_eq!(ptb_src, dst_fips.to_ipv6(), "PTB source must be remote peer (original dst), not local node");
assert_eq!(ptb_dst, src_fips.to_ipv6(), "PTB destination must be local node (original src)");
assert_eq!(
ptb_src,
dst_fips.to_ipv6(),
"PTB source must be remote peer (original dst), not local node"
);
assert_eq!(
ptb_dst,
src_fips.to_ipv6(),
"PTB destination must be local node (original src)"
);
// Verify reported MTU is the path MTU (not local MTU)
let reported_mtu = u32::from_be_bytes([ptb[44], ptb[45], ptb[46], ptb[47]]);
@@ -1941,7 +2087,8 @@ async fn test_multihop_pmtud_heterogeneous_mtu() {
let ptb_messages3: Vec<Vec<u8>> = std::iter::from_fn(|| tun_rx3.try_recv().ok()).collect();
assert_eq!(
ptb_messages3.len(), 0,
ptb_messages3.len(),
0,
"Should not generate PTB for packet fitting within path MTU"
);
+25 -14
View File
@@ -69,7 +69,9 @@ pub(super) async fn initiate_handshake(nodes: &mut [TestNode], i: usize, j: usiz
let our_index = initiator.node.index_allocator.allocate().unwrap();
let our_keypair = initiator.node.identity().keypair();
let noise_msg1 = conn.start_handshake(our_keypair, initiator.node.startup_epoch, 1000).unwrap();
let noise_msg1 = conn
.start_handshake(our_keypair, initiator.node.startup_epoch, 1000)
.unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
conn.set_source_addr(responder_addr.clone());
@@ -184,7 +186,12 @@ pub(super) fn print_tree_snapshot(label: &str, nodes: &[TestNode]) {
.count();
eprintln!(
" node[{}] root=node[{}] depth={} parent=node[{}] peers={} pending={}",
i, root_idx, ts.my_coords().depth(), parent_idx, tn.node.peer_count(), pending,
i,
root_idx,
ts.my_coords().depth(),
parent_idx,
tn.node.peer_count(),
pending,
);
}
} else if correct_root_count < nodes.len() {
@@ -209,7 +216,9 @@ pub(super) fn print_tree_snapshot(label: &str, nodes: &[TestNode]) {
///
/// Returns the number of packets processed.
pub(super) async fn process_available_packets(nodes: &mut [TestNode]) -> usize {
use crate::node::wire::{CommonPrefix, FMP_VERSION, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2, COMMON_PREFIX_SIZE};
use crate::node::wire::{
COMMON_PREFIX_SIZE, CommonPrefix, FMP_VERSION, PHASE_ESTABLISHED, PHASE_MSG1, PHASE_MSG2,
};
let mut count = 0;
for node in nodes.iter_mut() {
@@ -224,9 +233,7 @@ pub(super) async fn process_available_packets(nodes: &mut [TestNode]) -> usize {
match prefix.phase {
PHASE_MSG1 => node.node.handle_msg1(packet).await,
PHASE_MSG2 => node.node.handle_msg2(packet).await,
PHASE_ESTABLISHED => {
node.node.handle_encrypted_frame(packet).await
}
PHASE_ESTABLISHED => node.node.handle_encrypted_frame(packet).await,
_ => {}
}
count += 1;
@@ -319,7 +326,11 @@ pub(super) async fn drain_all_packets(nodes: &mut [TestNode], verbose: bool) ->
///
/// First builds a random spanning tree to ensure connectivity,
/// then adds extra edges up to the target count.
pub(super) fn generate_random_edges(n: usize, target_edges: usize, seed: u64) -> Vec<(usize, usize)> {
pub(super) fn generate_random_edges(
n: usize,
target_edges: usize,
seed: u64,
) -> Vec<(usize, usize)> {
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
@@ -373,11 +384,7 @@ pub(super) fn verify_tree_convergence(nodes: &[TestNode]) {
assert!(n > 0);
// Find the expected root (smallest NodeAddr across all nodes)
let expected_root = nodes
.iter()
.map(|tn| *tn.node.node_addr())
.min()
.unwrap();
let expected_root = nodes.iter().map(|tn| *tn.node.node_addr()).min().unwrap();
// All nodes should agree on the root
for (i, tn) in nodes.iter().enumerate() {
@@ -627,12 +634,16 @@ pub(super) async fn run_tree_test_with_mtus(
assert!(
nodes[i].node.get_peer(&j_addr).is_some(),
"Node {} should have peer {} (node {})",
i, j_addr, j
i,
j_addr,
j
);
assert!(
nodes[j].node.get_peer(&i_addr).is_some(),
"Node {} should have peer {} (node {})",
j, i_addr, i
j,
i_addr,
i
);
}
+2 -2
View File
@@ -8,9 +8,9 @@
use super::*;
use crate::config::TcpConfig;
use crate::transport::tcp::TcpTransport;
use crate::transport::{packet_channel, TransportAddr, TransportHandle, TransportId};
use crate::transport::{TransportAddr, TransportHandle, TransportId, packet_channel};
use spanning_tree::{
cleanup_nodes, drain_all_packets, initiate_handshake, verify_tree_convergence, TestNode,
TestNode, cleanup_nodes, drain_all_packets, initiate_handshake, verify_tree_convergence,
};
use std::time::Duration;
+49 -34
View File
@@ -96,7 +96,10 @@ fn test_node_link_management() {
assert_eq!(node.link_count(), 0);
// Lookup should be gone
assert!(node.find_link_by_addr(TransportId::new(1), &TransportAddr::from_string("test")).is_none());
assert!(
node.find_link_by_addr(TransportId::new(1), &TransportAddr::from_string("test"))
.is_none()
);
}
#[test]
@@ -183,8 +186,14 @@ fn test_node_promote_connection() {
let peer = node.get_peer(&node_addr).unwrap();
assert_eq!(peer.authenticated_at(), 2000);
assert!(peer.has_session(), "Promoted peer should have NoiseSession");
assert!(peer.our_index().is_some(), "Promoted peer should have our_index");
assert!(peer.their_index().is_some(), "Promoted peer should have their_index");
assert!(
peer.our_index().is_some(),
"Promoted peer should have our_index"
);
assert!(
peer.their_index().is_some(),
"Promoted peer should have their_index"
);
// Verify peers_by_index is populated
let our_index = peer.our_index().unwrap();
@@ -201,8 +210,7 @@ fn test_node_cross_connection_resolution() {
// First connection and promotion (becomes active peer)
let link_id1 = LinkId::new(1);
let (conn1, identity) =
make_completed_connection(&mut node, link_id1, transport_id, 1000);
let (conn1, identity) = make_completed_connection(&mut node, link_id1, transport_id, 1000);
let node_addr = *identity.node_addr();
node.add_connection(conn1).unwrap();
@@ -236,8 +244,7 @@ fn test_node_peer_limit() {
// Add two peers via promotion
for i in 0..2 {
let link_id = LinkId::new(i as u64 + 1);
let (conn, identity) =
make_completed_connection(&mut node, link_id, transport_id, 1000);
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
node.add_connection(conn).unwrap();
node.promote_connection(link_id, identity, 2000).unwrap();
}
@@ -246,8 +253,7 @@ fn test_node_peer_limit() {
// Third should fail
let link_id = LinkId::new(3);
let (conn, identity) =
make_completed_connection(&mut node, link_id, transport_id, 3000);
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 3000);
node.add_connection(conn).unwrap();
let result = node.promote_connection(link_id, identity, 4000);
@@ -296,23 +302,20 @@ fn test_node_sendable_peers() {
// Add a healthy peer
let link_id1 = LinkId::new(1);
let (conn1, identity1) =
make_completed_connection(&mut node, link_id1, transport_id, 1000);
let (conn1, identity1) = make_completed_connection(&mut node, link_id1, transport_id, 1000);
let node_addr1 = *identity1.node_addr();
node.add_connection(conn1).unwrap();
node.promote_connection(link_id1, identity1, 2000).unwrap();
// Add another peer and mark it stale (still sendable)
let link_id2 = LinkId::new(2);
let (conn2, identity2) =
make_completed_connection(&mut node, link_id2, transport_id, 1000);
let (conn2, identity2) = make_completed_connection(&mut node, link_id2, transport_id, 1000);
node.add_connection(conn2).unwrap();
node.promote_connection(link_id2, identity2, 2000).unwrap();
// Add a third peer and mark it disconnected (not sendable)
let link_id3 = LinkId::new(3);
let (conn3, identity3) =
make_completed_connection(&mut node, link_id3, transport_id, 1000);
let (conn3, identity3) = make_completed_connection(&mut node, link_id3, transport_id, 1000);
let node_addr3 = *identity3.node_addr();
node.add_connection(conn3).unwrap();
node.promote_connection(link_id3, identity3, 2000).unwrap();
@@ -345,14 +348,16 @@ fn test_node_pending_outbound_tracking() {
let index = node.index_allocator.allocate().unwrap();
// Track in pending_outbound
node.pending_outbound.insert((transport_id, index.as_u32()), link_id);
node.pending_outbound
.insert((transport_id, index.as_u32()), link_id);
// Verify we can look it up
let found = node.pending_outbound.get(&(transport_id, index.as_u32()));
assert_eq!(found, Some(&link_id));
// Clean up
node.pending_outbound.remove(&(transport_id, index.as_u32()));
node.pending_outbound
.remove(&(transport_id, index.as_u32()));
let _ = node.index_allocator.free(index);
assert_eq!(node.index_allocator.count(), 0);
@@ -369,7 +374,8 @@ fn test_node_peers_by_index_tracking() {
let index = node.index_allocator.allocate().unwrap();
// Track in peers_by_index
node.peers_by_index.insert((transport_id, index.as_u32()), node_addr);
node.peers_by_index
.insert((transport_id, index.as_u32()), node_addr);
// Verify lookup
let found = node.peers_by_index.get(&(transport_id, index.as_u32()));
@@ -450,7 +456,9 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() {
PeerConnection::outbound(pending_link_id, peer_b_identity, pending_time_ms);
let our_keypair = node.identity.keypair();
let _msg1 = pending_conn.start_handshake(our_keypair, node.startup_epoch, pending_time_ms).unwrap();
let _msg1 = pending_conn
.start_handshake(our_keypair, node.startup_epoch, pending_time_ms)
.unwrap();
let pending_index = node.index_allocator.allocate().unwrap();
pending_conn.set_our_index(pending_index);
@@ -483,11 +491,8 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() {
let completing_link_id = LinkId::new(2);
let completing_time_ms = 2000;
let mut completing_conn = PeerConnection::outbound(
completing_link_id,
peer_b_identity,
completing_time_ms,
);
let mut completing_conn =
PeerConnection::outbound(completing_link_id, peer_b_identity, completing_time_ms);
let our_keypair = node.identity.keypair();
let msg1 = completing_conn
@@ -573,7 +578,10 @@ fn test_schedule_retry_creates_entry() {
assert_eq!(node.retry_pending.len(), 1);
let state = node.retry_pending.get(&peer_node_addr).unwrap();
assert_eq!(state.retry_count, 1);
assert!(state.reconnect, "Auto-connect peers always get reconnect=true");
assert!(
state.reconnect,
"Auto-connect peers always get reconnect=true"
);
// Default base = 5s, 2^1 = 10s, but first retry is 2^0... let me check:
// retry_count is set to 1, backoff_ms(5000) = 5000 * 2^1 = 10000
assert_eq!(state.retry_after_ms, 1000 + 10_000);
@@ -597,7 +605,10 @@ fn test_schedule_retry_increments() {
// First failure
node.schedule_retry(peer_node_addr, 1000);
assert_eq!(node.retry_pending.get(&peer_node_addr).unwrap().retry_count, 1);
assert_eq!(
node.retry_pending.get(&peer_node_addr).unwrap().retry_count,
1
);
// Second failure
node.schedule_retry(peer_node_addr, 11_000);
@@ -637,7 +648,10 @@ fn test_schedule_retry_auto_connect_never_exhausts() {
node.retry_pending.contains_key(&peer_node_addr),
"Auto-connect peers should never exhaust retries"
);
assert_eq!(node.retry_pending.get(&peer_node_addr).unwrap().retry_count, 3);
assert_eq!(
node.retry_pending.get(&peer_node_addr).unwrap().retry_count,
3
);
}
/// Test that schedule_retry does nothing when max_retries is 0.
@@ -725,7 +739,7 @@ fn test_schedule_reconnect_preserves_backoff() {
let mut node = Node::new(config).unwrap();
// Simulate two stale handshake timeouts incrementing the retry count.
node.schedule_retry(peer_node_addr, 1_000); // count=1, delay=10s
node.schedule_retry(peer_node_addr, 1_000); // count=1, delay=10s
node.schedule_retry(peer_node_addr, 11_000); // count=2, delay=20s
{
let state = node.retry_pending.get(&peer_node_addr).unwrap();
@@ -738,10 +752,7 @@ fn test_schedule_reconnect_preserves_backoff() {
node.schedule_reconnect(peer_node_addr, 31_000);
let state = node.retry_pending.get(&peer_node_addr).unwrap();
assert!(
state.reconnect,
"Entry should be marked as reconnect"
);
assert!(state.reconnect, "Entry should be marked as reconnect");
assert_eq!(
state.retry_count, 3,
"schedule_reconnect should increment existing count (was 2), not reset to 0 (regression: issue #5)"
@@ -752,7 +763,8 @@ fn test_schedule_reconnect_preserves_backoff() {
let max_ms = node.config.node.retry.max_backoff_secs * 1000;
let expected_delay = state.backoff_ms(base_ms, max_ms);
assert_eq!(
state.retry_after_ms, 31_000 + expected_delay,
state.retry_after_ms,
31_000 + expected_delay,
"retry_after_ms should reflect count=3 backoff"
);
}
@@ -778,7 +790,10 @@ fn test_schedule_reconnect_fresh_state() {
let state = node.retry_pending.get(&peer_node_addr).unwrap();
assert!(state.reconnect, "Entry should be marked as reconnect");
assert_eq!(state.retry_count, 0, "Fresh reconnect should start at count=0");
assert_eq!(
state.retry_count, 0,
"Fresh reconnect should start at count=0"
);
// Base delay: 5s * 2^0 = 5s
let base_ms = node.config.node.retry.base_interval_secs * 1000;
let max_ms = node.config.node.retry.max_backoff_secs * 1000;