a3d5f64ef5
Corrected real errors: several 1.7.x release dates, resource_pack_send version, config packet ordering, structured-component count (56), PLAYER_LOADED (1.21.4), entity_sound_effect field order. Confirmed+cited the rest; remaining ~19 items re-marked UNCONFIRMED (third-party/ViaLegacy/26.2 internals unreachable from refs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
6.8 KiB
Markdown
100 lines
6.8 KiB
Markdown
# 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.8–1.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. <!-- UNCONFIRMED: the claim that Velocity modern forwarding coexists cleanly with modern Forge's login-plugin handshake is plausible (distinct channels) but depends on the backend mod (e.g. FabricProxy-Lite or a Forge-specific forwarding mod) and was not verified from Velocity or NeoForge 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. <!-- UNCONFIRMED: ViaForge source not available in refs; description is based on the tool's stated purpose, not code inspection -->
|
||
- 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).
|