feat(edition): tell backends which players came from Bedrock - #35
Merged
Conversation
Nothing downstream of the proxy knows today. Geyser terminates the Bedrock session and connects as an ordinary Java client, and Velocity's modern forwarding carries a UUID, a username, skin properties and an address — no edition. Backends make do with the shape of the Floodgate UUID (mostSignificantBits == 0), which is right for unlinked players and silently wrong for linked ones: they arrive under their Mojang UUID. That is not a corner case. The Floodgate build the network ships defaults to enable-global-linking: true, and a global link is one the player may have made on any Geyser server. Stamps grounds:edition=bedrock onto the game profile at PostLoginEvent, which fires before the player reaches a backend, so it is in the payload Velocity HMACs with the forwarding secret — and in every later one, since a server switch rebuilds it from the same profile. Being inside the signature is the point rather than a detail: the flag turns anti-cheat off downstream, so a marker the client could set would be a self-exemption for any modified Java client. It is why the client brand is not used, despite Geyser announcing itself in it. Floodgate is reached by reflection over one method. Only the Bedrock proxy carries it, Floodgate publishes its API as a moving -SNAPSHOT and nothing else, and library-gui already avoids the dependency for the same reasons. Where Floodgate is absent the listener is not registered at all. The stamp appends, and Floodgate's own skin applier removes only textures, so neither can drop what the other wrote.
lusu007
approved these changes
Aug 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backends currently cannot tell a Bedrock player from a Java one except by guessing from the UUID shape, and that guess is wrong for linked accounts. This puts the answer inside the signed forwarding payload.
The problem
Geyser terminates the Bedrock session and connects to
velocity-bedrockas an ordinary Java client. Velocity's modern forwarding then carries a UUID, a username, skin properties and an address — no edition, and no free slot for one. So backends fall back to the shape of the Floodgate UUID (mostSignificantBits == 0), which is whatlibrary-guiandduel's anti-cheat exemption do today.That is right for unlinked players and silently wrong for linked ones: Floodgate builds the forwarded profile from
getCorrectUniqueId(), so a linked Bedrock player arrives under their Mojang UUID and is shaped like anyone else.Not a corner case — the Floodgate build we pin (
containers/plugin-floodgate, 2.2.5 build 140) ships:Global linking needs no local database, so the
emptyDirover/app/pluginsdoes not suppress it — and a global link is one the player may have made on any Geyser server.What this does
Stamps
grounds:edition=bedrockonto the game profile onPostLoginEvent, which fires before the player is sent to a backend. The property is therefore in the payload Velocity HMACs with the forwarding secret — and in every later one, since a server switch rebuilds it from the same profile.Being inside the signature is the whole point, not an implementation detail. Downstream this flag turns anti-cheat off, so a marker the client could choose would be a self-exemption for any modified Java client. That is exactly why the obvious alternative is unusable: Geyser does announce itself as
Geyserinminecraft:brand, but brand is pure client input and Velocity forwards it unmodified — Geyser is itself the proof that a Java-protocol client writes whatever it likes there. Cookies fail the same test, being stored unsigned on the client.Choices worth reviewing
FloodgateApi.getInstance().isFloodgatePlayer(uuid). Only one of the three proxies carries Floodgate, so a compile-time dependency would put an unresolvable class on the other two; and Floodgate publishes its API only as a moving-SNAPSHOT, which is not a version this repo can pin.library-guiavoids the dependency for the same reasons. Where Floodgate is absent the listener is not registered at all.isFloodgatePlayer, notisFloodgateId. The latter is the UUID-shape test and has the same blind spot as the status quo. The former resolves throughgetPlayer(uuid), which falls back to scanning for a player whosegetCorrectUniqueId()matches. Verified in the bytecode of the jar we actually ship (downloaded build 140, sha256 matches the pinnedf5867ad7…):isFloodgatePlayerisgetPlayer(uuid) != null, andgetPlayerdoes iterateplayers.values()comparinggetCorrectUniqueId().textureslives in the same list, and Floodgate'sVelocitySkinAppliercopies the list andremoveIf(name == "textures")— so neither side can drop what the other wrote, whichever order they run in.Backend side
Not in this PR.
duelkeeps the UUID check and will prefer this property when present, so the two halves can ship in either order and neither breaks without the other. Background:duel/docs/bedrock-anticheat.md.Note for whoever owns
feat/permissionBranched off
origin/maindeliberately, so your working tree was not touched. The only overlap is one added line invelocity/build.gradle.ktsand one call inGroundsPluginPlayer.onInitialize— trivial to merge either way round../gradlew buildgreen; 4 new tests.