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>
7.2 KiB
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) 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():
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:
// 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:
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
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 (adds a secret token to the properties) and Velocity modern forwarding (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.
Sources
BungeeCord/proxy/src/main/java/net/md_5/bungee/ServerConnector.java:116-123— the WRITE ofhost\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.