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

100 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
> ```java
> // 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`
> ```java
> // 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](bungeecord-legacy.md) **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`:
> ```java
> // 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:
> ```java
> // 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](velocity-modern.md) 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. <!-- VERIFY: modern Forge + native modern forwarding compatibility depends on the backend mod (e.g. a Velocity-forwarding mod alongside Forge); not exhaustively read from source -->
- **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. <!-- VERIFY: ViaForge specifics not read from cloned source -->
- 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-20``FML_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,34``HANDSHAKE_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](bungeecord-legacy.md) (the colliding `\0` transport) and [velocity-modern.md](velocity-modern.md) (the field-free alternative).