mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Integrates PR #50 (peer ACL enforcement) into the XX handshake architecture. ACL enforcement points adapted for XX's deferred identity learning: InboundHandshake check moves from handle_msg1 to handle_msg3 (responder learns initiator identity), OutboundHandshake check remains in handle_msg2 (initiator learns responder identity). Borrow scopes restructured to release connection borrows before authorize_peer calls.
1013 lines
36 KiB
Rust
1013 lines
36 KiB
Rust
//! Discovery protocol tests: LookupRequest and LookupResponse.
|
|
//!
|
|
//! Unit tests for handler logic (dedup, TTL, response caching) and
|
|
//! integration tests for multi-node forwarding and reverse-path
|
|
//! response routing.
|
|
|
|
use super::*;
|
|
use crate::node::RecentRequest;
|
|
use crate::protocol::{LookupRequest, LookupResponse};
|
|
use crate::tree::TreeCoordinate;
|
|
use spanning_tree::{
|
|
cleanup_nodes, generate_random_edges, lock_large_network_test, process_available_packets,
|
|
run_tree_test, run_tree_test_with_mtus, verify_tree_convergence,
|
|
};
|
|
|
|
// ============================================================================
|
|
// Unit Tests — LookupRequest Handler
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_request_decode_error() {
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
// Too-short payload: should log error and return without panic
|
|
node.handle_lookup_request(&from, &[0x00; 5]).await;
|
|
assert!(node.recent_requests.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_request_dedup() {
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
let target = make_node_addr(0xBB);
|
|
let origin = make_node_addr(0xCC);
|
|
|
|
let request = LookupRequest::new(999, target, origin, 5, 0);
|
|
let payload = &request.encode()[1..]; // skip msg_type byte
|
|
|
|
// First request: accepted
|
|
node.handle_lookup_request(&from, payload).await;
|
|
assert_eq!(node.recent_requests.len(), 1);
|
|
|
|
// Duplicate request: dropped
|
|
node.handle_lookup_request(&from, payload).await;
|
|
assert_eq!(node.recent_requests.len(), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_request_target_is_self() {
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
let origin = make_node_addr(0xCC);
|
|
let my_addr = *node.node_addr();
|
|
|
|
// Request targeting us
|
|
let request = LookupRequest::new(777, my_addr, origin, 5, 0);
|
|
let payload = &request.encode()[1..];
|
|
|
|
// Should succeed without panic (response send will fail silently
|
|
// since we have no peers to route toward origin)
|
|
node.handle_lookup_request(&from, payload).await;
|
|
assert!(node.recent_requests.contains_key(&777));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_request_ttl_zero_not_forwarded() {
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
let target = make_node_addr(0xBB);
|
|
let origin = make_node_addr(0xCC);
|
|
|
|
let request = LookupRequest::new(666, target, origin, 0, 0);
|
|
let payload = &request.encode()[1..];
|
|
|
|
node.handle_lookup_request(&from, payload).await;
|
|
// Request recorded, but not forwarded (TTL=0, and no peers anyway)
|
|
assert!(node.recent_requests.contains_key(&666));
|
|
}
|
|
|
|
// ============================================================================
|
|
// Unit Tests — LookupResponse Handler
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_response_decode_error() {
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
node.handle_lookup_response(&from, &[0x00; 10]).await;
|
|
// No panic, no route cached
|
|
assert!(node.coord_cache().is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_response_originator_caches_route() {
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
|
|
// Use the target identity's actual node_addr for consistency
|
|
let target_identity = Identity::generate();
|
|
let target = *target_identity.node_addr();
|
|
let root = make_node_addr(0xF0);
|
|
let coords = TreeCoordinate::from_addrs(vec![target, root]).unwrap();
|
|
|
|
// Register target identity in cache so verification can find it
|
|
node.register_identity(target, target_identity.pubkey_full());
|
|
|
|
// Create a valid response with a real proof signature (includes coords)
|
|
let proof_data = LookupResponse::proof_bytes(555, &target, &coords);
|
|
let proof = target_identity.sign(&proof_data);
|
|
|
|
let response = LookupResponse::new(555, target, coords.clone(), proof);
|
|
let payload = &response.encode()[1..]; // skip msg_type
|
|
|
|
// No entry in recent_requests for 555 → we're the originator
|
|
assert!(!node.recent_requests.contains_key(&555));
|
|
|
|
node.handle_lookup_response(&from, payload).await;
|
|
|
|
// Route should be cached in coord_cache
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
assert!(node.coord_cache().contains(&target, now_ms));
|
|
assert_eq!(node.coord_cache().get(&target, now_ms).unwrap(), &coords);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_response_transit_needs_recent_request() {
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
let target = make_node_addr(0xBB);
|
|
let root = make_node_addr(0xF0);
|
|
let coords = TreeCoordinate::from_addrs(vec![target, root]).unwrap();
|
|
|
|
// Transit nodes don't verify proofs, so any valid signature suffices
|
|
let proof_data = LookupResponse::proof_bytes(444, &target, &coords);
|
|
let target_identity = Identity::generate();
|
|
let proof = target_identity.sign(&proof_data);
|
|
|
|
let response = LookupResponse::new(444, target, coords, proof);
|
|
let payload = &response.encode()[1..];
|
|
|
|
// Simulate being a transit node: record a recent_request for this ID
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_millis() as u64;
|
|
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)
|
|
node.handle_lookup_response(&from, payload).await;
|
|
|
|
// Should NOT cache in coord_cache (we're transit, not originator)
|
|
let now_ms2 = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
assert!(!node.coord_cache().contains(&target, now_ms2));
|
|
}
|
|
|
|
// ============================================================================
|
|
// Unit Tests — LookupResponse Proof Verification
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_response_proof_verification_success() {
|
|
// Verify that a properly signed response is accepted and cached
|
|
// when the origin has the target's pubkey in identity_cache.
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
|
|
let target_identity = Identity::generate();
|
|
let target = *target_identity.node_addr();
|
|
let root = make_node_addr(0xF0);
|
|
let coords = TreeCoordinate::from_addrs(vec![target, root]).unwrap();
|
|
|
|
// Register target in identity_cache
|
|
node.register_identity(target, target_identity.pubkey_full());
|
|
|
|
// Sign with correct proof_bytes (including coords)
|
|
let proof_data = LookupResponse::proof_bytes(700, &target, &coords);
|
|
let proof = target_identity.sign(&proof_data);
|
|
|
|
let response = LookupResponse::new(700, target, coords.clone(), proof);
|
|
let payload = &response.encode()[1..];
|
|
|
|
node.handle_lookup_response(&from, payload).await;
|
|
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
assert!(
|
|
node.coord_cache().contains(&target, now_ms),
|
|
"Valid proof should result in cached coords"
|
|
);
|
|
assert_eq!(node.coord_cache().get(&target, now_ms).unwrap(), &coords);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_response_proof_verification_failure() {
|
|
// Verify that a response with a bad signature is discarded.
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
|
|
let target_identity = Identity::generate();
|
|
let target = *target_identity.node_addr();
|
|
let root = make_node_addr(0xF0);
|
|
let coords = TreeCoordinate::from_addrs(vec![target, root]).unwrap();
|
|
|
|
// Register target in identity_cache
|
|
node.register_identity(target, target_identity.pubkey_full());
|
|
|
|
// Sign with a DIFFERENT identity (wrong key)
|
|
let wrong_identity = Identity::generate();
|
|
let proof_data = LookupResponse::proof_bytes(701, &target, &coords);
|
|
let proof = wrong_identity.sign(&proof_data);
|
|
|
|
let response = LookupResponse::new(701, target, coords, proof);
|
|
let payload = &response.encode()[1..];
|
|
|
|
node.handle_lookup_response(&from, payload).await;
|
|
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
assert!(
|
|
!node.coord_cache().contains(&target, now_ms),
|
|
"Bad signature should NOT result in cached coords"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_response_identity_cache_miss() {
|
|
// Verify that a response is discarded when the origin lacks the
|
|
// target's pubkey in identity_cache (e.g., XX responder before msg3).
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
|
|
let target_identity = Identity::generate();
|
|
let target = *target_identity.node_addr();
|
|
let root = make_node_addr(0xF0);
|
|
let coords = TreeCoordinate::from_addrs(vec![target, root]).unwrap();
|
|
|
|
// Do NOT register target in identity_cache
|
|
|
|
let proof_data = LookupResponse::proof_bytes(702, &target, &coords);
|
|
let proof = target_identity.sign(&proof_data);
|
|
|
|
let response = LookupResponse::new(702, target, coords, proof);
|
|
let payload = &response.encode()[1..];
|
|
|
|
node.handle_lookup_response(&from, payload).await;
|
|
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
assert!(
|
|
!node.coord_cache().contains(&target, now_ms),
|
|
"identity_cache miss should discard the response"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_response_coord_substitution_detected() {
|
|
// Verify that if the proof was signed with correct coords but
|
|
// different coords are placed in the response, verification fails.
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
|
|
let target_identity = Identity::generate();
|
|
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();
|
|
|
|
// Register target in identity_cache
|
|
node.register_identity(target, target_identity.pubkey_full());
|
|
|
|
// Sign proof with real coords
|
|
let proof_data = LookupResponse::proof_bytes(703, &target, &real_coords);
|
|
let proof = target_identity.sign(&proof_data);
|
|
|
|
// But construct the response with FAKE coords
|
|
let response = LookupResponse::new(703, target, fake_coords, proof);
|
|
let payload = &response.encode()[1..];
|
|
|
|
node.handle_lookup_response(&from, payload).await;
|
|
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
assert!(
|
|
!node.coord_cache().contains(&target, now_ms),
|
|
"Substituted coords should be detected and response discarded"
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Unit Tests — RecentRequest Expiry
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_recent_request_expiry() {
|
|
let mut node = make_node();
|
|
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.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));
|
|
|
|
// Insert a recent request
|
|
node.recent_requests
|
|
.insert(456, RecentRequest::new(make_node_addr(2), now_ms));
|
|
|
|
assert_eq!(node.recent_requests.len(), 2);
|
|
|
|
// Trigger purge via a new lookup request
|
|
let target = make_node_addr(0xBB);
|
|
let origin = make_node_addr(0xCC);
|
|
let request = LookupRequest::new(789, target, origin, 3, 0);
|
|
let payload = &request.encode()[1..];
|
|
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));
|
|
assert!(node.recent_requests.contains_key(&456));
|
|
assert!(node.recent_requests.contains_key(&789));
|
|
}
|
|
|
|
// ============================================================================
|
|
// Integration Tests — Multi-Node Forwarding
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_request_forwarding_two_node() {
|
|
// Set up a two-node topology: node0 — node1
|
|
// Send a LookupRequest from node0 targeting node1's address.
|
|
// Node1 should receive the forwarded request.
|
|
let edges = vec![(0, 1)];
|
|
let mut nodes = run_tree_test(2, &edges, false).await;
|
|
|
|
let node0_addr = *nodes[0].node.node_addr();
|
|
let target = *nodes[1].node.node_addr(); // target node1 (in bloom filters)
|
|
|
|
let request = LookupRequest::new(42, target, node0_addr, 5, 0);
|
|
let payload = &request.encode()[1..];
|
|
|
|
// Handle on node0 as if we received it from outside
|
|
nodes[0]
|
|
.node
|
|
.handle_lookup_request(&node0_addr, payload)
|
|
.await;
|
|
|
|
// 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"
|
|
);
|
|
|
|
// Node1 should have recorded the request
|
|
assert!(
|
|
nodes[1].node.recent_requests.contains_key(&42),
|
|
"Node 1 should have recorded the forwarded request"
|
|
);
|
|
|
|
cleanup_nodes(&mut nodes).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_request_target_found_generates_response() {
|
|
// Set up a two-node topology: node0 — node1
|
|
// Node0 initiates a lookup targeting node1.
|
|
// Node1 receives, detects it's the target, generates a LookupResponse.
|
|
// Response routes back to node0 which caches the coordinates.
|
|
let edges = vec![(0, 1)];
|
|
let mut nodes = run_tree_test(2, &edges, false).await;
|
|
|
|
let node1_addr = *nodes[1].node.node_addr();
|
|
|
|
// Node0 initiates lookup (doesn't record in recent_requests)
|
|
nodes[0].node.initiate_lookup(&node1_addr, 5).await;
|
|
|
|
// Process packets in rounds to allow request + response
|
|
for _ in 0..4 {
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
process_available_packets(&mut nodes).await;
|
|
}
|
|
|
|
// Node0 should have cached node1's route (it originated the request)
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
assert!(
|
|
nodes[0].node.coord_cache().contains(&node1_addr, now_ms),
|
|
"Node 0 should have cached node 1's route from LookupResponse"
|
|
);
|
|
|
|
cleanup_nodes(&mut nodes).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_request_three_node_chain() {
|
|
// Topology: node0 — node1 — node2
|
|
// Node0 initiates a lookup targeting node2.
|
|
// Request should propagate: node0 → node1 → node2.
|
|
// Node2 generates response, reverse-path: node2 → node1 → node0.
|
|
let edges = vec![(0, 1), (1, 2)];
|
|
let mut nodes = run_tree_test(3, &edges, false).await;
|
|
|
|
let node2_addr = *nodes[2].node.node_addr();
|
|
let node2_pubkey = nodes[2].node.identity().pubkey_full();
|
|
|
|
// Pre-populate node0's identity_cache with node2's identity
|
|
// (in production, DNS resolution or prior handshake would do this)
|
|
nodes[0].node.register_identity(node2_addr, node2_pubkey);
|
|
|
|
// Node0 initiates lookup (doesn't record in recent_requests)
|
|
nodes[0].node.initiate_lookup(&node2_addr, 8).await;
|
|
|
|
// Process packets in rounds to allow multi-hop propagation + response
|
|
// Chain: node0→node1→node2 (request), node2→node1→node0 (response)
|
|
for _ in 0..10 {
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
process_available_packets(&mut nodes).await;
|
|
}
|
|
|
|
// Node1 should have been a transit node (has the request_id in recent_requests)
|
|
assert!(
|
|
!nodes[1].node.recent_requests.is_empty(),
|
|
"Node 1 should have recorded the forwarded request"
|
|
);
|
|
|
|
// Node2 should have received the request (it's the target)
|
|
assert!(
|
|
!nodes[2].node.recent_requests.is_empty(),
|
|
"Node 2 should have received the request"
|
|
);
|
|
|
|
// Node0 should have cached node2's route
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
assert!(
|
|
nodes[0].node.coord_cache().contains(&node2_addr, now_ms),
|
|
"Node 0 should have cached node 2's route through 3-node chain"
|
|
);
|
|
|
|
cleanup_nodes(&mut nodes).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_request_dedup_convergent_paths() {
|
|
// Topology: triangle (node0 — node1, node0 — node2, node1 — node2)
|
|
// A request from node0 targeting node2 may reach it via two paths
|
|
// depending on bloom filter state. If both paths deliver the request,
|
|
// the second arrival at node2 should be deduped.
|
|
let edges = vec![(0, 1), (0, 2), (1, 2)];
|
|
let mut nodes = run_tree_test(3, &edges, false).await;
|
|
|
|
let node0_addr = *nodes[0].node.node_addr();
|
|
let target = *nodes[2].node.node_addr(); // target node2 (in bloom filters)
|
|
|
|
let request = LookupRequest::new(300, target, node0_addr, 5, 0);
|
|
let payload = &request.encode()[1..];
|
|
|
|
// Node0 handles the request (forwards to peers whose bloom filter
|
|
// contains node2 — bloom-guided, not flooding)
|
|
nodes[0]
|
|
.node
|
|
.handle_lookup_request(&node0_addr, payload)
|
|
.await;
|
|
|
|
// Process several rounds
|
|
for _ in 0..5 {
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
process_available_packets(&mut nodes).await;
|
|
}
|
|
|
|
// Node2 (the target) must have received the request
|
|
assert!(
|
|
nodes[2].node.recent_requests.contains_key(&300),
|
|
"Node 2 (target) should have received the request"
|
|
);
|
|
|
|
// If node1 also received and forwarded it, node2 would have seen a
|
|
// duplicate — verify dedup counter reflects convergent arrivals.
|
|
// With bloom-guided routing, node1 may or may not receive the request
|
|
// depending on filter state, so we only assert the target received it.
|
|
|
|
cleanup_nodes(&mut nodes).await;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Integration Tests — 100-Node Discovery
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Long-running (~2 min): run explicitly with --ignored
|
|
async fn test_discovery_100_nodes() {
|
|
let _guard = lock_large_network_test().await;
|
|
|
|
// Set up a 100-node random topology (same seed as other 100-node tests).
|
|
// Each node initiates lookups to a sample of other nodes in batches,
|
|
// processing packets between batches to avoid flooding the network.
|
|
const NUM_NODES: usize = 100;
|
|
const TARGET_EDGES: usize = 250;
|
|
const SEED: u64 = 42;
|
|
const TTL: u8 = 20; // must exceed tree diameter (can reach 17+ hops)
|
|
let edges = generate_random_edges(NUM_NODES, TARGET_EDGES, SEED);
|
|
let mut nodes = run_tree_test(NUM_NODES, &edges, false).await;
|
|
verify_tree_convergence(&nodes);
|
|
|
|
// Disable forward rate limiting: in this test all 100 nodes look up
|
|
// the same 10 targets in <1s wall time. The 2s per-target rate limit
|
|
// would suppress nearly all transit forwarding.
|
|
for tn in nodes.iter_mut() {
|
|
tn.node.disable_discovery_forward_rate_limit();
|
|
}
|
|
|
|
// 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_pubkeys: Vec<secp256k1::PublicKey> = nodes
|
|
.iter()
|
|
.map(|tn| tn.node.identity().pubkey_full())
|
|
.collect();
|
|
|
|
// Pre-populate identity caches: each source needs the target's pubkey
|
|
// for proof verification. In production, DNS resolution populates this
|
|
// before lookups are initiated.
|
|
for (src, node) in nodes.iter_mut().enumerate() {
|
|
for dst in (0..NUM_NODES).step_by(10) {
|
|
if src == dst {
|
|
continue;
|
|
}
|
|
node.node
|
|
.register_identity(all_addrs[dst], all_pubkeys[dst]);
|
|
}
|
|
}
|
|
|
|
// Each node looks up every 10th other node (~10 targets per node).
|
|
// Build the full list of (src, dst) pairs.
|
|
let mut lookup_pairs: Vec<(usize, usize)> = Vec::new();
|
|
for src in 0..NUM_NODES {
|
|
for dst in (0..NUM_NODES).step_by(10) {
|
|
if src == dst {
|
|
continue;
|
|
}
|
|
lookup_pairs.push((src, dst));
|
|
}
|
|
}
|
|
let total_lookups = lookup_pairs.len();
|
|
|
|
// Process one source node at a time. Each node initiates ~10 lookups,
|
|
// which route through the tree via bloom filters. We drain until
|
|
// quiescent before moving to the next node.
|
|
for src in 0..NUM_NODES {
|
|
// Initiate all lookups for this source node
|
|
let mut initiated = false;
|
|
for &(s, dst) in &lookup_pairs {
|
|
if s == src {
|
|
nodes[src].node.initiate_lookup(&all_addrs[dst], TTL).await;
|
|
initiated = true;
|
|
}
|
|
}
|
|
if !initiated {
|
|
continue;
|
|
}
|
|
|
|
// Drain packets until quiescent. With single-path tree routing,
|
|
// a packet forwarded by node X may land in node Y's queue where
|
|
// Y < X in iteration order, causing a zero-count round even though
|
|
// packets are in flight. Use a higher idle threshold to handle this.
|
|
let mut idle_rounds = 0;
|
|
for _ in 0..80 {
|
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
let count = process_available_packets(&mut nodes).await;
|
|
if count == 0 {
|
|
idle_rounds += 1;
|
|
if idle_rounds >= 5 {
|
|
break;
|
|
}
|
|
} else {
|
|
idle_rounds = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify: each originator should have the target's coords in coord_cache
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
let mut resolved = 0usize;
|
|
let mut failed = 0usize;
|
|
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)
|
|
{
|
|
resolved += 1;
|
|
} else {
|
|
failed += 1;
|
|
if failed_pairs.len() < 20 {
|
|
failed_pairs.push((src, dst));
|
|
}
|
|
}
|
|
}
|
|
|
|
eprintln!("\n === Discovery 100-Node Test ===",);
|
|
eprintln!(
|
|
" Lookups: {} | Resolved: {} | Failed: {} | Success rate: {:.1}%",
|
|
total_lookups,
|
|
resolved,
|
|
failed,
|
|
resolved as f64 / total_lookups as f64 * 100.0
|
|
);
|
|
|
|
// 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();
|
|
eprintln!(
|
|
" Coord cache entries: total={} min={} max={} avg={:.1}",
|
|
total_cached,
|
|
min_cached,
|
|
max_cached,
|
|
total_cached as f64 / NUM_NODES as f64
|
|
);
|
|
|
|
// Detailed diagnostics for failures (to aid future debugging)
|
|
if !failed_pairs.is_empty() {
|
|
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 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
|
|
);
|
|
}
|
|
}
|
|
|
|
assert_eq!(
|
|
failed, 0,
|
|
"All {} lookups should resolve, but {} failed",
|
|
total_lookups, failed
|
|
);
|
|
|
|
cleanup_nodes(&mut nodes).await;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Integration Tests — MTU Propagation
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_response_path_mtu_two_node() {
|
|
// Two-node topology: node0 — node1
|
|
// Node0 initiates lookup for node1. The response should carry path_mtu
|
|
// reflecting the transport MTU (1280 in tests) clamped by transit.
|
|
// In a two-node setup: node1 (target) initializes path_mtu=u16::MAX,
|
|
// then the response is sent directly to node0. Since node1 is the
|
|
// target and sends directly, the transit logic does not apply for the
|
|
// first hop (the target sends directly). But node0 is the originator
|
|
// and doesn't apply transit MTU. So path_mtu should be u16::MAX in
|
|
// this simple case (no transit nodes to clamp it).
|
|
let edges = vec![(0, 1)];
|
|
let mut nodes = run_tree_test(2, &edges, false).await;
|
|
|
|
let node1_addr = *nodes[1].node.node_addr();
|
|
|
|
nodes[0].node.initiate_lookup(&node1_addr, 5).await;
|
|
|
|
for _ in 0..4 {
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
process_available_packets(&mut nodes).await;
|
|
}
|
|
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
|
|
assert!(
|
|
nodes[0].node.coord_cache().contains(&node1_addr, now_ms),
|
|
"Node 0 should have cached node 1's route"
|
|
);
|
|
|
|
// 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");
|
|
// In a 2-node setup, no transit node applies the min() so path_mtu stays u16::MAX
|
|
assert_eq!(
|
|
path_mtu,
|
|
u16::MAX,
|
|
"Two-node path_mtu should be u16::MAX (no transit nodes to clamp)"
|
|
);
|
|
|
|
cleanup_nodes(&mut nodes).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_response_path_mtu_three_node_chain() {
|
|
// Topology: node0 — node1 — node2
|
|
// Node0 initiates lookup for node2. The response travels node2→node1→node0.
|
|
// Node1 is a transit node and applies path_mtu = min(u16::MAX, link_mtu).
|
|
// With test transport MTU of 1280, the final path_mtu at node0 should be 1280.
|
|
let edges = vec![(0, 1), (1, 2)];
|
|
let mut nodes = run_tree_test(3, &edges, false).await;
|
|
|
|
let node2_addr = *nodes[2].node.node_addr();
|
|
let node2_pubkey = nodes[2].node.identity().pubkey_full();
|
|
|
|
nodes[0].node.register_identity(node2_addr, node2_pubkey);
|
|
|
|
nodes[0].node.initiate_lookup(&node2_addr, 8).await;
|
|
|
|
for _ in 0..10 {
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
process_available_packets(&mut nodes).await;
|
|
}
|
|
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
|
|
assert!(
|
|
nodes[0].node.coord_cache().contains(&node2_addr, now_ms),
|
|
"Node 0 should have cached node 2's route"
|
|
);
|
|
|
|
// 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");
|
|
assert_eq!(
|
|
path_mtu, 1280,
|
|
"Three-node chain path_mtu should reflect transit node's transport MTU (1280)"
|
|
);
|
|
|
|
cleanup_nodes(&mut nodes).await;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Unit Tests — Cache Entry path_mtu
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_cache_entry_path_mtu_stored() {
|
|
// Verify that insert_with_path_mtu stores the path_mtu in the cache entry
|
|
let mut node = make_node();
|
|
let target = make_node_addr(0xBB);
|
|
|
|
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);
|
|
|
|
let entry = node.coord_cache().get_entry(&target).unwrap();
|
|
assert_eq!(entry.path_mtu(), Some(1280));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cache_entry_no_path_mtu_from_regular_insert() {
|
|
// Verify that regular insert() does not set path_mtu
|
|
let mut node = make_node();
|
|
let target = make_node_addr(0xBB);
|
|
|
|
let coords = TreeCoordinate::from_addrs(vec![target, make_node_addr(0)]).unwrap();
|
|
|
|
let now_ms = 1000u64;
|
|
node.coord_cache_mut().insert(target, coords, now_ms);
|
|
|
|
let entry = node.coord_cache().get_entry(&target).unwrap();
|
|
assert_eq!(entry.path_mtu(), None);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Unit Tests — LookupRequest min_mtu field
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_request_min_mtu_preserved_through_encode_decode() {
|
|
// Verify min_mtu survives encode/decode in the handler test context
|
|
let target = make_node_addr(0xBB);
|
|
let origin = make_node_addr(0xCC);
|
|
|
|
let request = LookupRequest::new(100, target, origin, 5, 1386);
|
|
let encoded = request.encode();
|
|
let decoded = LookupRequest::decode(&encoded[1..]).unwrap();
|
|
assert_eq!(decoded.min_mtu, 1386);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Unit Tests — LookupResponse path_mtu in originator handling
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_originator_stores_path_mtu_in_cache() {
|
|
// Verify that the originator stores path_mtu from the response in coord_cache
|
|
let mut node = make_node();
|
|
let from = make_node_addr(0xAA);
|
|
|
|
let target_identity = Identity::generate();
|
|
let target = *target_identity.node_addr();
|
|
let root = make_node_addr(0xF0);
|
|
let coords = TreeCoordinate::from_addrs(vec![target, root]).unwrap();
|
|
|
|
node.register_identity(target, target_identity.pubkey_full());
|
|
|
|
let proof_data = LookupResponse::proof_bytes(800, &target, &coords);
|
|
let proof = target_identity.sign(&proof_data);
|
|
|
|
let mut response = LookupResponse::new(800, target, coords.clone(), proof);
|
|
// Simulate transit having reduced path_mtu
|
|
response.path_mtu = 1280;
|
|
|
|
let payload = &response.encode()[1..];
|
|
|
|
node.handle_lookup_response(&from, payload).await;
|
|
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
|
|
assert!(node.coord_cache().contains(&target, now_ms));
|
|
|
|
let entry = node.coord_cache().get_entry(&target).unwrap();
|
|
assert_eq!(
|
|
entry.path_mtu(),
|
|
Some(1280),
|
|
"Originator should store path_mtu from LookupResponse in cache"
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Integration Tests — min_mtu transit pruning
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_transit_prunes_lookup_by_min_mtu() {
|
|
// Topology: node0(1280) — node1(800) — node2(1280)
|
|
// Node0 initiates lookup for node2 with min_mtu=1280 (default TUN MTU).
|
|
// Node1's transport MTU is 800 < 1280, so node1 should NOT forward
|
|
// the request to node2. The lookup should fail (no cache entry).
|
|
let mtus = [1280, 800, 1280];
|
|
let edges = vec![(0, 1), (1, 2)];
|
|
let mut nodes = run_tree_test_with_mtus(&mtus, &edges).await;
|
|
|
|
let node2_addr = *nodes[2].node.node_addr();
|
|
let node2_pubkey = nodes[2].node.identity().pubkey_full();
|
|
nodes[0].node.register_identity(node2_addr, node2_pubkey);
|
|
|
|
nodes[0].node.initiate_lookup(&node2_addr, 8).await;
|
|
|
|
for _ in 0..10 {
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
process_available_packets(&mut nodes).await;
|
|
}
|
|
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
|
|
assert!(
|
|
!nodes[0].node.coord_cache().contains(&node2_addr, now_ms),
|
|
"Node0 should NOT have cached node2 route (transit pruned by min_mtu)"
|
|
);
|
|
|
|
cleanup_nodes(&mut nodes).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_transit_forwards_when_mtu_sufficient() {
|
|
// Topology: node0(1280) — node1(1400) — node2(1280)
|
|
// Node0 initiates lookup for node2 with min_mtu=1280 (default TUN MTU).
|
|
// Node1's transport MTU is 1400 >= 1280, so the request passes through.
|
|
// Node1 annotates path_mtu = min(u16::MAX, 1400) = 1400 on response.
|
|
let mtus = [1280, 1400, 1280];
|
|
let edges = vec![(0, 1), (1, 2)];
|
|
let mut nodes = run_tree_test_with_mtus(&mtus, &edges).await;
|
|
|
|
let node2_addr = *nodes[2].node.node_addr();
|
|
let node2_pubkey = nodes[2].node.identity().pubkey_full();
|
|
nodes[0].node.register_identity(node2_addr, node2_pubkey);
|
|
|
|
nodes[0].node.initiate_lookup(&node2_addr, 8).await;
|
|
|
|
for _ in 0..10 {
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
process_available_packets(&mut nodes).await;
|
|
}
|
|
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
|
|
assert!(
|
|
nodes[0].node.coord_cache().contains(&node2_addr, now_ms),
|
|
"Node0 should have cached node2 route (MTU sufficient)"
|
|
);
|
|
|
|
let entry = nodes[0].node.coord_cache().get_entry(&node2_addr).unwrap();
|
|
let path_mtu = entry.path_mtu().expect("path_mtu should be set");
|
|
assert_eq!(
|
|
path_mtu, 1400,
|
|
"path_mtu should reflect transit node's transport MTU (1400)"
|
|
);
|
|
|
|
cleanup_nodes(&mut nodes).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_response_path_mtu_four_node_chain() {
|
|
// Topology: node0(1280) — node1(1400) — node2(900) — node3(1280)
|
|
// Node0 initiates lookup for node3. Response travels node3→node2→node1→node0.
|
|
// Transit nodes apply min(): node2 sees min(u16::MAX, 900) = 900,
|
|
// node1 sees min(900, 1400) = 900.
|
|
// Final path_mtu at node0 should be 900 (bottleneck at node2).
|
|
//
|
|
// Note: min_mtu=1280 from TUN config. Node2's MTU (900) < 1280 would prune
|
|
// the forward request at node2, so node3 would never be reached. To test
|
|
// path_mtu annotation we need all transit links to pass the min_mtu check.
|
|
// Use MTUs above 1280 to avoid pruning but with different values to verify min().
|
|
let mtus = [1280, 1500, 1350, 1280];
|
|
let edges = vec![(0, 1), (1, 2), (2, 3)];
|
|
let mut nodes = run_tree_test_with_mtus(&mtus, &edges).await;
|
|
|
|
let node3_addr = *nodes[3].node.node_addr();
|
|
let node3_pubkey = nodes[3].node.identity().pubkey_full();
|
|
nodes[0].node.register_identity(node3_addr, node3_pubkey);
|
|
|
|
nodes[0].node.initiate_lookup(&node3_addr, 8).await;
|
|
|
|
for _ in 0..15 {
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
process_available_packets(&mut nodes).await;
|
|
}
|
|
|
|
let now_ms = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_millis() as u64)
|
|
.unwrap_or(0);
|
|
|
|
assert!(
|
|
nodes[0].node.coord_cache().contains(&node3_addr, now_ms),
|
|
"Node0 should have cached node3 route"
|
|
);
|
|
|
|
let entry = nodes[0].node.coord_cache().get_entry(&node3_addr).unwrap();
|
|
let path_mtu = entry.path_mtu().expect("path_mtu should be set");
|
|
assert_eq!(
|
|
path_mtu, 1350,
|
|
"Four-node chain path_mtu should be min of transit MTUs (1350)"
|
|
);
|
|
|
|
cleanup_nodes(&mut nodes).await;
|
|
}
|