Files
minecraft_protocol/proxy-forwarding/online-offline-modes.md
T
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

73 lines
6.2 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.
# 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](../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`:
```java
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](https://minecraft.wiki/w/Universally_unique_identifier) 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](bungeecord-legacy.md) and [velocity-modern.md](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](bungeeguard.md)).
- **Modern/Velocity** — an HMAC the backend verifies, so forged identities are cryptographically rejected ([velocity-modern.md](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)`; `:135` `onlineMode`; `:595` `isOnlineMode()` gating the auth call.
- [minecraft.wiki — Universally unique identifier](https://minecraft.wiki/w/Universally_unique_identifier) — version-3 MD5 `OfflinePlayer:<username>` derivation.
- [minecraft.wiki — Java Edition protocol (login + encryption)](https://minecraft.wiki/w/Java_Edition_protocol) — Encryption Request/Response, server-id hash, `hasJoined`.
- See also [../05-login-encryption.md](../05-login-encryption.md) for the encryption packet detail.