Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ netease_client_support: true
only_allow_netease_client: false
```

## Supported Bedrock Protocol Versions
Comment thread
Miroshka000 marked this conversation as resolved.
Outdated

The latest supported Bedrock version is **1.26.30** (`v1001`). This includes the new `1.26.20` (`v975`) and `1.26.30` (`v1001`) protocol updates.

### Supported NetEase Protocol Versions

| Protocol Version | Game Version |
Expand Down
5 changes: 3 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ val isDevBuild = providers.gradleProperty("waterdog.is-dev-build")
val log4j2Version = "2.25.3"
val jlineVersion = "3.30.6"
val nettyVersion = "4.1.101.Final"
val raklibVersion = "1.0.0.CR3-20260328.145829-29"
val protocolVersion = "1.26.10-R2"
val raklibVersion = "1.0.0.CR3-20260421.213623-35"
val protocolVersion = "1.26.20-R4-SNAPSHOT"

group = "org.allaymc"
version = if (isDevBuild) "$baseVersion-SNAPSHOT" else baseVersion
Expand All @@ -42,6 +42,7 @@ java {

repositories {
mavenCentral()
maven("https://central.sonatype.com/repository/maven-snapshots/")
maven("https://repo.opencollab.dev/maven-releases/")
maven("https://repo.opencollab.dev/maven-snapshots/")
maven("https://repo.waterdog.dev/main")
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true

waterdog.version=1.26.10-R2
waterdog.version=1.26.30-R1
waterdog.is-dev-build=true
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ public class ProtocolCodecs {
HANDLED_PACKETS.add(PlayerUpdateEntityOverridesPacket.class);
HANDLED_PACKETS.add(PlayerLocationPacket.class);
HANDLED_PACKETS.add(CameraPresetsPacket.class);
HANDLED_PACKETS.add(PrimitiveShapesPacket.class);
HANDLED_PACKETS.add(AddVolumeEntityPacket.class);
HANDLED_PACKETS.add(RemoveVolumeEntityPacket.class);
HANDLED_PACKETS.add(PlayerFogPacket.class);
HANDLED_PACKETS.add(UpdateClientInputLocksPacket.class);
HANDLED_PACKETS.add(SetHudPacket.class);
HANDLED_PACKETS.add(ContainerOpenPacket.class);
HANDLED_PACKETS.add(ContainerClosePacket.class);
HANDLED_PACKETS.add(SetTimePacket.class);
}

private static final List<ProtocolCodecUpdater> UPDATERS = new ObjectArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import lombok.Setter;
import lombok.ToString;
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.cloudburstmc.protocol.bedrock.codec.v1001.Bedrock_v1001;
import org.cloudburstmc.protocol.bedrock.codec.v313.Bedrock_v313;
import org.cloudburstmc.protocol.bedrock.codec.v332.Bedrock_v332;
import org.cloudburstmc.protocol.bedrock.codec.v340.Bedrock_v340;
Expand Down Expand Up @@ -83,6 +84,7 @@
import org.cloudburstmc.protocol.bedrock.codec.v898.Bedrock_v898;
import org.cloudburstmc.protocol.bedrock.codec.v924.Bedrock_v924;
import org.cloudburstmc.protocol.bedrock.codec.v944.Bedrock_v944;
import org.cloudburstmc.protocol.bedrock.codec.v975.Bedrock_v975;

@ToString(exclude = {"defaultCodec", "bedrockCodec"})
public enum ProtocolVersion {
Expand Down Expand Up @@ -148,6 +150,8 @@ public enum ProtocolVersion {
MINECRAFT_PE_1_21_130(898, Bedrock_v898.CODEC),
MINECRAFT_PE_1_26_0(924, Bedrock_v924.CODEC),
MINECRAFT_PE_1_26_10(944, Bedrock_v944.CODEC),
MINECRAFT_PE_1_26_20(975, Bedrock_v975.CODEC),
MINECRAFT_PE_1_26_30(1001, Bedrock_v1001.CODEC),
;

private static final ProtocolVersion[] VALUES = values();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData;
import org.cloudburstmc.protocol.bedrock.data.primitiveshape.*;
import org.cloudburstmc.protocol.bedrock.packet.*;
import dev.waterdog.waterdogpe.network.protocol.rewrite.types.RewriteData;
import dev.waterdog.waterdogpe.network.protocol.user.PlayerRewriteUtils;
Expand Down Expand Up @@ -374,6 +375,68 @@ public PacketSignal handle(CameraInstructionPacket packet) {
return signal;
}

@Override
public PacketSignal handle(PrimitiveShapesPacket packet) {
PacketSignal signal = PacketSignal.UNHANDLED;
ListIterator<PrimitiveShape> iterator = packet.getShapes().listIterator();
while (iterator.hasNext()) {
PrimitiveShape shape = iterator.next();
Long attachedEntityId = shape.getAttachedToEntityId();
if (attachedEntityId != null) {
long rewrittenId = PlayerRewriteUtils.rewriteId(attachedEntityId, this.rewrite.getEntityId(), this.rewrite.getOriginalEntityId());
if (rewrittenId != attachedEntityId) {
iterator.set(withAttachedEntityId(shape, rewrittenId));
signal = PacketSignal.HANDLED;
}
}
}
return signal;
}

private static PrimitiveShape withAttachedEntityId(PrimitiveShape shape, Long attachedEntityId) {
return switch (shape) {
case PrimitiveLine line -> new PrimitiveLine(line.getId(), line.getDimension(), line.getPosition(),
line.getScale(), line.getRotation(), line.getTotalTimeLeft(), line.getColor(),
line.getMaximumRenderDistance(), line.getLineEndPosition(), attachedEntityId);
case PrimitiveBox box -> new PrimitiveBox(box.getId(), box.getDimension(), box.getPosition(),
box.getScale(), box.getRotation(), box.getTotalTimeLeft(), box.getColor(),
box.getMaximumRenderDistance(), box.getBoxBounds(), attachedEntityId);
case PrimitiveSphere sphere -> new PrimitiveSphere(sphere.getId(), sphere.getDimension(),
sphere.getPosition(), sphere.getScale(), sphere.getRotation(), sphere.getTotalTimeLeft(),
sphere.getColor(), sphere.getMaximumRenderDistance(), sphere.getSegments(), attachedEntityId);
case PrimitiveCircle circle -> new PrimitiveCircle(circle.getId(), circle.getDimension(),
circle.getPosition(), circle.getScale(), circle.getRotation(), circle.getTotalTimeLeft(),
circle.getColor(), circle.getMaximumRenderDistance(), circle.getSegments(), attachedEntityId);
case PrimitiveText text -> new PrimitiveText(text.getId(), text.getDimension(), text.getPosition(),
text.getScale(), text.getRotation(), text.getTotalTimeLeft(), text.getColor(), text.getText(),
text.isUseRotation(), text.getBackgroundColor(), text.isDepthTest(), text.isShowBackface(),
text.isShowTextBackface(), text.getMaximumRenderDistance(), attachedEntityId);
case PrimitiveArrow arrow -> new PrimitiveArrow(arrow.getId(), arrow.getDimension(), arrow.getPosition(),
arrow.getScale(), arrow.getRotation(), arrow.getTotalTimeLeft(), arrow.getColor(),
arrow.getMaximumRenderDistance(), arrow.getArrowEndPosition(), arrow.getArrowHeadLength(),
arrow.getArrowHeadRadius(), arrow.getArrowHeadSegments(), attachedEntityId);
case PrimitiveCylinder cylinder -> new PrimitiveCylinder(cylinder.getId(), cylinder.getDimension(),
cylinder.getPosition(), cylinder.getScale(), cylinder.getRotation(), cylinder.getTotalTimeLeft(),
cylinder.getColor(), cylinder.getMaximumRenderDistance(), cylinder.getHeight(),
cylinder.getSegments(), cylinder.getRadiusX(), cylinder.getRadiusZ(), attachedEntityId);
case PrimitivePyramid pyramid -> new PrimitivePyramid(pyramid.getId(), pyramid.getDimension(),
pyramid.getPosition(), pyramid.getScale(), pyramid.getRotation(), pyramid.getTotalTimeLeft(),
pyramid.getColor(), pyramid.getMaximumRenderDistance(), pyramid.getHeight(), pyramid.getWidth(),
pyramid.getDepth(), attachedEntityId);
case PrimitiveEllipsoid ellipsoid -> new PrimitiveEllipsoid(ellipsoid.getId(), ellipsoid.getDimension(),
ellipsoid.getPosition(), ellipsoid.getScale(), ellipsoid.getRotation(),
ellipsoid.getTotalTimeLeft(), ellipsoid.getColor(), ellipsoid.getMaximumRenderDistance(),
ellipsoid.getSegments(), ellipsoid.getRadii(), attachedEntityId);
case PrimitiveCone cone -> new PrimitiveCone(cone.getId(), cone.getDimension(), cone.getPosition(),
cone.getScale(), cone.getRotation(), cone.getTotalTimeLeft(), cone.getColor(),
cone.getMaximumRenderDistance(), cone.getHeight(), cone.getSegments(), cone.getRadii(),
attachedEntityId);
default -> new PrimitiveShape(shape.getId(), shape.getDimension(), shape.getPosition(), shape.getScale(),
shape.getRotation(), shape.getTotalTimeLeft(), shape.getColor(), shape.getMaximumRenderDistance(),
attachedEntityId);
};
}

private PacketSignal rewriteMetadata(EntityDataMap metadata) {
PacketSignal signal = PacketSignal.UNHANDLED;
for (EntityDataType<Long> data : ENTITY_DATA_FIELDS) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,24 @@ public class HandshakeEntry {
private final String xuid;
private final UUID uuid;
private final String displayName;
private final String minecraftId;
private final boolean xboxAuthed;
private final boolean isChainPayload;
private final boolean shouldSendCertificateChain;
private final boolean netEaseClient;
private final LoginData.NetEaseData netEaseData;
@Setter
private ProtocolVersion protocol;

public HandshakeEntry(ECPublicKey identityPublicKey, JsonObject clientData, String xuid, UUID uuid, String displayName, boolean xboxAuthed, ProtocolVersion protocol, boolean isChainPayload, boolean netEaseClient, LoginData.NetEaseData netEaseData) {
public HandshakeEntry(ECPublicKey identityPublicKey, JsonObject clientData, String xuid, UUID uuid, String displayName, String minecraftId, boolean xboxAuthed, ProtocolVersion protocol, boolean shouldSendCertificateChain, boolean netEaseClient, LoginData.NetEaseData netEaseData) {
this.identityPublicKey = identityPublicKey;
this.clientData = clientData;
this.xuid = xuid;
this.uuid = uuid;
this.displayName = displayName;
this.minecraftId = minecraftId;
this.xboxAuthed = xboxAuthed;
this.protocol = protocol;
this.isChainPayload = isChainPayload;
this.shouldSendCertificateChain = shouldSendCertificateChain;
this.netEaseClient = netEaseClient;
this.netEaseData = netEaseData;
}
Expand All @@ -69,13 +71,14 @@ public LoginData buildData(BedrockServerSession session, ProxyServer proxy) {
builder.displayName(this.displayName);
builder.uuid(this.uuid);
builder.xuid(this.xuid);
builder.minecraftId(this.minecraftId);
builder.xboxAuthed(this.xboxAuthed);
builder.protocol(this.protocol);
builder.joinHostname(this.clientData.get("ServerAddress").getAsString().split(":")[0]);
builder.address(session.getSocketAddress());
builder.keyPair(event.getKeyPair());
builder.clientData(this.clientData);
builder.isChainPayload(this.isChainPayload);
builder.shouldSendCertificateChain(this.shouldSendCertificateChain);
builder.netEaseClient(this.netEaseClient);
builder.netEaseData(this.netEaseData);
if (this.clientData.has("DeviceModel")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import lombok.extern.log4j.Log4j2;
import org.cloudburstmc.protocol.bedrock.BedrockSession;
import org.cloudburstmc.protocol.bedrock.data.auth.CertificateChainPayload;
import org.cloudburstmc.protocol.bedrock.data.auth.TokenPayload;
import org.cloudburstmc.protocol.bedrock.packet.LoginPacket;
import org.cloudburstmc.protocol.bedrock.packet.ServerToClientHandshakePacket;
import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult;
Expand Down Expand Up @@ -71,6 +72,14 @@ public class HandshakeUtils {
}
}

public static JsonObject createChainExtraData(String displayName, String xuid, UUID uuid) {
JsonObject extraData = new JsonObject();
extraData.addProperty("displayName", displayName);
extraData.addProperty("XUID", xuid);
extraData.addProperty("identity", uuid.toString());
return extraData;
}

public static SignedJWT createClientDataChain(KeyPair pair, JsonObject extraData) {
String publicKeyBase64 = Base64.getEncoder().encodeToString(pair.getPublic().getEncoded());
long timestamp = System.currentTimeMillis() / 1000;
Expand All @@ -87,17 +96,21 @@ public static SignedJWT createClientDataChain(KeyPair pair, JsonObject extraData
return encodeJWT(pair, dataChain);
}

public static SignedJWT createClientDataToken(KeyPair pair, String displayName, String xuid) {
public static SignedJWT createClientDataToken(KeyPair pair, String displayName, String xuid, UUID uuid, String minecraftId) {
String publicKeyBase64 = Base64.getEncoder().encodeToString(pair.getPublic().getEncoded());
long timestamp = System.currentTimeMillis() / 1000;

JsonObject dataChain = new JsonObject();
dataChain.addProperty("cpk", publicKeyBase64);
dataChain.addProperty("leguuid", uuid.toString());
dataChain.addProperty("iat", timestamp);
dataChain.addProperty("xname", displayName);
dataChain.addProperty("exp", timestamp + 24 * 3600);
dataChain.addProperty("mid", minecraftId);
dataChain.addProperty("ap", 7);
dataChain.addProperty("iss", "self");
dataChain.addProperty("cpk", publicKeyBase64);
dataChain.addProperty("aud", "api://auth-minecraft-services/multiplayer");
dataChain.addProperty("xid", xuid);
dataChain.addProperty("xname", displayName);
return encodeJWT(pair, dataChain);
}

Expand Down Expand Up @@ -140,6 +153,7 @@ public static HandshakeEntry processHandshake(BedrockSession session, LoginPacke
String xuid = identityData.xuid;
//UUID uuid = UUID.nameUUIDFromBytes(("pocket-auth-1-xuid:" + xuid).getBytes(StandardCharsets.UTF_8));
UUID uuid = identityData.identity;
String minecraftId = identityData.minecraftId;

SignedJWT clientDataJwt = SignedJWT.parse(packet.getClientJwt());
JsonObject clientData = HandshakeUtils.parseClientData(clientDataJwt, xuid, session);
Expand All @@ -166,8 +180,12 @@ public static HandshakeEntry processHandshake(BedrockSession session, LoginPacke
netEaseData = extractNetEaseData(result.rawIdentityClaims());
}

return new HandshakeEntry(identityPublicKey, clientData, xuid, uuid, displayName, xboxAuth, protocol,
packet.getAuthPayload() instanceof CertificateChainPayload, neteaseClient, netEaseData);
boolean shouldSendCertificateChain = packet.getAuthPayload() instanceof CertificateChainPayload ||
protocol.isBefore(ProtocolVersion.MINECRAFT_PE_1_26_20) ||
(neteaseClient && !(packet.getAuthPayload() instanceof TokenPayload));

return new HandshakeEntry(identityPublicKey, clientData, xuid, uuid, displayName, minecraftId, xboxAuth, protocol,
shouldSendCertificateChain, neteaseClient, netEaseData);
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -218,11 +236,4 @@ public static void processEncryption(BedrockSession session, PublicKey key) thro
});
}

public static JsonObject createChainExtraData(String displayName, String xuid, UUID uuid) {
JsonObject extraData = new JsonObject();
extraData.addProperty("displayName", displayName);
extraData.addProperty("XUID", xuid);
extraData.addProperty("identity", uuid.toString());
return extraData;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import com.google.gson.JsonObject;
import com.nimbusds.jwt.SignedJWT;
import dev.waterdog.waterdogpe.ProxyServer;
import dev.waterdog.waterdogpe.network.protocol.ProtocolVersion;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -46,6 +45,7 @@ public class LoginData {
private final String displayName;
private final UUID uuid;
private final String xuid;
private final String minecraftId;
private final boolean xboxAuthed;
private final SocketAddress address;
private final ProtocolVersion protocol;
Expand All @@ -62,7 +62,7 @@ public class LoginData {
private final JsonObject clientData;
private final NetEaseData netEaseData;
private final boolean netEaseClient;
private final boolean isChainPayload;
private final boolean shouldSendCertificateChain;
private LoginPacket loginPacket;
@Setter
@Builder.Default
Expand All @@ -82,7 +82,7 @@ public LoginPacket rebuildLoginPacket() {
SignedJWT signedClientData = HandshakeUtils.encodeJWT(this.keyPair, this.clientData);
loginPacket.setClientJwt(signedClientData.serialize());
loginPacket.setProtocolVersion(this.protocol.getProtocol());
if (isChainPayload || ProxyServer.getInstance().getConfiguration().useCertificatePayload()) {
if (shouldSendCertificateChain) {
JsonObject extraData = HandshakeUtils.createChainExtraData(displayName, xuid, uuid);
// Add NetEase-specific fields if this is a NetEase client
if (this.netEaseData != null) {
Expand All @@ -98,7 +98,7 @@ public LoginPacket rebuildLoginPacket() {
SignedJWT signedPayload = HandshakeUtils.createClientDataChain(this.keyPair, extraData);
loginPacket.setAuthPayload(new CertificateChainPayload(Collections.singletonList(signedPayload.serialize()), AuthType.SELF_SIGNED));
} else {
SignedJWT signedPayload = HandshakeUtils.createClientDataToken(this.keyPair, displayName, xuid);
SignedJWT signedPayload = HandshakeUtils.createClientDataToken(this.keyPair, displayName, xuid, uuid, minecraftId);
loginPacket.setAuthPayload(new TokenPayload(signedPayload.serialize(), AuthType.SELF_SIGNED));
}
this.loginPacket = loginPacket;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,6 @@ public class ProxyConfig extends YamlConfig {
@Comment("If enabled, the proxy will pass information like XUID or IP to the downstream server using custom fields in the LoginPacket")
private boolean useLoginExtras = false;

@Path("use_certificate_payload")
@Accessors(fluent = true)
@Comment("If enabled, the proxy will always send Certificate payload in the LoginPacket")
private boolean useCertificatePayload = true;

@Path("replace_username_spaces")
@Comment("Replaces username spaces with underscores if enabled")
private boolean replaceUsernameSpaces = false;
Expand Down Expand Up @@ -195,7 +190,7 @@ public class ProxyConfig extends YamlConfig {

@Path("netease_client_support")
@Comments({
"Enable support for NetEase (China) Minecraft clients. Only protocol v766 (1.21.50) is supported.",
"Enable support for NetEase (China) Minecraft clients. Supported protocols: v630, v686, v766, v819.",
"Warning: Enabling this will treat all RakNet v8 clients as NetEase clients"
})
private boolean neteaseClientSupport = false;
Expand Down
Loading