Conversation
- Custom StartGamePacket serializer appending 3 education-specific strings - Codec processor to swap serializer for education clients - Handle CONTINUE_BREAK and STOP_BREAK block actions sent by education clients - Deserialize education fields from client JWT (IsEduMode, ADRole, EduTokenChain)
- Detect education clients and skip Xbox chain validation - Extract and verify MESS-signed server token from EduTokenChain - Echo client's token back in handshake JWT (stateless, no config needed) - Set xuid to MESS-verified oid for identity and duplicate detection - Education session fields (client flag, tenant ID, server token, skin hash) - Education codec swap and codebuilder gamerule disable in StartGamePacket
- Education fields in BedrockData for Floodgate (education flag, tenantId, adRole) - Education UUID generation (SHA-256 of tenantId:username, MSB 0x0000000100000001) - Education skin relay integration for Java player visibility - Hash-based skin matching fallback for education players - MSB check fix in JavaLoginFinishedTranslator for Bedrock skin cache
* Update README.md * Fuck, messed it up by accident * I still messed it up gng ;-; * Fix branding (EduGeyser, one word) and supported versions --------- Co-authored-by: SendableMetatype <263203301+SendableMetatype@users.noreply.github.com>
Previously, a client could set isEduMode=true in clientData and bypass both Xbox Live validation and education token verification by simply omitting the EduTokenChain. The code only rejected clients with bad signatures, not missing or malformed tokens, letting attackers connect with fully self-signed login chains and any claimed xuid/username. Now any client claiming education edition must present a valid MESS-signed token. Rejections are silent (no logging) to prevent log amplification from spammed invalid tokens. Also remove the empty-string tenant ID fallback in GeyserSessionAdapter that caused UUID collisions across token-less edu clients sharing a username.
The previous UUID scheme hashed tenantId:username, which is unsafe because usernames come from the self-signed login chain and can be spoofed by a modified client. OIDs are cryptographically signed in the MESS token and immutable per user per tenant. The LSB is now 64 purely random bits extracted from the OID by stripping the 6 fixed UUID v4 bits (version nibble at 48-51, variant at 64-65). The education MSB (0x0000000100000001) is unchanged. BREAKING: existing education player data (inventories, permissions, bans, builds) will not carry over. The legacy-uuid branch preserves the old behavior for servers that need a migration window.
- Enforce MESS token expiry (ISO 8601 UTC, matches client semantics: failed parse = expired)
- Distinct user-facing messages for EXPIRED vs INVALID tokens so expired sessions get actionable "restart MCEE" guidance
- Cache parsed MESS RSA key once at class load instead of rebuilding per verification
- Validate OID/tenantId are well-formed UUIDs at the verification boundary
- Stricter token parsing: split("\|", -1) + exact 4-field count, odd-length hex rejected
- Drop pointless base64 padding loop; Base64.getUrlDecoder() accepts unpadded input
- Remove unused CONTINUE_BREAK / STOP_BREAK block-break aliases (confirmed unnecessary in testing)
- Document adRole as client-controlled and unsafe for authorization decisions
- Document why the outer EduTokenChain JWT signature is intentionally not verified
- Fix inconsistent field naming in EducationCodecProcessor javadoc
- Revert stray whitespace-only changes in GeyserSession
- Rewrite comments that used em dashes or arrows as sentence separators; the replacement prose uses proper sentence structure instead. - Reword the OID javadoc to describe it as an Entra account identifier (globally unique, analogous to an Xbox xuid) rather than "per tenant", which was confusing.
Move education-specific protocol handling down into CloudburstProtocol via libs.versions.toml pointing at a fork on JitPack that provides: MESS token verification, EduTokenChain JWT extraction, the handshake JWT overload with signedToken claim, StartGamePacket education fields, and an education-variant codec on Bedrock_v898. - LoginEncryptionUtils: drop in-tree MESS key, hexToBytes, token verification, and manual jose4j handshake JWT construction; call EncryptionUtils.extractServerTokenFromEduTokenChain, validateEducationToken, and the three-arg createHandshakeJwt - GameProtocol: register Bedrock_v898.EDUCATION_CODEC and expose getEducationCodec(protocolVersion) for session-level lookup - GeyserSession: use the pre-registered education codec from GameProtocol instead of per-session runtime codec wrapping; null-guard for protocol versions without an education variant - Delete EducationCodecProcessor and EducationStartGameSerializer (superseded by library equivalents)
Explains that a codec swap is structurally unavoidable here: the initial codec is chosen at RequestNetworkSettingsPacket time when only the protocol version is known, and education clients report the same protocol version as standard Bedrock. The education flag first surfaces in LoginPacket's clientData, so the swap happens here rather than at connection time.
# Conflicts: # gradle/libs.versions.toml
…r their protocol version Instead of silently continuing with the standard codec (which omits the edu-specific StartGamePacket fields and causes a confusing later disconnect), reject the client immediately with a clear message that the server needs updating.
# Conflicts: # core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java # gradle/libs.versions.toml
The upstream GUEST auth commit added a signed check before education detection. Education clients are always unsigned (self-signed chains), so they'd be rejected before we can identify them as education. Our education-aware check further down already handles both cases correctly.
Avoids blocking Geyser's global ScheduledExecutorService with blocking HTTP calls to the signing relay.
Written by the skin executor thread, read by the WebSocket SKIN_UPLOADED handler. Without volatile the WebSocket thread can see stale null and miss the hash match, causing the player to spawn skinless.
A connecting client can send the marker string GEYSER_DISABLE_BEDROCK_ENCRYPTION_V1 as its first RakNet message. The one-shot handler sets a channel attribute and removes itself. LoginEncryptionUtils checks the attribute and skips enableEncryption() while still sending the ServerToClientHandshake JWT for education token echo. This lets the Nethernet relay extension forward unencrypted bytes from the WebRTC side (where DTLS provides transport encryption) without building a full protocol proxy.
Log remote address at debug level when the encryption disable marker is consumed. Add 1.21.133 to the v898 codec version list.
Add 1.21.133 to supported Education versions. Extension is no longer optional since direct IP connection methods were removed in 1.21.133.
Accept Nethernet connections directly in Geyser's session handling instead of relying on an external relay. A framing adapter converts between Nethernet ByteBufs and RakNet's RakMessage format so the existing Bedrock protocol pipeline works unchanged. - NethernetManager API for extensions to start/stop the server - Connection ID persisted in nethernet/connection-id.yml - PlayFab MCToken auth and signaling managed internally - Automatic signaling health checks and reconnection - Bedrock encryption skipped for Nethernet (DTLS provides it) - Guard GeyserSession.ping() for non-RakNet channels
The 3.0 branch was rebased onto upstream, which also folds in the new ClientboundDataStorePacket dynamic value support. The previous pin pointed at a pre rebase commit that no longer exists on any branch.
Adds the batch flush delay system property: the 1 ms coalescing window stays the default, and deployments that prefer compression efficiency over latency can widen it up to 50 ms, or disable it with zero.
Emits all fourteen ServerData members on GET /v1/join, the NetherNet equivalent of the RakNet unconnected pong that vanilla 1.26.50 introduced. The document is built from the same pong the RakNet listener answers pings with (MOTD passthrough, the ping event, and the fallbacks included), so the two transports always describe the server identically; onQuery gained a channel free overload for that. Both auth bits are true since we accept Microsoft authenticated and self signed identities alike, the nonce is generated once per process like vanilla, and connection reports LANWebRTCSignaling as vanilla does on this endpoint.
The 26.40 client falls back to plain HTTP with its TOFU flow only when the server refuses TLS outright; an invalid certificate sends it straight to RakNet (proven live with a deliberately broken manual cert). Serving a dead certificate is therefore the one behavior that loses NetherNet, and no certificate at all is strictly better. One gate applies to every certificate regardless of origin: temporal validity plus a chain to the platform trust anchors, checked the way a stock client checks them (SAN matching and privately trusted setups deliberately out of scope). The ACME manager validates at adoption and withdraws past notAfter, self reversing on the next renewal. Manual certificates moved to a per connection source that re-reads cert.pem and key.pem whenever they change, so operators can swap certificates live without a rebind or restart; removal withdraws TLS, and the automatic manager takes over on the next restart.
Findings from four adversarial review passes over the TLS and status work, each verified against the code before fixing: The status document is a snapshot served from cache and refreshed at most once a second, primed at start, on its own thread rather than the maintenance scheduler, so a hung ping listener cannot silence the signaling watchdog. Probes previously built it inline on connection I/O threads shared with player traffic, where the Velocity ping passthrough blocks on the plugin event chain, so anyone looping the unauthenticated probe could stall NetherNet players; a configured hostname also cost a blocking DNS lookup per probe, now resolved once with a loopback fallback. The snapshot age advances on failed builds too, so a throwing passthrough is retried once a second rather than as fast as probes arrive, and each refresh clears the gate it claimed, so a refresh left running by a previous start cannot release a later one. Certificate and key are checked as a pair before either source serves them, because netty builds a context from a mismatched pair and fails only at handshake time, which clients read as an invalid certificate and answer by abandoning NetherNet. The manual path caught the two step copy going live as new certificate plus old key; the automatic path can drift the same way when an interrupted issuance leaves a fresh key beside the old certificate, and now routes that to a reissue. Manual file change detection is throttled to once a second and compares sizes alongside modification times. Persisted material that cannot be used converges on one reissue with one accurate reason, covering every shape a torn write takes: only a zero byte file parses to an empty chain, most tear points throw, and a tear inside the intermediate parses clean as a short chain that then fails path building. Adoption is retried on the backoff, which heals temporal rejections like clock skew for free, but only a bounded number of times, after which the material is replaced rather than retried until the certificate expires. Key files are written atomically like the certificate and regenerated rather than wedging issuance forever when they are unreadable, since a torn key file can otherwise leave the served certificate working while renewal can never succeed again. The manager publishes the certificate, its expiry and its warned flag as one immutable reference, keeps serving a previously valid certificate when a replacement fails validation, compares the disk leaf against the published one so a rejected replacement is genuinely retried, and warns once per distinct rejection cause rather than once per streak, so a cause that changes is not swallowed. Trust check exceptions with null messages no longer read as acceptance anywhere. Also reattaches two javadoc blocks orphaned by earlier method moves.
Brings in the join probe server status document and the per connection TLS or plaintext selection on the signaling port.
The on demand flush and its configurable delay landed upstream as PR 341 with protected scheduling follow ups, so the fork now carries only the four education commits on top of upstream, which also brings the SyncedAttribute serialization fix and the PersonaPieceType update.
# Conflicts: # core/src/main/java/org/geysermc/geyser/session/GeyserSessionAdapter.java
… and entity fixes # Conflicts: # README.md # core/src/main/java/org/geysermc/geyser/network/GameProtocol.java # core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java # gradle/libs.versions.toml
…L fix # Conflicts: # README.md # core/src/main/java/org/geysermc/geyser/network/GameProtocol.java # gradle/libs.versions.toml
Geyser ships dev.kastle.webrtc and the NetherNet transport unrelocated, as diverged forks. Parent-first delegation replaced the copy an extension bundles with the fork. The fork moved fragment handling from the channel into a pipeline codec, so the stock library and the fork are not wire compatible. The extension classloader now loads dev.kastle classes and the webrtc-java native resources from the extension jar first. An extension that does not bundle them still falls through to the Geyser copy.
…ixes, protocol and MCPL bumps The skin plugin message now goes out through both mechanisms: the upstream spawn gated send and the fork's fixed 5 second fallback, in the retail path and in the education fallback path. A duplicate apply overwrites the same skin, so the double send is safe. The protocol pin moves to the rebased fork head 623c4e4e (upstream NBT bump plus the four education commits).
The signaling service keeps a socket open after our registration on it has died, so the socket level checks kept passing while the first join after idle failed. The watchdog now also requires the fork's route probe to succeed, with the nethernet pin bumped for it.
…ession tick Living entities were interpolated on the session tick: the first step waited up to a full tick for the next boundary and the three steps always assumed a 50 ms cadence. Steps now go out the moment a Java movement update arrives and are spaced evenly over the measured interval between that entity's updates, seeded with the server's declared tick rate, so an entity updated every two ticks gets two steps and one updated every tick is forwarded as is. The 26.40 codec stopped serializing the teleport flag, so the snap is sent as the force-move field. Without it, current clients interpolated Geyser's steps a second time. The tick-end immediate flush is skipped when nothing is queued.
Both transports come from NetworkM 1.0.0 and the NetherNet native from webrtc-java 0.17.0-sm.1, both on Maven Central.
… packet stage gate
The Protocol fork now depends on NetworkM's RakNet from Maven Central and merges upstream 26.50 with its v2168 packet fixes. Its JitPack group moves to the EduGeyser organization, and -PlocalProtocol is independent of -PlocalNetworkM again because the pinned build carries the NetworkM packages itself. 26.50 added a default biome to DimensionDefinition; pass minecraft:plains as upstream's 26.50 branch does.
Fabric relocates the shared transport package root, so core's NetherNet references no longer matched the nested transport jar once both transports moved to NetworkM. NeoForge loads nested jars into its plugin layer, which cannot see the WebRTC classes shaded into the mod, so NetherNet never started there. Shading the transport next to the WebRTC classes fixes both.
Fork-first structure with one introduction, links to the EduGeyser organization repositories and the website documentation, dedicated NetherNet and bundled extension sections, and corrected build instructions.
Author
|
whoops, wrong repo |
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.
No description provided.