Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 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
a3e7bff
fix(nethernet): get ICE before offer and disable trickle candidates
yntha May 9, 2026
0f2b0dc
feat(signal): add jsonrpc protocol support and fix endpoint
yntha May 10, 2026
478bde5
feat(auth): handle NETHERNET_JSONRPC realm join in realmAuthenticate
yntha May 10, 2026
1ba2ccb
feat(signal): automatically fall back to jsonrpc if legacy signalling…
yntha May 10, 2026
bd09b87
feat(client): pass signalling host from realmAuthenticate to Netherne…
yntha May 10, 2026
c0305ef
fix(auth): use raw join endpoint and set options for NETHERNET_JSONRPC
yntha May 10, 2026
672b718
fix(client): init nethernet early and pass signalling protocol option
yntha May 10, 2026
c29bad7
fix(createClient): read skipPing from client.options
yntha May 10, 2026
f841a0c
feat(signal): rewrite NethernetSignal for JSON-RPC signalling API
yntha May 10, 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
Comment on lines +13 to +14

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

networkId and useSignalling seem poorly named from API standpoint.

Can you explain these? It seems these options can be put into something like xboxSession: { ...props } to be clearer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

networkId is the identifier used to connect to the target server. Normally can be found from the discovery packet but for example in Realms the API returns the networkId in the response which you can then use the signalling channel to connect.

useSignalling is a flag that determines whether the signalling server should be contacted to create a connection, when it's false it will only do discovery over LAN

@extremeheat extremeheat May 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ok from internal standpoint but not useful configuration jargon for people trying to connect to a server

If this is for connecting to LAN or Realm servers we should have better named options

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey team, I managed to get rid of this in commit 478bde5. In the example I provided, you don't need to pass any extra options at all; the client handles it in the backend.

})

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",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a PR for this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to do a release of node-nethernet but there are some further changes that need to be merged still

"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"
}
}
59 changes: 53 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,22 @@ 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.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 +89,28 @@ 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,
{ protocol: this.options._signallingProtocol, host: this.options._signallingHost }
)

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 +123,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 +299,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
87 changes: 77 additions & 10 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,12 +17,66 @@ 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)

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

const api = RealmAPI.from(options.authflow, 'bedrock')
const api = RealmAPI.from(options.authflow, 'bedrock', { minecraftVersion: options.version })

const getRealms = async () => {
const realms = await api.getRealms()
Expand All @@ -45,12 +100,22 @@ async function realmAuthenticate (options) {

if (!realm) throw Error('Couldn\'t find a Realm to connect to. Authenticated account must be the owner or has been invited to the Realm.')

const { host, port } = await realm.getAddress()

debug('realms connection', { host, port })

options.host = host
options.port = port
const join = await api.rest.get(`/worlds/${realm.id}/join`)

debug('realms connection', join)

if (join.networkProtocol === 'NETHERNET_JSONRPC') {
options.transport = 'nethernet'
options.networkId = join.address
options.useSignalling = true
options.skipPing = true
options._signallingProtocol = 'jsonrpc'
const region = join.sessionRegionData?.regionName
if (region) options._signallingHost = `signal-${String(region).toLowerCase()}.franchise.minecraft-services.net`
} else {
options.host = join.host
options.port = join.port
}
}

/**
Expand All @@ -64,8 +129,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 +182,7 @@ function postAuthenticate (client, profile, auth = {}) {
module.exports = {
createOfflineSession,
authenticate,
realmAuthenticate
realmAuthenticate,
worldAuthenticate,
serverAuthenticate
}
Loading