From ffcf09ece4f74efa74211700845d2d3600da85b6 Mon Sep 17 00:00:00 2001 From: ZlordHUN <137647604+ZlordHUN@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:00:41 +0200 Subject: [PATCH] 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 --- Cargo.lock | 30 +- Cargo.toml | 3 +- crates/pumpkin-config/src/lib.rs | 7 - .../pumpkin-config/src/networking/bedrock.rs | 6 - crates/pumpkin-protocol/Cargo.toml | 2 - .../fuzz/fuzz_targets/decoder_bedrock.rs | 28 +- .../fuzz/fuzz_targets/encoder_bedrock.rs | 4 +- crates/pumpkin-protocol/src/bedrock/ack.rs | 85 -- .../src/bedrock/client/handshake.rs | 15 - .../src/bedrock/client/mod.rs | 4 - .../src/bedrock/client/raknet/connection.rs | 141 --- .../client/raknet/incompatible_protocol.rs | 24 - .../src/bedrock/client/raknet/mod.rs | 4 - .../bedrock/client/raknet/open_connection.rs | 63 -- .../bedrock/client/raknet/unconnected_pong.rs | 76 -- crates/pumpkin-protocol/src/bedrock/crypto.rs | 121 --- .../pumpkin-protocol/src/bedrock/frame_set.rs | 150 --- crates/pumpkin-protocol/src/bedrock/mod.rs | 103 +-- .../src/bedrock/packet_decoder.rs | 101 +- .../src/bedrock/packet_encoder.rs | 125 +-- .../server/client_to_server_handshake.rs | 7 - .../src/bedrock/server/mod.rs | 4 - .../src/bedrock/server/raknet/connection.rs | 55 -- .../src/bedrock/server/raknet/mod.rs | 3 - .../bedrock/server/raknet/open_connection.rs | 31 - .../bedrock/server/raknet/unconnected_ping.rs | 29 - crates/pumpkin-protocol/src/bedrock/status.rs | 123 +++ crates/pumpkin-util/Cargo.toml | 2 +- crates/pumpkin-util/src/jwt/mod.rs | 50 - crates/pumpkin/src/lib.rs | 109 +-- crates/pumpkin/src/main.rs | 9 +- crates/pumpkin/src/net/bedrock/connection.rs | 32 - crates/pumpkin/src/net/bedrock/login.rs | 105 +-- crates/pumpkin/src/net/bedrock/mod.rs | 874 ++---------------- .../src/net/bedrock/open_connection.rs | 52 -- crates/pumpkin/src/net/bedrock/status.rs | 136 +++ crates/pumpkin/src/net/bedrock/unconnected.rs | 58 -- 37 files changed, 413 insertions(+), 2358 deletions(-) delete mode 100644 crates/pumpkin-protocol/src/bedrock/ack.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/client/handshake.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/client/raknet/connection.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/client/raknet/incompatible_protocol.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/client/raknet/mod.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/client/raknet/open_connection.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/crypto.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/frame_set.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/server/client_to_server_handshake.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/server/raknet/connection.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/server/raknet/mod.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/server/raknet/open_connection.rs delete mode 100644 crates/pumpkin-protocol/src/bedrock/server/raknet/unconnected_ping.rs create mode 100644 crates/pumpkin-protocol/src/bedrock/status.rs delete mode 100644 crates/pumpkin/src/net/bedrock/connection.rs delete mode 100644 crates/pumpkin/src/net/bedrock/open_connection.rs create mode 100644 crates/pumpkin/src/net/bedrock/status.rs delete mode 100644 crates/pumpkin/src/net/bedrock/unconnected.rs diff --git a/Cargo.lock b/Cargo.lock index 9278436e5..35693f13d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index f94501faa..14a5d9911 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/crates/pumpkin-config/src/lib.rs b/crates/pumpkin-config/src/lib.rs index b53f3e316..5fad1cb2b 100644 --- a/crates/pumpkin-config/src/lib.rs +++ b/crates/pumpkin-config/src/lib.rs @@ -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, diff --git a/crates/pumpkin-config/src/networking/bedrock.rs b/crates/pumpkin-config/src/networking/bedrock.rs index a9fb56e10..93cb9f4c6 100644 --- a/crates/pumpkin-config/src/networking/bedrock.rs +++ b/crates/pumpkin-config/src/networking/bedrock.rs @@ -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(), diff --git a/crates/pumpkin-protocol/Cargo.toml b/crates/pumpkin-protocol/Cargo.toml index 828538c10..b943aa550 100644 --- a/crates/pumpkin-protocol/Cargo.toml +++ b/crates/pumpkin-protocol/Cargo.toml @@ -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 diff --git a/crates/pumpkin-protocol/fuzz/fuzz_targets/decoder_bedrock.rs b/crates/pumpkin-protocol/fuzz/fuzz_targets/decoder_bedrock.rs index 46f346b54..66525c458 100644 --- a/crates/pumpkin-protocol/fuzz/fuzz_targets/decoder_bedrock.rs +++ b/crates/pumpkin-protocol/fuzz/fuzz_targets/decoder_bedrock.rs @@ -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) { diff --git a/crates/pumpkin-protocol/fuzz/fuzz_targets/encoder_bedrock.rs b/crates/pumpkin-protocol/fuzz/fuzz_targets/encoder_bedrock.rs index 6a3bb527f..b35292f1f 100644 --- a/crates/pumpkin-protocol/fuzz/fuzz_targets/encoder_bedrock.rs +++ b/crates/pumpkin-protocol/fuzz/fuzz_targets/encoder_bedrock.rs @@ -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)); diff --git a/crates/pumpkin-protocol/src/bedrock/ack.rs b/crates/pumpkin-protocol/src/bedrock/ack.rs deleted file mode 100644 index 9e14ef474..000000000 --- a/crates/pumpkin-protocol/src/bedrock/ack.rs +++ /dev/null @@ -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, -} - -impl Acknowledge { - #[must_use] - pub const fn new(sequences: Vec) -> Self { - Self { sequences } - } - - fn write_range(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(reader: &mut R) -> Result { - 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(&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) - } -} diff --git a/crates/pumpkin-protocol/src/bedrock/client/handshake.rs b/crates/pumpkin-protocol/src/bedrock/client/handshake.rs deleted file mode 100644 index a6198f686..000000000 --- a/crates/pumpkin-protocol/src/bedrock/client/handshake.rs +++ /dev/null @@ -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 } - } -} diff --git a/crates/pumpkin-protocol/src/bedrock/client/mod.rs b/crates/pumpkin-protocol/src/bedrock/client/mod.rs index a82548b44..18785ccba 100644 --- a/crates/pumpkin-protocol/src/bedrock/client/mod.rs +++ b/crates/pumpkin-protocol/src/bedrock/client/mod.rs @@ -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::*; diff --git a/crates/pumpkin-protocol/src/bedrock/client/raknet/connection.rs b/crates/pumpkin-protocol/src/bedrock/client/raknet/connection.rs deleted file mode 100644 index cb85cf598..000000000 --- a/crates/pumpkin-protocol/src/bedrock/client/raknet/connection.rs +++ /dev/null @@ -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: -#[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: -#[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: -#[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: -#[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: -#[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: -#[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: -#[derive(PacketWrite)] -#[packet(0x15)] -pub struct CDisconnect; diff --git a/crates/pumpkin-protocol/src/bedrock/client/raknet/incompatible_protocol.rs b/crates/pumpkin-protocol/src/bedrock/client/raknet/incompatible_protocol.rs deleted file mode 100644 index 8af478cb9..000000000 --- a/crates/pumpkin-protocol/src/bedrock/client/raknet/incompatible_protocol.rs +++ /dev/null @@ -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: -#[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, - } - } -} diff --git a/crates/pumpkin-protocol/src/bedrock/client/raknet/mod.rs b/crates/pumpkin-protocol/src/bedrock/client/raknet/mod.rs deleted file mode 100644 index c75e08f73..000000000 --- a/crates/pumpkin-protocol/src/bedrock/client/raknet/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod connection; -pub mod incompatible_protocol; -pub mod open_connection; -pub mod unconnected_pong; diff --git a/crates/pumpkin-protocol/src/bedrock/client/raknet/open_connection.rs b/crates/pumpkin-protocol/src/bedrock/client/raknet/open_connection.rs deleted file mode 100644 index 7a22d5fcd..000000000 --- a/crates/pumpkin-protocol/src/bedrock/client/raknet/open_connection.rs +++ /dev/null @@ -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: -#[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: -#[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, - } - } -} diff --git a/crates/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs b/crates/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs deleted file mode 100644 index 937d58f17..000000000 --- a/crates/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs +++ /dev/null @@ -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: -#[packet(0x1c)] -pub struct CUnconnectedPong { - time: u64, - server_guid: u64, - magic: [u8; 16], - server_id: String, -} - -impl PacketWrite for CUnconnectedPong { - fn 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, - } - } -} diff --git a/crates/pumpkin-protocol/src/bedrock/crypto.rs b/crates/pumpkin-protocol/src/bedrock/crypto.rs deleted file mode 100644 index ab0ee0ead..000000000 --- a/crates/pumpkin-protocol/src/bedrock/crypto.rs +++ /dev/null @@ -1,121 +0,0 @@ -use aes::Aes256; -use ctr::Ctr128BE; -use ctr::cipher::{KeyIvInit, StreamCipher}; -use sha2::{Digest, Sha256}; - -type BedrockCtr = Ctr128BE; - -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) { - // 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) -> 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); - } -} diff --git a/crates/pumpkin-protocol/src/bedrock/frame_set.rs b/crates/pumpkin-protocol/src/bedrock/frame_set.rs deleted file mode 100644 index 807a12e5b..000000000 --- a/crates/pumpkin-protocol/src/bedrock/frame_set.rs +++ /dev/null @@ -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, -} - -impl FrameSet { - pub fn read(reader: &mut R) -> Result { - Ok(Self { - sequence: u24::read(reader)?, - frames: Frame::read(reader)?, - }) - } - - pub fn write_packet_data(&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, - 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) -> 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(reader: &mut R) -> Result, 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(&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) - } -} diff --git a/crates/pumpkin-protocol/src/bedrock/mod.rs b/crates/pumpkin-protocol/src/bedrock/mod.rs index addd04357..0d47fdf31 100644 --- a/crates/pumpkin-protocol/src/bedrock/mod.rs +++ b/crates/pumpkin-protocol/src/bedrock/mod.rs @@ -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 { - 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 { diff --git a/crates/pumpkin-protocol/src/bedrock/packet_decoder.rs b/crates/pumpkin-protocol/src/bedrock/packet_decoder.rs index e87c0afe6..c09c983d8 100644 --- a/crates/pumpkin-protocol/src/bedrock/packet_decoder.rs +++ b/crates/pumpkin-protocol/src/bedrock/packet_decoder.rs @@ -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 { - Decrypt(Box>), - None(R), -} - -impl DecryptionReader { - #[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 AsyncRead for DecryptionReader { - #[inline] - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - 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, - decryptor: Option, } -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, @@ -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) diff --git a/crates/pumpkin-protocol/src/bedrock/packet_encoder.rs b/crates/pumpkin-protocol/src/bedrock/packet_encoder.rs index 1410ee420..1e1bd5c2b 100644 --- a/crates/pumpkin-protocol/src/bedrock/packet_encoder.rs +++ b/crates/pumpkin-protocol/src/bedrock/packet_encoder.rs @@ -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 { - Encrypt(Box>), - None(W), -} - -impl EncryptionWriter { - #[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 AsyncWrite for EncryptionWriter { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - 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> { - 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> { - 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, } -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> { - 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?; diff --git a/crates/pumpkin-protocol/src/bedrock/server/client_to_server_handshake.rs b/crates/pumpkin-protocol/src/bedrock/server/client_to_server_handshake.rs deleted file mode 100644 index d074b63fa..000000000 --- a/crates/pumpkin-protocol/src/bedrock/server/client_to_server_handshake.rs +++ /dev/null @@ -1,7 +0,0 @@ -use pumpkin_macros::packet; - -use crate::serial::PacketRead; - -#[derive(PacketRead)] -#[packet(0x04)] -pub struct SClientToServerHandshake; diff --git a/crates/pumpkin-protocol/src/bedrock/server/mod.rs b/crates/pumpkin-protocol/src/bedrock/server/mod.rs index f109434de..4733f8fb7 100644 --- a/crates/pumpkin-protocol/src/bedrock/server/mod.rs +++ b/crates/pumpkin-protocol/src/bedrock/server/mod.rs @@ -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::*; diff --git a/crates/pumpkin-protocol/src/bedrock/server/raknet/connection.rs b/crates/pumpkin-protocol/src/bedrock/server/raknet/connection.rs deleted file mode 100644 index 1b1ee2cbd..000000000 --- a/crates/pumpkin-protocol/src/bedrock/server/raknet/connection.rs +++ /dev/null @@ -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: -#[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: -#[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: -#[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: -#[packet(0x15)] -pub struct SDisconnect; - -/// Internal notification signal for a connection lost due to socket error or timeout. -/// -/// Ref: -#[packet(0x16)] -pub struct SConnectionLost; diff --git a/crates/pumpkin-protocol/src/bedrock/server/raknet/mod.rs b/crates/pumpkin-protocol/src/bedrock/server/raknet/mod.rs deleted file mode 100644 index b729d3e9d..000000000 --- a/crates/pumpkin-protocol/src/bedrock/server/raknet/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod connection; -pub mod open_connection; -pub mod unconnected_ping; diff --git a/crates/pumpkin-protocol/src/bedrock/server/raknet/open_connection.rs b/crates/pumpkin-protocol/src/bedrock/server/raknet/open_connection.rs deleted file mode 100644 index 6927d6c66..000000000 --- a/crates/pumpkin-protocol/src/bedrock/server/raknet/open_connection.rs +++ /dev/null @@ -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: -#[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: -#[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, -} diff --git a/crates/pumpkin-protocol/src/bedrock/server/raknet/unconnected_ping.rs b/crates/pumpkin-protocol/src/bedrock/server/raknet/unconnected_ping.rs deleted file mode 100644 index 3429bb6e3..000000000 --- a/crates/pumpkin-protocol/src/bedrock/server/raknet/unconnected_ping.rs +++ /dev/null @@ -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: -#[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: -#[derive(PacketRead)] -#[packet(0x02)] -pub struct SUnconnectedPingOpenConnections { - #[serial(big_endian)] - pub time: u64, - pub magic: [u8; 16], - #[serial(big_endian)] - pub client_guid: u64, -} diff --git a/crates/pumpkin-protocol/src/bedrock/status.rs b/crates/pumpkin-protocol/src/bedrock/status.rs new file mode 100644 index 000000000..36325c0e7 --- /dev/null +++ b/crates/pumpkin-protocol/src/bedrock/status.rs @@ -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(&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;" + ); + } +} diff --git a/crates/pumpkin-util/Cargo.toml b/crates/pumpkin-util/Cargo.toml index 70295d08b..69f8e49c3 100644 --- a/crates/pumpkin-util/Cargo.toml +++ b/crates/pumpkin-util/Cargo.toml @@ -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 diff --git a/crates/pumpkin-util/src/jwt/mod.rs b/crates/pumpkin-util/src/jwt/mod.rs index 0b85f4305..5ee498903 100644 --- a/crates/pumpkin-util/src/jwt/mod.rs +++ b/crates/pumpkin-util/src/jwt/mod.rs @@ -558,53 +558,3 @@ pub fn extract_cpk_from_token(token: &str) -> Result { 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 { - 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 -} diff --git a/crates/pumpkin/src/lib.rs b/crates/pumpkin/src/lib.rs index 2397957d0..2b9d605bf 100644 --- a/crates/pumpkin/src/lib.rs +++ b/crates/pumpkin/src/lib.rs @@ -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>( pub struct PumpkinServer { pub server: Arc, pub tcp_listener: Option, - pub udp_socket: Option>, + pub bedrock_status: Option, pub nethernet_listener: Option, } @@ -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 { + 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, bedrock_clients: &Arc>>>, ) -> 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| 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, diff --git a/crates/pumpkin/src/main.rs b/crates/pumpkin/src/main.rs index 992335540..dc7209058 100644 --- a/crates/pumpkin/src/main.rs +++ b/crates/pumpkin/src/main.rs @@ -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() diff --git a/crates/pumpkin/src/net/bedrock/connection.rs b/crates/pumpkin/src/net/bedrock/connection.rs deleted file mode 100644 index 4c6bcf06d..000000000 --- a/crates/pumpkin/src/net/bedrock/connection.rs +++ /dev/null @@ -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"); - //}); - } -} diff --git a/crates/pumpkin/src/net/bedrock/login.rs b/crates/pumpkin/src/net/bedrock/login.rs index 102598298..4706a6a9e 100644 --- a/crates/pumpkin/src/net/bedrock/login.rs +++ b/crates/pumpkin/src/net/bedrock/login.rs @@ -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, packet: SLogin, server: &Server, - ) -> Result, LoginError> { + ) -> Result { self.try_handle_login(packet, server).await } - #[expect(clippy::too_many_lines)] pub async fn try_handle_login( self: &Arc, packet: SLogin, server: &Server, - ) -> Result, LoginError> { + ) -> Result { 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( diff --git a/crates/pumpkin/src/net/bedrock/mod.rs b/crates/pumpkin/src/net/bedrock/mod.rs index c2341ad49..8c45d77a4 100644 --- a/crates/pumpkin/src/net/bedrock/mod.rs +++ b/crates/pumpkin/src/net/bedrock/mod.rs @@ -1,15 +1,15 @@ pub mod nethernet; pub mod play; +pub mod status; use crossbeam::atomic::AtomicCell; use std::{ - collections::{BTreeMap, HashMap, HashSet}, + collections::HashMap, io::{Cursor, Error, Write}, - net::{Ipv4Addr, SocketAddrV4}, + net::SocketAddr, sync::{ Arc, - atomic::{AtomicBool, AtomicU16, AtomicU32, Ordering}, + atomic::{AtomicBool, AtomicU32}, }, - time::UNIX_EPOCH, }; use tracing::{debug, error, warn}; @@ -19,61 +19,28 @@ use pumpkin_config::networking::compression::CompressionInfo; use pumpkin_protocol::{ BClientPacket, PacketDecodeError, RawPacket, bedrock::{ - MTU, RAKNET_ACK, RAKNET_GAME_PACKET, RAKNET_NACK, RAKNET_VALID, RakReliability, - SPLIT_FRAME_MAX_CONTENT, SubClient, UDP_HEADER_SIZE, - ack::Acknowledge, - client::{ - disconnect_player::CDisconnectPlayer, - level_chunk::CLevelChunk, - play_status::CPlayStatus, - raknet::connection::{ - CAlreadyConnected, CConnectionBanned, CConnectionRequestAccepted, CDisconnect, - CNoFreeIncomingConnections, - }, - resource_packs_info::{CResourcePacksInfo, ResourcePackEntry}, - }, - frame_set::{Frame, FrameSet}, - packet_decoder::UDPNetworkDecoder, - packet_encoder::UDPNetworkEncoder, + BEDROCK_GAME_PACKET, SubClient, + client::{disconnect_player::CDisconnectPlayer, level_chunk::CLevelChunk}, + packet_decoder::BedrockBatchDecoder, + packet_encoder::BedrockBatchEncoder, server::{ - animate::SAnimate, - block_pick_request::SBlockPickRequest, - client_cache_status::SClientCacheStatus, - client_to_server_handshake::SClientToServerHandshake, - command_request::SCommandRequest, - container_close::SContainerClose, - emote::SEmote, - interaction::SInteraction, - inventory_transaction::SInventoryTransaction, - loading_screen::SLoadingScreen, - login::SLogin, - mob_equipment::SMobEquipment, - player_action::SPlayerAction, - player_auth_input::SPlayerAuthInput, - raknet::{ - connection::{ - SConnectedPing, SConnectionLost, SConnectionRequest, SDisconnect, - SNewIncomingConnection, - }, - open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2}, - unconnected_ping::{SUnconnectedPing, SUnconnectedPingOpenConnections}, - }, - request_ability::SRequestAbility, + animate::SAnimate, block_pick_request::SBlockPickRequest, + client_cache_status::SClientCacheStatus, command_request::SCommandRequest, + container_close::SContainerClose, emote::SEmote, interaction::SInteraction, + inventory_transaction::SInventoryTransaction, loading_screen::SLoadingScreen, + login::SLogin, mob_equipment::SMobEquipment, player_action::SPlayerAction, + player_auth_input::SPlayerAuthInput, request_ability::SRequestAbility, request_chunk_radius::SRequestChunkRadius, request_network_settings::SRequestNetworkSettings, resource_pack_response::SResourcePackResponse, set_local_player_as_initialized::SSetLocalPlayerAsInitialized, - set_player_inventory_options::SSetPlayerInventoryOptions, - text::SText, + set_player_inventory_options::SSetPlayerInventoryOptions, text::SText, }, }, - codec::u24, packet::Packet, serial::{PacketRead, PacketReadSlice}, }; -use std::net::SocketAddr; use tokio::{ - net::UdpSocket, sync::mpsc::{Receiver, Sender}, sync::{Mutex, RwLock, oneshot}, task::JoinHandle, @@ -81,14 +48,11 @@ use tokio::{ use tokio_util::{sync::CancellationToken, task::TaskTracker}; -pub mod connection; pub mod login; -pub mod open_connection; -pub mod unconnected; use self::nethernet::NetherNetSession; use crate::{ entity::player::Player, - net::{DisconnectReason, GameProfile, PacketHandlerResult, PlayerConfig}, + net::{DisconnectReason, PacketHandlerResult}, plugin::api::events::world::chunk_send::ChunkSend, server::Server, }; @@ -118,13 +82,8 @@ impl OutgoingPacket { } } -enum BedrockTransport { - RakNet(Arc), - NetherNet(Arc), -} - pub struct BedrockClient { - transport: BedrockTransport, + session: Arc, /// The client's IP address. pub address: SocketAddr, pub player: ArcSwap>>, @@ -144,101 +103,52 @@ pub struct BedrockClient { outgoing_packet_priority_recv: Mutex>>, /// The packet encoder for outgoing packets. - network_writer: Arc>, + network_writer: Arc>, /// The packet decoder for incoming packets. - network_reader: Mutex, + network_reader: Mutex, - _use_frame_sets: AtomicBool, - output_sequence_number: AtomicU32, - output_reliable_number: AtomicU32, - output_split_number: AtomicU16, - output_sequenced_index: AtomicU32, - output_ordered_index: AtomicU32, /// The next form ID to use for custom forms. pub next_form_id: AtomicU32, pub inventory_opened: AtomicBool, /// An notifier that is triggered when this client is closed. close_token: CancellationToken, last_seen: Arc>, - /// Store Fragments until the packet is complete - compounds: Arc>>>>, - //input_sequence_number: AtomicU32, - received_sequences: Mutex>, - pending_acks: Mutex>, - #[allow(clippy::type_complexity)] - unacked_outgoing_frames: Mutex, std::time::Instant)>>, - expected_order_index: Mutex>, - highest_sequence_index: Mutex>, - ordered_queues: Mutex>>, incoming_game_packet_send: Sender, incoming_game_packet_recv: Mutex>>, - pending_profile: ArcSwap>>, } impl BedrockClient { #[must_use] pub fn new( - socket: Arc, - address: SocketAddr, - be_clients: Arc>>>, - ) -> Self { - Self::with_transport(BedrockTransport::RakNet(socket), address, be_clients) - } - - #[must_use] - pub fn new_nethernet( session: Arc, address: SocketAddr, be_clients: Arc>>>, - ) -> Self { - Self::with_transport(BedrockTransport::NetherNet(session), address, be_clients) - } - - fn with_transport( - transport: BedrockTransport, - address: SocketAddr, - be_clients: Arc>>>, ) -> Self { let (send, recv) = tokio::sync::mpsc::channel(4096); let (priority_send, priority_recv) = tokio::sync::mpsc::channel(4096); let (incoming_send, incoming_recv) = tokio::sync::mpsc::channel(4096); let rt_handle = tokio::runtime::Handle::current(); Self { - transport, + session, player: ArcSwap::new(Arc::new(None)), address, version: AtomicCell::new(BedrockMinecraftVersion::Unknown), client_data: ArcSwap::new(Arc::new(None)), be_clients, - network_writer: Arc::new(RwLock::new(UDPNetworkEncoder::new())), - network_reader: Mutex::new(UDPNetworkDecoder::new()), + network_writer: Arc::new(RwLock::new(BedrockBatchEncoder::new())), + network_reader: Mutex::new(BedrockBatchDecoder::new()), tasks: TaskTracker::new(), rt_handle, outgoing_packet_queue_send: send, outgoing_packet_queue_recv: Mutex::new(Some(recv)), outgoing_packet_priority_send: priority_send, outgoing_packet_priority_recv: Mutex::new(Some(priority_recv)), - _use_frame_sets: AtomicBool::new(false), - output_sequence_number: AtomicU32::new(0), - output_reliable_number: AtomicU32::new(0), - output_split_number: AtomicU16::new(0), - output_sequenced_index: AtomicU32::new(0), - output_ordered_index: AtomicU32::new(0), next_form_id: AtomicU32::new(0), inventory_opened: AtomicBool::new(false), - compounds: Arc::new(Mutex::new(HashMap::new())), close_token: CancellationToken::new(), last_seen: Arc::new(AtomicCell::new(std::time::Instant::now())), - received_sequences: Mutex::new(HashSet::new()), - pending_acks: Mutex::new(Vec::new()), - unacked_outgoing_frames: Mutex::new(HashMap::new()), - expected_order_index: Mutex::new(HashMap::new()), - highest_sequence_index: Mutex::new(HashMap::new()), - ordered_queues: Mutex::new(HashMap::new()), - //input_sequence_number: AtomicU32::new(0), incoming_game_packet_send: incoming_send, incoming_game_packet_recv: Mutex::new(Some(incoming_recv)), - pending_profile: ArcSwap::new(Arc::new(None)), } } @@ -269,7 +179,7 @@ impl BedrockClient { let mut interval = tokio::time::interval(std::time::Duration::from_millis(100)); while !client.close_token.is_cancelled() { - let mut packet = tokio::select! { + let packet = tokio::select! { biased; () = client.close_token.cancelled() => break, res = priority_packet_receiver.recv() => match res { @@ -277,7 +187,7 @@ impl BedrockClient { None => break, }, _ = interval.tick() => { - if !client.tick_outgoing_transport().await { + if !client.tick_connection().await { break; } continue; @@ -288,42 +198,17 @@ impl BedrockClient { }, }; - // NetherNet is already encrypted by DTLS and must not use Minecraft encryption. - if !client.is_nethernet() && packet.data.len() > 1 && packet.data[0] == 0xfe { - let mut encoder = client.network_writer.write().await; - if let Some(encryptor) = encoder.encryptor_mut() { - let mut data_to_encrypt = packet.data[1..].to_vec(); - encryptor.encrypt(&mut data_to_encrypt); - let mut encrypted_payload = Vec::with_capacity(1 + data_to_encrypt.len()); - encrypted_payload.push(0xfe); - encrypted_payload.extend_from_slice(&data_to_encrypt); - packet.data = encrypted_payload.into(); - } - } - - match &client.transport { - BedrockTransport::RakNet(_) => { - client - .send_framed_packet_data( - packet.data.to_vec(), - RakReliability::ReliableOrdered, - ) - .await; - } - BedrockTransport::NetherNet(session) => { - let data = packet.data.strip_prefix(&[RAKNET_GAME_PACKET as u8]); - let Some(data) = data else { - warn!("Refusing to send a non-game packet over NetherNet"); - continue; - }; - if let Err(error) = session.send(Bytes::copy_from_slice(data)).await { - warn!( - "Failed to send NetherNet packet to {}: {error}", - client.address - ); - client.close().await; - } - } + let data = packet.data.strip_prefix(&[BEDROCK_GAME_PACKET]); + let Some(data) = data else { + warn!("Refusing to send a non-game packet over NetherNet"); + continue; + }; + if let Err(error) = client.session.send(Bytes::copy_from_slice(data)).await { + warn!( + "Failed to send NetherNet packet to {}: {error}", + client.address + ); + client.close().await; } if let Some(completion) = packet.completion { @@ -333,65 +218,21 @@ impl BedrockClient { }); } - async fn tick_outgoing_transport(&self) -> bool { + async fn tick_connection(&self) -> bool { if self.last_seen.load().elapsed() > std::time::Duration::from_secs(10) { debug!("Bedrock client {} timed out", self.address); self.close().await; return false; } - let Some(socket) = self.raknet_socket() else { - return true; - }; - - let mut pending = self.pending_acks.lock().await; - if !pending.is_empty() { - let ack = Acknowledge::new(pending.clone()); - pending.clear(); - let _ = self.send_acknowledgement(&ack, RAKNET_ACK).await; - } - - let now = std::time::Instant::now(); - let mut resend = Vec::new(); - let mut unacked = self.unacked_outgoing_frames.lock().await; - for (sequence, (id, data, timestamp)) in unacked.iter_mut() { - if now.duration_since(*timestamp) > std::time::Duration::from_secs(1) { - resend.push((*sequence, *id, data.clone())); - *timestamp = now; - if resend.len() >= 50 { - break; - } - } - } - drop(unacked); - - let encoder = self.network_writer.read().await; - for (sequence, id, data) in resend { - debug!("Resending reliable sequence {sequence} (ID: {id})"); - if let Err(error) = encoder.write_packet(&data, self.address, socket).await { - warn!("Failed to resend packet for sequence {sequence}: {error}"); - } - } true } - pub async fn process_packet(self: &Arc, server: &Arc, packet: Bytes) { - self.last_seen.store(std::time::Instant::now()); - if let Err(error) = self.handle_packet_payload(server, packet).await { - error!( - "Failed to handle packet payload for {}: {}", - self.address, error - ); - self.kick(DisconnectReason::BadPacket, error.to_string()) - .await; - } - } - pub async fn process_nethernet_packet(self: &Arc, server: &Arc, packet: Bytes) { self.last_seen.store(std::time::Instant::now()); - let mut raknet_batch = Vec::with_capacity(packet.len() + 1); - raknet_batch.push(RAKNET_GAME_PACKET as u8); - raknet_batch.extend_from_slice(&packet); - if let Err(error) = self.process_frame_payload(server, raknet_batch).await { + let mut batch = Vec::with_capacity(packet.len() + 1); + batch.push(BEDROCK_GAME_PACKET); + batch.extend_from_slice(&packet); + if let Err(error) = self.process_batch(server, batch).await { error!( "Failed to handle NetherNet payload for {}: {error}", self.address @@ -401,22 +242,8 @@ impl BedrockClient { } } - pub const fn is_nethernet(&self) -> bool { - matches!(self.transport, BedrockTransport::NetherNet(_)) - } - - pub fn nethernet_public_key(&self) -> Option<&pumpkin_util::p384::PublicKey> { - match &self.transport { - BedrockTransport::NetherNet(session) => Some(session.client_public_key()), - BedrockTransport::RakNet(_) => None, - } - } - - fn raknet_socket(&self) -> Option<&UdpSocket> { - match &self.transport { - BedrockTransport::RakNet(socket) => Some(socket), - BedrockTransport::NetherNet(_) => None, - } + pub fn nethernet_public_key(&self) -> &pumpkin_util::p384::PublicKey { + self.session.client_public_key() } pub async fn set_compression(&self, compression: CompressionInfo) { @@ -614,10 +441,10 @@ impl BedrockClient { pub fn write_raw_packet( packet: &P, - mut write: impl Write, + mut writer: impl Write, ) -> Result<(), Error> { - write.write_all(&[P::PACKET_ID as u8])?; - packet.write_packet(write) + writer.write_all(&[P::PACKET_ID as u8])?; + packet.write_packet(writer) } pub async fn write_game_packet( @@ -638,20 +465,6 @@ impl BedrockClient { ) } - pub async fn send_offline_packet( - packet: &P, - addr: SocketAddr, - socket: &UdpSocket, - ) { - let mut data = Vec::new(); - if let Err(err) = Self::write_raw_packet(packet, &mut data) { - error!("Failed to write offline packet: {err}"); - return; - } - // We dont care if it works, if not the client will try again! - let _ = socket.send_to(&data, addr).await; - } - pub async fn send_game_packet(&self, packet: &P) { let mut packet_buf = Vec::new(); match self.write_game_packet(packet, &mut packet_buf).await { @@ -685,145 +498,12 @@ impl BedrockClient { } } - pub async fn write_game_packet_to_set( - &self, - packet: &P, - frame_set: &mut FrameSet, - ) { - let mut payload = Vec::new(); - match self.write_game_packet(packet, &mut payload).await { - Ok(()) => { - frame_set.frames.push(Frame::new_unreliable(payload)); - } - Err(err) => error!("Failed to write game packet to set: {err}"), - } - } - - pub async fn send_framed_packet( - &self, - packet: &P, - reliability: RakReliability, - ) { - let mut packet_buf = Vec::new(); - match Self::write_raw_packet(packet, &mut packet_buf) { - Ok(()) => self.send_framed_packet_data(packet_buf, reliability).await, - Err(err) => error!("Failed to write framed packet: {err}"), - } - } - - pub async fn send_framed_packet_data( - &self, - packet_buf: Vec, - mut reliability: RakReliability, - ) { - let mut split_size = 0; - let mut split_id = 0; - let mut order_index = 0; - - let mut max_content_len = - MTU - UDP_HEADER_SIZE - 12 - if reliability.is_ordered() { 4 } else { 0 }; - - let count = if packet_buf.len() > max_content_len { - reliability = RakReliability::ReliableOrdered; - split_id = self.output_split_number.fetch_add(1, Ordering::Relaxed); - max_content_len = SPLIT_FRAME_MAX_CONTENT; - split_size = packet_buf.len().div_ceil(max_content_len) as u32; - split_size as usize - } else { - 1 - }; - - if reliability.is_ordered() { - order_index = self.output_ordered_index.fetch_add(1, Ordering::Relaxed); - } - - for i in 0..count { - let end = if i + 1 == count && !packet_buf.len().is_multiple_of(max_content_len) { - packet_buf.len() % max_content_len - } else { - max_content_len - }; - let chunk = &packet_buf[i * max_content_len..i * max_content_len + end]; - - let mut frame_set = FrameSet { - sequence: u24(0), - frames: Vec::with_capacity(1), - }; - - let mut frame = Frame { - payload: chunk.to_vec(), - reliability, - split_index: i as u32, - reliable_number: 0, - sequence_index: 0, - order_index, - order_channel: 0, - split_size, - split_id, - }; - - if reliability.is_reliable() { - frame.reliable_number = self.output_reliable_number.fetch_add(1, Ordering::Relaxed); - } - - if reliability.is_sequenced() { - frame.sequence_index = self.output_sequenced_index.fetch_add(1, Ordering::Relaxed); - } - - frame_set.frames.push(frame); - - let id = if i == 0 { 0x84 } else { 0x8c }; - self.send_frame_set(frame_set, id).await; - } - } - - pub async fn send_frame_set(&self, mut frame_set: FrameSet, id: u8) { - let Some(socket) = self.raknet_socket() else { - return; - }; - let sequence = self.output_sequence_number.fetch_add(1, Ordering::Relaxed); - frame_set.sequence = u24(sequence); - - let mut frame_set_buf = Vec::new(); - if let Err(err) = frame_set.write_packet_data(&mut frame_set_buf, id) { - error!("Failed to write frame set data: {err}"); - return; - } - - if frame_set.frames.iter().any(|f| f.reliability.is_reliable()) { - self.unacked_outgoing_frames.lock().await.insert( - sequence, - (id, frame_set_buf.clone(), std::time::Instant::now()), - ); - } - - if let Err(err) = self - .network_writer - .read() - .await - .write_packet(&frame_set_buf, self.address, socket) - .await - && !self.is_closed() - { - warn!("Failed to send packet to client {}: {}", self.address, err); - self.close_token.cancel(); - } - } - pub async fn close(&self) { - if self.is_closed() { + if self.close_token.is_cancelled() { return; } self.close_token.cancel(); - - match &self.transport { - BedrockTransport::RakNet(_) => { - self.send_framed_packet(&CDisconnect, RakReliability::Unreliable) - .await; - } - BedrockTransport::NetherNet(session) => session.close().await, - } - + self.session.close().await; self.be_clients.lock().await.remove(&self.address); } @@ -833,8 +513,7 @@ impl BedrockClient { } pub fn is_closed(&self) -> bool { - self.close_token.is_cancelled() - || matches!(&self.transport, BedrockTransport::NetherNet(session) if session.is_closed()) + self.close_token.is_cancelled() || self.session.is_closed() } pub fn enqueue_spawn_packet(self: &Arc, entity: Arc) { @@ -844,244 +523,25 @@ impl BedrockClient { }); } - pub async fn send_acknowledgement(&self, ack: &Acknowledge, id: u8) -> Result<(), Error> { - let Some(socket) = self.raknet_socket() else { - return Ok(()); - }; - let mut packet_buf = Vec::new(); - ack.write(&mut packet_buf, id)?; - - if let Err(err) = self - .network_writer - .read() - .await - .write_packet(&packet_buf, self.address, socket) - .await - { - warn!("Failed to send acknowledgement to {}: {err}", self.address); - self.close().await; - return Err(err); - } - Ok(()) - } - - pub async fn handle_packet_payload( - self: &Arc, - server: &Arc, - packet: Bytes, - ) -> Result<(), Error> { - let reader = &mut Cursor::new(packet); - - match u8::read(reader)? { - RAKNET_ACK => { - self.handle_ack(&Acknowledge::read(reader)?).await; - } - RAKNET_NACK => { - self.handle_nack(&Acknowledge::read(reader)?).await; - } - RAKNET_VALID..=0x8d => { - self.handle_frame_set(server, FrameSet::read(reader)?) - .await?; - } - id => { - warn!("Bedrock: Received unknown packet header {id}"); - } - } - Ok(()) - } - - async fn handle_ack(&self, ack: &Acknowledge) { - let mut unacked = self.unacked_outgoing_frames.lock().await; - for seq in &ack.sequences { - unacked.remove(seq); - } - } - - async fn handle_nack(&self, nack: &Acknowledge) { - debug!("Received NACK for sequences: {:?}", nack.sequences); - let mut resend_data = Vec::new(); - { - let unacked = self.unacked_outgoing_frames.lock().await; - for seq in &nack.sequences { - if let Some((_id, data, _timestamp)) = unacked.get(seq) { - resend_data.push(data.clone()); - } - } - } - - for data in resend_data { - let Some(socket) = self.raknet_socket() else { - return; - }; - if let Err(err) = self - .network_writer - .read() - .await - .write_packet(&data, self.address, socket) - .await - { - warn!("Failed to resend packet from NACK: {}", err); - } - } - } - - async fn handle_frame_set( - self: &Arc, - server: &Arc, - frame_set: FrameSet, - ) -> Result<(), Error> { - let sequence = frame_set.sequence.0; - - { - let mut received = self.received_sequences.lock().await; - if received.contains(&sequence) { - debug!("Received duplicate RakNet sequence: {}", sequence); - return Ok(()); - } - received.insert(sequence); - // Limit the size of received sequences to avoid memory leak - if received.len() > 4096 { - // This is a very simple way to clear it, ideally we'd use a sliding window - received.clear(); - } - } - - self.pending_acks.lock().await.push(sequence); - - for frame in frame_set.frames { - self.handle_frame(server, frame).await?; - } - Ok(()) - } - - async fn handle_frame( - self: &Arc, - server: &Arc, - mut frame: Frame, - ) -> Result<(), Error> { - if frame.split_size > 0 { - let fragment_index = frame.split_index as usize; - let compound_id = frame.split_id; - let mut compounds = self.compounds.lock().await; - - let entry = compounds.entry(compound_id).or_insert_with(|| { - let mut vec = Vec::with_capacity(frame.split_size as usize); - vec.resize_with(frame.split_size as usize, || None); - vec - }); - - if fragment_index >= entry.len() { - return Err(Error::other(format!( - "Fragment index {fragment_index} out of bounds for size {}", - entry.len() - ))); - } - - entry[fragment_index] = Some(frame); - - // Check if all fragments are received - if entry.iter().any(Option::is_none) { - return Ok(()); - } - - let mut frames_opt = compounds - .remove(&compound_id) - .ok_or_else(|| Error::other("Compound ID vanished"))?; - - let total_len: usize = frames_opt.iter().flatten().map(|f| f.payload.len()).sum(); - - let mut merged = Vec::with_capacity(total_len); - - for f in frames_opt.iter().flatten() { - merged.extend_from_slice(&f.payload); - } - - frame = frames_opt[0] - .take() - .ok_or_else(|| Error::other("Failed to retrieve primary frame"))?; - - frame.payload = merged; - frame.split_size = 0; - } - - // Handling Sequencing - if frame.reliability.is_sequenced() { - let mut highest_sequenced = self.highest_sequence_index.lock().await; - let current_highest = highest_sequenced.entry(frame.order_channel).or_insert(0); - if frame.sequence_index < *current_highest { - return Ok(()); - } - *current_highest = frame.sequence_index; - } - - // Handling Ordering - if frame.reliability.is_ordered() { - let mut expected_order = self.expected_order_index.lock().await; - let expected = expected_order.entry(frame.order_channel).or_insert(0); - - if frame.order_index == *expected { - *expected += 1; - self.process_frame_payload(server, frame.payload).await?; - - // Check for queued frames - let mut ordered_queues = self.ordered_queues.lock().await; - if let Some(queue) = ordered_queues.get_mut(&frame.order_channel) { - while let Some(next_frame) = queue.remove(expected) { - *expected += 1; - self.process_frame_payload(server, next_frame.payload) - .await?; - } - } - } else if frame.order_index > *expected { - let mut ordered_queues = self.ordered_queues.lock().await; - let queue = ordered_queues - .entry(frame.order_channel) - .or_insert_with(BTreeMap::new); - queue.insert(frame.order_index, frame); - } - // If frame.order_index < *expected, it's an old frame, discard it. - } else { - self.process_frame_payload(server, frame.payload).await?; - } - - Ok(()) - } - - async fn process_frame_payload( + async fn process_batch( self: &Arc, server: &Arc, payload: Vec, ) -> Result<(), Error> { - if payload.is_empty() { - return Ok(()); - } - let id = payload[0]; + let decompressed_payload = self + .get_packet_payload(payload) + .await + .ok_or_else(|| Error::other("Failed to decompress game packet batch"))?; + let mut cursor = Cursor::new(decompressed_payload); - if id == RAKNET_GAME_PACKET as u8 { - // Decompress the batch - let decompressed_payload = self - .get_packet_payload(payload) + while (cursor.position() as usize) < cursor.get_ref().len() { + let game_packet = self + .network_reader + .lock() .await - .ok_or_else(|| Error::other("Failed to decompress game packet batch"))?; - - // Loop through the decompressed buffer to extract ALL batched packets - let mut cursor = Cursor::new(decompressed_payload); - - while (cursor.position() as usize) < cursor.get_ref().len() { - let game_packet = self - .network_reader - .lock() - .await - .get_game_packet(&mut cursor) - .map_err(|e| Error::other(e.to_string()))?; - - self.handle_game_packet(server, game_packet).await?; - } - } else { - // It's an internal RakNet message (like SConnectedPing) - let mut cursor = Cursor::new(payload); - let _id = u8::read(&mut cursor)?; // consume ID byte - self.handle_raknet_packet(i32::from(id), cursor).await?; + .get_game_packet(&mut cursor) + .map_err(|e| Error::other(e.to_string()))?; + self.handle_game_packet(server, game_packet).await?; } Ok(()) @@ -1126,67 +586,13 @@ impl BedrockClient { } }; match self.handle_login(packet, server).await { - Ok(Some(result)) => return result, - Ok(None) => {} // encryption enabled, wait for handshake + Ok(result) => return result, Err(err) => { self.kick(DisconnectReason::Unknown, err.to_string()).await; return PacketHandlerResult::Stop; } } } - SClientToServerHandshake::PACKET_ID => { - let _packet = match SClientToServerHandshake::read(payload) { - Ok(p) => p, - Err(err) => { - error!("Failed to read SClientToServerHandshake: {err}"); - continue; - } - }; - let pending = self.pending_profile.swap(Arc::new(None)); - if let Some(pending) = pending.as_ref() { - let (profile, new_config) = pending.as_ref(); - self.enqueue_packet_internal(&CPlayStatus::LoginSuccess) - .await; - let br_config = &server.advanced_config.resource_pack.bedrock; - - let mut entries = Vec::new(); - if br_config.enabled { - for pack in &br_config.packs { - entries.push(ResourcePackEntry { - uuid: pack.uuid, - version: pack.version.clone(), - size: pack.size, - download_url: pack.download_url.clone(), - content_key: pack.content_key.clone(), - sub_pack_name: pack.sub_pack_name.clone(), - content_id: pack.content_id.clone(), - has_scripts: pack.has_scripts, - addon_pack: pack.addon_pack, - rtx_enabled: pack.rtx_enabled, - }); - } - } - - let packs_info = CResourcePacksInfo { - resource_pack_required: br_config.force, - has_addon_packs: false, - has_scripts: false, - is_vibrant_visuals_force_disabled: false, - world_template_id: uuid::Uuid::nil(), - world_template_version: String::new(), - resource_packs: entries, - }; - self.enqueue_packet_internal(&packs_info).await; - return PacketHandlerResult::ReadyToPlay( - profile.clone(), - new_config.clone(), - ); - } - error!("Received ClientToServerHandshake but no pending profile was found."); - self.kick(DisconnectReason::BadPacket, "Handshake error".into()) - .await; - return PacketHandlerResult::Stop; - } _ => { debug!( "Received unexpected game packet {} during login sequence", @@ -1321,162 +727,6 @@ impl BedrockClient { Ok(()) } - async fn handle_raknet_packet( - self: &Arc, - packet_id: i32, - mut payload: Cursor>, - ) -> Result<(), Error> { - let reader = &mut payload; - match packet_id { - SConnectionRequest::PACKET_ID => { - let request = SConnectionRequest::read(reader)?; - - self.send_framed_packet( - &CConnectionRequestAccepted::new( - self.address, - 0, - [SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 19132)); 10], - request.time, - UNIX_EPOCH.elapsed().unwrap().as_millis() as u64, - ), - RakReliability::Unreliable, - ) - .await; - } - SNewIncomingConnection::PACKET_ID => { - self.handle_new_incoming_connection(&SNewIncomingConnection::read(reader)?); - } - SConnectedPing::PACKET_ID => { - self.handle_connected_ping(SConnectedPing::read(reader)?) - .await; - } - SDisconnect::PACKET_ID | SConnectionLost::PACKET_ID => { - self.close().await; - } - _ => { - warn!("Bedrock: Received Unknown RakNet Online packet: {packet_id}"); - } - } - Ok(()) - } - - #[expect(clippy::too_many_lines)] - pub async fn handle_offline_packet( - server: &Server, - packet_id: u8, - payload: &mut Cursor<&[u8]>, - addr: SocketAddr, - socket: &UdpSocket, - be_clients: &Arc>>>, - ) -> Result<(), Error> { - let packet_id_i32 = i32::from(packet_id); - if packet_id_i32 == SOpenConnectionRequest1::PACKET_ID { - let is_banned = { - let mut banned_ips = server.data.banned_ip_list.write().await; - banned_ips.get_entry(&addr.ip()).is_some() - }; - if is_banned { - Self::send_offline_packet( - &CConnectionBanned::new(server.server_guid), - addr, - socket, - ) - .await; - return Ok(()); - } - - let player_count = { - let status = server.get_status().lock().await; - status - .status_response - .players - .as_ref() - .map_or(0, |p| p.online) as u32 - }; - if player_count >= server.advanced_config.networking.bedrock.max_players { - Self::send_offline_packet( - &CNoFreeIncomingConnections::new(server.server_guid), - addr, - socket, - ) - .await; - return Ok(()); - } - - let old_client = { - let mut clients_guard = be_clients.lock().await; - clients_guard.remove(&addr) - }; - if let Some(client) = old_client { - debug!( - "Closing old Bedrock client connection for {} due to new connection request", - addr - ); - client.close().await; - } - } - - match packet_id_i32 { - SUnconnectedPing::PACKET_ID => { - Self::handle_unconnected_ping( - server, - SUnconnectedPing::read(payload)?, - addr, - socket, - ) - .await; - } - SUnconnectedPingOpenConnections::PACKET_ID => { - let packet = SUnconnectedPingOpenConnections::read(payload)?; - Self::handle_unconnected_ping( - server, - SUnconnectedPing { - time: packet.time, - magic: packet.magic, - client_guid: packet.client_guid, - }, - addr, - socket, - ) - .await; - } - SOpenConnectionRequest1::PACKET_ID => { - Self::handle_open_connection_1( - server, - SOpenConnectionRequest1::read(payload)?, - addr, - socket, - ) - .await; - } - SOpenConnectionRequest2::PACKET_ID => { - let is_already_connected = { - let clients_guard = be_clients.lock().await; - clients_guard.contains_key(&addr) - }; - if is_already_connected { - Self::send_offline_packet( - &CAlreadyConnected::new(server.server_guid), - addr, - socket, - ) - .await; - return Ok(()); - } - - Self::handle_open_connection_2( - server, - SOpenConnectionRequest2::read(payload)?, - addr, - socket, - ) - .await; - } - _ => error!("Bedrock: Received Unknown RakNet Offline packet: {packet_id}"), - } - Ok(()) - } - pub async fn await_close_interrupt(&self) { self.close_token.cancelled().await; } diff --git a/crates/pumpkin/src/net/bedrock/open_connection.rs b/crates/pumpkin/src/net/bedrock/open_connection.rs deleted file mode 100644 index c14084206..000000000 --- a/crates/pumpkin/src/net/bedrock/open_connection.rs +++ /dev/null @@ -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; - } -} diff --git a/crates/pumpkin/src/net/bedrock/status.rs b/crates/pumpkin/src/net/bedrock/status.rs new file mode 100644 index 000000000..28baa93db --- /dev/null +++ b/crates/pumpkin/src/net/bedrock/status.rs @@ -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 { + 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(()) +} diff --git a/crates/pumpkin/src/net/bedrock/unconnected.rs b/crates/pumpkin/src/net/bedrock/unconnected.rs deleted file mode 100644 index 33d8bf5d1..000000000 --- a/crates/pumpkin/src/net/bedrock/unconnected.rs +++ /dev/null @@ -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; - } -}