chore: remove bedrock raknet (#2811)

* Remove legacy RakNet transport

* Replace RakNet status with NetherNet discovery

* Restore Bedrock server-list status

* Remove NetherNet LAN discovery

* Remove unused AES dependency
This commit is contained in:
ZlordHUN
2026-08-07 20:00:41 +02:00
committed by GitHub
parent 7e2505b055
commit ffcf09ece4
37 changed files with 413 additions and 2358 deletions

30
Cargo.lock generated
View File

@@ -58,7 +58,7 @@ dependencies = [
"aead",
"aes 0.8.4",
"cipher 0.4.4",
"ctr 0.9.2",
"ctr",
"ghash",
"subtle",
]
@@ -457,7 +457,7 @@ checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847"
dependencies = [
"aead",
"cipher 0.4.4",
"ctr 0.9.2",
"ctr",
"subtle",
]
@@ -567,7 +567,6 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
dependencies = [
"block-buffer 0.12.1",
"crypto-common 0.2.2",
"inout 0.2.2",
]
@@ -1125,15 +1124,6 @@ dependencies = [
"cipher 0.4.4",
]
[[package]]
name = "ctr"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21"
dependencies = [
"cipher 0.5.2",
]
[[package]]
name = "ctutils"
version = "0.4.2"
@@ -1416,7 +1406,7 @@ dependencies = [
"ff 0.13.1",
"generic-array",
"group 0.13.0",
"hkdf 0.12.4",
"hkdf",
"pem-rfc7468 0.7.0",
"pkcs8 0.10.2",
"rand_core 0.6.4",
@@ -1437,7 +1427,6 @@ dependencies = [
"digest 0.11.3",
"ff 0.14.0",
"group 0.14.0",
"hkdf 0.13.0",
"hybrid-array",
"pkcs8 0.11.0",
"rand_core 0.10.1",
@@ -1881,15 +1870,6 @@ dependencies = [
"hmac 0.12.1",
]
[[package]]
name = "hkdf"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018"
dependencies = [
"hmac 0.13.0",
]
[[package]]
name = "hmac"
version = "0.12.1"
@@ -3424,7 +3404,6 @@ dependencies = [
"bitflags 2.13.1",
"bytes",
"cfb8",
"ctr 0.10.1",
"flate2",
"hybrid-array",
"pumpkin-data",
@@ -3433,7 +3412,6 @@ dependencies = [
"pumpkin-util",
"pumpkin-world",
"serde",
"sha2 0.11.0",
"thiserror 2.0.19",
"tokio",
"uuid",
@@ -5572,7 +5550,7 @@ dependencies = [
"aes-gcm",
"byteorder",
"bytes",
"ctr 0.9.2",
"ctr",
"hmac 0.12.1",
"log",
"rtcp",

View File

@@ -143,7 +143,6 @@ base64 = { version = "0.23.1", default-features = false, features = ["std"] }
bitflags = { version = "2.13.1", default-features = false, features = ["std"] }
cesu8 = { version = "1.1", default-features = false }
cfb8 = { version = "0.9", default-features = false }
ctr = { version = "0.10.1", default-features = false }
colored = { version = "3.1", default-features = false }
console-subscriber = { version = "0.5.0", default-features = false }
crc-fast = { version = "1.10.0", default-features = false, features = ["std"] }
@@ -165,7 +164,7 @@ md5 = { version = "0.8", default-features = false }
num-bigint = { version = "0.5", default-features = false, features = ["std"] }
num-derive = { version = "0.5", default-features = false }
num-traits = { version = "0.2", default-features = false, features = ["std"] }
p384 = { version = "0.14.0", default-features = false, features = ["std", "arithmetic", "pkcs8", "ecdh"] }
p384 = { version = "0.14.0", default-features = false, features = ["std", "arithmetic", "pkcs8"] }
phf = { version = "0.14.0", default-features = false, features = ["std"] }
proc-macro2 = { version = "1.0", default-features = false, features = ["proc-macro"] }
pumpkin-codecs = { path = "crates/pumpkin-codecs", default-features = false }

View File

@@ -112,13 +112,6 @@ impl LoadConfiguration for PumpkinConfig {
self.advanced.networking.bedrock.view_distance <= max_vd,
"Bedrock View distance must be less than 64"
);
if self.advanced.networking.bedrock.online_mode {
assert!(
self.advanced.networking.bedrock.encryption,
"When online mode is enabled, bedrock_encryption must be enabled"
);
}
if self.basic.allow_chat_reports {
assert!(
self.advanced.networking.java.online_mode,

View File

@@ -60,10 +60,6 @@ impl Default for BedrockAuthenticationConfig {
pub struct BedrockConfig {
/// Whether Bedrock Edition Clients are Accepted.
pub enabled: bool,
/// Whether Bedrock Edition Clients are Accepted.
pub address: SocketAddr,
/// Whether packet encryption is enabled for Bedrock Edition.
pub encryption: bool,
/// Whether online mode is enabled.
pub online_mode: bool,
/// The maximum number of players allowed on the server. Specifying `0` disables the limit.
@@ -86,8 +82,6 @@ impl Default for BedrockConfig {
fn default() -> Self {
Self {
enabled: true,
address: "0.0.0.0:19132".parse().unwrap(),
encryption: true,
online_mode: true,
max_players: 1000,
view_distance: NonZeroU8::new(16).unwrap(),

View File

@@ -27,8 +27,6 @@ bytes.workspace = true
# encryption
aes.workspace = true
cfb8.workspace = true
ctr.workspace = true
sha2.workspace = true
hybrid-array = "0.4"
# compression

View File

@@ -1,6 +1,6 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::bedrock::packet_decoder::UDPNetworkDecoder;
use pumpkin_protocol::bedrock::packet_decoder::BedrockBatchDecoder;
use pumpkin_protocol::bedrock::server::{
client_cache_status::SClientCacheStatus,
command_request::SCommandRequest,
@@ -9,11 +9,6 @@ use pumpkin_protocol::bedrock::server::{
loading_screen::SLoadingScreen,
login::SLogin,
player_auth_input::SPlayerAuthInput,
raknet::{
connection::SConnectionRequest,
open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2},
unconnected_ping::{SUnconnectedPing, SUnconnectedPingOpenConnections},
},
request_chunk_radius::SRequestChunkRadius,
request_network_settings::SRequestNetworkSettings,
text::SText,
@@ -50,42 +45,27 @@ fn fuzz_serverbound_packets(payload: &[u8]) {
SRequestNetworkSettings,
SText,
);
// RakNet Handshake Packets (Usually read without version)
cursor.set_position(0);
let _ = SConnectionRequest::read(&mut cursor);
cursor.set_position(0);
let _ = SOpenConnectionRequest1::read(&mut cursor);
cursor.set_position(0);
let _ = SOpenConnectionRequest2::read(&mut cursor);
cursor.set_position(0);
let _ = SUnconnectedPing::read(&mut cursor);
cursor.set_position(0);
let _ = SUnconnectedPingOpenConnections::read(&mut cursor);
}
// ---------------------------------------------------------------------------
// Fuzz Target
// ---------------------------------------------------------------------------
fuzz_target!(|data: &[u8]| {
if data.len() < 20 {
if data.len() < 2 {
return;
}
// Split data for decoder configuration vs raw payload
let threshold_raw = data[0];
let key: [u8; 16] = data[1..17].try_into().unwrap();
let stream_data = &data[17..];
let stream_data = &data[1..];
let mut decoder = UDPNetworkDecoder::new();
let mut decoder = BedrockBatchDecoder::new();
// Setup Decoder
if threshold_raw > 0 {
// Assuming your CompressionThreshold is a wrapper around u32
decoder.set_compression((threshold_raw as u32).try_into().unwrap());
}
decoder.set_encryption(&key);
// 1. Fuzz the Decoder (Framing/VarInts/Bitmasks)
let decoder_cursor = Cursor::new(stream_data.to_vec());
if let Ok(raw_packet) = decoder.get_game_packet(decoder_cursor) {

View File

@@ -1,6 +1,6 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::bedrock::packet_encoder::UDPNetworkEncoder;
use pumpkin_protocol::bedrock::packet_encoder::BedrockBatchEncoder;
use pumpkin_protocol::bedrock::SubClient;
fuzz_target!(|data: &[u8]| {
@@ -26,7 +26,7 @@ fuzz_target!(|data: &[u8]| {
let use_compression = data[6] % 2 == 0;
let packet_payload = &data[7..];
let mut encoder = UDPNetworkEncoder::new();
let mut encoder = BedrockBatchEncoder::new();
if use_compression {
encoder.set_compression((compression_threshold, compression_level));

View File

@@ -1,85 +0,0 @@
use std::io::{Error, ErrorKind, Read, Write};
const MAX_ACK_RECORDS: u16 = 4096;
use crate::{
codec::u24,
serial::{PacketRead, PacketWrite},
};
pub struct Acknowledge {
pub sequences: Vec<u32>,
}
impl Acknowledge {
#[must_use]
pub const fn new(sequences: Vec<u32>) -> Self {
Self { sequences }
}
fn write_range<W: Write>(start: u32, end: u32, writer: &mut W) -> Result<(), Error> {
if start == end {
1u8.write(writer)?;
u24(start).write(writer)
} else {
0u8.write(writer)?;
u24(start).write(writer)?;
u24(end).write(writer)
}
}
pub fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let size = u16::read_be(reader)?;
if size > MAX_ACK_RECORDS {
return Err(Error::new(
ErrorKind::InvalidData,
"Acknowledge packet range is too large.",
));
}
let mut sequences = Vec::with_capacity(size as usize);
for _ in 0..size {
let single = bool::read(reader)?;
if single {
sequences.push(u24::read(reader)?.0);
} else {
let start = u24::read(reader)?.0;
let end = u24::read(reader)?.0;
for i in start..=end {
sequences.push(i);
}
}
}
Ok(Self { sequences })
}
pub fn write<W: Write>(&self, writer: &mut W, id: u8) -> Result<(), Error> {
id.write(writer)?;
if self.sequences.is_empty() {
0u16.write_be(writer)?;
return Ok(());
}
let mut count: u16 = 0;
let mut buf = Vec::new();
let mut sequences = self.sequences.clone();
sequences.sort_unstable();
let mut start = sequences[0];
let mut end = start;
for seq in sequences.iter().copied().skip(1) {
if seq != end + 1 {
Self::write_range(start, end, &mut buf)?;
count += 1;
start = seq;
}
end = seq;
}
Self::write_range(start, end, &mut buf)?;
count += 1;
count.write_be(writer)?;
writer.write_all(&buf)
}
}

View File

@@ -1,15 +0,0 @@
use crate::serial::PacketWrite;
use pumpkin_macros::packet;
#[derive(PacketWrite)]
#[packet(0x03)]
pub struct CHandshake {
jwt_data: String,
}
impl CHandshake {
#[must_use]
pub const fn new(jwt_data: String) -> Self {
Self { jwt_data }
}
}

View File

@@ -12,7 +12,6 @@ pub mod crafting_data;
pub mod creative_content;
pub mod disconnect_player;
pub mod gamerules_changed;
pub mod handshake;
pub mod inventory_content;
pub mod inventory_slot;
pub mod item_registry;
@@ -28,7 +27,6 @@ pub mod network_settings;
pub mod play_status;
pub mod player_hotbar;
pub mod player_list;
pub mod raknet;
pub mod remove_actor;
pub mod resource_pack_stack;
pub mod resource_packs_info;
@@ -66,7 +64,6 @@ pub use crafting_data::*;
pub use creative_content::*;
pub use disconnect_player::*;
pub use gamerules_changed::*;
pub use handshake::*;
pub use inventory_content::*;
pub use inventory_slot::*;
pub use item_registry::*;
@@ -83,7 +80,6 @@ pub use network_settings::*;
pub use play_status::*;
pub use player_hotbar::*;
pub use player_list::*;
pub use raknet::*;
pub use remove_actor::*;
pub use resource_pack_stack::*;
pub use resource_packs_info::*;

View File

@@ -1,141 +0,0 @@
use std::net::SocketAddr;
use pumpkin_macros::packet;
use crate::{bedrock::RAKNET_MAGIC, serial::PacketWrite};
/// Sent in response to a `ConnectedPing` (`0x00`) to calculate round-trip latency and synchronize time across an established connection.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Connected_Pong>
#[derive(PacketWrite)]
#[packet(0x03)]
pub struct CConnectedPong {
ping: u64,
pong: u64,
}
impl CConnectedPong {
#[must_use]
#[expect(clippy::similar_names)]
pub const fn new(ping: u64, pong: u64) -> Self {
Self { ping, pong }
}
}
/// Sent by the server to accept an incoming `ConnectionRequest` (`0x09`), confirming connection parameters and system addresses.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Connection_Request_Accepted>
#[derive(PacketWrite)]
#[packet(0x10)]
pub struct CConnectionRequestAccepted {
client_address: SocketAddr,
system_index: u16,
system_addresses: [SocketAddr; 10],
requested_timestamp: u64,
timestamp: u64,
}
impl CConnectionRequestAccepted {
#[must_use]
pub const fn new(
client_address: SocketAddr,
system_index: u16,
system_addresses: [SocketAddr; 10],
requested_timestamp: u64,
timestamp: u64,
) -> Self {
Self {
client_address,
system_index,
system_addresses,
requested_timestamp,
timestamp,
}
}
}
/// Sent by the server when a client attempts to connect while already being connected.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Already_Connected>
#[derive(PacketWrite)]
#[packet(0x12)]
pub struct CAlreadyConnected {
magic: [u8; 16],
server_guid: u64,
}
impl CAlreadyConnected {
#[must_use]
pub const fn new(server_guid: u64) -> Self {
Self {
magic: RAKNET_MAGIC,
server_guid,
}
}
}
/// Sent by the server when it has reached its maximum connection capacity.
///
/// Ref: <https://minecraft.wiki/w/RakNet#No_Free_Incoming_Connections>
#[derive(PacketWrite)]
#[packet(0x14)]
pub struct CNoFreeIncomingConnections {
magic: [u8; 16],
server_guid: u64,
}
impl CNoFreeIncomingConnections {
#[must_use]
pub const fn new(server_guid: u64) -> Self {
Self {
magic: RAKNET_MAGIC,
server_guid,
}
}
}
/// Sent by the server when a client attempts to connect from a banned IP address or identifier.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Connection_Banned>
#[derive(PacketWrite)]
#[packet(0x17)]
pub struct CConnectionBanned {
magic: [u8; 16],
server_guid: u64,
}
impl CConnectionBanned {
#[must_use]
pub const fn new(server_guid: u64) -> Self {
Self {
magic: RAKNET_MAGIC,
server_guid,
}
}
}
/// Sent by the server when a client attempts to connect again too quickly after disconnecting.
///
/// Ref: <https://minecraft.wiki/w/RakNet#IP_Recently_Connected>
#[derive(PacketWrite)]
#[packet(0x1A)]
pub struct CIpRecentlyConnected {
magic: [u8; 16],
server_guid: u64,
}
impl CIpRecentlyConnected {
#[must_use]
pub const fn new(server_guid: u64) -> Self {
Self {
magic: RAKNET_MAGIC,
server_guid,
}
}
}
/// Sent by the server to initiate graceful termination of the connection session.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Disconnection_Notification>
#[derive(PacketWrite)]
#[packet(0x15)]
pub struct CDisconnect;

View File

@@ -1,24 +0,0 @@
use crate::{bedrock::RAKNET_MAGIC, serial::PacketWrite};
use pumpkin_macros::packet;
/// Sent by the server when the client's `RakNet` protocol version does not match the server's expected protocol version (`11`).
///
/// Ref: <https://minecraft.wiki/w/RakNet#Incompatible_Protocol_Version>
#[derive(PacketWrite)]
#[packet(0x19)]
pub struct CIncompatibleProtocolVersion {
protocol_version: u8,
magic: [u8; 16],
server_guid: u64,
}
impl CIncompatibleProtocolVersion {
#[must_use]
pub const fn new(protocol_version: u8, server_guid: u64) -> Self {
Self {
protocol_version,
magic: RAKNET_MAGIC,
server_guid,
}
}
}

View File

@@ -1,4 +0,0 @@
pub mod connection;
pub mod incompatible_protocol;
pub mod open_connection;
pub mod unconnected_pong;

View File

@@ -1,63 +0,0 @@
use std::net::SocketAddr;
use pumpkin_macros::packet;
use crate::{bedrock::RAKNET_MAGIC, serial::PacketWrite};
/// Sent by the server in response to `OpenConnectionRequest1` (`0x05`), negotiating security options, server GUID, and MTU.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Open_Connection_Reply_1>
#[derive(PacketWrite)]
#[packet(0x06)]
pub struct COpenConnectionReply1 {
magic: [u8; 16],
server_guid: u64,
has_server_security: bool,
// Only write when has_server_security
// cookie: u32,
mtu: u16,
}
impl COpenConnectionReply1 {
#[must_use]
pub const fn new(server_guid: u64, has_server_security: bool, mtu: u16) -> Self {
Self {
magic: RAKNET_MAGIC,
server_guid,
has_server_security,
// cookie,
mtu,
}
}
}
/// Sent by the server in response to `OpenConnectionRequest2` (`0x07`), confirming the connection setup and client address before establishing session state.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Open_Connection_Reply_2>
#[derive(PacketWrite)]
#[packet(0x08)]
pub struct COpenConnectionReply2 {
magic: [u8; 16],
server_guid: u64,
client_address: SocketAddr,
mtu: u16,
security: bool,
}
impl COpenConnectionReply2 {
#[must_use]
pub const fn new(
server_guid: u64,
client_address: SocketAddr,
mtu: u16,
security: bool,
) -> Self {
Self {
magic: RAKNET_MAGIC,
server_guid,
client_address,
mtu,
security,
}
}
}

View File

@@ -1,76 +0,0 @@
use core::fmt;
use std::io::{Error, Write};
use pumpkin_macros::packet;
use crate::serial::PacketWrite;
/// Sent by the server in response to an `UnconnectedPing` (`0x01` / `0x02`), containing server metadata (MOTD, protocol version, player counts, edition).
///
/// Ref: <https://minecraft.wiki/w/RakNet#Unconnected_Pong>
#[packet(0x1c)]
pub struct CUnconnectedPong {
time: u64,
server_guid: u64,
magic: [u8; 16],
server_id: String,
}
impl PacketWrite for CUnconnectedPong {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.time.write_be(writer)?;
self.server_guid.write_be(writer)?;
writer.write_all(&self.magic)?;
writer.write_all(&(self.server_id.len() as u16).to_be_bytes())?;
writer.write_all(self.server_id.as_bytes())
}
}
pub struct ServerInfo {
/// (BE or MCEE for Education Edition)
pub edition: &'static str,
pub motd_line_1: String,
pub protocol_version: u32,
pub version_name: &'static str,
pub player_count: i32,
pub max_player_count: u32,
pub server_unique_id: u64,
pub motd_line_2: String,
pub game_mode: &'static str,
pub game_mode_numeric: u32,
pub port_ipv4: u16,
pub port_ipv6: u16,
}
impl fmt::Display for ServerInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{};{};{};{};{};{};{};{};{};{};{};{};0;",
self.edition,
self.motd_line_1,
self.protocol_version,
self.version_name,
self.player_count,
self.max_player_count,
self.server_unique_id,
self.motd_line_2,
self.game_mode,
self.game_mode_numeric,
self.port_ipv4,
self.port_ipv6
)
}
}
impl CUnconnectedPong {
#[must_use]
pub const fn new(time: u64, server_guid: u64, magic: [u8; 16], server_id: String) -> Self {
Self {
time,
server_guid,
magic,
server_id,
}
}
}

View File

@@ -1,121 +0,0 @@
use aes::Aes256;
use ctr::Ctr128BE;
use ctr::cipher::{KeyIvInit, StreamCipher};
use sha2::{Digest, Sha256};
type BedrockCtr = Ctr128BE<Aes256>;
pub struct BedrockEncryptor {
cipher: BedrockCtr,
key: [u8; 32],
send_counter: u64,
}
impl BedrockEncryptor {
#[must_use]
pub fn new(key: &[u8; 32]) -> Self {
let mut iv = [0u8; 16];
iv[..12].copy_from_slice(&key[..12]);
iv[12..].copy_from_slice(&[0, 0, 0, 2]);
Self {
cipher: BedrockCtr::new(key.into(), &iv.into()),
key: *key,
send_counter: 0,
}
}
pub fn encrypt(&mut self, data: &mut Vec<u8>) {
// data contains the payload after 0xfe
let mut hasher = Sha256::new();
hasher.update(self.send_counter.to_le_bytes());
hasher.update(&data[..]);
hasher.update(self.key);
let hash = hasher.finalize();
data.extend_from_slice(&hash[..8]);
self.cipher.apply_keystream(data);
self.send_counter += 1;
}
}
pub struct BedrockDecryptor {
cipher: BedrockCtr,
key: [u8; 32],
send_counter: u64,
}
impl BedrockDecryptor {
#[must_use]
pub fn new(key: &[u8; 32]) -> Self {
let mut iv = [0u8; 16];
iv[..12].copy_from_slice(&key[..12]);
iv[12..].copy_from_slice(&[0, 0, 0, 2]);
Self {
cipher: BedrockCtr::new(key.into(), &iv.into()),
key: *key,
send_counter: 0,
}
}
#[expect(clippy::needless_borrow)] // False positive
pub fn decrypt(&mut self, data: &mut Vec<u8>) -> Result<(), String> {
let ciphertext = data.clone();
self.cipher.apply_keystream(data);
if data.len() < 8 {
return Err("Encrypted packet must be at least 8 bytes long".to_string());
}
let (payload, checksum) = data.split_at(data.len() - 8);
let mut hasher = Sha256::new();
hasher.update(self.send_counter.to_le_bytes());
hasher.update(payload);
hasher.update(self.key);
let our_checksum = &hasher.finalize()[..8];
if checksum != our_checksum {
let cipher_prefix = if ciphertext.len() > 16 {
&ciphertext[..16]
} else {
&ciphertext
};
let plain_prefix = if data.len() > 16 { &data[..16] } else { &data };
return Err(format!(
"Invalid checksum: expected {:x?}, got {:x?}. Cipher prefix: {:x?}, Plain prefix: {:x?}, Key: {:x?}, Counter: {}",
our_checksum, checksum, cipher_prefix, plain_prefix, self.key, self.send_counter
));
}
data.truncate(payload.len());
self.send_counter += 1;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gcm_compatibility() {
let key = [1u8; 32];
let mut iv = [0u8; 16];
iv[..12].copy_from_slice(&key[..12]);
iv[12..].copy_from_slice(&[0, 0, 0, 2]);
let mut cipher = BedrockCtr::new((&key).into(), (&iv).into());
let mut data = b"Hello Bedrock encryption!".to_vec();
cipher.apply_keystream(&mut data);
let expected = &[
0xfa, 0x1c, 0xd6, 0xf6, 0x06, 0xd7, 0x47, 0x96, 0x8e, 0xd7, 0x60, 0xfe, 0xc5, 0x1c,
0x2b, 0xc7, 0x7e, 0x46, 0x17, 0x74, 0x25, 0x96, 0x34, 0x0c, 0xac,
];
assert_eq!(data, expected);
}
}

View File

@@ -1,150 +0,0 @@
use std::io::{Error, Read, Write};
use crate::bedrock::{MTU, RAKNET_SPLIT, RakReliability};
use crate::codec::u24;
use crate::serial::{PacketRead, PacketWrite};
pub struct FrameSet {
pub sequence: u24,
pub frames: Vec<Frame>,
}
impl FrameSet {
pub fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
Ok(Self {
sequence: u24::read(reader)?,
frames: Frame::read(reader)?,
})
}
pub fn write_packet_data<W: Write>(&self, writer: &mut W, id: u8) -> Result<(), Error> {
id.write(writer)?;
self.sequence.write(writer)?;
for frame in &self.frames {
frame.write(writer)?;
}
Ok(())
}
}
impl Default for FrameSet {
fn default() -> Self {
Self {
sequence: u24(0),
frames: Vec::default(),
}
}
}
#[derive(Default)]
pub struct Frame {
pub reliability: RakReliability,
// If we write a packet we dont want to own the payload to avoid cloning
pub payload: Vec<u8>,
pub reliable_number: u32,
pub sequence_index: u32,
pub order_index: u32,
pub order_channel: u8,
pub split_size: u32,
pub split_id: u16,
pub split_index: u32,
}
impl Frame {
#[must_use]
pub const fn new_unreliable(payload: Vec<u8>) -> Self {
Self {
reliability: RakReliability::Unreliable,
payload,
reliable_number: 0,
sequence_index: 0,
order_index: 0,
order_channel: 0,
split_size: 0,
split_id: 0,
split_index: 0,
}
}
pub fn read<R: Read>(reader: &mut R) -> Result<Vec<Self>, Error> {
let mut frames = Vec::new();
let mut header_buf = [0u8; 1];
while {
let n = reader.read(&mut header_buf)?;
n > 0
} {
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 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;
}
if reliability.is_sequenced() {
frame.sequence_index = u24::read(reader)?.0;
}
if reliability.is_ordered() {
frame.order_index = u24::read(reader)?.0;
frame.order_channel = u8::read(reader)?;
}
if split {
frame.split_size = u32::read_be(reader)?;
frame.split_id = u16::read_be(reader)?;
frame.split_index = u32::read_be(reader)?;
}
frame.reliability = reliability;
frame.payload = vec![0; byte_length as usize];
reader.read_exact(&mut frame.payload)?;
frames.push(frame);
}
Ok(frames)
}
pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
let is_split = self.split_size > 0;
let mut flags = self.reliability.to_id() << 5;
if is_split {
flags |= RAKNET_SPLIT;
}
flags.write(writer)?;
// Size
((self.payload.len() as u16) << 3).write_be(writer)?;
if self.reliability.is_reliable() {
u24(self.reliable_number).write(writer)?;
}
if self.reliability.is_sequenced() {
u24(self.sequence_index).write(writer)?;
}
if self.reliability.is_ordered() {
u24(self.order_index).write(writer)?;
self.order_channel.write(writer)?;
}
if is_split {
self.split_size.write_be(writer)?;
self.split_id.write_be(writer)?;
self.split_index.write_be(writer)?;
}
writer.write_all(&self.payload)
}
}

View File

@@ -1,110 +1,11 @@
pub mod ack;
pub mod client;
pub mod crypto;
pub mod frame_set;
pub mod network_item;
pub mod packet_decoder;
pub mod packet_encoder;
pub mod server;
pub mod status;
pub const RAKNET_PROTOCOL_VERSION: u8 = 11;
pub const UDP_HEADER_SIZE: usize = 28;
pub const MTU: usize = 1400;
// 26 bytes is RakNet header for FrameSet containing a single, ReliableOrdered, split/fragmented frame
pub const SPLIT_FRAME_MAX_CONTENT: usize = MTU - UDP_HEADER_SIZE - 26;
pub const RAKNET_MAGIC: [u8; 16] = [
0x00, 0xff, 0xff, 0x0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, 0x56, 0x78,
];
pub const RAKNET_VALID: u8 = 0x80;
pub const RAKNET_ACK: u8 = 0xC0;
pub const RAKNET_NACK: u8 = 0xA0;
pub const RAKNET_GAME_PACKET: i32 = 0xfe;
pub const RAKNET_SPLIT: u8 = 0x10;
#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)]
pub enum RakReliability {
Unreliable,
UnreliableSequenced,
Reliable,
#[default]
ReliableOrdered,
ReliableSequenced,
UnreliableWithAckReceipt,
ReliableWithAckReceipt,
ReliableOrderedWithAckReceipt,
}
impl RakReliability {
#[must_use]
pub const fn is_reliable(&self) -> bool {
matches!(
self,
Self::Reliable
| Self::ReliableOrdered
| Self::ReliableSequenced
| Self::ReliableWithAckReceipt
| Self::ReliableOrderedWithAckReceipt
)
}
#[must_use]
pub const fn is_sequenced(&self) -> bool {
matches!(self, Self::ReliableSequenced | Self::UnreliableSequenced)
}
#[must_use]
pub const fn is_ordered(&self) -> bool {
matches!(
self,
Self::UnreliableSequenced
| Self::ReliableOrdered
| Self::ReliableSequenced
| Self::ReliableOrderedWithAckReceipt
)
}
#[must_use]
pub const fn is_order_exclusive(&self) -> bool {
matches!(
self,
Self::ReliableOrdered | Self::ReliableOrderedWithAckReceipt
)
}
#[must_use]
pub const fn from_id(id: u8) -> Option<Self> {
match id {
0 => Some(Self::Unreliable),
1 => Some(Self::UnreliableSequenced),
2 => Some(Self::Reliable),
3 => Some(Self::ReliableOrdered),
4 => Some(Self::ReliableSequenced),
5 => Some(Self::UnreliableWithAckReceipt),
6 => Some(Self::ReliableWithAckReceipt),
7 => Some(Self::ReliableOrderedWithAckReceipt),
_ => None,
}
}
#[must_use]
pub const fn to_id(&self) -> u8 {
match self {
Self::Unreliable => 0,
Self::UnreliableSequenced => 1,
Self::Reliable => 2,
Self::ReliableOrdered => 3,
Self::ReliableSequenced => 4,
Self::UnreliableWithAckReceipt => 5,
Self::ReliableWithAckReceipt => 6,
Self::ReliableOrderedWithAckReceipt => 7,
}
}
}
pub const BEDROCK_GAME_PACKET: u8 = 0xfe;
#[repr(u16)]
pub enum SubClient {

View File

@@ -1,95 +1,35 @@
use std::{
io::{Cursor, Read},
pin::Pin,
task::{Context, Poll},
};
use std::io::{Cursor, Read};
use async_compression::tokio::bufread::DeflateDecoder;
use tokio::io::{AsyncRead, BufReader, ReadBuf};
use tokio::io::BufReader;
use crate::{
Aes128Cfb8Dec, CompressionThreshold, MAX_PACKET_DATA_SIZE, PacketDecodeError, RawPacket,
StreamDecryptor, codec::var_uint::VarUInt, ser::ReadingError,
CompressionThreshold, MAX_PACKET_DATA_SIZE, PacketDecodeError, RawPacket,
bedrock::BEDROCK_GAME_PACKET, codec::var_uint::VarUInt, ser::ReadingError,
};
pub enum DecryptionReader<R: AsyncRead + Unpin> {
Decrypt(Box<StreamDecryptor<R>>),
None(R),
}
impl<R: AsyncRead + Unpin> DecryptionReader<R> {
#[must_use]
pub fn upgrade(self, cipher: Aes128Cfb8Dec) -> Self {
match self {
Self::None(stream) => Self::Decrypt(Box::new(StreamDecryptor::new(cipher, stream))),
Self::Decrypt(_) => self,
}
}
}
impl<R: AsyncRead + Unpin> AsyncRead for DecryptionReader<R> {
#[inline]
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
match self.get_mut() {
Self::Decrypt(reader) => {
let reader = Pin::new(reader);
reader.poll_read(cx, buf)
}
Self::None(reader) => {
let reader = Pin::new(reader);
reader.poll_read(cx, buf)
}
}
}
}
use crate::bedrock::crypto::BedrockDecryptor;
/// Decoder: Client -> Server
/// Supports `ZLib` decoding/decompression
/// Supports Aes256 Encryption
pub struct UDPNetworkDecoder {
/// Supports Zlib decompression.
pub struct BedrockBatchDecoder {
compression: Option<CompressionThreshold>,
decryptor: Option<BedrockDecryptor>,
}
impl Default for UDPNetworkDecoder {
impl Default for BedrockBatchDecoder {
fn default() -> Self {
Self::new()
}
}
use thiserror::Error;
#[derive(Debug, Error)]
#[error("Encryption already enabled")]
pub struct EncryptionAlreadyEnabledError;
impl UDPNetworkDecoder {
impl BedrockBatchDecoder {
#[must_use]
pub const fn new() -> Self {
Self {
compression: None,
decryptor: None,
}
Self { compression: None }
}
pub const fn set_compression(&mut self, threshold: CompressionThreshold) {
self.compression = Some(threshold);
}
pub fn set_encryption(&mut self, key: &[u8; 32]) -> Result<(), EncryptionAlreadyEnabledError> {
if self.decryptor.is_some() {
return Err(EncryptionAlreadyEnabledError);
}
self.decryptor = Some(BedrockDecryptor::new(key));
Ok(())
}
pub async fn get_packet_payload(
&mut self,
full_packet: Vec<u8>,
@@ -98,27 +38,20 @@ impl UDPNetworkDecoder {
return Err(PacketDecodeError::MalformedLength("Empty packet".into()));
}
// If the first byte isn't 0xfe, it's likely a RakNet control packet or encrypted.
// Ensure your RakNet implementation is providing ONLY the payload here.
if full_packet[0] != 0xfe {
// NetherNet carries the batch without the Bedrock game-packet marker.
// The transport adapter restores it before decoding.
if full_packet[0] != BEDROCK_GAME_PACKET {
return Err(PacketDecodeError::MalformedLength(format!(
"Missing 0xfe header (found 0x{:02x})",
full_packet[0]
)));
}
let mut data_to_decrypt = full_packet[1..].to_vec();
if let Some(decryptor) = &mut self.decryptor {
decryptor
.decrypt(&mut data_to_decrypt)
.map_err(PacketDecodeError::Message)?;
}
let full_packet_payload = data_to_decrypt;
let full_packet_payload = &full_packet[1..];
// If compression is NOT enabled yet, the payload starts at index 0 of full_packet_payload
if self.compression.is_none() {
let payload = &full_packet_payload[..];
let payload = full_packet_payload;
if payload.len() > MAX_PACKET_DATA_SIZE {
return Err(PacketDecodeError::TooLong);
}
@@ -217,7 +150,7 @@ impl UDPNetworkDecoder {
mod tests {
use std::io::Cursor;
use crate::bedrock::{SubClient, packet_encoder::UDPNetworkEncoder};
use crate::bedrock::{SubClient, packet_encoder::BedrockBatchEncoder};
use super::*;
@@ -226,7 +159,7 @@ mod tests {
const PAYLOAD_LEN: usize = 2 * 1024 * 1024 + 1;
let payload = vec![0x2a; PAYLOAD_LEN];
let mut wire_buf = Vec::new();
let network_encoder = UDPNetworkEncoder::new();
let network_encoder = BedrockBatchEncoder::new();
network_encoder
.write_game_packet(
0x01,
@@ -238,7 +171,7 @@ mod tests {
.expect("encode Bedrock game packet");
let mut cursor = Cursor::new(wire_buf[1..].to_vec());
let mut decoder = UDPNetworkDecoder::new();
let mut decoder = BedrockBatchDecoder::new();
let packet = decoder
.get_game_packet(&mut cursor)

View File

@@ -1,104 +1,31 @@
use std::{
io::{self, Error, Write},
net::SocketAddr,
pin::Pin,
task::{Context, Poll},
};
use std::io::{Error, Write};
use flate2::{Compression, write::DeflateEncoder};
use tokio::{io::AsyncWrite, net::UdpSocket};
use crate::{
Aes128Cfb8Enc, CompressionLevel, CompressionThreshold, StreamEncryptor, bedrock::SubClient,
codec::var_uint::VarUInt, ser::NetworkWriteExt,
CompressionLevel, CompressionThreshold,
bedrock::{BEDROCK_GAME_PACKET, SubClient},
codec::var_uint::VarUInt,
ser::NetworkWriteExt,
};
// raw -> compress -> encrypt
pub enum EncryptionWriter<W: AsyncWrite + Unpin> {
Encrypt(Box<StreamEncryptor<W>>),
None(W),
}
impl<W: AsyncWrite + Unpin> EncryptionWriter<W> {
#[must_use]
pub fn upgrade(self, cipher: Aes128Cfb8Enc) -> Self {
match self {
Self::None(stream) => Self::Encrypt(Box::new(StreamEncryptor::new(cipher, stream))),
Self::Encrypt(_) => panic!("Cannot upgrade a stream that already has a cipher!"),
}
}
}
impl<W: AsyncWrite + Unpin> AsyncWrite for EncryptionWriter<W> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, io::Error>> {
match self.get_mut() {
Self::Encrypt(writer) => {
let writer = Pin::new(writer);
writer.poll_write(cx, buf)
}
Self::None(writer) => {
let writer = Pin::new(writer);
writer.poll_write(cx, buf)
}
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
match self.get_mut() {
Self::Encrypt(writer) => {
let writer = Pin::new(writer);
writer.poll_flush(cx)
}
Self::None(writer) => {
let writer = Pin::new(writer);
writer.poll_flush(cx)
}
}
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
match self.get_mut() {
Self::Encrypt(writer) => {
let writer = Pin::new(writer);
writer.poll_shutdown(cx)
}
Self::None(writer) => {
let writer = Pin::new(writer);
writer.poll_shutdown(cx)
}
}
}
}
use crate::bedrock::crypto::BedrockEncryptor;
/// Encoder: Server -> Client
/// Supports `ZLib` endecoding/compression
/// Supports Aes256 Encryption
pub struct UDPNetworkEncoder {
/// Supports Zlib compression.
pub struct BedrockBatchEncoder {
// compression and compression threshold
compression: Option<(CompressionThreshold, CompressionLevel)>,
encryptor: Option<BedrockEncryptor>,
}
impl Default for UDPNetworkEncoder {
impl Default for BedrockBatchEncoder {
fn default() -> Self {
Self::new()
}
}
impl UDPNetworkEncoder {
impl BedrockBatchEncoder {
#[must_use]
pub const fn new() -> Self {
Self {
compression: None,
encryptor: None,
}
Self { compression: None }
}
pub const fn set_compression(
@@ -108,21 +35,6 @@ impl UDPNetworkEncoder {
self.compression = Some(compression_info);
}
pub fn set_encryption(
&mut self,
key: &[u8; 32],
) -> Result<(), crate::bedrock::packet_decoder::EncryptionAlreadyEnabledError> {
if self.encryptor.is_some() {
return Err(crate::bedrock::packet_decoder::EncryptionAlreadyEnabledError);
}
self.encryptor = Some(BedrockEncryptor::new(key));
Ok(())
}
pub const fn encryptor_mut(&mut self) -> Option<&mut BedrockEncryptor> {
self.encryptor.as_mut()
}
pub fn write_game_packet(
&self,
packet_id: u16,
@@ -152,7 +64,7 @@ impl UDPNetworkEncoder {
// Handle Outer Container
writer
.write_u8(0xfe)
.write_u8(BEDROCK_GAME_PACKET)
.map_err(|e| Error::other(e.to_string()))?; // Bedrock Game Packet Header
let mut data_to_write = Vec::new();
@@ -174,26 +86,17 @@ impl UDPNetworkEncoder {
Ok(())
}
pub async fn write_packet(
&self,
packet_data: &[u8],
addr: SocketAddr,
socket: &UdpSocket,
) -> Result<(), Error> {
socket.send_to(packet_data, addr).await.map(|_| ())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bedrock::packet_decoder::UDPNetworkDecoder;
use crate::bedrock::packet_decoder::BedrockBatchDecoder;
use std::io::Cursor;
#[tokio::test]
async fn bedrock_compression_cycle() -> Result<(), Box<dyn std::error::Error>> {
let mut encoder = UDPNetworkEncoder::new();
let mut encoder = BedrockBatchEncoder::new();
encoder.set_compression((256, 6));
let packet_id = 1;
@@ -208,7 +111,7 @@ mod tests {
&mut encoded_buf,
)?;
let mut decoder = UDPNetworkDecoder::new();
let mut decoder = BedrockBatchDecoder::new();
decoder.set_compression(256);
let decompressed_payload = decoder.get_packet_payload(encoded_buf).await?;

View File

@@ -1,7 +0,0 @@
use pumpkin_macros::packet;
use crate::serial::PacketRead;
#[derive(PacketRead)]
#[packet(0x04)]
pub struct SClientToServerHandshake;

View File

@@ -2,7 +2,6 @@ pub mod actor_event;
pub mod animate;
pub mod block_pick_request;
pub mod client_cache_status;
pub mod client_to_server_handshake;
pub mod command_request;
pub mod container_close;
pub mod emote;
@@ -17,7 +16,6 @@ pub mod modal_form_response;
pub mod player_action;
pub mod player_auth_input;
pub mod player_hotbar;
pub mod raknet;
pub mod request_ability;
pub mod request_chunk_radius;
pub mod request_network_settings;
@@ -30,7 +28,6 @@ pub use actor_event::*;
pub use animate::*;
pub use block_pick_request::*;
pub use client_cache_status::*;
pub use client_to_server_handshake::*;
pub use command_request::*;
pub use container_close::*;
pub use emote::*;
@@ -45,7 +42,6 @@ pub use modal_form_response::*;
pub use player_action::{Action as PlayerActionType, SPlayerAction};
pub use player_auth_input::*;
pub use player_hotbar::*;
pub use raknet::*;
pub use request_ability::*;
pub use request_chunk_radius::*;
pub use request_network_settings::*;

View File

@@ -1,55 +0,0 @@
use std::net::SocketAddr;
use pumpkin_macros::packet;
use crate::serial::PacketRead;
/// Sent periodically by a connected client to measure round-trip time.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Connected_Ping>
#[derive(PacketRead)]
#[packet(0x00)]
pub struct SConnectedPing {
/// Time since start
#[serial(big_endian)]
pub time: u64,
}
/// Sent by the client after receiving `OpenConnectionReply2` to request formal session establishment.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Connection_Request>
#[derive(PacketRead)]
#[packet(0x09)]
pub struct SConnectionRequest {
#[serial(big_endian)]
pub client_guid: u64,
#[serial(big_endian)]
pub time: u64,
pub security: bool,
}
/// Sent by the client to confirm local network address and finish connection establishment.
///
/// Ref: <https://minecraft.wiki/w/RakNet#New_Incoming_Connection>
#[derive(PacketRead)]
#[packet(0x13)]
pub struct SNewIncomingConnection {
pub server_address: SocketAddr,
pub internal_address: SocketAddr,
#[serial(big_endian)]
pub ping_time: u64,
#[serial(big_endian)]
pub pong_time: u64,
}
/// Sent by the client to notify the server of graceful disconnection.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Disconnection_Notification>
#[packet(0x15)]
pub struct SDisconnect;
/// Internal notification signal for a connection lost due to socket error or timeout.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Disconnection_Notification>
#[packet(0x16)]
pub struct SConnectionLost;

View File

@@ -1,3 +0,0 @@
pub mod connection;
pub mod open_connection;
pub mod unconnected_ping;

View File

@@ -1,31 +0,0 @@
use std::net::SocketAddr;
use pumpkin_macros::packet;
use crate::serial::PacketRead;
/// Sent by a connecting client to initiate `RakNet` handshake and check server MTU size.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Open_Connection_Request_1>
#[derive(PacketRead)]
#[packet(0x05)]
pub struct SOpenConnectionRequest1 {
pub magic: [u8; 16],
pub protocol_version: u8,
#[serial(big_endian)]
pub mtu: u16,
}
/// Sent by a connecting client following `OpenConnectionReply1` to verify server address, client GUID, and MTU.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Open_Connection_Request_2>
#[derive(PacketRead)]
#[packet(0x07)]
pub struct SOpenConnectionRequest2 {
pub magic: [u8; 16],
pub server_address: SocketAddr,
#[serial(big_endian)]
pub mtu: u16,
#[serial(big_endian)]
pub client_guid: u64,
}

View File

@@ -1,29 +0,0 @@
use pumpkin_macros::packet;
use crate::serial::PacketRead;
/// Sent by an unconnected client to request server information, status, and MOTD.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Unconnected_Ping>
#[derive(PacketRead)]
#[packet(0x01)]
pub struct SUnconnectedPing {
#[serial(big_endian)]
pub time: u64,
pub magic: [u8; 16],
#[serial(big_endian)]
pub client_guid: u64,
}
/// Sent by a client to query server information when connections are open.
///
/// Ref: <https://minecraft.wiki/w/RakNet#Unconnected_Ping_Open_Connections>
#[derive(PacketRead)]
#[packet(0x02)]
pub struct SUnconnectedPingOpenConnections {
#[serial(big_endian)]
pub time: u64,
pub magic: [u8; 16],
#[serial(big_endian)]
pub client_guid: u64,
}

View File

@@ -0,0 +1,123 @@
use core::fmt;
use std::io::{Error, Write};
use pumpkin_macros::packet;
use crate::serial::{PacketRead, PacketWrite};
pub const OFFLINE_MESSAGE_MAGIC: [u8; 16] = [
0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, 0x56, 0x78,
];
#[derive(PacketRead)]
#[packet(0x01)]
pub struct SUnconnectedPing {
#[serial(big_endian)]
pub time: u64,
pub magic: [u8; 16],
#[serial(big_endian)]
pub client_guid: u64,
}
#[derive(PacketRead)]
#[packet(0x02)]
pub struct SUnconnectedPingOpenConnections {
#[serial(big_endian)]
pub time: u64,
pub magic: [u8; 16],
#[serial(big_endian)]
pub client_guid: u64,
}
#[packet(0x1c)]
pub struct CUnconnectedPong {
time: u64,
server_guid: u64,
magic: [u8; 16],
server_id: String,
}
impl CUnconnectedPong {
#[must_use]
pub const fn new(time: u64, server_guid: u64, server_id: String) -> Self {
Self {
time,
server_guid,
magic: OFFLINE_MESSAGE_MAGIC,
server_id,
}
}
}
impl PacketWrite for CUnconnectedPong {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.time.write_be(writer)?;
self.server_guid.write_be(writer)?;
writer.write_all(&self.magic)?;
let length = u16::try_from(self.server_id.len())
.map_err(|_| Error::other("Bedrock server advertisement is too long"))?;
writer.write_all(&length.to_be_bytes())?;
writer.write_all(self.server_id.as_bytes())
}
}
pub struct ServerInfo<'a> {
pub motd: &'a str,
pub protocol: u32,
pub version: &'static str,
pub players: i32,
pub max_players: u32,
pub server_guid: u64,
pub level_name: &'a str,
pub game_mode: &'static str,
pub game_mode_id: u32,
pub ipv4_port: u16,
pub ipv6_port: u16,
}
impl fmt::Display for ServerInfo<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"MCPE;{};{};{};{};{};{};{};{};{};{};{};0;",
self.motd,
self.protocol,
self.version,
self.players,
self.max_players,
self.server_guid,
self.level_name,
self.game_mode,
self.game_mode_id,
self.ipv4_port,
self.ipv6_port
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formats_vanilla_26_40_advertisement() {
let info = ServerInfo {
motd: "Pumpkin",
protocol: 2168,
version: "1.26.40",
players: 2,
max_players: 20,
server_guid: 42,
level_name: "world",
game_mode: "Creative",
game_mode_id: 1,
ipv4_port: 19132,
ipv6_port: 19133,
};
assert_eq!(
info.to_string(),
"MCPE;Pumpkin;2168;1.26.40;2;20;42;world;Creative;1;19132;19133;0;"
);
}
}

View File

@@ -24,7 +24,7 @@ proc-macro2 = { workspace = true, optional = true }
uuid.workspace = true
tokio = { workspace = true, features = ["sync"] }
base64.workspace = true
p384 = { workspace = true, features = ["ecdsa", "ecdh"] }
p384 = { workspace = true, features = ["ecdsa"] }
thiserror.workspace = true
ecdsa.workspace = true
rsa.workspace = true

View File

@@ -558,53 +558,3 @@ pub fn extract_cpk_from_token(token: &str) -> Result<PublicKey, AuthError> {
build_public_key_from_b64(cpk_b64)
}
/// Generates a signed Bedrock handshake JWT containing the server's public key and salt.
pub fn generate_handshake_jwt(
signing_key: &p384::ecdsa::SigningKey,
salt: &[u8],
) -> Result<String, AuthError> {
use p384::ecdsa::signature::Signer;
use p384::pkcs8::EncodePublicKey;
let public_key = p384::PublicKey::from(signing_key.verifying_key());
let der_bytes = public_key
.to_public_key_der()
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?;
let x5u = general_purpose::STANDARD.encode(der_bytes.as_bytes());
let salt_b64 = general_purpose::STANDARD_NO_PAD.encode(salt);
let header_json = serde_json::json!({
"alg": "ES384",
"x5u": x5u
});
let payload_json = serde_json::json!({
"salt": salt_b64
});
let header_b64 = general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header_json)?);
let payload_b64 = general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload_json)?);
let signing_input = format!("{header_b64}.{payload_b64}");
let signature: p384::ecdsa::Signature = signing_key.sign(signing_input.as_bytes());
let signature_bytes = signature.to_bytes();
let signature_b64 = general_purpose::URL_SAFE_NO_PAD.encode(signature_bytes);
Ok(format!("{signing_input}.{signature_b64}"))
}
/// Computes an ECDH shared secret using the server's P-384 signing key and the client's public key.
#[must_use]
pub fn compute_shared_secret(
signing_key: &p384::ecdsa::SigningKey,
client_public_key: &p384::PublicKey,
) -> [u8; 48] {
let secret = p384::SecretKey::from(signing_key);
let shared_secret =
p384::ecdh::diffie_hellman(secret.to_nonzero_scalar(), client_public_key.as_affine());
let mut secret_bytes = [0u8; 48];
secret_bytes.copy_from_slice(&shared_secret.raw_secret_bytes()[..]);
secret_bytes
}

View File

@@ -10,6 +10,7 @@ use crate::logging::{GzipRollingLogger, PumpkinCommandCompleter, ReadlineLogWrap
use crate::net::bedrock::{
BedrockClient,
nethernet::{NetherNetListener, load_or_create_identity_key},
status::StatusResponder,
};
use crate::net::java::JavaClient;
use crate::net::java::pending::PendingConnection;
@@ -25,14 +26,14 @@ use rustyline::Editor;
use rustyline::history::FileHistory;
use rustyline::{Config, error::ReadlineError};
use std::collections::HashMap;
use std::io::{Cursor, ErrorKind, IsTerminal, stdin};
use std::io::{ErrorKind, IsTerminal, stdin};
use std::process::exit;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use std::{net::SocketAddr, sync::LazyLock};
use tokio::net::{TcpListener, UdpSocket};
use tokio::net::TcpListener;
use tokio::select;
use tokio::sync::Mutex;
use tokio::time::sleep;
@@ -210,7 +211,7 @@ fn resolve_some<T: Future, D, F: FnOnce(D) -> T>(
pub struct PumpkinServer {
pub server: Arc<Server>,
pub tcp_listener: Option<TcpListener>,
pub udp_socket: Option<Arc<UdpSocket>>,
pub bedrock_status: Option<StatusResponder>,
pub nethernet_listener: Option<NetherNetListener>,
}
@@ -299,22 +300,13 @@ impl PumpkinServer {
});
};
let udp_socket = if server.advanced_config.networking.bedrock.enabled {
Some(Arc::new(
UdpSocket::bind(server.advanced_config.networking.bedrock.address)
.await
.expect("Failed to bind UDP Socket"),
))
} else {
None
};
let nethernet_listener = Self::bind_nethernet(&server).await;
let bedrock_status = Self::bind_bedrock_status(&server, nethernet_listener.is_some()).await;
Self {
server,
tcp_listener,
udp_socket,
bedrock_status,
nethernet_listener,
}
}
@@ -346,6 +338,21 @@ impl PumpkinServer {
)
}
async fn bind_bedrock_status(server: &Server, enabled: bool) -> Option<StatusResponder> {
if !enabled {
return None;
}
let responder =
StatusResponder::bind(server.advanced_config.networking.bedrock.nethernet.address)
.await
.expect("Failed to bind Bedrock server-list status");
let (ipv4, ipv6) = responder
.local_addrs()
.expect("Bedrock status sockets should have local addresses");
info!("Bedrock server-list status is listening on {ipv4} (IPv4) and {ipv6} (IPv6)");
Some(responder)
}
pub async fn init_plugins(&self) -> std::time::Duration {
match self.server.plugin_manager.load_plugins(&self.server).await {
Ok(duration) => duration,
@@ -468,8 +475,6 @@ impl PumpkinServer {
tasks: &Arc<TaskTracker>,
bedrock_clients: &Arc<Mutex<HashMap<SocketAddr, Arc<BedrockClient>>>>,
) -> bool {
let mut udp_buf = [0; 1496]; // Buffer for UDP receive
select! {
// Branch for TCP connections (Java Edition)
tcp_result = resolve_some(self.tcp_listener.as_ref(), tokio::net::TcpListener::accept) => {
@@ -542,68 +547,14 @@ impl PumpkinServer {
}
},
// Branch for UDP packets (Bedrock Edition)
udp_result = resolve_some(self.udp_socket.as_ref(), |sock: &Arc<UdpSocket>| sock.recv_from(&mut udp_buf)) => {
match udp_result {
Ok((len, client_addr)) => {
if len > 0 {
let Some(socket) = self.udp_socket.clone() else {
error!("UDP socket disappeared during receive");
return true;
};
let id = udp_buf[0];
let is_online = id & pumpkin_protocol::bedrock::RAKNET_VALID != 0;
if is_online {
let be_clients = bedrock_clients.clone();
let mut clients_guard = bedrock_clients.lock().await;
if clients_guard
.get(&client_addr)
.is_some_and(|client| client.is_closed())
{
clients_guard.remove(&client_addr);
}
let mut is_new = false;
let client = clients_guard.entry(client_addr).or_insert_with(|| {
is_new = true;
*master_client_id_counter += 1;
let new_client = Arc::new(BedrockClient::new(
socket,
client_addr,
be_clients
));
new_client.start_outgoing_packet_task();
new_client
}).clone();
if is_new {
self.spawn_bedrock_client_task(client.clone(), tasks);
}
let packet_bytes = udp_buf[..len].to_vec();
let server = self.server.clone();
tasks.spawn(async move {
client.process_packet(&server, packet_bytes.into()).await;
});
} else if let Some(sock) = self.udp_socket.as_ref() {
let _ = BedrockClient::handle_offline_packet(
&self.server,
id,
&mut Cursor::new(&udp_buf[1..len]),
client_addr,
sock,
bedrock_clients,
).await;
}
}
}
Err(e) => error!("UDP socket error: {e}"),
// Remote server-list status remains a RakNet unconnected ping/pong even
// when the game connection itself is negotiated over NetherNet.
status_result = resolve_some(
self.bedrock_status.as_ref(),
|status: &StatusResponder| status.receive(&self.server),
) => {
if let Err(error) = status_result {
debug!("Bedrock status packet failed: {error}");
}
},
@@ -612,7 +563,7 @@ impl PumpkinServer {
if let Some((session, client_addr)) = nethernet_result {
*master_client_id_counter += 1;
let be_clients = bedrock_clients.clone();
let client = Arc::new(BedrockClient::new_nethernet(
let client = Arc::new(BedrockClient::new(
session.clone(),
client_addr,
be_clients,

View File

@@ -149,9 +149,12 @@ async fn main() {
TextComponent::text("Bedrock Edition:")
.color_named(NamedColor::Gold)
.to_pretty_console(),
TextComponent::text(format!("{}", advanced_config.networking.bedrock.address))
.color_named(NamedColor::DarkBlue)
.to_pretty_console()
TextComponent::text(format!(
"{}",
advanced_config.networking.bedrock.nethernet.address
))
.color_named(NamedColor::DarkBlue)
.to_pretty_console()
)
} else {
TextComponent::text(String::new()).to_pretty_console()

View File

@@ -1,32 +0,0 @@
use std::time::UNIX_EPOCH;
use pumpkin_protocol::bedrock::{
RakReliability,
client::raknet::connection::CConnectedPong,
server::raknet::connection::{SConnectedPing, SNewIncomingConnection},
};
use crate::net::bedrock::BedrockClient;
impl BedrockClient {
pub const fn handle_new_incoming_connection(&self, _packet: &SNewIncomingConnection) {
// self.connection_state.store(ConnectionState::Login);
}
pub async fn handle_connected_ping(&self, packet: SConnectedPing) {
self.send_framed_packet(
&CConnectedPong::new(
packet.time,
UNIX_EPOCH.elapsed().unwrap().as_millis() as u64,
),
RakReliability::Unreliable,
)
.await;
// TODO Make this cleaner and handle it only with the ClientPlatform
// This would also help with potential deadlocks by preventing to lock the player
//self.player.lock().await.clone().map(async |player| {
// player.wait_for_keep_alive.store(false, Ordering::Relaxed);
// println!("ping procedet");
//});
}
}

View File

@@ -5,7 +5,6 @@ use crate::{
server::Server,
};
use arc_swap::ArcSwap;
use pumpkin_protocol::bedrock::client::handshake::CHandshake;
use pumpkin_protocol::bedrock::{
client::{
network_settings::CNetworkSettings, play_status::CPlayStatus,
@@ -21,10 +20,8 @@ use pumpkin_protocol::bedrock::{
use pumpkin_util::jwt::AuthError;
use pumpkin_util::version::BedrockMinecraftVersion;
use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
use rand::RngExt;
use serde::{Deserialize, de::Error};
use serde_repr::Deserialize_repr;
use sha2::{Digest, Sha256};
use std::sync::Arc;
use thiserror::Error;
use tracing::debug;
@@ -126,16 +123,15 @@ impl BedrockClient {
self: &Arc<Self>,
packet: SLogin,
server: &Server,
) -> Result<Option<PacketHandlerResult>, LoginError> {
) -> Result<PacketHandlerResult, LoginError> {
self.try_handle_login(packet, server).await
}
#[expect(clippy::too_many_lines)]
pub async fn try_handle_login(
self: &Arc<Self>,
packet: SLogin,
server: &Server,
) -> Result<Option<PacketHandlerResult>, LoginError> {
) -> Result<PacketHandlerResult, LoginError> {
let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?;
let player_data = if server.advanced_config.networking.bedrock.online_mode {
match auth_payload.authentication_type {
@@ -188,93 +184,14 @@ impl BedrockClient {
profile_actions: None,
};
if let Some(peer_public_key) = self.nethernet_public_key() {
let login_public_key = pumpkin_util::jwt::extract_cpk_from_token(&auth_payload.token)
.map_err(LoginError::ChainValidationFailed)?;
if peer_public_key != &login_public_key {
return Err(LoginError::ChainValidationFailed(
AuthError::PublicKeyBuild(
"NetherNet and Bedrock login identities do not match".into(),
),
));
}
}
if server.advanced_config.networking.bedrock.encryption && !self.is_nethernet() {
let client_public_key = pumpkin_util::jwt::extract_cpk_from_token(&auth_payload.token)
.map_err(LoginError::ChainValidationFailed)?;
let (server_private_key, salt) = {
let server_key_arc = server
.bedrock_private_key
.get_or_init(|| async {
let mut rng = rand::rng();
loop {
let mut private_key_bytes = [0u8; 48];
for b in &mut private_key_bytes {
*b = rng.random();
}
if let Ok(key) = pumpkin_util::p384::ecdsa::SigningKey::from_slice(
&private_key_bytes,
) {
break Arc::new(key);
}
}
})
.await
.clone();
let mut salt = [0u8; 16];
for b in &mut salt {
*b = rand::rng().random();
}
(server_key_arc, salt)
};
let handshake_jwt =
pumpkin_util::jwt::generate_handshake_jwt(&server_private_key, &salt)
.map_err(LoginError::ChainValidationFailed)?;
let handshake_packet = CHandshake::new(handshake_jwt);
self.send_game_packet(&handshake_packet).await;
let shared_secret =
pumpkin_util::jwt::compute_shared_secret(&server_private_key, &client_public_key);
let mut hasher = Sha256::new();
hasher.update(salt);
hasher.update(shared_secret);
let key_bytes: [u8; 32] = hasher.finalize().into();
self.network_reader
.lock()
.await
.set_encryption(&key_bytes)
.map_err(|_| {
LoginError::ChainValidationFailed(AuthError::PublicKeyBuild(
"encryption enable error".into(),
))
})?;
self.network_writer
.write()
.await
.set_encryption(&key_bytes)
.map_err(|_| {
LoginError::ChainValidationFailed(AuthError::PublicKeyBuild(
"encryption enable error".into(),
))
})?;
let new_config = PlayerConfig {
locale: client_data.language_code.clone(),
..Default::default()
};
self.client_data
.store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data))));
self.pending_profile
.store(std::sync::Arc::new(Some(std::sync::Arc::new((
profile, new_config,
)))));
return Ok(None);
let login_public_key = pumpkin_util::jwt::extract_cpk_from_token(&auth_payload.token)
.map_err(LoginError::ChainValidationFailed)?;
if self.nethernet_public_key() != &login_public_key {
return Err(LoginError::ChainValidationFailed(
AuthError::PublicKeyBuild(
"NetherNet and Bedrock login identities do not match".into(),
),
));
}
self.enqueue_packet_internal(&CPlayStatus::LoginSuccess)
@@ -318,7 +235,7 @@ impl BedrockClient {
self.client_data
.store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data))));
Ok(Some(PacketHandlerResult::ReadyToPlay(profile, new_config)))
Ok(PacketHandlerResult::ReadyToPlay(profile, new_config))
}
pub async fn handle_resource_pack_response(

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +0,0 @@
use std::net::SocketAddr;
use pumpkin_protocol::bedrock::{
MTU, RAKNET_PROTOCOL_VERSION,
client::raknet::{
incompatible_protocol::CIncompatibleProtocolVersion,
open_connection::{COpenConnectionReply1, COpenConnectionReply2},
},
server::raknet::open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2},
};
use tokio::net::UdpSocket;
use crate::{net::bedrock::BedrockClient, server::Server};
impl BedrockClient {
pub async fn handle_open_connection_1(
server: &Server,
packet: SOpenConnectionRequest1,
addr: SocketAddr,
socket: &UdpSocket,
) {
if packet.protocol_version != RAKNET_PROTOCOL_VERSION {
Self::send_offline_packet(
&CIncompatibleProtocolVersion::new(RAKNET_PROTOCOL_VERSION, server.server_guid),
addr,
socket,
)
.await;
return;
}
Self::send_offline_packet(
&COpenConnectionReply1::new(server.server_guid, false, MTU as u16),
addr,
socket,
)
.await;
}
pub async fn handle_open_connection_2(
server: &Server,
packet: SOpenConnectionRequest2,
addr: SocketAddr,
socket: &UdpSocket,
) {
Self::send_offline_packet(
&COpenConnectionReply2::new(server.server_guid, addr, packet.mtu, false),
addr,
socket,
)
.await;
}
}

View File

@@ -0,0 +1,136 @@
use std::{
io::{Cursor, Error},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
};
use pumpkin_protocol::{
BClientPacket,
bedrock::status::{
CUnconnectedPong, OFFLINE_MESSAGE_MAGIC, SUnconnectedPing, SUnconnectedPingOpenConnections,
ServerInfo,
},
packet::Packet,
serial::PacketRead,
};
use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
use tokio::net::UdpSocket;
use crate::server::Server;
pub struct StatusResponder {
ipv4: UdpSocket,
ipv6: UdpSocket,
ipv4_port: u16,
ipv6_port: u16,
}
impl StatusResponder {
pub async fn bind(address: SocketAddr) -> Result<Self, Error> {
let ipv4_ip = match address.ip() {
IpAddr::V4(ip) => ip,
IpAddr::V6(_) => Ipv4Addr::UNSPECIFIED,
};
let ipv4_port = address.port();
let ipv6_port = ipv4_port.saturating_add(1);
Ok(Self {
ipv4: UdpSocket::bind((ipv4_ip, ipv4_port)).await?,
ipv6: UdpSocket::bind((Ipv6Addr::UNSPECIFIED, ipv6_port)).await?,
ipv4_port,
ipv6_port,
})
}
pub fn local_addrs(&self) -> Result<(SocketAddr, SocketAddr), Error> {
Ok((self.ipv4.local_addr()?, self.ipv6.local_addr()?))
}
pub async fn receive(&self, server: &Server) -> Result<(), Error> {
let mut ipv4_buffer = [0; 64];
let mut ipv6_buffer = [0; 64];
tokio::select! {
result = self.ipv4.recv_from(&mut ipv4_buffer) => {
let (length, client) = result?;
self.respond(server, &self.ipv4, &ipv4_buffer[..length], client).await
}
result = self.ipv6.recv_from(&mut ipv6_buffer) => {
let (length, client) = result?;
self.respond(server, &self.ipv6, &ipv6_buffer[..length], client).await
}
}
}
async fn respond(
&self,
server: &Server,
socket: &UdpSocket,
packet: &[u8],
client: SocketAddr,
) -> Result<(), Error> {
let Some((&packet_id, payload)) = packet.split_first() else {
return Ok(());
};
handle_packet(
server,
packet_id,
payload,
client,
socket,
self.ipv4_port,
self.ipv6_port,
)
.await
}
}
pub async fn handle_packet(
server: &Server,
packet_id: u8,
payload: &[u8],
client: SocketAddr,
socket: &UdpSocket,
ipv4_port: u16,
ipv6_port: u16,
) -> Result<(), Error> {
let (time, magic) = match i32::from(packet_id) {
SUnconnectedPing::PACKET_ID => {
let packet = SUnconnectedPing::read(&mut Cursor::new(payload))?;
(packet.time, packet.magic)
}
SUnconnectedPingOpenConnections::PACKET_ID => {
let packet = SUnconnectedPingOpenConnections::read(&mut Cursor::new(payload))?;
(packet.time, packet.magic)
}
_ => return Ok(()),
};
if magic != OFFLINE_MESSAGE_MAGIC {
return Ok(());
}
let players = server
.get_status()
.lock()
.await
.status_response
.players
.as_ref()
.map_or(0, |players| players.online) as i32;
let game_mode = server.defaultgamemode.lock().await.gamemode;
let info = ServerInfo {
motd: &server.advanced_config.networking.bedrock.motd,
protocol: CURRENT_BEDROCK_MC_PROTOCOL,
version: CURRENT_BEDROCK_MC_VERSION,
players,
max_players: server.advanced_config.networking.bedrock.max_players,
server_guid: server.server_guid,
level_name: &server.basic_config.default_level_name,
game_mode: game_mode.to_str(),
game_mode_id: 1,
ipv4_port,
ipv6_port,
};
let pong = CUnconnectedPong::new(time, server.server_guid, info.to_string());
let mut response = vec![CUnconnectedPong::PACKET_ID as u8];
pong.write_packet(&mut response)?;
socket.send_to(&response, client).await?;
Ok(())
}

View File

@@ -1,58 +0,0 @@
use std::net::SocketAddr;
use pumpkin_protocol::bedrock::{
client::raknet::unconnected_pong::{CUnconnectedPong, ServerInfo},
server::raknet::unconnected_ping::SUnconnectedPing,
};
use tokio::net::UdpSocket;
use crate::{net::bedrock::BedrockClient, server::Server};
use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
impl BedrockClient {
pub async fn handle_unconnected_ping(
server: &Server,
packet: SUnconnectedPing,
addr: SocketAddr,
socket: &UdpSocket,
) {
// TODO
let player_count = server
.get_status()
.lock()
.await
.status_response
.players
.as_ref()
.unwrap()
.online as _;
let motd_string = ServerInfo {
edition: "MCPE",
// TODO The default motd is to long to be displayed completely
motd_line_1: server.advanced_config.networking.bedrock.motd.clone(),
protocol_version: CURRENT_BEDROCK_MC_PROTOCOL,
version_name: CURRENT_BEDROCK_MC_VERSION,
player_count,
// A large number looks wreird on the client worlds window
max_player_count: server.advanced_config.networking.bedrock.max_players,
server_unique_id: server.server_guid,
motd_line_2: server.basic_config.default_level_name.clone(),
game_mode: server.defaultgamemode.lock().await.gamemode.to_str(),
game_mode_numeric: 1,
port_ipv4: 19132,
port_ipv6: 19133,
};
Self::send_offline_packet(
&CUnconnectedPong::new(
packet.time,
server.server_guid,
packet.magic,
format!("{motd_string}"),
),
addr,
socket,
)
.await;
}
}