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>
6.2 KiB
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:
- Client →
LoginStart(username, and on 1.19+ a profile public key / on 1.20.2+ the profile UUID). - Server →
Encryption Request(server-id string, server's RSA public key, verify token). - Client →
Encryption Response(the AES shared secret + verify token, both RSA-encrypted with the server's public key). - Both sides switch to AES/CFB8 encryption using the shared secret.
Steps 2–4 — 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 matchingjoinendpoint). The response carries the player's real account UUID and properties (thetexturesproperty — skin & cape, signed by Mojang). IfhasJoinedreturns nothing, the login is rejected. - Offline mode: the server skips the
hasJoinedcall entirely. No Mojang verification. The server trusts the username fromLoginStartas-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
hasJoinedresponse. 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.nameUUIDFromBytesproduces 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:560and 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:
- 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.
- 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
BungeeCord/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java:560— offline UUID =nameUUIDFromBytes("OfflinePlayer:"+name);:135onlineMode;:595isOnlineMode()gating the auth call.- minecraft.wiki — Universally unique identifier — version-3 MD5
OfflinePlayer:<username>derivation. - minecraft.wiki — Java Edition protocol (login + encryption) — Encryption Request/Response, server-id hash,
hasJoined. - See also ../05-login-encryption.md for the encryption packet detail.