From 15aef74f5ce4d8c81fd6cf3c870cf91bf8e357ad Mon Sep 17 00:00:00 2001 From: Patrick Lorio Date: Wed, 11 Feb 2026 16:57:32 -0800 Subject: [PATCH 1/2] rework reflection logic --- .../gg/playit/minecraft/PlayitTcpTunnel.java | 2 +- .../gg/playit/minecraft/ReflectionHelper.java | 150 +++++++++++++----- 2 files changed, 111 insertions(+), 41 deletions(-) diff --git a/src/main/java/gg/playit/minecraft/PlayitTcpTunnel.java b/src/main/java/gg/playit/minecraft/PlayitTcpTunnel.java index d78dac3..53bd687 100644 --- a/src/main/java/gg/playit/minecraft/PlayitTcpTunnel.java +++ b/src/main/java/gg/playit/minecraft/PlayitTcpTunnel.java @@ -177,7 +177,7 @@ protected void initChannel(SocketChannel socketChannel) { } private boolean addChannelToMinecraftServer() { - ReflectionHelper reflect = new ReflectionHelper(); + ReflectionHelper reflect = new ReflectionHelper(server); log.info("Reflect: " + reflect); Object minecraftServer = reflect.getMinecraftServer(server); diff --git a/src/main/java/gg/playit/minecraft/ReflectionHelper.java b/src/main/java/gg/playit/minecraft/ReflectionHelper.java index f19c71f..edb5235 100644 --- a/src/main/java/gg/playit/minecraft/ReflectionHelper.java +++ b/src/main/java/gg/playit/minecraft/ReflectionHelper.java @@ -31,22 +31,57 @@ public class ReflectionHelper { private final Class CraftServer; public ReflectionHelper() { - ServerConnection = cls("net.minecraft.server.network.ServerConnection"); - LegacyPingHandler = cls("net.minecraft.server.network.LegacyPingHandler"); + this(null); + } + + public ReflectionHelper(Server server) { + ServerConnection = cls( + "net.minecraft.server.network.ServerConnection", + "net.minecraft.server.network.ServerConnectionListener"); + LegacyPingHandler = cls( + "net.minecraft.server.network.LegacyPingHandler", + "net.minecraft.server.network.LegacyQueryHandler"); MinecraftServer = cls("net.minecraft.server.MinecraftServer"); - PacketSplitter = cls("net.minecraft.network.PacketSplitter"); + PacketSplitter = cls( + "net.minecraft.network.PacketSplitter", + "net.minecraft.network.Varint21FrameDecoder"); PacketDecoder = cls("net.minecraft.network.PacketDecoder"); - EnumProtocolDirection = cls("net.minecraft.network.protocol.EnumProtocolDirection"); - PacketPrepender = cls("net.minecraft.network.PacketPrepender"); + EnumProtocolDirection = cls( + "net.minecraft.network.protocol.EnumProtocolDirection", + "net.minecraft.network.protocol.ConnectionProtocol"); + PacketPrepender = cls( + "net.minecraft.network.PacketPrepender", + "net.minecraft.network.Varint21LengthFieldPrepender"); PacketEncoder = cls("net.minecraft.network.PacketEncoder"); - NetworkManagerServer = cls("net.minecraft.network.NetworkManagerServer"); - NetworkManager = cls("net.minecraft.network.NetworkManager"); - HandshakeListener = cls("net.minecraft.server.network.HandshakeListener"); + NetworkManagerServer = cls( + "net.minecraft.network.NetworkManagerServer", + "net.minecraft.network.RateKickingConnection"); + NetworkManager = cls( + "net.minecraft.network.NetworkManager", + "net.minecraft.network.Connection"); + HandshakeListener = cls( + "net.minecraft.server.network.HandshakeListener", + "net.minecraft.server.network.ServerHandshakePacketListenerImpl"); PacketListener = cls("net.minecraft.network.PacketListener"); - CraftServer = cls( + CraftServer = resolveCraftServer(server); + } + + private static Class resolveCraftServer(Server server) { + if (server != null) { + String pkg = server.getClass().getPackage().getName(); + if (pkg.startsWith("org.bukkit.craftbukkit")) { + Class c = cls(pkg + ".CraftServer"); + if (c != null) return c; + } + } + return cls( "org.bukkit.craftbukkit.CraftServer", - "org.bukkit.craftbukkit.v1_19_R1.CraftServer" - ); + "org.bukkit.craftbukkit.v1_21_R1.CraftServer", + "org.bukkit.craftbukkit.v1_20_R3.CraftServer", + "org.bukkit.craftbukkit.v1_20_R1.CraftServer", + "org.bukkit.craftbukkit.v1_19_R3.CraftServer", + "org.bukkit.craftbukkit.v1_19_R1.CraftServer", + "org.bukkit.craftbukkit.v1_16_R3.CraftServer"); } static Class cls(String className) { @@ -123,17 +158,30 @@ public boolean addToServerConnections(Object serverConnection, Object networkMan log.warning("failed set field connections, error: " + e); } + try { + Method getConnections = searchMethod(ServerConnection, "getConnections"); + getConnections.setAccessible(true); + @SuppressWarnings("unchecked") + List list = (List) getConnections.invoke(serverConnection); + if (list != null) { + list.add(networkManager); + return true; + } + } catch (Exception e) { + log.warning("failed to add via getConnections(), error: " + e); + } + HashSet potentialFieldObjects = new HashSet<>(); var search = ServerConnection; while (search != null) { - for (var field : ServerConnection.getDeclaredFields()) { + for (var field : search.getDeclaredFields()) { if (List.class.isAssignableFrom(field.getType())) { if (field.getGenericType() instanceof ParameterizedType parameterizedType) { var type = parameterizedType.getActualTypeArguments()[0]; var typeClass = cls(type.getTypeName()); - if (typeClass != null && NetworkManager.isAssignableFrom(typeClass)) { + if (typeClass != null && NetworkManager != null && NetworkManager.isAssignableFrom(typeClass)) { try { field.setAccessible(true); potentialFieldObjects.add(field.get(serverConnection)); @@ -149,13 +197,14 @@ public boolean addToServerConnections(Object serverConnection, Object networkMan if (potentialFieldObjects.size() == 1) { var found = potentialFieldObjects.toArray()[0]; try { - var list = (List) found; + @SuppressWarnings("unchecked") + var list = (List) found; list.add(networkManager); return true; } catch (Exception e) { log.warning("failed to add connection to " + found + ", error: " + e); } - } else { + } else if (potentialFieldObjects.size() > 1) { log.warning("multiple connection lists: " + potentialFieldObjects); } @@ -163,15 +212,21 @@ public boolean addToServerConnections(Object serverConnection, Object networkMan } public Object newHandshakeListener(Object minecraftServer, Object networkManager) { - if (HandshakeListener == null) { + if (HandshakeListener == null || MinecraftServer == null) { return null; } try { return HandshakeListener.getConstructor(MinecraftServer, NetworkManager).newInstance(minecraftServer, networkManager); - } catch (Exception e) { - return null; + } catch (Exception ignored) { } + + try { + return HandshakeListener.getConstructor(MinecraftServer, Class.forName("net.minecraft.network.Connection")).newInstance(minecraftServer, networkManager); + } catch (Exception ignored) { + } + + return null; } public boolean setRemoteAddress(Channel channel, SocketAddress address) { @@ -247,9 +302,15 @@ public Object newLegacyPingHandler(Object serverConnection) { try { return LegacyPingHandler.getConstructor(ServerConnection).newInstance(serverConnection); } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | - IllegalAccessException e) { - return null; + IllegalAccessException ignored) { + } + + try { + return LegacyPingHandler.getConstructor(Class.forName("net.minecraft.server.network.ServerConnectionListener")).newInstance(serverConnection); + } catch (Exception ignored) { } + + return null; } public Object newPacketSplitter() { @@ -326,27 +387,33 @@ public Object newServerNetworkManager() { } private Object serverBound() { - if (EnumProtocolDirection == null) { - return null; - } - - try { - return Enum.valueOf((Class) EnumProtocolDirection, "SERVERBOUND"); - } catch (Exception e) { - return null; + for (Class enumClass : new Class[]{ + EnumProtocolDirection, + cls("net.minecraft.network.protocol.PacketFlow") + }) { + if (enumClass != null) { + try { + return Enum.valueOf((Class) enumClass, "SERVERBOUND"); + } catch (Exception ignored) { + } + } } + return null; } private Object clientBound() { - if (EnumProtocolDirection == null) { - return null; - } - - try { - return Enum.valueOf((Class) EnumProtocolDirection, "CLIENTBOUND"); - } catch (Exception e) { - return null; + for (Class enumClass : new Class[]{ + EnumProtocolDirection, + cls("net.minecraft.network.protocol.PacketFlow") + }) { + if (enumClass != null) { + try { + return Enum.valueOf((Class) enumClass, "CLIENTBOUND"); + } catch (Exception ignored) { + } + } } + return null; } public Object getMinecraftServer(Server server) { @@ -400,16 +467,19 @@ public Object serverConnectionFromMCServer(Object object) { } try { - var field = MinecraftServer.getDeclaredField("connection"); + Field field = searchForFieldByName(MinecraftServer, "connection"); field.setAccessible(true); var res = field.get(object); - if (ServerConnection.isInstance(res)) { + if (ServerConnection != null && ServerConnection.isInstance(res)) { return res; } - } catch (Exception e) { + } catch (Exception ignored) { } - return searchForAttribute(MinecraftServer, ServerConnection, object); + if (ServerConnection != null) { + return searchForAttribute(MinecraftServer, ServerConnection, object); + } + return null; } public Method searchMethod(Class subject, String name, Class... parameterTypes) throws NoSuchMethodException { From 8a892fcf76e93cdc6ae5806dda80de36baaebe11 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 19 Feb 2026 11:39:02 -0800 Subject: [PATCH 2/2] Stage --- build.gradle | 57 +- gradle.properties | 1 + settings.gradle | 9 + .../playit/channel/BaseChannelInserter.java | 421 ++++++++++++ .../gg/playit/channel/ChannelInserter.java | 25 + .../channel/ChannelInserterProvider.java | 197 ++++++ .../playit/channel/ChannelInserterV1_16.java | 229 +++++++ .../java/gg/playit/control/ChannelSetup.java | 5 +- .../gg/playit/minecraft/PlayitTcpTunnel.java | 133 +--- .../gg/playit/minecraft/ReflectionHelper.java | 601 ------------------ .../playit/channel/ChannelInserterV1_17.java | 173 +++++ .../playit/channel/ChannelInserterV1_21.java | 270 ++++++++ .../channel/ChannelInserterV1_21_4.java | 139 ++++ 13 files changed, 1552 insertions(+), 708 deletions(-) create mode 100644 gradle.properties create mode 100644 src/main/java/gg/playit/channel/BaseChannelInserter.java create mode 100644 src/main/java/gg/playit/channel/ChannelInserter.java create mode 100644 src/main/java/gg/playit/channel/ChannelInserterProvider.java create mode 100644 src/main/java/gg/playit/channel/ChannelInserterV1_16.java delete mode 100644 src/main/java/gg/playit/minecraft/ReflectionHelper.java create mode 100644 src/v1_17/java/gg/playit/channel/ChannelInserterV1_17.java create mode 100644 src/v1_21/java/gg/playit/channel/ChannelInserterV1_21.java create mode 100644 src/v1_21_4/java/gg/playit/channel/ChannelInserterV1_21_4.java diff --git a/build.gradle b/build.gradle index 5955d3c..2c7ab9f 100644 --- a/build.gradle +++ b/build.gradle @@ -67,16 +67,53 @@ tasks.register('runControlExample', JavaExec) { mainClass = 'gg.playit.control.example.ControlExample' } +configurations { + compileOnly_v1_17 { + extendsFrom compileOnly + canBeResolved = true + } + compileOnly_v1_21 { + extendsFrom compileOnly + canBeResolved = true + } + compileOnly_v1_21_4 { + extendsFrom compileOnly + canBeResolved = true + } +} + repositories { + mavenLocal() mavenCentral() maven { url = 'https://hub.spigotmc.org/nexus/content/repositories/public/' } + maven { url = 'https://maven.elmakers.com/repository/' } + maven { url = 'https://repo.codemc.io/repository/nms/' } maven { url = 'https://repo.dmulloy2.net/repository/public/' } + maven { url = 'https://repo.screamingsandals.org/public' } } jar { enabled = false } +sourceSets { + v1_17 { + java { srcDirs = ['src/v1_17/java'] } + compileClasspath += sourceSets.main.output + compileClasspath += configurations.compileOnly_v1_17 + } + v1_21 { + java { srcDirs = ['src/v1_21/java'] } + compileClasspath += sourceSets.main.output + compileClasspath += configurations.compileOnly_v1_21 + } + v1_21_4 { + java { srcDirs = ['src/v1_21_4/java'] } + compileClasspath += sourceSets.main.output + compileClasspath += configurations.compileOnly_v1_21_4 + } +} + dependencies { implementation 'jakarta.validation:jakarta.validation-api:3.0.2' implementation 'commons-io:commons-io:2.11.0' @@ -84,13 +121,31 @@ dependencies { implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0-rc1' compileOnly 'io.netty:netty-all:4.1.82.Final' - compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT' + compileOnly 'org.spigotmc:spigot-api:1.21.1-R0.1-SNAPSHOT' + + compileOnly_v1_17('org.spigotmc:spigot:1.17.1-R0.1-SNAPSHOT:remapped-mojang') { transitive = false } + compileOnly_v1_21('org.spigotmc:spigot:1.21.1-R0.1-SNAPSHOT:remapped-mojang') { transitive = false } + compileOnly_v1_21_4('org.spigotmc:spigot:1.21.4-R0.1-SNAPSHOT:remapped-mojang') { transitive = false } } shadowJar { archiveBaseName.set('playit-minecraft-plugin') archiveClassifier.set('') archiveVersion.set(project.version as String) + + from sourceSets.v1_17.output + from sourceSets.v1_21.output + from sourceSets.v1_21_4.output + + exclude('META-INF/LICENSE') + exclude('META-INF/LICENSE*') + exclude('META-INF/NOTICE') + exclude('META-INF/NOTICE*') } +tasks.named('compileV1_17Java') { dependsOn compileJava } +tasks.named('compileV1_21Java') { dependsOn compileJava } +tasks.named('compileV1_21_4Java') { dependsOn compileJava } + +shadowJar.dependsOn compileV1_17Java, compileV1_21Java, compileV1_21_4Java build.dependsOn shadowJar diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..dff2f06 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx4g -Xms512m diff --git a/settings.gradle b/settings.gradle index c5984cf..cc03340 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,10 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + url = 'https://repo.screamingsandals.org/public' + } + } +} + rootProject.name = 'playit-minecraft-java-plugin' diff --git a/src/main/java/gg/playit/channel/BaseChannelInserter.java b/src/main/java/gg/playit/channel/BaseChannelInserter.java new file mode 100644 index 0000000..da199f5 --- /dev/null +++ b/src/main/java/gg/playit/channel/BaseChannelInserter.java @@ -0,0 +1,421 @@ +package gg.playit.channel; + +import io.netty.channel.AbstractChannel; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.timeout.ReadTimeoutHandler; +import org.bukkit.Server; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.List; +import java.util.logging.Logger; + +/** + * Abstract base class for version-specific ChannelInserter implementations. + * Contains shared logic for pipeline setup, address manipulation, and + * server instance retrieval that is common across all Minecraft versions. + */ +public abstract class BaseChannelInserter implements ChannelInserter { + protected static final Logger log = Logger.getLogger(BaseChannelInserter.class.getName()); + + protected final String version; + + /** + * @param version the Minecraft version string (e.g. "1.21.1") + */ + protected BaseChannelInserter(String version) { + this.version = version; + } + + /** + * Get the MinecraftServer instance from the Bukkit server. + * Works across all CraftBukkit versions by resolving the CraftServer class + * and calling getServer() or reading the console field. + */ + protected Object getMinecraftServer(Server server) { + Class minecraftServerClass; + try { + minecraftServerClass = Class.forName("net.minecraft.server.MinecraftServer"); + } catch (ClassNotFoundException e) { + return null; + } + + if (minecraftServerClass.isInstance(server)) { + return server; + } + + Class craftServerClass = resolveCraftServer(server); + if (craftServerClass == null) return null; + + // Try getServer() method + try { + Method m = craftServerClass.getMethod("getServer"); + m.setAccessible(true); + Object mcServer = m.invoke(server); + if (minecraftServerClass.isInstance(mcServer)) return mcServer; + } catch (Exception ignored) { + } + + // Try console field + try { + Field f = findField(craftServerClass, "console"); + if (f != null) { + f.setAccessible(true); + Object mcServer = f.get(server); + if (minecraftServerClass.isInstance(mcServer)) return mcServer; + } + } catch (Exception ignored) { + } + + return null; + } + + private Class resolveCraftServer(Server server) { + if (server != null) { + String pkg = server.getClass().getPackage().getName(); + if (pkg.startsWith("org.bukkit.craftbukkit")) { + try { + return Class.forName(pkg + ".CraftServer"); + } catch (ClassNotFoundException ignored) { + } + } + } + try { + return Class.forName("org.bukkit.craftbukkit.CraftServer"); + } catch (ClassNotFoundException e) { + return null; + } + } + + /** + * Get the ServerConnectionListener from the MinecraftServer. + */ + protected Object getServerConnection(Object minecraftServer, Class serverConnectionClass) { + log.info("resolved ServerConnectionListener to: " + serverConnectionClass.getName()); + log.info("MinecraftServer class: " + minecraftServer.getClass().getName()); + + // Search all fields of the MinecraftServer for one that is a ServerConnectionListener + Object result = searchForInstance(minecraftServer.getClass(), serverConnectionClass, minecraftServer); + if (result == null) { + // Diagnostic: log all fields in the hierarchy + for (Class c = minecraftServer.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { + for (Field f : c.getDeclaredFields()) { + if (f.getType().getName().contains("Connection") || f.getType().getName().contains("connection")) { + log.info(" candidate field in " + c.getSimpleName() + ": " + + f.getType().getName() + " " + f.getName()); + } + } + } + } + return result; + } + + /** + * Add a network manager to the server connection's connection list. + */ + protected boolean addToServerConnections(Object serverConnection, Object networkManager, + Class serverConnectionClass, Field connectionsField) { + if (connectionsField != null) { + try { + connectionsField.setAccessible(true); + @SuppressWarnings("unchecked") + List list = (List) connectionsField.get(serverConnection); + list.add(networkManager); + return true; + } catch (Exception ignored) { + } + } + + // Fallback: search for a List field in the server connection class + if (serverConnectionClass != null) { + for (Field f : serverConnectionClass.getDeclaredFields()) { + if (List.class.isAssignableFrom(f.getType())) { + try { + f.setAccessible(true); + @SuppressWarnings("unchecked") + List list = (List) f.get(serverConnection); + if (list != null) { + list.add(networkManager); + return true; + } + } catch (Exception ignored) { + } + } + } + } + return false; + } + + /** + * Set the remote address on a Netty channel (AbstractChannel.remoteAddress field). + */ + protected boolean setRemoteAddress(Channel channel, SocketAddress address) { + try { + Field field = AbstractChannel.class.getDeclaredField("remoteAddress"); + field.setAccessible(true); + field.set(channel, address); + return true; + } catch (Exception e) { + log.warning("failed to set remoteAddress: " + e); + return false; + } + } + + /** + * Set the address field on the Connection/NetworkManager object. + */ + protected boolean setConnectionAddress(Object networkManager, SocketAddress address, Field addressField) { + if (networkManager == null || addressField == null) return false; + if (!SocketAddress.class.isAssignableFrom(addressField.getType())) return false; + try { + addressField.setAccessible(true); + addressField.set(networkManager, address); + return true; + } catch (Exception e) { + log.warning("failed to set connection address: " + e); + return false; + } + } + + /** + * Set the packet listener on a network manager via a method or field. + */ + protected boolean setPacketListener(Object networkManager, Object listener, + Class connectionClass) { + + // Try setListener(PacketListener) method - search by name + for (Method m : connectionClass.getDeclaredMethods()) { + if (m.getParameterCount() == 1 && m.getName().equals("setListener")) { + try { + m.setAccessible(true); + m.invoke(networkManager, listener); + return true; + } catch (Exception ignored) { + } + } + } + + // Try setListenerForServerboundHandshake or similar + for (Method m : connectionClass.getDeclaredMethods()) { + if (m.getParameterCount() == 1 && m.getName().contains("Listener")) { + try { + m.setAccessible(true); + m.invoke(networkManager, listener); + return true; + } catch (Exception ignored) { + } + } + } + + // Try packetListener field + Field f = findField(connectionClass, "packetListener"); + if (f != null) { + try { + f.setAccessible(true); + f.set(networkManager, listener); + return true; + } catch (Exception ignored) { + } + } + + return false; + } + + /** + * Install the full Minecraft pipeline onto a tunnel channel. + * Returns true on success, false if adding to server connections failed (and rolls back). + */ + protected boolean setupPipeline(Channel tunnelChannel, InetSocketAddress trueIp, + int connectionTimeoutSeconds, + ChannelHandler legacyHandler, + Object packetSplitter, Object packetDecoder, + Object packetPrepender, Object packetEncoder, + Object networkManager, + Object serverConnection, + Class serverConnectionClass, + Field connectionsField, + Field addressField) { + if (!setRemoteAddress(tunnelChannel, trueIp)) { + log.warning("failed to set remote address to " + trueIp); + } + + ChannelHandler removed = tunnelChannel.pipeline().removeLast(); + tunnelChannel.pipeline() + .addLast("timeout", new ReadTimeoutHandler(connectionTimeoutSeconds)) + .addLast("legacy_query", legacyHandler) + .addLast("splitter", (ChannelHandler) packetSplitter) + .addLast("decoder", (ChannelHandler) packetDecoder) + .addLast("prepender", (ChannelHandler) packetPrepender) + .addLast("encoder", (ChannelHandler) packetEncoder) + .addLast("packet_handler", (ChannelHandler) networkManager); + + if (!addToServerConnections(serverConnection, networkManager, serverConnectionClass, connectionsField)) { + log.info("failed to add to server connections"); + tunnelChannel.pipeline().remove("timeout"); + tunnelChannel.pipeline().remove("legacy_query"); + tunnelChannel.pipeline().remove("splitter"); + tunnelChannel.pipeline().remove("decoder"); + tunnelChannel.pipeline().remove("prepender"); + tunnelChannel.pipeline().remove("encoder"); + tunnelChannel.pipeline().remove("packet_handler"); + tunnelChannel.pipeline().addLast(removed); + return false; + } + + tunnelChannel.pipeline().fireChannelActive(); + if (!setConnectionAddress(networkManager, trueIp, addressField)) { + log.warning("failed to set connection address to " + trueIp); + } + return true; + } + + /** + * Install the full Minecraft pipeline by delegating to Connection.addHandlers. + * This ensures the pipeline matches vanilla exactly (including UNBUNDLER/BUNDLER). + * Returns true on success, false if adding to server connections failed (and rolls back). + * + * @param monitor BandwidthDebugMonitor for Varint21FrameDecoder; may be null for older versions + */ + protected boolean setupPipelineWithAddHandlers(Channel tunnelChannel, InetSocketAddress trueIp, + int connectionTimeoutSeconds, + ChannelHandler legacyHandler, + Object networkManager, + Object monitor, + Object serverConnection, + Class serverConnectionClass, + Field connectionsField, + Field addressField) { + if (!setRemoteAddress(tunnelChannel, trueIp)) { + log.warning("failed to set remote address to " + trueIp); + } + + ChannelHandler removed = tunnelChannel.pipeline().removeLast(); + var pipeline = tunnelChannel.pipeline(); + + pipeline.addLast("timeout", new ReadTimeoutHandler(connectionTimeoutSeconds)); + pipeline.addLast("legacy_query", legacyHandler); + + if (!invokeAddHandlers(networkManager, pipeline, monitor)) { + log.info("Connection.addHandlers failed"); + pipeline.remove("timeout"); + pipeline.remove("legacy_query"); + pipeline.addLast(removed); + return false; + } + + if (pipeline.get("packet_handler") == null) { + pipeline.addLast("packet_handler", (ChannelHandler) networkManager); + } + + if (!addToServerConnections(serverConnection, networkManager, serverConnectionClass, connectionsField)) { + log.info("failed to add to server connections"); + rollbackPipeline(pipeline, removed); + return false; + } + + tunnelChannel.pipeline().fireChannelActive(); + if (!setConnectionAddress(networkManager, trueIp, addressField)) { + log.warning("failed to set connection address to " + trueIp); + } + return true; + } + + /** + * Invoke Connection.addHandlers(pipeline, PacketFlow.SERVERBOUND, false, monitor) via reflection. + */ + private boolean invokeAddHandlers(Object connection, io.netty.channel.ChannelPipeline pipeline, Object monitor) { + try { + Object serverBound = resolvePacketFlowServerbound(); + if (serverBound == null) return false; + + for (Method m : connection.getClass().getMethods()) { + if ((m.getName().equals("addHandlers") || m.getName().equals("configureSerialization")) + && m.getParameterCount() == 4) { + Class[] params = m.getParameterTypes(); + if (io.netty.channel.ChannelPipeline.class.isAssignableFrom(params[0]) + && params[2] == boolean.class + && (monitor == null || params[3].isInstance(monitor))) { + m.setAccessible(true); + m.invoke(connection, pipeline, serverBound, false, monitor); + return true; + } + } + } + } catch (Exception e) { + log.warning("failed to invoke addHandlers: " + e); + } + return false; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Object resolvePacketFlowServerbound() { + try { + Class flowClass = Class.forName("net.minecraft.network.protocol.PacketFlow"); + return Enum.valueOf((Class) flowClass, "SERVERBOUND"); + } catch (Exception e) { + return null; + } + } + + private void rollbackPipeline(io.netty.channel.ChannelPipeline pipeline, ChannelHandler removed) { + pipeline.remove("timeout"); + pipeline.remove("legacy_query"); + pipeline.remove("splitter"); + pipeline.remove("decoder"); + pipeline.remove("prepender"); + pipeline.remove("encoder"); + pipeline.remove("unbundler"); + pipeline.remove("bundler"); + pipeline.remove("packet_handler"); + pipeline.addLast(removed); + } + + /** + * Create a fallback legacy handler that passes through all reads. + */ + protected ChannelHandler createPassthroughHandler() { + return new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ctx.fireChannelRead(msg); + } + }; + } + + // --- Reflection utilities --- + + protected Field findField(Class subject, String name) { + if (subject == null || name == null) return null; + for (Class c = subject; c != null; c = c.getSuperclass()) { + try { + return c.getDeclaredField(name); + } catch (NoSuchFieldException ignored) { + } + } + return null; + } + + protected Object searchForInstance(Class parent, Class targetType, Object subject) { + // Walk up the class hierarchy to find fields declared in superclasses too + // (e.g., ServerConnectionListener is declared in MinecraftServer but + // the runtime class is DedicatedServer) + for (Class c = parent; c != null && c != Object.class; c = c.getSuperclass()) { + for (Field field : c.getDeclaredFields()) { + if (targetType.isAssignableFrom(field.getType())) { + try { + field.setAccessible(true); + Object res = field.get(subject); + if (targetType.isInstance(res)) return res; + } catch (Exception ignored) { + } + } + } + } + return null; + } +} diff --git a/src/main/java/gg/playit/channel/ChannelInserter.java b/src/main/java/gg/playit/channel/ChannelInserter.java new file mode 100644 index 0000000..6fa4017 --- /dev/null +++ b/src/main/java/gg/playit/channel/ChannelInserter.java @@ -0,0 +1,25 @@ +package gg.playit.channel; + +import io.netty.channel.Channel; +import org.bukkit.Server; + +import java.net.InetSocketAddress; + +/** + * Interface for inserting a Netty channel directly into a running Minecraft server's + * handler pipeline, bypassing the public network interface. + */ +public interface ChannelInserter { + /** + * Attempts to add the tunnel channel to the Minecraft server. + * + * @param server the Bukkit server + * @param tunnelChannel the channel to insert (from the tunnel connection) + * @param trueIp the client's real IP address + * @param connectionTimeoutSeconds timeout for the connection + * @param rateLimit rate limit packets per second (0 if none) + * @return true if the channel was successfully inserted + */ + boolean insertChannel(Server server, Channel tunnelChannel, InetSocketAddress trueIp, + int connectionTimeoutSeconds, int rateLimit); +} diff --git a/src/main/java/gg/playit/channel/ChannelInserterProvider.java b/src/main/java/gg/playit/channel/ChannelInserterProvider.java new file mode 100644 index 0000000..e210e3e --- /dev/null +++ b/src/main/java/gg/playit/channel/ChannelInserterProvider.java @@ -0,0 +1,197 @@ +package gg.playit.channel; + +import org.bukkit.Server; + +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Provides the correct {@link ChannelInserter} implementation for the detected + * Minecraft server version at runtime. + *

+ * Version detection uses the CraftBukkit package suffix (e.g. {@code v1_21_R2}) + * to determine the major version and revision. + */ +public final class ChannelInserterProvider { + private static final Logger log = Logger.getLogger(ChannelInserterProvider.class.getName()); + + /** + * Pattern to extract version components from CraftBukkit package suffix. + * Matches patterns like v1_16_R3, v1_21_R1, v1_21_R2, etc. + */ + private static final Pattern VERSION_PATTERN = Pattern.compile("v(\\d+)_(\\d+)_R(\\d+)"); + + private ChannelInserterProvider() {} + + /** + * Detects the Minecraft server version and returns an appropriate ChannelInserter, + * or null if the version is not supported. + * + * @param server the Bukkit server instance + * @return a ChannelInserter for the detected version, or null if unsupported + */ + public static ChannelInserter get(Server server) { + if (server == null) return null; + + String pkg = server.getClass().getPackage().getName(); + if (!pkg.startsWith("org.bukkit.craftbukkit")) { + log.info("server class package does not start with org.bukkit.craftbukkit: " + pkg); + return null; + } + + // Handle flat CraftBukkit packaging (e.g., Paper 1.20.5+ uses "org.bukkit.craftbukkit" with no version suffix) + if (pkg.equals("org.bukkit.craftbukkit") || !pkg.startsWith("org.bukkit.craftbukkit.")) { + return fromBukkitVersion(server); + } + + // Extract the version suffix (e.g., "v1_21_R2") + String suffix = pkg.substring("org.bukkit.craftbukkit.".length()); + if (suffix.isEmpty() || !suffix.startsWith("v")) { + return fromBukkitVersion(server); + } + + // If the suffix has further dots (e.g., "v1_21_R2.something"), take only the first part + int dotIdx = suffix.indexOf('.'); + if (dotIdx > 0) { + suffix = suffix.substring(0, dotIdx); + } + + Matcher m = VERSION_PATTERN.matcher(suffix); + if (!m.matches()) { + log.info("could not parse CraftBukkit version suffix: " + suffix); + return fromBukkitVersion(server); + } + + int major = Integer.parseInt(m.group(1)); + int minor = Integer.parseInt(m.group(2)); + int revision = Integer.parseInt(m.group(3)); + + return createInserter(major, minor, revision); + } + + /** + * Attempt to determine the Minecraft version from {@code Bukkit.getVersion()} + * or {@code Bukkit.getBukkitVersion()} for servers without a versioned CraftBukkit package + * (e.g., Paper 1.20.5+ with flat packaging). + */ + private static ChannelInserter fromBukkitVersion(Server server) { + String bukkitVersion = server.getBukkitVersion(); // e.g., "1.21.1-R0.1-SNAPSHOT" + if (bukkitVersion == null) return null; + + // Parse "1.21.1-R0.1-SNAPSHOT" -> major=1, minor=21, patch=1 + String[] parts = bukkitVersion.split("-")[0].split("\\."); + if (parts.length < 2) return null; + + try { + int minor = Integer.parseInt(parts[1]); + int patch = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + + // Map to approximate version group + // We use revision=1 as default; for 1.21.4+ we need revision=2 + if (minor >= 21 && patch >= 4) { + String mcVersion = "1." + minor + "." + patch; + return createInserter("ChannelInserterV1_21_4", mcVersion); + } else if (minor >= 21) { + String mcVersion = "1." + minor + (patch > 0 ? "." + patch : ""); + return createInserter("ChannelInserterV1_21", mcVersion); + } else if (minor >= 17) { + String mcVersion = "1." + minor + (patch > 0 ? "." + patch : ""); + return createInserter("ChannelInserterV1_17", mcVersion); + } else if (minor == 16) { + String mcVersion = "1.16" + (patch > 0 ? "." + patch : ""); + return createInserter("ChannelInserterV1_16", mcVersion); + } + } catch (NumberFormatException ignored) { + } + + log.info("unsupported Minecraft version: " + bukkitVersion); + return null; + } + + /** + * Create a ChannelInserter based on parsed version components from the + * CraftBukkit package suffix (e.g., v1_21_R2 -> major=1, minor=21, revision=2). + */ + private static ChannelInserter createInserter(int major, int minor, int revision) { + if (major != 1) { + log.info("unsupported major version: " + major); + return null; + } + + // Map CraftBukkit package suffix to Minecraft version string + String mcVersion = toMcVersion(minor, revision); + + if (minor >= 21 && revision >= 2) { + // v1_21_R2 = 1.21.4+ (BandwidthDebugMonitor required) + log.info("using ChannelInserterV1_21_4 for v" + major + "_" + minor + "_R" + revision); + return createInserter("ChannelInserterV1_21_4", mcVersion); + } else if (minor >= 21) { + // v1_21_R1 = 1.21.0-1.21.3 (ProtocolInfo-based) + log.info("using ChannelInserterV1_21 for v" + major + "_" + minor + "_R" + revision); + return createInserter("ChannelInserterV1_21", mcVersion); + } else if (minor >= 17) { + // v1_17_R1 through v1_20_R3 + log.info("using ChannelInserterV1_17 for v" + major + "_" + minor + "_R" + revision); + return createInserter("ChannelInserterV1_17", mcVersion); + } else if (minor == 16) { + // v1_16_R3 + log.info("using ChannelInserterV1_16 for v" + major + "_" + minor + "_R" + revision); + return createInserter("ChannelInserterV1_16", mcVersion); + } + + log.info("unsupported Minecraft version: 1." + minor); + return null; + } + + /** + * Create a ChannelInserter by loading the version-specific class via reflection. + * This allows main to compile without depending on version source sets. + */ + private static ChannelInserter createInserter(String className, String mcVersion) { + try { + Class clazz = Class.forName("gg.playit.channel." + className); + return (ChannelInserter) clazz.getConstructor(String.class).newInstance(mcVersion); + } catch (Exception e) { + log.warning("failed to load " + className + ": " + e); + return null; + } + } + + /** + * Map a CraftBukkit minor version + revision to the closest Minecraft version string. + */ + private static String toMcVersion(int minor, int revision) { + return switch (minor) { + case 16 -> switch (revision) { + case 3 -> "1.16.5"; + default -> "1.16.5"; + }; + case 17 -> "1.17.1"; + case 18 -> switch (revision) { + case 1 -> "1.18.1"; + case 2 -> "1.18.2"; + default -> "1.18.2"; + }; + case 19 -> switch (revision) { + case 1 -> "1.19.2"; + case 2 -> "1.19.3"; + case 3 -> "1.19.4"; + default -> "1.19.4"; + }; + case 20 -> switch (revision) { + case 1 -> "1.20.1"; + case 2 -> "1.20.2"; + case 3 -> "1.20.4"; + case 4 -> "1.20.6"; + default -> "1.20.6"; + }; + case 21 -> switch (revision) { + case 1 -> "1.21.1"; + case 2 -> "1.21.4"; + default -> "1.21.4"; + }; + default -> "1." + minor; + }; + } +} diff --git a/src/main/java/gg/playit/channel/ChannelInserterV1_16.java b/src/main/java/gg/playit/channel/ChannelInserterV1_16.java new file mode 100644 index 0000000..53dc721 --- /dev/null +++ b/src/main/java/gg/playit/channel/ChannelInserterV1_16.java @@ -0,0 +1,229 @@ +package gg.playit.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import org.bukkit.Server; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; + +/** + * ChannelInserter for Minecraft 1.16.x. + *

+ * Uses reflection to access NMS classes (Spigot 1.16 has no remapped-mojang artifact). + * Uses the old NMS naming convention: + *

    + *
  • NetworkManager instead of Connection
  • + *
  • ServerConnection instead of ServerConnectionListener
  • + *
  • LegacyPingHandler instead of LegacyQueryHandler
  • + *
  • PacketSplitter instead of Varint21FrameDecoder
  • + *
  • EnumProtocolDirection instead of PacketFlow
  • + *
+ * LegacyPingHandler takes ServerConnection as constructor arg. + */ +public final class ChannelInserterV1_16 extends BaseChannelInserter { + + private static final String[] SERVER_CONNECTION_NAMES = { + "net.minecraft.server.network.ServerConnection", + "net.minecraft.server.network.ServerConnectionListener" + }; + private static final String[] CONNECTION_NAMES = { + "net.minecraft.network.NetworkManager", + "net.minecraft.network.Connection" + }; + + public ChannelInserterV1_16(String version) { + super(version); + } + + @Override + public boolean insertChannel(Server server, Channel tunnelChannel, InetSocketAddress trueIp, + int connectionTimeoutSeconds, int rateLimit) { + ClassLoader loader = nmsClassLoader(); + + Class serverConnectionClass = loadClass(loader, SERVER_CONNECTION_NAMES); + Class connectionClass = loadClass(loader, CONNECTION_NAMES); + if (serverConnectionClass == null || connectionClass == null) { + log.warning("could not load 1.16 NMS classes"); + return false; + } + + Field connectionsField = findField(serverConnectionClass, "connections"); + Field addressField = findField(connectionClass, "address"); + if (connectionsField == null || addressField == null) { + log.warning("could not find connections or address field"); + return false; + } + + Object minecraftServer = getMinecraftServer(server); + if (minecraftServer == null) { + log.info("failed to get Minecraft server from Bukkit.getServer()"); + return false; + } + + Object serverConnection = getServerConnection(minecraftServer, serverConnectionClass); + if (serverConnection == null) { + log.info("failed to get ServerConnection from Minecraft Server"); + return false; + } + + ChannelHandler legacyHandler = createLegacyHandler(loader, serverConnection); + Object packetSplitter = createInstance(loader, "net.minecraft.network.PacketSplitter", "net.minecraft.network.Varint21FrameDecoder"); + if (packetSplitter == null) { + log.info("packetSplitter construction failed"); + return false; + } + + Class packetFlowClass = loadClass(loader, "net.minecraft.network.protocol.EnumProtocolDirection", "net.minecraft.network.protocol.PacketFlow"); + Object serverBound = resolveEnumValue(packetFlowClass, "SERVERBOUND"); + if (serverBound == null) { + log.info("failed to resolve SERVERBOUND enum"); + return false; + } + + Object packetDecoder = createInstance(loader, packetFlowClass, serverBound, "PacketDecoder"); + if (packetDecoder == null) { + log.info("packetDecoder construction failed"); + return false; + } + + Object packetPrepender = createInstance(loader, "net.minecraft.network.PacketPrepender", "net.minecraft.network.Varint21LengthFieldPrepender"); + if (packetPrepender == null) { + log.info("packetPrepender construction failed"); + return false; + } + + Object clientBound = resolveEnumValue(packetFlowClass, "CLIENTBOUND"); + Object packetEncoder = createInstance(loader, packetFlowClass, clientBound != null ? clientBound : serverBound, "PacketEncoder"); + if (packetEncoder == null) { + log.info("packetEncoder construction failed"); + return false; + } + + Object networkManager = createNetworkManager(loader, connectionClass, packetFlowClass, serverBound, rateLimit); + if (networkManager == null) { + log.info("networkManager construction failed"); + return false; + } + + Object handshakeListener = createHandshakeListener(loader, minecraftServer, networkManager); + if (handshakeListener == null) { + log.info("handshakeListener construction failed"); + return false; + } + + if (!setPacketListener(networkManager, handshakeListener, connectionClass)) { + log.info("failed to set handshake listener on network manager"); + return false; + } + + return setupPipeline(tunnelChannel, trueIp, connectionTimeoutSeconds, + legacyHandler, packetSplitter, packetDecoder, packetPrepender, packetEncoder, + networkManager, serverConnection, serverConnectionClass, connectionsField, addressField); + } + + private ClassLoader nmsClassLoader() { + return getClass().getClassLoader(); + } + + private Class loadClass(ClassLoader loader, String... names) { + for (String name : names) { + try { + return Class.forName(name, true, loader); + } catch (ClassNotFoundException ignored) { + } + } + return null; + } + + private ChannelHandler createLegacyHandler(ClassLoader loader, Object serverConnection) { + String[] names = {"net.minecraft.server.network.LegacyPingHandler", "net.minecraft.server.network.LegacyQueryHandler"}; + for (String name : names) { + try { + Class c = Class.forName(name, true, loader); + for (Constructor ctor : c.getConstructors()) { + if (ctor.getParameterCount() == 1 && ctor.getParameterTypes()[0].isInstance(serverConnection)) { + return (ChannelHandler) ctor.newInstance(serverConnection); + } + } + } catch (Exception ignored) { + } + } + log.warning("LegacyPingHandler constructor not found, using pass-through"); + return createPassthroughHandler(); + } + + private Object createInstance(ClassLoader loader, String... classNames) { + for (String name : classNames) { + try { + Class c = Class.forName(name, true, loader); + return c.getConstructor().newInstance(); + } catch (Exception ignored) { + } + } + return null; + } + + private Object createInstance(ClassLoader loader, Class paramClass, Object param, String baseName) { + String[] packages = {"net.minecraft.network.", "net.minecraft.server.network."}; + for (String pkg : packages) { + try { + Class c = Class.forName(pkg + baseName, true, loader); + Constructor ctor = c.getConstructor(paramClass); + return ctor.newInstance(param); + } catch (Exception ignored) { + } + } + return null; + } + + private Object createNetworkManager(ClassLoader loader, Class connectionClass, Class packetFlowClass, + Object serverBound, int rateLimit) { + if (rateLimit > 0) { + String[] rateNames = {"net.minecraft.server.network.NetworkManagerServer", "net.minecraft.network.RateKickingConnection"}; + for (String name : rateNames) { + try { + Class c = Class.forName(name, true, loader); + return c.getConstructor(int.class).newInstance(rateLimit); + } catch (Exception ignored) { + } + } + } + try { + Constructor ctor = connectionClass.getConstructor(packetFlowClass); + return ctor.newInstance(serverBound); + } catch (Exception e) { + log.info("Connection construction failed: " + e); + } + return null; + } + + private Object createHandshakeListener(ClassLoader loader, Object minecraftServer, Object networkManager) { + String[] names = {"net.minecraft.server.network.HandshakeListener", "net.minecraft.server.network.ServerHandshakePacketListenerImpl"}; + for (String name : names) { + try { + Class c = Class.forName(name, true, loader); + for (Constructor cons : c.getConstructors()) { + if (cons.getParameterCount() == 2 + && cons.getParameterTypes()[0].isInstance(minecraftServer) + && cons.getParameterTypes()[1].isInstance(networkManager)) { + return cons.newInstance(minecraftServer, networkManager); + } + } + } catch (Exception ignored) { + } + } + return null; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Object resolveEnumValue(Class enumClass, String name) { + if (enumClass == null || !enumClass.isEnum()) return null; + try { + return Enum.valueOf((Class) enumClass, name); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/gg/playit/control/ChannelSetup.java b/src/main/java/gg/playit/control/ChannelSetup.java index 869e7dc..4960614 100644 --- a/src/main/java/gg/playit/control/ChannelSetup.java +++ b/src/main/java/gg/playit/control/ChannelSetup.java @@ -29,9 +29,6 @@ public class ChannelSetup { /** Default Minecraft plugin variant ID (UUID) */ private static final String PLUGIN_VARIANT_ID = "308943e8-faef-4835-a2ba-270351f72aa3"; - /** Default agent version when none is provided (0.1.4) */ - public static final AgentVersion DEFAULT_AGENT_VERSION = new AgentVersion(PLUGIN_VARIANT_ID, 0, 1, 4); - static Logger log = Logger.getLogger(ChannelSetup.class.getName()); /** @@ -84,7 +81,7 @@ public static FindSuitableChannel start(String secretKey, AgentVersion agentVers var setup = new FindSuitableChannel(); setup.options = addresses.toArray(new InetSocketAddress[0]); setup.apiClient = apiClient; - setup.agentVersion = agentVersion != null ? agentVersion : DEFAULT_AGENT_VERSION; + setup.agentVersion = agentVersion; return setup; } diff --git a/src/main/java/gg/playit/minecraft/PlayitTcpTunnel.java b/src/main/java/gg/playit/minecraft/PlayitTcpTunnel.java index 53bd687..f62ff06 100644 --- a/src/main/java/gg/playit/minecraft/PlayitTcpTunnel.java +++ b/src/main/java/gg/playit/minecraft/PlayitTcpTunnel.java @@ -6,7 +6,6 @@ import io.netty.channel.*; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.timeout.ReadTimeoutHandler; import org.bukkit.Server; import java.net.InetSocketAddress; @@ -177,116 +176,46 @@ protected void initChannel(SocketChannel socketChannel) { } private boolean addChannelToMinecraftServer() { - ReflectionHelper reflect = new ReflectionHelper(server); - log.info("Reflect: " + reflect); - - Object minecraftServer = reflect.getMinecraftServer(server); - if (minecraftServer == null) { - log.info("failed to get Minecraft server from Bukkit.getServer()"); - return false; - } - - Object serverConnection = reflect.serverConnectionFromMCServer(minecraftServer); - if (serverConnection == null) { - log.info("failed to get ServerConnection from Minecraft Server"); - return false; - } - - Object legacyPingHandler = reflect.newLegacyPingHandler(serverConnection); - if (legacyPingHandler == null) { - log.info("legacyPingHandler is null"); - return false; - } - - Object packetSplitter = reflect.newPacketSplitter(); - if (packetSplitter == null) { - log.info("packetSplitter is null"); + gg.playit.channel.ChannelInserter inserter = gg.playit.channel.ChannelInserterProvider.get(server); + if (inserter == null) { + log.info("no ChannelInserter for this Minecraft version"); return false; } - Object packetDecoder = reflect.newServerBoundPacketDecoder(); - if (packetDecoder == null) { - log.info("packetDecoder is null"); - return false; - } - - Object packetPrepender = reflect.newPacketPrepender(); - if (packetPrepender == null) { - log.info("packetPrepender is null"); - return false; - } - - Object packetEncoder = reflect.newClientBoundPacketEncoder(); - if (packetEncoder == null) { - log.info("packetEncoder is null"); - return false; - } - - Integer rateLimitNullable = reflect.getRateLimitFromMCServer(minecraftServer); - if (rateLimitNullable == null) { - rateLimitNullable = 0; - } - - int rateLimit = rateLimitNullable; - - Object networkManager; - if (rateLimit > 0) { - networkManager = reflect.newNetworkManagerServer(rateLimit); - } else { - networkManager = reflect.newServerNetworkManager(); - } - - if (networkManager == null) { - log.info("networkManager is null"); - return false; - } - - Object handshakeListener = reflect.newHandshakeListener(minecraftServer, networkManager); - if (handshakeListener == null) { - log.info("handshakeListener is null"); - return false; - } - - if (!reflect.networkManagerSetListener(networkManager, handshakeListener)) { - log.info("failed to set handshake listener on network manager"); - return false; - } - - if (!reflect.setRemoteAddress(tunnelChannel, trueIp)) { - log.warning("failed to set remote address to " + trueIp); + int rateLimit = 0; + try { + Object minecraftServer = getMinecraftServerForRateLimit(); + if (minecraftServer != null) { + Integer rl = getRateLimitFromMinecraftServer(minecraftServer); + if (rl != null) rateLimit = rl; + } + } catch (Exception ignored) { } - var channel = tunnelChannel.pipeline().removeLast(); - tunnelChannel.pipeline() - .addLast("timeout", new ReadTimeoutHandler(connectionTimeoutSeconds)) - .addLast("legacy_query", (ChannelHandler) legacyPingHandler) - .addLast("splitter", (ChannelHandler) packetSplitter) - .addLast("decoder", (ChannelHandler) packetDecoder) - .addLast("prepender", (ChannelHandler) packetPrepender) - .addLast("encoder", (ChannelHandler) packetEncoder) - .addLast("packet_handler", (ChannelHandler) networkManager); - - if (!reflect.addToServerConnections(serverConnection, networkManager)) { - log.info("failed to add to server connections"); - - tunnelChannel.pipeline().remove("timeout"); - tunnelChannel.pipeline().remove("legacy_query"); - tunnelChannel.pipeline().remove("splitter"); - tunnelChannel.pipeline().remove("decoder"); - tunnelChannel.pipeline().remove("prepender"); - tunnelChannel.pipeline().remove("encoder"); - tunnelChannel.pipeline().remove("packet_handler"); - - tunnelChannel.pipeline().addLast(channel); + return inserter.insertChannel(server, tunnelChannel, trueIp, connectionTimeoutSeconds, rateLimit); + } - return false; + private Object getMinecraftServerForRateLimit() { + try { + Class craftServer = server.getClass(); + if (craftServer.getName().startsWith("org.bukkit.craftbukkit")) { + Object mc = craftServer.getMethod("getServer").invoke(server); + if (mc != null && mc.getClass().getName().equals("net.minecraft.server.MinecraftServer")) { + return mc; + } + } + } catch (Exception ignored) { } + return null; + } - tunnelChannel.pipeline().fireChannelActive(); - if (!reflect.setConnectionAddress(networkManager, trueIp)) { - log.warning("failed to set connection address to " + trueIp); + private Integer getRateLimitFromMinecraftServer(Object minecraftServer) { + try { + var m = minecraftServer.getClass().getMethod("getRateLimitPacketsPerSecond"); + return (Integer) m.invoke(minecraftServer); + } catch (Exception e) { + return null; } - return true; } } diff --git a/src/main/java/gg/playit/minecraft/ReflectionHelper.java b/src/main/java/gg/playit/minecraft/ReflectionHelper.java deleted file mode 100644 index edb5235..0000000 --- a/src/main/java/gg/playit/minecraft/ReflectionHelper.java +++ /dev/null @@ -1,601 +0,0 @@ -package gg.playit.minecraft; - -import io.netty.channel.AbstractChannel; -import io.netty.channel.Channel; -import org.bukkit.Server; - -import java.lang.reflect.*; -import java.net.SocketAddress; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.logging.Logger; - -public class ReflectionHelper { - static Logger log = Logger.getLogger(ReflectionHelper.class.getName()); - - private final Class ServerConnection; - private final Class LegacyPingHandler; - private final Class MinecraftServer; - - private final Class PacketSplitter; - private final Class PacketDecoder; - private final Class EnumProtocolDirection; - private final Class PacketPrepender; - private final Class PacketEncoder; - private final Class NetworkManagerServer; - private final Class NetworkManager; - private final Class HandshakeListener; - private final Class PacketListener; - - private final Class CraftServer; - - public ReflectionHelper() { - this(null); - } - - public ReflectionHelper(Server server) { - ServerConnection = cls( - "net.minecraft.server.network.ServerConnection", - "net.minecraft.server.network.ServerConnectionListener"); - LegacyPingHandler = cls( - "net.minecraft.server.network.LegacyPingHandler", - "net.minecraft.server.network.LegacyQueryHandler"); - MinecraftServer = cls("net.minecraft.server.MinecraftServer"); - PacketSplitter = cls( - "net.minecraft.network.PacketSplitter", - "net.minecraft.network.Varint21FrameDecoder"); - PacketDecoder = cls("net.minecraft.network.PacketDecoder"); - EnumProtocolDirection = cls( - "net.minecraft.network.protocol.EnumProtocolDirection", - "net.minecraft.network.protocol.ConnectionProtocol"); - PacketPrepender = cls( - "net.minecraft.network.PacketPrepender", - "net.minecraft.network.Varint21LengthFieldPrepender"); - PacketEncoder = cls("net.minecraft.network.PacketEncoder"); - NetworkManagerServer = cls( - "net.minecraft.network.NetworkManagerServer", - "net.minecraft.network.RateKickingConnection"); - NetworkManager = cls( - "net.minecraft.network.NetworkManager", - "net.minecraft.network.Connection"); - HandshakeListener = cls( - "net.minecraft.server.network.HandshakeListener", - "net.minecraft.server.network.ServerHandshakePacketListenerImpl"); - PacketListener = cls("net.minecraft.network.PacketListener"); - CraftServer = resolveCraftServer(server); - } - - private static Class resolveCraftServer(Server server) { - if (server != null) { - String pkg = server.getClass().getPackage().getName(); - if (pkg.startsWith("org.bukkit.craftbukkit")) { - Class c = cls(pkg + ".CraftServer"); - if (c != null) return c; - } - } - return cls( - "org.bukkit.craftbukkit.CraftServer", - "org.bukkit.craftbukkit.v1_21_R1.CraftServer", - "org.bukkit.craftbukkit.v1_20_R3.CraftServer", - "org.bukkit.craftbukkit.v1_20_R1.CraftServer", - "org.bukkit.craftbukkit.v1_19_R3.CraftServer", - "org.bukkit.craftbukkit.v1_19_R1.CraftServer", - "org.bukkit.craftbukkit.v1_16_R3.CraftServer"); - } - - static Class cls(String className) { - try { - return Class.forName(className); - } catch (ClassNotFoundException ignored) { - return null; - } - } - - static Class cls(String... classNames) { - for (var name : classNames) { - var res = cls(name); - if (res != null) { - return res; - } - } - return null; - } - - public boolean networkManagerSetListener(Object networkManager, Object listener) { - if (NetworkManager == null || PacketListener == null) { - return false; - } - - try { - Method method = searchMethod(NetworkManager, "setListener", PacketListener); - method.setAccessible(true); - method.invoke(networkManager, listener); - return true; - } catch (Exception e) { - log.warning("failed to call setListener: " + e); - } - - try { - var field = searchForFieldByName(NetworkManager, "packetListener"); - field.setAccessible(true); - field.set(networkManager, listener); - return true; - } catch (Exception e) { - log.warning("failed to set packetListener" + e); - } - - var options = searchForFieldByType(NetworkManager, PacketListener); - if (options.size() == 1) { - try { - options.get(0).setAccessible(true); - options.get(0).set(networkManager, listener); - return true; - } catch (Exception e) { - log.warning("failed to set packetListener directly to type" + options.get(0) + ", error: " + e); - } - } else { - log.warning("got multiple options for packet listener field: " + options); - } - - return false; - } - - public boolean addToServerConnections(Object serverConnection, Object networkManager) { - if (ServerConnection == null || NetworkManager == null) { - return false; - } - - try { - Field field = searchForFieldByName(ServerConnection, "connections"); - field.setAccessible(true); - - var list = (List) field.get(serverConnection); - list.add(networkManager); - - return true; - } catch (Exception e) { - log.warning("failed set field connections, error: " + e); - } - - try { - Method getConnections = searchMethod(ServerConnection, "getConnections"); - getConnections.setAccessible(true); - @SuppressWarnings("unchecked") - List list = (List) getConnections.invoke(serverConnection); - if (list != null) { - list.add(networkManager); - return true; - } - } catch (Exception e) { - log.warning("failed to add via getConnections(), error: " + e); - } - - HashSet potentialFieldObjects = new HashSet<>(); - - var search = ServerConnection; - while (search != null) { - for (var field : search.getDeclaredFields()) { - if (List.class.isAssignableFrom(field.getType())) { - if (field.getGenericType() instanceof ParameterizedType parameterizedType) { - var type = parameterizedType.getActualTypeArguments()[0]; - var typeClass = cls(type.getTypeName()); - - if (typeClass != null && NetworkManager != null && NetworkManager.isAssignableFrom(typeClass)) { - try { - field.setAccessible(true); - potentialFieldObjects.add(field.get(serverConnection)); - } catch (Exception ignore) { - } - } - } - } - } - search = search.getSuperclass(); - } - - if (potentialFieldObjects.size() == 1) { - var found = potentialFieldObjects.toArray()[0]; - try { - @SuppressWarnings("unchecked") - var list = (List) found; - list.add(networkManager); - return true; - } catch (Exception e) { - log.warning("failed to add connection to " + found + ", error: " + e); - } - } else if (potentialFieldObjects.size() > 1) { - log.warning("multiple connection lists: " + potentialFieldObjects); - } - - return false; - } - - public Object newHandshakeListener(Object minecraftServer, Object networkManager) { - if (HandshakeListener == null || MinecraftServer == null) { - return null; - } - - try { - return HandshakeListener.getConstructor(MinecraftServer, NetworkManager).newInstance(minecraftServer, networkManager); - } catch (Exception ignored) { - } - - try { - return HandshakeListener.getConstructor(MinecraftServer, Class.forName("net.minecraft.network.Connection")).newInstance(minecraftServer, networkManager); - } catch (Exception ignored) { - } - - return null; - } - - public boolean setRemoteAddress(Channel channel, SocketAddress address) { - try { - Field field = AbstractChannel.class.getDeclaredField("remoteAddress"); - field.setAccessible(true); - field.set(channel, address); - return true; - } catch (Exception error) { - log.warning("failed to set remoteAddress, error: " + error); - return false; - } - } - - /** - * Sets the address field on Minecraft's Connection/NetworkManager object. - * This is the source of truth for Player.getAddress() and works across - * Minecraft versions (NetworkManager pre-1.20, Connection 1.20+) and - * Java 17+ where Netty's AbstractChannel.remoteAddress reflection fails. - */ - public boolean setConnectionAddress(Object networkManager, SocketAddress address) { - if (networkManager == null) { - return false; - } - Class connClass = networkManager.getClass(); - - try { - Field field = searchForFieldByName(connClass, "address"); - if (SocketAddress.class.isAssignableFrom(field.getType())) { - field.setAccessible(true); - field.set(networkManager, address); - return true; - } - } catch (NoSuchFieldException ignored) { - } catch (Exception e) { - log.warning("failed to set connection address via 'address' field, error: " + e); - } - - var options = searchForFieldByType(connClass, SocketAddress.class); - if (options.size() == 1) { - try { - Field field = options.get(0); - field.setAccessible(true); - field.set(networkManager, address); - return true; - } catch (Exception e) { - log.warning("failed to set connection address via type search, error: " + e); - } - } else if (options.size() > 1) { - log.warning("multiple SocketAddress fields on " + connClass + ", cannot set address"); - } - - return false; - } - - public Integer getRateLimitFromMCServer(Object server) { - if (MinecraftServer == null) { - return null; - } - - try { - return (Integer) searchMethod(MinecraftServer, "getRateLimitPacketsPerSecond").invoke(server); - } catch (Exception e) { - return null; - } - } - - public Object newLegacyPingHandler(Object serverConnection) { - if (LegacyPingHandler == null || ServerConnection == null) { - return null; - } - - try { - return LegacyPingHandler.getConstructor(ServerConnection).newInstance(serverConnection); - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | - IllegalAccessException ignored) { - } - - try { - return LegacyPingHandler.getConstructor(Class.forName("net.minecraft.server.network.ServerConnectionListener")).newInstance(serverConnection); - } catch (Exception ignored) { - } - - return null; - } - - public Object newPacketSplitter() { - if (PacketSplitter == null) { - return null; - } - - try { - return PacketSplitter.getConstructor().newInstance(); - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | - IllegalAccessException e) { - return null; - } - } - - public Object newServerBoundPacketDecoder() { - if (PacketDecoder == null) { - return null; - } - - try { - return PacketDecoder.getConstructor(EnumProtocolDirection).newInstance(serverBound()); - } catch (Exception e) { - return null; - } - } - - public Object newClientBoundPacketEncoder() { - if (PacketEncoder == null) { - return null; - } - - try { - return PacketEncoder.getConstructor(EnumProtocolDirection).newInstance(clientBound()); - } catch (Exception e) { - return null; - } - } - - public Object newPacketPrepender() { - if (PacketPrepender == null) { - return null; - } - - try { - return PacketPrepender.getConstructor().newInstance(); - } catch (Exception e) { - return null; - } - } - - public Object newNetworkManagerServer(int rateLimit) { - if (NetworkManagerServer == null) { - return null; - } - - try { - return NetworkManagerServer.getConstructor(Integer.class).newInstance(rateLimit); - } catch (Exception e) { - return null; - } - } - - public Object newServerNetworkManager() { - if (NetworkManager == null) { - return null; - } - - try { - return NetworkManager.getConstructor(EnumProtocolDirection).newInstance(serverBound()); - } catch (Exception e) { - return null; - } - } - - private Object serverBound() { - for (Class enumClass : new Class[]{ - EnumProtocolDirection, - cls("net.minecraft.network.protocol.PacketFlow") - }) { - if (enumClass != null) { - try { - return Enum.valueOf((Class) enumClass, "SERVERBOUND"); - } catch (Exception ignored) { - } - } - } - return null; - } - - private Object clientBound() { - for (Class enumClass : new Class[]{ - EnumProtocolDirection, - cls("net.minecraft.network.protocol.PacketFlow") - }) { - if (enumClass != null) { - try { - return Enum.valueOf((Class) enumClass, "CLIENTBOUND"); - } catch (Exception ignored) { - } - } - } - return null; - } - - public Object getMinecraftServer(Server server) { - if (MinecraftServer == null) { - return null; - } - if (MinecraftServer.isInstance(server)) { - return server; - } - - if (CraftServer != null) { - try { - Method method = searchMethod(CraftServer, "getServer"); - method.setAccessible(true); - - Object mcServer = method.invoke(server); - if (MinecraftServer.isInstance(mcServer)) { - return mcServer; - } - } catch (Exception ignore) { - } - - try { - var field = searchForFieldByName(CraftServer, "console"); - field.setAccessible(true); - Object mcServer = field.get(server); - if (MinecraftServer.isInstance(mcServer)) { - return mcServer; - } - } catch (Exception ignore) { - } - } - - - return null; - } - - public Object serverConnectionFromMCServer(Object object) { - if (object == null || MinecraftServer == null) { - return null; - } - - try { - Method getConnection = searchMethod(MinecraftServer, "getConnection"); - getConnection.setAccessible(true); - var res = getConnection.invoke(object); - if (ServerConnection.isInstance(res)) { - return res; - } - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ignore) { - } - - try { - Field field = searchForFieldByName(MinecraftServer, "connection"); - field.setAccessible(true); - var res = field.get(object); - if (ServerConnection != null && ServerConnection.isInstance(res)) { - return res; - } - } catch (Exception ignored) { - } - - if (ServerConnection != null) { - return searchForAttribute(MinecraftServer, ServerConnection, object); - } - return null; - } - - public Method searchMethod(Class subject, String name, Class... parameterTypes) throws NoSuchMethodException { - try { - return subject.getMethod(name, parameterTypes); - } catch (Exception ignore) { - } - - while (subject != null) { - try { - return subject.getDeclaredMethod(name, parameterTypes); - } catch (Exception ignore) { - } - - for (var method : subject.getDeclaredMethods()) { - Class[] searchParamTypes = method.getParameterTypes(); - if (!method.getName().equals(name) || searchParamTypes.length != parameterTypes.length) { - continue; - } - - boolean match = true; - for (int i = 0; i < parameterTypes.length; i++) { - if (!searchParamTypes[i].isAssignableFrom(parameterTypes[i])) { - match = false; - break; - } - } - - if (match) { - return method; - } - } - - subject = subject.getSuperclass(); - } - - throw new NoSuchMethodException(name); - } - - public Object searchForAttribute(Class parent, Class child, Object subject) { - for (var field : parent.getFields()) { - if (child.isAssignableFrom(field.getType())) { - try { - field.setAccessible(true); - var res = field.get(subject); - if (child.isInstance(res)) { - return res; - } - } catch (Exception ignore) { - } - } - } - - for (var field : parent.getDeclaredFields()) { - if (child.isAssignableFrom(field.getType())) { - try { - field.setAccessible(true); - var res = field.get(subject); - if (child.isInstance(res)) { - return res; - } - } catch (Exception ignore) { - } - } - } - - return null; - } - - public Field searchForFieldByName(Class subject, String name) throws NoSuchFieldException { - while (subject != null) { - try { - return subject.getDeclaredField(name); - } catch (Exception ignore) { - } - subject = subject.getSuperclass(); - } - - throw new NoSuchFieldException(name); - } - - public List searchForFieldByType(Class subject, Class type) { - var fields = new ArrayList(); - - while (subject != null) { - try { - for (var f : subject.getDeclaredFields()) { - if (type.isAssignableFrom(f.getType())) { - fields.add(f); - } - } - } catch (Exception ignore) { - } - - subject = subject.getSuperclass(); - } - - return fields; - } - - @Override - public String toString() { - return "ReflectionHelper{" + - "ServerConnection=" + ServerConnection + - ", LegacyPingHandler=" + LegacyPingHandler + - ", MinecraftServer=" + MinecraftServer + - ", PacketSplitter=" + PacketSplitter + - ", PacketDecoder=" + PacketDecoder + - ", EnumProtocolDirection=" + EnumProtocolDirection + - ", PacketPrepender=" + PacketPrepender + - ", PacketEncoder=" + PacketEncoder + - ", NetworkManagerServer=" + NetworkManagerServer + - ", NetworkManager=" + NetworkManager + - ", HandshakeListener=" + HandshakeListener + - ", PacketListener=" + PacketListener + - ", CraftServer=" + CraftServer + - '}'; - } -} diff --git a/src/v1_17/java/gg/playit/channel/ChannelInserterV1_17.java b/src/v1_17/java/gg/playit/channel/ChannelInserterV1_17.java new file mode 100644 index 0000000..f924b2f --- /dev/null +++ b/src/v1_17/java/gg/playit/channel/ChannelInserterV1_17.java @@ -0,0 +1,173 @@ +package gg.playit.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketDecoder; +import net.minecraft.network.PacketEncoder; +import net.minecraft.network.RateKickingConnection; +import net.minecraft.network.Varint21FrameDecoder; +import net.minecraft.network.Varint21LengthFieldPrepender; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.LegacyQueryHandler; +import net.minecraft.server.network.ServerConnectionListener; +import net.minecraft.server.network.ServerHandshakePacketListenerImpl; +import org.bukkit.Server; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; + +/** + * ChannelInserter for Minecraft 1.17 through 1.20.x. + *

+ * Uses the modern NMS naming convention: + *

    + *
  • Connection (was NetworkManager)
  • + *
  • ServerConnectionListener (was ServerConnection)
  • + *
  • LegacyQueryHandler (was LegacyPingHandler)
  • + *
  • Varint21FrameDecoder (was PacketSplitter)
  • + *
  • PacketFlow (was EnumProtocolDirection)
  • + *
+ * LegacyQueryHandler takes MinecraftServer as constructor arg. + * PacketDecoder/PacketEncoder take PacketFlow. + * No BandwidthDebugMonitor support. + */ +public final class ChannelInserterV1_17 extends BaseChannelInserter { + + public ChannelInserterV1_17(String version) { + super(version); + } + + @Override + public boolean insertChannel(Server server, Channel tunnelChannel, InetSocketAddress trueIp, + int connectionTimeoutSeconds, int rateLimit) { + Class serverConnectionClass = ServerConnectionListener.class; + Class connectionClass = Connection.class; + Field connectionsField = findField(ServerConnectionListener.class, "connections"); + Field addressField = findField(Connection.class, "address"); + + // 1. Get MinecraftServer + Object minecraftServer = getMinecraftServer(server); + if (minecraftServer == null) { + log.info("failed to get Minecraft server from Bukkit.getServer()"); + return false; + } + + // 2. Get ServerConnectionListener + Object serverConnection = getServerConnection(minecraftServer, serverConnectionClass); + if (serverConnection == null) { + log.info("failed to get ServerConnectionListener from Minecraft Server"); + return false; + } + + // 3. Create LegacyQueryHandler - takes ServerConnectionListener (ServerNetworkIo) + ChannelHandler legacyHandler; + try { + legacyHandler = new LegacyQueryHandler((ServerConnectionListener) serverConnection); + } catch (Exception e) { + log.warning("LegacyQueryHandler construction failed: " + e); + legacyHandler = createPassthroughHandler(); + } + + // 4. Create Varint21FrameDecoder (no-arg constructor) + Object packetSplitter; + try { + packetSplitter = new Varint21FrameDecoder(); + } catch (Exception e) { + log.info("Varint21FrameDecoder construction failed: " + e); + return false; + } + + // 5. Resolve SERVERBOUND PacketFlow + Object serverBound = resolveEnumValue(PacketFlow.class, "SERVERBOUND"); + if (serverBound == null) { + log.info("failed to resolve SERVERBOUND enum"); + return false; + } + + // 6. Create PacketDecoder(PacketFlow.SERVERBOUND) + Object packetDecoder; + try { + packetDecoder = new PacketDecoder((PacketFlow) serverBound); + } catch (Exception e) { + log.info("PacketDecoder construction failed: " + e); + return false; + } + + // 7. Create Varint21LengthFieldPrepender (no-arg) + Object packetPrepender; + try { + packetPrepender = new Varint21LengthFieldPrepender(); + } catch (Exception e) { + log.info("Varint21LengthFieldPrepender construction failed: " + e); + return false; + } + + // 8. Resolve CLIENTBOUND + Object clientBound = resolveEnumValue(PacketFlow.class, "CLIENTBOUND"); + + // 9. Create PacketEncoder(PacketFlow.CLIENTBOUND) + Object packetEncoder; + try { + packetEncoder = new PacketEncoder((PacketFlow) (clientBound != null ? clientBound : serverBound)); + } catch (Exception e) { + log.info("PacketEncoder construction failed: " + e); + return false; + } + + // 10. Create Connection - use RateKickingConnection if rate limit > 0 + Object networkManager; + if (rateLimit > 0) { + try { + networkManager = new RateKickingConnection(rateLimit); + } catch (Exception e) { + log.info("RateKickingConnection construction failed: " + e); + networkManager = createConnection(serverBound); + if (networkManager == null) return false; + } + } else { + networkManager = createConnection(serverBound); + if (networkManager == null) return false; + } + + // 11. Create ServerHandshakePacketListenerImpl(MinecraftServer, Connection) + Object handshakeListener; + try { + handshakeListener = new ServerHandshakePacketListenerImpl((MinecraftServer) minecraftServer, (Connection) networkManager); + } catch (Exception e) { + log.info("ServerHandshakePacketListenerImpl construction failed: " + e); + return false; + } + + // 12. Set the packet listener + if (!setPacketListener(networkManager, handshakeListener, connectionClass)) { + log.info("failed to set handshake listener on network manager"); + return false; + } + + // 13. Setup the pipeline + return setupPipeline(tunnelChannel, trueIp, connectionTimeoutSeconds, + legacyHandler, packetSplitter, packetDecoder, packetPrepender, packetEncoder, + networkManager, serverConnection, serverConnectionClass, connectionsField, addressField); + } + + private Object createConnection(Object serverBound) { + try { + return new Connection((PacketFlow) serverBound); + } catch (Exception e) { + log.info("Connection construction failed: " + e); + } + return null; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Object resolveEnumValue(Class enumClass, String name) { + if (enumClass == null || !enumClass.isEnum()) return null; + try { + return Enum.valueOf((Class) enumClass, name); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/v1_21/java/gg/playit/channel/ChannelInserterV1_21.java b/src/v1_21/java/gg/playit/channel/ChannelInserterV1_21.java new file mode 100644 index 0000000..a5ebe43 --- /dev/null +++ b/src/v1_21/java/gg/playit/channel/ChannelInserterV1_21.java @@ -0,0 +1,270 @@ +package gg.playit.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketDecoder; +import net.minecraft.network.PacketEncoder; +import net.minecraft.network.RateKickingConnection; +import net.minecraft.network.Varint21FrameDecoder; +import net.minecraft.network.Varint21LengthFieldPrepender; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.handshake.HandshakeProtocols; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.LegacyQueryHandler; +import net.minecraft.server.network.ServerConnectionListener; +import net.minecraft.server.network.ServerHandshakePacketListenerImpl; +import org.bukkit.Server; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.InetSocketAddress; + +/** + * ChannelInserter for Minecraft 1.21.0 through 1.21.3. + *

+ * Same class names as V1_17 but with protocol state changes: + *

    + *
  • PacketDecoder/PacketEncoder may take ProtocolInfo instead of PacketFlow
  • + *
  • HandshakeProtocols provides initial protocol state
  • + *
  • Connection may have a static INITIAL_PROTOCOL field
  • + *
+ * LegacyQueryHandler takes MinecraftServer as constructor arg. + * No BandwidthDebugMonitor support for Varint21FrameDecoder. + */ +public final class ChannelInserterV1_21 extends BaseChannelInserter { + + public ChannelInserterV1_21(String version) { + super(version); + } + + @Override + public boolean insertChannel(Server server, Channel tunnelChannel, InetSocketAddress trueIp, + int connectionTimeoutSeconds, int rateLimit) { + Class serverConnectionClass = ServerConnectionListener.class; + Class connectionClass = Connection.class; + Field connectionsField = findField(ServerConnectionListener.class, "connections"); + Field addressField = findField(Connection.class, "address"); + + // 1. Get MinecraftServer + Object minecraftServer = getMinecraftServer(server); + if (minecraftServer == null) { + log.info("failed to get Minecraft server from Bukkit.getServer()"); + return false; + } + + // 2. Get ServerConnectionListener + Object serverConnection = getServerConnection(minecraftServer, serverConnectionClass); + if (serverConnection == null) { + log.info("failed to get ServerConnectionListener from Minecraft Server"); + return false; + } + + // 3. Create LegacyQueryHandler - 1.21+ takes MinecraftServer (implements ServerInfo) + ChannelHandler legacyHandler; + try { + legacyHandler = new LegacyQueryHandler((MinecraftServer) minecraftServer); + } catch (Exception e) { + log.warning("LegacyQueryHandler construction failed: " + e); + legacyHandler = createPassthroughHandler(); + } + + // 4. Create Varint21FrameDecoder - 1.21.0-1.21.3 has no-arg; some builds require BandwidthDebugMonitor + Object packetSplitter = createVarint21FrameDecoder(); + if (packetSplitter == null) { + log.info("Varint21FrameDecoder construction failed"); + return false; + } + + // 5. Resolve PacketFlow and initial protocol + Object serverBound = resolveEnumValue(PacketFlow.class, "SERVERBOUND"); + Object clientBound = resolveEnumValue(PacketFlow.class, "CLIENTBOUND"); + Object initialProtocol = resolveInitialProtocol(); + + // 6. Create PacketDecoder - try ProtocolInfo first, fallback to PacketFlow + Object packetDecoder = createPacketDecoder(initialProtocol, serverBound); + if (packetDecoder == null) { + log.info("PacketDecoder creation failed"); + return false; + } + + // 7. Create Varint21LengthFieldPrepender (no-arg) + Object packetPrepender; + try { + packetPrepender = new Varint21LengthFieldPrepender(); + } catch (Exception e) { + log.info("Varint21LengthFieldPrepender construction failed: " + e); + return false; + } + + // 8. Create PacketEncoder - try ProtocolInfo first, fallback to PacketFlow + Object packetEncoder = createPacketEncoder(initialProtocol, clientBound, serverBound); + if (packetEncoder == null) { + log.info("PacketEncoder creation failed"); + return false; + } + + // 9. Create Connection - use RateKickingConnection if rate limit > 0 + Object networkManager; + if (rateLimit > 0) { + try { + networkManager = new RateKickingConnection(rateLimit); + } catch (Exception e) { + log.info("RateKickingConnection construction failed: " + e); + networkManager = createConnection(serverBound); + if (networkManager == null) return false; + } + } else { + networkManager = createConnection(serverBound); + if (networkManager == null) return false; + } + + // 10. Create ServerHandshakePacketListenerImpl(MinecraftServer, Connection) + Object handshakeListener; + try { + handshakeListener = new ServerHandshakePacketListenerImpl((MinecraftServer) minecraftServer, (Connection) networkManager); + } catch (Exception e) { + log.info("ServerHandshakePacketListenerImpl construction failed: " + e); + return false; + } + + // 11. Set the packet listener + if (!setPacketListener(networkManager, handshakeListener, connectionClass)) { + log.info("failed to set handshake listener on network manager"); + return false; + } + + // 12. Setup the pipeline + return setupPipeline(tunnelChannel, trueIp, connectionTimeoutSeconds, + legacyHandler, packetSplitter, packetDecoder, packetPrepender, packetEncoder, + networkManager, serverConnection, serverConnectionClass, connectionsField, addressField); + } + + private Object resolveInitialProtocol() { + try { + Field f = HandshakeProtocols.class.getDeclaredField("SERVERBOUND"); + f.setAccessible(true); + Object value = f.get(null); + if (value != null) return value; + } catch (Exception ignored) { + } + try { + Field f = HandshakeProtocols.class.getDeclaredField("C2S"); + f.setAccessible(true); + Object value = f.get(null); + if (value != null) return value; + } catch (Exception ignored) { + } + for (String fieldName : new String[]{"C2S_HANDSHAKE_STATE", "INITIAL_PROTOCOL"}) { + try { + Field f = Connection.class.getDeclaredField(fieldName); + f.setAccessible(true); + Object value = f.get(null); + if (value != null) return value; + } catch (Exception ignored) { + } + } + for (Field f : Connection.class.getDeclaredFields()) { + if (Modifier.isStatic(f.getModifiers())) { + String typeName = f.getType().getName(); + if (typeName.contains("ProtocolInfo") || typeName.contains("NetworkState")) { + try { + f.setAccessible(true); + Object value = f.get(null); + if (value != null) return value; + } catch (Exception ignored) { + } + } + } + } + return null; + } + + private Object createVarint21FrameDecoder() { + String[] loggerNames = {"net.minecraft.network.handler.PacketSizeLogger", "net.minecraft.network.BandwidthDebugMonitor"}; + String[] sampleLogNames = {"net.minecraft.util.profiler.MultiValueDebugSampleLogImpl", "net.minecraft.util.debugchart.LocalSampleLogger"}; + for (String loggerName : loggerNames) { + for (String sampleLogName : sampleLogNames) { + try { + Class sampleLogClass = Class.forName(sampleLogName); + Object sampleLog = sampleLogClass.getConstructor(int.class).newInstance(1); + Class loggerClass = Class.forName(loggerName); + Object monitor = loggerClass.getConstructor(sampleLogClass).newInstance(sampleLog); + for (Constructor ctor : Varint21FrameDecoder.class.getConstructors()) { + if (ctor.getParameterCount() == 1 && ctor.getParameterTypes()[0].isInstance(monitor)) { + return ctor.newInstance(monitor); + } + } + } catch (Exception ignored) { + } + } + } + log.warning("Varint21FrameDecoder creation failed"); + return null; + } + + private Object createPacketDecoder(Object protocolInfo, Object serverBound) { + if (protocolInfo != null) { + try { + return PacketDecoder.class.getConstructor(protocolInfo.getClass()).newInstance(protocolInfo); + } catch (Exception ignored) { + } + for (Class iface : protocolInfo.getClass().getInterfaces()) { + try { + return PacketDecoder.class.getConstructor(iface).newInstance(protocolInfo); + } catch (Exception ignored) { + } + } + } + if (serverBound != null) { + try { + return PacketDecoder.class.getConstructor(PacketFlow.class).newInstance(serverBound); + } catch (Exception ignored) { + } + } + return null; + } + + private Object createPacketEncoder(Object protocolInfo, Object clientBound, Object serverBound) { + if (protocolInfo != null) { + try { + return PacketEncoder.class.getConstructor(protocolInfo.getClass()).newInstance(protocolInfo); + } catch (Exception ignored) { + } + for (Class iface : protocolInfo.getClass().getInterfaces()) { + try { + return PacketEncoder.class.getConstructor(iface).newInstance(protocolInfo); + } catch (Exception ignored) { + } + } + } + Object flow = clientBound != null ? clientBound : serverBound; + if (flow != null) { + try { + return PacketEncoder.class.getConstructor(PacketFlow.class).newInstance(flow); + } catch (Exception ignored) { + } + } + return null; + } + + private Object createConnection(Object serverBound) { + try { + return new Connection((PacketFlow) serverBound); + } catch (Exception e) { + log.info("Connection construction failed: " + e); + } + return null; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Object resolveEnumValue(Class enumClass, String name) { + if (enumClass == null || !enumClass.isEnum()) return null; + try { + return Enum.valueOf((Class) enumClass, name); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/v1_21_4/java/gg/playit/channel/ChannelInserterV1_21_4.java b/src/v1_21_4/java/gg/playit/channel/ChannelInserterV1_21_4.java new file mode 100644 index 0000000..cf35e14 --- /dev/null +++ b/src/v1_21_4/java/gg/playit/channel/ChannelInserterV1_21_4.java @@ -0,0 +1,139 @@ +package gg.playit.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import net.minecraft.network.Connection; +import net.minecraft.network.RateKickingConnection; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.LegacyQueryHandler; +import net.minecraft.server.network.ServerConnectionListener; +import net.minecraft.server.network.ServerHandshakePacketListenerImpl; +import org.bukkit.Server; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; + +/** + * ChannelInserter for Minecraft 1.21.4 and later. + *

+ * Delegates pipeline setup to Connection.addHandlers() to ensure exact vanilla + * compatibility (including PacketBundleUnpacker/PacketBundlePacker for 1.21+). + */ +public final class ChannelInserterV1_21_4 extends BaseChannelInserter { + + public ChannelInserterV1_21_4(String version) { + super(version); + } + + @Override + public boolean insertChannel(Server server, Channel tunnelChannel, InetSocketAddress trueIp, + int connectionTimeoutSeconds, int rateLimit) { + Class serverConnectionClass = ServerConnectionListener.class; + Class connectionClass = Connection.class; + Field connectionsField = findField(ServerConnectionListener.class, "connections"); + Field addressField = findField(Connection.class, "address"); + + // 1. Get MinecraftServer + Object minecraftServer = getMinecraftServer(server); + if (minecraftServer == null) { + log.info("failed to get Minecraft server from Bukkit.getServer()"); + return false; + } + + // 2. Get ServerConnectionListener + Object serverConnection = getServerConnection(minecraftServer, serverConnectionClass); + if (serverConnection == null) { + log.info("failed to get ServerConnectionListener from Minecraft Server"); + return false; + } + + // 3. Create LegacyQueryHandler - 1.21.4+ takes MinecraftServer (implements ServerInfo) + ChannelHandler legacyHandler; + try { + legacyHandler = new LegacyQueryHandler((MinecraftServer) minecraftServer); + } catch (Exception e) { + log.warning("LegacyQueryHandler construction failed: " + e); + legacyHandler = createPassthroughHandler(); + } + + // 4. Create BandwidthDebugMonitor for Connection.addHandlers + Object monitor = createBandwidthDebugMonitor(); + if (monitor == null) { + log.info("BandwidthDebugMonitor creation failed"); + return false; + } + + // 5. Create Connection - use RateKickingConnection if rate limit > 0 + Object serverBound = resolveEnumValue(PacketFlow.class, "SERVERBOUND"); + Object networkManager; + if (rateLimit > 0) { + try { + networkManager = new RateKickingConnection(rateLimit); + } catch (Exception e) { + log.info("RateKickingConnection construction failed: " + e); + networkManager = createConnection(serverBound); + if (networkManager == null) return false; + } + } else { + networkManager = createConnection(serverBound); + if (networkManager == null) return false; + } + + // 6. Create ServerHandshakePacketListenerImpl(MinecraftServer, Connection) + Object handshakeListener; + try { + handshakeListener = new ServerHandshakePacketListenerImpl((MinecraftServer) minecraftServer, (Connection) networkManager); + } catch (Exception e) { + log.info("ServerHandshakePacketListenerImpl construction failed: " + e); + return false; + } + + // 7. Set the packet listener + if (!setPacketListener(networkManager, handshakeListener, connectionClass)) { + log.info("failed to set handshake listener on network manager"); + return false; + } + + // 8. Setup the pipeline via Connection.addHandlers (vanilla-compatible) + return setupPipelineWithAddHandlers(tunnelChannel, trueIp, connectionTimeoutSeconds, + legacyHandler, networkManager, monitor, + serverConnection, serverConnectionClass, connectionsField, addressField); + } + + private Object createBandwidthDebugMonitor() { + String[] loggerNames = {"net.minecraft.network.handler.PacketSizeLogger", "net.minecraft.network.BandwidthDebugMonitor"}; + String[] sampleLogNames = {"net.minecraft.util.profiler.MultiValueDebugSampleLogImpl", "net.minecraft.util.debugchart.LocalSampleLogger"}; + for (String loggerName : loggerNames) { + for (String sampleLogName : sampleLogNames) { + try { + Class sampleLogClass = Class.forName(sampleLogName); + Object sampleLog = sampleLogClass.getConstructor(int.class).newInstance(1); + Class loggerClass = Class.forName(loggerName); + return loggerClass.getConstructor(sampleLogClass).newInstance(sampleLog); + } catch (Exception ignored) { + } + } + } + return null; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private Object resolveEnumValue(Class enumClass, String name) { + if (enumClass == null || !enumClass.isEnum()) return null; + try { + return Enum.valueOf((Class) enumClass, name); + } catch (Exception e) { + return null; + } + } + + private Object createConnection(Object serverBound) { + try { + return new Connection((PacketFlow) serverBound); + } catch (Exception e) { + log.info("Connection construction failed: " + e); + } + return null; + } +}