mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
chore: remove bedrock raknet (#2811)
* Remove legacy RakNet transport * Replace RakNet status with NetherNet discovery * Restore Bedrock server-list status * Remove NetherNet LAN discovery * Remove unused AES dependency
This commit is contained in:
30
Cargo.lock
generated
30
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
use std::io::{Error, ErrorKind, Read, Write};
|
||||
|
||||
const MAX_ACK_RECORDS: u16 = 4096;
|
||||
|
||||
use crate::{
|
||||
codec::u24,
|
||||
serial::{PacketRead, PacketWrite},
|
||||
};
|
||||
|
||||
pub struct Acknowledge {
|
||||
pub sequences: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Acknowledge {
|
||||
#[must_use]
|
||||
pub const fn new(sequences: Vec<u32>) -> Self {
|
||||
Self { sequences }
|
||||
}
|
||||
|
||||
fn write_range<W: Write>(start: u32, end: u32, writer: &mut W) -> Result<(), Error> {
|
||||
if start == end {
|
||||
1u8.write(writer)?;
|
||||
u24(start).write(writer)
|
||||
} else {
|
||||
0u8.write(writer)?;
|
||||
u24(start).write(writer)?;
|
||||
u24(end).write(writer)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
|
||||
let size = u16::read_be(reader)?;
|
||||
|
||||
if size > MAX_ACK_RECORDS {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"Acknowledge packet range is too large.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut sequences = Vec::with_capacity(size as usize);
|
||||
for _ in 0..size {
|
||||
let single = bool::read(reader)?;
|
||||
if single {
|
||||
sequences.push(u24::read(reader)?.0);
|
||||
} else {
|
||||
let start = u24::read(reader)?.0;
|
||||
let end = u24::read(reader)?.0;
|
||||
for i in start..=end {
|
||||
sequences.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self { sequences })
|
||||
}
|
||||
|
||||
pub fn write<W: Write>(&self, writer: &mut W, id: u8) -> Result<(), Error> {
|
||||
id.write(writer)?;
|
||||
if self.sequences.is_empty() {
|
||||
0u16.write_be(writer)?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut count: u16 = 0;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let mut sequences = self.sequences.clone();
|
||||
sequences.sort_unstable();
|
||||
|
||||
let mut start = sequences[0];
|
||||
let mut end = start;
|
||||
for seq in sequences.iter().copied().skip(1) {
|
||||
if seq != end + 1 {
|
||||
Self::write_range(start, end, &mut buf)?;
|
||||
count += 1;
|
||||
start = seq;
|
||||
}
|
||||
end = seq;
|
||||
}
|
||||
Self::write_range(start, end, &mut buf)?;
|
||||
count += 1;
|
||||
count.write_be(writer)?;
|
||||
writer.write_all(&buf)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use pumpkin_macros::packet;
|
||||
|
||||
use crate::{bedrock::RAKNET_MAGIC, serial::PacketWrite};
|
||||
/// Sent in response to a `ConnectedPing` (`0x00`) to calculate round-trip latency and synchronize time across an established connection.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Connected_Pong>
|
||||
#[derive(PacketWrite)]
|
||||
#[packet(0x03)]
|
||||
pub struct CConnectedPong {
|
||||
ping: u64,
|
||||
pong: u64,
|
||||
}
|
||||
|
||||
impl CConnectedPong {
|
||||
#[must_use]
|
||||
#[expect(clippy::similar_names)]
|
||||
pub const fn new(ping: u64, pong: u64) -> Self {
|
||||
Self { ping, pong }
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent by the server to accept an incoming `ConnectionRequest` (`0x09`), confirming connection parameters and system addresses.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Connection_Request_Accepted>
|
||||
#[derive(PacketWrite)]
|
||||
#[packet(0x10)]
|
||||
pub struct CConnectionRequestAccepted {
|
||||
client_address: SocketAddr,
|
||||
system_index: u16,
|
||||
system_addresses: [SocketAddr; 10],
|
||||
requested_timestamp: u64,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
impl CConnectionRequestAccepted {
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
client_address: SocketAddr,
|
||||
system_index: u16,
|
||||
system_addresses: [SocketAddr; 10],
|
||||
requested_timestamp: u64,
|
||||
timestamp: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
client_address,
|
||||
system_index,
|
||||
system_addresses,
|
||||
requested_timestamp,
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent by the server when a client attempts to connect while already being connected.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Already_Connected>
|
||||
#[derive(PacketWrite)]
|
||||
#[packet(0x12)]
|
||||
pub struct CAlreadyConnected {
|
||||
magic: [u8; 16],
|
||||
server_guid: u64,
|
||||
}
|
||||
|
||||
impl CAlreadyConnected {
|
||||
#[must_use]
|
||||
pub const fn new(server_guid: u64) -> Self {
|
||||
Self {
|
||||
magic: RAKNET_MAGIC,
|
||||
server_guid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent by the server when it has reached its maximum connection capacity.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#No_Free_Incoming_Connections>
|
||||
#[derive(PacketWrite)]
|
||||
#[packet(0x14)]
|
||||
pub struct CNoFreeIncomingConnections {
|
||||
magic: [u8; 16],
|
||||
server_guid: u64,
|
||||
}
|
||||
|
||||
impl CNoFreeIncomingConnections {
|
||||
#[must_use]
|
||||
pub const fn new(server_guid: u64) -> Self {
|
||||
Self {
|
||||
magic: RAKNET_MAGIC,
|
||||
server_guid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent by the server when a client attempts to connect from a banned IP address or identifier.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Connection_Banned>
|
||||
#[derive(PacketWrite)]
|
||||
#[packet(0x17)]
|
||||
pub struct CConnectionBanned {
|
||||
magic: [u8; 16],
|
||||
server_guid: u64,
|
||||
}
|
||||
|
||||
impl CConnectionBanned {
|
||||
#[must_use]
|
||||
pub const fn new(server_guid: u64) -> Self {
|
||||
Self {
|
||||
magic: RAKNET_MAGIC,
|
||||
server_guid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent by the server when a client attempts to connect again too quickly after disconnecting.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#IP_Recently_Connected>
|
||||
#[derive(PacketWrite)]
|
||||
#[packet(0x1A)]
|
||||
pub struct CIpRecentlyConnected {
|
||||
magic: [u8; 16],
|
||||
server_guid: u64,
|
||||
}
|
||||
|
||||
impl CIpRecentlyConnected {
|
||||
#[must_use]
|
||||
pub const fn new(server_guid: u64) -> Self {
|
||||
Self {
|
||||
magic: RAKNET_MAGIC,
|
||||
server_guid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent by the server to initiate graceful termination of the connection session.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Disconnection_Notification>
|
||||
#[derive(PacketWrite)]
|
||||
#[packet(0x15)]
|
||||
pub struct CDisconnect;
|
||||
@@ -1,24 +0,0 @@
|
||||
use crate::{bedrock::RAKNET_MAGIC, serial::PacketWrite};
|
||||
use pumpkin_macros::packet;
|
||||
|
||||
/// Sent by the server when the client's `RakNet` protocol version does not match the server's expected protocol version (`11`).
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Incompatible_Protocol_Version>
|
||||
#[derive(PacketWrite)]
|
||||
#[packet(0x19)]
|
||||
pub struct CIncompatibleProtocolVersion {
|
||||
protocol_version: u8,
|
||||
magic: [u8; 16],
|
||||
server_guid: u64,
|
||||
}
|
||||
|
||||
impl CIncompatibleProtocolVersion {
|
||||
#[must_use]
|
||||
pub const fn new(protocol_version: u8, server_guid: u64) -> Self {
|
||||
Self {
|
||||
protocol_version,
|
||||
magic: RAKNET_MAGIC,
|
||||
server_guid,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod connection;
|
||||
pub mod incompatible_protocol;
|
||||
pub mod open_connection;
|
||||
pub mod unconnected_pong;
|
||||
@@ -1,63 +0,0 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use pumpkin_macros::packet;
|
||||
|
||||
use crate::{bedrock::RAKNET_MAGIC, serial::PacketWrite};
|
||||
|
||||
/// Sent by the server in response to `OpenConnectionRequest1` (`0x05`), negotiating security options, server GUID, and MTU.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Open_Connection_Reply_1>
|
||||
#[derive(PacketWrite)]
|
||||
#[packet(0x06)]
|
||||
pub struct COpenConnectionReply1 {
|
||||
magic: [u8; 16],
|
||||
server_guid: u64,
|
||||
has_server_security: bool,
|
||||
// Only write when has_server_security
|
||||
// cookie: u32,
|
||||
mtu: u16,
|
||||
}
|
||||
|
||||
impl COpenConnectionReply1 {
|
||||
#[must_use]
|
||||
pub const fn new(server_guid: u64, has_server_security: bool, mtu: u16) -> Self {
|
||||
Self {
|
||||
magic: RAKNET_MAGIC,
|
||||
server_guid,
|
||||
has_server_security,
|
||||
// cookie,
|
||||
mtu,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent by the server in response to `OpenConnectionRequest2` (`0x07`), confirming the connection setup and client address before establishing session state.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Open_Connection_Reply_2>
|
||||
#[derive(PacketWrite)]
|
||||
#[packet(0x08)]
|
||||
pub struct COpenConnectionReply2 {
|
||||
magic: [u8; 16],
|
||||
server_guid: u64,
|
||||
client_address: SocketAddr,
|
||||
mtu: u16,
|
||||
security: bool,
|
||||
}
|
||||
|
||||
impl COpenConnectionReply2 {
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
server_guid: u64,
|
||||
client_address: SocketAddr,
|
||||
mtu: u16,
|
||||
security: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
magic: RAKNET_MAGIC,
|
||||
server_guid,
|
||||
client_address,
|
||||
mtu,
|
||||
security,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
use core::fmt;
|
||||
use std::io::{Error, Write};
|
||||
|
||||
use pumpkin_macros::packet;
|
||||
|
||||
use crate::serial::PacketWrite;
|
||||
|
||||
/// Sent by the server in response to an `UnconnectedPing` (`0x01` / `0x02`), containing server metadata (MOTD, protocol version, player counts, edition).
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Unconnected_Pong>
|
||||
#[packet(0x1c)]
|
||||
pub struct CUnconnectedPong {
|
||||
time: u64,
|
||||
server_guid: u64,
|
||||
magic: [u8; 16],
|
||||
server_id: String,
|
||||
}
|
||||
|
||||
impl PacketWrite for CUnconnectedPong {
|
||||
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
|
||||
self.time.write_be(writer)?;
|
||||
self.server_guid.write_be(writer)?;
|
||||
writer.write_all(&self.magic)?;
|
||||
writer.write_all(&(self.server_id.len() as u16).to_be_bytes())?;
|
||||
writer.write_all(self.server_id.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServerInfo {
|
||||
/// (BE or MCEE for Education Edition)
|
||||
pub edition: &'static str,
|
||||
pub motd_line_1: String,
|
||||
pub protocol_version: u32,
|
||||
pub version_name: &'static str,
|
||||
pub player_count: i32,
|
||||
pub max_player_count: u32,
|
||||
pub server_unique_id: u64,
|
||||
pub motd_line_2: String,
|
||||
pub game_mode: &'static str,
|
||||
pub game_mode_numeric: u32,
|
||||
pub port_ipv4: u16,
|
||||
pub port_ipv6: u16,
|
||||
}
|
||||
|
||||
impl fmt::Display for ServerInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{};{};{};{};{};{};{};{};{};{};{};{};0;",
|
||||
self.edition,
|
||||
self.motd_line_1,
|
||||
self.protocol_version,
|
||||
self.version_name,
|
||||
self.player_count,
|
||||
self.max_player_count,
|
||||
self.server_unique_id,
|
||||
self.motd_line_2,
|
||||
self.game_mode,
|
||||
self.game_mode_numeric,
|
||||
self.port_ipv4,
|
||||
self.port_ipv6
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CUnconnectedPong {
|
||||
#[must_use]
|
||||
pub const fn new(time: u64, server_guid: u64, magic: [u8; 16], server_id: String) -> Self {
|
||||
Self {
|
||||
time,
|
||||
server_guid,
|
||||
magic,
|
||||
server_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
use aes::Aes256;
|
||||
use ctr::Ctr128BE;
|
||||
use ctr::cipher::{KeyIvInit, StreamCipher};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
type BedrockCtr = Ctr128BE<Aes256>;
|
||||
|
||||
pub struct BedrockEncryptor {
|
||||
cipher: BedrockCtr,
|
||||
key: [u8; 32],
|
||||
send_counter: u64,
|
||||
}
|
||||
|
||||
impl BedrockEncryptor {
|
||||
#[must_use]
|
||||
pub fn new(key: &[u8; 32]) -> Self {
|
||||
let mut iv = [0u8; 16];
|
||||
iv[..12].copy_from_slice(&key[..12]);
|
||||
iv[12..].copy_from_slice(&[0, 0, 0, 2]);
|
||||
|
||||
Self {
|
||||
cipher: BedrockCtr::new(key.into(), &iv.into()),
|
||||
key: *key,
|
||||
send_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt(&mut self, data: &mut Vec<u8>) {
|
||||
// data contains the payload after 0xfe
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(self.send_counter.to_le_bytes());
|
||||
hasher.update(&data[..]);
|
||||
hasher.update(self.key);
|
||||
|
||||
let hash = hasher.finalize();
|
||||
data.extend_from_slice(&hash[..8]);
|
||||
|
||||
self.cipher.apply_keystream(data);
|
||||
|
||||
self.send_counter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BedrockDecryptor {
|
||||
cipher: BedrockCtr,
|
||||
key: [u8; 32],
|
||||
send_counter: u64,
|
||||
}
|
||||
|
||||
impl BedrockDecryptor {
|
||||
#[must_use]
|
||||
pub fn new(key: &[u8; 32]) -> Self {
|
||||
let mut iv = [0u8; 16];
|
||||
iv[..12].copy_from_slice(&key[..12]);
|
||||
iv[12..].copy_from_slice(&[0, 0, 0, 2]);
|
||||
|
||||
Self {
|
||||
cipher: BedrockCtr::new(key.into(), &iv.into()),
|
||||
key: *key,
|
||||
send_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::needless_borrow)] // False positive
|
||||
pub fn decrypt(&mut self, data: &mut Vec<u8>) -> Result<(), String> {
|
||||
let ciphertext = data.clone();
|
||||
self.cipher.apply_keystream(data);
|
||||
|
||||
if data.len() < 8 {
|
||||
return Err("Encrypted packet must be at least 8 bytes long".to_string());
|
||||
}
|
||||
|
||||
let (payload, checksum) = data.split_at(data.len() - 8);
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(self.send_counter.to_le_bytes());
|
||||
hasher.update(payload);
|
||||
hasher.update(self.key);
|
||||
|
||||
let our_checksum = &hasher.finalize()[..8];
|
||||
|
||||
if checksum != our_checksum {
|
||||
let cipher_prefix = if ciphertext.len() > 16 {
|
||||
&ciphertext[..16]
|
||||
} else {
|
||||
&ciphertext
|
||||
};
|
||||
let plain_prefix = if data.len() > 16 { &data[..16] } else { &data };
|
||||
return Err(format!(
|
||||
"Invalid checksum: expected {:x?}, got {:x?}. Cipher prefix: {:x?}, Plain prefix: {:x?}, Key: {:x?}, Counter: {}",
|
||||
our_checksum, checksum, cipher_prefix, plain_prefix, self.key, self.send_counter
|
||||
));
|
||||
}
|
||||
|
||||
data.truncate(payload.len());
|
||||
self.send_counter += 1;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn gcm_compatibility() {
|
||||
let key = [1u8; 32];
|
||||
let mut iv = [0u8; 16];
|
||||
iv[..12].copy_from_slice(&key[..12]);
|
||||
iv[12..].copy_from_slice(&[0, 0, 0, 2]);
|
||||
|
||||
let mut cipher = BedrockCtr::new((&key).into(), (&iv).into());
|
||||
let mut data = b"Hello Bedrock encryption!".to_vec();
|
||||
cipher.apply_keystream(&mut data);
|
||||
let expected = &[
|
||||
0xfa, 0x1c, 0xd6, 0xf6, 0x06, 0xd7, 0x47, 0x96, 0x8e, 0xd7, 0x60, 0xfe, 0xc5, 0x1c,
|
||||
0x2b, 0xc7, 0x7e, 0x46, 0x17, 0x74, 0x25, 0x96, 0x34, 0x0c, 0xac,
|
||||
];
|
||||
assert_eq!(data, expected);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
use std::io::{Error, Read, Write};
|
||||
|
||||
use crate::bedrock::{MTU, RAKNET_SPLIT, RakReliability};
|
||||
use crate::codec::u24;
|
||||
use crate::serial::{PacketRead, PacketWrite};
|
||||
|
||||
pub struct FrameSet {
|
||||
pub sequence: u24,
|
||||
pub frames: Vec<Frame>,
|
||||
}
|
||||
|
||||
impl FrameSet {
|
||||
pub fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
sequence: u24::read(reader)?,
|
||||
frames: Frame::read(reader)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_packet_data<W: Write>(&self, writer: &mut W, id: u8) -> Result<(), Error> {
|
||||
id.write(writer)?;
|
||||
self.sequence.write(writer)?;
|
||||
for frame in &self.frames {
|
||||
frame.write(writer)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FrameSet {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sequence: u24(0),
|
||||
frames: Vec::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Frame {
|
||||
pub reliability: RakReliability,
|
||||
// If we write a packet we dont want to own the payload to avoid cloning
|
||||
pub payload: Vec<u8>,
|
||||
pub reliable_number: u32,
|
||||
pub sequence_index: u32,
|
||||
pub order_index: u32,
|
||||
pub order_channel: u8,
|
||||
pub split_size: u32,
|
||||
pub split_id: u16,
|
||||
pub split_index: u32,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
#[must_use]
|
||||
pub const fn new_unreliable(payload: Vec<u8>) -> Self {
|
||||
Self {
|
||||
reliability: RakReliability::Unreliable,
|
||||
payload,
|
||||
reliable_number: 0,
|
||||
sequence_index: 0,
|
||||
order_index: 0,
|
||||
order_channel: 0,
|
||||
split_size: 0,
|
||||
split_id: 0,
|
||||
split_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read<R: Read>(reader: &mut R) -> Result<Vec<Self>, Error> {
|
||||
let mut frames = Vec::new();
|
||||
let mut header_buf = [0u8; 1];
|
||||
|
||||
while {
|
||||
let n = reader.read(&mut header_buf)?;
|
||||
n > 0
|
||||
} {
|
||||
let header = header_buf[0];
|
||||
let mut frame = Self::default();
|
||||
|
||||
let reliability_id = (header & 0xE0) >> 5;
|
||||
let Some(reliability) = RakReliability::from_id(reliability_id) else {
|
||||
return Err(Error::other("Invalid reliability"));
|
||||
};
|
||||
|
||||
let split = (header & RAKNET_SPLIT) != 0;
|
||||
|
||||
let bit_length = u16::read_be(reader)?;
|
||||
let byte_length = (bit_length + 7) >> 3;
|
||||
|
||||
if byte_length > MTU as u16 {
|
||||
return Err(Error::other("Frame payload length exceeds RakNet MTU"));
|
||||
}
|
||||
|
||||
if reliability.is_reliable() {
|
||||
frame.reliable_number = u24::read(reader)?.0;
|
||||
}
|
||||
|
||||
if reliability.is_sequenced() {
|
||||
frame.sequence_index = u24::read(reader)?.0;
|
||||
}
|
||||
|
||||
if reliability.is_ordered() {
|
||||
frame.order_index = u24::read(reader)?.0;
|
||||
frame.order_channel = u8::read(reader)?;
|
||||
}
|
||||
|
||||
if split {
|
||||
frame.split_size = u32::read_be(reader)?;
|
||||
frame.split_id = u16::read_be(reader)?;
|
||||
frame.split_index = u32::read_be(reader)?;
|
||||
}
|
||||
|
||||
frame.reliability = reliability;
|
||||
frame.payload = vec![0; byte_length as usize];
|
||||
reader.read_exact(&mut frame.payload)?;
|
||||
|
||||
frames.push(frame);
|
||||
}
|
||||
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
|
||||
let is_split = self.split_size > 0;
|
||||
let mut flags = self.reliability.to_id() << 5;
|
||||
if is_split {
|
||||
flags |= RAKNET_SPLIT;
|
||||
}
|
||||
flags.write(writer)?;
|
||||
// Size
|
||||
((self.payload.len() as u16) << 3).write_be(writer)?;
|
||||
if self.reliability.is_reliable() {
|
||||
u24(self.reliable_number).write(writer)?;
|
||||
}
|
||||
if self.reliability.is_sequenced() {
|
||||
u24(self.sequence_index).write(writer)?;
|
||||
}
|
||||
if self.reliability.is_ordered() {
|
||||
u24(self.order_index).write(writer)?;
|
||||
self.order_channel.write(writer)?;
|
||||
}
|
||||
if is_split {
|
||||
self.split_size.write_be(writer)?;
|
||||
self.split_id.write_be(writer)?;
|
||||
self.split_index.write_be(writer)?;
|
||||
}
|
||||
|
||||
writer.write_all(&self.payload)
|
||||
}
|
||||
}
|
||||
@@ -1,110 +1,11 @@
|
||||
pub mod ack;
|
||||
pub mod client;
|
||||
pub mod crypto;
|
||||
pub mod frame_set;
|
||||
pub mod network_item;
|
||||
pub mod packet_decoder;
|
||||
pub mod packet_encoder;
|
||||
pub mod server;
|
||||
pub mod status;
|
||||
|
||||
pub const RAKNET_PROTOCOL_VERSION: u8 = 11;
|
||||
pub const UDP_HEADER_SIZE: usize = 28;
|
||||
pub const MTU: usize = 1400;
|
||||
|
||||
// 26 bytes is RakNet header for FrameSet containing a single, ReliableOrdered, split/fragmented frame
|
||||
pub const SPLIT_FRAME_MAX_CONTENT: usize = MTU - UDP_HEADER_SIZE - 26;
|
||||
|
||||
pub const RAKNET_MAGIC: [u8; 16] = [
|
||||
0x00, 0xff, 0xff, 0x0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, 0x56, 0x78,
|
||||
];
|
||||
|
||||
pub const RAKNET_VALID: u8 = 0x80;
|
||||
pub const RAKNET_ACK: u8 = 0xC0;
|
||||
pub const RAKNET_NACK: u8 = 0xA0;
|
||||
|
||||
pub const RAKNET_GAME_PACKET: i32 = 0xfe;
|
||||
|
||||
pub const RAKNET_SPLIT: u8 = 0x10;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)]
|
||||
pub enum RakReliability {
|
||||
Unreliable,
|
||||
UnreliableSequenced,
|
||||
Reliable,
|
||||
#[default]
|
||||
ReliableOrdered,
|
||||
ReliableSequenced,
|
||||
UnreliableWithAckReceipt,
|
||||
ReliableWithAckReceipt,
|
||||
ReliableOrderedWithAckReceipt,
|
||||
}
|
||||
|
||||
impl RakReliability {
|
||||
#[must_use]
|
||||
pub const fn is_reliable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Reliable
|
||||
| Self::ReliableOrdered
|
||||
| Self::ReliableSequenced
|
||||
| Self::ReliableWithAckReceipt
|
||||
| Self::ReliableOrderedWithAckReceipt
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_sequenced(&self) -> bool {
|
||||
matches!(self, Self::ReliableSequenced | Self::UnreliableSequenced)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_ordered(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::UnreliableSequenced
|
||||
| Self::ReliableOrdered
|
||||
| Self::ReliableSequenced
|
||||
| Self::ReliableOrderedWithAckReceipt
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_order_exclusive(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::ReliableOrdered | Self::ReliableOrderedWithAckReceipt
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn from_id(id: u8) -> Option<Self> {
|
||||
match id {
|
||||
0 => Some(Self::Unreliable),
|
||||
1 => Some(Self::UnreliableSequenced),
|
||||
2 => Some(Self::Reliable),
|
||||
3 => Some(Self::ReliableOrdered),
|
||||
4 => Some(Self::ReliableSequenced),
|
||||
5 => Some(Self::UnreliableWithAckReceipt),
|
||||
6 => Some(Self::ReliableWithAckReceipt),
|
||||
7 => Some(Self::ReliableOrderedWithAckReceipt),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn to_id(&self) -> u8 {
|
||||
match self {
|
||||
Self::Unreliable => 0,
|
||||
Self::UnreliableSequenced => 1,
|
||||
Self::Reliable => 2,
|
||||
Self::ReliableOrdered => 3,
|
||||
Self::ReliableSequenced => 4,
|
||||
Self::UnreliableWithAckReceipt => 5,
|
||||
Self::ReliableWithAckReceipt => 6,
|
||||
Self::ReliableOrderedWithAckReceipt => 7,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub const BEDROCK_GAME_PACKET: u8 = 0xfe;
|
||||
|
||||
#[repr(u16)]
|
||||
pub enum SubClient {
|
||||
|
||||
@@ -1,95 +1,35 @@
|
||||
use std::{
|
||||
io::{Cursor, Read},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use async_compression::tokio::bufread::DeflateDecoder;
|
||||
use tokio::io::{AsyncRead, BufReader, ReadBuf};
|
||||
use tokio::io::BufReader;
|
||||
|
||||
use crate::{
|
||||
Aes128Cfb8Dec, CompressionThreshold, MAX_PACKET_DATA_SIZE, PacketDecodeError, RawPacket,
|
||||
StreamDecryptor, codec::var_uint::VarUInt, ser::ReadingError,
|
||||
CompressionThreshold, MAX_PACKET_DATA_SIZE, PacketDecodeError, RawPacket,
|
||||
bedrock::BEDROCK_GAME_PACKET, codec::var_uint::VarUInt, ser::ReadingError,
|
||||
};
|
||||
|
||||
pub enum DecryptionReader<R: AsyncRead + Unpin> {
|
||||
Decrypt(Box<StreamDecryptor<R>>),
|
||||
None(R),
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + Unpin> DecryptionReader<R> {
|
||||
#[must_use]
|
||||
pub fn upgrade(self, cipher: Aes128Cfb8Dec) -> Self {
|
||||
match self {
|
||||
Self::None(stream) => Self::Decrypt(Box::new(StreamDecryptor::new(cipher, stream))),
|
||||
Self::Decrypt(_) => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + Unpin> AsyncRead for DecryptionReader<R> {
|
||||
#[inline]
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
match self.get_mut() {
|
||||
Self::Decrypt(reader) => {
|
||||
let reader = Pin::new(reader);
|
||||
reader.poll_read(cx, buf)
|
||||
}
|
||||
Self::None(reader) => {
|
||||
let reader = Pin::new(reader);
|
||||
reader.poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::bedrock::crypto::BedrockDecryptor;
|
||||
|
||||
/// Decoder: Client -> Server
|
||||
/// Supports `ZLib` decoding/decompression
|
||||
/// Supports Aes256 Encryption
|
||||
pub struct UDPNetworkDecoder {
|
||||
/// Supports Zlib decompression.
|
||||
pub struct BedrockBatchDecoder {
|
||||
compression: Option<CompressionThreshold>,
|
||||
decryptor: Option<BedrockDecryptor>,
|
||||
}
|
||||
|
||||
impl Default for UDPNetworkDecoder {
|
||||
impl Default for BedrockBatchDecoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Encryption already enabled")]
|
||||
pub struct EncryptionAlreadyEnabledError;
|
||||
|
||||
impl UDPNetworkDecoder {
|
||||
impl BedrockBatchDecoder {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
compression: None,
|
||||
decryptor: None,
|
||||
}
|
||||
Self { compression: None }
|
||||
}
|
||||
|
||||
pub const fn set_compression(&mut self, threshold: CompressionThreshold) {
|
||||
self.compression = Some(threshold);
|
||||
}
|
||||
|
||||
pub fn set_encryption(&mut self, key: &[u8; 32]) -> Result<(), EncryptionAlreadyEnabledError> {
|
||||
if self.decryptor.is_some() {
|
||||
return Err(EncryptionAlreadyEnabledError);
|
||||
}
|
||||
self.decryptor = Some(BedrockDecryptor::new(key));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_packet_payload(
|
||||
&mut self,
|
||||
full_packet: Vec<u8>,
|
||||
@@ -98,27 +38,20 @@ impl UDPNetworkDecoder {
|
||||
return Err(PacketDecodeError::MalformedLength("Empty packet".into()));
|
||||
}
|
||||
|
||||
// If the first byte isn't 0xfe, it's likely a RakNet control packet or encrypted.
|
||||
// Ensure your RakNet implementation is providing ONLY the payload here.
|
||||
if full_packet[0] != 0xfe {
|
||||
// NetherNet carries the batch without the Bedrock game-packet marker.
|
||||
// The transport adapter restores it before decoding.
|
||||
if full_packet[0] != BEDROCK_GAME_PACKET {
|
||||
return Err(PacketDecodeError::MalformedLength(format!(
|
||||
"Missing 0xfe header (found 0x{:02x})",
|
||||
full_packet[0]
|
||||
)));
|
||||
}
|
||||
|
||||
let mut data_to_decrypt = full_packet[1..].to_vec();
|
||||
if let Some(decryptor) = &mut self.decryptor {
|
||||
decryptor
|
||||
.decrypt(&mut data_to_decrypt)
|
||||
.map_err(PacketDecodeError::Message)?;
|
||||
}
|
||||
|
||||
let full_packet_payload = data_to_decrypt;
|
||||
let full_packet_payload = &full_packet[1..];
|
||||
|
||||
// If compression is NOT enabled yet, the payload starts at index 0 of full_packet_payload
|
||||
if self.compression.is_none() {
|
||||
let payload = &full_packet_payload[..];
|
||||
let payload = full_packet_payload;
|
||||
if payload.len() > MAX_PACKET_DATA_SIZE {
|
||||
return Err(PacketDecodeError::TooLong);
|
||||
}
|
||||
@@ -217,7 +150,7 @@ impl UDPNetworkDecoder {
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::bedrock::{SubClient, packet_encoder::UDPNetworkEncoder};
|
||||
use crate::bedrock::{SubClient, packet_encoder::BedrockBatchEncoder};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -226,7 +159,7 @@ mod tests {
|
||||
const PAYLOAD_LEN: usize = 2 * 1024 * 1024 + 1;
|
||||
let payload = vec![0x2a; PAYLOAD_LEN];
|
||||
let mut wire_buf = Vec::new();
|
||||
let network_encoder = UDPNetworkEncoder::new();
|
||||
let network_encoder = BedrockBatchEncoder::new();
|
||||
network_encoder
|
||||
.write_game_packet(
|
||||
0x01,
|
||||
@@ -238,7 +171,7 @@ mod tests {
|
||||
.expect("encode Bedrock game packet");
|
||||
|
||||
let mut cursor = Cursor::new(wire_buf[1..].to_vec());
|
||||
let mut decoder = UDPNetworkDecoder::new();
|
||||
let mut decoder = BedrockBatchDecoder::new();
|
||||
|
||||
let packet = decoder
|
||||
.get_game_packet(&mut cursor)
|
||||
|
||||
@@ -1,104 +1,31 @@
|
||||
use std::{
|
||||
io::{self, Error, Write},
|
||||
net::SocketAddr,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use std::io::{Error, Write};
|
||||
|
||||
use flate2::{Compression, write::DeflateEncoder};
|
||||
use tokio::{io::AsyncWrite, net::UdpSocket};
|
||||
|
||||
use crate::{
|
||||
Aes128Cfb8Enc, CompressionLevel, CompressionThreshold, StreamEncryptor, bedrock::SubClient,
|
||||
codec::var_uint::VarUInt, ser::NetworkWriteExt,
|
||||
CompressionLevel, CompressionThreshold,
|
||||
bedrock::{BEDROCK_GAME_PACKET, SubClient},
|
||||
codec::var_uint::VarUInt,
|
||||
ser::NetworkWriteExt,
|
||||
};
|
||||
|
||||
// raw -> compress -> encrypt
|
||||
|
||||
pub enum EncryptionWriter<W: AsyncWrite + Unpin> {
|
||||
Encrypt(Box<StreamEncryptor<W>>),
|
||||
None(W),
|
||||
}
|
||||
|
||||
impl<W: AsyncWrite + Unpin> EncryptionWriter<W> {
|
||||
#[must_use]
|
||||
pub fn upgrade(self, cipher: Aes128Cfb8Enc) -> Self {
|
||||
match self {
|
||||
Self::None(stream) => Self::Encrypt(Box::new(StreamEncryptor::new(cipher, stream))),
|
||||
Self::Encrypt(_) => panic!("Cannot upgrade a stream that already has a cipher!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: AsyncWrite + Unpin> AsyncWrite for EncryptionWriter<W> {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, io::Error>> {
|
||||
match self.get_mut() {
|
||||
Self::Encrypt(writer) => {
|
||||
let writer = Pin::new(writer);
|
||||
writer.poll_write(cx, buf)
|
||||
}
|
||||
Self::None(writer) => {
|
||||
let writer = Pin::new(writer);
|
||||
writer.poll_write(cx, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
|
||||
match self.get_mut() {
|
||||
Self::Encrypt(writer) => {
|
||||
let writer = Pin::new(writer);
|
||||
writer.poll_flush(cx)
|
||||
}
|
||||
Self::None(writer) => {
|
||||
let writer = Pin::new(writer);
|
||||
writer.poll_flush(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
|
||||
match self.get_mut() {
|
||||
Self::Encrypt(writer) => {
|
||||
let writer = Pin::new(writer);
|
||||
writer.poll_shutdown(cx)
|
||||
}
|
||||
Self::None(writer) => {
|
||||
let writer = Pin::new(writer);
|
||||
writer.poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::bedrock::crypto::BedrockEncryptor;
|
||||
|
||||
/// Encoder: Server -> Client
|
||||
/// Supports `ZLib` endecoding/compression
|
||||
/// Supports Aes256 Encryption
|
||||
pub struct UDPNetworkEncoder {
|
||||
/// Supports Zlib compression.
|
||||
pub struct BedrockBatchEncoder {
|
||||
// compression and compression threshold
|
||||
compression: Option<(CompressionThreshold, CompressionLevel)>,
|
||||
encryptor: Option<BedrockEncryptor>,
|
||||
}
|
||||
|
||||
impl Default for UDPNetworkEncoder {
|
||||
impl Default for BedrockBatchEncoder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl UDPNetworkEncoder {
|
||||
impl BedrockBatchEncoder {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
compression: None,
|
||||
encryptor: None,
|
||||
}
|
||||
Self { compression: None }
|
||||
}
|
||||
|
||||
pub const fn set_compression(
|
||||
@@ -108,21 +35,6 @@ impl UDPNetworkEncoder {
|
||||
self.compression = Some(compression_info);
|
||||
}
|
||||
|
||||
pub fn set_encryption(
|
||||
&mut self,
|
||||
key: &[u8; 32],
|
||||
) -> Result<(), crate::bedrock::packet_decoder::EncryptionAlreadyEnabledError> {
|
||||
if self.encryptor.is_some() {
|
||||
return Err(crate::bedrock::packet_decoder::EncryptionAlreadyEnabledError);
|
||||
}
|
||||
self.encryptor = Some(BedrockEncryptor::new(key));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const fn encryptor_mut(&mut self) -> Option<&mut BedrockEncryptor> {
|
||||
self.encryptor.as_mut()
|
||||
}
|
||||
|
||||
pub fn write_game_packet(
|
||||
&self,
|
||||
packet_id: u16,
|
||||
@@ -152,7 +64,7 @@ impl UDPNetworkEncoder {
|
||||
|
||||
// Handle Outer Container
|
||||
writer
|
||||
.write_u8(0xfe)
|
||||
.write_u8(BEDROCK_GAME_PACKET)
|
||||
.map_err(|e| Error::other(e.to_string()))?; // Bedrock Game Packet Header
|
||||
|
||||
let mut data_to_write = Vec::new();
|
||||
@@ -174,26 +86,17 @@ impl UDPNetworkEncoder {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write_packet(
|
||||
&self,
|
||||
packet_data: &[u8],
|
||||
addr: SocketAddr,
|
||||
socket: &UdpSocket,
|
||||
) -> Result<(), Error> {
|
||||
socket.send_to(packet_data, addr).await.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bedrock::packet_decoder::UDPNetworkDecoder;
|
||||
use crate::bedrock::packet_decoder::BedrockBatchDecoder;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[tokio::test]
|
||||
async fn bedrock_compression_cycle() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut encoder = UDPNetworkEncoder::new();
|
||||
let mut encoder = BedrockBatchEncoder::new();
|
||||
encoder.set_compression((256, 6));
|
||||
|
||||
let packet_id = 1;
|
||||
@@ -208,7 +111,7 @@ mod tests {
|
||||
&mut encoded_buf,
|
||||
)?;
|
||||
|
||||
let mut decoder = UDPNetworkDecoder::new();
|
||||
let mut decoder = BedrockBatchDecoder::new();
|
||||
decoder.set_compression(256);
|
||||
|
||||
let decompressed_payload = decoder.get_packet_payload(encoded_buf).await?;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
use pumpkin_macros::packet;
|
||||
|
||||
use crate::serial::PacketRead;
|
||||
|
||||
#[derive(PacketRead)]
|
||||
#[packet(0x04)]
|
||||
pub struct SClientToServerHandshake;
|
||||
@@ -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::*;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use pumpkin_macros::packet;
|
||||
|
||||
use crate::serial::PacketRead;
|
||||
|
||||
/// Sent periodically by a connected client to measure round-trip time.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Connected_Ping>
|
||||
#[derive(PacketRead)]
|
||||
#[packet(0x00)]
|
||||
pub struct SConnectedPing {
|
||||
/// Time since start
|
||||
#[serial(big_endian)]
|
||||
pub time: u64,
|
||||
}
|
||||
|
||||
/// Sent by the client after receiving `OpenConnectionReply2` to request formal session establishment.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Connection_Request>
|
||||
#[derive(PacketRead)]
|
||||
#[packet(0x09)]
|
||||
pub struct SConnectionRequest {
|
||||
#[serial(big_endian)]
|
||||
pub client_guid: u64,
|
||||
#[serial(big_endian)]
|
||||
pub time: u64,
|
||||
pub security: bool,
|
||||
}
|
||||
|
||||
/// Sent by the client to confirm local network address and finish connection establishment.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#New_Incoming_Connection>
|
||||
#[derive(PacketRead)]
|
||||
#[packet(0x13)]
|
||||
pub struct SNewIncomingConnection {
|
||||
pub server_address: SocketAddr,
|
||||
pub internal_address: SocketAddr,
|
||||
#[serial(big_endian)]
|
||||
pub ping_time: u64,
|
||||
#[serial(big_endian)]
|
||||
pub pong_time: u64,
|
||||
}
|
||||
|
||||
/// Sent by the client to notify the server of graceful disconnection.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Disconnection_Notification>
|
||||
#[packet(0x15)]
|
||||
pub struct SDisconnect;
|
||||
|
||||
/// Internal notification signal for a connection lost due to socket error or timeout.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Disconnection_Notification>
|
||||
#[packet(0x16)]
|
||||
pub struct SConnectionLost;
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod connection;
|
||||
pub mod open_connection;
|
||||
pub mod unconnected_ping;
|
||||
@@ -1,31 +0,0 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use pumpkin_macros::packet;
|
||||
|
||||
use crate::serial::PacketRead;
|
||||
|
||||
/// Sent by a connecting client to initiate `RakNet` handshake and check server MTU size.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Open_Connection_Request_1>
|
||||
#[derive(PacketRead)]
|
||||
#[packet(0x05)]
|
||||
pub struct SOpenConnectionRequest1 {
|
||||
pub magic: [u8; 16],
|
||||
pub protocol_version: u8,
|
||||
#[serial(big_endian)]
|
||||
pub mtu: u16,
|
||||
}
|
||||
|
||||
/// Sent by a connecting client following `OpenConnectionReply1` to verify server address, client GUID, and MTU.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Open_Connection_Request_2>
|
||||
#[derive(PacketRead)]
|
||||
#[packet(0x07)]
|
||||
pub struct SOpenConnectionRequest2 {
|
||||
pub magic: [u8; 16],
|
||||
pub server_address: SocketAddr,
|
||||
#[serial(big_endian)]
|
||||
pub mtu: u16,
|
||||
#[serial(big_endian)]
|
||||
pub client_guid: u64,
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use pumpkin_macros::packet;
|
||||
|
||||
use crate::serial::PacketRead;
|
||||
|
||||
/// Sent by an unconnected client to request server information, status, and MOTD.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Unconnected_Ping>
|
||||
#[derive(PacketRead)]
|
||||
#[packet(0x01)]
|
||||
pub struct SUnconnectedPing {
|
||||
#[serial(big_endian)]
|
||||
pub time: u64,
|
||||
pub magic: [u8; 16],
|
||||
#[serial(big_endian)]
|
||||
pub client_guid: u64,
|
||||
}
|
||||
|
||||
/// Sent by a client to query server information when connections are open.
|
||||
///
|
||||
/// Ref: <https://minecraft.wiki/w/RakNet#Unconnected_Ping_Open_Connections>
|
||||
#[derive(PacketRead)]
|
||||
#[packet(0x02)]
|
||||
pub struct SUnconnectedPingOpenConnections {
|
||||
#[serial(big_endian)]
|
||||
pub time: u64,
|
||||
pub magic: [u8; 16],
|
||||
#[serial(big_endian)]
|
||||
pub client_guid: u64,
|
||||
}
|
||||
123
crates/pumpkin-protocol/src/bedrock/status.rs
Normal file
123
crates/pumpkin-protocol/src/bedrock/status.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use core::fmt;
|
||||
use std::io::{Error, Write};
|
||||
|
||||
use pumpkin_macros::packet;
|
||||
|
||||
use crate::serial::{PacketRead, PacketWrite};
|
||||
|
||||
pub const OFFLINE_MESSAGE_MAGIC: [u8; 16] = [
|
||||
0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, 0x56, 0x78,
|
||||
];
|
||||
|
||||
#[derive(PacketRead)]
|
||||
#[packet(0x01)]
|
||||
pub struct SUnconnectedPing {
|
||||
#[serial(big_endian)]
|
||||
pub time: u64,
|
||||
pub magic: [u8; 16],
|
||||
#[serial(big_endian)]
|
||||
pub client_guid: u64,
|
||||
}
|
||||
|
||||
#[derive(PacketRead)]
|
||||
#[packet(0x02)]
|
||||
pub struct SUnconnectedPingOpenConnections {
|
||||
#[serial(big_endian)]
|
||||
pub time: u64,
|
||||
pub magic: [u8; 16],
|
||||
#[serial(big_endian)]
|
||||
pub client_guid: u64,
|
||||
}
|
||||
|
||||
#[packet(0x1c)]
|
||||
pub struct CUnconnectedPong {
|
||||
time: u64,
|
||||
server_guid: u64,
|
||||
magic: [u8; 16],
|
||||
server_id: String,
|
||||
}
|
||||
|
||||
impl CUnconnectedPong {
|
||||
#[must_use]
|
||||
pub const fn new(time: u64, server_guid: u64, server_id: String) -> Self {
|
||||
Self {
|
||||
time,
|
||||
server_guid,
|
||||
magic: OFFLINE_MESSAGE_MAGIC,
|
||||
server_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWrite for CUnconnectedPong {
|
||||
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
|
||||
self.time.write_be(writer)?;
|
||||
self.server_guid.write_be(writer)?;
|
||||
writer.write_all(&self.magic)?;
|
||||
let length = u16::try_from(self.server_id.len())
|
||||
.map_err(|_| Error::other("Bedrock server advertisement is too long"))?;
|
||||
writer.write_all(&length.to_be_bytes())?;
|
||||
writer.write_all(self.server_id.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServerInfo<'a> {
|
||||
pub motd: &'a str,
|
||||
pub protocol: u32,
|
||||
pub version: &'static str,
|
||||
pub players: i32,
|
||||
pub max_players: u32,
|
||||
pub server_guid: u64,
|
||||
pub level_name: &'a str,
|
||||
pub game_mode: &'static str,
|
||||
pub game_mode_id: u32,
|
||||
pub ipv4_port: u16,
|
||||
pub ipv6_port: u16,
|
||||
}
|
||||
|
||||
impl fmt::Display for ServerInfo<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"MCPE;{};{};{};{};{};{};{};{};{};{};{};0;",
|
||||
self.motd,
|
||||
self.protocol,
|
||||
self.version,
|
||||
self.players,
|
||||
self.max_players,
|
||||
self.server_guid,
|
||||
self.level_name,
|
||||
self.game_mode,
|
||||
self.game_mode_id,
|
||||
self.ipv4_port,
|
||||
self.ipv6_port
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn formats_vanilla_26_40_advertisement() {
|
||||
let info = ServerInfo {
|
||||
motd: "Pumpkin",
|
||||
protocol: 2168,
|
||||
version: "1.26.40",
|
||||
players: 2,
|
||||
max_players: 20,
|
||||
server_guid: 42,
|
||||
level_name: "world",
|
||||
game_mode: "Creative",
|
||||
game_mode_id: 1,
|
||||
ipv4_port: 19132,
|
||||
ipv6_port: 19133,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
info.to_string(),
|
||||
"MCPE;Pumpkin;2168;1.26.40;2;20;42;world;Creative;1;19132;19133;0;"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -558,53 +558,3 @@ pub fn extract_cpk_from_token(token: &str) -> Result<PublicKey, AuthError> {
|
||||
|
||||
build_public_key_from_b64(cpk_b64)
|
||||
}
|
||||
|
||||
/// Generates a signed Bedrock handshake JWT containing the server's public key and salt.
|
||||
pub fn generate_handshake_jwt(
|
||||
signing_key: &p384::ecdsa::SigningKey,
|
||||
salt: &[u8],
|
||||
) -> Result<String, AuthError> {
|
||||
use p384::ecdsa::signature::Signer;
|
||||
use p384::pkcs8::EncodePublicKey;
|
||||
|
||||
let public_key = p384::PublicKey::from(signing_key.verifying_key());
|
||||
let der_bytes = public_key
|
||||
.to_public_key_der()
|
||||
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?;
|
||||
|
||||
let x5u = general_purpose::STANDARD.encode(der_bytes.as_bytes());
|
||||
let salt_b64 = general_purpose::STANDARD_NO_PAD.encode(salt);
|
||||
|
||||
let header_json = serde_json::json!({
|
||||
"alg": "ES384",
|
||||
"x5u": x5u
|
||||
});
|
||||
let payload_json = serde_json::json!({
|
||||
"salt": salt_b64
|
||||
});
|
||||
|
||||
let header_b64 = general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header_json)?);
|
||||
let payload_b64 = general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload_json)?);
|
||||
|
||||
let signing_input = format!("{header_b64}.{payload_b64}");
|
||||
|
||||
let signature: p384::ecdsa::Signature = signing_key.sign(signing_input.as_bytes());
|
||||
let signature_bytes = signature.to_bytes();
|
||||
let signature_b64 = general_purpose::URL_SAFE_NO_PAD.encode(signature_bytes);
|
||||
|
||||
Ok(format!("{signing_input}.{signature_b64}"))
|
||||
}
|
||||
|
||||
/// Computes an ECDH shared secret using the server's P-384 signing key and the client's public key.
|
||||
#[must_use]
|
||||
pub fn compute_shared_secret(
|
||||
signing_key: &p384::ecdsa::SigningKey,
|
||||
client_public_key: &p384::PublicKey,
|
||||
) -> [u8; 48] {
|
||||
let secret = p384::SecretKey::from(signing_key);
|
||||
let shared_secret =
|
||||
p384::ecdh::diffie_hellman(secret.to_nonzero_scalar(), client_public_key.as_affine());
|
||||
let mut secret_bytes = [0u8; 48];
|
||||
secret_bytes.copy_from_slice(&shared_secret.raw_secret_bytes()[..]);
|
||||
secret_bytes
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::logging::{GzipRollingLogger, PumpkinCommandCompleter, ReadlineLogWrap
|
||||
use crate::net::bedrock::{
|
||||
BedrockClient,
|
||||
nethernet::{NetherNetListener, load_or_create_identity_key},
|
||||
status::StatusResponder,
|
||||
};
|
||||
use crate::net::java::JavaClient;
|
||||
use crate::net::java::pending::PendingConnection;
|
||||
@@ -25,14 +26,14 @@ use rustyline::Editor;
|
||||
use rustyline::history::FileHistory;
|
||||
use rustyline::{Config, error::ReadlineError};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Cursor, ErrorKind, IsTerminal, stdin};
|
||||
use std::io::{ErrorKind, IsTerminal, stdin};
|
||||
use std::process::exit;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
use std::{net::SocketAddr, sync::LazyLock};
|
||||
use tokio::net::{TcpListener, UdpSocket};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::select;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
@@ -210,7 +211,7 @@ fn resolve_some<T: Future, D, F: FnOnce(D) -> T>(
|
||||
pub struct PumpkinServer {
|
||||
pub server: Arc<Server>,
|
||||
pub tcp_listener: Option<TcpListener>,
|
||||
pub udp_socket: Option<Arc<UdpSocket>>,
|
||||
pub bedrock_status: Option<StatusResponder>,
|
||||
pub nethernet_listener: Option<NetherNetListener>,
|
||||
}
|
||||
|
||||
@@ -299,22 +300,13 @@ impl PumpkinServer {
|
||||
});
|
||||
};
|
||||
|
||||
let udp_socket = if server.advanced_config.networking.bedrock.enabled {
|
||||
Some(Arc::new(
|
||||
UdpSocket::bind(server.advanced_config.networking.bedrock.address)
|
||||
.await
|
||||
.expect("Failed to bind UDP Socket"),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let nethernet_listener = Self::bind_nethernet(&server).await;
|
||||
let bedrock_status = Self::bind_bedrock_status(&server, nethernet_listener.is_some()).await;
|
||||
|
||||
Self {
|
||||
server,
|
||||
tcp_listener,
|
||||
udp_socket,
|
||||
bedrock_status,
|
||||
nethernet_listener,
|
||||
}
|
||||
}
|
||||
@@ -346,6 +338,21 @@ impl PumpkinServer {
|
||||
)
|
||||
}
|
||||
|
||||
async fn bind_bedrock_status(server: &Server, enabled: bool) -> Option<StatusResponder> {
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
let responder =
|
||||
StatusResponder::bind(server.advanced_config.networking.bedrock.nethernet.address)
|
||||
.await
|
||||
.expect("Failed to bind Bedrock server-list status");
|
||||
let (ipv4, ipv6) = responder
|
||||
.local_addrs()
|
||||
.expect("Bedrock status sockets should have local addresses");
|
||||
info!("Bedrock server-list status is listening on {ipv4} (IPv4) and {ipv6} (IPv6)");
|
||||
Some(responder)
|
||||
}
|
||||
|
||||
pub async fn init_plugins(&self) -> std::time::Duration {
|
||||
match self.server.plugin_manager.load_plugins(&self.server).await {
|
||||
Ok(duration) => duration,
|
||||
@@ -468,8 +475,6 @@ impl PumpkinServer {
|
||||
tasks: &Arc<TaskTracker>,
|
||||
bedrock_clients: &Arc<Mutex<HashMap<SocketAddr, Arc<BedrockClient>>>>,
|
||||
) -> bool {
|
||||
let mut udp_buf = [0; 1496]; // Buffer for UDP receive
|
||||
|
||||
select! {
|
||||
// Branch for TCP connections (Java Edition)
|
||||
tcp_result = resolve_some(self.tcp_listener.as_ref(), tokio::net::TcpListener::accept) => {
|
||||
@@ -542,68 +547,14 @@ impl PumpkinServer {
|
||||
}
|
||||
},
|
||||
|
||||
// Branch for UDP packets (Bedrock Edition)
|
||||
udp_result = resolve_some(self.udp_socket.as_ref(), |sock: &Arc<UdpSocket>| sock.recv_from(&mut udp_buf)) => {
|
||||
match udp_result {
|
||||
Ok((len, client_addr)) => {
|
||||
if len > 0 {
|
||||
let Some(socket) = self.udp_socket.clone() else {
|
||||
error!("UDP socket disappeared during receive");
|
||||
return true;
|
||||
};
|
||||
|
||||
let id = udp_buf[0];
|
||||
let is_online = id & pumpkin_protocol::bedrock::RAKNET_VALID != 0;
|
||||
|
||||
if is_online {
|
||||
let be_clients = bedrock_clients.clone();
|
||||
let mut clients_guard = bedrock_clients.lock().await;
|
||||
|
||||
if clients_guard
|
||||
.get(&client_addr)
|
||||
.is_some_and(|client| client.is_closed())
|
||||
{
|
||||
clients_guard.remove(&client_addr);
|
||||
}
|
||||
|
||||
let mut is_new = false;
|
||||
let client = clients_guard.entry(client_addr).or_insert_with(|| {
|
||||
is_new = true;
|
||||
*master_client_id_counter += 1;
|
||||
|
||||
let new_client = Arc::new(BedrockClient::new(
|
||||
socket,
|
||||
client_addr,
|
||||
be_clients
|
||||
));
|
||||
|
||||
new_client.start_outgoing_packet_task();
|
||||
new_client
|
||||
}).clone();
|
||||
|
||||
if is_new {
|
||||
self.spawn_bedrock_client_task(client.clone(), tasks);
|
||||
}
|
||||
|
||||
let packet_bytes = udp_buf[..len].to_vec();
|
||||
let server = self.server.clone();
|
||||
|
||||
tasks.spawn(async move {
|
||||
client.process_packet(&server, packet_bytes.into()).await;
|
||||
});
|
||||
} else if let Some(sock) = self.udp_socket.as_ref() {
|
||||
let _ = BedrockClient::handle_offline_packet(
|
||||
&self.server,
|
||||
id,
|
||||
&mut Cursor::new(&udp_buf[1..len]),
|
||||
client_addr,
|
||||
sock,
|
||||
bedrock_clients,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => error!("UDP socket error: {e}"),
|
||||
// Remote server-list status remains a RakNet unconnected ping/pong even
|
||||
// when the game connection itself is negotiated over NetherNet.
|
||||
status_result = resolve_some(
|
||||
self.bedrock_status.as_ref(),
|
||||
|status: &StatusResponder| status.receive(&self.server),
|
||||
) => {
|
||||
if let Err(error) = status_result {
|
||||
debug!("Bedrock status packet failed: {error}");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -612,7 +563,7 @@ impl PumpkinServer {
|
||||
if let Some((session, client_addr)) = nethernet_result {
|
||||
*master_client_id_counter += 1;
|
||||
let be_clients = bedrock_clients.clone();
|
||||
let client = Arc::new(BedrockClient::new_nethernet(
|
||||
let client = Arc::new(BedrockClient::new(
|
||||
session.clone(),
|
||||
client_addr,
|
||||
be_clients,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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");
|
||||
//});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ use crate::{
|
||||
server::Server,
|
||||
};
|
||||
use arc_swap::ArcSwap;
|
||||
use pumpkin_protocol::bedrock::client::handshake::CHandshake;
|
||||
use pumpkin_protocol::bedrock::{
|
||||
client::{
|
||||
network_settings::CNetworkSettings, play_status::CPlayStatus,
|
||||
@@ -21,10 +20,8 @@ use pumpkin_protocol::bedrock::{
|
||||
use pumpkin_util::jwt::AuthError;
|
||||
use pumpkin_util::version::BedrockMinecraftVersion;
|
||||
use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
|
||||
use rand::RngExt;
|
||||
use serde::{Deserialize, de::Error};
|
||||
use serde_repr::Deserialize_repr;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use tracing::debug;
|
||||
@@ -126,16 +123,15 @@ impl BedrockClient {
|
||||
self: &Arc<Self>,
|
||||
packet: SLogin,
|
||||
server: &Server,
|
||||
) -> Result<Option<PacketHandlerResult>, LoginError> {
|
||||
) -> Result<PacketHandlerResult, LoginError> {
|
||||
self.try_handle_login(packet, server).await
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
pub async fn try_handle_login(
|
||||
self: &Arc<Self>,
|
||||
packet: SLogin,
|
||||
server: &Server,
|
||||
) -> Result<Option<PacketHandlerResult>, LoginError> {
|
||||
) -> Result<PacketHandlerResult, LoginError> {
|
||||
let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?;
|
||||
let player_data = if server.advanced_config.networking.bedrock.online_mode {
|
||||
match auth_payload.authentication_type {
|
||||
@@ -188,93 +184,14 @@ impl BedrockClient {
|
||||
profile_actions: None,
|
||||
};
|
||||
|
||||
if let Some(peer_public_key) = self.nethernet_public_key() {
|
||||
let login_public_key = pumpkin_util::jwt::extract_cpk_from_token(&auth_payload.token)
|
||||
.map_err(LoginError::ChainValidationFailed)?;
|
||||
if peer_public_key != &login_public_key {
|
||||
return Err(LoginError::ChainValidationFailed(
|
||||
AuthError::PublicKeyBuild(
|
||||
"NetherNet and Bedrock login identities do not match".into(),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if server.advanced_config.networking.bedrock.encryption && !self.is_nethernet() {
|
||||
let client_public_key = pumpkin_util::jwt::extract_cpk_from_token(&auth_payload.token)
|
||||
.map_err(LoginError::ChainValidationFailed)?;
|
||||
|
||||
let (server_private_key, salt) = {
|
||||
let server_key_arc = server
|
||||
.bedrock_private_key
|
||||
.get_or_init(|| async {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let mut private_key_bytes = [0u8; 48];
|
||||
for b in &mut private_key_bytes {
|
||||
*b = rng.random();
|
||||
}
|
||||
if let Ok(key) = pumpkin_util::p384::ecdsa::SigningKey::from_slice(
|
||||
&private_key_bytes,
|
||||
) {
|
||||
break Arc::new(key);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.clone();
|
||||
|
||||
let mut salt = [0u8; 16];
|
||||
for b in &mut salt {
|
||||
*b = rand::rng().random();
|
||||
}
|
||||
|
||||
(server_key_arc, salt)
|
||||
};
|
||||
|
||||
let handshake_jwt =
|
||||
pumpkin_util::jwt::generate_handshake_jwt(&server_private_key, &salt)
|
||||
.map_err(LoginError::ChainValidationFailed)?;
|
||||
let handshake_packet = CHandshake::new(handshake_jwt);
|
||||
self.send_game_packet(&handshake_packet).await;
|
||||
|
||||
let shared_secret =
|
||||
pumpkin_util::jwt::compute_shared_secret(&server_private_key, &client_public_key);
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(salt);
|
||||
hasher.update(shared_secret);
|
||||
let key_bytes: [u8; 32] = hasher.finalize().into();
|
||||
|
||||
self.network_reader
|
||||
.lock()
|
||||
.await
|
||||
.set_encryption(&key_bytes)
|
||||
.map_err(|_| {
|
||||
LoginError::ChainValidationFailed(AuthError::PublicKeyBuild(
|
||||
"encryption enable error".into(),
|
||||
))
|
||||
})?;
|
||||
self.network_writer
|
||||
.write()
|
||||
.await
|
||||
.set_encryption(&key_bytes)
|
||||
.map_err(|_| {
|
||||
LoginError::ChainValidationFailed(AuthError::PublicKeyBuild(
|
||||
"encryption enable error".into(),
|
||||
))
|
||||
})?;
|
||||
|
||||
let new_config = PlayerConfig {
|
||||
locale: client_data.language_code.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
self.client_data
|
||||
.store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data))));
|
||||
self.pending_profile
|
||||
.store(std::sync::Arc::new(Some(std::sync::Arc::new((
|
||||
profile, new_config,
|
||||
)))));
|
||||
return Ok(None);
|
||||
let login_public_key = pumpkin_util::jwt::extract_cpk_from_token(&auth_payload.token)
|
||||
.map_err(LoginError::ChainValidationFailed)?;
|
||||
if self.nethernet_public_key() != &login_public_key {
|
||||
return Err(LoginError::ChainValidationFailed(
|
||||
AuthError::PublicKeyBuild(
|
||||
"NetherNet and Bedrock login identities do not match".into(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
self.enqueue_packet_internal(&CPlayStatus::LoginSuccess)
|
||||
@@ -318,7 +235,7 @@ impl BedrockClient {
|
||||
self.client_data
|
||||
.store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data))));
|
||||
|
||||
Ok(Some(PacketHandlerResult::ReadyToPlay(profile, new_config)))
|
||||
Ok(PacketHandlerResult::ReadyToPlay(profile, new_config))
|
||||
}
|
||||
|
||||
pub async fn handle_resource_pack_response(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
136
crates/pumpkin/src/net/bedrock/status.rs
Normal file
136
crates/pumpkin/src/net/bedrock/status.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use std::{
|
||||
io::{Cursor, Error},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
};
|
||||
|
||||
use pumpkin_protocol::{
|
||||
BClientPacket,
|
||||
bedrock::status::{
|
||||
CUnconnectedPong, OFFLINE_MESSAGE_MAGIC, SUnconnectedPing, SUnconnectedPingOpenConnections,
|
||||
ServerInfo,
|
||||
},
|
||||
packet::Packet,
|
||||
serial::PacketRead,
|
||||
};
|
||||
use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
use crate::server::Server;
|
||||
|
||||
pub struct StatusResponder {
|
||||
ipv4: UdpSocket,
|
||||
ipv6: UdpSocket,
|
||||
ipv4_port: u16,
|
||||
ipv6_port: u16,
|
||||
}
|
||||
|
||||
impl StatusResponder {
|
||||
pub async fn bind(address: SocketAddr) -> Result<Self, Error> {
|
||||
let ipv4_ip = match address.ip() {
|
||||
IpAddr::V4(ip) => ip,
|
||||
IpAddr::V6(_) => Ipv4Addr::UNSPECIFIED,
|
||||
};
|
||||
let ipv4_port = address.port();
|
||||
let ipv6_port = ipv4_port.saturating_add(1);
|
||||
Ok(Self {
|
||||
ipv4: UdpSocket::bind((ipv4_ip, ipv4_port)).await?,
|
||||
ipv6: UdpSocket::bind((Ipv6Addr::UNSPECIFIED, ipv6_port)).await?,
|
||||
ipv4_port,
|
||||
ipv6_port,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn local_addrs(&self) -> Result<(SocketAddr, SocketAddr), Error> {
|
||||
Ok((self.ipv4.local_addr()?, self.ipv6.local_addr()?))
|
||||
}
|
||||
|
||||
pub async fn receive(&self, server: &Server) -> Result<(), Error> {
|
||||
let mut ipv4_buffer = [0; 64];
|
||||
let mut ipv6_buffer = [0; 64];
|
||||
tokio::select! {
|
||||
result = self.ipv4.recv_from(&mut ipv4_buffer) => {
|
||||
let (length, client) = result?;
|
||||
self.respond(server, &self.ipv4, &ipv4_buffer[..length], client).await
|
||||
}
|
||||
result = self.ipv6.recv_from(&mut ipv6_buffer) => {
|
||||
let (length, client) = result?;
|
||||
self.respond(server, &self.ipv6, &ipv6_buffer[..length], client).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn respond(
|
||||
&self,
|
||||
server: &Server,
|
||||
socket: &UdpSocket,
|
||||
packet: &[u8],
|
||||
client: SocketAddr,
|
||||
) -> Result<(), Error> {
|
||||
let Some((&packet_id, payload)) = packet.split_first() else {
|
||||
return Ok(());
|
||||
};
|
||||
handle_packet(
|
||||
server,
|
||||
packet_id,
|
||||
payload,
|
||||
client,
|
||||
socket,
|
||||
self.ipv4_port,
|
||||
self.ipv6_port,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_packet(
|
||||
server: &Server,
|
||||
packet_id: u8,
|
||||
payload: &[u8],
|
||||
client: SocketAddr,
|
||||
socket: &UdpSocket,
|
||||
ipv4_port: u16,
|
||||
ipv6_port: u16,
|
||||
) -> Result<(), Error> {
|
||||
let (time, magic) = match i32::from(packet_id) {
|
||||
SUnconnectedPing::PACKET_ID => {
|
||||
let packet = SUnconnectedPing::read(&mut Cursor::new(payload))?;
|
||||
(packet.time, packet.magic)
|
||||
}
|
||||
SUnconnectedPingOpenConnections::PACKET_ID => {
|
||||
let packet = SUnconnectedPingOpenConnections::read(&mut Cursor::new(payload))?;
|
||||
(packet.time, packet.magic)
|
||||
}
|
||||
_ => return Ok(()),
|
||||
};
|
||||
if magic != OFFLINE_MESSAGE_MAGIC {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let players = server
|
||||
.get_status()
|
||||
.lock()
|
||||
.await
|
||||
.status_response
|
||||
.players
|
||||
.as_ref()
|
||||
.map_or(0, |players| players.online) as i32;
|
||||
let game_mode = server.defaultgamemode.lock().await.gamemode;
|
||||
let info = ServerInfo {
|
||||
motd: &server.advanced_config.networking.bedrock.motd,
|
||||
protocol: CURRENT_BEDROCK_MC_PROTOCOL,
|
||||
version: CURRENT_BEDROCK_MC_VERSION,
|
||||
players,
|
||||
max_players: server.advanced_config.networking.bedrock.max_players,
|
||||
server_guid: server.server_guid,
|
||||
level_name: &server.basic_config.default_level_name,
|
||||
game_mode: game_mode.to_str(),
|
||||
game_mode_id: 1,
|
||||
ipv4_port,
|
||||
ipv6_port,
|
||||
};
|
||||
let pong = CUnconnectedPong::new(time, server.server_guid, info.to_string());
|
||||
let mut response = vec![CUnconnectedPong::PACKET_ID as u8];
|
||||
pong.write_packet(&mut response)?;
|
||||
socket.send_to(&response, client).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user