Files
minecraft_protocol/proxy-forwarding/forge-fml.md
T
claude-timemachine cf6aa978d0 verify pass 3: FML token correction + BungeeGuard/cmd-providers from live source
archive.org Wayback was network-blocked; pivoted to live GitHub. Key fix: FML
host token is \0FML\0 (not \0FML2/3\0 — those are FMLNETVERSION ints on the
fml:handshake channel); modern Forge uses \0FORGE/\0FORGEn. Confirmed BungeeGuard
backend token check (lucko source), 4 cmd suggestion providers, 26.2 interval_select
worldgen-only, corrected 1.14 villager metadata layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:26:37 +02:00

8.4 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 Notes
\0FML\0 FML 1.8 1.19.x (legacy Forge) Confirmed: Velocity LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN = "\0FML\0" (1.81.12.2 stated in comment). The "FML2"/"FML3" labels refer to NetworkConstants.FMLNETVERSION (an integer negotiated over fml:handshake channel), not to a different host-field token — the host-field token stays \0FML\0 throughout this era.
\0FORGE or \0FORGEn Modern Forge / NeoForge 1.20.2+ Confirmed: Velocity ModernForgeConstants.MODERN_FORGE_TOKEN = "FORGE"; ModernForgeConnectionType.getModernToken() returns "\0FORGE" or "\0FORGE" + natVersion (e.g. "\0FORGE3"). Protocol ≥ 1.20.2 gates this path.

So a Forge client's handshake host looks like mc.example.com\0FML\0 (legacy era) or mc.example.com\0FORGE / mc.example.com\0FORGE3 (modern era) 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.20.2+) uses its own login plugin-message handshake on the fml:handshake channel; a Velocity backend running modern forwarding handles both because they occupy distinct login-plugin-message channels (velocity:player_info vs fml:handshake).
  • 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 (legacy, 1.81.19.x) or \0FORGE/\0FORGEn (modern, 1.20.2+) marker is Forge saying "I'm modded," appended to the handshake host. The "FML2"/"FML3" version numbers are negotiated over the fml:handshake login-plugin channel, not encoded in the host-field token.
  • 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.
  • PaperMC/Velocity/proxy/.../forge/legacy/LegacyForgeConstants.javaHANDSHAKE_HOSTNAME_TOKEN = "\0FML\0", comment: "Clients attempting to connect to 1.8-1.12.2 Forge servers will have this token appended to the hostname." Confirmed that the legacy-era wire token is \0FML\0 regardless of FML version number.
  • PaperMC/Velocity/proxy/.../forge/modern/ModernForgeConstants.javaMODERN_FORGE_TOKEN = "FORGE".
  • PaperMC/Velocity/proxy/.../forge/modern/ModernForgeConnectionType.javagetModernToken() returns "\0FORGE" or "\0FORGE" + natVersion; gated on protocol ≥ 1.20.2.
  • MinecraftForge/src/.../network/NetworkConstants.java (1.18.x/1.19.x) — FMLNETMARKER="FML", FMLNETVERSION=3, NETVERSION="FML3" — confirms "FML3" is the channel-negotiated version, not the host-field token.
  • See bungeecord-legacy.md (the colliding \0 transport) and velocity-modern.md (the field-free alternative).