From c029979932963fea001dd30ec411e6dd8ef181cc Mon Sep 17 00:00:00 2001 From: Wouter Gritter Date: Fri, 23 Oct 2026 20:30:56 +0200 Subject: [PATCH 1/2] Minecraft 26.4-snapshot-5 --- .../api/network/ProtocolVersion.java | 3 +- .../connection/MinecraftSessionHandler.java | 10 +++ .../backend/BackendPlaySessionHandler.java | 3 +- .../backend/ConfigSessionHandler.java | 3 +- .../backend/LoginSessionHandler.java | 20 ++++- .../connection/client/AuthSessionHandler.java | 20 ++++- .../client/ClientConfigSessionHandler.java | 17 ++++ .../connection/client/ConnectedPlayer.java | 15 ++++ .../client/HandshakeSessionHandler.java | 7 ++ .../client/InitialLoginSessionHandler.java | 59 ++++++++++++-- .../proxy/protocol/StateRegistry.java | 9 +++ .../packet/ClientboundApplyOptionsPacket.java | 79 +++++++++++++++++++ .../protocol/packet/ConnectionOptions.java | 41 ++++++++++ .../protocol/packet/HandshakePacket.java | 8 +- .../proxy/protocol/packet/JoinGamePacket.java | 10 ++- .../proxy/protocol/packet/RespawnPacket.java | 8 +- .../packet/ServerLoginSuccessPacket.java | 23 ++++++ .../ServerboundOptionsAppliedPacket.java | 58 ++++++++++++++ .../proxy/protocol/packet/TransferPacket.java | 40 ++++++++++ .../protocol/util/PluginMessageUtil.java | 12 +++ .../client/HandshakeSessionHandlerTest.java | 7 ++ .../protocol/packet/TransferPacketTest.java | 62 +++++++++++++++ 22 files changed, 495 insertions(+), 19 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundApplyOptionsPacket.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ConnectionOptions.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundOptionsAppliedPacket.java create mode 100644 proxy/src/test/java/com/velocitypowered/proxy/protocol/packet/TransferPacketTest.java diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index 6653ff9c1a..e5d823fea1 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -97,7 +97,8 @@ public boolean isSupported() { MINECRAFT_1_21_11(774, "1.21.11"), MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2"), MINECRAFT_26_2(776, "26.2"), - MINECRAFT_26_3(777, "26.3"); + MINECRAFT_26_3(777, "26.3"), + MINECRAFT_26_4(-1, 343, "26.4"); // TODO private static final int SNAPSHOT_BIT = 30; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index 9df71c3f7a..99e25a8946 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -22,6 +22,7 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundApplyOptionsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundPostEffectsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket; @@ -54,6 +55,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundOptionsAppliedPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; @@ -409,4 +411,12 @@ default boolean handle(ClientboundStopSoundPacket packet) { default boolean handle(ClientboundPostEffectsPacket packet) { return false; } + + default boolean handle(ClientboundApplyOptionsPacket packet) { + return false; + } + + default boolean handle(ServerboundOptionsAppliedPacket packet) { + return false; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index 37a61d357d..462905ff09 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -421,7 +421,8 @@ public boolean handle(TransferPacket packet) { resultedAddress = originalAddress; } this.playerConnection.write(new TransferPacket( - resultedAddress.getHostName(), resultedAddress.getPort())); + resultedAddress.getHostName(), resultedAddress.getPort(), + packet.properties())); } }, playerConnection.eventLoop()); return true; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java index 904e848f3a..bda5e3977c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -328,7 +328,8 @@ public boolean handle(TransferPacket packet) { resultedAddress = originalAddress; } serverConn.getPlayer().getConnection().write(new TransferPacket( - resultedAddress.getHostName(), resultedAddress.getPort())); + resultedAddress.getHostName(), resultedAddress.getPort(), + packet.properties())); } }, serverConn.ensureConnected().eventLoop()); return true; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index 14884af467..35a1cc05c9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -35,6 +35,7 @@ import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; +import com.velocitypowered.proxy.protocol.packet.ConnectionOptions; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.LoginAcknowledgedPacket; @@ -66,6 +67,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { private final VelocityServerConnection serverConn; private final CompletableFuture resultFuture; private boolean informationForwarded; + private int pendingCompressionThreshold = -1; // 26.4+ LoginSessionHandler(VelocityServer server, VelocityServerConnection serverConn, CompletableFuture resultFuture) { @@ -137,7 +139,13 @@ public boolean handle(DisconnectPacket packet) { @Override public boolean handle(SetCompressionPacket packet) { - serverConn.ensureConnected().setCompressionThreshold(packet.getThreshold()); + MinecraftConnection smc = serverConn.ensureConnected(); + if (smc.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_4)) { + // Only announces the threshold; LoginSuccess says whether to switch it on. + pendingCompressionThreshold = packet.getThreshold(); + } else { + smc.setCompressionThreshold(packet.getThreshold()); + } return true; } @@ -158,6 +166,16 @@ public boolean handle(ServerLoginSuccessPacket packet) { smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture)); } else { smc.write(new LoginAcknowledgedPacket()); + if (smc.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_4) + && ConnectionOptions.isCompression(packet.getOptions())) { + if (pendingCompressionThreshold < 0) { + throw new QuietRuntimeException("Backend server asked for compression in LoginSuccess" + + " without sending a compression threshold first"); + } + // LoginAcknowledged has been encoded uncompressed above (we are on the event loop); the + // server only replies once it has received it, so both directions switch over here. + smc.setCompressionThreshold(pendingCompressionThreshold); + } smc.setActiveSessionHandler(StateRegistry.CONFIG, new ConfigSessionHandler(server, serverConn, resultFuture)); ConnectedPlayer player = serverConn.getPlayer(); if (player.getClientSettingsPacket() != null) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index fee642b495..7fbfb61c7e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.connection.client; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_4; import com.google.common.base.Preconditions; import com.velocitypowered.api.event.connection.DisconnectEvent; @@ -40,6 +41,7 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.ConnectionOptions; import com.velocitypowered.proxy.protocol.packet.LoginAcknowledgedPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; @@ -70,6 +72,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler { private final boolean onlineMode; private State loginState = State.START; // 1.20.2+ private final String serverIdHash; + // 26.4+: SetCompression only announces the threshold; the client switches compression on + // once LoginSuccess (with the compression option) is acknowledged. + private int pendingCompressionThreshold = -1; AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound, GameProfile profile, boolean onlineMode, String serverIdHash) { @@ -139,7 +144,11 @@ private void startLoginCompletion(ConnectedPlayer player) { int threshold = server.getConfiguration().getCompressionThreshold(); if (threshold >= 0 && mcConnection.getProtocolVersion().noLessThan(MINECRAFT_1_8)) { mcConnection.write(new SetCompressionPacket(threshold)); - mcConnection.setCompressionThreshold(threshold); + if (mcConnection.getProtocolVersion().noLessThan(MINECRAFT_26_4)) { + pendingCompressionThreshold = threshold; + } else { + mcConnection.setCompressionThreshold(threshold); + } } VelocityConfiguration configuration = server.getConfiguration(); UUID playerUniqueId = player.getUniqueId(); @@ -182,6 +191,12 @@ public boolean handle(LoginAcknowledgedPacket packet) { inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data")); } else { loginState = State.ACKNOWLEDGED; + if (pendingCompressionThreshold >= 0) { + // LoginAcknowledged was the last uncompressed packet in both directions. We are still + // inside the frame decoder's read loop, so the next frame already goes through the + // decompressor. + mcConnection.setCompressionThreshold(pendingCompressionThreshold); + } mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, new ClientConfigSessionHandler(server, connectedPlayer)); server.getEventManager().fire(new PostLoginEvent(connectedPlayer)).thenCompose(ignored -> { @@ -239,6 +254,9 @@ private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { if (inbound.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_2)) { success.setSessionId(server.getSessionId()); } + if (pendingCompressionThreshold >= 0) { + success.setOptions(ConnectionOptions.COMPRESSION); + } mcConnection.write(success); loginState = State.SUCCESS_SENT; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java index b86041035b..6715d4cf17 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -133,11 +133,22 @@ public boolean handle(final PluginMessagePacket packet) { brandChannel = packet.getChannel(); // Client sends `minecraft:brand` packet immediately after Login, // but at this time the backend server may not be ready + } else if (PluginMessageUtil.isModList(packet)) { + // 26.4+ clients send `minecraft:mod_list` right after the brand, once per session. + player.setClientModList(ByteBufUtil.getBytes(packet.content())); } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { return true; } else if (serverConn != null) { ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); + if (serverConn.getConnection() == null) { + // The backend connection is still being set up; there is nowhere to send this yet. + // TODO: Do we queue this instead? + logger.warn("Dropping plugin message on {} from {}: backend connection to {} not ready", + packet.getChannel(), player, serverConn.getServerInfo().getName()); + return true; + } + if (id == null) { serverConn.ensureConnected().write(packet.retain()); return true; @@ -338,6 +349,12 @@ public CompletableFuture handleBackendFinishUpdate(VelocityServerConnectio smc.write(brandPacket); } + final byte[] modList = serverConn.getPlayer().getClientModList(); + if (modList != null) { + smc.write(new PluginMessagePacket(PluginMessageUtil.MOD_LIST_CHANNEL, + Unpooled.wrappedBuffer(modList))); + } + callConfigurationEvent().thenCompose(v -> { return server.getEventManager().fire(new PlayerFinishConfigurationEvent(player, serverConn)) .completeOnTimeout(null, 5, TimeUnit.SECONDS); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index f5898aceea..14ecae9aed 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -195,6 +195,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private final BundleDelimiterHandler bundleHandler = new BundleDelimiterHandler(this); private @Nullable String clientBrand; + private byte @Nullable [] clientModList; // 26.4+, minecraft:mod_list payload private @Nullable Locale effectiveLocale; private final @Nullable IdentifiedKey playerKey; private @Nullable ClientSettingsPacket clientSettingsPacket; @@ -1028,6 +1029,20 @@ void setClientBrand(final @Nullable String clientBrand) { this.clientBrand = clientBrand; } + /** + * Returns the raw {@code minecraft:mod_list} payload the client sent after login (26.4+). + * The client only sends it once; its is replayed to every backend we connect to. + * + * @return the payload, or {@code null} if the client didn't send one + */ + byte @Nullable [] getClientModList() { + return clientModList; + } + + void setClientModList(final byte @Nullable [] clientModList) { + this.clientModList = clientModList; + } + @Override public void playSound(@NotNull Sound sound, @NotNull Sound.Emitter emitter) { Preconditions.checkNotNull(sound, "sound"); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index 5515edbd85..8ed9a6d743 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -215,6 +215,13 @@ static String cleanVhost(String hostname) { cleaned = hostname.substring(0, zeroIdx); } + // Minecraft 26.4+ can append query properties to the host ("host?_o=original:port&_id=..."). + // A '?' can never be part of a hostname, so strip them before the forced host lookup. + int queryIdx = cleaned.indexOf('?'); + if (queryIdx > -1) { + cleaned = cleaned.substring(0, queryIdx); + } + // If we connect through an SRV record, there will be a period at the end (DNS usually elides // this ending octet). if (!cleaned.isEmpty() && cleaned.charAt(cleaned.length() - 1) == '.') { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java index 6b63a6f8e2..b1190df8e7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java @@ -36,10 +36,13 @@ import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; +import com.velocitypowered.proxy.protocol.packet.ClientboundApplyOptionsPacket; +import com.velocitypowered.proxy.protocol.packet.ConnectionOptions; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundOptionsAppliedPacket; import com.velocitypowered.proxy.util.VelocityProperties; import io.netty.buffer.ByteBuf; import java.net.InetSocketAddress; @@ -58,6 +61,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Handles authenticating the player to Mojang's servers. @@ -77,6 +81,9 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { private @MonotonicNonNull ServerLoginPacket login; private byte[] verify = EMPTY_BYTE_ARRAY; private LoginState currentState = LoginState.LOGIN_PACKET_EXPECTED; + // 26.4+: encryption waits until the client confirms ApplyOptions. + private byte @Nullable [] pendingSharedSecret; + private @Nullable AuthSessionHandler pendingAuthSessionHandler; private final boolean forceKeyAuthentication; InitialLoginSessionHandler(VelocityServer server, MinecraftConnection mcConnection, @@ -201,9 +208,14 @@ public boolean handle(EncryptionResponsePacket packet) { byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret()); - // Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption - // is enabled. - mcConnection.enableEncryption(decryptedSharedSecret); + if (mcConnection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_4)) { + // 26.4+ clients keep talking in plaintext until we send ApplyOptions after authenticating. + pendingSharedSecret = decryptedSharedSecret; + } else { + // Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption + // is enabled. + mcConnection.enableEncryption(decryptedSharedSecret); + } String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic()); String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString(); @@ -247,8 +259,16 @@ public boolean handle(EncryptionResponsePacket packet) { } } // All went well, initialize the session. - mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, - new AuthSessionHandler(server, inbound, profile, true, serverId)); + final AuthSessionHandler authSessionHandler = + new AuthSessionHandler(server, inbound, profile, true, serverId); + if (pendingSharedSecret != null) { + // 26.4+: ask the client to switch encryption on and wait for its confirmation. + pendingAuthSessionHandler = authSessionHandler; + currentState = LoginState.APPLY_OPTIONS_SENT; + mcConnection.write(new ClientboundApplyOptionsPacket(ConnectionOptions.ENCRYPTION)); + } else { + mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, authSessionHandler); + } } else if (response.statusCode() == 204) { // Apparently an offline-mode user logged onto this online-mode proxy. inbound.disconnect( @@ -271,6 +291,31 @@ public boolean handle(EncryptionResponsePacket packet) { return true; } + @Override + public boolean handle(ServerboundOptionsAppliedPacket packet) { + assertState(LoginState.APPLY_OPTIONS_SENT); + byte[] sharedSecret = pendingSharedSecret; + AuthSessionHandler authSessionHandler = pendingAuthSessionHandler; + if (mcConnection.isClosed() || sharedSecret == null || authSessionHandler == null) { + return true; + } + this.currentState = LoginState.OPTIONS_APPLIED; + this.pendingSharedSecret = null; + this.pendingAuthSessionHandler = null; + + try { + // OptionsApplied was the client's last plaintext packet. The client sends nothing else + // until we answer, so no encrypted bytes can be buffered behind it yet. + mcConnection.enableEncryption(sharedSecret); + } catch (GeneralSecurityException e) { + logger.error("Unable to enable encryption", e); + mcConnection.close(true); + return true; + } + mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, authSessionHandler); + return true; + } + private EncryptionRequestPacket generateEncryptionRequest() { byte[] verify = new byte[4]; SECURE_RANDOM.nextBytes(verify); @@ -306,6 +351,8 @@ private enum LoginState { LOGIN_PACKET_EXPECTED, LOGIN_PACKET_RECEIVED, ENCRYPTION_REQUEST_SENT, - ENCRYPTION_RESPONSE_RECEIVED + ENCRYPTION_RESPONSE_RECEIVED, + APPLY_OPTIONS_SENT, // 26.4+ + OPTIONS_APPLIED // 26.4+ } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 1da9c424a0..568032ef81 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -48,6 +48,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9_4; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_1; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_3; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_4; import static com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION; import static com.velocitypowered.api.network.ProtocolVersion.SUPPORTED_VERSIONS; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; @@ -60,6 +61,7 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundApplyOptionsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundPostEffectsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket; @@ -90,6 +92,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundOptionsAppliedPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; @@ -902,6 +905,9 @@ public enum StateRegistry { serverbound.register( ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new, map(0x04, MINECRAFT_1_20_5, false)); + serverbound.register( + ServerboundOptionsAppliedPacket.class, () -> ServerboundOptionsAppliedPacket.INSTANCE, + map(0x05, MINECRAFT_26_4, false)); clientbound.register( DisconnectPacket.class, () -> new DisconnectPacket(this), @@ -922,6 +928,9 @@ public enum StateRegistry { clientbound.register( ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new, map(0x05, MINECRAFT_1_20_5, false)); + clientbound.register( + ClientboundApplyOptionsPacket.class, ClientboundApplyOptionsPacket::new, + map(0x06, MINECRAFT_26_4, false)); } }; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundApplyOptionsPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundApplyOptionsPacket.java new file mode 100644 index 0000000000..32a80e554c --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundApplyOptionsPacket.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2026 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +/** + * Asks the client to turn on connection options (26.4+). The client switches on inbound handling, + * answers with {@link ServerboundOptionsAppliedPacket} in the old mode and then switches on + * outbound handling. Vanilla only uses it for {@link ConnectionOptions#ENCRYPTION}. + */ +public class ClientboundApplyOptionsPacket implements MinecraftPacket { + + private byte options; + + public ClientboundApplyOptionsPacket() { + } + + public ClientboundApplyOptionsPacket(byte options) { + this.options = options; + } + + public byte getOptions() { + return options; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + this.options = buf.readByte(); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + buf.writeByte(this.options); + } + + @Override + public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 1; + } + + @Override + public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 1; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + @Override + public String toString() { + return "ClientboundApplyOptionsPacket{options=" + options + '}'; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ConnectionOptions.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ConnectionOptions.java new file mode 100644 index 0000000000..51c8c87f9c --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ConnectionOptions.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2026 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet; + +/** + * The connection option flags carried by {@link ServerLoginSuccessPacket} and + * {@link ClientboundApplyOptionsPacket} since Minecraft 26.4. + */ +public final class ConnectionOptions { + + public static final byte NONE = 0; + public static final byte ENCRYPTION = 1; + public static final byte COMPRESSION = 2; + + private ConnectionOptions() { + throw new AssertionError(); + } + + public static boolean isEncryption(byte options) { + return (options & ENCRYPTION) != 0; + } + + public static boolean isCompression(byte options) { + return (options & COMPRESSION) != 0; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java index 88cb3688bd..329cd25ac5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java @@ -32,6 +32,8 @@ public class HandshakePacket implements MinecraftPacket { // This size was chosen to ensure Forge clients can still connect even with very long hostnames. // While DNS technically allows any character to be used, in practice ASCII is used. private static final int MAXIMUM_HOSTNAME_LENGTH = 255 + HANDSHAKE_HOSTNAME_TOKEN.length() + 1; + // 26.4 raised the limit to 1024 (the client can append query properties to the host). + private static final int MAXIMUM_HOSTNAME_LENGTH_26_4 = 1024 + HANDSHAKE_HOSTNAME_TOKEN.length() + 1; private ProtocolVersion protocolVersion; private String serverAddress = ""; private int port; @@ -89,7 +91,9 @@ public String toString() { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion ignored) { int realProtocolVersion = ProtocolUtils.readVarInt(buf); this.protocolVersion = ProtocolVersion.getProtocolVersion(realProtocolVersion); - this.serverAddress = ProtocolUtils.readString(buf, MAXIMUM_HOSTNAME_LENGTH); + this.serverAddress = ProtocolUtils.readString(buf, + this.protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_26_4) + ? MAXIMUM_HOSTNAME_LENGTH_26_4 : MAXIMUM_HOSTNAME_LENGTH); this.port = buf.readUnsignedShort(); this.nextStatus = ProtocolUtils.readVarInt(buf); this.intent = HandshakeIntent.getById(nextStatus); @@ -117,7 +121,7 @@ public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction directio @Override public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - return 9 + (MAXIMUM_HOSTNAME_LENGTH * 3); + return 9 + (MAXIMUM_HOSTNAME_LENGTH_26_4 * 3); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java index ca777a274d..63adf46213 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java @@ -34,7 +34,7 @@ public class JoinGamePacket implements MinecraftPacket { private int entityId; private int gamemode; private int dimension; - private long partialHashedSeed; // 1.15+ + private long partialHashedSeed; // 1.15-26.3 private short difficulty; private boolean isHardcore; private int maxPlayers; @@ -343,7 +343,9 @@ private void decode1202Up(ByteBuf buf, ProtocolVersion version) { dimensionKey = ProtocolUtils.readString(buf); } String levelName = ProtocolUtils.readString(buf); - this.partialHashedSeed = buf.readLong(); + if (version.lessThan(ProtocolVersion.MINECRAFT_26_4)) { + this.partialHashedSeed = buf.readLong(); + } if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) { this.gamemode = ProtocolUtils.readVarInt(buf); @@ -501,7 +503,9 @@ private void encode1202Up(ByteBuf buf, ProtocolVersion version) { ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); } ProtocolUtils.writeString(buf, dimensionInfo.getLevelName()); - buf.writeLong(partialHashedSeed); + if (version.lessThan(ProtocolVersion.MINECRAFT_26_4)) { + buf.writeLong(partialHashedSeed); + } if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) { ProtocolUtils.writeVarInt(buf, this.gamemode); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java index d20c253f98..a13636fabf 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java @@ -31,7 +31,7 @@ public class RespawnPacket implements MinecraftPacket { private int dimension; - private long partialHashedSeed; + private long partialHashedSeed; // 1.15-26.3 private short difficulty; private int gamemode; private String levelType = ""; @@ -194,7 +194,8 @@ public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_13_2)) { this.difficulty = buf.readUnsignedByte(); } - if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) { + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15) + && version.lessThan(ProtocolVersion.MINECRAFT_26_4)) { this.partialHashedSeed = buf.readLong(); } if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) { @@ -254,7 +255,8 @@ public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_13_2)) { buf.writeByte(difficulty); } - if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) { + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15) + && version.lessThan(ProtocolVersion.MINECRAFT_26_4)) { buf.writeLong(partialHashedSeed); } if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccessPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccessPacket.java index 4b8e722a5b..2740906be0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccessPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccessPacket.java @@ -36,6 +36,7 @@ public class ServerLoginSuccessPacket implements MinecraftPacket { private @Nullable String username; private @Nullable List properties; private @Nullable UUID sessionId; + private byte options = ConnectionOptions.NONE; // 26.4+ private static final boolean strictErrorHandling = VelocityProperties .readBoolean("velocity.strictErrorHandling", true); @@ -73,12 +74,26 @@ public void setSessionId(@Nullable UUID sessionId) { this.sessionId = sessionId; } + /** + * Returns the connection options to switch on once the login is acknowledged (26.4+). + * + * @return the {@link ConnectionOptions} flags + */ + public byte getOptions() { + return options; + } + + public void setOptions(byte options) { + this.options = options; + } + @Override public String toString() { return "ServerLoginSuccess{" + "uuid=" + uuid + ", username='" + username + '\'' + ", properties='" + properties + '\'' + + ", options=" + options + '}'; } @@ -105,6 +120,10 @@ public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) { this.sessionId = ProtocolUtils.readUuid(buf); } + + if (version.noLessThan(ProtocolVersion.MINECRAFT_26_4)) { + this.options = buf.readByte(); + } } @Override @@ -140,6 +159,10 @@ public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) { ProtocolUtils.writeUuid(buf, this.sessionId); } + + if (version.noLessThan(ProtocolVersion.MINECRAFT_26_4)) { + buf.writeByte(this.options); + } } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundOptionsAppliedPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundOptionsAppliedPacket.java new file mode 100644 index 0000000000..61a32b509b --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundOptionsAppliedPacket.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2026 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +/** + * The client's answer to {@link ClientboundApplyOptionsPacket} (26.4+). It is still sent without + * the new options; everything after it uses them. + */ +public final class ServerboundOptionsAppliedPacket implements MinecraftPacket { + + public static final ServerboundOptionsAppliedPacket INSTANCE = + new ServerboundOptionsAppliedPacket(); + + private ServerboundOptionsAppliedPacket() { + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 0; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TransferPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TransferPacket.java index b5a74e49f2..ed8bf1a204 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TransferPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TransferPacket.java @@ -22,19 +22,40 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; +import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame; + +import com.google.common.collect.ImmutableMap; import java.net.InetSocketAddress; +import java.util.LinkedHashMap; +import java.util.Map; import org.jetbrains.annotations.Nullable; public class TransferPacket implements MinecraftPacket { private String host; private int port; + private Map properties = Map.of(); // 26.4+ public TransferPacket() { } public TransferPacket(final String host, final int port) { + this(host, port, Map.of()); + } + + public TransferPacket(final String host, final int port, final Map properties) { this.host = host; this.port = port; + this.properties = ImmutableMap.copyOf(properties); + } + + /** + * Returns the properties the client appends to the handshake host of its next connection + * (26.4+, empty for older clients). + * + * @return the transfer properties + */ + public Map properties() { + return properties; } @Nullable @@ -49,12 +70,31 @@ public InetSocketAddress address() { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { this.host = ProtocolUtils.readString(buf); this.port = ProtocolUtils.readVarInt(buf); + if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_26_4)) { + int size = ProtocolUtils.readVarInt(buf); + checkFrame(size >= 0 && size <= buf.readableBytes(), + "Bad transfer property count (%s)", size); + Map properties = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + properties.put(ProtocolUtils.readString(buf), ProtocolUtils.readString(buf)); + } + this.properties = ImmutableMap.copyOf(properties); + } else { + this.properties = Map.of(); + } } @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { ProtocolUtils.writeString(buf, host); ProtocolUtils.writeVarInt(buf, port); + if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_26_4)) { + ProtocolUtils.writeVarInt(buf, properties.size()); + for (Map.Entry property : properties.entrySet()) { + ProtocolUtils.writeString(buf, property.getKey()); + ProtocolUtils.writeString(buf, property.getValue()); + } + } } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java index 298e74d4f2..c18b7c78ef 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java @@ -45,6 +45,7 @@ */ public final class PluginMessageUtil { + public static final String MOD_LIST_CHANNEL = "minecraft:mod_list"; private static final String BRAND_CHANNEL_LEGACY = "MC|Brand"; private static final String BRAND_CHANNEL = "minecraft:brand"; private static final String REGISTER_CHANNEL_LEGACY = "REGISTER"; @@ -56,6 +57,17 @@ private PluginMessageUtil() { throw new AssertionError(); } + /** + * Determines whether this plugin message is the client's mod list (Minecraft 26.4+). + * + * @param message the plugin message + * @return whether this is a {@code minecraft:mod_list} message + */ + public static boolean isModList(PluginMessagePacket message) { + checkNotNull(message, "message"); + return message.getChannel().equals(MOD_LIST_CHANNEL); + } + /** * Determines whether or not this is a brand plugin message. This is shown on the client. * diff --git a/proxy/src/test/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandlerTest.java b/proxy/src/test/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandlerTest.java index 73ab243648..4710195308 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandlerTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandlerTest.java @@ -49,6 +49,13 @@ void cleanVhostHandlesOctetsAndForge() { assertEquals("mc.example.com", cleanVhost("mc.example.com." + HANDSHAKE_HOSTNAME_TOKEN)); } + @Test + void cleanVhostHandlesQueryProperties() { + assertEquals("mc.example.com", cleanVhost("mc.example.com?_o=play.example.com%3A25565")); + assertEquals("mc.example.com", cleanVhost("mc.example.com.?_o=play.example.com%3A25565&_id=a")); + assertEquals("mc.example.com", cleanVhost("mc.example.com?k=v" + HANDSHAKE_HOSTNAME_TOKEN)); + } + @Test void cleanVhostHandlesEmptyHostnames() { assertEquals("", cleanVhost("")); diff --git a/proxy/src/test/java/com/velocitypowered/proxy/protocol/packet/TransferPacketTest.java b/proxy/src/test/java/com/velocitypowered/proxy/protocol/packet/TransferPacketTest.java new file mode 100644 index 0000000000..6f8dadc653 --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/protocol/packet/TransferPacketTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2026 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class TransferPacketTest { + + private static TransferPacket roundTrip(TransferPacket packet, ProtocolVersion version) { + ByteBuf buf = Unpooled.buffer(); + try { + packet.encode(buf, ProtocolUtils.Direction.CLIENTBOUND, version); + TransferPacket decoded = new TransferPacket(); + decoded.decode(buf, ProtocolUtils.Direction.CLIENTBOUND, version); + assertFalse(buf.isReadable(), "trailing bytes"); + return decoded; + } finally { + buf.release(); + } + } + + @Test + void keepsPropertiesOn264() { + TransferPacket decoded = roundTrip( + new TransferPacket("mc.example.com", 25565, Map.of("token", "abc", "empty", "")), + ProtocolVersion.MINECRAFT_26_4); + assertEquals("mc.example.com", decoded.address().getHostString()); + assertEquals(25565, decoded.address().getPort()); + assertEquals(Map.of("token", "abc", "empty", ""), decoded.properties()); + } + + @Test + void dropsPropertiesBefore264() { + TransferPacket decoded = roundTrip( + new TransferPacket("mc.example.com", 25565, Map.of("token", "abc")), + ProtocolVersion.MINECRAFT_26_3); + assertEquals(Map.of(), decoded.properties()); + } +} From 77274a8fb40c316a3c4096b5bb0d8a309a907fbd Mon Sep 17 00:00:00 2001 From: Wouter Gritter Date: Fri, 23 Oct 2026 20:31:41 +0200 Subject: [PATCH 2/2] Revert "Minecraft 26.4-snapshot-5" This reverts commit c029979932963fea001dd30ec411e6dd8ef181cc. --- .../api/network/ProtocolVersion.java | 3 +- .../connection/MinecraftSessionHandler.java | 10 --- .../backend/BackendPlaySessionHandler.java | 3 +- .../backend/ConfigSessionHandler.java | 3 +- .../backend/LoginSessionHandler.java | 20 +---- .../connection/client/AuthSessionHandler.java | 20 +---- .../client/ClientConfigSessionHandler.java | 17 ---- .../connection/client/ConnectedPlayer.java | 15 ---- .../client/HandshakeSessionHandler.java | 7 -- .../client/InitialLoginSessionHandler.java | 59 ++------------ .../proxy/protocol/StateRegistry.java | 9 --- .../packet/ClientboundApplyOptionsPacket.java | 79 ------------------- .../protocol/packet/ConnectionOptions.java | 41 ---------- .../protocol/packet/HandshakePacket.java | 8 +- .../proxy/protocol/packet/JoinGamePacket.java | 10 +-- .../proxy/protocol/packet/RespawnPacket.java | 8 +- .../packet/ServerLoginSuccessPacket.java | 23 ------ .../ServerboundOptionsAppliedPacket.java | 58 -------------- .../proxy/protocol/packet/TransferPacket.java | 40 ---------- .../protocol/util/PluginMessageUtil.java | 12 --- .../client/HandshakeSessionHandlerTest.java | 7 -- .../protocol/packet/TransferPacketTest.java | 62 --------------- 22 files changed, 19 insertions(+), 495 deletions(-) delete mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundApplyOptionsPacket.java delete mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ConnectionOptions.java delete mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundOptionsAppliedPacket.java delete mode 100644 proxy/src/test/java/com/velocitypowered/proxy/protocol/packet/TransferPacketTest.java diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index e5d823fea1..6653ff9c1a 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -97,8 +97,7 @@ public boolean isSupported() { MINECRAFT_1_21_11(774, "1.21.11"), MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2"), MINECRAFT_26_2(776, "26.2"), - MINECRAFT_26_3(777, "26.3"), - MINECRAFT_26_4(-1, 343, "26.4"); // TODO + MINECRAFT_26_3(777, "26.3"); private static final int SNAPSHOT_BIT = 30; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index 99e25a8946..9df71c3f7a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -22,7 +22,6 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; -import com.velocitypowered.proxy.protocol.packet.ClientboundApplyOptionsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundPostEffectsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket; @@ -55,7 +54,6 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; -import com.velocitypowered.proxy.protocol.packet.ServerboundOptionsAppliedPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; @@ -411,12 +409,4 @@ default boolean handle(ClientboundStopSoundPacket packet) { default boolean handle(ClientboundPostEffectsPacket packet) { return false; } - - default boolean handle(ClientboundApplyOptionsPacket packet) { - return false; - } - - default boolean handle(ServerboundOptionsAppliedPacket packet) { - return false; - } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index 462905ff09..37a61d357d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -421,8 +421,7 @@ public boolean handle(TransferPacket packet) { resultedAddress = originalAddress; } this.playerConnection.write(new TransferPacket( - resultedAddress.getHostName(), resultedAddress.getPort(), - packet.properties())); + resultedAddress.getHostName(), resultedAddress.getPort())); } }, playerConnection.eventLoop()); return true; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java index bda5e3977c..904e848f3a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -328,8 +328,7 @@ public boolean handle(TransferPacket packet) { resultedAddress = originalAddress; } serverConn.getPlayer().getConnection().write(new TransferPacket( - resultedAddress.getHostName(), resultedAddress.getPort(), - packet.properties())); + resultedAddress.getHostName(), resultedAddress.getPort())); } }, serverConn.ensureConnected().eventLoop()); return true; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index 35a1cc05c9..14884af467 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -35,7 +35,6 @@ import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; -import com.velocitypowered.proxy.protocol.packet.ConnectionOptions; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.LoginAcknowledgedPacket; @@ -67,7 +66,6 @@ public class LoginSessionHandler implements MinecraftSessionHandler { private final VelocityServerConnection serverConn; private final CompletableFuture resultFuture; private boolean informationForwarded; - private int pendingCompressionThreshold = -1; // 26.4+ LoginSessionHandler(VelocityServer server, VelocityServerConnection serverConn, CompletableFuture resultFuture) { @@ -139,13 +137,7 @@ public boolean handle(DisconnectPacket packet) { @Override public boolean handle(SetCompressionPacket packet) { - MinecraftConnection smc = serverConn.ensureConnected(); - if (smc.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_4)) { - // Only announces the threshold; LoginSuccess says whether to switch it on. - pendingCompressionThreshold = packet.getThreshold(); - } else { - smc.setCompressionThreshold(packet.getThreshold()); - } + serverConn.ensureConnected().setCompressionThreshold(packet.getThreshold()); return true; } @@ -166,16 +158,6 @@ public boolean handle(ServerLoginSuccessPacket packet) { smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture)); } else { smc.write(new LoginAcknowledgedPacket()); - if (smc.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_4) - && ConnectionOptions.isCompression(packet.getOptions())) { - if (pendingCompressionThreshold < 0) { - throw new QuietRuntimeException("Backend server asked for compression in LoginSuccess" - + " without sending a compression threshold first"); - } - // LoginAcknowledged has been encoded uncompressed above (we are on the event loop); the - // server only replies once it has received it, so both directions switch over here. - smc.setCompressionThreshold(pendingCompressionThreshold); - } smc.setActiveSessionHandler(StateRegistry.CONFIG, new ConfigSessionHandler(server, serverConn, resultFuture)); ConnectedPlayer player = serverConn.getPlayer(); if (player.getClientSettingsPacket() != null) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index 7fbfb61c7e..fee642b495 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -18,7 +18,6 @@ package com.velocitypowered.proxy.connection.client; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_4; import com.google.common.base.Preconditions; import com.velocitypowered.api.event.connection.DisconnectEvent; @@ -41,7 +40,6 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.StateRegistry; -import com.velocitypowered.proxy.protocol.packet.ConnectionOptions; import com.velocitypowered.proxy.protocol.packet.LoginAcknowledgedPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; @@ -72,9 +70,6 @@ public class AuthSessionHandler implements MinecraftSessionHandler { private final boolean onlineMode; private State loginState = State.START; // 1.20.2+ private final String serverIdHash; - // 26.4+: SetCompression only announces the threshold; the client switches compression on - // once LoginSuccess (with the compression option) is acknowledged. - private int pendingCompressionThreshold = -1; AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound, GameProfile profile, boolean onlineMode, String serverIdHash) { @@ -144,11 +139,7 @@ private void startLoginCompletion(ConnectedPlayer player) { int threshold = server.getConfiguration().getCompressionThreshold(); if (threshold >= 0 && mcConnection.getProtocolVersion().noLessThan(MINECRAFT_1_8)) { mcConnection.write(new SetCompressionPacket(threshold)); - if (mcConnection.getProtocolVersion().noLessThan(MINECRAFT_26_4)) { - pendingCompressionThreshold = threshold; - } else { - mcConnection.setCompressionThreshold(threshold); - } + mcConnection.setCompressionThreshold(threshold); } VelocityConfiguration configuration = server.getConfiguration(); UUID playerUniqueId = player.getUniqueId(); @@ -191,12 +182,6 @@ public boolean handle(LoginAcknowledgedPacket packet) { inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data")); } else { loginState = State.ACKNOWLEDGED; - if (pendingCompressionThreshold >= 0) { - // LoginAcknowledged was the last uncompressed packet in both directions. We are still - // inside the frame decoder's read loop, so the next frame already goes through the - // decompressor. - mcConnection.setCompressionThreshold(pendingCompressionThreshold); - } mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, new ClientConfigSessionHandler(server, connectedPlayer)); server.getEventManager().fire(new PostLoginEvent(connectedPlayer)).thenCompose(ignored -> { @@ -254,9 +239,6 @@ private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { if (inbound.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_2)) { success.setSessionId(server.getSessionId()); } - if (pendingCompressionThreshold >= 0) { - success.setOptions(ConnectionOptions.COMPRESSION); - } mcConnection.write(success); loginState = State.SUCCESS_SENT; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java index 6715d4cf17..b86041035b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -133,22 +133,11 @@ public boolean handle(final PluginMessagePacket packet) { brandChannel = packet.getChannel(); // Client sends `minecraft:brand` packet immediately after Login, // but at this time the backend server may not be ready - } else if (PluginMessageUtil.isModList(packet)) { - // 26.4+ clients send `minecraft:mod_list` right after the brand, once per session. - player.setClientModList(ByteBufUtil.getBytes(packet.content())); } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { return true; } else if (serverConn != null) { ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); - if (serverConn.getConnection() == null) { - // The backend connection is still being set up; there is nowhere to send this yet. - // TODO: Do we queue this instead? - logger.warn("Dropping plugin message on {} from {}: backend connection to {} not ready", - packet.getChannel(), player, serverConn.getServerInfo().getName()); - return true; - } - if (id == null) { serverConn.ensureConnected().write(packet.retain()); return true; @@ -349,12 +338,6 @@ public CompletableFuture handleBackendFinishUpdate(VelocityServerConnectio smc.write(brandPacket); } - final byte[] modList = serverConn.getPlayer().getClientModList(); - if (modList != null) { - smc.write(new PluginMessagePacket(PluginMessageUtil.MOD_LIST_CHANNEL, - Unpooled.wrappedBuffer(modList))); - } - callConfigurationEvent().thenCompose(v -> { return server.getEventManager().fire(new PlayerFinishConfigurationEvent(player, serverConn)) .completeOnTimeout(null, 5, TimeUnit.SECONDS); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 14ecae9aed..f5898aceea 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -195,7 +195,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private final BundleDelimiterHandler bundleHandler = new BundleDelimiterHandler(this); private @Nullable String clientBrand; - private byte @Nullable [] clientModList; // 26.4+, minecraft:mod_list payload private @Nullable Locale effectiveLocale; private final @Nullable IdentifiedKey playerKey; private @Nullable ClientSettingsPacket clientSettingsPacket; @@ -1029,20 +1028,6 @@ void setClientBrand(final @Nullable String clientBrand) { this.clientBrand = clientBrand; } - /** - * Returns the raw {@code minecraft:mod_list} payload the client sent after login (26.4+). - * The client only sends it once; its is replayed to every backend we connect to. - * - * @return the payload, or {@code null} if the client didn't send one - */ - byte @Nullable [] getClientModList() { - return clientModList; - } - - void setClientModList(final byte @Nullable [] clientModList) { - this.clientModList = clientModList; - } - @Override public void playSound(@NotNull Sound sound, @NotNull Sound.Emitter emitter) { Preconditions.checkNotNull(sound, "sound"); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index 8ed9a6d743..5515edbd85 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -215,13 +215,6 @@ static String cleanVhost(String hostname) { cleaned = hostname.substring(0, zeroIdx); } - // Minecraft 26.4+ can append query properties to the host ("host?_o=original:port&_id=..."). - // A '?' can never be part of a hostname, so strip them before the forced host lookup. - int queryIdx = cleaned.indexOf('?'); - if (queryIdx > -1) { - cleaned = cleaned.substring(0, queryIdx); - } - // If we connect through an SRV record, there will be a period at the end (DNS usually elides // this ending octet). if (!cleaned.isEmpty() && cleaned.charAt(cleaned.length() - 1) == '.') { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java index b1190df8e7..6b63a6f8e2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java @@ -36,13 +36,10 @@ import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; -import com.velocitypowered.proxy.protocol.packet.ClientboundApplyOptionsPacket; -import com.velocitypowered.proxy.protocol.packet.ConnectionOptions; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket; -import com.velocitypowered.proxy.protocol.packet.ServerboundOptionsAppliedPacket; import com.velocitypowered.proxy.util.VelocityProperties; import io.netty.buffer.ByteBuf; import java.net.InetSocketAddress; @@ -61,7 +58,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.Nullable; /** * Handles authenticating the player to Mojang's servers. @@ -81,9 +77,6 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { private @MonotonicNonNull ServerLoginPacket login; private byte[] verify = EMPTY_BYTE_ARRAY; private LoginState currentState = LoginState.LOGIN_PACKET_EXPECTED; - // 26.4+: encryption waits until the client confirms ApplyOptions. - private byte @Nullable [] pendingSharedSecret; - private @Nullable AuthSessionHandler pendingAuthSessionHandler; private final boolean forceKeyAuthentication; InitialLoginSessionHandler(VelocityServer server, MinecraftConnection mcConnection, @@ -208,14 +201,9 @@ public boolean handle(EncryptionResponsePacket packet) { byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret()); - if (mcConnection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_4)) { - // 26.4+ clients keep talking in plaintext until we send ApplyOptions after authenticating. - pendingSharedSecret = decryptedSharedSecret; - } else { - // Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption - // is enabled. - mcConnection.enableEncryption(decryptedSharedSecret); - } + // Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption + // is enabled. + mcConnection.enableEncryption(decryptedSharedSecret); String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic()); String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString(); @@ -259,16 +247,8 @@ public boolean handle(EncryptionResponsePacket packet) { } } // All went well, initialize the session. - final AuthSessionHandler authSessionHandler = - new AuthSessionHandler(server, inbound, profile, true, serverId); - if (pendingSharedSecret != null) { - // 26.4+: ask the client to switch encryption on and wait for its confirmation. - pendingAuthSessionHandler = authSessionHandler; - currentState = LoginState.APPLY_OPTIONS_SENT; - mcConnection.write(new ClientboundApplyOptionsPacket(ConnectionOptions.ENCRYPTION)); - } else { - mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, authSessionHandler); - } + mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, + new AuthSessionHandler(server, inbound, profile, true, serverId)); } else if (response.statusCode() == 204) { // Apparently an offline-mode user logged onto this online-mode proxy. inbound.disconnect( @@ -291,31 +271,6 @@ public boolean handle(EncryptionResponsePacket packet) { return true; } - @Override - public boolean handle(ServerboundOptionsAppliedPacket packet) { - assertState(LoginState.APPLY_OPTIONS_SENT); - byte[] sharedSecret = pendingSharedSecret; - AuthSessionHandler authSessionHandler = pendingAuthSessionHandler; - if (mcConnection.isClosed() || sharedSecret == null || authSessionHandler == null) { - return true; - } - this.currentState = LoginState.OPTIONS_APPLIED; - this.pendingSharedSecret = null; - this.pendingAuthSessionHandler = null; - - try { - // OptionsApplied was the client's last plaintext packet. The client sends nothing else - // until we answer, so no encrypted bytes can be buffered behind it yet. - mcConnection.enableEncryption(sharedSecret); - } catch (GeneralSecurityException e) { - logger.error("Unable to enable encryption", e); - mcConnection.close(true); - return true; - } - mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, authSessionHandler); - return true; - } - private EncryptionRequestPacket generateEncryptionRequest() { byte[] verify = new byte[4]; SECURE_RANDOM.nextBytes(verify); @@ -351,8 +306,6 @@ private enum LoginState { LOGIN_PACKET_EXPECTED, LOGIN_PACKET_RECEIVED, ENCRYPTION_REQUEST_SENT, - ENCRYPTION_RESPONSE_RECEIVED, - APPLY_OPTIONS_SENT, // 26.4+ - OPTIONS_APPLIED // 26.4+ + ENCRYPTION_RESPONSE_RECEIVED } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 568032ef81..1da9c424a0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -48,7 +48,6 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9_4; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_1; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_3; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_4; import static com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION; import static com.velocitypowered.api.network.ProtocolVersion.SUPPORTED_VERSIONS; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; @@ -61,7 +60,6 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; -import com.velocitypowered.proxy.protocol.packet.ClientboundApplyOptionsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundPostEffectsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket; @@ -92,7 +90,6 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; -import com.velocitypowered.proxy.protocol.packet.ServerboundOptionsAppliedPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; @@ -905,9 +902,6 @@ public enum StateRegistry { serverbound.register( ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new, map(0x04, MINECRAFT_1_20_5, false)); - serverbound.register( - ServerboundOptionsAppliedPacket.class, () -> ServerboundOptionsAppliedPacket.INSTANCE, - map(0x05, MINECRAFT_26_4, false)); clientbound.register( DisconnectPacket.class, () -> new DisconnectPacket(this), @@ -928,9 +922,6 @@ public enum StateRegistry { clientbound.register( ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new, map(0x05, MINECRAFT_1_20_5, false)); - clientbound.register( - ClientboundApplyOptionsPacket.class, ClientboundApplyOptionsPacket::new, - map(0x06, MINECRAFT_26_4, false)); } }; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundApplyOptionsPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundApplyOptionsPacket.java deleted file mode 100644 index 32a80e554c..0000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundApplyOptionsPacket.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2026 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.protocol.packet; - -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; - -/** - * Asks the client to turn on connection options (26.4+). The client switches on inbound handling, - * answers with {@link ServerboundOptionsAppliedPacket} in the old mode and then switches on - * outbound handling. Vanilla only uses it for {@link ConnectionOptions#ENCRYPTION}. - */ -public class ClientboundApplyOptionsPacket implements MinecraftPacket { - - private byte options; - - public ClientboundApplyOptionsPacket() { - } - - public ClientboundApplyOptionsPacket(byte options) { - this.options = options; - } - - public byte getOptions() { - return options; - } - - @Override - public void decode(ByteBuf buf, ProtocolUtils.Direction direction, - ProtocolVersion protocolVersion) { - this.options = buf.readByte(); - } - - @Override - public void encode(ByteBuf buf, ProtocolUtils.Direction direction, - ProtocolVersion protocolVersion) { - buf.writeByte(this.options); - } - - @Override - public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, - ProtocolVersion version) { - return 1; - } - - @Override - public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, - ProtocolVersion version) { - return 1; - } - - @Override - public boolean handle(MinecraftSessionHandler handler) { - return handler.handle(this); - } - - @Override - public String toString() { - return "ClientboundApplyOptionsPacket{options=" + options + '}'; - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ConnectionOptions.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ConnectionOptions.java deleted file mode 100644 index 51c8c87f9c..0000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ConnectionOptions.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2026 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.protocol.packet; - -/** - * The connection option flags carried by {@link ServerLoginSuccessPacket} and - * {@link ClientboundApplyOptionsPacket} since Minecraft 26.4. - */ -public final class ConnectionOptions { - - public static final byte NONE = 0; - public static final byte ENCRYPTION = 1; - public static final byte COMPRESSION = 2; - - private ConnectionOptions() { - throw new AssertionError(); - } - - public static boolean isEncryption(byte options) { - return (options & ENCRYPTION) != 0; - } - - public static boolean isCompression(byte options) { - return (options & COMPRESSION) != 0; - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java index 329cd25ac5..88cb3688bd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HandshakePacket.java @@ -32,8 +32,6 @@ public class HandshakePacket implements MinecraftPacket { // This size was chosen to ensure Forge clients can still connect even with very long hostnames. // While DNS technically allows any character to be used, in practice ASCII is used. private static final int MAXIMUM_HOSTNAME_LENGTH = 255 + HANDSHAKE_HOSTNAME_TOKEN.length() + 1; - // 26.4 raised the limit to 1024 (the client can append query properties to the host). - private static final int MAXIMUM_HOSTNAME_LENGTH_26_4 = 1024 + HANDSHAKE_HOSTNAME_TOKEN.length() + 1; private ProtocolVersion protocolVersion; private String serverAddress = ""; private int port; @@ -91,9 +89,7 @@ public String toString() { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion ignored) { int realProtocolVersion = ProtocolUtils.readVarInt(buf); this.protocolVersion = ProtocolVersion.getProtocolVersion(realProtocolVersion); - this.serverAddress = ProtocolUtils.readString(buf, - this.protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_26_4) - ? MAXIMUM_HOSTNAME_LENGTH_26_4 : MAXIMUM_HOSTNAME_LENGTH); + this.serverAddress = ProtocolUtils.readString(buf, MAXIMUM_HOSTNAME_LENGTH); this.port = buf.readUnsignedShort(); this.nextStatus = ProtocolUtils.readVarInt(buf); this.intent = HandshakeIntent.getById(nextStatus); @@ -121,7 +117,7 @@ public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction directio @Override public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - return 9 + (MAXIMUM_HOSTNAME_LENGTH_26_4 * 3); + return 9 + (MAXIMUM_HOSTNAME_LENGTH * 3); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java index 63adf46213..ca777a274d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java @@ -34,7 +34,7 @@ public class JoinGamePacket implements MinecraftPacket { private int entityId; private int gamemode; private int dimension; - private long partialHashedSeed; // 1.15-26.3 + private long partialHashedSeed; // 1.15+ private short difficulty; private boolean isHardcore; private int maxPlayers; @@ -343,9 +343,7 @@ private void decode1202Up(ByteBuf buf, ProtocolVersion version) { dimensionKey = ProtocolUtils.readString(buf); } String levelName = ProtocolUtils.readString(buf); - if (version.lessThan(ProtocolVersion.MINECRAFT_26_4)) { - this.partialHashedSeed = buf.readLong(); - } + this.partialHashedSeed = buf.readLong(); if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) { this.gamemode = ProtocolUtils.readVarInt(buf); @@ -503,9 +501,7 @@ private void encode1202Up(ByteBuf buf, ProtocolVersion version) { ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); } ProtocolUtils.writeString(buf, dimensionInfo.getLevelName()); - if (version.lessThan(ProtocolVersion.MINECRAFT_26_4)) { - buf.writeLong(partialHashedSeed); - } + buf.writeLong(partialHashedSeed); if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) { ProtocolUtils.writeVarInt(buf, this.gamemode); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java index a13636fabf..d20c253f98 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java @@ -31,7 +31,7 @@ public class RespawnPacket implements MinecraftPacket { private int dimension; - private long partialHashedSeed; // 1.15-26.3 + private long partialHashedSeed; private short difficulty; private int gamemode; private String levelType = ""; @@ -194,8 +194,7 @@ public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_13_2)) { this.difficulty = buf.readUnsignedByte(); } - if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15) - && version.lessThan(ProtocolVersion.MINECRAFT_26_4)) { + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) { this.partialHashedSeed = buf.readLong(); } if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) { @@ -255,8 +254,7 @@ public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_13_2)) { buf.writeByte(difficulty); } - if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15) - && version.lessThan(ProtocolVersion.MINECRAFT_26_4)) { + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) { buf.writeLong(partialHashedSeed); } if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccessPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccessPacket.java index 2740906be0..4b8e722a5b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccessPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLoginSuccessPacket.java @@ -36,7 +36,6 @@ public class ServerLoginSuccessPacket implements MinecraftPacket { private @Nullable String username; private @Nullable List properties; private @Nullable UUID sessionId; - private byte options = ConnectionOptions.NONE; // 26.4+ private static final boolean strictErrorHandling = VelocityProperties .readBoolean("velocity.strictErrorHandling", true); @@ -74,26 +73,12 @@ public void setSessionId(@Nullable UUID sessionId) { this.sessionId = sessionId; } - /** - * Returns the connection options to switch on once the login is acknowledged (26.4+). - * - * @return the {@link ConnectionOptions} flags - */ - public byte getOptions() { - return options; - } - - public void setOptions(byte options) { - this.options = options; - } - @Override public String toString() { return "ServerLoginSuccess{" + "uuid=" + uuid + ", username='" + username + '\'' + ", properties='" + properties + '\'' - + ", options=" + options + '}'; } @@ -120,10 +105,6 @@ public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) { this.sessionId = ProtocolUtils.readUuid(buf); } - - if (version.noLessThan(ProtocolVersion.MINECRAFT_26_4)) { - this.options = buf.readByte(); - } } @Override @@ -159,10 +140,6 @@ public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) { ProtocolUtils.writeUuid(buf, this.sessionId); } - - if (version.noLessThan(ProtocolVersion.MINECRAFT_26_4)) { - buf.writeByte(this.options); - } } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundOptionsAppliedPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundOptionsAppliedPacket.java deleted file mode 100644 index 61a32b509b..0000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundOptionsAppliedPacket.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2026 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.protocol.packet; - -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; - -/** - * The client's answer to {@link ClientboundApplyOptionsPacket} (26.4+). It is still sent without - * the new options; everything after it uses them. - */ -public final class ServerboundOptionsAppliedPacket implements MinecraftPacket { - - public static final ServerboundOptionsAppliedPacket INSTANCE = - new ServerboundOptionsAppliedPacket(); - - private ServerboundOptionsAppliedPacket() { - } - - @Override - public void decode(ByteBuf buf, ProtocolUtils.Direction direction, - ProtocolVersion protocolVersion) { - } - - @Override - public void encode(ByteBuf buf, ProtocolUtils.Direction direction, - ProtocolVersion protocolVersion) { - } - - @Override - public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, - ProtocolVersion version) { - return 0; - } - - @Override - public boolean handle(MinecraftSessionHandler handler) { - return handler.handle(this); - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TransferPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TransferPacket.java index ed8bf1a204..b5a74e49f2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TransferPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TransferPacket.java @@ -22,40 +22,19 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; -import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame; - -import com.google.common.collect.ImmutableMap; import java.net.InetSocketAddress; -import java.util.LinkedHashMap; -import java.util.Map; import org.jetbrains.annotations.Nullable; public class TransferPacket implements MinecraftPacket { private String host; private int port; - private Map properties = Map.of(); // 26.4+ public TransferPacket() { } public TransferPacket(final String host, final int port) { - this(host, port, Map.of()); - } - - public TransferPacket(final String host, final int port, final Map properties) { this.host = host; this.port = port; - this.properties = ImmutableMap.copyOf(properties); - } - - /** - * Returns the properties the client appends to the handshake host of its next connection - * (26.4+, empty for older clients). - * - * @return the transfer properties - */ - public Map properties() { - return properties; } @Nullable @@ -70,31 +49,12 @@ public InetSocketAddress address() { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { this.host = ProtocolUtils.readString(buf); this.port = ProtocolUtils.readVarInt(buf); - if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_26_4)) { - int size = ProtocolUtils.readVarInt(buf); - checkFrame(size >= 0 && size <= buf.readableBytes(), - "Bad transfer property count (%s)", size); - Map properties = new LinkedHashMap<>(size); - for (int i = 0; i < size; i++) { - properties.put(ProtocolUtils.readString(buf), ProtocolUtils.readString(buf)); - } - this.properties = ImmutableMap.copyOf(properties); - } else { - this.properties = Map.of(); - } } @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { ProtocolUtils.writeString(buf, host); ProtocolUtils.writeVarInt(buf, port); - if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_26_4)) { - ProtocolUtils.writeVarInt(buf, properties.size()); - for (Map.Entry property : properties.entrySet()) { - ProtocolUtils.writeString(buf, property.getKey()); - ProtocolUtils.writeString(buf, property.getValue()); - } - } } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java index c18b7c78ef..298e74d4f2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java @@ -45,7 +45,6 @@ */ public final class PluginMessageUtil { - public static final String MOD_LIST_CHANNEL = "minecraft:mod_list"; private static final String BRAND_CHANNEL_LEGACY = "MC|Brand"; private static final String BRAND_CHANNEL = "minecraft:brand"; private static final String REGISTER_CHANNEL_LEGACY = "REGISTER"; @@ -57,17 +56,6 @@ private PluginMessageUtil() { throw new AssertionError(); } - /** - * Determines whether this plugin message is the client's mod list (Minecraft 26.4+). - * - * @param message the plugin message - * @return whether this is a {@code minecraft:mod_list} message - */ - public static boolean isModList(PluginMessagePacket message) { - checkNotNull(message, "message"); - return message.getChannel().equals(MOD_LIST_CHANNEL); - } - /** * Determines whether or not this is a brand plugin message. This is shown on the client. * diff --git a/proxy/src/test/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandlerTest.java b/proxy/src/test/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandlerTest.java index 4710195308..73ab243648 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandlerTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandlerTest.java @@ -49,13 +49,6 @@ void cleanVhostHandlesOctetsAndForge() { assertEquals("mc.example.com", cleanVhost("mc.example.com." + HANDSHAKE_HOSTNAME_TOKEN)); } - @Test - void cleanVhostHandlesQueryProperties() { - assertEquals("mc.example.com", cleanVhost("mc.example.com?_o=play.example.com%3A25565")); - assertEquals("mc.example.com", cleanVhost("mc.example.com.?_o=play.example.com%3A25565&_id=a")); - assertEquals("mc.example.com", cleanVhost("mc.example.com?k=v" + HANDSHAKE_HOSTNAME_TOKEN)); - } - @Test void cleanVhostHandlesEmptyHostnames() { assertEquals("", cleanVhost("")); diff --git a/proxy/src/test/java/com/velocitypowered/proxy/protocol/packet/TransferPacketTest.java b/proxy/src/test/java/com/velocitypowered/proxy/protocol/packet/TransferPacketTest.java deleted file mode 100644 index 6f8dadc653..0000000000 --- a/proxy/src/test/java/com/velocitypowered/proxy/protocol/packet/TransferPacketTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2026 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.protocol.packet; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; - -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import java.util.Map; -import org.junit.jupiter.api.Test; - -class TransferPacketTest { - - private static TransferPacket roundTrip(TransferPacket packet, ProtocolVersion version) { - ByteBuf buf = Unpooled.buffer(); - try { - packet.encode(buf, ProtocolUtils.Direction.CLIENTBOUND, version); - TransferPacket decoded = new TransferPacket(); - decoded.decode(buf, ProtocolUtils.Direction.CLIENTBOUND, version); - assertFalse(buf.isReadable(), "trailing bytes"); - return decoded; - } finally { - buf.release(); - } - } - - @Test - void keepsPropertiesOn264() { - TransferPacket decoded = roundTrip( - new TransferPacket("mc.example.com", 25565, Map.of("token", "abc", "empty", "")), - ProtocolVersion.MINECRAFT_26_4); - assertEquals("mc.example.com", decoded.address().getHostString()); - assertEquals(25565, decoded.address().getPort()); - assertEquals(Map.of("token", "abc", "empty", ""), decoded.properties()); - } - - @Test - void dropsPropertiesBefore264() { - TransferPacket decoded = roundTrip( - new TransferPacket("mc.example.com", 25565, Map.of("token", "abc")), - ProtocolVersion.MINECRAFT_26_3); - assertEquals(Map.of(), decoded.properties()); - } -}