Skip to content

Add support for NETHERNET_JSONRPC Realms#735

Open
yntha wants to merge 41 commits into
PrismarineJS:masterfrom
yntha:nethernet-jsonrpc
Open

Add support for NETHERNET_JSONRPC Realms#735
yntha wants to merge 41 commits into
PrismarineJS:masterfrom
yntha:nethernet-jsonrpc

Conversation

@yntha
Copy link
Copy Markdown

@yntha yntha commented May 10, 2026

Adds support for NETHERNET_JSONRPC Realms, which is the connection type currently returned by the Realms join API (according to regions I tested). Before, createClient({ realms: { pickRealm: ... } }) would fail silently or hang because the old signalling code used the legacy WebSocket endpoint and message format, which this realm type does not accept.

NETHERNET_JSONRPC realms use a different signalling server endpoint (/ws/v1.0/messaging/connect) with a json-rpc 2.0 message format:

  • TURN credentials are fetched via a Signaling_TurnAuth_v1_0 request immediately on connect.
  • Sending signals uses Signaling_SendClientMessage_v1_0. The message field contains a json-stringified Signaling_WebRtc_v1_0 object that has the local nethernet id alongside the signal text.
  • Receiving signals arrives as Signaling_ReceiveMessage_v1_0 server notifications, which the client acknowledges with a null result response.
  • Keep-alive is System_Ping_v1_0 every 5 seconds.

LucienHH and others added 11 commits April 17, 2026 14:57
The realms server expects the full ice candidate list to be bundled with
the offer. Trickled candidates are silently ignored on the server,
resulting in the handshake never completing.
New bedrock signalling servers use a different endpoint and the jsonrpc
format instead of the old Type/From/Message format. The old endpoint
was wss://signal.franchise.minecraft-services.net/ws/v1.0/signaling/<id>,
the new one is wss://<host>/ws/v1.0/messaging/connect with the network
id passed in the jsonrpc message body instead of the url.

- constructor now accepts an object with options.protocol and
  options.host
- init() connects to the new endpoint and uses randomUUID() for
  session-id and request-id headers instead of networkId and Date.now()
- write() sends jsonrpc or legacy format based on `this.protocol`
- onMessage() Signal case now calls parseSignalMessage() instead of
  SignalStructure.fromString() directly so both formats work
- onMessage() now has a default case that catches inbound jsonrpc
  messages and routes them to onJsonRpcMessage()
- added onJsonRpcMessage() method
- added parseSignalMessage() helper that tries jsonrpc first, falls back
  to SignalStructure.fromString() for legacy format
- added parseJsonRpcSignal() helper that extracts signal data from a
  jsonrpc payload
Realms on current minecraft versions return networkProtocol:
`NETHERNET_JSONRPC` from the join endpoint instead of a host:port
address. `realmAuthenticate` now detects this and sets the nethernet
transport, networkId, and signalling host.
realm.getAddress() strips networkProtocol from the response so we lose the
`NETHERNET_JSONRPC` flag. Call the raw `/worlds/{id}/join` endpoint
so we can get networkProtocol, address, and sessionRegionData.

For `NETHERNET_JSONRPC` realms: set skipPing, set `_signallingProtocol`
to `jsonrpc`, and `_signallingHost` to the regional signal server. Also
pass minecraftVersion to RealmAPI.from so the Realms API does not reject
the request with `unknown_client_version`.
Two fixes for NetherNet Realms support:
1. `this.nethernet ??= {}` before NethernetClient creation so the object
   exists even when realmAuthenticate sets the backend after
   construction.
2. Forward `_signallingProtocol` to NethernetSignal so it knows to use
   the jsonrpc endpoint instead of the legacy endpoint.
realmAuthenticate adds skipPing, transport, networkId, etc. to
`client.options` after the Client is constructed. The original code
checked the `options` closure variable which is not the same object,
so skipPing was never seen as true and the client tried to ping a
nethernet uuid as a host, leading to a connect timeout.
NetherNet Realms use a different signalling server endpoint and protocol
than legacy Realms:

- Endpoint: /ws/v1.0/messaging/connect
- TURN creds: Signaling_TurnAuth_v1_0 request/response
- Send signals: `Signaling_SendClientMessage_v1_0` with a message field
  containing `Signaling_WebRtc_v1_0` jsonrpc payload (which carries
  our local nethernet id so the server can route CONNECTRESPONSE back)
- Receive signals: `Signaling_ReceiveMessage_v1_0` server notifications
- Keep-alive: `System_Ping_v1_0` every 5 s

The class now detects the protocol from options.protocol ('jsonrpc' or
'legacy') and falls back from legacy to jsonrpc on close if credentials
were never received allowing compatibility with older Realms that still
use the legacy endpoint/format.
@yntha
Copy link
Copy Markdown
Author

yntha commented May 10, 2026

Here is an example client that can be used to test this PR:

const bedrock = require('.')

const client = bedrock.createClient({
  version: '1.26.20',
  realms: {
    pickRealm: (realms) => realms[0]
  }
})

client.on('spawn', () => {
  console.log('Hooray for PR #735!!!')
  client.close()
  process.exit(0)
})

client.on('kick', (p) => console.log('Kicked:', p.message))
client.on('disconnect', (p) => console.log('Disconnected:', p.message))
client.on('error', (e) => console.error('Error:', e?.message || e))

process.once('SIGINT', () => { client.close(); process.exit(0) })

Copy link
Copy Markdown
Member

@extremeheat extremeheat left a comment

Choose a reason for hiding this comment

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

Thanks for working on this. I don't have Minecraft realms so I can't test this locally but overall the code LGTM, I left some comments

Comment thread src/server.js
this.transportServer = require('./rak')(this.options.raknetBackend).RakServer
this.advertisement = new ServerAdvertisement(this.options.motd, this.options.port, this.options.version)
this.batchHeader = 0xfe
this.disableEncryption = false
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 seems like misnomer as it would imply it could affect raknet. Why not flag on the transport ?

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.

Yep true

Comment thread src/transforms/framer.js
Comment on lines -43 to +44
if (this.batchHeader && buf[0] !== this.batchHeader) throw Error(`bad batch packet header, received: ${buf[0]}, expected: ${this.batchHeader}`)
const buffer = buf.slice(1)
if (client.batchHeader && buf[0] !== client.batchHeader) throw Error(`bad batch packet header, received: ${buf[0]}, expected: ${client.batchHeader}`)
const buffer = buf.slice(client.batchHeader ? 1 : 0)
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.

Why this change to go into client? If it's constant per Framer instance you can get from there

Also it seems batchHeader should be checking != null rather falsy conditions

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.

Yeah this could be moved, originally it was due to being transport specific and that was handled on the client

Comment on lines +13 to +14
networkId: pong.sender_id,
useSignalling: false
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

Copy link
Copy Markdown
Member

@extremeheat extremeheat May 10, 2026

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.

Comment thread package.json
"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

@LucienHH
Copy link
Copy Markdown
Contributor

@extremeheat this is to add Nethernet support to bedrock-protocol so this also allows joining or hosting Minecraft world sessions which previously was not possible.

An extension of the #533 PR. Not sure if we should move the changes there or close 533

@AdemonG1tHub
Copy link
Copy Markdown

Thanks for working on this. I don't have Minecraft realms so I can't test this locally but overall the code LGTM, I left some comments

I can help test this as I own a Realm.

@extremeheat
Copy link
Copy Markdown
Member

extremeheat commented May 11, 2026

@LucienHH do you want to continue working on other PR? If so yntha could open PR against your branch

We could also merge your PR into a separate branch for now and change head on this PR to that

@LucienHH
Copy link
Copy Markdown
Contributor

@extremeheat Lets do the latter option and focus on getting something stable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants