Files
minecraft_protocol/03-handshake.md
T
claude-timemachine a3d5f64ef5 verify pass: resolve VERIFY flags (corrections + citations + honest UNCONFIRMED)
Corrected real errors: several 1.7.x release dates, resource_pack_send version,
config packet ordering, structured-component count (56), PLAYER_LOADED (1.21.4),
entity_sound_effect field order. Confirmed+cited the rest; remaining ~19 items
re-marked UNCONFIRMED (third-party/ViaLegacy/26.2 internals unreachable from refs).

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

229 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.
# 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 / Legacy (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+ <!-- UNCONFIRMED: "\0FML2\0" token name comes from Forge internals; neither BungeeCord nor Velocity source in /tmp/mcproto-refs uses or defines this string. Velocity notes "1.13+ uses a slightly different token" (HandshakeSessionHandler.java:176) but does not handle it. --> |
| Modern (1.20.2+) | `\0FORGE` or `\0FORGEn` | NeoForge / Forge 1.20.2+ <!-- UNCONFIRMED: version number suffix `n` observed in Velocity ModernForgeConnectionType.getModernToken(); exact token for NeoForge-only (FML3) not confirmed from available sources. Velocity ModernForgeConstants.java defines MODERN_FORGE_TOKEN = "FORGE" and gates it on protocol ≥ 1.20.2 (HandshakeSessionHandler.java:166167). --> |
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.** Confirmed in BungeeCord HEAD (commit f56d37f, 2026-06-18): `ServerConnector.java:124128` still has the `TODO: Add support for this data with IP forwarding` comment and the `else if` branch is unchanged.
---
### 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 a different token (exact string unconfirmed from proxy sources; see table above) |
| 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 |