#![cfg(all(feature = "net", feature = "nat-traversal"))]
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use net::adapter::net::behavior::capability::CapabilitySet;
use net::adapter::net::traversal::classify::NatClass;
use net::adapter::net::traversal::TraversalError;
use net::adapter::net::{EntityKeypair, MeshNode, MeshNodeConfig, SocketBufferConfig};
const TEST_BUFFER_SIZE: usize = 256 * 1024;
const PSK: [u8; 32] = [0x42u8; 32];
fn test_config() -> MeshNodeConfig {
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let mut cfg = MeshNodeConfig::new(addr, PSK)
.with_heartbeat_interval(Duration::from_millis(200))
.with_session_timeout(Duration::from_secs(5))
.with_handshake(4, Duration::from_secs(4));
cfg.socket_buffers = SocketBufferConfig {
send_buffer_size: TEST_BUFFER_SIZE,
recv_buffer_size: TEST_BUFFER_SIZE,
};
cfg
}
async fn build_node() -> Arc<MeshNode> {
Arc::new(
MeshNode::new(EntityKeypair::generate(), test_config())
.await
.expect("MeshNode::new"),
)
}
async fn connect_pair(a: &Arc<MeshNode>, b: &Arc<MeshNode>) {
let a_id = a.node_id();
let b_pub = *b.public_key();
let b_addr = b.local_addr();
let b_id = b.node_id();
let b_clone = b.clone();
let accept = tokio::spawn(async move { b_clone.accept(a_id).await });
a.connect(b_addr, &b_pub, b_id)
.await
.expect("connect failed");
accept
.await
.expect("accept task panicked")
.expect("accept failed");
}
async fn wait_for<F: Fn() -> bool>(limit: Duration, check: F) -> bool {
let start = tokio::time::Instant::now();
while start.elapsed() < limit {
if check() {
return true;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
check()
}
async fn punch_topology() -> (Arc<MeshNode>, Arc<MeshNode>, Arc<MeshNode>, Arc<MeshNode>) {
let a = build_node().await;
let r = build_node().await;
let b = build_node().await;
let x = build_node().await;
connect_pair(&a, &r).await;
connect_pair(&b, &r).await;
connect_pair(&a, &x).await;
connect_pair(&b, &x).await;
connect_pair(&r, &x).await;
a.start();
r.start();
b.start();
x.start();
(a, r, b, x)
}
async fn force_classes_and_announce(
a: &Arc<MeshNode>,
b: &Arc<MeshNode>,
a_class: NatClass,
b_class: NatClass,
) {
a.reclassify_nat().await;
b.reclassify_nat().await;
a.force_nat_class_for_test(a_class);
b.force_nat_class_for_test(b_class);
a.announce_capabilities(CapabilitySet::new())
.await
.expect("A announce");
b.announce_capabilities(CapabilitySet::new())
.await
.expect("B announce");
let a_poll = a.clone();
let b_id = b.node_id();
let b_bind = b.local_addr();
assert!(
wait_for(Duration::from_secs(3), || {
a_poll.peer_reflex_addr(b_id) == Some(b_bind) && a_poll.peer_nat_class(b_id) == b_class
})
.await,
"A should see B's {b_class:?} class + reflex",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn symmetric_cone_punch_failure_attempts_exactly_once() {
let (a, r, b, _x) = punch_topology().await;
force_classes_and_announce(&a, &b, NatClass::Symmetric, NatClass::Cone).await;
let b_id = b.node_id();
let b_bind = b.local_addr();
let b_pub = *b.public_key();
a.block_peer(b_bind);
let before = a.traversal_stats();
let start = tokio::time::Instant::now();
let session_id = a
.connect_direct(b_id, &b_pub, r.node_id())
.await
.expect("punch-failed connect_direct must still resolve via the relay");
let elapsed = start.elapsed();
assert_eq!(session_id, b_id);
let after = a.traversal_stats();
assert_eq!(
after.punches_attempted - before.punches_attempted,
1,
"Symmetric × Cone must attempt the punch EXACTLY once — no retry loop",
);
assert_eq!(
after.punches_succeeded, before.punches_succeeded,
"the blocked punch must not be counted as a success",
);
assert_eq!(
after.relay_fallbacks - before.relay_fallbacks,
1,
"the single failed attempt falls back to the relay exactly once",
);
assert_eq!(
after.punches_failed - before.punches_failed,
1,
"derived failure count reflects the one failed attempt",
);
assert_eq!(
after.punch_timeouts - before.punch_timeouts,
1,
"the failure cause is the ack-wait deadline (B never observed A's train)",
);
assert_eq!(
after.punch_rejections, before.punch_rejections,
"no coordinator rejection was involved",
);
assert!(
elapsed >= Duration::from_secs(4),
"the ack-wait should consume ~punch_deadline; elapsed {elapsed:?}",
);
assert!(
elapsed < Duration::from_secs(12),
"punch-failed fallback must resolve within deadline + fallback \
budget; elapsed {elapsed:?}",
);
assert_eq!(
a.peer_addr(b_id),
Some(r.local_addr()),
"the fallback session must ride the relay",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fresh_joiner_punches_using_only_the_announcement() {
let (a, r, b, _x) = punch_topology().await;
force_classes_and_announce(&a, &b, NatClass::Cone, NatClass::Cone).await;
let b_id = b.node_id();
let b_bind = b.local_addr();
let b_pub = *b.public_key();
assert_eq!(
a.peer_addr(b_id),
None,
"precondition: A has never talked to B directly",
);
match a.probe_reflex(b_id).await {
Err(TraversalError::PeerNotReachable) => {}
other => panic!(
"probe_reflex to a never-connected peer must be PeerNotReachable \
(proving the punch can't have probed); got {other:?}",
),
}
let before = a.traversal_stats();
let session_id = a
.connect_direct(b_id, &b_pub, r.node_id())
.await
.expect("announcement-only punch should succeed");
assert_eq!(session_id, b_id);
let after = a.traversal_stats();
assert_eq!(
after.punches_attempted - before.punches_attempted,
1,
"one mediated punch attempt",
);
assert_eq!(
after.punches_succeeded - before.punches_succeeded,
1,
"the punch landed",
);
assert_eq!(
after.relay_fallbacks, before.relay_fallbacks,
"no fallback — the announced reflex was enough",
);
assert_eq!(
a.peer_addr(b_id),
Some(b_bind),
"the punched session must land on B's ANNOUNCED reflex",
);
}