Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
57f1b3d
Implement Nethernet spec
LucienHH Oct 4, 2024
3611fc8
Add WebSocket signalling channel
LucienHH Oct 4, 2024
ff60244
Add Nethernet ping advertisement
LucienHH Oct 4, 2024
fbb490e
Add Session handling
LucienHH Oct 4, 2024
a5f1a68
Add nethernet transport
LucienHH Oct 4, 2024
f7b6873
Fix tests
LucienHH Oct 4, 2024
9128997
Correctly build credentials
LucienHH Oct 5, 2024
d64d420
Use active broadcast address
LucienHH Nov 5, 2024
e622abc
Fix signalling handling
LucienHH Nov 5, 2024
8663b31
Downgrade to werift v0.19.9
LucienHH Nov 5, 2024
bee7e2f
Lint
LucienHH Nov 5, 2024
dac29d0
Remove unnecessary ping
LucienHH Nov 5, 2024
beb436f
Remove debug logs
LucienHH Nov 6, 2024
d3746db
Send initial discovery request
LucienHH Nov 8, 2024
a30b7ef
Rename `Signal` to `NethernetSignal`
LucienHH Jan 20, 2025
5f4ed39
Compression, batching and protocol fixes
LucienHH Jan 29, 2025
0349567
Use correct buffer
LucienHH Jan 29, 2025
4e5ef2a
Update to latest pauth API
LucienHH Jan 29, 2025
c9bc0c1
Use static arguments
LucienHH Jan 29, 2025
552fc8b
Linting
LucienHH Jan 29, 2025
4b082fc
Move protocol to `node-nethernet`
LucienHH Apr 11, 2025
d24817f
Fix connecting via signalling
LucienHH Apr 11, 2025
7a73902
Move nethernet properties under .nethernet.*
LucienHH Apr 12, 2025
6c048a7
Create nethernet_local.js
LucienHH Apr 12, 2025
60c7ff9
Remove node-fetch
LucienHH Apr 12, 2025
6bf0c1f
Rename rta to session
LucienHH Apr 12, 2025
f3230cf
Implement ServerData
LucienHH Aug 15, 2025
508a15c
Cleanup unused methods
LucienHH Aug 15, 2025
c754c0c
Lint
LucienHH Aug 15, 2025
71ecfee
Update to support PrismarineJS/nethernet
LucienHH Sep 15, 2025
5fc6377
Update Nethernet signalling support and update advertisement
LucienHH Apr 17, 2026
e301fc7
Update auth
LucienHH Apr 22, 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(0n)

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
}
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"node-nethernet": "github:LucienHH/node-nethernet#protocol",
"prismarine-auth": "^3.0.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 loginData = await authflow.getMinecraftBedrockToken(client.clientX509).catch(e => {
options.authflow ??= new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode)
const loginData = 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 @@ -117,5 +172,7 @@ function postAuthenticate (client, profile, auth = {}) {
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