diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerHandshakeEvent.java b/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerHandshakeEvent.java index 31c77a4b64f9..14c396e60216 100644 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerHandshakeEvent.java +++ b/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerHandshakeEvent.java @@ -25,6 +25,7 @@ public class PlayerHandshakeEvent extends Event implements Cancellable { private static final HandlerList HANDLER_LIST = new HandlerList(); + private final UUID connectionId; private final String originalHandshake; private final String originalSocketAddressHostname; private @Nullable String serverHostname; @@ -42,9 +43,16 @@ public PlayerHandshakeEvent(final String originalHandshake, final boolean cancel this(originalHandshake, "127.0.0.1", cancelled); } + @Deprecated @ApiStatus.Internal public PlayerHandshakeEvent(final String originalHandshake, final String originalSocketAddressHostname, final boolean cancelled) { + this(UUID.randomUUID(), originalHandshake, originalSocketAddressHostname, cancelled); + } + + @ApiStatus.Internal + public PlayerHandshakeEvent(final UUID connectionId, final String originalHandshake, final String originalSocketAddressHostname, final boolean cancelled) { super(true); + this.connectionId = connectionId; this.originalHandshake = originalHandshake; this.originalSocketAddressHostname = originalSocketAddressHostname; this.cancelled = cancelled; @@ -59,6 +67,19 @@ public String getOriginalHandshake() { return this.originalHandshake; } + /** + * Gets the stable connection identifier associated with this handshake. + * + *

This identifier remains the same throughout the entire connection + * lifecycle and can be used to correlate this handshake with subsequent + * connection stages such as login and join.

+ * + * @return the stable connection identifier + */ + public UUID getConnectionId() { + return this.connectionId; + } + /** * Gets the original socket address hostname. * diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/server/PaperServerListPingEvent.java b/paper-api/src/main/java/com/destroystokyo/paper/event/server/PaperServerListPingEvent.java index 15b12abe17da..45c98c7353d8 100644 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/server/PaperServerListPingEvent.java +++ b/paper-api/src/main/java/com/destroystokyo/paper/event/server/PaperServerListPingEvent.java @@ -56,9 +56,16 @@ public class PaperServerListPingEvent extends ServerListPingEvent implements Can private Object[] players; @ApiStatus.Internal + @Deprecated(forRemoval = true) public PaperServerListPingEvent(@NotNull StatusClient client, @NotNull net.kyori.adventure.text.Component motd, int numPlayers, int maxPlayers, @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon) { - super("", client.getAddress().getAddress(), motd, numPlayers, maxPlayers); + this(UUID.randomUUID(), client, motd, numPlayers, maxPlayers, version, protocolVersion, favicon); + } + + @ApiStatus.Internal + public PaperServerListPingEvent(@NotNull UUID connectionId, @NotNull StatusClient client, @NotNull net.kyori.adventure.text.Component motd, int numPlayers, int maxPlayers, + @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon) { + super(connectionId, "", client.getAddress().getAddress(), motd, numPlayers, maxPlayers); this.client = client; this.numPlayers = numPlayers; this.version = version; diff --git a/paper-api/src/main/java/io/papermc/paper/connection/PlayerConnection.java b/paper-api/src/main/java/io/papermc/paper/connection/PlayerConnection.java index 2dd2fdc199a7..317d205b2ed2 100644 --- a/paper-api/src/main/java/io/papermc/paper/connection/PlayerConnection.java +++ b/paper-api/src/main/java/io/papermc/paper/connection/PlayerConnection.java @@ -2,6 +2,7 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.UUID; import net.kyori.adventure.text.Component; import org.jspecify.annotations.Nullable; @@ -62,4 +63,19 @@ public interface PlayerConnection { * @return the player's proxy address, null if the server doesn't have Proxy Protocol enabled, or the player didn't connect to an HAProxy instance */ @Nullable InetSocketAddress getHAProxyAddress(); + + /** + * Gets a stable identifier for this connection that remains the same + * throughout its entire lifecycle, from the initial handshake until the + * connection is closed or the player fully joins the server. + * + *

This identifier is unique per connection and can be used to reliably + * correlate data collected during earlier connection stages (e.g. handshake + * or login) with the player that eventually joins the server, without + * relying on the client's IP address.

+ * + * @return the stable connection identifier + */ + UUID getConnectionId(); + } diff --git a/paper-api/src/main/java/org/bukkit/event/server/ServerListPingEvent.java b/paper-api/src/main/java/org/bukkit/event/server/ServerListPingEvent.java index fdc39724354f..703571648283 100644 --- a/paper-api/src/main/java/org/bukkit/event/server/ServerListPingEvent.java +++ b/paper-api/src/main/java/org/bukkit/event/server/ServerListPingEvent.java @@ -2,6 +2,7 @@ import com.google.common.base.Preconditions; import java.net.InetAddress; +import java.util.UUID; import java.util.Iterator; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @@ -32,28 +33,19 @@ public class ServerListPingEvent extends ServerEvent implements Iterable private final int numPlayers; private Component motd; private int maxPlayers; + private final UUID connectionId; @ApiStatus.Internal @Deprecated(forRemoval = true) public ServerListPingEvent(@NotNull final String hostname, @NotNull final InetAddress address, @NotNull final String motd, final int numPlayers, final int maxPlayers) { - super(true); + this(UUID.randomUUID(), hostname, address, LegacyComponentSerializer.legacySection().deserialize(motd), numPlayers, maxPlayers); Preconditions.checkArgument(numPlayers >= 0, "Cannot have negative number of players online", numPlayers); - this.hostname = hostname; - this.address = address; - this.motd = LegacyComponentSerializer.legacySection().deserialize(motd); - this.numPlayers = numPlayers; - this.maxPlayers = maxPlayers; } @ApiStatus.Internal @Deprecated(forRemoval = true) protected ServerListPingEvent(@NotNull final String hostname, @NotNull final InetAddress address, @NotNull final String motd, final int maxPlayers) { - super(true); - this.numPlayers = MAGIC_PLAYER_COUNT; - this.hostname = hostname; - this.address = address; - this.motd = LegacyComponentSerializer.legacySection().deserialize(motd); - this.maxPlayers = maxPlayers; + this(UUID.randomUUID(), hostname, address, LegacyComponentSerializer.legacySection().deserialize(motd), maxPlayers); } @ApiStatus.Internal @@ -63,8 +55,15 @@ public ServerListPingEvent(@NotNull final InetAddress address, @NotNull final Co } @ApiStatus.Internal + @Deprecated(forRemoval = true) public ServerListPingEvent(@NotNull final String hostname, @NotNull final InetAddress address, @NotNull final Component motd, final int numPlayers, final int maxPlayers) { + this(UUID.randomUUID(), hostname, address, motd, numPlayers, maxPlayers); + } + + @ApiStatus.Internal + public ServerListPingEvent(@NotNull final UUID connectionId, @NotNull final String hostname, @NotNull final InetAddress address, @NotNull final Component motd, final int numPlayers, final int maxPlayers) { super(true); + this.connectionId = connectionId; this.hostname = hostname; this.address = address; this.motd = motd; @@ -84,8 +83,15 @@ protected ServerListPingEvent(@NotNull final InetAddress address, @NotNull final * count. */ @ApiStatus.Internal + @Deprecated(forRemoval = true) protected ServerListPingEvent(final @NotNull String hostname, final @NotNull InetAddress address, final @NotNull Component motd, final int maxPlayers) { + this(UUID.randomUUID(), hostname, address, motd, maxPlayers); + } + + @ApiStatus.Internal + protected ServerListPingEvent(final @NotNull UUID connectionId, final @NotNull String hostname, final @NotNull InetAddress address, final @NotNull Component motd, final int maxPlayers) { this.numPlayers = MAGIC_PLAYER_COUNT; + this.connectionId = connectionId; this.hostname = hostname; this.address = address; this.motd = motd; @@ -103,6 +109,20 @@ public String getHostname() { return this.hostname; } + /** + * Gets the stable connection identifier associated with this ping. + * + *

This identifier remains the same throughout the entire connection + * lifecycle and can be used to correlate this ping with subsequent + * connection stages such as handshake, login and join.

+ * + * @return the stable connection identifier + */ + @NotNull + public UUID getConnectionId() { + return this.connectionId; + } + /** * Get the address the ping is coming from. * diff --git a/paper-server/patches/features/0035-Add-stable-connection-identifier-API.patch b/paper-server/patches/features/0035-Add-stable-connection-identifier-API.patch new file mode 100644 index 000000000000..ad06b8276418 --- /dev/null +++ b/paper-server/patches/features/0035-Add-stable-connection-identifier-API.patch @@ -0,0 +1,37 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Umbre11as +Date: Sun, 30 Aug 2026 05:00:22 +0300 +Subject: [PATCH] Add stable connection identifier API + + +diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java +index 9fbc7e93712672dbab67f931e2aee447d803607b..e952765eb0e9a5a41b6b5948a9b3b85f4d4b2b49 100644 +--- a/net/minecraft/network/Connection.java ++++ b/net/minecraft/network/Connection.java +@@ -93,6 +93,13 @@ public class Connection extends SimpleChannelInboundHandler> { + public java.net.InetSocketAddress virtualHost; + private static boolean enableExplicitFlush = Boolean.getBoolean("paper.explicit-flush"); // Paper - Disable explicit connection flushing + // Paper end ++ // Paper start - stable connection identifier ++ private final UUID connectionId = UUID.randomUUID(); ++ ++ public UUID getConnectionId() { ++ return this.connectionId; ++ } ++ // Paper end - stable connection identifier + // Paper start - add utility methods + public final net.minecraft.server.level.ServerPlayer getPlayer() { + if (this.packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl impl) { +diff --git a/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java b/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java +index 649f9646abb1bd84f1f22409aee09d2a4d27e581..4826053a4bd2c2449d70d64170262cfbc2125b16 100644 +--- a/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java +@@ -122,7 +122,7 @@ public class ServerHandshakePacketListenerImpl implements ServerHandshakePacketL + if (com.destroystokyo.paper.event.player.PlayerHandshakeEvent.getHandlerList().getRegisteredListeners().length != 0) { // Hello? Can you hear me? + java.net.SocketAddress socketAddress = this.connection.getRemoteAddress(); + String hostnameOfRemote = socketAddress instanceof java.net.InetSocketAddress ? ((java.net.InetSocketAddress) socketAddress).getHostString() : java.net.InetAddress.getLoopbackAddress().getHostAddress(); +- com.destroystokyo.paper.event.player.PlayerHandshakeEvent event = new com.destroystokyo.paper.event.player.PlayerHandshakeEvent(packet.hostName(), hostnameOfRemote, !proxyLogicEnabled); ++ com.destroystokyo.paper.event.player.PlayerHandshakeEvent event = new com.destroystokyo.paper.event.player.PlayerHandshakeEvent(this.connection.getConnectionId(), packet.hostName(), hostnameOfRemote, !proxyLogicEnabled); + if (event.callEvent()) { + // If we've failed somehow, let the client know so and go no further. + if (event.isFailed()) { diff --git a/paper-server/src/main/java/com/destroystokyo/paper/network/PaperLegacyStatusClient.java b/paper-server/src/main/java/com/destroystokyo/paper/network/PaperLegacyStatusClient.java index ebc03a8e4780..62b015892f63 100644 --- a/paper-server/src/main/java/com/destroystokyo/paper/network/PaperLegacyStatusClient.java +++ b/paper-server/src/main/java/com/destroystokyo/paper/network/PaperLegacyStatusClient.java @@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringUtils; import java.net.InetSocketAddress; +import java.util.UUID; import javax.annotation.Nullable; @@ -46,7 +47,7 @@ public boolean isLegacy() { public static PaperServerListPingEvent processRequest(MinecraftServer server, InetSocketAddress address, int protocolVersion, @Nullable InetSocketAddress virtualHost) { - PaperServerListPingEvent event = new PaperServerListPingEventImpl(server, + PaperServerListPingEvent event = new PaperServerListPingEventImpl(UUID.randomUUID(), server, new PaperLegacyStatusClient(address, protocolVersion, virtualHost), Byte.MAX_VALUE, null); server.server.getPluginManager().callEvent(event); diff --git a/paper-server/src/main/java/com/destroystokyo/paper/network/PaperServerListPingEventImpl.java b/paper-server/src/main/java/com/destroystokyo/paper/network/PaperServerListPingEventImpl.java index 13b03dab807c..6ea7abb92d1f 100644 --- a/paper-server/src/main/java/com/destroystokyo/paper/network/PaperServerListPingEventImpl.java +++ b/paper-server/src/main/java/com/destroystokyo/paper/network/PaperServerListPingEventImpl.java @@ -6,14 +6,15 @@ import org.bukkit.entity.Player; import org.bukkit.util.CachedServerIcon; +import java.util.UUID; import javax.annotation.Nullable; class PaperServerListPingEventImpl extends PaperServerListPingEvent { private final MinecraftServer server; - PaperServerListPingEventImpl(MinecraftServer server, StatusClient client, int protocolVersion, @Nullable CachedServerIcon icon) { - super(client, server.motd(), server.getPlayerCount(), server.getMaxPlayers(), + PaperServerListPingEventImpl(UUID connectionId, MinecraftServer server, StatusClient client, int protocolVersion, @Nullable CachedServerIcon icon) { + super(connectionId, client, server.motd(), server.getPlayerCount(), server.getMaxPlayers(), server.getServerModName() + ' ' + server.getServerVersion(), protocolVersion, icon); this.server = server; } diff --git a/paper-server/src/main/java/com/destroystokyo/paper/network/StandardPaperServerListPingEventImpl.java b/paper-server/src/main/java/com/destroystokyo/paper/network/StandardPaperServerListPingEventImpl.java index 5e89bad7231d..624840c2e447 100644 --- a/paper-server/src/main/java/com/destroystokyo/paper/network/StandardPaperServerListPingEventImpl.java +++ b/paper-server/src/main/java/com/destroystokyo/paper/network/StandardPaperServerListPingEventImpl.java @@ -21,7 +21,7 @@ public final class StandardPaperServerListPingEventImpl extends PaperServerListP private List originalSample; private StandardPaperServerListPingEventImpl(MinecraftServer server, Connection connection, ServerStatus ping) { - super(server, new PaperStatusClient(connection), ping.version().map(ServerStatus.Version::protocol).orElse(-1), server.server.getServerIcon()); + super(connection.getConnectionId(), server, new PaperStatusClient(connection), ping.version().map(ServerStatus.Version::protocol).orElse(-1), server.server.getServerIcon()); this.originalSample = ping.players().map(ServerStatus.Players::sample).orElse(null); // GH-1473 - pre-tick race condition NPE } diff --git a/paper-server/src/main/java/io/papermc/paper/connection/PaperCommonConnection.java b/paper-server/src/main/java/io/papermc/paper/connection/PaperCommonConnection.java index 5cc97ae87c1d..ae710f2c760e 100644 --- a/paper-server/src/main/java/io/papermc/paper/connection/PaperCommonConnection.java +++ b/paper-server/src/main/java/io/papermc/paper/connection/PaperCommonConnection.java @@ -7,6 +7,7 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Map; +import java.util.UUID; import net.kyori.adventure.text.Component; import net.minecraft.network.protocol.common.ClientboundCustomReportDetailsPacket; import net.minecraft.network.protocol.common.ClientboundServerLinksPacket; @@ -105,6 +106,11 @@ public InetSocketAddress getClientAddress() { return this.packetListener.connection.haProxyAddress instanceof final InetSocketAddress inetSocketAddress ? inetSocketAddress : null; } + @Override + public UUID getConnectionId() { + return this.packetListener.connection.getConnectionId(); + } + @Override public void storeCookie(final NamespacedKey key, final byte[] value) { Preconditions.checkArgument(key != null, "Cookie key cannot be null"); diff --git a/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerLoginConnection.java b/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerLoginConnection.java index 6fcaa17ff5d9..1424b1fc8613 100644 --- a/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerLoginConnection.java +++ b/paper-server/src/main/java/io/papermc/paper/connection/PaperPlayerLoginConnection.java @@ -5,6 +5,7 @@ import io.papermc.paper.adventure.PaperAdventure; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.UUID; import net.kyori.adventure.text.Component; import net.minecraft.server.network.ServerLoginPacketListenerImpl; import org.jspecify.annotations.NullMarked; @@ -66,4 +67,10 @@ public void disconnect(final Component component) { public boolean isConnected() { return this.packetListener.connection.isConnected(); } + + @Override + public UUID getConnectionId() { + return this.packetListener.connection.getConnectionId(); + } + }