use std::{
io,
net::SocketAddr,
sync::{Arc, atomic::Ordering},
};
use infrarust_config::models::logging::LogType;
use infrarust_protocol::{
minecraft::java::{
handshake::ServerBoundHandshake,
legacy::{
handshake::parse_legacy_handshake,
kick::{build_legacy_kick_beta, build_legacy_kick_v1_4},
ping::{LegacyPingVariant, parse_legacy_ping},
},
status::serverbound_request::SERVERBOUND_REQUEST_ID,
},
types::VarInt,
version::Version,
};
use tracing::{debug, info, warn};
use uuid::Uuid;
use crate::{
Connection,
network::packet::{Packet, PacketCodec},
server::{
ServerRequest,
backend::Server,
gateway::Gateway,
motd::{MotdState, generate_legacy_motd_for_state, generate_legacy_motd_from_packet},
},
};
pub async fn handle_legacy_ping(
conn: &mut Connection,
gateway: &Arc<Gateway>,
session_id: Uuid,
client_addr: SocketAddr,
) -> io::Result<()> {
debug!(
log_type = LogType::PacketProcessing.as_str(),
"Detected legacy ping from {}", client_addr
);
let raw_data = read_legacy_ping_data(conn).await?;
let variant = parse_legacy_ping(&raw_data).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to parse legacy ping: {}", e),
)
})?;
debug!(
log_type = LogType::PacketProcessing.as_str(),
"Legacy ping variant: {:?}, hostname: {:?}",
match &variant {
LegacyPingVariant::Beta => "Beta",
LegacyPingVariant::V1_4 => "V1_4",
LegacyPingVariant::V1_6 { .. } => "V1_6",
},
variant.hostname()
);
let server_config = match &variant {
LegacyPingVariant::V1_6 { hostname, .. } => gateway.find_server(hostname).await,
LegacyPingVariant::Beta | LegacyPingVariant::V1_4 => {
let response_bytes = generate_legacy_connect_prompt(&variant);
conn.write_raw(&response_bytes).await?;
conn.flush().await?;
let _ = conn.close().await;
return Ok(());
}
};
let response_bytes = match server_config {
Some(config) => {
match tokio::time::timeout(
std::time::Duration::from_secs(5),
forward_legacy_ping_to_backend(&raw_data, &config, session_id),
)
.await
{
Ok(Ok(bytes)) => bytes,
Ok(Err(e)) => {
debug!(
log_type = LogType::PacketProcessing.as_str(),
"Legacy ping passthrough failed: {}, trying modern fetch", e
);
match fetch_and_convert_to_legacy(&variant, &config, session_id).await {
Ok(bytes) => bytes,
Err(e2) => {
debug!(
log_type = LogType::PacketProcessing.as_str(),
"Modern fetch also failed: {}, using fallback", e2
);
generate_legacy_fallback(&variant, &config)
}
}
}
Err(_) => {
debug!(
log_type = LogType::PacketProcessing.as_str(),
"Legacy ping passthrough timed out, trying modern fetch"
);
match fetch_and_convert_to_legacy(&variant, &config, session_id).await {
Ok(bytes) => bytes,
Err(e) => {
debug!(
log_type = LogType::PacketProcessing.as_str(),
"Modern fetch also failed: {}, using fallback", e
);
generate_legacy_fallback(&variant, &config)
}
}
}
}
}
None => {
debug!(
log_type = LogType::PacketProcessing.as_str(),
"No server config found for legacy ping, sending fallback"
);
generate_legacy_no_server(&variant)
}
};
conn.write_raw(&response_bytes).await?;
conn.flush().await?;
let _ = conn.close().await;
debug!(
log_type = LogType::PacketProcessing.as_str(),
"Legacy ping response sent to {}", client_addr
);
Ok(())
}
pub async fn handle_legacy_login(
mut conn: Connection,
gateway: &Arc<Gateway>,
session_id: Uuid,
client_addr: SocketAddr,
) -> io::Result<()> {
debug!(
log_type = LogType::PacketProcessing.as_str(),
"Detected legacy login handshake from {}", client_addr
);
let raw_data = read_legacy_handshake_data(&mut conn).await?;
let handshake = parse_legacy_handshake(&raw_data).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to parse legacy handshake: {}", e),
)
})?;
debug!(
log_type = LogType::PacketProcessing.as_str(),
"Legacy handshake: proto={}, user={}, host={}, port={}",
handshake.protocol_version,
handshake.username,
handshake.hostname,
handshake.port
);
let server_config = gateway.find_server(&handshake.hostname).await;
let server_config = match server_config {
Some(config) => config,
None => {
warn!(
log_type = LogType::PacketProcessing.as_str(),
"No server found for legacy login from {} (hostname: {})",
client_addr,
handshake.hostname
);
let _ = conn.close().await;
return Ok(());
}
};
let server = Server::new(server_config.clone()).map_err(|e| {
io::Error::new(
io::ErrorKind::ConnectionRefused,
format!("Failed to create server: {}", e),
)
})?;
let mut backend = match server.dial(session_id).await {
Ok(conn) => conn,
Err(e) => {
warn!(
log_type = LogType::PacketProcessing.as_str(),
"Failed to connect to backend for legacy login: {}", e
);
let _ = conn.close().await;
return Ok(());
}
};
backend.write_raw(&raw_data).await.map_err(|e| {
io::Error::new(
io::ErrorKind::BrokenPipe,
format!("Failed to replay handshake to backend: {}", e),
)
})?;
backend.flush().await.map_err(|e| {
io::Error::new(
io::ErrorKind::BrokenPipe,
format!("Failed to flush handshake to backend: {}", e),
)
})?;
debug!(
log_type = LogType::PacketProcessing.as_str(),
"Legacy handshake replayed to backend, starting bidirectional forwarding"
);
let configured_mode = server_config.proxy_mode.unwrap_or_default();
warn!(
log_type = LogType::ProxyMode.as_str(),
"Legacy connection from {}: using zerocopy mode (configured: {:?})",
client_addr,
configured_mode
);
let shutdown = gateway
.shared
.actor_supervisor()
.register_legacy_actor_pair(
&server_config.config_id,
handshake.username.clone(),
handshake.hostname.clone(),
client_addr,
session_id,
)
.await;
info!(
"Player '{}' connected to '{}' ({}) [legacy]",
&handshake.username, &handshake.hostname, &server_config.config_id
);
let client_stream = conn.into_tcp_stream()?;
let backend_stream = backend.into_tcp_stream()?;
let (mut client_read, mut client_write) = client_stream.into_split();
let (mut server_read, mut server_write) = backend_stream.into_split();
let client_to_server = tokio::io::copy(&mut client_read, &mut server_write);
let server_to_client = tokio::io::copy(&mut server_read, &mut client_write);
let shutdown_watcher = {
let shutdown = shutdown.clone();
async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
if shutdown.load(Ordering::Acquire) {
break;
}
}
}
};
tokio::select! {
result = client_to_server => {
if let Err(e) = result {
debug!(log_type = LogType::TcpConnection.as_str(), "Legacy client->server copy ended: {}", e);
}
}
result = server_to_client => {
if let Err(e) = result {
debug!(log_type = LogType::TcpConnection.as_str(), "Legacy server->client copy ended: {}", e);
}
}
_ = shutdown_watcher => {
debug!(log_type = LogType::TcpConnection.as_str(), "Legacy connection shutdown requested via kick");
}
}
shutdown.store(true, Ordering::SeqCst);
Ok(())
}
async fn read_legacy_ping_data(conn: &mut Connection) -> io::Result<Vec<u8>> {
let mut data = Vec::with_capacity(256);
let mut fe_byte = [0u8; 1];
conn.read_exact_raw(&mut fe_byte).await?;
data.push(fe_byte[0]);
let mut next = [0u8; 1];
if let Ok(Ok(())) = tokio::time::timeout(
std::time::Duration::from_millis(100),
conn.read_exact_raw(&mut next),
)
.await
{
data.push(next[0]);
if next[0] == 0x01
&& let Ok(Ok(more)) = tokio::time::timeout(
std::time::Duration::from_millis(100),
read_remaining_v1_6_data(conn),
)
.await
{
data.extend_from_slice(&more);
}
}
Ok(data)
}
async fn read_remaining_v1_6_data(conn: &mut Connection) -> io::Result<Vec<u8>> {
let mut data = Vec::new();
let mut byte = [0u8; 1];
conn.read_exact_raw(&mut byte).await?;
data.push(byte[0]);
if byte[0] != 0xFA {
return Ok(data); }
let mut len_bytes = [0u8; 2];
conn.read_exact_raw(&mut len_bytes).await?;
data.extend_from_slice(&len_bytes);
let str_len = u16::from_be_bytes(len_bytes) as usize;
let mut str_data = vec![0u8; str_len * 2];
conn.read_exact_raw(&mut str_data).await?;
data.extend_from_slice(&str_data);
let mut data_len_bytes = [0u8; 2];
conn.read_exact_raw(&mut data_len_bytes).await?;
data.extend_from_slice(&data_len_bytes);
let data_len = u16::from_be_bytes(data_len_bytes) as usize;
let mut remaining = vec![0u8; data_len];
conn.read_exact_raw(&mut remaining).await?;
data.extend_from_slice(&remaining);
Ok(data)
}
async fn read_legacy_handshake_data(conn: &mut Connection) -> io::Result<Vec<u8>> {
let mut data = Vec::with_capacity(256);
let mut packet_id = [0u8; 1];
conn.read_exact_raw(&mut packet_id).await?;
data.push(packet_id[0]);
let mut format_byte = [0u8; 1];
conn.read_exact_raw(&mut format_byte).await?;
data.push(format_byte[0]);
if format_byte[0] == 0x00 {
let mut low_byte = [0u8; 1];
conn.read_exact_raw(&mut low_byte).await?;
data.push(low_byte[0]);
let str_len = u16::from_be_bytes([0x00, low_byte[0]]) as usize;
let mut str_data = vec![0u8; str_len * 2];
conn.read_exact_raw(&mut str_data).await?;
data.extend_from_slice(&str_data);
} else {
let username_bytes = read_legacy_string_bytes(conn).await?;
data.extend_from_slice(&username_bytes);
let hostname_bytes = read_legacy_string_bytes(conn).await?;
data.extend_from_slice(&hostname_bytes);
let mut port = [0u8; 4];
conn.read_exact_raw(&mut port).await?;
data.extend_from_slice(&port);
}
Ok(data)
}
async fn read_legacy_string_bytes(conn: &mut Connection) -> io::Result<Vec<u8>> {
let mut len_bytes = [0u8; 2];
conn.read_exact_raw(&mut len_bytes).await?;
let char_count = u16::from_be_bytes(len_bytes) as usize;
let mut str_data = vec![0u8; char_count * 2];
conn.read_exact_raw(&mut str_data).await?;
let mut result = Vec::with_capacity(2 + str_data.len());
result.extend_from_slice(&len_bytes);
result.extend_from_slice(&str_data);
Ok(result)
}
async fn fetch_and_convert_to_legacy(
variant: &LegacyPingVariant,
server_config: &Arc<infrarust_config::ServerConfig>,
session_id: Uuid,
) -> io::Result<Vec<u8>> {
let server = Server::new(server_config.clone())
.map_err(|e| io::Error::other(format!("Failed to create server: {}", e)))?;
let hostname = variant.hostname().unwrap_or_else(|| {
server_config
.domains
.first()
.map(|s| s.as_str())
.unwrap_or("localhost")
});
let request = build_synthetic_status_request(hostname, session_id)?;
let status_packet = server
.fetch_status_directly(&request)
.await
.map_err(|e| io::Error::other(format!("Backend status fetch failed: {}", e)))?;
generate_legacy_motd_from_packet(&status_packet, variant)
}
pub(crate) fn build_synthetic_status_request(
hostname: &str,
session_id: Uuid,
) -> io::Result<ServerRequest> {
let handshake = ServerBoundHandshake {
protocol_version: VarInt(47), server_address: infrarust_protocol::types::ProtocolString(hostname.to_string()),
server_port: infrarust_protocol::types::UnsignedShort(25565),
next_state: VarInt(1), };
let mut handshake_packet = Packet::new(0x00);
handshake_packet
.encode(&handshake)
.map_err(|e| io::Error::other(format!("Failed to encode synthetic handshake: {}", e)))?;
let status_request_packet = Packet::new(SERVERBOUND_REQUEST_ID);
Ok(ServerRequest {
client_addr: SocketAddr::from(([127, 0, 0, 1], 0)),
original_client_addr: None,
domain: hostname.into(),
is_login: false,
protocol_version: Version::from(47),
read_packets: Arc::new([handshake_packet, status_request_packet]),
session_id,
})
}
async fn forward_legacy_ping_to_backend(
raw_ping_data: &[u8],
server_config: &Arc<infrarust_config::ServerConfig>,
session_id: Uuid,
) -> io::Result<Vec<u8>> {
let server = Server::new(server_config.clone())
.map_err(|e| io::Error::other(format!("Failed to create server: {}", e)))?;
let mut backend = server
.dial(session_id)
.await
.map_err(|e| io::Error::other(format!("Failed to connect to backend: {}", e)))?;
backend
.write_raw(raw_ping_data)
.await
.map_err(|e| io::Error::other(format!("Failed to send legacy ping to backend: {}", e)))?;
backend
.flush()
.await
.map_err(|e| io::Error::other(format!("Failed to flush legacy ping to backend: {}", e)))?;
let response = read_legacy_kick_response(&mut backend).await?;
let _ = backend.close().await;
Ok(response)
}
pub(crate) async fn read_legacy_kick_response(
conn: &mut crate::network::connection::ServerConnection,
) -> io::Result<Vec<u8>> {
let mut packet_id = [0u8; 1];
conn.read_exact_raw(&mut packet_id).await?;
if packet_id[0] != 0xFF {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Expected legacy kick packet 0xFF, got 0x{:02X}",
packet_id[0]
),
));
}
let mut len_bytes = [0u8; 2];
conn.read_exact_raw(&mut len_bytes).await?;
let str_len = u16::from_be_bytes(len_bytes) as usize;
if str_len > 32767 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Legacy kick string length too large: {}", str_len),
));
}
let byte_len = str_len * 2;
let mut payload = vec![0u8; byte_len];
conn.read_exact_raw(&mut payload).await?;
let mut result = Vec::with_capacity(1 + 2 + byte_len);
result.push(0xFF);
result.extend_from_slice(&len_bytes);
result.extend_from_slice(&payload);
Ok(result)
}
fn generate_legacy_fallback(
variant: &LegacyPingVariant,
config: &infrarust_config::ServerConfig,
) -> Vec<u8> {
let motd_config = config.motds.unreachable.as_ref();
match generate_legacy_motd_for_state(&MotdState::Unreachable, motd_config, variant) {
Ok(bytes) => bytes,
Err(_) => {
if variant.uses_v1_4_response_format() {
build_legacy_kick_v1_4(0, "Infrarust", "Server unreachable", 0, 0)
} else {
build_legacy_kick_beta("Server unreachable", 0, 0)
}
}
}
}
fn generate_legacy_connect_prompt(variant: &LegacyPingVariant) -> Vec<u8> {
if variant.uses_v1_4_response_format() {
build_legacy_kick_v1_4(0, "Infrarust", "Direct Connect to join the server.", 0, 0)
} else {
build_legacy_kick_beta("Connect to join the server.", 0, 0)
}
}
fn generate_legacy_no_server(variant: &LegacyPingVariant) -> Vec<u8> {
if variant.uses_v1_4_response_format() {
build_legacy_kick_v1_4(0, "Infrarust", "Unknown server", 0, 0)
} else {
build_legacy_kick_beta("Unknown server", 0, 0)
}
}