diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java new file mode 100644 index 000000000..3badab15f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2018-2023 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.connection; + +import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON; + +import com.google.common.collect.ImmutableList; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.crypto.IdentifiedKey; +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.function.UnaryOperator; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import org.jspecify.annotations.Nullable; + +@SuppressWarnings({"MissingJavadocMethod", "MissingJavadocType"}) +public final class PlayerDataForwarding { + private static final String ALGORITHM = "HmacSHA256"; + + public static final String CHANNEL = "velocity:player_info"; + + public static final int MODERN_DEFAULT = 1; + public static final int MODERN_WITH_KEY = 2; + public static final int MODERN_WITH_KEY_V2 = 3; + public static final int MODERN_LAZY_SESSION = 4; + public static final int MODERN_MAX_VERSION = MODERN_LAZY_SESSION; + + private static final char LEGACY_SEPARATOR = '\0'; + + private static final String BUNGEE_GUARD_TOKEN_PROPERTY_NAME = "bungeeguard-token"; + + private PlayerDataForwarding() { + } + + public static ByteBuf createForwardingData( + final byte[] secret, + final String address, + final ProtocolVersion protocol, + final GameProfile profile, + final @Nullable IdentifiedKey key, + final int requestedVersion + ) { + final ByteBuf forwarded = Unpooled.buffer(2048); + try { + final int actualVersion = findForwardingVersion(requestedVersion, protocol, key); + + ProtocolUtils.writeVarInt(forwarded, actualVersion); + ProtocolUtils.writeString(forwarded, address); + ProtocolUtils.writeUuid(forwarded, profile.getId()); + ProtocolUtils.writeString(forwarded, profile.getName()); + ProtocolUtils.writeProperties(forwarded, profile.getProperties()); + + // This serves as additional redundancy. The key normally is stored in the + // login start to the server, but some setups require this. + if (actualVersion >= MODERN_WITH_KEY + && actualVersion < MODERN_LAZY_SESSION) { + assert key != null; + ProtocolUtils.writePlayerKey(forwarded, key); + + // Provide the signer UUID since the UUID may differ from the + // assigned UUID. Doing that breaks the signatures anyway but the server + // should be able to verify the key independently. + if (actualVersion >= MODERN_WITH_KEY_V2) { + if (key.getSignatureHolder() != null) { + forwarded.writeBoolean(true); + ProtocolUtils.writeUuid(forwarded, key.getSignatureHolder()); + } else { + // Should only not be provided if the player was connected + // as offline-mode and the signer UUID was not backfilled + forwarded.writeBoolean(false); + } + } + } + + final Mac mac = Mac.getInstance(ALGORITHM); + mac.init(new SecretKeySpec(secret, ALGORITHM)); + mac.update(forwarded.array(), forwarded.arrayOffset(), forwarded.readableBytes()); + final byte[] sig = mac.doFinal(); + + return Unpooled.wrappedBuffer(Unpooled.wrappedBuffer(sig), forwarded); + } catch (final InvalidKeyException e) { + forwarded.release(); + throw new RuntimeException("Unable to authenticate data", e); + } catch (final NoSuchAlgorithmException e) { + // Should never happen + forwarded.release(); + throw new AssertionError(e); + } + } + + private static int findForwardingVersion( + int requested, + final ProtocolVersion protocol, + final @Nullable IdentifiedKey key + ) { + // Ensure we are in range + requested = Math.min(requested, MODERN_MAX_VERSION); + if (requested > MODERN_DEFAULT) { + if (protocol.noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) { + return requested >= MODERN_LAZY_SESSION + ? MODERN_LAZY_SESSION + : MODERN_DEFAULT; + } + if (key != null) { + return switch (key.getKeyRevision()) { + case GENERIC_V1 -> MODERN_WITH_KEY; + // Since V2 is not backwards compatible we have to throw the key if v2 and requested is v1 + case LINKED_V2 -> requested >= MODERN_WITH_KEY_V2 + ? MODERN_WITH_KEY_V2 + : MODERN_DEFAULT; + }; + } else { + return MODERN_DEFAULT; + } + } + return MODERN_DEFAULT; + } + + public static String createLegacyForwardingAddress( + final String serverAddress, + final String playerAddress, + final GameProfile profile + ) { + return createLegacyForwardingAddress( + serverAddress, + playerAddress, + profile, + UnaryOperator.identity() + ); + } + + private static String createLegacyForwardingAddress( + final String serverAddress, + final String playerAddress, + final GameProfile profile, + final UnaryOperator> propertiesTransform + ) { + // BungeeCord IP forwarding is simply a special injection after the "address" in the handshake, + // separated by \0 (the null byte). In order, you send the original host, the player's IP, their + // UUID (undashed), and if you are in online-mode, their login properties (from Mojang). + final StringBuilder data = new StringBuilder() + .append(serverAddress) + .append(LEGACY_SEPARATOR) + .append(playerAddress) + .append(LEGACY_SEPARATOR) + .append(profile.getUndashedId()) + .append(LEGACY_SEPARATOR); + GENERAL_GSON + .toJson(propertiesTransform.apply(profile.getProperties()), data); + return data.toString(); + } + + public static String createBungeeGuardForwardingAddress( + final String serverAddress, + final String playerAddress, + final GameProfile profile, + final byte[] forwardingSecret + ) { + // Append forwarding secret as a BungeeGuard token. + final GameProfile.Property property = new GameProfile.Property( + BUNGEE_GUARD_TOKEN_PROPERTY_NAME, + new String(forwardingSecret, StandardCharsets.UTF_8), + "" + ); + return createLegacyForwardingAddress( + serverAddress, + playerAddress, + profile, + properties -> ImmutableList.builder() + .addAll(properties) + .add(property) + .build() + ); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java index a32fb6005..e9b426d8d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java @@ -26,12 +26,5 @@ public class VelocityConstants { throw new AssertionError(); } - public static final String VELOCITY_IP_FORWARDING_CHANNEL = "velocity:player_info"; - public static final int MODERN_FORWARDING_DEFAULT = 1; - public static final int MODERN_FORWARDING_WITH_KEY = 2; - public static final int MODERN_FORWARDING_WITH_KEY_V2 = 3; - public static final int MODERN_LAZY_SESSION = 4; - public static final int MODERN_FORWARDING_MAX_VERSION = MODERN_LAZY_SESSION; - public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; } 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 88186bc7b..0c0e5e553 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 @@ -19,19 +19,17 @@ package com.velocitypowered.proxy.connection.backend; import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent; import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.proxy.connection.VelocityConstants; +import com.velocitypowered.proxy.connection.PlayerDataForwarding; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; -import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; @@ -44,12 +42,7 @@ import com.velocitypowered.proxy.util.except.QuietRuntimeException; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.util.concurrent.CompletableFuture; -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import net.kyori.adventure.text.Component; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -86,15 +79,20 @@ public class LoginSessionHandler implements MinecraftSessionHandler { MinecraftConnection mc = serverConn.ensureConnected(); VelocityConfiguration configuration = server.getConfiguration(); if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN - && packet.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) { + && packet.getChannel().equals(PlayerDataForwarding.CHANNEL)) { - int requestedForwardingVersion = VelocityConstants.MODERN_FORWARDING_DEFAULT; + int requestedForwardingVersion = PlayerDataForwarding.MODERN_DEFAULT; // Check version if (packet.content().readableBytes() == 1) { requestedForwardingVersion = packet.content().readByte(); } - ByteBuf forwardingData = createForwardingData(configuration.getForwardingSecret(), - serverConn.getPlayerRemoteAddressAsString(), serverConn.getPlayer(), + ConnectedPlayer player = serverConn.getPlayer(); + ByteBuf forwardingData = PlayerDataForwarding.createForwardingData( + configuration.getForwardingSecret(), + serverConn.getPlayerRemoteAddressAsString(), + player.getProtocolVersion(), + player.getGameProfile(), + player.getIdentifiedKey(), requestedForwardingVersion); LoginPluginResponsePacket response = new LoginPluginResponsePacket( @@ -197,85 +195,4 @@ public class LoginSessionHandler implements MinecraftSessionHandler { ); } } - - private static int findForwardingVersion(int requested, ConnectedPlayer player) { - // Ensure we are in range - requested = Math.min(requested, VelocityConstants.MODERN_FORWARDING_MAX_VERSION); - if (requested > VelocityConstants.MODERN_FORWARDING_DEFAULT) { - if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) { - return requested >= VelocityConstants.MODERN_LAZY_SESSION - ? VelocityConstants.MODERN_LAZY_SESSION - : VelocityConstants.MODERN_FORWARDING_DEFAULT; - } - if (player.getIdentifiedKey() != null) { - // No enhanced switch on java 11 - switch (player.getIdentifiedKey().getKeyRevision()) { - case GENERIC_V1: - return VelocityConstants.MODERN_FORWARDING_WITH_KEY; - // Since V2 is not backwards compatible we have to throw the key if v2 and requested is v1 - case LINKED_V2: - return requested >= VelocityConstants.MODERN_FORWARDING_WITH_KEY_V2 - ? VelocityConstants.MODERN_FORWARDING_WITH_KEY_V2 - : VelocityConstants.MODERN_FORWARDING_DEFAULT; - default: - return VelocityConstants.MODERN_FORWARDING_DEFAULT; - } - } else { - return VelocityConstants.MODERN_FORWARDING_DEFAULT; - } - } - return VelocityConstants.MODERN_FORWARDING_DEFAULT; - } - - private static ByteBuf createForwardingData(byte[] hmacSecret, String address, - ConnectedPlayer player, int requestedVersion) { - ByteBuf forwarded = Unpooled.buffer(2048); - try { - int actualVersion = findForwardingVersion(requestedVersion, player); - - ProtocolUtils.writeVarInt(forwarded, actualVersion); - ProtocolUtils.writeString(forwarded, address); - ProtocolUtils.writeUuid(forwarded, player.getGameProfile().getId()); - ProtocolUtils.writeString(forwarded, player.getGameProfile().getName()); - ProtocolUtils.writeProperties(forwarded, player.getGameProfile().getProperties()); - - // This serves as additional redundancy. The key normally is stored in the - // login start to the server, but some setups require this. - if (actualVersion >= VelocityConstants.MODERN_FORWARDING_WITH_KEY - && actualVersion < VelocityConstants.MODERN_LAZY_SESSION) { - IdentifiedKey key = player.getIdentifiedKey(); - assert key != null; - ProtocolUtils.writePlayerKey(forwarded, key); - - // Provide the signer UUID since the UUID may differ from the - // assigned UUID. Doing that breaks the signatures anyway but the server - // should be able to verify the key independently. - if (actualVersion >= VelocityConstants.MODERN_FORWARDING_WITH_KEY_V2) { - if (key.getSignatureHolder() != null) { - forwarded.writeBoolean(true); - ProtocolUtils.writeUuid(forwarded, key.getSignatureHolder()); - } else { - // Should only not be provided if the player was connected - // as offline-mode and the signer UUID was not backfilled - forwarded.writeBoolean(false); - } - } - } - - SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256"); - Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(key); - mac.update(forwarded.array(), forwarded.arrayOffset(), forwarded.readableBytes()); - byte[] sig = mac.doFinal(); - - return Unpooled.wrappedBuffer(Unpooled.wrappedBuffer(sig), forwarded); - } catch (InvalidKeyException e) { - forwarded.release(); - throw new RuntimeException("Unable to authenticate data", e); - } catch (NoSuchAlgorithmException e) { - // Should never happen - forwarded.release(); - throw new AssertionError(e); - } - } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index cf1b889cf..e0b448bc8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -17,24 +17,22 @@ package com.velocitypowered.proxy.connection.backend; -import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON; import static com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN; import static com.velocitypowered.proxy.network.Connections.HANDLER; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; -import com.velocitypowered.api.util.GameProfile.Property; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.PlayerDataForwarding; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.forge.modern.ModernForgeConnectionType; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; @@ -47,13 +45,10 @@ import com.velocitypowered.proxy.server.VelocityRegisteredServer; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; -import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.function.UnaryOperator; import net.kyori.adventure.nbt.CompoundBinaryTag; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -142,32 +137,23 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, } } - private String createLegacyForwardingAddress(UnaryOperator> propertiesTransform) { - // BungeeCord IP forwarding is simply a special injection after the "address" in the handshake, - // separated by \0 (the null byte). In order, you send the original host, the player's IP, their - // UUID (undashed), and if you are in online-mode, their login properties (from Mojang). - StringBuilder data = new StringBuilder().append(proxyPlayer.getVirtualHost().orElseGet(() -> - registeredServer.getServerInfo().getAddress()).getHostString()) - .append('\0') - .append(getPlayerRemoteAddressAsString()) - .append('\0') - .append(proxyPlayer.getGameProfile().getUndashedId()) - .append('\0'); - GENERAL_GSON - .toJson(propertiesTransform.apply(proxyPlayer.getGameProfile().getProperties()), data); - return data.toString(); - } - private String createLegacyForwardingAddress() { - return createLegacyForwardingAddress(UnaryOperator.identity()); + return PlayerDataForwarding.createLegacyForwardingAddress( + proxyPlayer.getVirtualHost().orElseGet(() -> + registeredServer.getServerInfo().getAddress()).getHostString(), + getPlayerRemoteAddressAsString(), + proxyPlayer.getGameProfile() + ); } private String createBungeeGuardForwardingAddress(byte[] forwardingSecret) { - // Append forwarding secret as a BungeeGuard token. - Property property = - new Property("bungeeguard-token", new String(forwardingSecret, StandardCharsets.UTF_8), ""); - return createLegacyForwardingAddress( - properties -> ImmutableList.builder().addAll(properties).add(property).build()); + return PlayerDataForwarding.createBungeeGuardForwardingAddress( + proxyPlayer.getVirtualHost().orElseGet(() -> + registeredServer.getServerInfo().getAddress()).getHostString(), + getPlayerRemoteAddressAsString(), + proxyPlayer.getGameProfile(), + forwardingSecret + ); } private void startHandshake() {