use std::sync::Arc; use std::time::Duration; use base64::Engine as _; use crate::{ common::{ PeerId, error::Error, global_ctx::{ NetworkIdentity, TrustedKeySource, tests::{get_mock_global_ctx, get_mock_global_ctx_with_network}, }, stats_manager::{LabelSet, LabelType, MetricName}, }, proto::api::instance::TrustedKeySourcePb, tunnel::{ common::tests::wait_for_condition, packet_def::{PacketType, ZCPacket}, ring::create_ring_tunnel_pair, }, }; use super::{ create_packet_recv_chan, peer_conn::tests::set_secure_mode_cfg, peer_manager::{PeerManager, RouteAlgoType}, peer_map::PeerMap, peer_session::{PeerSession, PeerSessionStore, SessionKey}, relay_peer_map::RelayPeerMap, route_trait::NextHopPolicy, }; pub async fn create_mock_peer_manager() -> Arc { let (s, _r) = create_packet_recv_chan(); let peer_mgr = Arc::new(PeerManager::new( RouteAlgoType::Ospf, get_mock_global_ctx(), s, )); peer_mgr.run().await.unwrap(); peer_mgr } pub async fn create_mock_peer_manager_with_name(network_name: String) -> Arc { let (s, _r) = create_packet_recv_chan(); let g = get_mock_global_ctx_with_network(Some(NetworkIdentity::new(network_name, "".to_string()))); let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, g, s)); peer_mgr.run().await.unwrap(); peer_mgr } pub async fn create_mock_peer_manager_secure( network_name: String, network_secret: String, ) -> Arc { let (s, _r) = create_packet_recv_chan(); let g = get_mock_global_ctx_with_network(Some(NetworkIdentity::new(network_name, network_secret))); set_secure_mode_cfg(&g, true); let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, g, s)); peer_mgr.run().await.unwrap(); peer_mgr } fn set_private_mode(peer_mgr: &PeerManager, enabled: bool) { let global_ctx = peer_mgr.get_global_ctx(); let mut flags = global_ctx.get_flags(); flags.private_mode = enabled; global_ctx.set_flags(flags); } async fn connect_client_and_server( client: Arc, server: Arc, ) -> (Result<(), Error>, Result<(), Error>) { let (client_ring, server_ring) = create_ring_tunnel_pair(); tokio::join!( { let client = client.clone(); async move { client.add_client_tunnel(client_ring, false).await?; Ok(()) } }, { let server = server.clone(); async move { server.add_tunnel_as_server(server_ring, true).await } } ) } async fn wait_for_foreign_network(server: Arc, network_name: &'static str) { wait_for_condition( || { let server = server.clone(); async move { server .get_foreign_network_manager() .list_foreign_networks() .await .foreign_networks .contains_key(network_name) } }, Duration::from_secs(10), ) .await; } async fn wait_for_foreign_network_peer_count_at_least( server: Arc, network_name: &'static str, min_peer_count: usize, ) { wait_for_condition( || { let server = server.clone(); async move { server .get_foreign_network_manager() .list_foreign_networks() .await .foreign_networks .get(network_name) .map(|entry| entry.peers.len() >= min_peer_count) .unwrap_or(false) } }, Duration::from_secs(10), ) .await; } async fn wait_for_public_peers_empty(client: Arc) { wait_for_condition( || { let client = client.clone(); async move { client .get_foreign_network_client() .list_public_peers() .await .is_empty() } }, Duration::from_secs(5), ) .await; } pub async fn connect_peer_manager(client: Arc, server: Arc) { let (a_ring, b_ring) = create_ring_tunnel_pair(); let a_mgr_copy = client; tokio::spawn(async move { a_mgr_copy.add_client_tunnel(a_ring, false).await.unwrap(); }); let b_mgr_copy = server; tokio::spawn(async move { b_mgr_copy.add_tunnel_as_server(b_ring, true).await.unwrap(); }); } pub async fn wait_route_appear_with_cost( peer_mgr: Arc, node_id: PeerId, cost: Option, ) -> Result<(), Error> { let now = std::time::Instant::now(); while now.elapsed().as_secs() < 5 { let route = peer_mgr.list_routes().await; if route .iter() .any(|r| r.peer_id == node_id && (cost.is_none() || r.cost == cost.unwrap())) { return Ok(()); } tokio::time::sleep(std::time::Duration::from_millis(50)).await; } Err(Error::NotFound) } pub async fn wait_route_appear( peer_mgr: Arc, target_peer: Arc, ) -> Result<(), Error> { wait_route_appear_with_cost(peer_mgr.clone(), target_peer.my_peer_id(), None).await?; wait_route_appear_with_cost(target_peer, peer_mgr.my_peer_id(), None).await } fn metric_value(peer_mgr: &PeerManager, metric: MetricName, network_name: &str) -> u64 { peer_mgr .get_global_ctx() .stats_manager() .get_metric( metric, &LabelSet::new().with_label_type(LabelType::NetworkName(network_name.to_string())), ) .map(|metric| metric.value) .unwrap_or(0) } #[tokio::test] async fn foreign_mgr_stress_test() { const FOREIGN_NETWORK_COUNT: i32 = 20; const PEER_PER_NETWORK: i32 = 3; const PUBLIC_PEER_COUNT: i32 = 3; let mut public_peers = Vec::new(); for _ in 0..PUBLIC_PEER_COUNT { public_peers.push(create_mock_peer_manager().await); } connect_peer_manager(public_peers[0].clone(), public_peers[1].clone()).await; connect_peer_manager(public_peers[0].clone(), public_peers[2].clone()).await; connect_peer_manager(public_peers[1].clone(), public_peers[2].clone()).await; let mut foreigns = Vec::new(); for i in 0..FOREIGN_NETWORK_COUNT { let mut peers = Vec::new(); let name = format!("foreign-network-test-{}", i); for _ in 0..PEER_PER_NETWORK { let mgr = create_mock_peer_manager_with_name(name.clone()).await; let public_peer_idx = rand::random::() % public_peers.len(); connect_peer_manager(mgr.clone(), public_peers[public_peer_idx].clone()).await; peers.push(mgr); } foreigns.push(peers); } for _ in 0..5 { for i in 0..PUBLIC_PEER_COUNT { let p = public_peers[i as usize].clone(); println!( "public peer {} routes: {:?}, global_foreign_network: {:?}, peers: {:?}", i, p.list_routes().await, p.list_global_foreign_network().await.foreign_networks.len(), p.get_peer_map().list_peers() ); } tokio::time::sleep(std::time::Duration::from_secs(1)).await; let new_peer = create_mock_peer_manager().await; connect_peer_manager(new_peer.clone(), public_peers[0].clone()).await; while let Err(e) = wait_route_appear(public_peers[1].clone(), new_peer.clone()).await { println!("wait route ret: {:?}", e); } } } #[tokio::test] async fn relay_peer_map_secure_session_decrypt() { let (s, _r) = create_packet_recv_chan(); let ctx = get_mock_global_ctx_with_network(Some(NetworkIdentity::new( "net1".to_string(), "sec1".to_string(), ))); set_secure_mode_cfg(&ctx, true); let peer_map = Arc::new(PeerMap::new(s, ctx.clone(), 10)); let store = Arc::new(PeerSessionStore::new()); let relay_map = RelayPeerMap::new(peer_map, None, ctx.clone(), 10, store.clone()); let algo = ctx.get_flags().encryption_algorithm.clone(); let root_key = [7u8; 32]; let session = Arc::new(PeerSession::new( 20, root_key, 1, 1, algo.clone(), algo.clone(), None, )); let key = SessionKey::new(ctx.get_network_identity().network_name, 20); store.insert_session(key.clone(), session.clone()); relay_map .ensure_session(20, NextHopPolicy::LeastHop) .await .unwrap(); assert!(relay_map.has_session(20)); let mut packet = ZCPacket::new_with_payload(b"relay-hello"); packet.fill_peer_manager_hdr(20, 10, PacketType::Data as u8); session.encrypt_payload(20, 10, &mut packet).unwrap(); assert!(relay_map.decrypt_if_needed(&mut packet).await.unwrap()); assert_eq!(packet.payload(), b"relay-hello"); } #[tokio::test] async fn private_mode_allows_foreign_network_with_same_secret() { let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await; let client = create_mock_peer_manager_secure("tenant-a".to_string(), "shared".to_string()).await; set_private_mode(&server, true); let (client_ret, server_ret) = connect_client_and_server(client, server.clone()).await; assert!(client_ret.is_ok(), "client should connect in private mode"); assert!( server_ret.is_ok(), "server should accept foreign network with matching secret: {:?}", server_ret ); wait_for_foreign_network(server, "tenant-a").await; } #[tokio::test] async fn private_mode_rejects_foreign_network_with_different_secret() { let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await; let client = create_mock_peer_manager_secure("tenant-a".to_string(), "other".to_string()).await; set_private_mode(&server, true); let (client_ret, server_ret) = connect_client_and_server(client.clone(), server.clone()).await; assert!( server_ret.is_err(), "server should reject foreign network with mismatched secret in private mode" ); let _ = client_ret; wait_for_public_peers_empty(client).await; assert!( server .get_foreign_network_manager() .list_foreign_networks() .await .foreign_networks .is_empty() ); } #[tokio::test] async fn private_mode_allows_trusted_foreign_credential() { let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await; let admin = create_mock_peer_manager_secure("tenant-a".to_string(), "shared".to_string()).await; set_private_mode(&server, true); let (_cred_id, cred_secret) = admin .get_global_ctx() .get_credential_manager() .generate_credential(vec![], false, vec![], Duration::from_secs(3600)); let privkey_bytes: [u8; 32] = base64::engine::general_purpose::STANDARD .decode(&cred_secret) .unwrap() .try_into() .unwrap(); let private = x25519_dalek::StaticSecret::from(privkey_bytes); let public = x25519_dalek::PublicKey::from(&private); let credential = create_mock_peer_manager_credential("tenant-a".to_string(), &private).await; connect_peer_manager(admin.clone(), server.clone()).await; wait_for_condition( || { let server = server.clone(); let pubkey = public.as_bytes().to_vec(); async move { server .get_foreign_network_manager() .list_foreign_networks_with_options(true) .await .foreign_networks .get("tenant-a") .map(|entry| { entry.trusted_keys.iter().any(|trusted_key| { trusted_key.pubkey == pubkey && trusted_key.source == TrustedKeySourcePb::OspfCredential as i32 }) }) .unwrap_or(false) } }, Duration::from_secs(10), ) .await; let (client_ret, server_ret) = connect_client_and_server(credential, server.clone()).await; assert!( client_ret.is_ok(), "trusted foreign credential client should connect in private mode" ); assert!( server_ret.is_ok(), "server should allow trusted foreign credential in private mode: {:?}", server_ret ); wait_for_foreign_network_peer_count_at_least(server, "tenant-a", 2).await; } #[tokio::test] async fn private_mode_rejects_untrusted_foreign_credential() { let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await; let admin = create_mock_peer_manager_secure("tenant-a".to_string(), "shared".to_string()).await; set_private_mode(&server, true); let random_private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng); let unknown_credential = create_mock_peer_manager_credential("tenant-a".to_string(), &random_private).await; connect_peer_manager(admin.clone(), server.clone()).await; wait_for_foreign_network(server.clone(), "tenant-a").await; let (client_ret, server_ret) = connect_client_and_server(unknown_credential, server.clone()).await; let _ = client_ret; assert!( server_ret.is_err(), "server should reject untrusted foreign credential in private mode" ); wait_for_condition( || { let server = server.clone(); async move { server .get_foreign_network_manager() .list_foreign_networks() .await .foreign_networks .get("tenant-a") .map(|entry| entry.peers.len() == 1) .unwrap_or(false) } }, Duration::from_secs(10), ) .await; } #[tokio::test] async fn relay_peer_map_retry_backoff_and_evict() { let (s, _r) = create_packet_recv_chan(); let ctx_secure = get_mock_global_ctx(); set_secure_mode_cfg(&ctx_secure, true); let peer_map = Arc::new(PeerMap::new(s, ctx_secure.clone(), 10)); let relay_map = RelayPeerMap::new( peer_map, None, ctx_secure.clone(), 10, Arc::new(PeerSessionStore::new()), ); let ret = relay_map .handshake_session(20, NextHopPolicy::LeastHop, None) .await; assert!(ret.is_err()); assert!(relay_map.failure_count(20).unwrap_or(0) >= 1); assert!(relay_map.is_backoff_active(20)); let (s2, _r2) = create_packet_recv_chan(); let ctx_plain = get_mock_global_ctx(); let peer_map_plain = Arc::new(PeerMap::new(s2, ctx_plain.clone(), 30)); let relay_map_plain = RelayPeerMap::new( peer_map_plain, None, ctx_plain.clone(), 30, Arc::new(PeerSessionStore::new()), ); let mut pkt = ZCPacket::new_with_payload(b"evict"); pkt.fill_peer_manager_hdr(30, 40, PacketType::Data as u8); let _ = relay_map_plain .send_msg(pkt, 40, NextHopPolicy::LeastHop) .await; assert!(relay_map_plain.has_state(40)); relay_map_plain.evict_idle_sessions(Duration::from_millis(0)); assert!(!relay_map_plain.has_state(40)); } #[tokio::test] async fn relay_peer_map_pending_packet_buffer() { // Verify that packets sent during handshake are buffered (not dropped), // and flushed after handshake completes. let (s, _r) = create_packet_recv_chan(); let ctx = get_mock_global_ctx_with_network(Some(NetworkIdentity::new( "net1".to_string(), "sec1".to_string(), ))); set_secure_mode_cfg(&ctx, true); let peer_map = Arc::new(PeerMap::new(s, ctx.clone(), 10)); let store = Arc::new(PeerSessionStore::new()); let relay_map = RelayPeerMap::new(peer_map, None, ctx.clone(), 10, store.clone()); // Send multiple packets while no session exists (handshake will fail, but packets should be buffered) for i in 0..5u8 { let mut pkt = ZCPacket::new_with_payload(&[i]); pkt.fill_peer_manager_hdr(10, 20, PacketType::Data as u8); let _ = relay_map.send_msg(pkt, 20, NextHopPolicy::LeastHop).await; } // Verify packets were buffered assert_eq!( relay_map .pending_packets .get(&20) .map(|v| v.len()) .unwrap_or(0), 5, "5 packets should be buffered during handshake" ); // Verify buffer respects capacity limit for i in 0..50u8 { let mut pkt = ZCPacket::new_with_payload(&[i]); pkt.fill_peer_manager_hdr(10, 20, PacketType::Data as u8); let _ = relay_map.send_msg(pkt, 20, NextHopPolicy::LeastHop).await; } let buffered = relay_map .pending_packets .get(&20) .map(|v| v.len()) .unwrap_or(0); assert!( buffered <= 32, "buffer should not exceed MAX_PENDING_PACKETS_PER_PEER, got {buffered}" ); // Verify remove_peer clears pending packets relay_map.remove_peer(20); assert_eq!( relay_map .pending_packets .get(&20) .map(|v| v.len()) .unwrap_or(0), 0, "pending packets should be cleared on peer removal" ); } #[tokio::test] async fn relay_peer_map_pending_packets_flushed_on_handshake_success() { // Test that pending packets are flushed after handshake succeeds. // We pre-populate the buffer, then run handshake, and verify it's cleared. let peer_a = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; let peer_b = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; let peer_c = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; connect_peer_manager(peer_a.clone(), peer_b.clone()).await; connect_peer_manager(peer_b.clone(), peer_c.clone()).await; let peer_a_id = peer_a.my_peer_id(); let peer_c_id = peer_c.my_peer_id(); // Wait for routes to propagate wait_for_condition( || { let peer_a = peer_a.clone(); let peer_c = peer_c.clone(); async move { wait_route_appear(peer_a.clone(), peer_c).await.is_ok() } }, Duration::from_secs(10), ) .await; // Wait for noise_static_pubkey to be available on both sides wait_for_condition( || { let peer_a = peer_a.clone(); async move { peer_a .get_peer_map() .get_route_peer_info(peer_c_id) .await .map(|info| !info.noise_static_pubkey.is_empty()) .unwrap_or(false) } }, Duration::from_secs(10), ) .await; let relay_a = peer_a.get_relay_peer_map(); // Pre-populate pending packets buffer (simulating what send_msg does during handshake) for i in 0..3u8 { let mut pkt = ZCPacket::new_with_payload(&[i]); pkt.fill_peer_manager_hdr(peer_a_id, peer_c_id, PacketType::Data as u8); relay_a .pending_packets .entry(peer_c_id) .or_default() .push((pkt, NextHopPolicy::LeastHop)); } assert_eq!( relay_a .pending_packets .get(&peer_c_id) .map(|v| v.len()) .unwrap_or(0), 3, "3 packets should be in the buffer" ); // Run handshake — on success it should flush the buffer relay_a .handshake_session(peer_c_id, NextHopPolicy::LeastHop, None) .await .unwrap(); // Verify session established and buffer cleared assert!(relay_a.has_session(peer_c_id)); assert_eq!( relay_a .pending_packets .get(&peer_c_id) .map(|v| v.len()) .unwrap_or(0), 0, "pending packets should be flushed after successful handshake" ); } #[tokio::test] async fn relay_peer_map_real_link_handshake_success() { let peer_a = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; let peer_b = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; let peer_c = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; connect_peer_manager(peer_a.clone(), peer_b.clone()).await; connect_peer_manager(peer_b.clone(), peer_c.clone()).await; let peer_a_id = peer_a.my_peer_id(); let peer_b_id = peer_b.my_peer_id(); let peer_c_id = peer_c.my_peer_id(); let a_control_tx_before = metric_value(&peer_a, MetricName::TrafficControlBytesTx, "net1"); let a_control_rx_before = metric_value(&peer_a, MetricName::TrafficControlBytesRx, "net1"); let c_control_tx_before = metric_value(&peer_c, MetricName::TrafficControlBytesTx, "net1"); let c_control_rx_before = metric_value(&peer_c, MetricName::TrafficControlBytesRx, "net1"); wait_for_condition( || { let peer_a = peer_a.clone(); let peer_c = peer_c.clone(); async move { wait_route_appear(peer_a.clone(), peer_c).await.is_ok() } }, Duration::from_secs(10), ) .await; wait_for_condition( || { let peer_a = peer_a.clone(); async move { peer_a .get_peer_map() .get_gateway_peer_id(peer_c_id, NextHopPolicy::LeastHop) .await == Some(peer_b_id) } }, Duration::from_secs(5), ) .await; wait_for_condition( || { let peer_a = peer_a.clone(); async move { peer_a .get_peer_map() .get_route_peer_info(peer_c_id) .await .map(|info| !info.noise_static_pubkey.is_empty()) .unwrap_or(false) } }, Duration::from_secs(10), ) .await; let relay_a = peer_a.get_relay_peer_map(); let relay_c = peer_c.get_relay_peer_map(); relay_a .handshake_session(peer_c_id, NextHopPolicy::LeastHop, None) .await .unwrap(); wait_for_condition( || { let relay_a = relay_a.clone(); async move { relay_a.has_session(peer_c_id) } }, Duration::from_secs(5), ) .await; wait_for_condition( || { let relay_c = relay_c.clone(); async move { relay_c.has_session(peer_a_id) } }, Duration::from_secs(5), ) .await; assert!(metric_value(&peer_a, MetricName::TrafficControlBytesTx, "net1") > a_control_tx_before); assert!(metric_value(&peer_a, MetricName::TrafficControlBytesRx, "net1") > a_control_rx_before); assert!(metric_value(&peer_c, MetricName::TrafficControlBytesTx, "net1") > c_control_tx_before); assert!(metric_value(&peer_c, MetricName::TrafficControlBytesRx, "net1") > c_control_rx_before); } #[tokio::test] async fn relay_peer_map_responder_rejects_mismatched_pubkey() { // Create three peers: A -> B -> C let peer_a = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; let peer_b = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; let peer_c = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; connect_peer_manager(peer_a.clone(), peer_b.clone()).await; connect_peer_manager(peer_b.clone(), peer_c.clone()).await; let peer_a_id = peer_a.my_peer_id(); let peer_c_id = peer_c.my_peer_id(); // Wait for routes to propagate wait_for_condition( || { let peer_a = peer_a.clone(); let peer_c = peer_c.clone(); async move { wait_route_appear(peer_a.clone(), peer_c).await.is_ok() } }, Duration::from_secs(10), ) .await; // Wait for noise_static_pubkey to be available wait_for_condition( || { let peer_a = peer_a.clone(); async move { peer_a .get_peer_map() .get_route_peer_info(peer_c_id) .await .map(|info| !info.noise_static_pubkey.is_empty()) .unwrap_or(false) } }, Duration::from_secs(10), ) .await; // Get the original correct pubkey to verify it exists let original_info = peer_a .get_peer_map() .get_route_peer_info(peer_c_id) .await .expect("should have route info for peer_c"); assert!( !original_info.noise_static_pubkey.is_empty(), "noise_static_pubkey should be present" ); // Attempt handshake - this should succeed because pubkeys match let relay_a = peer_a.get_relay_peer_map(); let result = relay_a .handshake_session(peer_c_id, NextHopPolicy::LeastHop, None) .await; // The handshake should succeed because the pubkeys match assert!( result.is_ok(), "handshake should succeed with matching pubkeys" ); // Verify session was established on both sides wait_for_condition( || { let relay_a = relay_a.clone(); async move { relay_a.has_session(peer_c_id) } }, Duration::from_secs(5), ) .await; let relay_c = peer_c.get_relay_peer_map(); wait_for_condition( || { let relay_c = relay_c.clone(); async move { relay_c.has_session(peer_a_id) } }, Duration::from_secs(5), ) .await; } #[tokio::test] async fn relay_peer_map_remove_peer() { let (s, _r) = create_packet_recv_chan(); let ctx = get_mock_global_ctx_with_network(Some(NetworkIdentity::new( "net1".to_string(), "sec1".to_string(), ))); set_secure_mode_cfg(&ctx, true); let peer_map = Arc::new(PeerMap::new(s, ctx.clone(), 10)); let store = Arc::new(PeerSessionStore::new()); let relay_map = RelayPeerMap::new(peer_map, None, ctx.clone(), 10, store.clone()); let peer_1: PeerId = 100; // Add session for peer_1 let root_key = [1u8; 32]; let session = Arc::new(PeerSession::new( peer_1, root_key, 1, 0, "aes-256-gcm".to_string(), "aes-256-gcm".to_string(), None, )); let key = SessionKey::new(ctx.get_network_name(), peer_1); store.insert_session(key.clone(), session); assert!(store.get(&key).is_some()); // Remove the peer relay state relay_map.remove_peer(peer_1); // Session should still be in the store (lifecycle is independent of relay state) assert!( store.get(&key).is_some(), "session should persist after relay peer removal" ); } /// Test bidirectional handshake race resolution. /// When both peers simultaneously initiate handshake, the one with smaller peer_id /// should become initiator, and the other should yield and become responder. #[tokio::test] async fn relay_peer_map_bidirectional_handshake_race() { // Create three peers: A -> B -> C let peer_a = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; let peer_b = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; let peer_c = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; connect_peer_manager(peer_a.clone(), peer_b.clone()).await; connect_peer_manager(peer_b.clone(), peer_c.clone()).await; let peer_a_id = peer_a.my_peer_id(); let peer_c_id = peer_c.my_peer_id(); // Wait for routes to propagate wait_for_condition( || { let peer_a = peer_a.clone(); let peer_c = peer_c.clone(); async move { wait_route_appear(peer_a.clone(), peer_c).await.is_ok() } }, Duration::from_secs(10), ) .await; // Wait for noise_static_pubkey to be available wait_for_condition( || { let peer_a = peer_a.clone(); async move { peer_a .get_peer_map() .get_route_peer_info(peer_c_id) .await .map(|info| !info.noise_static_pubkey.is_empty()) .unwrap_or(false) } }, Duration::from_secs(10), ) .await; wait_for_condition( || { let peer_c = peer_c.clone(); async move { peer_c .get_peer_map() .get_route_peer_info(peer_a_id) .await .map(|info| !info.noise_static_pubkey.is_empty()) .unwrap_or(false) } }, Duration::from_secs(10), ) .await; // Simulate bidirectional handshake race by having both sides initiate simultaneously let relay_a = peer_a.get_relay_peer_map(); let relay_c = peer_c.get_relay_peer_map(); // Both sides initiate handshake at the same time let handle_a = tokio::spawn({ let relay_a = relay_a.clone(); async move { relay_a .handshake_session(peer_c_id, NextHopPolicy::LeastHop, None) .await } }); let handle_c = tokio::spawn({ let relay_c = relay_c.clone(); async move { relay_c .handshake_session(peer_a_id, NextHopPolicy::LeastHop, None) .await } }); // Wait for both handshakes to complete let (result_a, result_c) = tokio::join!(handle_a, handle_c); // At least one should succeed (the initiator with smaller peer_id) // Both could succeed if race resolution worked correctly tracing::info!( ?peer_a_id, ?peer_c_id, ?result_a, ?result_c, "bidirectional handshake results" ); // Wait for sessions to be established wait_for_condition( || { let relay_a = relay_a.clone(); async move { relay_a.has_session(peer_c_id) } }, Duration::from_secs(5), ) .await; wait_for_condition( || { let relay_c = relay_c.clone(); async move { relay_c.has_session(peer_a_id) } }, Duration::from_secs(5), ) .await; // Both sides should have sessions after race resolution assert!( relay_a.has_session(peer_c_id), "peer_a should have session with peer_c" ); assert!( relay_c.has_session(peer_a_id), "peer_c should have session with peer_a" ); } /// Helper: create a secure peer manager for a credential node. /// Uses the given X25519 private key as the Noise static key, with no network_secret. pub async fn create_mock_peer_manager_credential( network_name: String, private_key: &x25519_dalek::StaticSecret, ) -> Arc { use crate::common::config::NetworkIdentity; use crate::proto::common::SecureModeConfig; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; let (s, _r) = create_packet_recv_chan(); let g = get_mock_global_ctx_with_network(Some(NetworkIdentity::new_credential(network_name))); let public = x25519_dalek::PublicKey::from(private_key); g.config.set_secure_mode(Some(SecureModeConfig { enabled: true, local_private_key: Some(BASE64_STANDARD.encode(private_key.as_bytes())), local_public_key: Some(BASE64_STANDARD.encode(public.as_bytes())), })); let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, g, s)); peer_mgr.run().await.unwrap(); peer_mgr } /// Test: credential node joins a 2-admin network and routes appear. /// Topology: Admin_A -- Credential_C, Admin_A -- Admin_B /// Credential node connects to the admin that generated the credential. #[tokio::test] async fn credential_node_joins_network() { let admin_a = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; let admin_b = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; // Generate credential on admin_a let (_cred_id, cred_secret) = admin_a .get_global_ctx() .get_credential_manager() .generate_credential( vec!["guest".to_string()], false, vec![], std::time::Duration::from_secs(3600), ); // Create credential node using the generated key let privkey_bytes: [u8; 32] = base64::engine::general_purpose::STANDARD .decode(&cred_secret) .unwrap() .try_into() .unwrap(); let private = x25519_dalek::StaticSecret::from(privkey_bytes); let cred_c = create_mock_peer_manager_credential("net1".to_string(), &private).await; // Connect admins first connect_peer_manager(admin_a.clone(), admin_b.clone()).await; // Admin A and B should discover each other wait_route_appear(admin_a.clone(), admin_b.clone()) .await .unwrap(); // Now connect credential node to admin A (credential as client) connect_peer_manager(cred_c.clone(), admin_a.clone()).await; // Credential node C should be reachable from admin B (via A) let cred_c_id = cred_c.my_peer_id(); wait_for_condition( || { let admin_b = admin_b.clone(); async move { admin_b .list_routes() .await .iter() .any(|r| r.peer_id == cred_c_id) } }, Duration::from_secs(10), ) .await; // Credential node C should see admin B wait_for_condition( || { let cred_c = cred_c.clone(); let admin_b_id = admin_b.my_peer_id(); async move { cred_c .list_routes() .await .iter() .any(|r| r.peer_id == admin_b_id) } }, Duration::from_secs(10), ) .await; } /// Test: credential node is rejected when its pubkey is not in any admin's trusted list. /// Topology: Admin_A -- Unknown_B (random key, not in trusted list) #[tokio::test] async fn unknown_credential_node_rejected() { let admin_a = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; // Create a credential node with a random key (NOT generated by admin) let random_private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng); let unknown_c = create_mock_peer_manager_credential("net1".to_string(), &random_private).await; // Try to connect: C -> A (unknown credential as client, admin as server) connect_peer_manager(unknown_c.clone(), admin_a.clone()).await; // The handshake should fail so the connection won't establish. // Wait a bit and verify no route appears. tokio::time::sleep(Duration::from_secs(3)).await; let routes = admin_a.list_routes().await; assert!( !routes.iter().any(|r| r.peer_id == unknown_c.my_peer_id()), "unknown credential node should NOT appear in admin's routes" ); } /// Test: after revocation, the credential node disappears from routes. /// Topology: Admin_A -- Credential_C, Admin_A -- Admin_B /// After revocation on A, C should be removed from B's route table. #[tokio::test] async fn credential_revocation_removes_from_routes() { let admin_a = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; let admin_b = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; let (cred_id, cred_secret) = admin_a .get_global_ctx() .get_credential_manager() .generate_credential(vec![], false, vec![], std::time::Duration::from_secs(3600)); let privkey_bytes: [u8; 32] = base64::engine::general_purpose::STANDARD .decode(&cred_secret) .unwrap() .try_into() .unwrap(); let private = x25519_dalek::StaticSecret::from(privkey_bytes); let cred_c = create_mock_peer_manager_credential("net1".to_string(), &private).await; // Connect: A -- B, C -> A (credential node as client, admin as server) connect_peer_manager(admin_a.clone(), admin_b.clone()).await; connect_peer_manager(cred_c.clone(), admin_a.clone()).await; // Wait for credential node to appear in admin_b's routes let cred_c_id = cred_c.my_peer_id(); wait_for_condition( || { let admin_b = admin_b.clone(); async move { admin_b .list_routes() .await .iter() .any(|r| r.peer_id == cred_c_id) } }, Duration::from_secs(10), ) .await; // Now revoke the credential assert!( admin_a .get_global_ctx() .get_credential_manager() .revoke_credential(&cred_id) ); // Issue event to trigger OSPF sync admin_a .get_global_ctx() .issue_event(crate::common::global_ctx::GlobalCtxEvent::CredentialChanged); // Wait for credential node to disappear from admin_b's routes wait_for_condition( || { let admin_b = admin_b.clone(); async move { !admin_b .list_routes() .await .iter() .any(|r| r.peer_id == cred_c_id) } }, Duration::from_secs(15), ) .await; } #[tokio::test] async fn credential_expiry_disconnects_from_all_admins() { let admin_a = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; let admin_b = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; connect_peer_manager(admin_a.clone(), admin_b.clone()).await; wait_route_appear(admin_a.clone(), admin_b.clone()) .await .unwrap(); let (_cred_id, cred_secret) = admin_a .get_global_ctx() .get_credential_manager() .generate_credential(vec![], false, vec![], std::time::Duration::from_secs(2)); admin_a .get_global_ctx() .issue_event(crate::common::global_ctx::GlobalCtxEvent::CredentialChanged); let privkey_bytes: [u8; 32] = base64::engine::general_purpose::STANDARD .decode(&cred_secret) .unwrap() .try_into() .unwrap(); let private = x25519_dalek::StaticSecret::from(privkey_bytes); let cred_c = create_mock_peer_manager_credential("net1".to_string(), &private).await; let cred_c_id = cred_c.my_peer_id(); connect_peer_manager(cred_c.clone(), admin_a.clone()).await; wait_for_condition( || { let admin_b = admin_b.clone(); async move { admin_b .list_routes() .await .iter() .any(|r| r.peer_id == cred_c_id) } }, Duration::from_secs(10), ) .await; connect_peer_manager(cred_c.clone(), admin_b.clone()).await; wait_for_condition( || { let admin_b = admin_b.clone(); async move { admin_b .get_peer_map() .list_peer_conns(cred_c_id) .await .is_some_and(|conns| !conns.is_empty()) } }, Duration::from_secs(10), ) .await; tokio::time::sleep(Duration::from_secs(3)).await; wait_for_condition( || { let admin_b = admin_b.clone(); async move { !admin_b .list_routes() .await .iter() .any(|r| r.peer_id == cred_c_id) } }, Duration::from_secs(20), ) .await; wait_for_condition( || { let admin_b = admin_b.clone(); async move { admin_b .get_peer_map() .list_peer_conns(cred_c_id) .await .is_none_or(|conns| conns.is_empty()) } }, Duration::from_secs(20), ) .await; } /// Test: admin node with credential — credential node gets group assignment. /// Verify that the credential node's groups appear in the OSPF sync data. #[tokio::test] async fn credential_node_group_assignment() { let admin_a = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; let admin_b = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; let (_cred_id, cred_secret) = admin_a .get_global_ctx() .get_credential_manager() .generate_credential( vec!["guest".to_string(), "limited".to_string()], false, vec![], std::time::Duration::from_secs(3600), ); let privkey_bytes: [u8; 32] = base64::engine::general_purpose::STANDARD .decode(&cred_secret) .unwrap() .try_into() .unwrap(); let private = x25519_dalek::StaticSecret::from(privkey_bytes); let cred_c = create_mock_peer_manager_credential("net1".to_string(), &private).await; connect_peer_manager(admin_a.clone(), admin_b.clone()).await; connect_peer_manager(cred_c.clone(), admin_a.clone()).await; // Wait for credential node route to appear on admin_b (via OSPF through admin_a) let cred_c_id = cred_c.my_peer_id(); wait_for_condition( || { let admin_b = admin_b.clone(); async move { admin_b .list_routes() .await .iter() .any(|r| r.peer_id == cred_c_id) } }, Duration::from_secs(10), ) .await; // Verify the credential node's groups are assigned via OSPF on admin_b // (admin_b gets the groups from admin_a's TrustedCredentialPubkey via OSPF sync) wait_for_condition( || { let admin_b = admin_b.clone(); async move { let g = admin_b.get_route().get_peer_groups(cred_c_id); g.contains(&"guest".to_string()) && g.contains(&"limited".to_string()) } }, Duration::from_secs(10), ) .await; } #[tokio::test] async fn credential_node_connected_via_admin_b_trusts_admin_a_groups() { use crate::proto::acl::{Acl, AclV1, GroupIdentity, GroupInfo}; let admin_a = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; let admin_b = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; let group_declares = vec![GroupIdentity { group_name: "platform-admin".to_string(), group_secret: "platform-admin-secret".to_string(), }]; admin_a.get_global_ctx().config.set_acl(Some(Acl { acl_v1: Some(AclV1 { group: Some(GroupInfo { declares: group_declares.clone(), members: vec!["platform-admin".to_string()], }), ..Default::default() }), })); admin_b.get_global_ctx().config.set_acl(Some(Acl { acl_v1: Some(AclV1 { group: Some(GroupInfo { declares: group_declares, members: vec![], }), ..Default::default() }), })); connect_peer_manager(admin_a.clone(), admin_b.clone()).await; wait_route_appear(admin_a.clone(), admin_b.clone()) .await .unwrap(); let (_cred_id, cred_secret) = admin_a .get_global_ctx() .get_credential_manager() .generate_credential(vec![], false, vec![], std::time::Duration::from_secs(3600)); admin_a .get_global_ctx() .issue_event(crate::common::global_ctx::GlobalCtxEvent::CredentialChanged); let privkey_bytes: [u8; 32] = base64::engine::general_purpose::STANDARD .decode(&cred_secret) .unwrap() .try_into() .unwrap(); let private = x25519_dalek::StaticSecret::from(privkey_bytes); let credential_pubkey = x25519_dalek::PublicKey::from(&private).as_bytes().to_vec(); wait_for_condition( || { let admin_b = admin_b.clone(); let credential_pubkey = credential_pubkey.clone(); async move { admin_b.get_global_ctx().is_pubkey_trusted_with_source( &credential_pubkey, "net1", TrustedKeySource::OspfCredential, ) } }, Duration::from_secs(10), ) .await; let cred_c = create_mock_peer_manager_credential("net1".to_string(), &private).await; connect_peer_manager(cred_c.clone(), admin_b.clone()).await; let admin_a_id = admin_a.my_peer_id(); wait_for_condition( || { let cred_c = cred_c.clone(); async move { cred_c .list_routes() .await .iter() .any(|r| r.peer_id == admin_a_id) } }, Duration::from_secs(10), ) .await; wait_for_condition( || { let cred_c = cred_c.clone(); async move { cred_c .get_route() .get_peer_groups(admin_a_id) .contains(&"platform-admin".to_string()) } }, Duration::from_secs(10), ) .await; } /// Minimal test: two secure peers connect and discover each other's route. #[tokio::test] async fn two_secure_peers_route_appear() { let peer_a = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; let peer_b = create_mock_peer_manager_secure("net1".to_string(), "sec1".to_string()).await; connect_peer_manager(peer_a.clone(), peer_b.clone()).await; wait_route_appear(peer_a.clone(), peer_b.clone()) .await .unwrap(); } #[tokio::test] async fn multi_admin_multi_credential_route_and_revocation_isolation() { let admin_a = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; let admin_b = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; let admin_d = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; connect_peer_manager(admin_a.clone(), admin_b.clone()).await; connect_peer_manager(admin_b.clone(), admin_d.clone()).await; connect_peer_manager(admin_a.clone(), admin_d.clone()).await; wait_route_appear(admin_a.clone(), admin_b.clone()) .await .unwrap(); wait_route_appear(admin_b.clone(), admin_d.clone()) .await .unwrap(); wait_route_appear(admin_a.clone(), admin_d.clone()) .await .unwrap(); let (cred1_id, cred1_secret) = admin_a .get_global_ctx() .get_credential_manager() .generate_credential( vec!["guest-a".to_string()], false, vec![], std::time::Duration::from_secs(3600), ); let (_cred2_id, cred2_secret) = admin_b .get_global_ctx() .get_credential_manager() .generate_credential( vec!["guest-b".to_string()], false, vec![], std::time::Duration::from_secs(3600), ); let cred1_private: [u8; 32] = base64::engine::general_purpose::STANDARD .decode(&cred1_secret) .unwrap() .try_into() .unwrap(); let cred2_private: [u8; 32] = base64::engine::general_purpose::STANDARD .decode(&cred2_secret) .unwrap() .try_into() .unwrap(); let cred_1 = create_mock_peer_manager_credential( "net1".to_string(), &x25519_dalek::StaticSecret::from(cred1_private), ) .await; let cred_2 = create_mock_peer_manager_credential( "net1".to_string(), &x25519_dalek::StaticSecret::from(cred2_private), ) .await; connect_peer_manager(cred_1.clone(), admin_a.clone()).await; connect_peer_manager(cred_2.clone(), admin_b.clone()).await; let cred_1_id = cred_1.my_peer_id(); let cred_2_id = cred_2.my_peer_id(); wait_for_condition( || { let admin_d = admin_d.clone(); async move { let routes = admin_d.list_routes().await; routes.iter().any(|r| r.peer_id == cred_1_id) && routes.iter().any(|r| r.peer_id == cred_2_id) } }, Duration::from_secs(15), ) .await; wait_for_condition( || { let admin_d = admin_d.clone(); async move { let g1 = admin_d.get_route().get_peer_groups(cred_1_id); let g2 = admin_d.get_route().get_peer_groups(cred_2_id); g1.contains(&"guest-a".to_string()) && g2.contains(&"guest-b".to_string()) } }, Duration::from_secs(15), ) .await; assert!( admin_a .get_global_ctx() .get_credential_manager() .revoke_credential(&cred1_id) ); admin_a .get_global_ctx() .issue_event(crate::common::global_ctx::GlobalCtxEvent::CredentialChanged); wait_for_condition( || { let admin_d = admin_d.clone(); async move { let routes = admin_d.list_routes().await; !routes.iter().any(|r| r.peer_id == cred_1_id) && routes.iter().any(|r| r.peer_id == cred_2_id) } }, Duration::from_secs(20), ) .await; } #[tokio::test] async fn unknown_credential_rejected_while_valid_credential_survives() { let admin_a = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; let admin_b = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; connect_peer_manager(admin_a.clone(), admin_b.clone()).await; wait_route_appear(admin_a.clone(), admin_b.clone()) .await .unwrap(); let (_cred_id, cred_secret) = admin_a .get_global_ctx() .get_credential_manager() .generate_credential( vec!["stable".to_string()], false, vec![], std::time::Duration::from_secs(3600), ); let valid_private: [u8; 32] = base64::engine::general_purpose::STANDARD .decode(&cred_secret) .unwrap() .try_into() .unwrap(); let valid_cred = create_mock_peer_manager_credential( "net1".to_string(), &x25519_dalek::StaticSecret::from(valid_private), ) .await; let unknown_private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng); let unknown_cred = create_mock_peer_manager_credential("net1".to_string(), &unknown_private).await; connect_peer_manager(valid_cred.clone(), admin_a.clone()).await; let (unknown_ring_client, unknown_ring_server) = create_ring_tunnel_pair(); let unknown_connect_client = tokio::spawn({ let unknown_cred = unknown_cred.clone(); async move { unknown_cred .add_client_tunnel(unknown_ring_client, false) .await } }); let unknown_connect_server = tokio::spawn({ let admin_a = admin_a.clone(); async move { admin_a .add_tunnel_as_server(unknown_ring_server, true) .await } }); let (unknown_client_ret, unknown_server_ret) = tokio::join!(unknown_connect_client, unknown_connect_server); assert!( unknown_client_ret.unwrap().is_err() || unknown_server_ret.unwrap().is_err(), "unknown credential connection should fail on at least one side" ); let valid_id = valid_cred.my_peer_id(); let unknown_id = unknown_cred.my_peer_id(); wait_for_condition( || { let admin_b = admin_b.clone(); async move { admin_b .list_routes() .await .iter() .any(|r| r.peer_id == valid_id) } }, Duration::from_secs(15), ) .await; tokio::time::sleep(Duration::from_secs(5)).await; let routes = admin_b.list_routes().await; assert!(routes.iter().any(|r| r.peer_id == valid_id)); assert!(!routes.iter().any(|r| r.peer_id == unknown_id)); }