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() {