use std::io;
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6};
use std::time::Duration;
use raknet_rust::client::{RaknetClient, RaknetClientEvent};
use raknet_rust::low_level::transport::{ShardedRuntimeConfig, TransportConfig};
use raknet_rust::server::{PeerId, RaknetServer, RaknetServerEvent};
use tokio::time::timeout;
fn allocate_ipv4_loopback_bind_addr() -> SocketAddr {
let socket = std::net::UdpSocket::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))
.expect("ephemeral IPv4 loopback bind must succeed");
socket
.local_addr()
.expect("ephemeral IPv4 loopback local addr must be available")
}
fn allocate_ipv6_loopback_bind_addr() -> io::Result<SocketAddr> {
let socket = std::net::UdpSocket::bind(SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::LOCALHOST,
0,
0,
0,
)))?;
socket.local_addr()
}
fn ipv6_loopback_available() -> bool {
allocate_ipv6_loopback_bind_addr().is_ok()
}
async fn wait_for_client_connected(client: &mut RaknetClient) -> io::Result<()> {
loop {
let event = timeout(Duration::from_secs(5), client.next_event())
.await
.map_err(|_| {
io::Error::new(
io::ErrorKind::TimedOut,
"timed out waiting for client event",
)
})?
.ok_or_else(|| {
io::Error::new(io::ErrorKind::UnexpectedEof, "client event stream ended")
})?;
match event {
RaknetClientEvent::Connected { .. } => return Ok(()),
RaknetClientEvent::Disconnected { reason } => {
return Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
format!("client disconnected before connect completed: {reason:?}"),
));
}
RaknetClientEvent::Packet { .. }
| RaknetClientEvent::ReceiptAcked { .. }
| RaknetClientEvent::DecodeError { .. } => {}
}
}
}
async fn wait_for_server_peer_connected(
server: &mut RaknetServer,
) -> io::Result<(PeerId, SocketAddr)> {
loop {
let event = timeout(Duration::from_secs(5), server.next_event())
.await
.map_err(|_| {
io::Error::new(
io::ErrorKind::TimedOut,
"timed out waiting for server event",
)
})?
.ok_or_else(|| {
io::Error::new(io::ErrorKind::UnexpectedEof, "server event stream ended")
})?;
match event {
RaknetServerEvent::PeerConnected { peer_id, addr, .. } => return Ok((peer_id, addr)),
RaknetServerEvent::WorkerError { message, .. } => {
return Err(io::Error::other(format!(
"worker error while waiting for peer connect: {message}"
)));
}
RaknetServerEvent::PeerDisconnected { .. }
| RaknetServerEvent::Packet { .. }
| RaknetServerEvent::OfflinePacket { .. }
| RaknetServerEvent::ReceiptAcked { .. }
| RaknetServerEvent::PeerRateLimited { .. }
| RaknetServerEvent::SessionLimitReached { .. }
| RaknetServerEvent::ProxyDropped { .. }
| RaknetServerEvent::DecodeError { .. }
| RaknetServerEvent::WorkerStopped { .. }
| RaknetServerEvent::Metrics { .. } => {}
}
}
}
#[tokio::test(flavor = "current_thread")]
async fn ipv4_only_bind_accepts_ipv4_client() -> io::Result<()> {
let bind_addr = allocate_ipv4_loopback_bind_addr();
let transport_config = TransportConfig {
bind_addr,
..TransportConfig::default()
};
let runtime_config = ShardedRuntimeConfig {
shard_count: 1,
..ShardedRuntimeConfig::default()
};
let mut server = RaknetServer::start_with_configs(transport_config, runtime_config).await?;
let mut client = RaknetClient::connect(bind_addr).await?;
wait_for_client_connected(&mut client).await?;
let _peer = wait_for_server_peer_connected(&mut server).await?;
let _ = client.disconnect(None).await;
server.shutdown().await?;
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn ipv6_only_bind_accepts_ipv6_client() -> io::Result<()> {
if !ipv6_loopback_available() {
eprintln!("ipv6 loopback unavailable; skipping ipv6-only integration test");
return Ok(());
}
let bind_addr = allocate_ipv6_loopback_bind_addr()?;
let transport_config = TransportConfig {
bind_addr,
ipv6_only: true,
..TransportConfig::default()
};
let runtime_config = ShardedRuntimeConfig {
shard_count: 1,
..ShardedRuntimeConfig::default()
};
let mut server = RaknetServer::start_with_configs(transport_config, runtime_config).await?;
let mut client = RaknetClient::connect(bind_addr).await?;
wait_for_client_connected(&mut client).await?;
let (_peer, peer_addr) = wait_for_server_peer_connected(&mut server).await?;
assert!(
peer_addr.is_ipv6(),
"ipv6-only bind should accept ipv6 client addr, got {peer_addr}"
);
let _ = client.disconnect(None).await;
server.shutdown().await?;
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn split_dual_stack_bind_accepts_ipv4_and_ipv6_clients() -> io::Result<()> {
if !ipv6_loopback_available() {
eprintln!("ipv6 loopback unavailable; skipping split dual-stack integration test");
return Ok(());
}
let port = allocate_ipv4_loopback_bind_addr().port();
let bind_addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, port));
let transport_config = TransportConfig {
bind_addr,
split_ipv4_ipv6_bind: true,
ipv6_only: true,
..TransportConfig::default()
};
let runtime_config = ShardedRuntimeConfig {
shard_count: 2,
..ShardedRuntimeConfig::default()
};
let mut server = RaknetServer::start_with_configs(transport_config, runtime_config).await?;
let mut client_v4 =
RaknetClient::connect(SocketAddr::from((Ipv4Addr::LOCALHOST, port))).await?;
wait_for_client_connected(&mut client_v4).await?;
let (_peer_v4, addr_v4) = wait_for_server_peer_connected(&mut server).await?;
let mut client_v6 = RaknetClient::connect(SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::LOCALHOST,
port,
0,
0,
)))
.await?;
wait_for_client_connected(&mut client_v6).await?;
let (_peer_v6, addr_v6) = wait_for_server_peer_connected(&mut server).await?;
assert!(
addr_v4 != addr_v6,
"split dual-stack should surface distinct remote addresses"
);
assert!(
addr_v4.is_ipv4() || addr_v6.is_ipv4(),
"split dual-stack should accept an IPv4 client; addresses=({addr_v4}, {addr_v6})"
);
assert!(
addr_v4.is_ipv6() || addr_v6.is_ipv6(),
"split dual-stack should accept an IPv6 client; addresses=({addr_v4}, {addr_v6})"
);
let _ = client_v4.disconnect(None).await;
let _ = client_v6.disconnect(None).await;
server.shutdown().await?;
Ok(())
}