deepslate 0.3.1

A high-performance Minecraft server proxy written in Rust.
Documentation
//! Backend (proxy -> server) connection handling.
//!
//! Handles connecting to a backend Minecraft server and performing the Velocity
//! modern forwarding login handshake. The resulting connection is in CONFIG state,
//! ready for config packet relay or PLAY transition.

use std::time::Instant;

use deepslate_protocol::codec;
use deepslate_protocol::packet::Packet;
use deepslate_protocol::packet::handshake::{HandshakeIntent, HandshakePacket};
use deepslate_protocol::packet::login::{
    LoginAcknowledgedPacket, LoginPluginRequestPacket, LoginPluginResponsePacket, LoginStartPacket,
    SetCompressionPacket,
};
use deepslate_protocol::types::GameProfile;
use tokio::net::TcpStream;
use tracing::{debug, warn};

use crate::config::Config;
use crate::connection::ConnectionError;
use crate::connection::MinecraftConnection;
use crate::connection::forwarding::{self, VELOCITY_FORWARDING_CHANNEL};
use crate::event::PlayerInfo;
use crate::metrics;

/// Connect to a backend server, perform the login handshake with Velocity
/// modern forwarding, and return the connection in CONFIG state.
///
/// This does NOT start relaying packets — the caller is responsible for
/// driving the CONFIG and PLAY phases.
///
/// # Errors
///
/// Returns [`ConnectionError`] if the connection or login handshake fails.
#[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
#[expect(
    clippy::large_futures,
    reason = "MinecraftConnection carries large framing buffers through login handshakes"
)]
pub async fn connect_and_login(
    addr: &str,
    profile: &GameProfile,
    player_info: &PlayerInfo,
    protocol_version: i32,
    config: &Config,
) -> Result<MinecraftConnection, ConnectionError> {
    let connect_start = Instant::now();
    let backend_stream = tokio::time::timeout(config.connect_timeout, TcpStream::connect(addr))
        .await
        .map_err(|_| {
            metrics::counter!("backend_connect_failures_total", "server" => addr.to_string());
            ConnectionError::Timeout
        })?
        .map_err(|e| {
            metrics::counter!("backend_connect_failures_total", "server" => addr.to_string());
            ConnectionError::from(e)
        })?;
    metrics::histogram!("backend_connect_duration_seconds", connect_start.elapsed().as_secs_f64(), "server" => addr.to_string());
    backend_stream.set_nodelay(true)?;
    let mut conn = MinecraftConnection::new(
        backend_stream,
        libdeflater::CompressionLvl::new(config.compression_level)
            .expect("validated in Config::validate"),
        config.read_timeout,
    );

    // Send Handshake (intent=LOGIN)
    conn.write_packet(&HandshakePacket {
        protocol_version,
        server_address: addr.to_string(),
        server_port: 0,
        next_state: HandshakeIntent::Login,
    })
    .await?;

    // Send LoginStart
    conn.write_packet(&LoginStartPacket {
        username: profile.name.clone(),
        uuid: profile.id,
    })
    .await?;

    // Handle backend login sequence
    let mut forwarding_done = false;

    loop {
        let Some(frame) = conn.read_frame_timeout().await? else {
            return Err(ConnectionError::BackendFailed {
                reason: "backend closed during login".to_string(),
            });
        };

        let mut cursor = &frame[..];
        let packet_id = codec::read_packet_id(&mut cursor)?;

        match packet_id {
            // LoginPluginRequest (0x04) — Velocity forwarding
            LoginPluginRequestPacket::PACKET_ID => {
                let plugin_msg = LoginPluginRequestPacket::decode(&mut cursor)?;

                if plugin_msg.channel == VELOCITY_FORWARDING_CHANNEL {
                    let requested_version = if plugin_msg.data.len() == 1 {
                        i32::from(plugin_msg.data[0])
                    } else {
                        forwarding::version::MODERN_DEFAULT
                    };

                    let forwarding_data = forwarding::create_forwarding_data(
                        &config.forwarding_secret,
                        &player_info.remote_addr,
                        profile,
                        requested_version,
                    );

                    conn.write_packet(&LoginPluginResponsePacket {
                        message_id: plugin_msg.message_id,
                        successful: true,
                        data: forwarding_data.into(),
                    })
                    .await?;

                    forwarding_done = true;
                    debug!("sent Velocity forwarding data to backend");
                } else {
                    conn.write_packet(&LoginPluginResponsePacket {
                        message_id: plugin_msg.message_id,
                        successful: false,
                        data: bytes::Bytes::new(),
                    })
                    .await?;
                }
            }

            // SetCompression (0x03)
            SetCompressionPacket::PACKET_ID => {
                let set_comp = SetCompressionPacket::decode(&mut cursor)?;
                debug!(threshold = set_comp.threshold, "backend set compression");
                conn.enable_compression(set_comp.threshold);
            }

            // LoginSuccess (0x02)
            0x02 => {
                if !forwarding_done {
                    return Err(ConnectionError::BackendFailed {
                        reason: "backend sent LoginSuccess without Velocity forwarding — \
                                 is the backend configured for velocity modern forwarding?"
                            .to_string(),
                    });
                }

                debug!("backend login success, sending LoginAcknowledged");
                conn.write_packet(&LoginAcknowledgedPacket).await?;
                // Connection is now in CONFIG state
                return Ok(conn);
            }

            // Disconnect (0x00)
            0x00 => {
                let reason = deepslate_protocol::types::read_string(&mut cursor)
                    .unwrap_or_else(|_| "unknown".to_string());
                warn!(reason, "backend disconnected during login");
                return Err(ConnectionError::BackendFailed {
                    reason: format!("backend rejected login: {reason}"),
                });
            }

            // EncryptionRequest — backend must be in offline mode
            0x01 => {
                return Err(ConnectionError::BackendFailed {
                    reason: "backend is in online-mode — it must be in offline-mode \
                             for proxy forwarding"
                        .to_string(),
                });
            }

            other => {
                warn!(packet_id = other, "unexpected packet during backend login");
            }
        }
    }
}