add new packets and working proxy

This commit is contained in:
extremeheat 2021-03-09 02:13:47 -05:00
commit 9e1500ec77
9 changed files with 430 additions and 9 deletions

View file

@ -150,6 +150,11 @@ mcpe_packet:
0x9a: position_tracking_db_request
0x99: position_tracking_db_broadcast
0x9c: packet_violation_warning
0x9d: motion_prediction_hints
0x9e: animate_entity
0x9f: camera_shake
0xa0: player_fog
0xa1: correct_player_move_prediction
0xa2: item_component
0xa3: filter_text_packet
@ -303,5 +308,10 @@ mcpe_packet:
if position_tracking_db_request: packet_position_tracking_db_request
if position_tracking_db_broadcast: packet_position_tracking_db_broadcast
if packet_violation_warning: packet_packet_violation_warning
if motion_prediction_hints: packet_motion_prediction_hints
if animate_entity: packet_animate_entity
if camera_shake: packet_camera_shake
if player_fog: packet_player_fog
if correct_player_move_prediction: packet_correct_player_move_prediction
if item_component: packet_item_component
if filter_text_packet: packet_filter_text_packet

View file

@ -2204,6 +2204,87 @@ packet_packet_violation_warning:
# ViolationContext holds a description on the violation of the packet.
reason: string
# MotionPredictionHints is sent by the server to the client. There is a predictive movement component for
# entities. This packet fills the "history" of that component and entity movement is computed based on the
# points. Vanilla sends this packet instead of the SetActorMotion packet when 'spatial optimisations' are
# enabled.
packet_motion_prediction_hints:
!id: 0x9d
!bound: client
# EntityRuntimeID is the runtime ID of the entity whose velocity is sent to the client.
entity_runtime_id: varint64
# Velocity is the server-calculated velocity of the entity at the point of sending the packet.
velocity: vec3f
# OnGround specifies if the server currently thinks the entity is on the ground.
on_ground: bool
# AnimateEntity is sent by the server to animate an entity client-side. It may be used to play a single
# animation, or to activate a controller which can start a sequence of animations based on different
# conditions specified in an animation controller.
# Much of the documentation of this packet can be found at
# https://minecraft.gamepedia.com/Bedrock_Edition_beta_animation_documentation.
packet_animate_entity:
!id: 0x9e
!bound: client
# Animation is the name of a single animation to start playing.
animation: string
# NextState is the first state to start with. These states are declared in animation controllers (which,
# in themselves, are animations too). These states in turn may have animations and transitions to move to
# a next state.
next_state: string
# StopCondition is a MoLang expression that specifies when the animation should be stopped.
stop_condition: string
# Controller is the animation controller that is used to manage animations. These controllers decide when
# to play which animation.
controller: string
# BlendOutTime does not currently seem to be used.
blend_out_time: lf32
# EntityRuntimeIDs is list of runtime IDs of entities that the animation should be applied to.
runtime_entity_ids: varint64[]varint
# CameraShake is sent by the server to make the camera shake client-side. This feature was added for map-
# making partners.
packet_camera_shake:
!id: 0x9f
!bound: client
# Intensity is the intensity of the shaking. The client limits this value to 4, so anything higher may
# not work.
intensity: lf32
# Duration is the number of seconds the camera will shake for.
duration: lf32
# Type is the type of shake, and is one of the constants listed above. The different type affects how
# the shake looks in game.
type: u8
# PlayerFog is sent by the server to render the different fogs in the Stack. The types of fog are controlled
# by resource packs to change how they are rendered, and the ability to create custom fog.
packet_player_fog:
!id: 0xa0
!bound: client
# Stack is a list of fog identifiers to be sent to the client. Examples of fog identifiers are
# "minecraft:fog_ocean" and "minecraft:fog_hell".
stack: string[]varint
# CorrectPlayerMovePrediction is sent by the server if and only if StartGame.ServerAuthoritativeMovementMode
# is set to AuthoritativeMovementModeServerWithRewind. The packet is used to correct movement at a specific
# point in time.
packet_correct_player_move_prediction:
!id: 0xa1
!bound: client
# Position is the position that the player is supposed to be at at the tick written in the field below.
# The client will change its current position based on movement after that tick starting from the
# Position.
position: vec3f
# Delta is the change in position compared to what the client sent as its position at that specific tick.
delta: vec3f
# OnGround specifies if the player was on the ground at the time of the tick below.
on_ground: bool
# Tick is the tick of the movement which was corrected by this packet.
tick: varint64
# ItemComponent is sent by the server to attach client-side components to a custom item.
packet_item_component:
!id: 0xa2
@ -2221,3 +2302,4 @@ packet_filter_text_packet:
text: string
# FromServer indicates if the packet was sent by the server or not.
from_server: bool

View file

@ -2694,6 +2694,11 @@
"153": "position_tracking_db_broadcast",
"154": "position_tracking_db_request",
"156": "packet_violation_warning",
"157": "motion_prediction_hints",
"158": "animate_entity",
"159": "camera_shake",
"160": "player_fog",
"161": "correct_player_move_prediction",
"162": "item_component",
"163": "filter_text_packet"
}
@ -2856,6 +2861,11 @@
"position_tracking_db_request": "packet_position_tracking_db_request",
"position_tracking_db_broadcast": "packet_position_tracking_db_broadcast",
"packet_violation_warning": "packet_packet_violation_warning",
"motion_prediction_hints": "packet_motion_prediction_hints",
"animate_entity": "packet_animate_entity",
"camera_shake": "packet_camera_shake",
"player_fog": "packet_player_fog",
"correct_player_move_prediction": "packet_correct_player_move_prediction",
"item_component": "packet_item_component",
"filter_text_packet": "packet_filter_text_packet"
},
@ -6743,6 +6753,111 @@
}
]
],
"packet_motion_prediction_hints": [
"container",
[
{
"name": "entity_runtime_id",
"type": "varint64"
},
{
"name": "velocity",
"type": "vec3f"
},
{
"name": "on_ground",
"type": "bool"
}
]
],
"packet_animate_entity": [
"container",
[
{
"name": "animation",
"type": "string"
},
{
"name": "next_state",
"type": "string"
},
{
"name": "stop_condition",
"type": "string"
},
{
"name": "controller",
"type": "string"
},
{
"name": "blend_out_time",
"type": "lf32"
},
{
"name": "runtime_entity_ids",
"type": [
"array",
{
"countType": "varint",
"type": "varint64"
}
]
}
]
],
"packet_camera_shake": [
"container",
[
{
"name": "intensity",
"type": "lf32"
},
{
"name": "duration",
"type": "lf32"
},
{
"name": "type",
"type": "u8"
}
]
],
"packet_player_fog": [
"container",
[
{
"name": "stack",
"type": [
"array",
{
"countType": "varint",
"type": "string"
}
]
}
]
],
"packet_correct_player_move_prediction": [
"container",
[
{
"name": "position",
"type": "vec3f"
},
{
"name": "delta",
"type": "vec3f"
},
{
"name": "on_ground",
"type": "bool"
},
{
"name": "tick",
"type": "varint64"
}
]
],
"packet_item_component": [
"container",
[

View file

@ -112,7 +112,7 @@ class Client extends Connection {
// console.log('packet', packet)
const des = this.deserializer.parsePacketBuffer(packet)
const pakData = { name: des.data.name, params: des.data.params }
this.inLog('-> C', pakData.name, serialize(pakData.params).slice(0, 100))
this.inLog('-> C', pakData.name/*, serialize(pakData.params).slice(0, 100)*/)
// No idea what this exotic 0xA0 packet is, it's not implemented anywhere
// and seems empty. Possible gibberish from the raknet impl
@ -149,7 +149,7 @@ class Client extends Connection {
fs.writeFileSync('start_game.json', JSON.stringify(des.data.params, (k, v) => typeof v == 'bigint' ? v.toString() : v))
break
case 'level_chunk':
fs.writeFileSync(`./chunks/chunk-${chunks++}.txt`, packet.toString('hex'))
// fs.writeFileSync(`./chunks/chunk-${chunks++}.txt`, packet.toString('hex'))
break
default:
// console.log('Sending to listeners')

View file

@ -19,7 +19,7 @@ class Connection extends EventEmitter {
// console.log('Need to encode', name, params)
var s = this.connect ? 'C' : 'S'
if (this.downQ) s += 'P'
this.outLog('<- ' + s, name)
this.outLog('NB <- ' + s, name,params)
const batch = new BatchPacket()
const packet = this.serializer.createPacketBuffer({ name, params })
// console.log('Sending buf', packet.toString('hex').)
@ -33,8 +33,13 @@ class Connection extends EventEmitter {
}
queue(name, params) {
this.outLog('Q <- ', name)
this.outLog('Q <- ', name, params)
const packet = this.serializer.createPacketBuffer({ name, params })
if (name == 'level_chunk') {
// Skip queue
this.sendMCPE(packet)
return
}
this.q.push(packet)
this.q2.push(name)
}
@ -48,16 +53,19 @@ class Connection extends EventEmitter {
this.outLog('<- BATCH', this.q2)
// For now, we're over conservative so send max 3 packets
// per batch and hold the rest for the next tick
for (let i = 0; /*i < 10 &&*/ i < this.q.length; i++) {
const sending = []
for (let i = 0; i < 3 && i < this.q.length; i++) {
const packet = this.q.shift()
sending.push(this.q2.shift())
batch.addEncodedPacket(packet)
}
// console.warn('~~ Sending', sending)
if (this.encryptionEnabled) {
this.sendEncryptedBatch(batch)
} else {
this.sendDecryptedBatch(batch)
}
this.q2 = []
// this.q2 = []
}
}, 100)
}
@ -162,5 +170,7 @@ class Connection extends EventEmitter {
// console.log('[client] handled incoming ', buffer)
}
}
function serialize(obj = {}, fmt) {
return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt)
}
module.exports = { Connection }

View file

@ -57,7 +57,8 @@ class RakNativeServer extends EventEmitter {
this.onEncapsulated = () => {}
this.raknet = new Server(options.hostname, options.port, {
maxConnections: options.maxConnections || 3,
minecraft: { message: new McPingMessage().toString() }
minecraft: { },
message: new McPingMessage().toBuffer()
})
this.raknet.on('openConnection', (client) => {

202
src/relay.js Normal file
View file

@ -0,0 +1,202 @@
process.env.DEBUG = 'minecraft-protocol raknet'
const { Client } = require("./client")
const { Server } = require("./server")
const { Player } = require("./serverPlayer")
const debug = require('debug')('minecraft-protocol relay')
/** @typedef {{ hostname: string, port: number, auth: 'client' | 'server' | null, destination?: { hostname: string, port: number } }} Options */
class RelayPlayer extends Player {
constructor(server, conn) {
super(server, conn)
this.server = server
this.conn = conn
this.startRelaying = false
this.once('join', () => {
this.write('client_cache_status', {enabled:false}) // disable this asap on join
this.flushDownQueue()
this.startRelaying = true
})
this.downQ = []
this.upQ = []
this.upInLog = (...msg) => console.info('** Backend -> Proxy', ...msg)
this.upOutLog = (...msg) => console.info('** Proxy -> Backend', ...msg)
this.downInLog = (...msg) => console.info('** Client -> Proxy', ...msg)
this.downOutLog = (...msg) => console.info('** Proxy -> Client', ...msg)
this.outLog = this.downOutLog
this.inLog = this.downInLog
}
// Called when we get a packet from backend server (Backend -> PROXY -> Client)
readUpstream(packet) {
if (!this.startRelaying) {
console.warn('The downstream client is not ready yet !!')
this.downQ.push(packet)
return
}
this.upInLog('Recv packet', packet)
const des = this.server.deserializer.parsePacketBuffer(packet)
const name = des.data.name
const params = des.data.params
this.upInLog('~~ Bounce B->C', name, serialize(params).slice(0, 100))
this.upInLog('~~ ', des.buffer)
if (name == 'play_status' && params.status == 'login_success') return
if (name == 'level_chunk') { //send chunk directly
this.upInLog('Would send chunk', params)
this.sendBuffer(packet)
return
} else this.upInLog('?',name)
// if (name == 'network_chunk_publisher_update') return
// if (name == 'crafting_data' || name == 'level_chunk') return // Alex breaks
this.queue(name, params)
// this.sendBuffer(packet)
}
flushDownQueue() {
for (const packet of this.downQ) {
const des = this.server.deserializer.parsePacketBuffer(packet)
this.write(des.data.name, des.data.params)
}
this.downQ = []
}
flushUpQueue() {
for (var e of this.upQ) { // Send the queue
const des = this.server.deserializer.parsePacketBuffer(e)
if (des.data.name == 'client_cache_status') { // already disabled on join
// this.upstream.write('client_cache_status', {enabled:false})
} else {
this.upstream.write(des.data.name, des.data.params)
}
}
this.upQ = []
}
// Called when the server gets a packet from the downstream player (Client -> PROXY -> Backend)
readPacket(packet) {
if (this.startRelaying) { // The DS client conn is established & we got a packet to send to US server
if (!this.upstream) { // Upstream is still connecting/handshaking
debug('Got downstream connected packet but upstream is not connected yet, added to q', this.queue.length)
this.upQ.push(packet) // Put into a queue
return
}
this.flushUpQueue() // Send queued packets
this.downInLog('Recv packet', packet)
const des = this.server.deserializer.parsePacketBuffer(packet)
switch (des.data.name) {
case 'client_cache_status':
this.upstream.queue('client_cache_status', {enabled:false})
break
// case 'request_chunk_radius':
// this.upstream.queue('request_chunk_radius', {chunk_radius: 1})
// break
default:
// Emit the packet as-is back to the upstream server
this.upstream.queue(des.data.name, des.data.params)
}
} else {
super.readPacket(packet)
}
}
}
class Relay extends Server {
/**
* Creates a new non-transparent proxy connection to a destination server
* @param {Options} options
*/
constructor(options) {
super(options)
this.RelayPlayer = options.relayPlayer || RelayPlayer
this.forceSingle = true
this.upstreams = new Map()
}
openUpstreamConnection(ds, clientAddr) {
const client = new Client({
hostname: this.options.destination.hostname,
port: this.options.destination.port,
encrypt: this.options.encrypt
})
client.outLog = ds.upOutLog
client.inLog = ds.upInLog
// console.log('Set upstream logs', client.outLog, client.inLog)
client.once('join', () => { // Intercept once handshaking done
ds.upstream = client
ds.flushUpQueue()
console.log('UPSTREAM HAS JOINED')
client.readPacket = (packet) => ds.readUpstream(packet)
})
this.upstreams.set(clientAddr.hash, client)
}
closeUpstreamConnection(clientAddr) {
const up = this.upstreams.get(clientAddr.hash)
if (!up) throw Error(`unable to close non-existant connection ${clientAddr.hash}`)
up.close()
this.upstreams.delete(clientAddr.hash)
debug('relay closed connection', clientAddr)
}
onOpenConnection = (conn) => {
debug('new connection', conn)
if (this.forceSingle && this.clientCount > 0) {
debug('dropping connection as single client relay', conn)
conn.close()
} else {
const player = new this.RelayPlayer(this, conn)
console.log('NEW CONNECTION', conn.address)
this.clients[conn.address] = player
this.emit('connect', { client: player })
this.openUpstreamConnection(player, conn.address)
}
}
}
console.log = () => {}
function serialize(obj = {}, fmt) {
return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt)
}
function createRelay() {
console.log('Creating relay')
/**
* Example to create a non-transparent proxy (or 'Relay') connection to destination server
* In Relay we de-code and re-encode packets
*/
const relay = new Relay({
/* Hostname and port for clients to listen to */
hostname: '0.0.0.0',
port: 19130,
/**
* Who does the authentication
* If set to `client`, all connecting clients will be sent a message with a link to authenticate
* If set to `server`, the server will authenticate and only one client will be able to join
* (Default) If set to `none`, no authentication will be done
*/
auth: 'server',
/**
* Sets if packets will automatically be forwarded. If set to false, you must listen for on('packet')
* events and
*/
auto: true,
/* Where to send upstream packets to */
destination: {
hostname: '127.0.0.1',
port: 19132,
encryption: true
}
})
relay.create()
}
createRelay()

View file

@ -90,6 +90,7 @@ class Player extends Connection {
// After sending Server to Client Handshake, this handles the client's
// Client to Server handshake response. This indicates successful encryption
onHandshake() {
// this.outLog('Sending login success!', this.status)
// https://wiki.vg/Bedrock_Protocol#Play_Status
this.write('play_status', { status: 'login_success' })
this.status = ClientStatus.Initializing

View file

@ -87,7 +87,7 @@ function createEncryptor(client, iv) {
return (blob) => {
client.outLog(client.options ? 'C':'S', '🟡 Encrypting', blob)
// client.outLog(client.options ? 'C':'S', '🟡 Encrypting', client.sendCounter, blob)
// stream.write(blob)
process(blob)
}