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
+228
View File
@@ -0,0 +1,228 @@
# 03 — Handshake Packet
**State:** Handshaking → Login or Status
**Direction:** Client → Server
**Packet ID:** `0x00`
**Stable since:** 1.7.2
The Handshake packet is the first packet every modern Minecraft client sends. It is sent exactly once per TCP connection, transitions the protocol state, and carries three pieces of metadata that have been overloaded with proxy and modloader signalling data over the years.
---
## Packet fields
| # | Field | Type | Notes |
|---|-------|------|-------|
| 1 | Protocol Version | VarInt | Negotiated version; e.g. 47 = 1.8, 340 = 1.12.2, 765 = 1.20.4, 766 = 1.20.5, 775 = 26.1 |
| 2 | Server Address | String (255) | Hostname/IP the client used to connect — **subject to all the overloads below** |
| 3 | Server Port | Unsigned Short | Default 25565 |
| 4 | Intent (Next State) | VarInt enum | `1` = Status, `2` = Login, `3` = Transfer *(1.20.5 / protocol 766+)* |
Source: [minecraft.wiki/w/Java_Edition_protocol/Packets](https://minecraft.wiki/w/Java_Edition_protocol/Packets) (Handshaking, C→S, 0x00); field definitions confirmed in `node-minecraft-protocol/src/client/setProtocol.js:21-26` (`protocolVersion`, `serverHost`, `serverPort`, `nextState`); packet schema in `minecraft-data/data/pc/1.8/protocol.json` (`packet_set_protocol`).
### Intent / Next State values
| Value | Meaning | Introduced |
|-------|---------|------------|
| `1` | Status ping | 1.7 |
| `2` | Login (play) | 1.7 |
| `3` | Transfer login | 1.20.5 (protocol 766) |
Intent `3` indicates the client arrived via a `Transfer` packet from another server (cookie-based server transfer introduced in 1.20.5). Both `2` and `3` transition into the Login protocol state; `3` lets the receiving server decide to accept or reject transfers. BungeeCord sets `transferred = true` at `InitialHandler.java:391` when intent is `3`.
---
## SRV record resolution
Before opening the TCP connection the client queries `_minecraft._tcp.<domain>` for a DNS SRV record. If found, the resolved hostname **and port** replace the raw user input:
- `options.host` and `options.port` are updated from the SRV response before `setSocket()` is called.
- The **Server Address field sent in the Handshake packet** is the SRV-resolved hostname, not the original domain the user typed.
Source: `node-minecraft-protocol/src/client/tcp_dns.js:21-33`
```js
// tcp_dns.js:21-33
dns.resolveSrv('_minecraft._tcp.' + options.host, (err, addresses) => {
if (addresses && addresses.length > 0) {
options.host = addresses[0].name // <-- replaces host
options.port = addresses[0].port // <-- replaces port
client.setSocket(net.connect(addresses[0].port, addresses[0].name))
}
})
```
Practical consequence: if `play.example.com` has an SRV record pointing to `mc1.example.com:19132`, the Handshake's Server Address field will contain `mc1.example.com`, not `play.example.com`. Proxies doing virtual-host routing must be SRV-aware.
BungeeCord also strips a trailing `.` that DNS resolvers sometimes append: `InitialHandler.java:362-365`.
---
## Server Address field overloads
The Server Address field is a plain UTF-8 string with a 255-character limit. Three separate systems have abused it by appending NUL-delimited (`\0`) data after the hostname.
### Parse order matters
```
raw Server Address field
├── contains \0? ──yes──▶ split on first \0
│ left = actual hostname (virtualHost)
│ right = extra payload (FML tag or BC forward data)
└── no ──▶ use as-is
```
BungeeCord implements this at `InitialHandler.java:355-359`:
```java
if ( handshake.getHost().contains( "\0" ) ) {
String[] split = handshake.getHost().split( "\0", 2 );
handshake.setHost( split[0] );
extraDataInHandshake = "\0" + split[1]; // re-prepends \0
}
```
---
### Overload 1 — Forge / FML client detection
Forge appends a NUL-delimited marker to signal that the connecting client has Forge loaded. The marker sits immediately after the hostname, before any BungeeCord forwarding data.
| Era | Marker (literal) | Forge versions |
|-----|-----------------|----------------|
| FML (1.71.12.2) | `\0FML\0` | Forge for MC 1.7.x 1.12.x |
| FML2 (1.13+) | `\0FML2\0` | Forge for MC 1.13 1.19.x <!-- VERIFY: exact cutoff --> |
| FML3 (Forge 36+) | `\0FML3\0` | NeoForge / recent Forge <!-- VERIFY: exact version range --> |
BungeeCord source defines only `\0FML\0` as `FML_HANDSHAKE_TOKEN` (`ForgeConstants.java:20`). The comment in `InitialHandler.java:351-354` reads:
> "Starting with FML 1.8, a `\0FML\0` token is appended to the handshake. This interferes with Bungee's IP forwarding, so we detect it, and remove it from the host string, for now. We know FML appends `\00FML\00`. However, we need to also consider that other systems might add their own data to the end of the string. So, we just take everything from the `\0` character and save it for later."
The Forge handler checks `extraDataInHandshake.contains(ForgeConstants.FML_HANDSHAKE_TOKEN)` to set `fmlTokenInHandshake` (`ForgeClientHandler.java:41-46`, `UserConnection.java`).
**Wire layout (Forge client, no proxy):**
```
hostname\0FML\0
```
Example: `mc.example.com\0FML\0`
**The FML-tag-breaks-naive-proxies gotcha:** Any proxy or server plugin that does a naive split on `\0` expecting exactly the BungeeCord forwarding format will get confused when a Forge client connects without going through BungeeCord. The first field after the split is `FML` (or `FML2`, `FML3`), not a client IP address. Always check whether the first extra token is a known FML marker before attempting to parse it as IP-forwarding data.
---
### Overload 2 — BungeeCord legacy IP forwarding
When `ip_forward: true` in BungeeCord's config, BungeeCord rewrites the Handshake packet it sends **to the backend server** to embed the real client IP, UUID, and texture properties. This happens in `ServerConnector.java:114-123`.
**Authoritative source — `ServerConnector.java:116-121`:**
```java
String newHost = copiedHandshake.getHost() + "\00" + AddressUtil.sanitizeAddress( user.getAddress() ) + "\00" + user.getUUID();
LoginResult profile = user.getPendingConnection().getLoginProfile();
if ( profile != null && profile.getProperties() != null && profile.getProperties().length > 0 ) {
newHost += "\00" + LoginResult.GSON.toJson( profile.getProperties() );
}
```
`user.getUUID()` returns the UUID without dashes (`InitialHandler.java:801-803`):
```java
public String getUUID() {
return uniqueId.toString().replace( "-", "" );
}
```
**Exact wire layout:**
```
<hostname>\0<clientIP>\0<uuidNoDashes>\0<propertiesJSON>
```
| Segment | Content | Notes |
|---------|---------|-------|
| `<hostname>` | Original virtual host (pre-stripped of FML tag) | e.g. `mc.example.com` |
| `\0` | NUL byte (0x00) | delimiter |
| `<clientIP>` | `AddressUtil.sanitizeAddress()` output | IPv4 dotted-decimal or IPv6 without scope ID |
| `\0` | NUL byte | delimiter |
| `<uuidNoDashes>` | Player UUID, 32 hex chars, **no dashes** | e.g. `069a79f444e94726a5befca90e38aaf5` |
| `\0` | NUL byte | delimiter (only present when properties follow) |
| `<propertiesJSON>` | `LoginResult.GSON.toJson(profile.getProperties())` | Texture properties array; **omitted entirely** if `profile == null` or properties array is empty |
Full example with textures:
```
mc.example.com\0203.0.113.42\0069a79f444e94726a5befca90e38aaf5\0[{"name":"textures","value":"eyJ0...","signature":"abc..."}]
```
Full example without textures (offline-mode or missing profile):
```
mc.example.com\0203.0.113.42\0069a79f444e94726a5befca90e38aaf5
```
Backend servers (e.g. Paper with `settings.bungeecord: true`) re-parse this field on login to reconstruct the real player identity.
**Note on FML + BungeeCord interaction:** When ip_forward is enabled, BungeeCord does NOT re-append the `extraDataInHandshake` (the FML tag). The code path is mutually exclusive (`ServerConnector.java:124-128`):
```java
} else if ( !user.getExtraDataInHandshake().isEmpty() ) {
// Only restore the extra data if IP forwarding is off.
// TODO: Add support for this data with IP forwarding.
copiedHandshake.setHost( copiedHandshake.getHost() + user.getExtraDataInHandshake() );
}
```
This means: **with BungeeCord ip_forward enabled, Forge mod detection via the handshake tag is broken at the backend.** <!-- VERIFY: still true in current BungeeCord HEAD -->
---
### Overload 3 — Velocity modern forwarding (does NOT use this field)
Velocity's **modern** forwarding mode does **not** overload the Server Address field. Instead it uses a Login Plugin Message exchange during the Login state on the backend connection (channel `velocity:player_info`). See [`proxy-forwarding/velocity-modern.md`](proxy-forwarding/velocity-modern.md) for details.
Velocity's **legacy/BungeeCord-compat** mode (`ip-forwarding-mode = LEGACY`) uses the same `\0`-delimited format described in Overload 2 above, for compatibility with servers that already support BungeeCord forwarding.
---
## State-machine diagram
```mermaid
flowchart LR
C["Client"] -->|"Handshake\n(0x00)"| P["Proxy / Server"]
P -->|"Intent=1"| S["Status state"]
P -->|"Intent=2"| L["Login state"]
P -->|"Intent=3 (1.20.5+)"| L
S --> Ping["Status / Ping\n(see 04-status-ping.md)"]
L --> Login["Login sequence\n(see 05-login.md)"]
```
---
## Version history summary
| Version | Change |
|---------|--------|
| 1.7.2 | Handshake packet introduced in this form; fields stable since |
| 1.71.12.2 | Forge appends `\0FML\0` for modded clients |
| 1.8+ | BungeeCord ip_forward writes `host\0ip\0uuid[\0props]` to backends |
| 1.13+ | Forge marker changes to `\0FML2\0` <!-- VERIFY --> |
| 1.20.5 (protocol 766) | Intent value `3` (Transfer) added |
---
## Key sources
| Source | What it confirms |
|--------|-----------------|
| `BungeeCord/proxy/…/connection/InitialHandler.java:355-359` | FML tag stripping logic, extraDataInHandshake |
| `BungeeCord/proxy/…/connection/InitialHandler.java:801-803` | UUID stripped of dashes |
| `BungeeCord/proxy/…/ServerConnector.java:114-123` | Authoritative ip_forward write path |
| `BungeeCord/proxy/…/forge/ForgeConstants.java:20` | `FML_HANDSHAKE_TOKEN = "\0FML\0"` |
| `node-minecraft-protocol/src/client/setProtocol.js:21-26` | Client-side field names; `tagHost` extension point |
| `node-minecraft-protocol/src/client/tcp_dns.js:21-33` | SRV resolution overwrites host/port before connect |
| `minecraft-data/data/pc/1.8/protocol.json` | `packet_set_protocol` field schema (`varint`, `string`, `u16`, `varint`) |
| minecraft.wiki/w/Java_Edition_protocol/Packets | Field names, String(255) limit, Intent=3 description |