Add the packet inspector proxy

This commit is contained in:
Ryan 2022-07-01 15:29:31 -07:00
parent 9a87fda211
commit a259bdf840
8 changed files with 425 additions and 196 deletions

View file

@ -36,7 +36,7 @@ sha1 = "0.10"
sha2 = "0.10"
thiserror = "1"
tokio = { version = "1", features = ["full"] }
url = {version = "2.2.2", features = ["serde"] }
url = { version = "2.2.2", features = ["serde"] }
uuid = "1"
vek = "0.15"
@ -55,9 +55,12 @@ anyhow = "1"
heck = "0.4"
proc-macro2 = "1"
quote = "1"
serde = {version = "1", features = ["derive"]}
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[features]
# Exposes the raw protocol API
protocol = []
[workspace]
members = ["packet-inspector"]

View file

@ -0,0 +1,12 @@
[package]
name = "packet-inspector"
version = "0.1.0"
edition = "2021"
description = "A simple Minecraft proxy for inspecting packets."
[dependencies]
valence = { path = "..", features = ["protocol"] }
clap = { version = "3.2.8", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
chrono = "0.4.19"

View file

@ -0,0 +1,197 @@
use std::error::Error;
use std::fmt;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::bail;
use chrono::{Utc, DateTime};
use clap::Parser;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Semaphore;
use valence::protocol::codec::{Decoder, Encoder};
use valence::protocol::packets::handshake::{Handshake, HandshakeNextState};
use valence::protocol::packets::login::c2s::{EncryptionResponse, LoginStart};
use valence::protocol::packets::login::s2c::{LoginSuccess, S2cLoginPacket};
use valence::protocol::packets::play::c2s::C2sPlayPacket;
use valence::protocol::packets::play::s2c::S2cPlayPacket;
use valence::protocol::packets::status::c2s::{PingRequest, StatusRequest};
use valence::protocol::packets::status::s2c::{PongResponse, StatusResponse};
use valence::protocol::packets::{DecodePacket, EncodePacket};
#[derive(Parser, Clone, Debug)]
#[clap(author, version, about)]
struct Cli {
/// The socket address to listen for connections on. This is the address
/// clients should connect to.
client: SocketAddr,
/// The socket address the proxy will connect to.
server: SocketAddr,
/// The maximum number of connections allowed to the proxy. By default,
/// there is no limit.
#[clap(short, long)]
max_connections: Option<usize>,
/// When enabled, prints a timestamp before each packet.
#[clap(short, long)]
timestamp: bool,
}
impl Cli {
fn print(&self, d: &impl fmt::Debug) {
if self.timestamp {
let now: DateTime<Utc> = Utc::now();
println!("{now} {d:?}");
} else {
println!("{d:?}");
}
}
async fn rw_packet<P: DecodePacket + EncodePacket>(
&self,
read: &mut Decoder<OwnedReadHalf>,
write: &mut Encoder<OwnedWriteHalf>,
) -> anyhow::Result<P> {
let pkt = read.read_packet().await?;
self.print(&pkt);
write.write_packet(&pkt).await?;
Ok(pkt)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
let sema = Arc::new(Semaphore::new(
cli.max_connections.unwrap_or(usize::MAX).min(100_000),
));
eprintln!("Waiting for connections on {}", cli.client);
let listen = TcpListener::bind(cli.client).await?;
while let Ok(permit) = sema.clone().acquire_owned().await {
let (client, remote_client_addr) = listen.accept().await?;
eprintln!("Accepted connection to {remote_client_addr}");
let cli = cli.clone();
tokio::spawn(async move {
if let Err(e) = handle_connection(client, cli).await {
eprintln!("Connection to {remote_client_addr} ended with: {e:#}");
} else {
eprintln!("Connection to {remote_client_addr} ended.");
}
drop(permit);
});
}
Ok(())
}
async fn handle_connection(client: TcpStream, cli: Cli) -> anyhow::Result<()> {
eprintln!("Connecting to {}", cli.server);
let server = TcpStream::connect(cli.server).await?;
let (client_read, client_write) = client.into_split();
let (server_read, server_write) = server.into_split();
let timeout = Duration::from_secs(10);
let mut client_read = Decoder::new(client_read, timeout);
let mut client_write = Encoder::new(client_write, timeout);
let mut server_read = Decoder::new(server_read, timeout);
let mut server_write = Encoder::new(server_write, timeout);
let handshake: Handshake = cli.rw_packet(&mut client_read, &mut server_write).await?;
match handshake.next_state {
HandshakeNextState::Status => {
cli.rw_packet::<StatusRequest>(&mut client_read, &mut server_write)
.await?;
cli.rw_packet::<StatusResponse>(&mut server_read, &mut client_write)
.await?;
cli.rw_packet::<PingRequest>(&mut client_read, &mut server_write)
.await?;
cli.rw_packet::<PongResponse>(&mut server_read, &mut client_write)
.await?;
}
HandshakeNextState::Login => {
cli.rw_packet::<LoginStart>(&mut client_read, &mut server_write)
.await?;
match cli
.rw_packet::<S2cLoginPacket>(&mut server_read, &mut client_write)
.await?
{
S2cLoginPacket::EncryptionRequest(_) => {
cli.rw_packet::<EncryptionResponse>(&mut client_read, &mut server_write)
.await?;
eprintln!("Encryption was enabled! I can't see what's going on anymore.");
return tokio::select! {
c2s = passthrough(client_read.into_inner(), server_write.into_inner()) => c2s,
s2c = passthrough(server_read.into_inner(), client_write.into_inner()) => s2c,
};
}
S2cLoginPacket::SetCompression(pkt) => {
let threshold = pkt.threshold.0 as u32;
client_read.enable_compression(threshold);
client_write.enable_compression(threshold);
server_read.enable_compression(threshold);
server_write.enable_compression(threshold);
cli.rw_packet::<LoginSuccess>(&mut server_read, &mut client_write)
.await?;
}
S2cLoginPacket::LoginSuccess(_) => {}
S2cLoginPacket::Disconnect(_) => return Ok(()),
S2cLoginPacket::LoginPluginRequest(_) => {
bail!("got login plugin request. Don't know how to proceed.")
}
}
let c2s = async {
loop {
cli.rw_packet::<C2sPlayPacket>(&mut client_read, &mut server_write)
.await?;
}
};
let s2c = async {
loop {
cli.rw_packet::<S2cPlayPacket>(&mut server_read, &mut client_write)
.await?;
}
};
return tokio::select! {
c2s = c2s => c2s,
s2c = s2c => s2c,
};
}
}
Ok(())
}
async fn passthrough(mut read: OwnedReadHalf, mut write: OwnedWriteHalf) -> anyhow::Result<()> {
let mut buf = vec![0u8; 4096].into_boxed_slice();
loop {
let bytes_read = read.read(&mut buf).await?;
let bytes = &mut buf[..bytes_read];
if bytes.is_empty() {
break;
}
write.write_all(bytes).await?;
}
Ok(())
}

View file

@ -21,6 +21,7 @@ pub mod ident;
mod player_list;
pub mod player_textures;
#[cfg(not(feature = "protocol"))]
#[allow(unused)]
mod protocol;
#[cfg(feature = "protocol")]
pub mod protocol;

View file

@ -99,6 +99,10 @@ impl<W: AsyncWrite + Unpin> Encoder<W> {
pub fn enable_compression(&mut self, threshold: u32) {
self.compression_threshold = Some(threshold);
}
pub fn into_inner(self) -> W {
self.write
}
}
pub struct Decoder<R> {
@ -222,6 +226,10 @@ impl<R: AsyncRead + Unpin> Decoder<R> {
pub fn enable_compression(&mut self, threshold: u32) {
self.compression_threshold = Some(threshold);
}
pub fn into_inner(self) -> R {
self.read
}
}
/// The AES block cipher with a 128 bit key, using the CFB-8 mode of

View file

@ -321,6 +321,66 @@ macro_rules! def_bitfield {
}
}
macro_rules! def_packet_group {
(
$(#[$attrs:meta])*
$group_name:ident {
$($packet:ident),* $(,)?
}
) => {
#[derive(Clone, Debug)]
$(#[$attrs])*
pub enum $group_name {
$($packet($packet)),*
}
$(
impl From<$packet> for $group_name {
fn from(p: $packet) -> Self {
Self::$packet(p)
}
}
)*
impl DecodePacket for $group_name {
fn decode_packet(r: &mut impl Read) -> anyhow::Result<Self> {
let packet_id = VarInt::decode(r)
.context(concat!("failed to read ", stringify!($group_name), " packet ID"))?.0;
match packet_id {
$(
$packet::PACKET_ID => {
let pkt = $packet::decode(r)?;
Ok(Self::$packet(pkt))
}
)*
id => bail!(concat!("unknown ", stringify!($group_name), " packet ID {:#04x}"), id),
}
}
}
impl EncodePacket for $group_name {
fn encode_packet(&self, w: &mut impl Write) -> anyhow::Result<()> {
match self {
$(
Self::$packet(pkt) => {
VarInt($packet::PACKET_ID)
.encode(w)
.context(concat!(
"failed to write ",
stringify!($group_name),
" packet ID for ",
stringify!($packet_name)
))?;
pkt.encode(w)
}
)*
}
}
}
}
}
def_struct! {
#[derive(PartialEq, Serialize, Deserialize)]
Property {
@ -366,7 +426,7 @@ pub mod status {
use super::super::*;
def_struct! {
Response 0x00 {
StatusResponse 0x00 {
json_response: String
}
}
@ -428,6 +488,24 @@ pub mod login {
threshold: VarInt
}
}
def_struct! {
LoginPluginRequest 0x04 {
message_id: VarInt,
channel: Ident,
data: RawBytes,
}
}
def_packet_group! {
S2cLoginPacket {
Disconnect,
EncryptionRequest,
LoginSuccess,
SetCompression,
LoginPluginRequest,
}
}
}
pub mod c2s {
@ -460,6 +538,21 @@ pub mod login {
sig: Vec<u8>, // TODO: bounds?
}
}
def_struct! {
LoginPluginResponse 0x02 {
message_id: VarInt,
data: Option<RawBytes>,
}
}
def_packet_group! {
C2sLoginPacket {
LoginStart,
EncryptionResponse,
LoginPluginResponse,
}
}
}
}
@ -1081,99 +1174,46 @@ pub mod play {
}
}
macro_rules! def_s2c_play_packet_enum {
{
$($packet:ident),* $(,)?
} => {
/// An enum of all s2c play packets.
#[derive(Clone, Debug)]
pub enum S2cPlayPacket {
$($packet($packet)),*
}
$(
impl From<$packet> for S2cPlayPacket {
fn from(p: $packet) -> S2cPlayPacket {
S2cPlayPacket::$packet(p)
}
}
)*
impl EncodePacket for S2cPlayPacket {
fn encode_packet(&self, w: &mut impl Write) -> anyhow::Result<()> {
match self {
$(
Self::$packet(p) => {
VarInt($packet::PACKET_ID)
.encode(w)
.context(concat!("failed to write s2c play packet ID for `", stringify!($packet), "`"))?;
p.encode(w)
}
)*
}
}
}
#[cfg(test)]
#[test]
fn s2c_play_packet_order() {
let ids = [
$(
(stringify!($packet), $packet::PACKET_ID),
)*
];
if let Some(w) = ids.windows(2).find(|w| w[0].1 >= w[1].1) {
panic!(
"the {} (ID {:#x}) and {} (ID {:#x}) variants of the s2c play packet enum are not properly sorted by their packet ID",
w[0].0,
w[0].1,
w[1].0,
w[1].1
);
}
}
def_packet_group! {
S2cPlayPacket {
AddEntity,
AddExperienceOrb,
AddPlayer,
Animate,
BlockChangeAck,
BlockDestruction,
BlockEntityData,
BlockEvent,
BlockUpdate,
BossEvent,
Disconnect,
EntityEvent,
ForgetLevelChunk,
GameEvent,
KeepAlive,
LevelChunkWithLight,
Login,
MoveEntityPosition,
MoveEntityPositionAndRotation,
MoveEntityRotation,
PlayerChat,
PlayerInfo,
PlayerPosition,
RemoveEntities,
RotateHead,
SectionBlocksUpdate,
SetCarriedItem,
SetChunkCacheCenter,
SetChunkCacheRadius,
SpawnPosition,
SetEntityMetadata,
SetEntityMotion,
SetTime,
SystemChat,
TabList,
TeleportEntity,
}
}
def_s2c_play_packet_enum! {
AddEntity,
AddExperienceOrb,
AddPlayer,
Animate,
BlockChangeAck,
BlockDestruction,
BlockEntityData,
BlockEvent,
BlockUpdate,
BossEvent,
Disconnect,
EntityEvent,
ForgetLevelChunk,
GameEvent,
KeepAlive,
LevelChunkWithLight,
Login,
MoveEntityPosition,
MoveEntityPositionAndRotation,
MoveEntityRotation,
PlayerChat,
PlayerInfo,
PlayerPosition,
RemoveEntities,
RotateHead,
SectionBlocksUpdate,
SetCarriedItem,
SetChunkCacheCenter,
SetChunkCacheRadius,
SpawnPosition,
SetEntityMetadata,
SetEntityMotion,
SetTime,
SystemChat,
TabList,
TeleportEntity,
}
}
pub mod c2s {
@ -1737,104 +1777,58 @@ pub mod play {
}
}
macro_rules! def_c2s_play_packet_enum {
{
$($packet:ident),* $(,)?
} => {
/// An enum of all client-to-server play packets.
#[derive(Clone, Debug)]
pub enum C2sPlayPacket {
$($packet($packet)),*
def_packet_group! {
C2sPlayPacket {
AcceptTeleportation,
BlockEntityTagQuery,
ChangeDifficulty,
ChatCommand,
Chat,
ChatPreview,
ClientCommand,
ClientInformation,
CommandSuggestion,
ContainerButtonClick,
ContainerClose,
CustomPayload,
EditBook,
EntityTagQuery,
Interact,
JigsawGenerate,
KeepAlive,
LockDifficulty,
MovePlayerPosition,
MovePlayerPositionAndRotation,
MovePlayerRotation,
MovePlayerStatusOnly,
MoveVehicle,
PaddleBoat,
PickItem,
PlaceRecipe,
PlayerAbilities,
PlayerAction,
PlayerCommand,
PlayerInput,
Pong,
RecipeBookChangeSettings,
RecipeBookSeenRecipe,
RenameItem,
ResourcePack,
SeenAdvancements,
SelectTrade,
SetBeacon,
SetCarriedItem,
SetCommandBlock,
SetCommandBlockMinecart,
SetCreativeModeSlot,
SetJigsawBlock,
SetStructureBlock,
SignUpdate,
Swing,
TeleportToEntity,
UseItemOn,
UseItem,
}
impl DecodePacket for C2sPlayPacket {
fn decode_packet(r: &mut impl Read) -> anyhow::Result<C2sPlayPacket> {
let packet_id = VarInt::decode(r).context("failed to read c2s play packet ID")?.0;
match packet_id {
$(
$packet::PACKET_ID => {
let pkt = $packet::decode(r)?;
Ok(C2sPlayPacket::$packet(pkt))
}
)*
id => bail!("unknown c2s play packet ID {:#04x}", id)
}
}
}
#[cfg(test)]
#[test]
fn c2s_play_packet_order() {
let ids = [
$(
(stringify!($packet), $packet::PACKET_ID),
)*
];
if let Some(w) = ids.windows(2).find(|w| w[0].1 >= w[1].1) {
panic!(
"the {} (ID {:#x}) and {} (ID {:#x}) variants of the c2s play packet enum are not properly sorted by their packet ID",
w[0].0,
w[0].1,
w[1].0,
w[1].1
);
}
}
}
}
def_c2s_play_packet_enum! {
AcceptTeleportation,
BlockEntityTagQuery,
ChangeDifficulty,
ChatCommand,
Chat,
ChatPreview,
ClientCommand,
ClientInformation,
CommandSuggestion,
ContainerButtonClick,
ContainerClose,
CustomPayload,
EditBook,
EntityTagQuery,
Interact,
JigsawGenerate,
KeepAlive,
LockDifficulty,
MovePlayerPosition,
MovePlayerPositionAndRotation,
MovePlayerRotation,
MovePlayerStatusOnly,
MoveVehicle,
PaddleBoat,
PickItem,
PlaceRecipe,
PlayerAbilities,
PlayerAction,
PlayerCommand,
PlayerInput,
Pong,
RecipeBookChangeSettings,
RecipeBookSeenRecipe,
RenameItem,
ResourcePack,
SeenAdvancements,
SelectTrade,
SetBeacon,
SetCarriedItem,
SetCommandBlock,
SetCommandBlockMinecart,
SetCreativeModeSlot,
SetJigsawBlock,
SetStructureBlock,
SignUpdate,
Swing,
TeleportToEntity,
UseItemOn,
UseItem,
}
}
}

View file

@ -34,7 +34,7 @@ use crate::protocol::packets::login::s2c::{EncryptionRequest, LoginSuccess, SetC
use crate::protocol::packets::play::c2s::C2sPlayPacket;
use crate::protocol::packets::play::s2c::S2cPlayPacket;
use crate::protocol::packets::status::c2s::{PingRequest, StatusRequest};
use crate::protocol::packets::status::s2c::{PongResponse, Response};
use crate::protocol::packets::status::s2c::{PongResponse, StatusResponse};
use crate::protocol::packets::{login, Property};
use crate::protocol::{BoundedArray, BoundedString, VarInt};
use crate::util::valid_username;
@ -547,7 +547,7 @@ async fn handle_status(
.insert("favicon".to_string(), Value::String(buf));
}
c.0.write_packet(&Response {
c.0.write_packet(&StatusResponse {
json_response: json.to_string(),
})
.await?;

View file

@ -275,8 +275,13 @@ pub trait TextFormat: Into<Text> {
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum TextContent {
Text { text: Cow<'static, str> },
// TODO: translate
Text {
text: Cow<'static, str>,
},
Translate {
translate: Cow<'static, str>,
// TODO: 'with' field
},
// TODO: score
// TODO: entity names
// TODO: keybind
@ -320,15 +325,24 @@ enum HoverEvent {
},
}
#[allow(clippy::self_named_constructors)]
impl Text {
pub fn text(plain: impl Into<Cow<'static, str>>) -> Self {
#![allow(clippy::self_named_constructors)]
Self {
content: TextContent::Text { text: plain.into() },
..Self::default()
}
}
pub fn translate(key: impl Into<Cow<'static, str>>) -> Self {
Self {
content: TextContent::Translate {
translate: key.into(),
},
..Self::default()
}
}
pub fn to_plain(&self) -> String {
let mut res = String::new();
self.write_plain(&mut res)