From 3c6f15d5ebaad8436cb2f20aaa04db86cbaaa336 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 26 Apr 2026 19:45:22 +0200 Subject: [PATCH] fix: bedrock protocol version check --- pumpkin-protocol/src/bedrock/frame_set.rs | 24 +++++-- pumpkin-protocol/src/bedrock/mod.rs | 1 + pumpkin-protocol/src/query.rs | 15 +++-- pumpkin/src/net/bedrock/login.rs | 11 +++- pumpkin/src/net/bedrock/open_connection.rs | 9 +-- pumpkin/src/net/rcon/mod.rs | 6 +- pumpkin/src/net/rcon/packet.rs | 75 +++++++++++++--------- 7 files changed, 91 insertions(+), 50 deletions(-) diff --git a/pumpkin-protocol/src/bedrock/frame_set.rs b/pumpkin-protocol/src/bedrock/frame_set.rs index 3d5d62fea..d7240e511 100644 --- a/pumpkin-protocol/src/bedrock/frame_set.rs +++ b/pumpkin-protocol/src/bedrock/frame_set.rs @@ -1,6 +1,6 @@ use std::io::{Error, Read, Write}; -use crate::bedrock::{RAKNET_SPLIT, RakReliability}; +use crate::bedrock::{MTU, RAKNET_SPLIT, RakReliability}; use crate::codec::u24; use crate::serial::{PacketRead, PacketWrite}; @@ -68,15 +68,30 @@ impl Frame { pub fn read(reader: &mut R) -> Result, Error> { let mut frames = Vec::new(); + let mut header_buf = [0u8; 1]; - while let Ok(header) = u8::read(reader) { + while match reader.read(&mut header_buf) { + Ok(0) => false, + Ok(1) => true, + Err(e) => return Err(e), + _ => unreachable!(), + } { + let header = header_buf[0]; let mut frame = Self::default(); + let reliability_id = (header & 0xE0) >> 5; let Some(reliability) = RakReliability::from_id(reliability_id) else { return Err(Error::other("Invalid reliability")); }; + let split = (header & RAKNET_SPLIT) != 0; - let length = u16::read_be(reader)? >> 3; + + let bit_length = u16::read_be(reader)?; + let byte_length = (bit_length + 7) >> 3; + + if byte_length > MTU as u16 { + return Err(Error::other("Frame payload length exceeds RakNet MTU")); + } if reliability.is_reliable() { frame.reliable_number = u24::read(reader)?.0; @@ -98,8 +113,9 @@ impl Frame { } frame.reliability = reliability; - frame.payload = vec![0; length as usize]; + frame.payload = vec![0; byte_length as usize]; reader.read_exact(&mut frame.payload)?; + frames.push(frame); } diff --git a/pumpkin-protocol/src/bedrock/mod.rs b/pumpkin-protocol/src/bedrock/mod.rs index 94fb96cf1..7bb7b0724 100644 --- a/pumpkin-protocol/src/bedrock/mod.rs +++ b/pumpkin-protocol/src/bedrock/mod.rs @@ -6,6 +6,7 @@ pub mod packet_decoder; pub mod packet_encoder; pub mod server; +pub const RAKNET_PROTOCOL_VERSION: u8 = 11; pub const UDP_HEADER_SIZE: u16 = 28; pub const MTU: usize = 1400; diff --git a/pumpkin-protocol/src/query.rs b/pumpkin-protocol/src/query.rs index f584e7d5e..73b2be88c 100644 --- a/pumpkin-protocol/src/query.rs +++ b/pumpkin-protocol/src/query.rs @@ -129,11 +129,16 @@ impl SStatusRequest { .await .map_err(|_| DecodeError::UnexpectedEof)?; - let mut buf = [0u8; 4]; - let is_full_request = match packet.reader.read(&mut buf).await { - Ok(0) => false, - Ok(4) => true, - // Any other byte count is malformed. + let position = packet.reader.position(); + let total_len = packet.reader.get_ref().len() as u64; + let remaining = total_len.saturating_sub(position); + + let is_full_request = match remaining { + 0 => false, + 4 => { + packet.reader.set_position(position + 4); + true + } _ => return Err(DecodeError::MalformedPayload), }; diff --git a/pumpkin/src/net/bedrock/login.rs b/pumpkin/src/net/bedrock/login.rs index 19213c572..0c0bfe5aa 100644 --- a/pumpkin/src/net/bedrock/login.rs +++ b/pumpkin/src/net/bedrock/login.rs @@ -18,7 +18,7 @@ use pumpkin_protocol::{ codec::var_uint::VarUInt, }; use pumpkin_util::jwt::{AuthError, verify_chain}; -use pumpkin_world::CURRENT_BEDROCK_MC_VERSION; +use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION}; use serde::Deserialize; use std::sync::Arc; use thiserror::Error; @@ -49,7 +49,14 @@ struct CertificateChainPayload { } impl BedrockClient { - pub async fn handle_request_network_settings(&self, _packet: SRequestNetworkSettings) { + pub async fn handle_request_network_settings(&self, packet: SRequestNetworkSettings) { + if packet.protocol_version < CURRENT_BEDROCK_MC_PROTOCOL as i32 { + self.send_game_packet(&CPlayStatus::OutdatedClient).await; + return; + } else if packet.protocol_version > CURRENT_BEDROCK_MC_PROTOCOL as i32 { + self.send_game_packet(&CPlayStatus::OutdatedServer).await; + return; + } self.send_game_packet(&CNetworkSettings::new(0, 0, false, 0, 0.0)) .await; self.set_compression(CompressionInfo::default()).await; diff --git a/pumpkin/src/net/bedrock/open_connection.rs b/pumpkin/src/net/bedrock/open_connection.rs index 108fa6a13..e7b815c78 100644 --- a/pumpkin/src/net/bedrock/open_connection.rs +++ b/pumpkin/src/net/bedrock/open_connection.rs @@ -1,13 +1,13 @@ use std::net::SocketAddr; use pumpkin_protocol::bedrock::{ + RAKNET_PROTOCOL_VERSION, client::raknet::{ incompatible_protocol::CIncompatibleProtocolVersion, open_connection::{COpenConnectionReply1, COpenConnectionReply2}, }, server::raknet::open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2}, }; -use pumpkin_world::CURRENT_BEDROCK_MC_PROTOCOL; use tokio::net::UdpSocket; use crate::{net::bedrock::BedrockClient, server::Server}; @@ -19,12 +19,9 @@ impl BedrockClient { addr: SocketAddr, socket: &UdpSocket, ) { - if packet.protocol_version != CURRENT_BEDROCK_MC_PROTOCOL as u8 { + if packet.protocol_version != RAKNET_PROTOCOL_VERSION { Self::send_offline_packet( - &CIncompatibleProtocolVersion::new( - CURRENT_BEDROCK_MC_PROTOCOL as u8, - server.server_guid, - ), + &CIncompatibleProtocolVersion::new(RAKNET_PROTOCOL_VERSION, server.server_guid), addr, socket, ) diff --git a/pumpkin/src/net/rcon/mod.rs b/pumpkin/src/net/rcon/mod.rs index bada820c7..789d3f226 100644 --- a/pumpkin/src/net/rcon/mod.rs +++ b/pumpkin/src/net/rcon/mod.rs @@ -97,7 +97,7 @@ impl RCONClient { } async fn poll(&mut self, server: &Arc, password: &str) -> Result<(), PacketError> { - let Some(packet) = self.receive_packet().await? else { + let Some(packet) = self.receive_packet()? else { return Ok(()); }; let config = &server.advanced_config.networking.rcon; @@ -178,7 +178,7 @@ impl RCONClient { Ok(()) } - async fn receive_packet(&mut self) -> Result, PacketError> { - Packet::deserialize(&mut self.incoming).await + fn receive_packet(&mut self) -> Result, PacketError> { + Packet::deserialize(&mut self.incoming) } } diff --git a/pumpkin/src/net/rcon/packet.rs b/pumpkin/src/net/rcon/packet.rs index 717be909a..5f207d10f 100644 --- a/pumpkin/src/net/rcon/packet.rs +++ b/pumpkin/src/net/rcon/packet.rs @@ -1,11 +1,7 @@ -use std::{ - io::{BufRead, Cursor}, - string::FromUtf8Error, -}; +use std::string::FromUtf8Error; use bytes::{BufMut, BytesMut}; use thiserror::Error; -use tokio::io::AsyncReadExt; /// Client -> Server #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -57,10 +53,10 @@ impl ClientboundPacket { pub enum PacketError { #[error("Invalid length")] InvalidLength, - #[error("Failed to read packet")] - FailedRead(std::io::Error), #[error("Dailed to send packet")] FailedSend(std::io::Error), + #[error("Missing packet lull terminator")] + MissingNullTerminator, #[error("Invalid packet string body")] InvalidBody(FromUtf8Error), } @@ -74,36 +70,55 @@ pub struct Packet { } impl Packet { - pub async fn deserialize(incoming: &mut Vec) -> Result, PacketError> { + pub fn deserialize(incoming: &mut Vec) -> Result, PacketError> { + // We need at least 4 bytes to read the packet length header if incoming.len() < 4 { return Ok(None); } - let mut buf = Cursor::new(&incoming); - let len = buf.read_i32_le().await.map_err(PacketError::FailedRead)? + 4; - if !(0..=1460).contains(&len) { - return Err(PacketError::InvalidLength); - } - let id = buf.read_i32_le().await.map_err(PacketError::FailedRead)?; - let ty = buf.read_i32_le().await.map_err(PacketError::FailedRead)?; - let mut payload = vec![]; - let _ = buf - .read_until(b'\0', &mut payload) - .map_err(PacketError::FailedRead)?; - payload.pop(); - buf.read_u8().await.map_err(PacketError::FailedRead)?; - if buf.position() != len as u64 { - return Err(PacketError::InvalidLength); - } - incoming.drain(0..len as usize); - let packet = Self { + // Read the 4-byte length header (little-endian) + let len_bytes: [u8; 4] = incoming[0..4].try_into().unwrap(); + let size = i32::from_le_bytes(len_bytes); + + // An RCON packet size includes: ID(4) + Type(4) + Body(n) + Null(1) + EmptyStringNull(1). + // Minimum size for an empty body is 10. Maximum MTU size is typically 1460. + if !(10..=1460).contains(&size) { + return Err(PacketError::InvalidLength); + } + + // Total bytes we need in the buffer (length header + the payload size) + let total_packet_len = (size as usize) + 4; + + // If the network hasn't delivered the full packet yet, return Ok(None) + if incoming.len() < total_packet_len { + return Ok(None); + } + + // We have the full packet. Parse it synchronously using fixed slices. + let id = i32::from_le_bytes(incoming[4..8].try_into().unwrap()); + let ty = i32::from_le_bytes(incoming[8..12].try_into().unwrap()); + + // Calculate body boundaries (starts after len, id, and ty -> 4+4+4 = 12) + // Ends 2 bytes before the total length (excluding the two trailing null bytes) + let body_start = 12; + let body_end = total_packet_len - 2; + + if incoming[body_end] != 0 || incoming[body_end + 1] != 0 { + return Err(PacketError::MissingNullTerminator); + } + + let payload = &incoming[body_start..body_end]; + let body = String::from_utf8(payload.to_vec()).map_err(PacketError::InvalidBody)?; + + incoming.drain(0..total_packet_len); + + Ok(Some(Self { id, ptype: ServerboundPacket::from_i32(ty), - body: String::from_utf8(payload).map_err(PacketError::InvalidBody)?, - }; - - Ok(Some(packet)) + body, + })) } + pub fn get_body(&self) -> &str { &self.body }