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

117 lines
7.2 KiB
Markdown

# BungeeCord Legacy IP Forwarding
The original forwarding scheme, introduced by BungeeCord and adopted everywhere. It works by **abusing the handshake's `serverAddress` field** — the proxy appends the player's identity to that string, null-byte delimited, and the offline-mode backend parses it back out. **No cryptography is involved.**
## The mechanism
The handshake packet (see [../03-handshake.md](../03-handshake.md)) has a `serverAddress` (a.k.a. *Server Address* / host) string — normally the hostname the client used to connect (e.g. `mc.example.com`). The vanilla server mostly ignores its content. BungeeCord repurposes it: when IP-forwarding is enabled, the proxy **rewrites** that field before opening the backend connection, packing four `\0`-separated segments into it.
The backend (a Spigot/Paper server with `settings.bungeecord: true`, or a Fabric server with an equivalent mod) recognizes the extra segments and reads the player's real IP, UUID, and properties out of them — instead of treating the whole string as a hostname.
## Exact wire format
The proxy rewrites the handshake host to:
```
realHost \0 clientIP \0 playerUUID(no dashes) \0 texturesPropertiesJSON
```
Where (in order):
| Segment | Content |
|---|---|
| `realHost` | the original handshake host the client sent (e.g. `mc.example.com`), minus any trailing FML marker |
| `clientIP` | the player's real socket IP, sanitized (brackets stripped from IPv6, scope id removed) |
| `playerUUID` | the player's UUID **with dashes removed** (32 hex chars) |
| `texturesPropertiesJSON` | a JSON array of the login profile's game-profile **properties** (the Mojang-signed `textures` skin/cape entry). **Omitted entirely** (along with its leading `\0`) if the profile has no properties — i.e. the player connected in offline/cracked mode upstream. |
This is exactly what BungeeCord writes in `ServerConnector.connected()`:
> ```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() );
> }
> copiedHandshake.setHost( newHost );
> ```
>
> — `BungeeCord/proxy/src/main/java/net/md_5/bungee/ServerConnector.java:116-123`
(BungeeCord writes the null byte as the Java octal escape `"\00"`, i.e. a single `U+0000`.) Note the **UUID has no dashes**`user.getUUID()` returns the undashed form. The properties JSON is the serialized array of `{name, value, signature}` objects; for an online player it's the single Mojang-signed `textures` property.
Velocity, when configured in `legacy` mode, builds the **same** string — and its source documents the format verbatim:
> ```java
> // BungeeCord IP forwarding is simply a special injection after the "address" in the handshake,
> // separated by \0 (the null byte). In order, you send the original host, the player's IP, their
> // UUID (undashed), and if you are in online-mode, their login properties (from Mojang).
> final StringBuilder data = new StringBuilder()
> .append(serverAddress).append(LEGACY_SEPARATOR)
> .append(playerAddress).append(LEGACY_SEPARATOR)
> .append(profile.getUndashedId()).append(LEGACY_SEPARATOR);
> GENERAL_GSON.toJson(profile.getProperties(), data);
> ```
>
> — `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java:160-172` (`LEGACY_SEPARATOR = '\0'`, defined at `:50`)
## How the backend parses it
The receiving side splits the host on `\0`. BungeeCord's *own* handshake handler (which is what a downstream BungeeCord-as-backend, or a Spigot `bungeecord:true` server emulating the same logic, does) splits the host and keeps the tail:
> ```java
> if ( handshake.getHost().contains( "\0" ) )
> {
> String[] split = handshake.getHost().split( "\0", 2 );
> handshake.setHost( split[0] );
> extraDataInHandshake = "\0" + split[1];
> }
> ```
>
> — `BungeeCord/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java:355-360`
A Spigot/Paper backend with bungeecord forwarding enabled does the analogous thing: it splits the host into `[host, ip, uuid, properties]`, sets the player's address to `ip`, the UUID to the dash-inserted form of `uuid`, and the game-profile properties to the parsed JSON.
## Sequence
```mermaid
sequenceDiagram
autonumber
actor C as "Client (player)"
participant P as "Proxy (BungeeCord, ip_forward=true)"
participant B as "Backend (Spigot bungeecord=true, offline-mode)"
C->>P: "Handshake host=mc.example.com"
C->>P: "LoginStart (username)"
Note over P: "online-mode auth vs Mojang (hasJoined)"
Note over P: "rewrite host -> realHost\\0clientIP\\0uuidNoDashes\\0propsJSON"
P->>B: "Handshake host='realHost\\0IP\\0UUID\\0props'"
P->>B: "LoginStart (username, rewriteId)"
Note over B: "split host on \\0; trust IP/UUID/props as-is"
B-->>C: "LoginSuccess (via proxy relay) -> play"
```
## Security: none
There is **no signature, no secret, no verification**. The backend trusts the `\0`-delimited string completely. If an attacker can reach the backend's port, they simply send a handshake with a hand-crafted `host\0ip\0uuid\0props` string and connect as **any player they like**, with any UUID and any skin.
The *only* defense for bare legacy forwarding is the network: **firewall the backend so only the proxy can connect** (bind to localhost / a private interface; drop everything else). This is the central weakness that [BungeeGuard](bungeeguard.md) (adds a secret token to the properties) and [Velocity modern forwarding](velocity-modern.md) (HMAC-signs the whole payload) exist to fix.
Velocity even warns the operator when a legacy backend closes the connection — almost always a misconfigured `bungeecord: true`:
> *"This is usually because the remote server does not have BungeeCord IP forwarding correctly enabled."* — `Velocity/.../backend/LoginSessionHandler.java:205-212`
## Forge note
If the client is on Forge, FML appends its own `\0FML\0` (or newer `\0FML2\0` / `\0FML3\0`) marker to the handshake host. The proxy must split that off **before** injecting the forwarding segments and re-append it after, or the marker collides with the forwarding `\0` delimiters. BungeeCord handles this by stashing everything from the first `\0` as `extraDataInHandshake` (`InitialHandler.java:355-360`) and restoring it only when IP forwarding is *off* (`ServerConnector.java:124-128`). See [forge-fml.md](forge-fml.md).
---
**Sources**
- `BungeeCord/proxy/src/main/java/net/md_5/bungee/ServerConnector.java:116-123` — the WRITE of `host\0ip\0uuid\0props`.
- `BungeeCord/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java:355-360` — the split/parse of the host on `\0`.
- `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java:50` (`LEGACY_SEPARATOR`), `:154-173` (`createLegacyForwardingAddress`) — Velocity building the identical string, with the format documented in-comment.
- `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java:205-212` — legacy-misconfiguration diagnostic.