Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 17 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,25 @@ Or with npm to use programmatically:
`npx minecraft-bedrock-server --version 1.18.0 --online --path ./my1.18server`

To query a version's Bedrock protocol metadata, start a temporary server and print
its RakNet PONG response as JSON:
its RakNet or Nethernet advertisement as JSON:

```sh
npx minecraft-bedrock-server -v 1.19.1 --dump-pong-details
```

This includes the raw PONG payload and any fields available from the response,
including `protocolVersion`, `versionName`, MOTDs, player counts, game mode,
and IPv4/IPv6 ports. Incomplete or non-standard PONG responses are returned
with the fields that could be extracted.
The result includes `transport` (`raknet` or `nethernet`) and available metadata:
`protocolVersion`, `versionName`, MOTDs, player counts, and game mode. RakNet
also advertises IPv4/IPv6 ports. `rawPong` contains the RakNet text or the
Nethernet hexadecimal advertisement; Nethernet also reports `advertisementVersion`.
Fields absent from the advertisement are omitted, including game/protocol versions
on older Nethernet v4 advertisements. No Minecraft version database is required.

Nethernet discovery uses local UDP port 7551 and requires
`enable-lan-visibility=true` (the server default). Run one discoverable local
server at a time when using this helper. The configured `server-port` is the
Nethernet HTTP signalling port, not the discovery port; `server-portv6` is ignored
by Nethernet BDS. Empty advertisements are retried and eventually produce an error.
Older servers continue to use RakNet discovery on their configured ports.

any extraneous -- options will be placed inside the `server.properties` file, e.g. `--level-name coolWorld`.

Expand Down Expand Up @@ -91,7 +100,7 @@ npx minecraft-bedrock-server -v 1.19.1 --dump-pong-details
**via code**

The underlying API `getPongDetails` downloads and starts the requested server version, sends a
RakNet unconnected ping, and stops the server after receiving its response:
discovery request using the transport in its generated `server.properties`, and stops the server after receiving its response:

```js
const details = await bedrockServer.getPongDetails('1.19.1', {
Expand All @@ -105,6 +114,7 @@ The above log or return data in this structure:

```js
{
transport: 'raknet',
rawPong: 'MCPE;Dedicated Server;527;1.19.1;0;10;...',
edition: 'MCPE',
motd: 'Dedicated Server',
Expand Down Expand Up @@ -135,7 +145,7 @@ Options:
--port6 Port to listen on for IPv6 (default: 19133)
--online Whether to run in online mode
--path Custom path to the server directory
--dump-pong-details Start a server and print its RakNet PONG details as JSON
--dump-pong-details Start a server and print its RakNet or Nethernet advertisement as JSON
--versions Passing --versions will list all versions
--download Download (but not run) the server binary for this platfrom (default: linux)
Usage:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"dependencies": {
"basic-args": "^1.4.0",
"debug": "^4.3.3",
"prismarine-nbt": "^2.5.0"
"prismarine-nbt": "^2.5.0",
"protodef": "^1.19.0"
},
"devDependencies": {
"minecraft-bedrock-server": "file:.",
Expand Down
122 changes: 122 additions & 0 deletions src/advertisement.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
{
"advertisement_v4": [
"container",
[
{
"name": "version",
"type": "u8"
},
{
"name": "motd",
"type": [
"pstring",
{
"countType": "u8"
}
]
},
{
"name": "levelName",
"type": [
"pstring",
{
"countType": "u8"
}
]
},
{
"name": "gamemodeId",
"type": "u8"
},
{
"name": "playerCount",
"type": "li32"
},
{
"name": "playersMax",
"type": "li32"
}
]
],
"advertisement_v7": [
"container",
[
{
"name": "version",
"type": "u8"
},
{
"name": "motd",
"type": [
"pstring",
{
"countType": "varint"
}
]
},
{
"name": "protocol",
"type": "zigzag32"
},
{
"name": "gameVersion",
"type": [
"pstring",
{
"countType": "varint"
}
]
},
{
"name": "levelName",
"type": [
"pstring",
{
"countType": "varint"
}
]
},
{
"name": "playerCount",
"type": "zigzag32"
},
{
"name": "playersMax",
"type": "zigzag32"
},
{
"name": "gamemodeId",
"type": "zigzag32"
},
{
"name": "isEditorWorld",
"type": "bool"
},
{
"name": "hardcore",
"type": "bool"
},
{
"name": "acceptsOnlineAuth",
"type": "bool"
},
{
"name": "acceptsSelfSignedAuth",
"type": "bool"
},
{
"name": "nonce",
"type": [
"pstring",
{
"countType": "varint"
}
]
},
{
"name": "connectionType",
"type": "zigzag32"
}
]
]
}
5 changes: 3 additions & 2 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const opt = require('basic-args')({
path: { type: String, description: 'Custom path to the server directory', default: null },

versions: { type: Boolean, description: 'Passing --versions will list all versions' },
'dump-pong-details': { type: Boolean, description: 'Start a server and print its RakNet PONG details as JSON' },
'dump-pong-details': { type: Boolean, description: 'Start a server and print its RakNet or Nethernet advertisement as JSON' },
download: { type: String, description: `Download (but not run) the server binary for this platfrom (default: ${process.platform})`, default: null }
},
examples: [
Expand Down Expand Up @@ -48,7 +48,8 @@ async function main () {
'server-port': opt.port,
'server-portv6': opt.port6,
'online-mode': Boolean(opt.online),
path: opt.path ? opt.path : undefined
path: opt.path ? opt.path : undefined,
...opt._
})))
} else {
const customOptions = opt._ || {}
Expand Down
34 changes: 20 additions & 14 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ declare module "minecraft-bedrock-server" {
type PermissionLevel = "visitor" | "member" | "operator";

type ServerOptions = {
"transport"?: "raknet" | "nethernet"; // Defaults to the server binary's configured transport
"enable-lan-visibility"?: boolean; // Required for Nethernet discovery
"server-name"?: string; // Used as the server name
"gamemode"?: GameMode; // Sets the game mode for new players
"force-gamemode"?: boolean; // Prevents the server from sending to the client gamemode values other than the gamemode value saved by the server during world creation
Expand Down Expand Up @@ -69,22 +71,26 @@ declare module "minecraft-bedrock-server" {
function startServerAndWait2(version: string, withTimeout: number, options: ServerOptionsEx): Promise<ChildProcess>

type PongDetails = {
rawPong: string,
edition: string,
motd: string,
protocolVersion: number,
versionName: string,
playerCount: number,
maxPlayerCount: number,
serverUniqueId: string,
motd2: string,
gameMode: string,
gameModeNumeric: number,
portIPv4: number,
portIPv6: number
transport: "raknet" | "nethernet",
rawPong: string, // RakNet text or Nethernet hexadecimal advertisement
advertisementVersion?: number,
edition?: string,
motd?: string,
protocolVersion?: number,
versionName?: string,
playerCount?: number,
maxPlayerCount?: number,
serverUniqueId?: string,
motd2?: string,
gameMode?: string,
gameModeNumeric?: number,
portIPv4?: number,
portIPv6?: number
}

// Starts a server, queries its RakNet PONG details, and stops it.
function parsePongDetails(buffer: Buffer): PongDetails

// Starts a server, queries its RakNet or Nethernet advertisement, and stops it.
function getPongDetails(version: string, options?: ServerOptionsEx & { timeout?: number, pingTimeout?: number, pongRetries?: number }): Promise<PongDetails>

interface BedrockVanillaServer {
Expand Down
42 changes: 25 additions & 17 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { join, resolve } = require('path')
const debug = process.env.CI ? console.debug : require('debug')('minecraft-bedrock-server')
const https = require('https')
const helpers = require('./helper')
const { createNethernetPing, parseNethernetPong } = require('./nethernet')

const serversJsonURL = 'https://net-secondary.web.minecraft-services.net/api/v1.0/download/links'

Expand Down Expand Up @@ -258,6 +259,7 @@ function parsePongDetails (buffer) {
] = rawPong.split(';')
const number = value => value && Number.isFinite(Number(value)) ? Number(value) : undefined
return {
transport: 'raknet',
rawPong,
edition,
motd,
Expand All @@ -274,16 +276,17 @@ function parsePongDetails (buffer) {
}
}

function requestPong (port, timeout = 5000) {
function requestPong (port, timeout = 5000, transport = 'raknet') {
return new Promise((resolve, reject) => {
const targets = Array.isArray(port) ? port : [{ port, host: '127.0.0.1', type: 'udp4' }]
const sockets = new Map()
const ping = Buffer.alloc(33)
ping[0] = 0x01
ping.writeBigInt64BE(BigInt(Date.now()), 1)
raknetMagic.copy(ping, 9)
crypto.randomBytes(8).copy(ping, 25)
let bestPong
const ping = transport === 'nethernet' ? createNethernetPing() : Buffer.alloc(33)
if (transport === 'raknet') {
ping[0] = 0x01
ping.writeBigInt64BE(BigInt(Date.now()), 1)
raknetMagic.copy(ping, 9)
crypto.randomBytes(8).copy(ping, 25)
}
let closed = false
const close = () => {
if (closed) return
Expand All @@ -298,27 +301,28 @@ function requestPong (port, timeout = 5000) {
const interval = setInterval(sendPing, 250)
const timer = setTimeout(() => {
close()
if (bestPong) resolve(bestPong)
else reject(new Error('Timed out waiting for RakNet PONG'))
reject(new Error(`Timed out waiting for a non-empty ${transport} advertisement`))
}, timeout)
for (const target of targets) {
if (!sockets.has(target.type)) {
const socket = dgram.createSocket(target.type)
socket.on('message', (message) => {
const pong = parsePongDetails(message)
if (pong.rawPong) {
try {
if (transport === 'raknet' && (message.length < 35 || message[0] !== 0x1c || !message.subarray(17, 33).equals(raknetMagic))) return
const pong = transport === 'nethernet' ? parseNethernetPong(message) : parsePongDetails(message)
if (!pong.rawPong) return
close()
resolve(pong)
} else {
bestPong = pong
} catch (error) {
debug('Ignoring unreadable discovery response', error.message)
}
})
socket.on('error', () => {
socket.close()
sockets.delete(target.type)
if (!sockets.size) {
close()
reject(new Error('Unable to send RakNet PONG request'))
reject(new Error(`Unable to send ${transport} discovery request`))
}
})
sockets.set(target.type, socket)
Expand All @@ -335,13 +339,17 @@ async function getPongDetails (version, options = { 'server-port': 19130, 'serve
if (!Number.isInteger(pongRetries) || pongRetries < 1) throw new Error('pongRetries must be a positive integer')
const handle = await startServerAndWait(version, timeout, serverOptions)
try {
const ports = [{ port, host: '127.0.0.1', type: 'udp4' }]
const directory = resolve(serverOptions.root || '.', serverOptions.path || 'bds-' + version)
const properties = fs.readFileSync(join(directory, 'server.properties'), 'utf8')
const transport = [...properties.matchAll(/^transport\s*=\s*(\w+)/gm)].at(-1)?.[1] || 'raknet'
if (!['raknet', 'nethernet'].includes(transport)) throw new Error(`Unsupported server transport: ${transport}`)
const ports = [{ port: transport === 'nethernet' ? 7551 : port, host: '127.0.0.1', type: 'udp4' }]
const port6 = Number(options['server-portv6'])
if (port6) ports.push({ port: port6, host: '::1', type: 'udp6' })
if (port6 && transport === 'raknet') ports.push({ port: port6, host: '::1', type: 'udp6' })
let lastError
for (let attempt = 0; attempt < pongRetries; attempt++) {
try {
return await requestPong(ports, pingTimeout)
return await requestPong(ports, pingTimeout, transport)
} catch (error) {
lastError = error
}
Expand Down
Loading
Loading