Files
minecraft_protocol/00-overview.md
T
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

269 lines
11 KiB
Markdown
Raw 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.
# Minecraft Java Edition Protocol — Overview
> **Scope:** Java Edition only. Protocol details change with each release; version-specific differences are called out explicitly. "Latest" means 1.21.x as of mid-2026 unless noted.
>
> **Primary sources:**
> - `node-minecraft-protocol` v1.66.2 (`/tmp/mcproto-refs/node-minecraft-protocol/`)
> - minecraft.wiki/w/Java_Edition_protocol (formerly wiki.vg) — [Packet format](https://minecraft.wiki/w/Java_Edition_protocol) / [Data types](https://minecraft.wiki/w/Java_Edition_protocol/Data_types)
---
## 1. Transport
- **TCP**, no UDP (voice-chat plugins add their own UDP layer out-of-band).
- Default port: **25565** (configurable).
- All data is **big-endian** except VarInt/VarLong, which use little-endian 7-bit groups (see `01-data-types.md`).
- **Encryption** is applied after the Login handshake (online-mode servers). The cipher is AES-128-CFB8, using the shared secret as both key and IV. Encryption wraps the raw TCP byte stream; packet framing runs on top of the encrypted layer.
- Source: `node-minecraft-protocol/src/transforms/encryption.js:6-9` (`aes-128-cfb8`, key=IV=shared secret).
---
## 2. Packet Framing
Every packet on the wire is length-prefixed. There are two modes: **uncompressed** (before the server sends `Set Compression`) and **compressed** (after).
### 2.1 Uncompressed packet format
| Field | Type | Notes |
|------------|---------|------------------------------------------------|
| Length | VarInt | Byte count of everything that follows (Packet ID + Data). |
| Packet ID | VarInt | State- and direction-scoped identifier. |
| Data | bytes | Payload; structure defined per packet. |
The `Framer` prepends the length:
```js
// node-minecraft-protocol/src/transforms/framing.js:16-21
const varIntSize = sizeOfVarInt(chunk.length)
const buffer = Buffer.alloc(varIntSize + chunk.length)
writeVarInt(chunk.length, buffer, 0)
chunk.copy(buffer, varIntSize)
```
The `Splitter` reads length, then slices exactly that many bytes — buffering partial reads across TCP segments (framing.js:48-73).
### 2.2 Compressed packet format
Enabled by the serverbound `Set Compression` packet (Login state). After that, **every** subsequent packet uses:
| Field | Type | Notes |
|----------------|---------|-----------------------------------------------------------------------------|
| Packet Length | VarInt | Byte count of `Data Length` field + compressed payload. |
| Data Length | VarInt | Uncompressed size of (Packet ID + Data). **0 = not compressed.** |
| Packet ID | VarInt | } zlib-deflated together when `Data Length > 0` |
| Data | bytes | } |
**Threshold rule:** if `len(Packet ID + Data) >= compressionThreshold`, compress and set `Data Length` to the uncompressed size. Otherwise send uncompressed with `Data Length = 0`.
```js
// node-minecraft-protocol/src/transforms/compression.js:22-40
if (chunk.length >= this.compressionThreshold) {
const newChunk = zlib.deflateSync(chunk)
// prepend VarInt(chunk.length), then compressed bytes
} else {
// prepend VarInt(0), then raw bytes
}
```
**Historical note:** Compression was added in **1.8** (protocol 47). Per `server/login.js:179`, node-minecraft-protocol gates compression on `protocolVersion >= 27` (snapshot 14w28a), which predates the 1.8 release — whole-protocol compression replaced earlier per-packet compression in that snapshot.
### 2.3 Packet size limits
- Uncompressed maximum: **2²¹ 1 = 2,097,151 bytes** (the `Length` VarInt may not exceed 3 bytes on the wire). — [minecraft.wiki](https://minecraft.wiki/w/Java_Edition_protocol)
- Serverbound compressed: uncompressed (Packet ID + Data) must be ≤ 2²³ bytes (8,388,608). <!-- VERIFY: wiki states this limit; not reflected in node-minecraft-protocol source -->
- Plugin message unrecognised-channel data: vanilla client caps at 1,048,576 bytes. <!-- VERIFY -->
---
## 3. Connection State Machine
The protocol is divided into **states**. Each state has its own independent set of packet IDs. Transitions are triggered by specific packets.
```js
// node-minecraft-protocol/src/states.js:3-8
const states = {
HANDSHAKING: 'handshaking',
STATUS: 'status',
LOGIN: 'login',
CONFIGURATION: 'configuration', // added in 1.20.2 (protocol 764)
PLAY: 'play'
}
```
### State diagram
```mermaid
stateDiagram-v2
[*] --> Handshaking : "TCP connect"
Handshaking --> Status : "Handshake (nextState=1)"
Handshaking --> Login : "Handshake (nextState=2)"
Handshaking --> Login : "Handshake (nextState=3, Transfer 1.20.5+)"
Status --> [*] : "Status Response + Ping/Pong"
Login --> Configuration : "Login Success → Login Acknowledged (1.20.2+)"
Login --> Play : "Login Success (pre-1.20.2)"
Configuration --> Play : "Finish Configuration (both sides)"
Play --> Configuration : "Start Configuration (server-initiated, 1.20.2+)"
Play --> [*] : "Disconnect"
```
### 3.1 Handshaking
Initial state on connect. Client sends exactly one packet:
- **Handshake** (0x00, serverbound): carries `protocolVersion`, `serverHost`, `serverPort`, and `nextState` (1 = Status, 2 = Login, 3 = Transfer).
After this packet the server transitions immediately to the indicated next state.
```js
// node-minecraft-protocol/src/server/handshake.js:34-38
if (packet.nextState === 1) {
client.state = states.STATUS
} else if (packet.nextState === 2) {
client.state = states.LOGIN
}
```
`nextState = 3` (Transfer) was added in **1.20.5** for server-transfer support. <!-- VERIFY exact protocol number for Transfer -->
### 3.2 Status
Server-list ping. Two round-trips:
1. Client → **Status Request** (0x00)
2. Server → **Status Response** (0x00) — JSON payload with MOTD, player count, favicon
3. Client → **Ping Request** (0x01) — 8-byte timestamp
4. Server → **Pong Response** (0x01) — echoes timestamp
Connection is then closed by the client. The server does not transition to another state.
### 3.3 Login
Authentication and encryption negotiation.
Typical online-mode sequence:
1. C→S: `Login Start` (username, UUID, optional profile-key signature)
2. S→C: `Encryption Request` (server ID, RSA public key, verify token)
3. C→S: `Encryption Response` (RSA-encrypted shared secret + verify token)
4. *(Both sides enable AES-128-CFB8 encryption)*
5. Server calls Mojang session-server to verify join
6. S→C: `Set Compression` (threshold; gates compressed mode) — added in **1.8** / snapshot 14w28a
7. S→C: `Login Success` (UUID, username, properties)
8. C→S: `Login Acknowledged` — triggers transition to Configuration (1.20.2+)
```js
// node-minecraft-protocol/src/server/login.js:179
if (client.protocolVersion >= 27) {
client.write('compress', { threshold: 256 })
client.compressionThreshold = 256
}
```
```js
// node-minecraft-protocol/src/server/login.js:189-193
if (client.supportFeature('hasConfigurationState')) {
client.once('login_acknowledged', onClientLoginAck)
} else {
client.state = states.PLAY
}
```
### 3.4 Configuration (1.20.2+, protocol 764+)
Introduced in **1.20.2**. Entered after Login Acknowledged; used for:
- Registry codec data (dimension types, biomes, etc.)
- Feature flags
- Known packs negotiation
- Server brand plugin channel
Both client and server exchange `Finish Configuration` to exit this state.
```js
// node-minecraft-protocol/src/server/login.js:224-238 (onClientLoginAck)
client.state = states.CONFIGURATION
// ... send registry_data, then finish_configuration
client.once('finish_configuration', () => {
client.state = states.PLAY
})
```
The server can re-enter Configuration from Play at any time by sending `Start Configuration` (serverbound: `Configuration Acknowledged` from client acknowledges the switch back).
```js
// node-minecraft-protocol/src/client/play.js:43-44
client.on('start_configuration', () => enterConfigState())
// ...
if (client.state === states.PLAY) {
client.write('configuration_acknowledged', {})
}
```
### 3.5 Play
Normal gameplay state. The bulk of all packets are Play-state packets. This state persists until disconnect or a server-initiated re-entry into Configuration.
---
## 4. Packet ID Scoping
Packet IDs are **not globally unique**. They are scoped by **both state and direction**:
- **State**: Handshaking / Status / Login / Configuration / Play
- **Direction**: serverbound (client → server) or clientbound (server → client)
A packet ID of `0x00` in Handshaking-serverbound (Handshake packet) is completely unrelated to `0x00` in Status-clientbound (Status Response), and so on. You need all three pieces — state, direction, ID — to unambiguously identify a packet.
The serializer key in node-minecraft-protocol reflects this:
```js
// node-minecraft-protocol/src/transforms/serializer.js:47-51
function createSerializer ({ state = states.HANDSHAKING, isServer = false, version, ... }) {
return new Serializer(
createProtocol(state, !isServer ? 'toServer' : 'toClient', version, ...),
'packet'
)
}
```
Concrete packet ID assignments live in `minecraft-data` (a separate package) and change between protocol versions. Always consult the version-specific protocol data, not hard-coded constants.
---
## 5. Encryption Layer Position
The pipeline (innermost to outermost on send) is:
```
Packet (ID + payload)
→ Compression (optional, zlib deflate)
→ Framing (VarInt length prefix)
→ Encryption (AES-128-CFB8, wraps the byte stream after framing)
→ TCP
```
Framing runs before encryption so the receiver can split frames while decrypting. Both directions use the same shared secret but independent cipher/decipher instances.
Source: `node-minecraft-protocol/src/transforms/encryption.js` — AES-128-CFB8 with `key = IV = sharedSecret`.
---
## 6. Legacy Server List Ping
Clients predating the current framing (pre-1.7) send a `0xFE` byte as the first byte of the TCP stream. node-minecraft-protocol detects this and shims it into the modern VarInt-framed pipeline:
```js
// node-minecraft-protocol/src/transforms/framing.js:36-43
if (this.recognizeLegacyPing && this.buffer[0] === LEGACY_PING_PACKET_ID) {
// Prefix a VarInt-encoded packet ID so the deserializer can handle it
}
```
This is a compatibility shim only; the modern protocol framing is always VarInt-prefixed as described in §2.
---
*See `01-data-types.md` for wire type encodings (VarInt, Position, NBT, etc.).*