d73c1c9537
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>
117 lines
7.2 KiB
Markdown
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.
|