mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
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:
+24
-17
@@ -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
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user