Files
minecraft_protocol/proxy-forwarding/forge-fml.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

6.6 KiB
Raw Blame History

Forge / FML and Proxies

Forge (via FML — Forge Mod Loader) changes the handshake in a way that collides with legacy forwarding, because both want to use the same field: the handshake Server Address string. A proxy in front of modded backends has to understand and preserve the FML markers, or modded clients fail to connect.

The FML handshake markers

Since FML 1.8, a Forge client appends a token to the handshake host so a Forge server can detect that the client is modded. The marker is a \0-delimited suffix:

Marker Era
\0FML\0 FML 1.8+ (legacy Forge, ~1.81.12)
\0FML2\0 newer FML (1.13+ "new" Forge networking)
\0FML3\0 later Forge revisions

So a Forge client's handshake host looks like mc.example.com\0FML\0 instead of plain mc.example.com. Both BungeeCord and Velocity hard-code the legacy token:

// BungeeCord
public static final String FML_TAG            = "FML";
public static final String FML_HANDSHAKE_TAG  = "FML|HS";
public static final String FML_HANDSHAKE_TOKEN = "\0FML\0";   // "The FML 1.8 handshake token."

BungeeCord/proxy/.../forge/ForgeConstants.java:13,14,20

// Velocity (legacy Forge)
public static final String HANDSHAKE_HOSTNAME_TOKEN = "\0FML\0";
public static final String FORGE_LEGACY_HANDSHAKE_CHANNEL = "FML|HS";

Velocity/proxy/.../forge/legacy/LegacyForgeConstants.java:29,34

The deeper mod-list negotiation then happens over the FML|HS plugin-message channel (legacy) or modern Forge login plugin messages — that's a separate, larger handshake. The \0FML\0 marker is just the "I am modded" flag riding in the address field.

The collision with legacy forwarding

Legacy/BungeeCord forwarding also packs data into the handshake host (realHost\0clientIP\0uuid\0props), \0-delimited. If a Forge client adds \0FML\0 and the proxy then naively appends \0IP\0UUID\0props, the two collide — the backend can't tell which \0 segment is which.

Proxies solve this by splitting the FML tail off first, doing their own thing, then restoring it. BungeeCord stashes everything from the first \0 as extraDataInHandshake:

// Starting with FML 1.8, a "\0FML\0" token is appended to the handshake. This interferes
// with Bungee's IP forwarding, so we detect it, and remove it from the host string, for now.
if ( handshake.getHost().contains( "\0" ) )
{
    String[] split = handshake.getHost().split( "\0", 2 );
    handshake.setHost( split[0] );
    extraDataInHandshake = "\0" + split[1];
}

BungeeCord/proxy/.../connection/InitialHandler.java:355-360

When IP forwarding is off, BungeeCord re-appends that saved tail so the modded backend still sees its \0FML\0:

copiedHandshake.setHost( copiedHandshake.getHost() + user.getExtraDataInHandshake() );BungeeCord/proxy/.../ServerConnector.java:124-128

But when IP forwarding is on, the FML tail can't be reattached — the forwarding segments already own the field. BungeeCord's own code marks this as a known gap (// TODO: Add support for this data with IP forwarding., ServerConnector.java:127). Velocity is explicit about the same conflict in legacy mode and works around it by moving the Forge flag into a profile property instead of the hostname:

// We can't forward the FML token to the server when we are running in legacy forwarding mode,
// since both use the "hostname" field in the handshake. We add a special property to the
// profile instead, which will be ignored by non-Forge servers and can be intercepted by a
// Forge coremod, such as SpongeForge.
if (forwardingType == PlayerInfoForwarding.LEGACY) {
    return original.addProperty(IS_FORGE_CLIENT_PROPERTY);
}

Velocity/proxy/.../forge/legacy/LegacyForgeConnectionType.java:41-49

Modern Forge + Velocity (modern forwarding)

Modern/Velocity forwarding does not touch the handshake address field, so the \0FML\0 host collision goes away — the FML marker and the identity data no longer fight over the same field. The remaining interplay is the Forge login-phase handshake itself, which runs as login plugin messages on the same login state where Velocity sends velocity:player_info. The two coexist (different channels), but a modded backend's FML handshake and the forwarding handshake must both complete during login.

Practical notes:

  • Modern Forge (1.13+) uses its own login plugin-message handshake; a Velocity backend running modern forwarding handles both because they're distinct channels.
  • ViaForge / client-side shims: tools like ViaForge let a Forge client speak to a backend across version gaps; they have to reproduce or tolerate the FML handshake markers so the proxy and backend negotiate the modded handshake correctly.
  • For modded networks, modern forwarding is preferable precisely because it sidesteps the address-field collision that makes legacy + Forge brittle.

Summary

  • The \0FML\0 / \0FML2\0 / \0FML3\0 marker is Forge saying "I'm modded," appended to the handshake host.
  • It collides with legacy forwarding (shared field) → proxies must split it off and restore it, and can't restore it while legacy IP-forwarding is on (BungeeCord TODO; Velocity moves the flag to a property).
  • Modern forwarding avoids the field entirely, so it's the cleaner choice for Forge backends; the Forge mod-list handshake then runs as separate login plugin messages.

Sources

  • BungeeCord/proxy/.../forge/ForgeConstants.java:13-20FML_TAG, FML_HANDSHAKE_TAG = "FML|HS", FML_HANDSHAKE_TOKEN = "\0FML\0".
  • BungeeCord/proxy/.../connection/InitialHandler.java:355-360 — split the \0FML\0 tail off the host, save as extraDataInHandshake.
  • BungeeCord/proxy/.../ServerConnector.java:124-128 — restore the FML tail only when IP forwarding is off (:127 TODO notes the gap when it's on).
  • Velocity/proxy/.../forge/legacy/LegacyForgeConstants.java:29,34HANDSHAKE_HOSTNAME_TOKEN = "\0FML\0", FORGE_LEGACY_HANDSHAKE_CHANNEL = "FML|HS".
  • Velocity/proxy/.../forge/legacy/LegacyForgeConnectionType.java:41-49 — legacy forwarding can't carry the FML host token; flag moved to a profile property instead.
  • See bungeecord-legacy.md (the colliding \0 transport) and velocity-modern.md (the field-free alternative).