Files
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

6.2 KiB
Raw Permalink Blame History

Online vs Offline Mode

The online-mode server property is the root of every forwarding decision. Understand it first; the four forwarding modes are all answers to the problem offline mode creates.

What online-mode controls

online-mode=true (the default in server.properties) means: after the login handshake, the server verifies the connecting player against Mojang's session servers. It does not by itself control whether the connection is encrypted.

The login sequence (see ../05-login-encryption.md for the full packet flow) is, in both modes:

  1. Client → LoginStart (username, and on 1.19+ a profile public key / on 1.20.2+ the profile UUID).
  2. Server → Encryption Request (server-id string, server's RSA public key, verify token).
  3. Client → Encryption Response (the AES shared secret + verify token, both RSA-encrypted with the server's public key).
  4. Both sides switch to AES/CFB8 encryption using the shared secret.

Steps 24 — the encryption negotiation — happen regardless of online/offline mode whenever the vanilla server requests them. What online mode adds is one extra check between steps 3 and 4 conceptually:

  • Online mode: the server computes the server-id hash (SHA-1 over the server-id string + shared secret + server public key) and calls Mojang's session endpoint hasJoined: https://sessionserver.mojang.com/session/minecraft/hasJoined?username=<name>&serverId=<hash>. Mojang confirms that this account really just authenticated with that server-id (the client side calls the matching join endpoint). The response carries the player's real account UUID and properties (the textures property — skin & cape, signed by Mojang). If hasJoined returns nothing, the login is rejected.
  • Offline mode: the server skips the hasJoined call entirely. No Mojang verification. The server trusts the username from LoginStart as-is. (Vanilla offline servers also skip the encryption step; the point is that no identity proof is required.)

So the difference that matters for forwarding is: offline mode does not call hasJoined, so it has no proof the username is real, and it gets no real UUID or skin from Mojang. It must invent both.

UUID derivation

How the player's UUID is determined differs by mode:

  • Online mode: the UUID is the player's real Mojang account UUID, returned by the hasJoined response. It's a stable, account-bound, "version 4"-style identifier assigned by Mojang.

  • Offline mode: the server derives a UUID deterministically from the username. The algorithm is a name-based (version 3 / MD5) UUID over the bytes of the ASCII string "OfflinePlayer:" + username:

    UUID offlineId = UUID.nameUUIDFromBytes(
        ("OfflinePlayer:" + name).getBytes(StandardCharsets.UTF_8));
    

    java.util.UUID.nameUUIDFromBytes produces an RFC-4122 version 3 UUID — it MD5-hashes the input bytes and stamps the version/variant bits. The same username always yields the same offline UUID, on every server, forever. (Note: no namespace UUID is prepended — it's a plain MD5 of just those bytes, which is why this is "version-3-like" rather than a strictly RFC-compliant namespaced v3.)

    This is confirmed directly in BungeeCord's source — InitialHandler.finish():

    offlineId = UUID.nameUUIDFromBytes( ( "OfflinePlayer:" + getName() ).getBytes( StandardCharsets.UTF_8 ) );

    BungeeCord/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java:560

    and the minecraft.wiki describes it the same way: "the unique identifier chosen for offline mode players is UUID version 3, generated from the MD5 hash of OfflinePlayer:<username>."

The practical consequence: a premium player has different UUIDs online vs offline. This is exactly why forwarding must carry the real (online) UUID to the backend — otherwise the offline backend would assign the player a different identity than the one their account/inventory/permissions are keyed to.

Security model

An offline-mode backend with a reachable port is wide open.

Because an offline server does no hasJoined check, it accepts whatever username the connecting party sends in LoginStart and derives the UUID from it. There is no proof. Anyone who can reach the port can connect as Notch, as an admin, as any player — and the offline server will hand them that player's UUID, inventory, and op level.

In a proxy setup the player's identity is established at the proxy (which runs online mode and does the real hasJoined). The backends are deliberately offline so the proxy can move players. That is the entire reason forwarding + hardening exist:

  1. Forwarding gives the backend the player's real IP / UUID / properties instead of the bogus offline-derived ones — see bungeecord-legacy.md and velocity-modern.md.
  2. Hardening stops an attacker from connecting directly to the offline backend and forging that same identity:
    • Firewall the backend so only the proxy's address can connect (the only defense for bare legacy / none).
    • BungeeGuard — a shared secret token the backend checks (bungeeguard.md).
    • Modern/Velocity — an HMAC the backend verifies, so forged identities are cryptographically rejected (velocity-modern.md).

Bind your backends to a private interface and trust nothing that arrives without one of these proofs.


Sources