Files
minecraft_protocol/proxy-forwarding/bungeecord-legacy.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

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 dashesuser.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 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.