rust-mc-status 3.0.0

High-performance asynchronous Rust library for querying Minecraft server status (Java & Bedrock)
Documentation
//! Bedrock Edition ping protocol (RakNet Unconnected Ping/Pong).
//!
//! Uses the RakNet offline message protocol over UDP:
//!
//! ```text
//! Client → Server: Unconnected Ping (0x01)
//!   - 8-byte timestamp
//!   - Magic bytes (offline message ID)
//!   - 8-byte client GUID
//!
//! Server → Client: Unconnected Pong (0x1C)
//!   - 8-byte timestamp echo
//!   - 8-byte server GUID
//!   - Magic bytes
//!   - MOTD string (semicolon-delimited, see BedrockStatus)
//! ```
//!
//! The MOTD string is parsed by [`BedrockStatus::parse`](crate::BedrockStatus).
//!
//! # UDP and proxies
//!
//! UDP cannot be proxied through standard SOCKS5 or HTTP CONNECT.
//! A proxy configured without [`UdpSupport::Yes`](crate::proxy::UdpSupport)
//! causes an immediate [`ProxyError::UdpUnsupported`](crate::error::ProxyError)
//! before any network connection is made.
//!
//! # Unit struct design
//!
//! [`BedrockProtocol`] is a zero-size unit struct — `Copy`, `Clone`, and `Default`.

use std::time::{Duration, SystemTime};

use tokio::net::UdpSocket;
use tokio::time::timeout;

use crate::error::McError;
use crate::models::{BedrockStatus, PingResult, ServerData};
use crate::proxy::ProxyConfig;
#[cfg(feature = "proxy")]
use crate::proxy::UdpSupport;
use super::{PingProtocol, ResolvedTarget};
use crate::core::time::{start_timer, elapsed_ms};

const READ_BUFFER_SIZE:          usize = 4096;
const BEDROCK_PING_PACKET_SIZE:  usize = 35;
const BEDROCK_MIN_RESPONSE_SIZE: usize = 35;

// ─── Protocol impl ────────────────────────────────────────────────────────────

/// Bedrock Edition ping over UDP (RakNet Unconnected Ping/Pong).
///
/// Unit struct — zero-size, `Copy`, no heap allocation.
#[derive(Debug, Clone, Copy, Default)]
pub struct BedrockProtocol;

impl PingProtocol for BedrockProtocol {
    fn name(&self) -> &'static str { "bedrock" }

    async fn ping(
        &self,
        target:  &ResolvedTarget,
        tout:    Duration,
        proxy:   Option<&ProxyConfig>,
    ) -> Result<PingResult, McError> {
        // Reject proxy configs that don't advertise UDP support.
        #[cfg(feature = "proxy")]
        if let Some(cfg) = proxy
            && cfg.udp_support() != UdpSupport::Yes {
                return Err(McError::proxy_udp_unsupported(cfg.addr().to_string()));
            }
        let _ = proxy; // silence unused warning when feature is disabled

        let start  = start_timer();
        let socket = UdpSocket::bind("0.0.0.0:0").await.map_err(McError::Io)?;

        timeout(tout, socket.send_to(&build_ping(), target.addr))
            .await
            .map_err(|_| McError::timeout())?
            .map_err(McError::Io)?;

        let mut buf = [0u8; READ_BUFFER_SIZE];
        let (len, _) = timeout(tout, socket.recv_from(&mut buf))
            .await
            .map_err(|_| McError::timeout())?
            .map_err(McError::Io)?;

        if len < BEDROCK_MIN_RESPONSE_SIZE {
            return Err(McError::invalid_response("response too short"));
        }

        let latency = elapsed_ms(start);
        let pong    = String::from_utf8(buf[BEDROCK_PING_PACKET_SIZE..len].to_vec())
            .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());

        Ok(PingResult::new(ServerData::Bedrock(BedrockStatus::parse(&pong)?), latency))
    }
}

// ─── Packet builder ───────────────────────────────────────────────────────────

fn build_ping() -> Vec<u8> {
    let mut p = Vec::with_capacity(BEDROCK_PING_PACKET_SIZE);
    p.push(0x01);
    let ts = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64;
    p.extend_from_slice(&ts.to_be_bytes());
    p.extend_from_slice(&[
        0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE,
        0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
    ]);
    p.extend_from_slice(&[0u8; 8]);
    p
}

// ─── BedrockStatus parser ─────────────────────────────────────────────────────