Files
claude-timemachine d73c1c9537 minecraft_protocol: foundation + per-version protocol docs 1.7.10->26.2
8 topical docs (overview, data types, lifecycle, handshake, status/ping,
login+encryption, configuration, version-differences) + proxy-forwarding set
+ 16 per-version release-line docs, sourced from minecraft.wiki, ViaVersion
(source + commits), minecraft-data, node-minecraft-protocol, Velocity, BungeeCord.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 14:15:32 +02:00

359 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 04 — Server List Ping (SLP)
How the multiplayer server list fetches a server's MOTD, version, and player count.
Covers modern SLP (1.7+) and the three legacy `0xFE` variants.
---
## 1. Modern SLP (1.7+)
### 1.1 Packet sequence
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
C->>S: TCP connect (port 25565)
C->>S: Handshake (0x00, nextState=1)
C->>S: Status Request (0x00, no fields)
S-->>C: Status Response (0x00, JSON string)
C->>S: Ping Request (0x01, Long timestamp)
S-->>C: Pong Response (0x01, echo Long)
S->>C: (server closes connection)
```
The client **may** close after receiving the Status Response without sending Ping
(e.g. when only scraping the MOTD). Notchian servers will wait up to ~30 s for
the Ping Request before timing out if the client leaves the connection open.
<!-- wiki: "Notchian servers will for unknown reasons wait to receive the
following Ping Request packet for 30 seconds before timing out" -->
### 1.2 Packet definitions
All packets use the standard [Minecraft packet framing](https://minecraft.wiki/w/Java_Edition_protocol)
(VarInt length prefix + VarInt packet ID + fields).
#### Handshaking state → Status
| Direction | ID | Name | Fields |
|-----------|------|------------|--------|
| C→S | 0x00 | Handshake | `protocolVersion` VarInt, `serverAddress` String(255), `serverPort` Unsigned Short, `nextState` VarInt (1=Status) |
Source: [minecraft.wiki/w/Java_Edition_protocol/Packets](https://minecraft.wiki/w/Java_Edition_protocol/Packets)
The wiki uses **Intent** as the field name (as of 1.20.5+); older docs call it
`nextState`. The value `1` always means "go to Status state."
<!-- VERIFY: field rename to "Intent" exact version -->
#### Status state
| Direction | ID | Name | Fields |
|-----------|------|-----------------|--------|
| C→S | 0x00 | Status Request | *(none)* |
| S→C | 0x00 | Status Response | `jsonResponse` String(32767) |
| C→S | 0x01 | Ping Request | `timestamp` Long |
| S→C | 0x01 | Pong Response | `timestamp` Long (echoed) |
Source: [minecraft.wiki/w/Java_Edition_protocol/Packets](https://minecraft.wiki/w/Java_Edition_protocol/Packets)
The Ping payload is a `Long` (8 bytes, signed, big-endian) — typically a
millisecond epoch timestamp the client uses to measure round-trip latency.
`node-minecraft-protocol` sends `[0, 0]` (two 32-bit zero words that compose
one 64-bit zero) and measures wall-clock delta separately
(`src/ping.js:55`, `src/server/ping.js:62`).
### 1.3 The Status Response JSON
The server writes the entire status as a single JSON string inside the
Status Response packet.
#### Annotated example
```json
{
"version": {
"name": "1.21.8", // free string — shown in client UI on version mismatch
"protocol": 772 // actual protocol number client compares
},
"players": {
"max": 20, // max player slots (can be overridden freely)
"online": 1, // current player count
"sample": [ // optional list shown in hover tooltip
{
"name": "thinkofdeath",
"id": "4566e69f-c907-48ee-8d71-d7ba5aa00d20" // must be well-formed UUID
}
]
},
"description": { // the MOTD — see §1.4 for evolution
"text": "Hello, world!"
},
"favicon": "data:image/png;base64,<data>", // 64×64 PNG, no newlines (1.13+)
"enforcesSecureChat": false // added in 1.19.1
}
```
Source: [minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping) — example JSON
`node-minecraft-protocol` server-side builds this object at
`src/server/ping.js:31-39`:
```js
const response = {
version: responseVersion,
players: { max: server.maxPlayers, online: server.playerCount, sample: [] },
description: server.motdMsg ?? { text: server.motd },
favicon: server.favicon
}
```
#### Field reference
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `version.name` | String | No (1.20+: omission shows "Old") | Display label — **not** version-checked by client |
| `version.protocol` | Integer | No | Client compares against its own; mismatch → red/orange label |
| `players.max` | Integer | No | `???` shown if `players` omitted; max value 2³¹−1 |
| `players.online` | Integer | No | Same |
| `players.sample` | Array | No | Objects with `name` (String) + `id` (UUID String) |
| `description` | Text Component | No | See §1.4 |
| `favicon` | String | No | `data:image/png;base64,…`; exactly 64×64 px; no newlines since 1.13 |
| `enforcesSecureChat` | Boolean | No | Added **1.19.1** — client shows warning if server value differs from expectation |
| `previewsChat` | Boolean | No | Added alongside `enforcesSecureChat`; deprecated/removed in later 1.19.x releases <!-- VERIFY: exact removal version --> |
Sources: [minecraft.wiki SLP page](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping);
`enforcesSecureChat` version from wiki field table.
<!-- VERIFY: previewsChat exact add/remove versions -->
### 1.4 Description field — chat component evolution
The `description` field has gone through three forms:
| Era | Format | Example |
|-----|--------|---------|
| 1.71.8 | Plain string with `§` codes | `"§aA §bcoloured §cMOTD"` |
| 1.91.15 | JSON Text Component object, but vanilla still embeds `§` codes inside `{"text":"…"}` | `{"text":"§aHello"}` |
| 1.16+ | Full structured Text Component (Spigot/Paper); vanilla still uses `§`-embedded strings | `{"text":"Hello","color":"green"}` |
The wiki notes: "Notchian servers embed section-sign-based codes within the
text value, while third-party servers such as Spigot and Paper will return full
components." Both forms are valid — clients accept either.
`node-minecraft-protocol`'s server prefers a structured object (`server.motdMsg`)
but falls back to `{ text: server.motd }` with a plain string
(`src/server/ping.js:38`).
<!-- VERIFY: exact version 1.7 vs 1.8 for original JSON SLP introduction -->
### 1.5 Version spoofing
`version.name` is a **free string** — the server can write anything here.
`version.protocol` is the **number** the client actually compares against its own.
Common patterns:
| Scenario | `version.name` | `version.protocol` |
|----------|----------------|--------------------|
| Matching client | `"1.21.8"` | `772` |
| Outdated client (server newer) | `"1.21.8"` | `772` — client shows "Outdated client!" |
| Outdated server (client newer) | `"1.20.4"` | `765` — client shows "Outdated server!" |
| ViaVersion (accepts many) | `"Requires 1.21+ (1.7-1.20.6 supported)"` | client's own protocol number |
| Discovery ping (unknown client ver) | `"Paper 1.21"` | `-1` (sentinel) |
ViaVersion sets `version.protocol` to the **connecting client's** protocol number
so the client always sees a match, then handles translation internally.
<!-- VERIFY: ViaVersion -1 sentinel vs client-echo behavior — wiki says -1 may cause close -->
The wiki notes that for 1.6 legacy responses from a 1.7+ server, the protocol
version in the response is always `127` (an incompatibility sentinel).
---
## 2. Legacy `0xFE` Server List Ping
Pre-1.7 clients used a simple kick-packet hack. There are three variants,
distinguished by how much data the client sends.
### 2.1 Overview
| Client version | Client sends | Response separator |
|----------------|--------------|--------------------|
| Beta 1.8 1.3.2 | `FE` | `§` (section sign, U+00A7) |
| 1.4 1.5.2 | `FE 01` | `\0` (null, U+0000) |
| 1.6.x | `FE 01 FA …` (plugin message) | `\0` (null) |
All responses are sent as a **kick packet** (`0xFF`) containing a UTF-16BE
encoded string.
### 2.2 Variant A — Beta 1.8 through 1.3.2
**Client → Server:** single byte `0xFE`
**Server → Client** (kick packet):
```
FF packet ID
XX XX UInt16BE: number of UTF-16 code units in the response string
[UTF-16BE data]
```
Response string format (fields joined with `§`):
```
<MOTD>§<online>§<max>
```
Example: `A Minecraft Server§5§20`
`node-minecraft-protocol` handles this as `payload === undefined` (neither 0 nor
1), falling into the `else` branch at `src/server/ping.js:73`:
```js
sendPingResponse([server.motd, server.playerCount.toString(),
server.maxPlayers.toString()].join('\xa7'))
```
(`\xa7` = `§`)
### 2.3 Variant B — 1.4.2 through 1.5.2
**Client → Server:** two bytes `FE 01`
**Server → Client:** same kick packet format, but fields joined with **null**
(``), and a `§1` prefix to signal the extended format:
```
§1<NUL><protocol><NUL><version><NUL><MOTD><NUL><online><NUL><max>
```
Where `<NUL>` is U+0000 (null character).
`node-minecraft-protocol` handles this at `src/server/ping.js:68-71`
(`packet.payload === 1`):
```js
sendPingResponse('\xa7' + [pingVersion, server.mcversion.version,
server.mcversion.minecraftVersion, server.motd,
server.playerCount.toString(), server.maxPlayers.toString()].join('\0'))
```
Note: `pingVersion` is the hardcoded constant `1` (the ping format version,
not the Minecraft protocol version).
### 2.4 Variant C — 1.6.x
**Client → Server** (full hex):
```
FE packet ID
01 payload
FA plugin message packet ID
00 0B string length: 11 (UTF-16BE code units)
00 4D 00 43 00 7C "MC|PingHost" in UTF-16BE
00 50 00 69 00 6E
00 67 00 48 00 6F
00 73 00 74
XX XX rest-of-data byte length = 7 + len(hostname bytes)
XX client's protocol version (single byte)
XX XX hostname length in UTF-16BE code units
[hostname in UTF-16BE]
XX XX XX XX server port (big-endian int32)
```
The wiki notes: "All Notchian servers only care about the first 3 bytes
(`FE 01 FA`)." The hostname/port data is informational and typically ignored.
**Server → Client** — same kick packet, same null-delimited format as variant B:
```
FF
XX XX UInt16BE length
00 A7 00 31 00 00 U+00A7 "§" + U+0031 "1" + U+0000 null separator
[UTF-16BE: protocol<NUL>version<NUL>MOTD<NUL>online<NUL>max]
```
For a 1.7+ server responding to a 1.6 client, `protocol` is always `127`
(incompatibility sentinel). Source: [minecraft.wiki SLP — Legacy ping](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping)
### 2.5 Response wire encoding
All legacy responses share the same packet structure
(`src/server/ping.js:77-93`):
```js
function sendPingResponse (responseString) {
function utf16be (s) {
return endianToggle(Buffer.from(s, 'utf16le'), 16) // swap to big-endian
}
const responseBuffer = utf16be(responseString)
const length = responseString.length // char count, not byte count
const lengthBuffer = Buffer.alloc(2)
lengthBuffer.writeUInt16BE(length)
const raw = Buffer.concat([Buffer.from('ff', 'hex'), lengthBuffer, responseBuffer])
client.socket.write(raw) // bypasses packet framer
}
```
The response is written **directly to the socket** (not through the packet
framer) because the length field counts UTF-16 code units, not bytes, and the
normal framer would prepend a VarInt length which legacy clients don't expect.
### 2.6 Detection on the server
A 1.7+ server recognises a legacy ping by the `0xFE` first byte, which is not a
valid VarInt-prefixed packet. `node-minecraft-protocol` maps this to the
`legacy_server_list_ping` event and distinguishes variants by `packet.payload`:
| `packet.payload` | Variant |
|------------------|---------|
| `undefined` | Beta 1.81.3.2 (`FE` only) |
| `1` | 1.41.5.2 (`FE 01`) or 1.6.x (`FE 01 FA …`) |
Source: `src/server/ping.js:67-93`
---
## 3. Protocol number reference
A selection of protocol numbers for version context:
| Release | Protocol |
|---------|----------|
| 1.7.10 | 5 |
| 1.8.9 | 47 |
| 1.12.2 | 340 |
| 1.16.5 | 754 |
| 1.19.1 | 760 |
| 1.20.4 | 765 |
| 1.21.1 | 767 |
| 1.21.8 | 772 (example from wiki) |
Current protocol: **775** (as of the Packets page at time of writing).
Source: [minecraft.wiki/w/Java_Edition_protocol/Packets](https://minecraft.wiki/w/Java_Edition_protocol/Packets)
<!-- VERIFY: protocol 772 vs 775 discrepancy — wiki example JSON uses 772,
Packets page header says 775; likely two different 1.21.x patch versions -->
---
## 4. Sources
| Source | Used for |
|--------|----------|
| [minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping) | JSON field table, legacy ping bytes, response formats, favicon 1.13 note, §1 sentinel |
| [minecraft.wiki/w/Java_Edition_protocol/Packets](https://minecraft.wiki/w/Java_Edition_protocol/Packets) | Packet IDs, field types, Handshake Intent field name |
| `node-minecraft-protocol` `src/ping.js` | Client-side modern SLP flow (lines 41-68) |
| `node-minecraft-protocol` `src/server/ping.js` | Server-side response construction (lines 1-94) |
---
## 5. VERIFY flags (open questions)
1. `<!-- VERIFY -->` **`enforcesSecureChat` exact version** — wiki does not specify
version inline; attributed to 1.19.1 based on the 1.19.1 secure-chat feature
rollout. Confirm against `wiki/w/Java_Edition_1.19.1`.
2. `<!-- VERIFY -->` **`previewsChat` add/remove versions** — present in 1.19.1
initial rollout, removed or no-op'd by 1.19.3. Confirm.
3. `<!-- VERIFY -->` **`nextState``Intent` field rename version** — wiki Packets
page uses "Intent" as of current; confirm when the rename happened (cosmetic
doc change, wire format unchanged).
4. `<!-- VERIFY -->` **ViaVersion protocol echo vs 1** — confirm ViaVersion echoes
the connecting client's protocol number (rather than using a fixed sentinel).
5. `<!-- VERIFY -->` **Protocol 772 vs 775** — wiki SLP example JSON shows `"protocol": 772`
but Packets page header shows current 775; resolve to exact MC versions.
6. `<!-- VERIFY -->` **Description plain-string support in 1.7** — confirm that
`"description": "plain string"` (no wrapping object) was accepted by the
official client in 1.7.x.