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>
This commit is contained in:
claude-timemachine
2026-06-19 14:15:32 +02:00
commit d73c1c9537
35 changed files with 8894 additions and 0 deletions
+358
View File
@@ -0,0 +1,358 @@
# 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.