diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44fbd37..1eb1879 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 1.0.9 — FastLoginPlus native login channel association
+
+- Associate the ProtocolLib `TemporaryPlayer` generated during PacketEvents pre-login callbacks with its actual Netty injector instead of leaving `TemporaryPlayerFactory.getInjectorFromPlayer()` null. FastLoginPlus uses that accessor during Mojang session verification.
+- Resolve the real `net.minecraft.network.Connection` from the channel's existing `packet_handler`, and update cached Netty injectors that were created before the native handler was installed. FastLoginPlus needs the genuine Connection (not the bare Netty Channel) to enable packet encryption.
+- Explicitly refuse binding a non-active channel or an injector lacking a native Connection; never fall back to cracked login in P2P code and never circumvent Mojang verification.
+- Handle FastLoginPlus's legacy method lookup `a(SecretKey)` on remapped Minecraft 1.21.11 where the native Connection method is named `setEncryptionKey(SecretKey)`; fallback only for one unambiguous void/SecretKey method.
+- Keep v1.0.8's encryption wrapper constructor fix and v1.0.7's temporary-user receive/send transport.
+
## 1.0.8 — Constructor-safe PacketEvents wrapper allocation
- Fix `NullPointerException: wrappper.bufferLock is null` when FastLoginPlus sends `LOGIN.SERVER.ENCRYPTION_REQUEST` with a `PacketContainer` constructed from scratch.
diff --git a/pom.xml b/pom.xml
index 4de86ca..024f859 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
com.comphenix.protocol
ProtocolLib2PacketEvents
- 1.0.8
+ 1.0.9
jar
ProtocolLib2PacketEvents
diff --git a/src/main/java/com/comphenix/protocol/ProtocolLib.java b/src/main/java/com/comphenix/protocol/ProtocolLib.java
index 1e20b0a..c3a1e37 100644
--- a/src/main/java/com/comphenix/protocol/ProtocolLib.java
+++ b/src/main/java/com/comphenix/protocol/ProtocolLib.java
@@ -182,6 +182,14 @@ public void onPlayerQuit(PlayerQuitEvent event) {
}
}
+ /** Resolve the pre-login facade to its native connection for auth plugins. */
+ public com.comphenix.protocol.injector.netty.Injector getTemporaryInjector(
+ com.github.retrooper.packetevents.protocol.player.User user,
+ org.bukkit.entity.Player temporary) {
+ return networkManagerInjector == null ? null
+ : networkManagerInjector.getTemporaryInjector(user, temporary);
+ }
+
@Override
public void onDisable() {
if (internalListener != null) {
diff --git a/src/main/java/com/comphenix/protocol/injector/netty/channel/InjectionFactory.java b/src/main/java/com/comphenix/protocol/injector/netty/channel/InjectionFactory.java
index 68eca5a..449767c 100644
--- a/src/main/java/com/comphenix/protocol/injector/netty/channel/InjectionFactory.java
+++ b/src/main/java/com/comphenix/protocol/injector/netty/channel/InjectionFactory.java
@@ -55,7 +55,10 @@ public Injector fromPlayer(Player player) {
public Injector fromChannel(Channel channel) {
if (channel == null) return EmptyInjector.WITHOUT_PLAYER;
Injector existing = byChannel.get(channel);
- if (existing != null) return existing;
+ if (existing != null) {
+ if (existing instanceof NettyChannelInjector netty) netty.refreshNativeConnection();
+ return existing;
+ }
Injector created = new NettyChannelInjector(null, null, channel, listeners, this, reporter);
Injector previous = byChannel.putIfAbsent(channel, created);
return previous == null ? created : previous;
diff --git a/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java b/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java
index 7da14cd..7fba2cd 100644
--- a/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java
+++ b/src/main/java/com/comphenix/protocol/injector/netty/channel/NettyChannelInjector.java
@@ -32,7 +32,11 @@ public class NettyChannelInjector implements Injector {
private static final String ENCODER = "p2p-wire-encoder";
private static final String OUTBOUND = "p2p-outbound";
private volatile Player player;
- private final Object networkManager;
+ // The Netty channel is discovered before the native Minecraft Connection
+ // handler may be in the pipeline. Refresh it as soon as the first LOGIN
+ // packet arrives so FastLoginPlus can enable AES encryption on the real
+ // NMS Connection. It must not be the raw Netty Channel or a dummy handle.
+ private volatile Object networkManager;
private final Channel channel;
private final ListenerManager listenerManager;
private final InjectionFactory factory;
@@ -49,11 +53,34 @@ public NettyChannelInjector(Player player, Object networkManager, Channel channe
this.player = player;
this.networkManager = networkManager;
this.channel = channel;
+ refreshNativeConnection();
this.listenerManager = listenerManager;
this.factory = factory;
this.reporter = reporter;
this.protocol = player == null ? PacketType.Protocol.HANDSHAKING : PacketType.Protocol.PLAY;
}
+ /**
+ * Find the actual net.minecraft.network.Connection handler already installed
+ * by the Minecraft server. No field/type guessing, no reflection into the
+ * packet pipeline and no mutation of its handler order.
+ */
+ public void refreshNativeConnection() {
+ if (networkManager != null || channel == null) return;
+ io.netty.channel.ChannelHandler packetHandler = channel.pipeline().get("packet_handler");
+ if (packetHandler == null) return;
+ for (Class> clazz = packetHandler.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
+ if ("net.minecraft.network.Connection".equals(clazz.getName())) {
+ networkManager = packetHandler;
+ return;
+ }
+ }
+ }
+
+ public boolean hasNativeConnection() {
+ refreshNativeConnection();
+ return networkManager != null;
+ }
+
public SocketAddress getAddress() { return channel == null ? null : channel.remoteAddress(); }
public int getProtocolVersion() {
if (player == null || PacketEvents.getAPI() == null) return -1;
diff --git a/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java b/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java
index 0100b05..d096e97 100644
--- a/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java
+++ b/src/main/java/com/comphenix/protocol/injector/netty/manager/NetworkManagerInjector.java
@@ -41,6 +41,29 @@ public NetworkManagerInjector(Plugin plugin, ListenerManager listenerManager, Er
public Injector getInjector(Player player) { return factory.fromPlayer(player); }
+ /**
+ * ProtocolLib's temporary player is not a CraftPlayer, so never pass it to
+ * PacketEvents PlayerManager.getChannel(Player): resolve the channel from
+ * the pre-login User instead. FastLoginPlus relies on this exact facade's
+ * getInjectorFromPlayer() to locate the real Netty channel and NMS
+ * Connection for session verification and encryption.
+ */
+ public Injector getTemporaryInjector(com.github.retrooper.packetevents.protocol.player.User user,
+ Player temporary) {
+ if (closed || user == null || temporary == null) return null;
+ if (!(user.getChannel() instanceof Channel channel) || !channel.isActive()) return null;
+ Injector injector = factory.fromChannel(channel);
+ if (!(injector instanceof com.comphenix.protocol.injector.netty.channel.NettyChannelInjector netty)) {
+ return null;
+ }
+ netty.refreshNativeConnection();
+ // A stale/partial channel cannot authenticate a login. Fail closed:
+ // don't give FastLoginPlus an injector whose networkManager is null.
+ if (!netty.hasNativeConnection()) return null;
+ netty.setPlayer(temporary);
+ return netty;
+ }
+
public void invalidate(Player player) {
if (player == null) return;
Injector injector = factory.invalidate(player);
diff --git a/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerAdapter.java b/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerAdapter.java
index e395a1c..85c6ac1 100644
--- a/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerAdapter.java
+++ b/src/main/java/com/comphenix/protocol/injector/temporary/TemporaryPlayerAdapter.java
@@ -27,6 +27,15 @@ public static Player create(User user) {
}
Player player = TemporaryPlayerFactory.createTemporaryPlayer();
USERS.put(player, user);
+ // The pre-login facade must carry the same native Connection as its
+ // PacketEvents User. Without it, FastLoginPlus's ProtocolLibCompat
+ // considers every player disconnected during Mojang session checks.
+ org.bukkit.plugin.Plugin plugin = com.comphenix.protocol.ProtocolLibrary.getPlugin();
+ if (plugin instanceof com.comphenix.protocol.ProtocolLib bridge) {
+ com.comphenix.protocol.injector.netty.Injector injector =
+ bridge.getTemporaryInjector(user, player);
+ if (injector != null) TemporaryPlayerFactory.setInjectorForPlayer(player, injector);
+ }
return player;
}
diff --git a/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java b/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java
index d463262..4af4fb7 100644
--- a/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java
+++ b/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java
@@ -102,6 +102,24 @@ public Method getMethodByParameters(String name, Class>... parameters) {
return accessible(method);
}
}
+ // FastLoginPlus still asks for the obfuscated Minecraft Connection
+ // method "a(SecretKey)". On remapped Paper/Leaf 1.21.11 the equivalent
+ // is named setEncryptionKey(SecretKey). Resolve by exact signature only
+ // when it is unique: never guess an arbitrary encryption/packet method.
+ if ("a".equals(name) && parameters.length == 1
+ && parameters[0] == javax.crypto.SecretKey.class) {
+ Method found = null;
+ for (Method method : getMethods()) {
+ if (!java.util.Arrays.equals(method.getParameterTypes(), parameters)
+ || method.getReturnType() != void.class) continue;
+ if (found != null && !found.equals(method)) {
+ throw new IllegalArgumentException("Ambiguous SecretKey encryption methods on "
+ + source.getName());
+ }
+ found = method;
+ }
+ if (found != null) return accessible(found);
+ }
throw new IllegalArgumentException("Unable to find method " + name + " on " + source.getName());
}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index a6cbd99..b9def08 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -10,7 +10,7 @@ main: com.comphenix.protocol.ProtocolLib
version: 5.4.0
author: CyoriaSMP Team
description: A drop-in ProtocolLib compatibility layer powered by PacketEvents.
-p2p-version: 1.0.8
+p2p-version: 1.0.9
protocol-lib-api-version: 5.4.0
depend: [packetevents]
provides: [ProtocolLib]
diff --git a/src/test/java/com/comphenix/protocol/injector/netty/channel/PreLoginNativeConnectionTest.java b/src/test/java/com/comphenix/protocol/injector/netty/channel/PreLoginNativeConnectionTest.java
new file mode 100644
index 0000000..ceed6e3
--- /dev/null
+++ b/src/test/java/com/comphenix/protocol/injector/netty/channel/PreLoginNativeConnectionTest.java
@@ -0,0 +1,32 @@
+package com.comphenix.protocol.injector.netty.channel;
+
+import io.netty.channel.embedded.EmbeddedChannel;
+import java.lang.reflect.Field;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class PreLoginNativeConnectionTest {
+ @Test
+ void findsLateNativePacketHandlerForFastLoginCipher() throws Exception {
+ EmbeddedChannel channel = new EmbeddedChannel();
+ NettyChannelInjector injector = new NettyChannelInjector(null, null, channel, null, null, null);
+ assertFalse(injector.hasNativeConnection(), "cannot fabricate a native NMS Connection");
+ net.minecraft.network.Connection nativeConn = new net.minecraft.network.Connection();
+ channel.pipeline().addLast("packet_handler", nativeConn);
+ injector.refreshNativeConnection();
+ assertTrue(injector.hasNativeConnection());
+ Field field = NettyChannelInjector.class.getDeclaredField("networkManager");
+ field.setAccessible(true);
+ assertSame(nativeConn, field.get(injector), "FastLoginPlus must receive actual NMS packet handler");
+ channel.finishAndReleaseAll();
+ }
+
+ @Test
+ void neverConfusesOtherChannelHandlerWithNmsConnection() {
+ EmbeddedChannel channel = new EmbeddedChannel();
+ NettyChannelInjector injector = new NettyChannelInjector(null, null, channel, null, null, null);
+ channel.pipeline().addLast("packet_handler", new io.netty.channel.ChannelInboundHandlerAdapter());
+ assertFalse(injector.hasNativeConnection());
+ channel.finishAndReleaseAll();
+ }
+}
diff --git a/src/test/java/com/comphenix/protocol/reflect/FuzzyReflectionSecretKeyTest.java b/src/test/java/com/comphenix/protocol/reflect/FuzzyReflectionSecretKeyTest.java
new file mode 100644
index 0000000..62a04d3
--- /dev/null
+++ b/src/test/java/com/comphenix/protocol/reflect/FuzzyReflectionSecretKeyTest.java
@@ -0,0 +1,40 @@
+package com.comphenix.protocol.reflect;
+
+import javax.crypto.SecretKey;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class FuzzyReflectionSecretKeyTest {
+ public static class ModernConnection {
+ public void setEncryptionKey(SecretKey key) { }
+ }
+ public static class AmbiguousConnection {
+ public void setEncryptionKey(SecretKey key) { }
+ public void anotherMethod(SecretKey key) { }
+ }
+ public static class OtherMethod {
+ public void send(Object obj) { }
+ }
+
+ @Test
+ void remappedModernMinecraftKeySetterIsDiscoverableByOldName() {
+ var result = FuzzyReflection.fromClass(ModernConnection.class)
+ .getMethodByParameters("a", SecretKey.class);
+ assertEquals("setEncryptionKey", result.getName());
+ }
+
+ @Test
+ void ambiguousCryptoMethodsDoNotGetPickedArbitrarily() {
+ assertThrows(IllegalArgumentException.class,
+ () -> FuzzyReflection.fromClass(AmbiguousConnection.class)
+ .getMethodByParameters("a", SecretKey.class));
+ }
+
+ @Test
+ void normalMethodNameLookupStaysStrict() {
+ assertThrows(IllegalArgumentException.class,
+ () -> FuzzyReflection.fromClass(OtherMethod.class)
+ .getMethodByParameters("a", Object.class));
+ }
+}
diff --git a/src/test/java/net/minecraft/network/Connection.java b/src/test/java/net/minecraft/network/Connection.java
new file mode 100644
index 0000000..486d0c8
--- /dev/null
+++ b/src/test/java/net/minecraft/network/Connection.java
@@ -0,0 +1,4 @@
+package net.minecraft.network;
+
+/** Test-only stand-in for Paper's registered packet_handler. */
+public class Connection extends io.netty.channel.ChannelInboundHandlerAdapter { }