Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
4905243
Implement Nethernet spec
LucienHH Oct 4, 2024
ba8b6cb
Add WebSocket signalling channel
LucienHH Oct 4, 2024
1f2059f
Add Nethernet ping advertisement
LucienHH Oct 4, 2024
4b03acf
Add Session handling
LucienHH Oct 4, 2024
e55a25a
Add nethernet transport
LucienHH Oct 4, 2024
1f2ce1d
Fix tests
LucienHH Oct 4, 2024
9d5a6d2
Correctly build credentials
LucienHH Oct 5, 2024
209df55
Use active broadcast address
LucienHH Nov 5, 2024
53dd116
Fix signalling handling
LucienHH Nov 5, 2024
9e7ddb8
Downgrade to werift v0.19.9
LucienHH Nov 5, 2024
8558cef
Lint
LucienHH Nov 5, 2024
c2ba687
Remove unnecessary ping
LucienHH Nov 5, 2024
9f99980
Remove debug logs
LucienHH Nov 6, 2024
79b1a08
Send initial discovery request
LucienHH Nov 8, 2024
b51aa9f
Rename `Signal` to `NethernetSignal`
LucienHH Jan 20, 2025
50ce274
Compression, batching and protocol fixes
LucienHH Jan 29, 2025
6ad814b
Use correct buffer
LucienHH Jan 29, 2025
c892976
Update to latest pauth API
LucienHH Jan 29, 2025
d037a00
Use static arguments
LucienHH Jan 29, 2025
b45fa84
Linting
LucienHH Jan 29, 2025
00bfc04
Move protocol to `node-nethernet`
LucienHH Apr 11, 2025
25f5cc6
Fix connecting via signalling
LucienHH Apr 11, 2025
d4421fc
Move nethernet properties under .nethernet.*
LucienHH Apr 12, 2025
e555e65
Create nethernet_local.js
LucienHH Apr 12, 2025
568398e
Remove node-fetch
LucienHH Apr 12, 2025
eab6e8f
Rename rta to session
LucienHH Apr 12, 2025
8906580
Implement ServerData
LucienHH Aug 15, 2025
a67937e
Cleanup unused methods
LucienHH Aug 15, 2025
4ac2b97
Lint
LucienHH Aug 15, 2025
19b16e1
Update to support PrismarineJS/nethernet
LucienHH Sep 15, 2025
87df4ff
Implement Nethernet spec
LucienHH Oct 4, 2024
ce18818
Add WebSocket signalling channel
LucienHH Oct 4, 2024
4d7bce7
Add Nethernet ping advertisement
LucienHH Oct 4, 2024
a2e6e45
Add Session handling
LucienHH Oct 4, 2024
f8cf68c
Add nethernet transport
LucienHH Oct 4, 2024
48d38f0
Fix tests
LucienHH Oct 4, 2024
c07d50e
Correctly build credentials
LucienHH Oct 5, 2024
6d13076
Use active broadcast address
LucienHH Nov 5, 2024
1a48c0d
Fix signalling handling
LucienHH Nov 5, 2024
5ae05ea
Downgrade to werift v0.19.9
LucienHH Nov 5, 2024
4c12952
Lint
LucienHH Nov 5, 2024
847cff9
Remove unnecessary ping
LucienHH Nov 5, 2024
eeb085a
Remove debug logs
LucienHH Nov 6, 2024
fb06cf8
Send initial discovery request
LucienHH Nov 8, 2024
dfc18c0
Rename `Signal` to `NethernetSignal`
LucienHH Jan 20, 2025
54c3909
Compression, batching and protocol fixes
LucienHH Jan 29, 2025
04719e9
Use correct buffer
LucienHH Jan 29, 2025
21901cb
Update to latest pauth API
LucienHH Jan 29, 2025
73f07a8
Use static arguments
LucienHH Jan 29, 2025
ae6bf81
Linting
LucienHH Jan 29, 2025
a2e4283
Move protocol to `node-nethernet`
LucienHH Apr 11, 2025
0e01ba5
Fix connecting via signalling
LucienHH Apr 11, 2025
03bf55a
Move nethernet properties under .nethernet.*
LucienHH Apr 12, 2025
e41648f
Create nethernet_local.js
LucienHH Apr 12, 2025
70c4935
Remove node-fetch
LucienHH Apr 12, 2025
27465c8
Rename rta to session
LucienHH Apr 12, 2025
04d51da
Implement ServerData
LucienHH Aug 15, 2025
d5f844f
Cleanup unused methods
LucienHH Aug 15, 2025
9383546
Lint
LucienHH Aug 15, 2025
b2a33ae
Update to support PrismarineJS/nethernet
LucienHH Sep 15, 2025
f19e0ce
Update Nethernet signalling support and update advertisement
LucienHH Jan 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions examples/client/nethernet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
process.env.DEBUG = 'minecraft-protocol'

const readline = require('readline')
const { createClient } = require('bedrock-protocol')

async function pickSession (availableSessions) {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})

console.log('Available Sessions:')

availableSessions.forEach((session, index) => console.log(`${index + 1}. ${session.customProperties.hostName} ${session.customProperties.worldName} (${session.customProperties.version})`))

rl.question('Please select a session by number: ', (answer) => {
const sessionIndex = parseInt(answer) - 1

if (sessionIndex >= 0 && sessionIndex < availableSessions.length) {
const selectedSession = availableSessions[sessionIndex]
console.log(`You selected: ${selectedSession.customProperties.hostName} ${selectedSession.customProperties.worldName} (${selectedSession.customProperties.version})`)
resolve(selectedSession)
} else {
console.log('Invalid selection. Please try again.')
resolve(pickSession())
}

rl.close()
})
})
}

const client = createClient({
transport: 'nethernet', // Use the Nethernet transport
world: {
pickSession
}
})

let ix = 0
client.on('packet', (args) => {
console.log(`Packet ${ix} recieved`)
ix++
})
22 changes: 22 additions & 0 deletions examples/client/nethernet_local.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
process.env.DEBUG = 'minecraft-protocol'

const { Client } = require('node-nethernet')
const { createClient } = require('bedrock-protocol')

const c = new Client()

c.once('pong', (pong) => {
c.close()

const client = createClient({
transport: 'nethernet', // Use the Nethernet transport
networkId: pong.sender_id,
useSignalling: false
})

let ix = 0
client.on('packet', (args) => {
console.log(`Packet ${ix} recieved`)
ix++
})
})
24 changes: 24 additions & 0 deletions examples/client/nethernet_realm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
process.env.DEBUG = 'minecraft-protocol'

const { createClient } = require('bedrock-protocol')

const client = createClient({
transport: 'nethernet', // Use the Nethernet transport
useSignalling: true,
networkId: '<guid>',
skipPing: true
})

client.on('text', (packet) => { // Listen for chat messages and echo them back.
if (packet.source_name !== client.username) {
client.queue('text', {
type: 'chat',
needs_translation: false,
source_name: client.username,
xuid: '',
platform_chat_id: '',
filtered_message: '',
message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}`
})
}
})
20 changes: 20 additions & 0 deletions examples/server/nethernet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable */
process.env.DEBUG = 'minecraft-protocol'

const bedrock = require('bedrock-protocol')

const server = bedrock.createServer({
transport: 'nethernet',
useSignalling: true, // disable for LAN connections only
motd: {
motd: 'Funtime Server',
levelName: 'Wonderland'
}
})

server.on('connect', client => {
client.on('join', () => { // The client has joined the server.
const date = new Date() // Once client is in the server, send a colorful kick message
client.disconnect(`Good ${date.getHours() < 12 ? '§emorning§r' : '§3afternoon§r'}\n\nMy time is ${date.toLocaleString()} !`)
})
})
5 changes: 3 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const { Relay } = require('./src/relay')
const { createClient, ping } = require('./src/createClient')
const { createServer } = require('./src/createServer')
const { Titles } = require('prismarine-auth')
const { ServerAdvertisement } = require('./src/server/advertisement')
const { ServerAdvertisement, NethernetServerAdvertisement } = require('./src/server/advertisement')

module.exports = {
Client,
Expand All @@ -20,5 +20,6 @@ module.exports = {
ping,
createServer,
title: Titles,
ServerAdvertisement
ServerAdvertisement,
NethernetServerAdvertisement
}
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bedrock-protocol",
"version": "3.52.0",
"version": "3.50.0",
"description": "Minecraft Bedrock Edition protocol library",
"main": "index.js",
"types": "index.d.ts",
Expand All @@ -22,16 +22,20 @@
"license": "MIT",
"dependencies": {
"debug": "^4.3.1",
"json-bigint": "^1.0.0",
"jsonwebtoken": "^9.0.0",
"jsp-raknet": "^2.1.3",
"minecraft-data": "^3.0.0",
"minecraft-folder-path": "^1.2.0",
"prismarine-auth": "^2.0.0",
"node-nethernet": "github:LucienHH/node-nethernet#protocol",
"prismarine-auth": "2.7.0",
"prismarine-nbt": "^2.0.0",
"prismarine-realms": "^1.1.0",
"protodef": "^1.14.0",
"raknet-native": "^1.0.3",
"uuid-1345": "^1.0.2"
"uuid-1345": "^1.0.2",
"ws": "^8.18.0",
"xbox-rta": "^2.1.0"
},
"optionalDependencies": {
"raknet-node": "^0.5.0"
Expand All @@ -53,4 +57,4 @@
"url": "https://github.com/PrismarineJS/bedrock-protocol/issues"
},
"homepage": "https://github.com/PrismarineJS/bedrock-protocol#readme"
}
}
53 changes: 47 additions & 6 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ const debug = require('debug')('minecraft-protocol')
const Options = require('./options')
const auth = require('./client/auth')
const initRaknet = require('./rak')
const { NethernetClient } = require('./nethernet')
const { KeyExchange } = require('./handshake/keyExchange')
const Login = require('./handshake/login')
const LoginVerify = require('./handshake/loginVerify')
const { NethernetSignal } = require('./websocket/signal')

const debugging = false

Expand All @@ -20,13 +22,16 @@ class Client extends Connection {
super()
this.options = { ...Options.defaultOptions, ...options }

if (this.options.transport === 'nethernet') {
this.nethernet = {}
}

this.startGameData = {}
this.clientRuntimeId = null
// Start off without compression on 1.19.30, zlib on below
this.compressionAlgorithm = this.versionGreaterThanOrEqualTo('1.19.30') ? 'none' : 'deflate'
this.compressionThreshold = 512
this.compressionLevel = this.options.compressionLevel
this.batchHeader = 0xfe

if (isDebug) {
this.inLog = (...args) => debug('C ->', ...args)
Expand All @@ -49,10 +54,21 @@ class Client extends Connection {
Login(this, null, this.options)
LoginVerify(this, null, this.options)

const { RakClient } = initRaknet(this.options.raknetBackend)
const host = this.options.host
const port = this.options.port
this.connection = new RakClient({ useWorkers: this.options.useRaknetWorkers, host, port }, this)

const networkId = this.options.networkId

if (this.options.transport === 'nethernet') {
this.connection = new NethernetClient({ networkId })
this.batchHeader = null
this.disableEncryption = true
} else if (this.options.transport === 'raknet') {
const { RakClient } = initRaknet(this.options.raknetBackend)
this.connection = new RakClient({ useWorkers: this.options.useRaknetWorkers, host, port }, this)
this.batchHeader = 0xfe
this.disableEncryption = false
}

this.emit('connect_allowed')
}
Expand All @@ -72,7 +88,23 @@ class Client extends Connection {

connect () {
if (!this.connection) throw new Error('Connect not currently allowed') // must wait for `connect_allowed`, or use `createClient`
this.on('session', this._connect)
this.on('session', (sessionData) => {
if (this.options.transport === 'nethernet' && this.options.useSignalling) {
this.nethernet.signalling = new NethernetSignal(this.connection.nethernet.networkId, this.options.authflow, this.options.version)

this.nethernet.signalling.connect()

this.connection.nethernet.signalHandler = this.nethernet.signalling.write.bind(this.nethernet.signalling)

this.nethernet.signalling.on('signal', signal => this.connection.nethernet.handleSignal(signal))
this.nethernet.signalling.on('credentials', (credentials) => {
this.connection.nethernet.credentials = credentials
this._connect(sessionData)
})
} else {
this._connect(sessionData)
}
})

if (this.options.offline) {
debug('offline mode, not authenticating', this.options)
Expand All @@ -85,7 +117,16 @@ class Client extends Connection {
}

validateOptions () {
if (!this.options.host || this.options.port == null) throw Error('Invalid host/port')
switch (this.options.transport) {
case 'nethernet':
if (!this.options.networkId) throw Error('Invalid networkId')
break
case 'raknet':
if (!this.options.host || this.options.port == null) throw Error('Invalid host/port')
break
default:
throw Error(`Unsupported transport: ${this.options.transport} (nethernet, raknet)`)
}
Options.validateOptions(this.options)
}

Expand Down Expand Up @@ -252,7 +293,7 @@ class Client extends Connection {
break
case 'start_game':
this.startGameData = pakData.params
// fallsthrough
// fallsthrough
case 'item_registry': // 1.21.60+ send itemstates in item_registry packet
pakData.params.itemstates?.forEach(state => {
if (state.name === 'minecraft:shield') {
Expand Down
63 changes: 60 additions & 3 deletions src/client/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const minecraftFolderPath = require('minecraft-folder-path')
const debug = require('debug')('minecraft-protocol')
const { uuidFrom } = require('../datatypes/util')
const { RealmAPI } = require('prismarine-realms')
const { SessionDirectory } = require('../xsapi/session')

function validateOptions (options) {
if (!options.profilesFolder) {
Expand All @@ -16,6 +17,60 @@ function validateOptions (options) {
}
}

async function serverAuthenticate (server, options) {
validateOptions(options)

options.authflow ??= new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode)

server.nethernet.session = new SessionDirectory(options.authflow, {
world: {
hostName: server.advertisement.motd,
name: server.advertisement.levelName,
version: options.version,
protocol: options.protocolVersion,
memberCount: server.advertisement.playerCount,
maxMemberCount: server.advertisement.playersMax
}
})

await server.nethernet.session.createSession(options.networkId)
}

async function worldAuthenticate (client, options) {
validateOptions(options)

options.authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode)

const xbl = await options.authflow.getXboxToken()

client.nethernet.session = new SessionDirectory(options.authflow, {})

const getSessions = async () => {
const sessions = await client.nethernet.session.host.rest.getSessions(xbl.userXUID)
debug('sessions', sessions)
if (!sessions.length) throw Error('Couldn\'t find any sessions for the authenticated account')
return sessions
}

let world

if (options.world.pickSession) {
if (typeof options.world.pickSession !== 'function') throw Error('world.pickSession must be a function')
const sessions = await getSessions()
world = await options.world.pickSession(sessions)
}

if (!world) throw Error('Couldn\'t find a session to connect to.')

const session = await client.nethernet.session.joinSession(world.sessionRef.name)

const networkId = session.properties.custom.SupportedConnections.find(e => e.ConnectionType === 3).NetherNetId

if (!networkId) throw Error('Couldn\'t find a Nethernet ID to connect to.')

options.networkId = BigInt(networkId)
}

async function realmAuthenticate (options) {
validateOptions(options)

Expand Down Expand Up @@ -64,8 +119,8 @@ async function realmAuthenticate (options) {
async function authenticate (client, options) {
validateOptions(options)
try {
const authflow = options.authflow || new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode)
const chains = await authflow.getMinecraftBedrockToken(client.clientX509).catch(e => {
options.authflow ??= new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode)
const chains = await options.authflow.getMinecraftBedrockToken(client.clientX509).catch(e => {
if (options.password) console.warn('Sign in failed, try removing the password field')
throw e
})
Expand Down Expand Up @@ -115,5 +170,7 @@ function postAuthenticate (client, profile, chains) {
module.exports = {
createOfflineSession,
authenticate,
realmAuthenticate
realmAuthenticate,
worldAuthenticate,
serverAuthenticate
}
1 change: 1 addition & 0 deletions src/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class Connection extends EventEmitter {
}

startEncryption (iv) {
if (this.disableEncryption) return
this.encryptionEnabled = true
this.inLog?.('Started encryption', this.sharedSecret, iv)
this.decrypt = cipher.createDecryptor(this, iv)
Expand Down
Loading
Loading