Files
minecraft_protocol/02-connection-lifecycle.md
2026-06-19 19:00:09 +02:00

16 KiB

Connection Lifecycle — Java Edition Protocol State Machine

Scope: 1.7.10 through current (26.1 / protocol 775). Version-specific divergences are called out inline. Connective-tissue doc: per-phase detail lives in 03-handshake.md, 04-status.md, 05-login.md, 06-configuration.md, 07-play.md. This doc covers the state machine, transition triggers, and the ordered exchange that takes a raw TCP socket to in-game.


1. States

The protocol defines five named states. Each state carries its own packet-ID namespace (IDs are reused across states):

State Integer ID Direction of entry Purpose
Handshaking (implicit, initial) TCP connect One packet; chooses the next state
Status 1 via Handshake intent=1 Server-list ping/pong; no auth
Login 2 via Handshake intent=2 (or 3) Auth, encryption, compression, identity
Configuration — (no integer) after Login Acknowledged (≥1.20.2) Registry sync, feature flags, resource packs
Play after Finish Configuration (or Login Success <1.20.2) Gameplay

Sources:

  • State names: node-minecraft-protocol/src/states.js:3-9
  • Velocity enum: Velocity/proxy/…/StateRegistry.java:140-263 (HANDSHAKE, STATUS, CONFIG, PLAY, LOGIN)
  • Integer IDs (STATUS_ID=1, LOGIN_ID=2, TRANSFER_ID=3): StateRegistry.java:881-883
  • Intent=3 (Transfer login, ≥1.20.5): minecraft.wiki/w/Java_Edition_protocol/Packets

2. State Machine Diagram

TCP connect
    │
    ▼
┌──────────────┐
│  HANDSHAKING │  (one packet: set_protocol / Handshake 0x00)
└──────┬───────┘
       │ intent field
       ├─ 1 ──────────────────────────┐
       │                              │
       ▼                              ▼
  ┌─────────┐                   ┌─────────┐
  │  STATUS │                   │  LOGIN  │
  └────┬────┘                   └────┬────┘
       │ ping done / disconnect       │ Login Success + LoginAcknowledged
       ▼                              │ (≥1.20.2 only)
   [close]                       ┌───┴──────────────┐
                                 │  CONFIGURATION   │  (≥1.20.2 / protocol 764)
                                 └───┬──────────────┘
                                     │ Finish Configuration
                                     │                  ┌─────────────────────┐
                                     ▼    ◄─────────────┤ Start Configuration │
                                 ┌────────┐             │ (Play→Config re-cfg) │
                                 │  PLAY  ├────────────►┘                     │
                                 └────────┘             └─────────────────────┘

Pre-1.20.2 path: Login Success → PLAY directly (no Configuration state, no Login Acknowledged).


3. Handshake: How next-state is chosen

The very first packet sent on a new TCP connection is the serverbound Handshake (packet ID 0x00 in the HANDSHAKING state, stable since 1.7.2).

Fields:

  • protocolVersion — VarInt, the client's protocol number
  • serverHost — String (max 255), the address the client typed
  • serverPort — Unsigned Short
  • nextState — VarInt enum:
    • 1 → STATUS
    • 2 → LOGIN
    • 3 → LOGIN after server transfer (≥1.20.5 / protocol 766). Source: Velocity HandshakeIntent.java:16 (TRANSFER(3)), StateRegistry.java:883 (TRANSFER_ID = 3), ProtocolVersion.java:89 (MINECRAFT_1_20_5(766, "1.20.5", "1.20.6")); ViaVersion InitialBaseProtocol.java:133 rejects intent=3 for servers older than v1_20_5.

The server reads nextState and immediately switches its decoder to the corresponding state. There is no server response in the Handshaking state.

Source: 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
}

The client side always sends nextState: 2 for normal login: node-minecraft-protocol/src/client/setProtocol.js:21-27

client.write('set_protocol', {
  protocolVersion: options.protocolVersion,
  serverHost: taggedHost,
  serverPort: options.port,
  nextState: 2
})
client.state = states.LOGIN

4. Status Path (Server List Ping)

Used by the client (and external tools) to read MOTD, player count, and favicon without authenticating.

sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: "Handshake (0x00, nextState=1)"
    Note over C,S: Both switch to STATUS state
    C->>S: "Status Request (0x00)"
    S->>C: "Status Response (0x00) — JSON payload"
    C->>S: "Ping Request (0x01) — Long timestamp"
    S->>C: "Pong Response (0x01) — echo timestamp"
    Note over C,S: Server closes connection

Notes:

  • The Ping Request / Pong Response round-trip is optional but used by vanilla to measure latency.
  • No encryption, no compression, no auth in this path.
  • Server handler: node-minecraft-protocol/src/server/ping.js:7-64 — writes server_info then echoes ping then ends.
  • Velocity STATUS registration: StateRegistry.java:148-161StatusRequestPacket 0x00, StatusPingPacket 0x01 (both directions).

5. Login Path

5a. Pre-1.20.2 (protocol < 764): Login → Play directly

sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: "Handshake (0x00, nextState=2)"
    Note over C,S: Both switch to LOGIN state
    C->>S: "Login Start (0x00) — username + UUID"
    Note over S: online-mode check
    S->>C: "Encryption Request (0x01) — serverId, publicKey, verifyToken [online-mode only]"
    Note over C: Mojang auth (joinServer call)
    C->>S: "Encryption Response (0x01) — encrypted sharedSecret + verifyToken"
    Note over C,S: Both enable AES-128-CFB8 encryption immediately
    Note over S: Yggdrasil hasJoined verification
    S->>C: "Set Compression (0x03) — threshold [optional, >=1.8]"
    Note over C,S: All subsequent packets use compressed format if enabled
    S->>C: "Login Success (0x02) — UUID + username + properties"
    Note over C,S: Both switch to PLAY state
    S->>C: "Join Game (Play 0x01/varies) — game state, dimension, etc."

Key sequencing rules (pre-1.20.2):

  • Set Compression must precede Login Success. Anything sent after Set Compression (including Login Success itself) uses the compressed packet format. Source: minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol
  • Encryption activates immediately after the server receives Encryption Response — both sides flip the AES cipher before the next byte. Source: node-minecraft-protocol/src/client/encrypt.js:73 (client.setEncryption(sharedSecret) called before returning from handler); node-minecraft-protocol/src/server/login.js:150 (server side, same).
  • Offline-mode servers skip Encryption Request/Response entirely (node-minecraft-protocol/src/server/login.js:88-106).
  • Set Compression was added in 1.8 (protocol 47). Prior versions (1.7.x) have no compression packet; per-packet compression existed in very early snapshots but was dropped. Source: StateRegistry.java:869-870 (SetCompressionPacket mapped from MINECRAFT_1_8).

5b. 1.20.2+ (protocol ≥ 764): Login → Configuration → Play

The big change in 1.20.2 inserted the Configuration state between Login and Play. The gate is LoginAcknowledged — the client must explicitly acknowledge Login Success before Configuration begins.

sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: "Handshake (0x00, nextState=2)"
    Note over C,S: Both switch to LOGIN state
    C->>S: "Login Start (0x00) — username + UUID"
    S->>C: "Encryption Request (0x01) [online-mode only]"
    Note over C: Mojang auth
    C->>S: "Encryption Response (0x01)"
    Note over C,S: AES-128-CFB8 encryption enabled
    S->>C: "Set Compression (0x03) [optional]"
    Note over C,S: Compressed format enabled if threshold >= 0
    S->>C: "Login Success (0x02) — UUID + username + properties"
    C->>S: "Login Acknowledged (0x03) [NEW in 1.20.2]"
    Note over C,S: Both switch to CONFIGURATION state
    S->>C: "Registry Data (0x05/0x07) — codec entries"
    S->>C: "Feature Flags (0x0C) — enabled experiments"
    S->>C: "Known Packs (0x0E) [>=1.20.5]"
    C->>S: "Known Packs (0x07) — client's pack list [>=1.20.5]"
    S->>C: "Tags (0x0D/varies) — tag registry"
    S->>C: "Finish Configuration (0x02/0x03)"
    C->>S: "Acknowledge Finish Configuration (0x02/0x03)"
    Note over C,S: Both switch to PLAY state
    S->>C: "Login / Join Game (Play 0x29/varies)"

Sources:

  • LoginAcknowledgedPacket registered serverbound at 0x03 from MINECRAFT_1_20_2: StateRegistry.java:853-854
  • Server side transition: node-minecraft-protocol/src/server/login.js:189-239 — on login_acknowledged, server switches to states.CONFIGURATION, sends registry_data, then finish_configuration; client acks with finish_configuration, server switches to states.PLAY
  • Client side: node-minecraft-protocol/src/client/play.js:40-43 — on success, writes login_acknowledged and calls enterConfigState()
  • KnownPacksPacket registered in CONFIG from MINECRAFT_1_20_5: StateRegistry.java:193-194, 249-250
  • Velocity CONFIG state registration starts at MINECRAFT_1_20_2 throughout: StateRegistry.java:163-261

6. Configuration State

Introduced: 1.20.2, protocol 764. Source: StateRegistry.java:163 (first CONFIG packet mapped from MINECRAFT_1_20_2); minecraft.wiki/w/Java_Edition_protocol/Packets.

Purpose: synchronise server-side data (registry codec, feature flags, resource packs, tags, known data packs) to the client before any gameplay packets flow. Replacing what was previously stuffed into the Login sequence or early Play packets.

Key packets (clientbound unless noted):

  • Plugin Message — channel-based custom data, same as Play
  • Disconnect — kick during config
  • Keep Alive — connection liveness check
  • Registry Data — serialised registry codec entries (dimension types, biomes, damage types, etc.)
  • Feature Flags — experimental feature toggles
  • Resource Pack Request — push a resource pack
  • Tags Update — tag data (formerly in Play)
  • Known Packs (≥1.20.5) — bidirectional; server asks, client responds with list of known data packs to skip redundant sync
  • Finish Configuration (clientbound) → Acknowledge Finish Configuration (serverbound) — the handshake that exits Configuration

Full CONFIG packet table: StateRegistry.java:163-261


7. Re-configuration: Play → Configuration

Since 1.20.2. A connected player can be sent back to Configuration state without disconnecting. This allows servers to push updated registries (e.g., when switching between sub-servers with different dimensions).

Trigger: server sends Start Configuration (StartUpdatePacket) in the Play state.

  • Velocity: StateRegistry.java:805-813, registered from MINECRAFT_1_20_2

Client responds: Acknowledge Configuration (serverbound Play packet, Velocity name FinishedUpdatePacket)

  • Velocity: StateRegistry.java:407-413, registered from MINECRAFT_1_20_2

Both sides switch back to Configuration state. The full Configuration exchange runs again (registry data, finish handshake). On Acknowledge Finish Configuration, both switch back to Play.

Client-side handling in node-minecraft-protocol:

// node-minecraft-protocol/src/client/play.js:43
client.on('start_configuration', () => enterConfigState())
// ...
// play.js:51-53: if state===PLAY, write 'configuration_acknowledged' before switching
if (client.state === states.PLAY) {
  client.write('configuration_acknowledged', {})
}
client.state = states.CONFIGURATION

Source: node-minecraft-protocol/src/client/play.js:43-55


8. Encryption Details

  • Algorithm: AES-128-CFB8, symmetric, both directions use the same 16-byte shared secret.
  • Key exchange: RSA (server's ephemeral keypair, ≥1024-bit); client encrypts shared secret and verify token with server's public key.
  • Activation point: immediately after Encryption Response is sent/received — before the next byte on the wire. Encryption applies to all subsequent data including Set Compression and Login Success.
  • Online-mode only (vanilla): offline-mode servers skip the Encryption Request/Response entirely. This is version-independent behaviour — node-minecraft-protocol gates the entire Encryption Request/Response exchange on needToVerify (server/login.js:52,88), with no protocol-version branch. The minecraft.wiki states "As of 1.21, the vanilla server never uses encryption in offline mode" — this is a documentation style phrasing (confirming current vanilla behaviour), not a description of a 1.21 protocol change. The behaviour has been stable since at least 1.8 and is unrelated to any specific protocol version.
  • Source: node-minecraft-protocol/src/client/encrypt.js (client), node-minecraft-protocol/src/server/login.js:88-155 (server)

9. Compression Details

  • Algorithm: zlib deflate, wrapped in the compressed packet format (prepended with uncompressed data length as VarInt).
  • Threshold: packets with uncompressed size ≥ threshold are compressed; smaller packets are sent uncompressed (length VarInt = 0).
  • Activation point: Set Compression (Login 0x03) must arrive before Login Success. All subsequent packets — including Login Success — use the compressed format. Source: minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol
  • Version gating: added in 1.8 (protocol 47). Not present in 1.7.x. Source: StateRegistry.java:869-870
  • Default threshold used by node-minecraft-protocol: 256 bytes. Source: node-minecraft-protocol/src/server/login.js:179
  • Client receives compress / set_compression event: node-minecraft-protocol/src/client/compress.js:3-8

10. Protocol Version Reference

Minecraft version Protocol number Notable state-machine change
1.7.2 4 Baseline modern protocol; Handshake/Status/Login/Play
1.8 47 Set Compression (0x03 in Login) added
1.13 393 Login Plugin Message (0x04) added
1.20.2 764 Configuration state inserted; Login Acknowledged; re-configuration
1.20.5 766 Known Packs exchange in Configuration; Cookie packets; Transfer login intent=3
1.21 767 (Config/Play packet renumbering)
1.21.2 769 (further renumbering)
26.1 775 Current as of 2026-06

Sources: Velocity StateRegistry.java import block (lines 21-50); minecraft.wiki/w/Java_Edition_protocol/Packets (protocol 773/775 noted).


Resolution notes (formerly VERIFY flags)

intent=3 (Transfer) minimum version — CONFIRMED ≥1.20.5 (protocol 766). Sources: Velocity HandshakeIntent.java:16, StateRegistry.java:883, ProtocolVersion.java:89; ViaVersion InitialBaseProtocol.java:55,133. Distinction: the Handshake intent=3 is a serverbound signal from the transferring client; the Transfer clientbound packet (Play state, 0x73 in 1.20.5/1.21) is what the originating server sends to instruct the client to reconnect. Both ship in 1.20.5. Velocity registers the clientbound TransferPacket from MINECRAFT_1_20_5 in both CONFIG (0x0B) and PLAY (0x73) state (StateRegistry.java:239-240, 819-821).

offline-mode encryption — CONFIRMED version-stable. node-minecraft-protocol server/login.js:52,88 gates the entire Encryption Request/Response on needToVerify with no protocol-version branch; minecraft.wiki's "as of 1.21" phrasing is documentation style for current vanilla behaviour, not a record of a 1.21 change. Stable since 1.8.

CONFIG packet IDs shift between 1.20.2 / 1.20.3 / 1.20.5 — CONFIRMED true. Velocity StateRegistry.java:163-261 documents the shifts (e.g. Disconnect stays 0x01 in 1.20.2, shifts to 0x02 in 1.20.5; FinishConfiguration shifts from 0x02→0x03). These are Velocity's values; vanilla wiki IDs may differ per-snapshot but the shift pattern is confirmed.