use phantom_protocol::api::session::PhantomSession;
use phantom_protocol::api::udp_listener::PhantomUdpListener;
use phantom_protocol::api::udp_transport::UdpClientTransport;
use phantom_protocol::crypto::hybrid_sign::HybridVerifyingKey;
use std::time::Duration;
use tokio::time::timeout;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_pinned_and_encrypted() {
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.expect("bind_udp");
let addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let session = listener.accept().await.expect("accept").session();
let msg = session.recv().await.expect("server recv");
assert_eq!(msg, b"hello-from-client");
session
.send(b"hello-from-server".to_vec())
.await
.expect("server send");
tokio::time::sleep(Duration::from_millis(200)).await;
});
let transport = UdpClientTransport::connect(addr)
.await
.expect("udp connect");
let client = PhantomSession::connect_with_transport(&addr.to_string(), transport, key);
client
.send(b"hello-from-client".to_vec())
.await
.expect("client send");
let reply = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout")
.expect("client recv");
assert_eq!(reply, b"hello-from-server");
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_client_stamps_cid0_and_server_routes_the_window() {
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.expect("bind_udp");
let addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let listener_for_server = listener.clone();
let server = tokio::spawn(async move {
let session = listener_for_server
.accept()
.await
.expect("accept")
.session();
let msg = session.recv().await.expect("server recv");
assert_eq!(msg, b"ping");
session.send(b"pong".to_vec()).await.expect("server send");
tokio::time::sleep(Duration::from_millis(300)).await;
});
let transport = UdpClientTransport::connect(addr)
.await
.expect("udp connect");
let client = PhantomSession::connect_with_transport(&addr.to_string(), transport, key);
client.send(b"ping".to_vec()).await.expect("client send");
let reply = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout")
.expect("client recv");
assert_eq!(reply, b"pong");
assert!(
listener.active_route_count() > 1,
"server must register the CID window (> 1 route); got {}",
listener.active_route_count()
);
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_two_sessions_one_client_socket_is_not_required_but_two_clients_ok() {
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.unwrap();
let addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
for _ in 0..2 {
let s = listener.accept().await.expect("accept").session();
let m = s.recv().await.expect("recv");
s.send(m).await.expect("echo"); }
tokio::time::sleep(Duration::from_millis(200)).await;
});
let mut handles = Vec::new();
for i in 0u8..2 {
let key = key.clone();
let addr = addr;
handles.push(tokio::spawn(async move {
let t = UdpClientTransport::connect(addr).await.unwrap();
let c = PhantomSession::connect_with_transport(&addr.to_string(), t, key);
let payload = vec![i; 32];
c.send(payload.clone()).await.unwrap();
let echo = timeout(Duration::from_secs(10), c.recv())
.await
.unwrap()
.unwrap();
assert_eq!(echo, payload);
}));
}
for h in handles {
h.await.unwrap();
}
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_recovers_dropped_first_flight() {
use tokio::net::UdpSocket;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.unwrap();
let server_addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let s = listener.accept().await.expect("accept").session();
let m = s.recv().await.expect("recv");
s.send(m).await.expect("echo");
tokio::time::sleep(Duration::from_millis(200)).await;
});
let relay = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let relay_addr = relay.local_addr().unwrap();
let upstream = UdpSocket::bind("127.0.0.1:0").await.unwrap();
upstream.connect(server_addr).await.unwrap();
tokio::spawn(async move {
let mut c2s_buf = vec![0u8; 2048];
let mut s2c_buf = vec![0u8; 2048];
let mut client_addr: Option<std::net::SocketAddr> = None;
let mut dropped_first = false;
loop {
tokio::select! {
r = relay.recv_from(&mut c2s_buf) => {
let (n, from) = r.unwrap();
client_addr = Some(from);
if !dropped_first { dropped_first = true; continue; } upstream.send(&c2s_buf[..n]).await.unwrap();
}
r = upstream.recv(&mut s2c_buf) => {
let n = r.unwrap();
if let Some(ca) = client_addr { relay.send_to(&s2c_buf[..n], ca).await.unwrap(); }
}
}
}
});
let transport = UdpClientTransport::connect(relay_addr).await.unwrap();
let client = PhantomSession::connect_with_transport(&relay_addr.to_string(), transport, key);
client.send(b"ping".to_vec()).await.unwrap();
let echo = timeout(Duration::from_secs(10), client.recv())
.await
.unwrap()
.unwrap();
assert_eq!(echo, b"ping");
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_migration_survives_mid_exchange() {
const ROUNDS: usize = 8;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.expect("bind_udp");
let addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let session = listener.accept().await.expect("accept").session();
for _ in 0..ROUNDS {
let m = session.recv().await.expect("server recv");
session.send(m).await.expect("server echo");
}
tokio::time::sleep(Duration::from_millis(200)).await;
});
let transport = UdpClientTransport::connect(addr)
.await
.expect("udp connect");
let client = PhantomSession::connect_with_transport(&addr.to_string(), transport, key);
let m0 = b"round-0-pre-migration".to_vec();
client.send(m0.clone()).await.expect("send 0");
let e0 = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout")
.expect("recv 0");
assert_eq!(e0, m0, "pre-migration echo must be byte-exact");
client
.migrate("127.0.0.1:0".to_string())
.await
.expect("migrate");
for i in 1..ROUNDS {
let m = format!("round-{i}-post-migration-{}", "x".repeat(i * 7)).into_bytes();
client.send(m.clone()).await.expect("send post-migration");
let e = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout (the session must survive the migration)")
.expect("recv post-migration");
assert_eq!(e, m, "post-migration echo must be byte-exact (round {i})");
}
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_cid_rotates_on_the_wire_across_migration() {
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use tokio::net::UdpSocket;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.unwrap();
let server_addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let s = listener.accept().await.expect("accept").session();
let m = s.recv().await.expect("recv");
s.send(m).await.expect("echo");
tokio::time::sleep(Duration::from_millis(800)).await;
});
let onertt_cids: Arc<Mutex<HashSet<[u8; 8]>>> = Arc::new(Mutex::new(HashSet::new()));
let onertt_s2c_cids: Arc<Mutex<HashSet<[u8; 8]>>> = Arc::new(Mutex::new(HashSet::new()));
let relay = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let relay_addr = relay.local_addr().unwrap();
let upstream = UdpSocket::bind("127.0.0.1:0").await.unwrap();
upstream.connect(server_addr).await.unwrap();
let cids = onertt_cids.clone();
let s2c_cids = onertt_s2c_cids.clone();
tokio::spawn(async move {
let mut c2s = vec![0u8; 2048];
let mut s2c = vec![0u8; 2048];
let mut client_addr: Option<std::net::SocketAddr> = None;
loop {
tokio::select! {
r = relay.recv_from(&mut c2s) => {
let (n, from) = r.unwrap();
client_addr = Some(from);
if n >= 9 && (c2s[0] >> 6) == 1 {
let mut cid = [0u8; 8];
cid.copy_from_slice(&c2s[1..9]);
cids.lock().unwrap().insert(cid);
}
upstream.send(&c2s[..n]).await.unwrap();
}
r = upstream.recv(&mut s2c) => {
let n = r.unwrap();
if n >= 9 && (s2c[0] >> 6) == 1 {
let mut cid = [0u8; 8];
cid.copy_from_slice(&s2c[1..9]);
s2c_cids.lock().unwrap().insert(cid);
}
if let Some(ca) = client_addr {
relay.send_to(&s2c[..n], ca).await.unwrap();
}
}
}
}
});
let transport = UdpClientTransport::connect(relay_addr).await.unwrap();
let client = PhantomSession::connect_with_transport(&relay_addr.to_string(), transport, key);
client.send(b"r0".to_vec()).await.unwrap();
let e0 = timeout(Duration::from_secs(10), client.recv())
.await
.unwrap()
.unwrap();
assert_eq!(e0, b"r0");
let before = onertt_cids.lock().unwrap().len();
assert!(before >= 1, "at least CID_0 must be seen pre-migration");
let s2c_before = onertt_s2c_cids.lock().unwrap().len();
assert!(
s2c_before >= 1,
"at least the server's s2c CID_0 must be seen pre-migration"
);
client.migrate("127.0.0.1:0".to_string()).await.unwrap();
for i in 1..4 {
let _ = client.send(format!("r{i}").into_bytes()).await;
tokio::time::sleep(Duration::from_millis(120)).await;
}
tokio::time::sleep(Duration::from_millis(300)).await;
let after = onertt_cids.lock().unwrap().len();
assert!(
after >= 2 && after > before,
"the on-wire c2s CID must rotate across migration (before {before}, after {after} distinct OneRtt CIDs)"
);
let s2c_after = onertt_s2c_cids.lock().unwrap().len();
assert!(
s2c_after >= 2 && s2c_after > s2c_before,
"the on-wire s2c CID must ALSO rotate across a client migration (before {s2c_before}, after {s2c_after} distinct OneRtt CIDs) — EPS-02"
);
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_server_migration_rotates_both_cids_and_survives() {
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use tokio::net::UdpSocket;
const POST: usize = 4;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.unwrap();
let server_addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let s = listener.accept().await.expect("accept").session();
let m = s.recv().await.expect("recv warmup");
s.send(m).await.expect("echo warmup");
s.migrate_server("127.0.0.1:0".to_string())
.await
.expect("server migrate");
for _ in 0..POST {
let m = s.recv().await.expect("recv post");
s.send(m).await.expect("echo post");
}
tokio::time::sleep(Duration::from_millis(300)).await;
});
let s2c_cids: Arc<Mutex<HashSet<[u8; 8]>>> = Arc::new(Mutex::new(HashSet::new()));
let s2c_srcs: Arc<Mutex<HashSet<std::net::SocketAddr>>> = Arc::new(Mutex::new(HashSet::new()));
let c2s_cids: Arc<Mutex<HashSet<[u8; 8]>>> = Arc::new(Mutex::new(HashSet::new()));
let relay = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let relay_addr = relay.local_addr().unwrap();
let upstream = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let s2c_c = s2c_cids.clone();
let s2c_s = s2c_srcs.clone();
let c2s_c = c2s_cids.clone();
tokio::spawn(async move {
let mut c2s = vec![0u8; 2048];
let mut s2c = vec![0u8; 2048];
let mut client_addr: Option<std::net::SocketAddr> = None;
loop {
tokio::select! {
r = relay.recv_from(&mut c2s) => {
let (n, from) = r.unwrap();
client_addr = Some(from);
if n >= 9 && (c2s[0] >> 6) == 1 {
let mut cid = [0u8; 8];
cid.copy_from_slice(&c2s[1..9]);
c2s_c.lock().unwrap().insert(cid);
}
upstream.send_to(&c2s[..n], server_addr).await.unwrap();
}
r = upstream.recv_from(&mut s2c) => {
let (n, src) = r.unwrap();
if n >= 9 && (s2c[0] >> 6) == 1 {
let mut cid = [0u8; 8];
cid.copy_from_slice(&s2c[1..9]);
s2c_c.lock().unwrap().insert(cid);
s2c_s.lock().unwrap().insert(src);
}
if let Some(ca) = client_addr {
relay.send_to(&s2c[..n], ca).await.unwrap();
}
}
}
}
});
let transport = UdpClientTransport::connect(relay_addr).await.unwrap();
let client = PhantomSession::connect_with_transport(&relay_addr.to_string(), transport, key);
client.send(b"r0".to_vec()).await.unwrap();
let e0 = timeout(Duration::from_secs(10), client.recv())
.await
.unwrap()
.unwrap();
assert_eq!(e0, b"r0");
for i in 0..POST {
let msg = format!("p{i}").into_bytes();
client.send(msg.clone()).await.unwrap();
let echo = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout through the server migration")
.expect("recv post-migration echo");
assert_eq!(
echo, msg,
"byte-exact bidirectional flow survives server migration"
);
}
tokio::time::sleep(Duration::from_millis(200)).await;
let s2c_srcs_seen = s2c_srcs.lock().unwrap();
assert!(
s2c_srcs_seen.contains(&server_addr),
"the server's pre-migration s2c must come from the listen address"
);
assert!(
s2c_srcs_seen.len() >= 2,
"the s2c source address must change across server migration (saw {} distinct sources)",
s2c_srcs_seen.len()
);
drop(s2c_srcs_seen);
let s2c_cids_seen = s2c_cids.lock().unwrap().len();
assert!(
s2c_cids_seen >= 2,
"the on-wire s2c CID must rotate across server migration (saw {s2c_cids_seen} distinct OneRtt CIDs)"
);
let c2s_cids_seen = c2s_cids.lock().unwrap().len();
assert!(
c2s_cids_seen >= 2,
"the client's c2s CID must rotate across a server migration (EPS-02 closure; saw {c2s_cids_seen})"
);
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_window_slides_across_many_migrations() {
use std::collections::HashSet;
const HOPS: usize = 12;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.expect("bind_udp");
let addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let s = listener.accept().await.expect("accept").session();
let mut got: HashSet<Vec<u8>> = HashSet::new();
while got.len() <= HOPS {
let m = s
.recv()
.await
.expect("server recv (the inbound window must slide)");
got.insert(m.to_vec());
}
got.len()
});
let transport = UdpClientTransport::connect(addr)
.await
.expect("udp connect");
let client = PhantomSession::connect_with_transport(&addr.to_string(), transport, key);
client.send(b"msg-0".to_vec()).await.expect("send 0");
for i in 1..=HOPS {
client
.migrate("127.0.0.1:0".to_string())
.await
.expect("migrate");
client
.send(format!("msg-{i}").into_bytes())
.await
.expect("send hop");
tokio::time::sleep(Duration::from_millis(120)).await;
}
let count = timeout(Duration::from_secs(25), server)
.await
.expect("server must receive every hop's message — the inbound window slid")
.unwrap();
assert_eq!(
count,
HOPS + 1,
"all rotated CIDs routed via the slid window"
);
}
async fn spawn_cuttable_relay(
server_addr: std::net::SocketAddr,
) -> (
std::net::SocketAddr,
std::sync::Arc<std::sync::atomic::AtomicBool>,
) {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::net::UdpSocket;
let relay = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let relay_addr = relay.local_addr().unwrap();
let upstream = UdpSocket::bind("127.0.0.1:0").await.unwrap();
upstream.connect(server_addr).await.unwrap();
let forward = Arc::new(AtomicBool::new(true));
let f = forward.clone();
tokio::spawn(async move {
let mut c2s = vec![0u8; 2048];
let mut s2c = vec![0u8; 2048];
let mut client_addr: Option<std::net::SocketAddr> = None;
loop {
tokio::select! {
r = relay.recv_from(&mut c2s) => {
let (n, from) = match r { Ok(x) => x, Err(_) => continue };
client_addr = Some(from);
if f.load(Ordering::Relaxed) {
let _ = upstream.send(&c2s[..n]).await;
}
}
r = upstream.recv(&mut s2c) => {
let n = match r { Ok(x) => x, Err(_) => continue };
if f.load(Ordering::Relaxed) {
if let Some(ca) = client_addr {
let _ = relay.send_to(&s2c[..n], ca).await;
}
}
}
}
}
});
(relay_addr, forward)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_liveness_dead_path_surfaces_migrating_then_dead() {
use phantom_protocol::api::session::ConnectionState;
use phantom_protocol::transport::liveness::LivenessConfig;
use std::sync::atomic::Ordering;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.expect("bind_udp");
let server_addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let session = listener.accept().await.expect("accept").session();
while let Ok(m) = session.recv().await {
let _ = session.send(m).await;
}
});
let (relay_addr, forward) = spawn_cuttable_relay(server_addr).await;
let transport = UdpClientTransport::connect(relay_addr)
.await
.expect("connect");
let client = PhantomSession::connect_with_transport(&relay_addr.to_string(), transport, key);
client.send(b"warmup".to_vec()).await.expect("send");
let echo = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout")
.expect("recv");
assert_eq!(echo, b"warmup");
assert!(
client.set_liveness_config(LivenessConfig::for_test()).await,
"session must be established to set the liveness config"
);
forward.store(false, Ordering::Relaxed);
for i in 0..30u32 {
let _ = client.send(format!("post-{i}").into_bytes()).await;
}
let mut saw_migrating = false;
let mut saw_dead = false;
for _ in 0..400 {
match client.connection_state() {
ConnectionState::Migrating => saw_migrating = true,
ConnectionState::Dead => {
saw_dead = true;
break;
}
_ => {}
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(
saw_migrating,
"a dead path must surface ConnectionState::Migrating"
);
assert!(
saw_dead,
"no recovery before the idle-timeout must surface ConnectionState::Dead"
);
let r = timeout(Duration::from_secs(2), client.recv()).await;
assert!(
matches!(r, Ok(Err(_))),
"recv() must error on a dead session (got {r:?})"
);
server.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_keepalive_download_only_path_detects_dead_downstream() {
use phantom_protocol::api::session::ConnectionState;
use phantom_protocol::transport::liveness::LivenessConfig;
use std::sync::atomic::Ordering;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.expect("bind_udp");
let server_addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let session = listener.accept().await.expect("accept").session();
session
.send(b"download-chunk".to_vec())
.await
.expect("server send");
tokio::time::sleep(Duration::from_secs(30)).await;
});
let (relay_addr, forward) = spawn_cuttable_relay(server_addr).await;
let transport = UdpClientTransport::connect(relay_addr)
.await
.expect("connect");
let client = PhantomSession::connect_with_transport(&relay_addr.to_string(), transport, key);
let chunk = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout")
.expect("recv download chunk");
assert_eq!(chunk, b"download-chunk");
assert!(
client.set_liveness_config(LivenessConfig::for_test()).await,
"session must be established to set the liveness config"
);
for _ in 0..15 {
tokio::time::sleep(Duration::from_millis(10)).await;
assert_eq!(
client.connection_state(),
ConnectionState::Connected,
"an idle path whose keep-alives are PONGed must stay Connected"
);
}
forward.store(false, Ordering::Relaxed);
let mut saw_migrating = false;
let mut saw_dead = false;
for _ in 0..400 {
match client.connection_state() {
ConnectionState::Migrating => saw_migrating = true,
ConnectionState::Dead => {
saw_dead = true;
break;
}
_ => {}
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(
saw_migrating,
"a dead DOWNLOAD-ONLY path must surface Migrating via the idle keep-alive probe"
);
assert!(
saw_dead,
"no recovery before the idle-timeout must surface Dead on a download-only path"
);
let r = timeout(Duration::from_secs(2), client.recv()).await;
assert!(
matches!(r, Ok(Err(_))),
"recv() must error on a dead session (got {r:?})"
);
server.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_liveness_recovers_when_the_path_returns() {
use phantom_protocol::api::session::ConnectionState;
use phantom_protocol::transport::liveness::LivenessConfig;
use std::sync::atomic::Ordering;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.expect("bind_udp");
let server_addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let session = listener.accept().await.expect("accept").session();
while let Ok(m) = session.recv().await {
let _ = session.send(m).await;
}
});
let (relay_addr, forward) = spawn_cuttable_relay(server_addr).await;
let transport = UdpClientTransport::connect(relay_addr)
.await
.expect("connect");
let client = PhantomSession::connect_with_transport(&relay_addr.to_string(), transport, key);
client.send(b"warmup".to_vec()).await.expect("send");
let echo = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout")
.expect("recv");
assert_eq!(echo, b"warmup");
let cfg = LivenessConfig {
min_pto: Duration::from_millis(10),
path_down_ptos: 3,
idle_timeout: Duration::from_secs(20),
keepalive_interval: None,
};
assert!(client.set_liveness_config(cfg).await);
forward.store(false, Ordering::Relaxed);
for i in 0..30u32 {
let _ = client.send(format!("gap-{i}").into_bytes()).await;
}
let mut saw_migrating = false;
for _ in 0..300 {
if client.connection_state() == ConnectionState::Migrating {
saw_migrating = true;
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(saw_migrating, "a dead path must surface Migrating");
forward.store(true, Ordering::Relaxed);
let mut recovered = false;
for _ in 0..500 {
if client.connection_state() == ConnectionState::Connected {
recovered = true;
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(
recovered,
"the path returning must recover the session to Connected"
);
client.send(b"after-recovery".to_vec()).await.expect("send");
let mut got_final = false;
for _ in 0..60 {
let m = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout")
.expect("recv");
if m == b"after-recovery" {
got_final = true;
break;
}
}
assert!(
got_final,
"the reliable stream must resume and deliver post-recovery data byte-exact"
);
server.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_passive_rebind_recovers_without_client_migrate() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::net::UdpSocket;
const ROUNDS: usize = 8;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.expect("bind_udp");
let server_addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let session = listener.accept().await.expect("accept").session();
for _ in 0..ROUNDS {
let m = session.recv().await.expect("server recv");
session.send(m).await.expect("server echo");
}
tokio::time::sleep(Duration::from_millis(300)).await;
});
let relay = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let relay_addr = relay.local_addr().unwrap();
let up_a = UdpSocket::bind("127.0.0.1:0").await.unwrap();
up_a.connect(server_addr).await.unwrap();
let up_b = UdpSocket::bind("127.0.0.1:0").await.unwrap();
up_b.connect(server_addr).await.unwrap();
let use_b = Arc::new(AtomicBool::new(false));
let ub = use_b.clone();
tokio::spawn(async move {
let mut c2s = vec![0u8; 2048];
let mut sa = vec![0u8; 2048];
let mut sb = vec![0u8; 2048];
let mut client_addr: Option<std::net::SocketAddr> = None;
loop {
tokio::select! {
r = relay.recv_from(&mut c2s) => {
let (n, from) = match r { Ok(x) => x, Err(_) => continue };
client_addr = Some(from);
if ub.load(Ordering::Relaxed) {
let _ = up_b.send(&c2s[..n]).await;
} else {
let _ = up_a.send(&c2s[..n]).await;
}
}
r = up_a.recv(&mut sa) => {
let n = match r { Ok(x) => x, Err(_) => continue };
if !ub.load(Ordering::Relaxed) {
if let Some(ca) = client_addr { let _ = relay.send_to(&sa[..n], ca).await; }
}
}
r = up_b.recv(&mut sb) => {
let n = match r { Ok(x) => x, Err(_) => continue };
if ub.load(Ordering::Relaxed) {
if let Some(ca) = client_addr { let _ = relay.send_to(&sb[..n], ca).await; }
}
}
}
}
});
let transport = UdpClientTransport::connect(relay_addr).await.unwrap();
let client = PhantomSession::connect_with_transport(&relay_addr.to_string(), transport, key);
let m0 = b"round-0-pre-rebind".to_vec();
client.send(m0.clone()).await.expect("send 0");
let e0 = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout")
.expect("recv 0");
assert_eq!(e0, m0, "pre-rebind echo must be byte-exact");
use_b.store(true, Ordering::Relaxed);
for i in 1..ROUNDS {
let m = format!("round-{i}-post-rebind-{}", "y".repeat(i * 5)).into_bytes();
client.send(m.clone()).await.expect("send post-rebind");
let e = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout (the session must survive the passive rebind)")
.expect("recv post-rebind");
assert_eq!(e, m, "post-rebind echo must be byte-exact (round {i})");
}
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_rekey_under_loss_survives_the_catchup_gate() {
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use tokio::net::UdpSocket;
const MSGS: usize = 24;
const THRESH: u64 = 8;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.unwrap();
let server_addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let s = listener.accept().await.expect("accept").session();
assert!(
s.set_rekey_threshold(THRESH).await,
"server session established"
);
for _ in 0..MSGS {
let m = s.recv().await.expect("server recv");
s.send(m).await.expect("server echo");
}
tokio::time::sleep(Duration::from_millis(300)).await;
});
let relay = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let relay_addr = relay.local_addr().unwrap();
let upstream = UdpSocket::bind("127.0.0.1:0").await.unwrap();
upstream.connect(server_addr).await.unwrap();
let lossy = Arc::new(AtomicBool::new(false));
let lossy_relay = lossy.clone();
tokio::spawn(async move {
let mut c2s = vec![0u8; 4096];
let mut s2c = vec![0u8; 4096];
let mut client_addr: Option<std::net::SocketAddr> = None;
let c2s_n = AtomicU64::new(0);
let s2c_n = AtomicU64::new(0);
loop {
tokio::select! {
r = relay.recv_from(&mut c2s) => {
let (n, from) = match r { Ok(x) => x, Err(_) => continue };
client_addr = Some(from);
let i = c2s_n.fetch_add(1, Ordering::Relaxed);
if lossy_relay.load(Ordering::Relaxed) && i % 3 == 2 { continue; }
let _ = upstream.send(&c2s[..n]).await;
}
r = upstream.recv(&mut s2c) => {
let n = match r { Ok(x) => x, Err(_) => continue };
let i = s2c_n.fetch_add(1, Ordering::Relaxed);
if lossy_relay.load(Ordering::Relaxed) && i % 3 == 2 { continue; }
if let Some(ca) = client_addr { let _ = relay.send_to(&s2c[..n], ca).await; }
}
}
}
});
let transport = UdpClientTransport::connect(relay_addr).await.unwrap();
let client = PhantomSession::connect_with_transport(&relay_addr.to_string(), transport, key);
client.send(b"warmup".to_vec()).await.unwrap();
let echo = timeout(Duration::from_secs(10), client.recv())
.await
.unwrap()
.unwrap();
assert_eq!(echo, b"warmup");
assert!(
client.set_rekey_threshold(THRESH).await,
"client session established"
);
lossy.store(true, Ordering::Relaxed);
for i in 1..MSGS {
let msg = format!("lossy-rekey-msg-{i:04}").into_bytes();
client.send(msg.clone()).await.unwrap();
let echo = timeout(Duration::from_secs(30), client.recv())
.await
.expect("no timeout — the gate must not strand retransmits at the new epoch")
.expect("client recv");
assert_eq!(echo, msg, "byte-exact echo #{i} under loss + rekey");
}
let epoch = client.current_epoch().await.expect("established");
assert!(
epoch > 0,
"the low watermark must have driven at least one mid-session rekey (epoch={epoch})"
);
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_size_padding_delivers_byte_exact() {
use phantom_protocol::api::session::TrafficShapingConfig;
use phantom_protocol::transport::shaping::PaddingPolicy;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.unwrap();
let addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let s = listener.accept().await.expect("accept").session();
assert!(
s.set_traffic_shaping(TrafficShapingConfig {
padding: PaddingPolicy::Padme,
jitter_ms: 0,
cover_interval_ms: 0,
})
.await,
"server session established"
);
for _ in 0..7 {
let m = s.recv().await.expect("server recv");
s.send(m).await.expect("server echo");
}
tokio::time::sleep(Duration::from_millis(300)).await;
});
let transport = UdpClientTransport::connect(addr).await.unwrap();
let client = PhantomSession::connect_with_transport(&addr.to_string(), transport, key);
client.send(b"warmup".to_vec()).await.unwrap();
let echo = timeout(Duration::from_secs(10), client.recv())
.await
.unwrap()
.unwrap();
assert_eq!(echo, b"warmup");
assert!(
client
.set_traffic_shaping(TrafficShapingConfig {
padding: PaddingPolicy::Padme,
jitter_ms: 0,
cover_interval_ms: 0,
})
.await,
"client session established"
);
for len in [1usize, 2, 30, 63, 200, 700] {
let msg = vec![0xA5u8; len];
client.send(msg.clone()).await.unwrap();
let echo = timeout(Duration::from_secs(15), client.recv())
.await
.expect("no timeout")
.expect("client recv");
assert_eq!(echo, msg, "byte-exact echo of a {len}-byte padded message");
}
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_timing_jitter_delivers_byte_exact() {
use phantom_protocol::api::session::TrafficShapingConfig;
use phantom_protocol::transport::shaping::PaddingPolicy;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.unwrap();
let addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let s = listener.accept().await.expect("accept").session();
assert!(
s.set_traffic_shaping(TrafficShapingConfig {
padding: PaddingPolicy::Padme,
jitter_ms: 15,
cover_interval_ms: 0,
})
.await
);
for _ in 0..5 {
let m = s.recv().await.expect("server recv");
s.send(m).await.expect("server echo");
}
tokio::time::sleep(Duration::from_millis(200)).await;
});
let transport = UdpClientTransport::connect(addr).await.unwrap();
let client = PhantomSession::connect_with_transport(&addr.to_string(), transport, key);
client.send(b"warmup".to_vec()).await.unwrap();
let echo = timeout(Duration::from_secs(10), client.recv())
.await
.unwrap()
.unwrap();
assert_eq!(echo, b"warmup");
assert!(
client
.set_traffic_shaping(TrafficShapingConfig {
padding: PaddingPolicy::None,
jitter_ms: 15,
cover_interval_ms: 0,
})
.await
);
for i in 0..4 {
let msg = format!("jitter-msg-{i}").into_bytes();
client.send(msg.clone()).await.unwrap();
let echo = timeout(Duration::from_secs(15), client.recv())
.await
.expect("no timeout — jitter only delays, never drops")
.expect("client recv");
assert_eq!(echo, msg, "byte-exact echo #{i} with timing jitter on");
}
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_cover_traffic_fills_idle_and_is_dropped() {
use phantom_protocol::api::session::TrafficShapingConfig;
use phantom_protocol::transport::shaping::PaddingPolicy;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::net::UdpSocket;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.unwrap();
let server_addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let s = listener.accept().await.expect("accept").session();
assert!(
s.set_traffic_shaping(TrafficShapingConfig {
padding: PaddingPolicy::None,
jitter_ms: 0,
cover_interval_ms: 40,
})
.await
);
for expected in [b"warmup".to_vec(), b"after-idle".to_vec()] {
let m = s.recv().await.expect("server recv");
assert_eq!(
m, expected,
"server must only recv real messages, never cover"
);
s.send(m).await.expect("server echo");
}
tokio::time::sleep(Duration::from_millis(200)).await;
});
let relay = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let relay_addr = relay.local_addr().unwrap();
let upstream = UdpSocket::bind("127.0.0.1:0").await.unwrap();
upstream.connect(server_addr).await.unwrap();
let recording = Arc::new(AtomicBool::new(false));
let c2s_count = Arc::new(AtomicUsize::new(0));
let rec = recording.clone();
let cnt = c2s_count.clone();
tokio::spawn(async move {
let mut c2s = vec![0u8; 4096];
let mut s2c = vec![0u8; 4096];
let mut client_addr: Option<std::net::SocketAddr> = None;
loop {
tokio::select! {
r = relay.recv_from(&mut c2s) => {
let (n, from) = match r { Ok(x) => x, Err(_) => continue };
client_addr = Some(from);
if rec.load(Ordering::Relaxed) { cnt.fetch_add(1, Ordering::Relaxed); }
let _ = upstream.send(&c2s[..n]).await;
}
r = upstream.recv(&mut s2c) => {
let n = match r { Ok(x) => x, Err(_) => continue };
if let Some(ca) = client_addr { let _ = relay.send_to(&s2c[..n], ca).await; }
}
}
}
});
let transport = UdpClientTransport::connect(relay_addr).await.unwrap();
let client = PhantomSession::connect_with_transport(&relay_addr.to_string(), transport, key);
client.send(b"warmup".to_vec()).await.unwrap();
let echo = timeout(Duration::from_secs(10), client.recv())
.await
.unwrap()
.unwrap();
assert_eq!(echo, b"warmup");
assert!(
client
.set_traffic_shaping(TrafficShapingConfig {
padding: PaddingPolicy::None,
jitter_ms: 0,
cover_interval_ms: 40,
})
.await
);
recording.store(true, Ordering::Relaxed);
tokio::time::sleep(Duration::from_millis(300)).await;
recording.store(false, Ordering::Relaxed);
let covered = c2s_count.load(Ordering::Relaxed);
assert!(
covered > 0,
"cover traffic must fill the idle window with client->server packets (got {covered})"
);
client.send(b"after-idle".to_vec()).await.unwrap();
let echo = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout")
.expect("client recv");
assert_eq!(
echo, b"after-idle",
"recv must return the real message, never cover"
);
server.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore]
async fn udp_integration_shaping_set_before_establishment_applies() {
use phantom_protocol::api::session::TrafficShapingConfig;
use phantom_protocol::transport::shaping::PaddingPolicy;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.unwrap();
let addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let key = HybridVerifyingKey::from_bytes(&listener.verifying_key_bytes()).unwrap();
let server = tokio::spawn(async move {
let s = listener.accept().await.expect("accept").session();
let m = s.recv().await.expect("server recv");
s.send(m).await.expect("server echo");
tokio::time::sleep(Duration::from_millis(200)).await;
});
let transport = UdpClientTransport::connect(addr).await.unwrap();
let client = PhantomSession::connect_with_transport(&addr.to_string(), transport, key);
let cfg = TrafficShapingConfig {
padding: PaddingPolicy::Padme,
jitter_ms: 7,
cover_interval_ms: 250,
};
assert!(
client.set_traffic_shaping(cfg).await,
"shaping must be accepted while still connecting (stored pending)"
);
client.send(b"hello".to_vec()).await.unwrap();
let echo = timeout(Duration::from_secs(10), client.recv())
.await
.expect("no timeout")
.expect("client recv");
assert_eq!(echo, b"hello");
assert_eq!(
client.traffic_shaping().await,
Some(cfg),
"pre-establishment shaping config must be applied to the installed session"
);
server.await.unwrap();
}