mirror of
https://github.com/layue13/MoonBridge.git
synced 2026-10-25 18:46:18 +00:00
Verify authenticated Forge transfer on encrypted session
This commit is contained in:
1 parent
aa4d7e560d
commit
c99d296cd4
2 files changed
+77
-8
No files matched your search
@@ -15,7 +15,7 @@ StrataProxy 是面向 Minecraft 1.7.10 Forge 的玩家会话代理。项目正
|
||||
|
||||
配置必须显式选择 `authentication: OFFLINE` 或 `ONLINE_BUNGEE`。两种模式均解析协议 5 Handshake 与 Login Start,等待异步落点,预留容量,连接后端,确认 Login Success,然后转发普通字节流。在线模式执行加密握手、异步会话校验,并向可信的 Bungee 兼容后端转发已验证身份。状态查询由代理直接回答:在线数是本代理已登录的会话数,最大值按当前已注册后端的声明容量计算,且不低于在线数。登录阶段的后端拒绝消息会在前端写出后再清理会话;后端关闭或读失败时,已提交给客户端的写入也会先排空再关闭前端,这两种等待最多 5 秒。`Players.transfer` 会在旧后端继续服务时登录候选后端;普通候选后端收到 Join Game 后即可开始交接,Forge 候选后端收到 ServerHello 后先切换握手链路,待客户端完成握手并收到 Join Game 时再合成 Respawn。`NETWORK_READY` 等待这次协议握手和世界切换包写出,但不表示目标服的插件已完成空岛或副本加载。切换后的玩家实体 ID 会映射到原客户端 ID;候选登录失败时保留旧链路。Forge 重置包写出后若目标拒绝握手,代理结束会话并返回失败,不能恢复旧链路。DNS 与 Agent 是调用通用注册 API 的独立插件。
|
||||
|
||||
**验证边界**:跨后端切换已通过普通协议与 Forge 握手的合成 TCP 测试,包括切换前失败回退、候选服 Join Game 后立即断开时保留旧会话、切换后 Forge 握手拒绝、维度变化后再转服、Forge 到普通后端的切换、握手后 Join Game、维度覆盖、玩家实体 ID 映射和中途断线清理。从 Forge 切换到普通后端会发送一次 FML 重置以恢复客户端注册表;随后进入 Forge 时不会重复发送重置。转服固定发送中间维度和目标维度两次 Respawn,避免依赖已经过期的客户端维度记录;交接期间客户端与旧后端的帧分别进入有上限的缓冲区。成功时,客户端帧在世界切换包写出后转发,旧后端帧丢弃;切换前失败恢复旧链路后,两侧暂存的帧都会回放。这些时序已通过合成 TCP 测试。在线认证仅通过注入会话校验器的合成 TCP 测试。最小 Uranium 1.7.10 后端已通过代理完成协议探针登录和保持连接,两台 Uranium 间的 Forge 转服探针也已通过。Prism 中的真实 Minecraft 1.7.10 + Forge 10.13.4.1614 客户端已通过最小 Uranium 的首次登录与两台 Uranium 间的插件触发转服,见 [真实客户端联机记录](smoke/results/2026-09-25-prism-forge-client.md)。目标 1.7.10 整合包、Mojang 在线认证、跨主机部署和目标流量下的性能验收仍未验证;当前不能宣称生产可用或目标整合包实服兼容。
|
||||
**验证边界**:跨后端切换已通过普通协议与 Forge 握手的合成 TCP 测试,包括切换前失败回退、候选服 Join Game 后立即断开时保留旧会话、切换后 Forge 握手拒绝、维度变化后再转服、Forge 到普通后端的切换、握手后 Join Game、维度覆盖、玩家实体 ID 映射和中途断线清理。从 Forge 切换到普通后端会发送一次 FML 重置以恢复客户端注册表;随后进入 Forge 时不会重复发送重置。转服固定发送中间维度和目标维度两次 Respawn,避免依赖已经过期的客户端维度记录;交接期间客户端与旧后端的帧分别进入有上限的缓冲区。成功时,客户端帧在世界切换包写出后转发,旧后端帧丢弃;切换前失败恢复旧链路后,两侧暂存的帧都会回放。这些时序已通过合成 TCP 测试。在线认证及其加密连接上的 Forge 转服已通过注入会话校验器的合成 TCP 测试,尚未调用真实 Mojang 会话服务。最小 Uranium 1.7.10 后端已通过代理完成协议探针登录和保持连接,两台 Uranium 间的 Forge 转服探针也已通过。Prism 中的真实 Minecraft 1.7.10 + Forge 10.13.4.1614 客户端已通过最小 Uranium 的首次登录与两台 Uranium 间的插件触发转服,见 [真实客户端联机记录](smoke/results/2026-09-25-prism-forge-client.md)。目标 1.7.10 整合包、Mojang 在线认证、跨主机部署和目标流量下的性能验收仍未验证;当前不能宣称生产可用或目标整合包实服兼容。
|
||||
|
||||
## 配置与运行
|
||||
|
||||
|
||||
+76
-7
@@ -817,7 +817,7 @@ final class ProxySessionListenerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlineTransferForwardsVerifiedIdentityToReplacementAndKeepsClientEncrypted() throws Exception {
|
||||
void onlineTransferToForgeForwardsVerifiedIdentityAndKeepsClientEncrypted() throws Exception {
|
||||
UUID uuid = UUID.fromString("12345678-1234-1234-1234-123456789abc");
|
||||
var catalog = new InMemoryBackendCatalog();
|
||||
try (ServerSocket oldServer = new ServerSocket(0, 8, InetAddress.getLoopbackAddress());
|
||||
@@ -843,7 +843,21 @@ final class ProxySessionListenerTest {
|
||||
var input = new DataInputStream(socket.getInputStream());
|
||||
var output = new DataOutputStream(socket.getOutputStream());
|
||||
acceptForwardedLogin(input, uuid);
|
||||
sendOnlineBackendPlayStart(output, uuid, 200);
|
||||
sendOnlineBackendLoginSuccess(output, uuid);
|
||||
writeFrame(output, forgeRegistration(true));
|
||||
writeFrame(output, forgeHello(7));
|
||||
assertArrayEquals(forgeRegistration(false), readFrame(input));
|
||||
assertArrayEquals(forgeMessage(false, new byte[]{1, 2}), readFrame(input));
|
||||
assertArrayEquals(forgeMessage(false, new byte[]{2, 0}), readFrame(input));
|
||||
writeFrame(output, forgeMessage(true, new byte[]{2, 0}));
|
||||
assertArrayEquals(forgeMessage(false, new byte[]{(byte) 0xff, 2}), readFrame(input));
|
||||
writeFrame(output, forgeMessage(true, new byte[]{3, 0, 0, 0}));
|
||||
writeFrame(output, forgeMessage(true, new byte[]{(byte) 0xff, 2}));
|
||||
assertArrayEquals(forgeMessage(false, new byte[]{(byte) 0xff, 3}), readFrame(input));
|
||||
assertArrayEquals(forgeMessage(false, new byte[]{(byte) 0xff, 4}), readFrame(input));
|
||||
writeFrame(output, forgeMessage(true, new byte[]{(byte) 0xff, 3}));
|
||||
assertArrayEquals(forgeMessage(false, new byte[]{(byte) 0xff, 5}), readFrame(input));
|
||||
sendOnlineBackendWorld(output, 200);
|
||||
assertArrayEquals(new byte[]{0x01, 0x33}, readFrame(input));
|
||||
writeFrame(output, new byte[]{0x03, 0x44});
|
||||
replacementRelayed.complete(null);
|
||||
@@ -898,6 +912,7 @@ final class ProxySessionListenerTest {
|
||||
|
||||
var encryptedInput = new DataInputStream(decryptingInput(client.getInputStream(),
|
||||
aes(secret, Cipher.DECRYPT_MODE)));
|
||||
Cipher encryptor = aes(secret, Cipher.ENCRYPT_MODE);
|
||||
var success = new java.io.ByteArrayInputStream(readFrame(encryptedInput));
|
||||
assertEquals(2, readVarInt(success));
|
||||
assertEquals(uuid.toString(), readString(success, 36));
|
||||
@@ -909,16 +924,30 @@ final class ProxySessionListenerTest {
|
||||
while (listener.online().isEmpty() && System.nanoTime() < deadline) Thread.sleep(5);
|
||||
assertEquals(1, listener.online().size());
|
||||
var player = listener.online().get(0);
|
||||
var transfer = listener.transfer(player.identity(), "new").toCompletableFuture()
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
var pendingTransfer = listener.transfer(player.identity(), "new").toCompletableFuture();
|
||||
byte[] reset = readFrame(encryptedInput);
|
||||
assertEquals(0x3f, readVarInt(new java.io.ByteArrayInputStream(reset)));
|
||||
assertEquals((byte) 0xfe, reset[reset.length - 1]);
|
||||
assertArrayEquals(forgeRegistration(true), readFrame(encryptedInput));
|
||||
assertArrayEquals(forgeHello(7), readFrame(encryptedInput));
|
||||
writeEncryptedFrame(clearOutput, encryptor, forgeRegistration(false));
|
||||
writeEncryptedFrame(clearOutput, encryptor, forgeMessage(false, new byte[]{1, 2}));
|
||||
writeEncryptedFrame(clearOutput, encryptor, forgeMessage(false, new byte[]{2, 0}));
|
||||
assertArrayEquals(forgeMessage(true, new byte[]{2, 0}), readFrame(encryptedInput));
|
||||
writeEncryptedFrame(clearOutput, encryptor, forgeMessage(false, new byte[]{(byte) 0xff, 2}));
|
||||
assertArrayEquals(forgeMessage(true, new byte[]{3, 0, 0, 0}), readFrame(encryptedInput));
|
||||
assertArrayEquals(forgeMessage(true, new byte[]{(byte) 0xff, 2}), readFrame(encryptedInput));
|
||||
writeEncryptedFrame(clearOutput, encryptor, forgeMessage(false, new byte[]{(byte) 0xff, 3}));
|
||||
writeEncryptedFrame(clearOutput, encryptor, forgeMessage(false, new byte[]{(byte) 0xff, 4}));
|
||||
assertArrayEquals(forgeMessage(true, new byte[]{(byte) 0xff, 3}), readFrame(encryptedInput));
|
||||
writeEncryptedFrame(clearOutput, encryptor, forgeMessage(false, new byte[]{(byte) 0xff, 5}));
|
||||
var transfer = pendingTransfer.get(5, TimeUnit.SECONDS);
|
||||
assertEquals(dev.strataproxy.api.TransferStatus.NETWORK_READY, transfer.status(),
|
||||
transfer.detail().orElse(""));
|
||||
assertEquals(7, readVarInt(new java.io.ByteArrayInputStream(readFrame(encryptedInput))));
|
||||
assertEquals(7, readVarInt(new java.io.ByteArrayInputStream(readFrame(encryptedInput))));
|
||||
assertArrayEquals(new byte[]{0x08}, readFrame(encryptedInput));
|
||||
byte[] encryptedPlay = aes(secret, Cipher.ENCRYPT_MODE).update(framed(new byte[]{0x01, 0x33}));
|
||||
clearOutput.write(encryptedPlay);
|
||||
clearOutput.flush();
|
||||
writeEncryptedFrame(clearOutput, encryptor, new byte[]{0x01, 0x33});
|
||||
replacementRelayed.get(5, TimeUnit.SECONDS);
|
||||
assertArrayEquals(new byte[]{0x03, 0x44}, readFrame(encryptedInput));
|
||||
assertEquals("new", listener.find(player.identity()).orElseThrow().currentServer().orElseThrow());
|
||||
@@ -945,11 +974,19 @@ final class ProxySessionListenerTest {
|
||||
}
|
||||
|
||||
private static void sendOnlineBackendPlayStart(DataOutputStream output, UUID uuid, int entityId) throws Exception {
|
||||
sendOnlineBackendLoginSuccess(output, uuid);
|
||||
sendOnlineBackendWorld(output, entityId);
|
||||
}
|
||||
|
||||
private static void sendOnlineBackendLoginSuccess(DataOutputStream output, UUID uuid) throws Exception {
|
||||
ByteArrayOutputStream success = new ByteArrayOutputStream();
|
||||
writeVarInt(success, 2);
|
||||
writeString(success, uuid.toString());
|
||||
writeString(success, "Alice");
|
||||
writeFrame(output, success.toByteArray());
|
||||
}
|
||||
|
||||
private static void sendOnlineBackendWorld(DataOutputStream output, int entityId) throws Exception {
|
||||
ByteArrayOutputStream join = new ByteArrayOutputStream();
|
||||
writeVarInt(join, 1);
|
||||
var data = new DataOutputStream(join);
|
||||
@@ -963,6 +1000,38 @@ final class ProxySessionListenerTest {
|
||||
writeFrame(output, new byte[]{0x08});
|
||||
}
|
||||
|
||||
private static void writeEncryptedFrame(DataOutputStream output, Cipher cipher, byte[] payload) throws Exception {
|
||||
output.write(cipher.update(framed(payload)));
|
||||
output.flush();
|
||||
}
|
||||
|
||||
private static byte[] forgeRegistration(boolean clientbound) throws Exception {
|
||||
return forgePacket(clientbound, "REGISTER", "FML|HS\0FML".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static byte[] forgeHello(int dimensionOverride) throws Exception {
|
||||
ByteArrayOutputStream hello = new ByteArrayOutputStream();
|
||||
var output = new DataOutputStream(hello);
|
||||
output.writeByte(0);
|
||||
output.writeByte(2);
|
||||
output.writeInt(dimensionOverride);
|
||||
return forgeMessage(true, hello.toByteArray());
|
||||
}
|
||||
|
||||
private static byte[] forgeMessage(boolean clientbound, byte[] handshake) throws Exception {
|
||||
return forgePacket(clientbound, "FML|HS", handshake);
|
||||
}
|
||||
|
||||
private static byte[] forgePacket(boolean clientbound, String channel, byte[] payload) throws Exception {
|
||||
ByteArrayOutputStream packet = new ByteArrayOutputStream();
|
||||
writeVarInt(packet, clientbound ? 0x3f : 0x17);
|
||||
writeString(packet, channel);
|
||||
var output = new DataOutputStream(packet);
|
||||
output.writeShort(payload.length);
|
||||
output.write(payload);
|
||||
return packet.toByteArray();
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlinePlacementRejectionIsEncryptedAndDelivered() throws Exception {
|
||||
UUID uuid = UUID.fromString("12345678-1234-1234-1234-123456789abc");
|
||||
|
||||
Reference in new issue
Block a user