diff --git a/README.md b/README.md
index 1fce7b5..646ea5e 100644
--- a/README.md
+++ b/README.md
@@ -9,13 +9,13 @@ For a full user guide go to the [Usage for Players](#usage-for-players-gui) sect
 - Classic (c0.0.15 - c0.30 including [CPE](https://wiki.vg/Classic_Protocol_Extension))
 - Alpha (a1.0.15 - a1.2.6)
 - Beta (b1.0 - b1.8.1)
-- Release (1.0.0 - 1.20)
+- Release (1.0.0 - 1.20.2)
 - April Fools (3D Shareware, 20w14infinite)
 - Combat Snapshots (Combat Test 8c)
 - Bedrock Edition 1.20.0 (In development)
 
 ## Supported Client versions
-- Release (1.7.2 - 1.20)
+- Release (1.7.2 - 1.20.2)
 - Bedrock Edition (Needs the [Geyser plugin](https://github.com/RaphiMC/ViaProxyGeyserPlugin))
 - Classic, Alpha, Beta, Release 1.0 - 1.6.4 (Only passthrough)
 
diff --git a/build.gradle b/build.gradle
index 7b6c6a9..82d3237 100644
--- a/build.gradle
+++ b/build.gradle
@@ -76,21 +76,21 @@ repositories {
 dependencies {
     compileOnly sourceSets.java17compat.output
 
-    include "com.viaversion:viaversion:4.7.1-SNAPSHOT"
-    include("com.viaversion:viabackwards-common:4.7.1-SNAPSHOT") {
+    include "com.viaversion:viaversion:4.8.0-23w32a-SNAPSHOT"
+    include("com.viaversion:viabackwards-common:4.8.0-23w32a-SNAPSHOT") {
         exclude group: "com.viaversion", module: "viaversion"
         exclude group: "io.netty", module: "netty-all"
         exclude group: "com.google.guava", module: "guava"
     }
-    include "com.viaversion:viarewind-core:2.0.4-SNAPSHOT"
-    include "net.raphimc:ViaLegacy:2.2.18-SNAPSHOT"
-    include "net.raphimc:ViaAprilFools:2.0.7"
+    include "com.viaversion:viarewind-core:3.0.0-SNAPSHOT"
+    include "net.raphimc:ViaLegacy:2.2.19-SNAPSHOT"
+    include "net.raphimc:ViaAprilFools:2.0.9-SNAPSHOT"
     include("net.raphimc:ViaBedrock:0.0.2-SNAPSHOT") {
         exclude group: "io.netty", module: "netty-codec-http"
         exclude group: "io.jsonwebtoken", module: "jjwt-impl"
         exclude group: "io.jsonwebtoken", module: "jjwt-gson"
     }
-    include("net.raphimc:ViaLoader:2.2.7") {
+    include("net.raphimc:ViaLoader:2.2.9-SNAPSHOT") {
         exclude group: "org.slf4j", module: "slf4j-api"
     }
 
@@ -108,7 +108,7 @@ dependencies {
     include "net.lenni0451.classtransform:additionalclassprovider:1.10.1"
     include "net.lenni0451:Reflect:1.2.2"
     include "net.lenni0451:LambdaEvents:2.2.0"
-    include "net.raphimc.netminecraft:all:2.3.4"
+    include "net.raphimc.netminecraft:all:2.3.6-SNAPSHOT"
     include("net.raphimc:MinecraftAuth:2.1.5-SNAPSHOT") {
         exclude group: "com.google.code.gson", module: "gson"
         exclude group: "org.slf4j", module: "slf4j-api"
diff --git a/src/main/java/net/raphimc/viaproxy/injection/mixins/MixinProtocolVersion.java b/src/main/java/net/raphimc/viaproxy/injection/mixins/MixinProtocolVersion.java
index 4243319..df031d4 100644
--- a/src/main/java/net/raphimc/viaproxy/injection/mixins/MixinProtocolVersion.java
+++ b/src/main/java/net/raphimc/viaproxy/injection/mixins/MixinProtocolVersion.java
@@ -51,6 +51,7 @@ public abstract class MixinProtocolVersion {
         remaps.put("1.18/1.18.1", new Pair<>("1.18-1.18.1", null));
         remaps.put("1.19.1/2", new Pair<>("1.19.1-1.19.2", null));
         remaps.put("1.20/1.20.1", new Pair<>("1.20-1.20.1", null));
+        remaps.put("1.20.2", new Pair<>("23w32a", null));
     }
 
     @Redirect(method = "<clinit>", at = @At(value = "INVOKE", target = "Lcom/viaversion/viaversion/api/protocol/version/ProtocolVersion;register(ILjava/lang/String;)Lcom/viaversion/viaversion/api/protocol/version/ProtocolVersion;"))
diff --git a/src/main/java/net/raphimc/viaproxy/plugins/events/ConnectEvent.java b/src/main/java/net/raphimc/viaproxy/plugins/events/ConnectEvent.java
new file mode 100644
index 0000000..f6f392d
--- /dev/null
+++ b/src/main/java/net/raphimc/viaproxy/plugins/events/ConnectEvent.java
@@ -0,0 +1,35 @@
+/*
+ * This file is part of ViaProxy - https://github.com/RaphiMC/ViaProxy
+ * Copyright (C) 2023 RK_01/RaphiMC and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package net.raphimc.viaproxy.plugins.events;
+
+import net.raphimc.viaproxy.plugins.events.types.EventCancellable;
+import net.raphimc.viaproxy.proxy.session.ProxyConnection;
+
+public class ConnectEvent extends EventCancellable {
+
+    private final ProxyConnection proxyConnection;
+
+    public ConnectEvent(final ProxyConnection proxyConnection) {
+        this.proxyConnection = proxyConnection;
+    }
+
+    public ProxyConnection getProxyConnection() {
+        return this.proxyConnection;
+    }
+
+}
diff --git a/src/main/java/net/raphimc/viaproxy/proxy/client2proxy/Client2ProxyHandler.java b/src/main/java/net/raphimc/viaproxy/proxy/client2proxy/Client2ProxyHandler.java
index 26c87ca..ae7dfd1 100644
--- a/src/main/java/net/raphimc/viaproxy/proxy/client2proxy/Client2ProxyHandler.java
+++ b/src/main/java/net/raphimc/viaproxy/proxy/client2proxy/Client2ProxyHandler.java
@@ -17,9 +17,8 @@
  */
 package net.raphimc.viaproxy.proxy.client2proxy;
 
-import com.mojang.authlib.GameProfile;
+import com.google.common.collect.Lists;
 import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;
-import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandler;
@@ -27,28 +26,19 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.handler.codec.haproxy.*;
 import net.raphimc.netminecraft.constants.ConnectionState;
-import net.raphimc.netminecraft.constants.MCPackets;
-import net.raphimc.netminecraft.constants.MCPipeline;
-import net.raphimc.netminecraft.netty.crypto.AESEncryption;
-import net.raphimc.netminecraft.netty.crypto.CryptUtil;
 import net.raphimc.netminecraft.packet.IPacket;
-import net.raphimc.netminecraft.packet.PacketTypes;
-import net.raphimc.netminecraft.packet.UnknownPacket;
 import net.raphimc.netminecraft.packet.impl.handshake.C2SHandshakePacket;
-import net.raphimc.netminecraft.packet.impl.login.*;
 import net.raphimc.netminecraft.util.ServerAddress;
 import net.raphimc.vialoader.util.VersionEnum;
 import net.raphimc.viaproxy.ViaProxy;
 import net.raphimc.viaproxy.cli.options.Options;
 import net.raphimc.viaproxy.plugins.PluginManager;
+import net.raphimc.viaproxy.plugins.events.ConnectEvent;
 import net.raphimc.viaproxy.plugins.events.PreConnectEvent;
 import net.raphimc.viaproxy.plugins.events.Proxy2ServerHandlerCreationEvent;
 import net.raphimc.viaproxy.plugins.events.ResolveSrvEvent;
 import net.raphimc.viaproxy.protocolhack.viaproxy.ViaBedrockTransferHolder;
-import net.raphimc.viaproxy.proxy.LoginState;
-import net.raphimc.viaproxy.proxy.external_interface.AuthLibServices;
-import net.raphimc.viaproxy.proxy.external_interface.ExternalInterface;
-import net.raphimc.viaproxy.proxy.external_interface.OpenAuthModConstants;
+import net.raphimc.viaproxy.proxy.packethandler.*;
 import net.raphimc.viaproxy.proxy.proxy2server.Proxy2ServerChannelInitializer;
 import net.raphimc.viaproxy.proxy.proxy2server.Proxy2ServerHandler;
 import net.raphimc.viaproxy.proxy.session.BedrockProxyConnection;
@@ -59,41 +49,24 @@ import net.raphimc.viaproxy.proxy.util.ExceptionUtil;
 import net.raphimc.viaproxy.util.ArrayHelper;
 import net.raphimc.viaproxy.util.logging.Logger;
 
-import javax.crypto.SecretKey;
-import java.math.BigInteger;
 import java.net.ConnectException;
 import java.net.Inet4Address;
 import java.net.InetSocketAddress;
 import java.nio.channels.UnresolvedAddressException;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.time.Instant;
-import java.util.Arrays;
 import java.util.Collections;
-import java.util.Random;
+import java.util.List;
 import java.util.function.Supplier;
 import java.util.regex.Pattern;
 
 public class Client2ProxyHandler extends SimpleChannelInboundHandler<IPacket> {
 
-    private static final KeyPair KEY_PAIR = CryptUtil.generateKeyPair();
-    private static final Random RANDOM = new Random();
-
     private ProxyConnection proxyConnection;
-    private LoginState loginState = LoginState.FIRST_PACKET;
-
-    private final byte[] verifyToken = new byte[4];
-    private int customPayloadPacketId = -1;
-    private int chatSessionUpdatePacketId = -1;
 
     @Override
     public void channelActive(ChannelHandlerContext ctx) throws Exception {
         super.channelActive(ctx);
 
-        RANDOM.nextBytes(this.verifyToken);
         this.proxyConnection = new DummyProxyConnection(ctx.channel());
-
         ViaProxy.c2pChannels.add(ctx.channel());
     }
 
@@ -112,30 +85,19 @@ public class Client2ProxyHandler extends SimpleChannelInboundHandler<IPacket> {
     protected void channelRead0(ChannelHandlerContext ctx, IPacket packet) throws Exception {
         if (this.proxyConnection.isClosed()) return;
 
-        switch (this.proxyConnection.getConnectionState()) {
-            case HANDSHAKING:
-                if (packet instanceof C2SHandshakePacket) this.handleHandshake((C2SHandshakePacket) packet);
-                else throw new IllegalStateException("Unexpected packet in HANDSHAKING state");
-
-                return;
-            case LOGIN:
-                if (packet instanceof C2SLoginHelloPacket1_7) this.handleLoginHello((C2SLoginHelloPacket1_7) packet);
-                else if (packet instanceof C2SLoginKeyPacket1_7) this.handleLoginKey((C2SLoginKeyPacket1_7) packet);
-                else if (packet instanceof C2SLoginCustomPayloadPacket) this.handleLoginCustomPayload((C2SLoginCustomPayloadPacket) packet);
-                else throw new IllegalStateException("Unexpected packet in LOGIN state");
-
-                return;
-            case PLAY:
-                final UnknownPacket unknownPacket = (UnknownPacket) packet;
-                if (unknownPacket.packetId == this.customPayloadPacketId) {
-                    if (this.handlePlayCustomPayload(Unpooled.wrappedBuffer(unknownPacket.data))) return;
-                } else if (unknownPacket.packetId == this.chatSessionUpdatePacketId && this.proxyConnection.getChannel().attr(MCPipeline.ENCRYPTION_ATTRIBUTE_KEY).get() == null) {
-                    return;
-                }
-                break;
+        if (this.proxyConnection.getConnectionState() == ConnectionState.HANDSHAKING) {
+            if (packet instanceof C2SHandshakePacket) this.handleHandshake((C2SHandshakePacket) packet);
+            else throw new IllegalStateException("Unexpected packet in HANDSHAKING state");
+            return;
         }
 
-        this.proxyConnection.getChannel().writeAndFlush(packet).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+        final List<ChannelFutureListener> listeners = Lists.newArrayList(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+        for (PacketHandler packetHandler : this.proxyConnection.getPacketHandlers()) {
+            if (!packetHandler.handleC2P(packet, listeners)) {
+                return;
+            }
+        }
+        this.proxyConnection.getChannel().writeAndFlush(packet).addListeners(listeners.toArray(new ChannelFutureListener[0]));
     }
 
     @Override
@@ -157,9 +119,6 @@ public class Client2ProxyHandler extends SimpleChannelInboundHandler<IPacket> {
             this.proxyConnection.kickClient("§cYour client version is not supported by ViaProxy!");
         }
 
-        this.customPayloadPacketId = MCPackets.C2S_PLUGIN_MESSAGE.getId(clientVersion.getVersion());
-        this.chatSessionUpdatePacketId = MCPackets.C2S_CHAT_SESSION_UPDATE.getId(clientVersion.getVersion());
-
         String[] handshakeParts = new String[]{packet.address};
         if (Options.PLAYER_INFO_FORWARDING) {
             handshakeParts = new String[3];
@@ -256,9 +215,16 @@ public class Client2ProxyHandler extends SimpleChannelInboundHandler<IPacket> {
         this.proxyConnection.setClientVersion(clientVersion);
         this.proxyConnection.setConnectionState(packet.intendedState);
         this.proxyConnection.setClassicMpPass(classicMpPass);
+        this.proxyConnection.getPacketHandlers().add(new StatusPacketHandler(this.proxyConnection));
+        this.proxyConnection.getPacketHandlers().add(new CustomPayloadPacketHandler(this.proxyConnection));
+        this.proxyConnection.getPacketHandlers().add(new LoginPacketHandler(this.proxyConnection));
+        this.proxyConnection.getPacketHandlers().add(new ConfigurationPacketHandler(this.proxyConnection));
+        this.proxyConnection.getPacketHandlers().add(new ResourcePackPacketHandler(this.proxyConnection));
+        this.proxyConnection.getPacketHandlers().add(new UnexpectedPacketHandler(this.proxyConnection));
 
         Logger.u_info("connect", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "[" + clientVersion.getName() + " <-> " + serverVersion.getName() + "] Connecting to " + serverAddress.getAddress() + ":" + serverAddress.getPort());
         try {
+            PluginManager.EVENT_MANAGER.call(new ConnectEvent(this.proxyConnection));
             this.proxyConnection.connectToServer(serverAddress, serverVersion);
         } catch (Throwable e) {
             if (e instanceof ConnectException || e instanceof UnresolvedAddressException) { // Trust me, this is not always false
@@ -284,91 +250,4 @@ public class Client2ProxyHandler extends SimpleChannelInboundHandler<IPacket> {
         this.proxyConnection.setConnectionState(packet.intendedState);
     }
 
-    private void handleLoginHello(C2SLoginHelloPacket1_7 packet) {
-        if (this.loginState != LoginState.FIRST_PACKET) throw CloseAndReturn.INSTANCE;
-        this.loginState = LoginState.SENT_HELLO;
-
-        if (packet instanceof C2SLoginHelloPacket1_19) {
-            final C2SLoginHelloPacket1_19 packet1_19 = (C2SLoginHelloPacket1_19) packet;
-            if (packet1_19.expiresAt != null && packet1_19.expiresAt.isBefore(Instant.now())) {
-                throw new IllegalStateException("Expired public key");
-            }
-        }
-
-        proxyConnection.setLoginHelloPacket(packet);
-        if (packet instanceof C2SLoginHelloPacket1_19_3) {
-            proxyConnection.setGameProfile(new GameProfile(((C2SLoginHelloPacket1_19_3) packet).uuid, packet.name));
-        } else {
-            proxyConnection.setGameProfile(new GameProfile(null, packet.name));
-        }
-
-        if (Options.ONLINE_MODE) {
-            this.proxyConnection.getC2P().writeAndFlush(new S2CLoginKeyPacket1_8("", KEY_PAIR.getPublic().getEncoded(), this.verifyToken)).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
-        } else {
-            ExternalInterface.fillPlayerData(this.proxyConnection);
-            this.proxyConnection.getChannel().writeAndFlush(this.proxyConnection.getLoginHelloPacket()).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
-        }
-    }
-
-    private void handleLoginKey(final C2SLoginKeyPacket1_7 packet) throws GeneralSecurityException {
-        if (this.proxyConnection.getClientVersion().isOlderThanOrEqualTo(VersionEnum.r1_12_2) && new String(packet.encryptedNonce, StandardCharsets.UTF_8).equals(OpenAuthModConstants.DATA_CHANNEL)) { // 1.8-1.12.2 OpenAuthMod response handling
-            final ByteBuf byteBuf = Unpooled.wrappedBuffer(packet.encryptedSecretKey);
-            this.proxyConnection.handleCustomPayload(PacketTypes.readVarInt(byteBuf), byteBuf);
-            return;
-        }
-
-        if (this.loginState != LoginState.SENT_HELLO) throw CloseAndReturn.INSTANCE;
-        this.loginState = LoginState.SENT_KEY;
-
-        if (packet.encryptedNonce != null) {
-            if (!Arrays.equals(this.verifyToken, CryptUtil.decryptData(KEY_PAIR.getPrivate(), packet.encryptedNonce))) {
-                Logger.u_err("auth", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Invalid verify token");
-                this.proxyConnection.kickClient("§cInvalid verify token!");
-            }
-        } else {
-            final C2SLoginKeyPacket1_19 keyPacket = (C2SLoginKeyPacket1_19) packet;
-            final C2SLoginHelloPacket1_19 helloPacket = (C2SLoginHelloPacket1_19) this.proxyConnection.getLoginHelloPacket();
-            if (helloPacket.key == null || !CryptUtil.verifySignedNonce(helloPacket.key, this.verifyToken, keyPacket.salt, keyPacket.signature)) {
-                Logger.u_err("auth", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Invalid verify token");
-                this.proxyConnection.kickClient("§cInvalid verify token!");
-            }
-        }
-
-        final SecretKey secretKey = CryptUtil.decryptSecretKey(KEY_PAIR.getPrivate(), packet.encryptedSecretKey);
-        this.proxyConnection.getC2P().attr(MCPipeline.ENCRYPTION_ATTRIBUTE_KEY).set(new AESEncryption(secretKey));
-
-        final String userName = this.proxyConnection.getGameProfile().getName();
-
-        try {
-            final String serverHash = new BigInteger(CryptUtil.computeServerIdHash("", KEY_PAIR.getPublic(), secretKey)).toString(16);
-            final GameProfile mojangProfile = AuthLibServices.SESSION_SERVICE.hasJoinedServer(this.proxyConnection.getGameProfile(), serverHash, null);
-            if (mojangProfile == null) {
-                Logger.u_err("auth", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Invalid session");
-                this.proxyConnection.kickClient("§cInvalid session! Please restart minecraft (and the launcher) and try again.");
-            } else {
-                this.proxyConnection.setGameProfile(mojangProfile);
-            }
-            Logger.u_info("auth", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Authenticated as " + this.proxyConnection.getGameProfile().getId().toString());
-        } catch (Throwable e) {
-            throw new RuntimeException("Failed to make session request for user '" + userName + "'!", e);
-        }
-
-        ExternalInterface.fillPlayerData(this.proxyConnection);
-        this.proxyConnection.getChannel().writeAndFlush(this.proxyConnection.getLoginHelloPacket()).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
-    }
-
-    private void handleLoginCustomPayload(final C2SLoginCustomPayloadPacket packet) {
-        if (packet.response == null || !this.proxyConnection.handleCustomPayload(packet.queryId, Unpooled.wrappedBuffer(packet.response))) {
-            this.proxyConnection.getChannel().writeAndFlush(packet).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
-        }
-    }
-
-    private boolean handlePlayCustomPayload(final ByteBuf packet) {
-        final String channel = PacketTypes.readString(packet, Short.MAX_VALUE); // channel
-        if (channel.equals(OpenAuthModConstants.DATA_CHANNEL)) {
-            return this.proxyConnection.handleCustomPayload(PacketTypes.readVarInt(packet), packet);
-        }
-        return false;
-    }
-
 }
diff --git a/src/main/java/net/raphimc/viaproxy/proxy/external_interface/ExternalInterface.java b/src/main/java/net/raphimc/viaproxy/proxy/external_interface/ExternalInterface.java
index 59eead2..8e9aee6 100644
--- a/src/main/java/net/raphimc/viaproxy/proxy/external_interface/ExternalInterface.java
+++ b/src/main/java/net/raphimc/viaproxy/proxy/external_interface/ExternalInterface.java
@@ -26,6 +26,7 @@ import net.raphimc.mcauth.step.bedrock.StepMCChain;
 import net.raphimc.mcauth.step.java.StepPlayerCertificates;
 import net.raphimc.netminecraft.packet.PacketTypes;
 import net.raphimc.netminecraft.packet.impl.login.C2SLoginHelloPacket1_19_3;
+import net.raphimc.netminecraft.packet.impl.login.C2SLoginHelloPacket1_20_2;
 import net.raphimc.netminecraft.packet.impl.login.C2SLoginKeyPacket1_19;
 import net.raphimc.viabedrock.protocol.storage.AuthChainData;
 import net.raphimc.vialoader.util.VersionEnum;
@@ -78,7 +79,7 @@ public class ExternalInterface {
                     if (proxyConnection.getClientVersion().equals(VersionEnum.r1_19)) {
                         loginHelloKeySignature = playerCertificates.legacyPublicKeySignature();
                     }
-                    proxyConnection.setLoginHelloPacket(new C2SLoginHelloPacket1_19_3(proxyConnection.getGameProfile().getName(), expiresAt, publicKey, loginHelloKeySignature, proxyConnection.getGameProfile().getId()));
+                    proxyConnection.setLoginHelloPacket(new C2SLoginHelloPacket1_20_2(proxyConnection.getGameProfile().getName(), expiresAt, publicKey, loginHelloKeySignature, proxyConnection.getGameProfile().getId()));
 
                     user.put(new ChatSession1_19_0(user, uuid, privateKey, new ProfileKey(expiresAtMillis, publicKeyBytes, playerCertificates.legacyPublicKeySignature())));
                     user.put(new ChatSession1_19_1(user, uuid, privateKey, new ProfileKey(expiresAtMillis, publicKeyBytes, keySignature)));
diff --git a/src/main/java/net/raphimc/viaproxy/proxy/packethandler/ConfigurationPacketHandler.java b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/ConfigurationPacketHandler.java
new file mode 100644
index 0000000..a685639
--- /dev/null
+++ b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/ConfigurationPacketHandler.java
@@ -0,0 +1,94 @@
+/*
+ * This file is part of ViaProxy - https://github.com/RaphiMC/ViaProxy
+ * Copyright (C) 2023 RK_01/RaphiMC and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package net.raphimc.viaproxy.proxy.packethandler;
+
+import io.netty.channel.ChannelFutureListener;
+import net.raphimc.netminecraft.constants.ConnectionState;
+import net.raphimc.netminecraft.constants.MCPackets;
+import net.raphimc.netminecraft.packet.IPacket;
+import net.raphimc.netminecraft.packet.UnknownPacket;
+import net.raphimc.netminecraft.packet.impl.configuration.C2SConfigFinishConfiguration1_20_2;
+import net.raphimc.netminecraft.packet.impl.configuration.S2CConfigFinishConfiguration1_20_2;
+import net.raphimc.netminecraft.packet.impl.login.C2SLoginStartConfiguration1_20_2;
+import net.raphimc.viaproxy.proxy.session.ProxyConnection;
+import net.raphimc.viaproxy.util.logging.Logger;
+
+import java.util.List;
+
+public class ConfigurationPacketHandler extends PacketHandler {
+
+    private final int configurationAcknowledgedId;
+    private final int startConfigurationId;
+
+    public ConfigurationPacketHandler(ProxyConnection proxyConnection) {
+        super(proxyConnection);
+
+        this.configurationAcknowledgedId = MCPackets.C2S_CONFIGURATION_ACKNOWLEDGED.getId(proxyConnection.getClientVersion().getVersion());
+        this.startConfigurationId = MCPackets.S2C_START_CONFIGURATION.getId(proxyConnection.getClientVersion().getVersion());
+    }
+
+    @Override
+    public boolean handleC2P(IPacket packet, List<ChannelFutureListener> listeners) {
+        if (packet instanceof UnknownPacket) {
+            final UnknownPacket unknownPacket = (UnknownPacket) packet;
+            if (unknownPacket.packetId == this.configurationAcknowledgedId) {
+                listeners.add(f -> {
+                    if (f.isSuccess()) {
+                        Logger.u_info("session", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Switching to CONFIGURATION state");
+                        this.proxyConnection.setConnectionState(ConnectionState.CONFIGURATION);
+                        this.proxyConnection.getChannel().config().setAutoRead(true);
+                    }
+                });
+            }
+        } else if (packet instanceof C2SLoginStartConfiguration1_20_2) {
+            this.proxyConnection.getC2P().config().setAutoRead(false);
+            listeners.add(f -> {
+                if (f.isSuccess()) {
+                    this.proxyConnection.setConnectionState(ConnectionState.CONFIGURATION);
+                    this.proxyConnection.getChannel().config().setAutoRead(true);
+                    this.proxyConnection.getC2P().config().setAutoRead(true);
+                }
+            });
+        } else if (packet instanceof C2SConfigFinishConfiguration1_20_2) {
+            listeners.add(f -> {
+                if (f.isSuccess()) {
+                    Logger.u_info("session", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Configuration finished! Switching to PLAY state");
+                    this.proxyConnection.setConnectionState(ConnectionState.PLAY);
+                    this.proxyConnection.getChannel().config().setAutoRead(true);
+                }
+            });
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean handleP2S(IPacket packet, List<ChannelFutureListener> listeners) {
+        if (packet instanceof UnknownPacket) {
+            final UnknownPacket unknownPacket = (UnknownPacket) packet;
+            if (unknownPacket.packetId == this.startConfigurationId) {
+                this.proxyConnection.getChannel().config().setAutoRead(false);
+            }
+        } else if (packet instanceof S2CConfigFinishConfiguration1_20_2) {
+            this.proxyConnection.getChannel().config().setAutoRead(false);
+        }
+
+        return true;
+    }
+
+}
diff --git a/src/main/java/net/raphimc/viaproxy/proxy/packethandler/CustomPayloadPacketHandler.java b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/CustomPayloadPacketHandler.java
new file mode 100644
index 0000000..0e1d517
--- /dev/null
+++ b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/CustomPayloadPacketHandler.java
@@ -0,0 +1,75 @@
+/*
+ * This file is part of ViaProxy - https://github.com/RaphiMC/ViaProxy
+ * Copyright (C) 2023 RK_01/RaphiMC and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package net.raphimc.viaproxy.proxy.packethandler;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFutureListener;
+import net.raphimc.netminecraft.constants.MCPackets;
+import net.raphimc.netminecraft.packet.IPacket;
+import net.raphimc.netminecraft.packet.PacketTypes;
+import net.raphimc.netminecraft.packet.UnknownPacket;
+import net.raphimc.netminecraft.packet.impl.login.C2SLoginCustomPayloadPacket;
+import net.raphimc.netminecraft.packet.impl.login.C2SLoginKeyPacket1_7;
+import net.raphimc.vialoader.util.VersionEnum;
+import net.raphimc.viaproxy.proxy.external_interface.OpenAuthModConstants;
+import net.raphimc.viaproxy.proxy.session.ProxyConnection;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+public class CustomPayloadPacketHandler extends PacketHandler {
+
+    private final int customPayloadId;
+
+    public CustomPayloadPacketHandler(ProxyConnection proxyConnection) {
+        super(proxyConnection);
+
+        this.customPayloadId = MCPackets.C2S_PLUGIN_MESSAGE.getId(proxyConnection.getClientVersion().getVersion());
+    }
+
+    @Override
+    public boolean handleC2P(IPacket packet, List<ChannelFutureListener> listeners) {
+        if (packet instanceof UnknownPacket) {
+            final UnknownPacket unknownPacket = (UnknownPacket) packet;
+            if (unknownPacket.packetId == this.customPayloadId) {
+                final ByteBuf data = Unpooled.wrappedBuffer(unknownPacket.data);
+                final String channel = PacketTypes.readString(data, Short.MAX_VALUE); // channel
+                if (channel.equals(OpenAuthModConstants.DATA_CHANNEL) && this.proxyConnection.handleCustomPayload(PacketTypes.readVarInt(data), data)) {
+                    return false;
+                }
+            }
+        } else if (packet instanceof C2SLoginCustomPayloadPacket) {
+            final C2SLoginCustomPayloadPacket loginCustomPayload = (C2SLoginCustomPayloadPacket) packet;
+            if (loginCustomPayload.response != null && this.proxyConnection.handleCustomPayload(loginCustomPayload.queryId, Unpooled.wrappedBuffer(loginCustomPayload.response))) {
+                return false;
+            }
+        } else if (packet instanceof C2SLoginKeyPacket1_7) {
+            final C2SLoginKeyPacket1_7 loginKeyPacket = (C2SLoginKeyPacket1_7) packet;
+
+            if (this.proxyConnection.getClientVersion().isOlderThanOrEqualTo(VersionEnum.r1_12_2) && new String(loginKeyPacket.encryptedNonce, StandardCharsets.UTF_8).equals(OpenAuthModConstants.DATA_CHANNEL)) { // 1.8-1.12.2 OpenAuthMod response handling
+                final ByteBuf byteBuf = Unpooled.wrappedBuffer(loginKeyPacket.encryptedSecretKey);
+                this.proxyConnection.handleCustomPayload(PacketTypes.readVarInt(byteBuf), byteBuf);
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+}
diff --git a/src/main/java/net/raphimc/viaproxy/proxy/packethandler/LoginPacketHandler.java b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/LoginPacketHandler.java
new file mode 100644
index 0000000..5147584
--- /dev/null
+++ b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/LoginPacketHandler.java
@@ -0,0 +1,219 @@
+/*
+ * This file is part of ViaProxy - https://github.com/RaphiMC/ViaProxy
+ * Copyright (C) 2023 RK_01/RaphiMC and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package net.raphimc.viaproxy.proxy.packethandler;
+
+import com.mojang.authlib.GameProfile;
+import io.netty.channel.ChannelFutureListener;
+import net.raphimc.netminecraft.constants.ConnectionState;
+import net.raphimc.netminecraft.constants.MCPackets;
+import net.raphimc.netminecraft.constants.MCPipeline;
+import net.raphimc.netminecraft.netty.crypto.AESEncryption;
+import net.raphimc.netminecraft.netty.crypto.CryptUtil;
+import net.raphimc.netminecraft.packet.IPacket;
+import net.raphimc.netminecraft.packet.UnknownPacket;
+import net.raphimc.netminecraft.packet.impl.login.*;
+import net.raphimc.vialegacy.protocols.release.protocol1_7_2_5to1_6_4.storage.ProtocolMetadataStorage;
+import net.raphimc.vialoader.util.VersionEnum;
+import net.raphimc.viaproxy.cli.options.Options;
+import net.raphimc.viaproxy.proxy.LoginState;
+import net.raphimc.viaproxy.proxy.external_interface.AuthLibServices;
+import net.raphimc.viaproxy.proxy.external_interface.ExternalInterface;
+import net.raphimc.viaproxy.proxy.session.ProxyConnection;
+import net.raphimc.viaproxy.proxy.util.CloseAndReturn;
+import net.raphimc.viaproxy.util.logging.Logger;
+
+import javax.crypto.SecretKey;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ExecutionException;
+
+public class LoginPacketHandler extends PacketHandler {
+
+    private static final KeyPair KEY_PAIR = CryptUtil.generateKeyPair();
+    private static final Random RANDOM = new Random();
+
+    private final byte[] verifyToken = new byte[4];
+    private LoginState loginState = LoginState.FIRST_PACKET;
+
+    private final int chatSessionUpdateId;
+
+    public LoginPacketHandler(ProxyConnection proxyConnection) {
+        super(proxyConnection);
+
+        RANDOM.nextBytes(this.verifyToken);
+        this.chatSessionUpdateId = MCPackets.C2S_CHAT_SESSION_UPDATE.getId(proxyConnection.getClientVersion().getVersion());
+    }
+
+    @Override
+    public boolean handleC2P(IPacket packet, List<ChannelFutureListener> listeners) throws GeneralSecurityException {
+        if (packet instanceof UnknownPacket) {
+            final UnknownPacket unknownPacket = (UnknownPacket) packet;
+            if (unknownPacket.packetId == this.chatSessionUpdateId && this.proxyConnection.getChannel().attr(MCPipeline.ENCRYPTION_ATTRIBUTE_KEY).get() == null) {
+                return false;
+            }
+        } else if (packet instanceof C2SLoginHelloPacket1_7) {
+            if (this.loginState != LoginState.FIRST_PACKET) throw CloseAndReturn.INSTANCE;
+            this.loginState = LoginState.SENT_HELLO;
+            final C2SLoginHelloPacket1_7 loginHelloPacket = (C2SLoginHelloPacket1_7) packet;
+
+            if (packet instanceof C2SLoginHelloPacket1_19) {
+                final C2SLoginHelloPacket1_19 packet1_19 = (C2SLoginHelloPacket1_19) packet;
+                if (packet1_19.expiresAt != null && packet1_19.expiresAt.isBefore(Instant.now())) {
+                    throw new IllegalStateException("Expired public key");
+                }
+            }
+
+            proxyConnection.setLoginHelloPacket(loginHelloPacket);
+            if (packet instanceof C2SLoginHelloPacket1_19_3) {
+                proxyConnection.setGameProfile(new GameProfile(((C2SLoginHelloPacket1_19_3) packet).uuid, loginHelloPacket.name));
+            } else {
+                proxyConnection.setGameProfile(new GameProfile(null, loginHelloPacket.name));
+            }
+
+            if (Options.ONLINE_MODE) {
+                this.proxyConnection.getC2P().writeAndFlush(new S2CLoginKeyPacket1_8("", KEY_PAIR.getPublic().getEncoded(), this.verifyToken)).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+            } else {
+                ExternalInterface.fillPlayerData(this.proxyConnection);
+                this.proxyConnection.getChannel().writeAndFlush(this.proxyConnection.getLoginHelloPacket()).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+            }
+
+            return false;
+        } else if (packet instanceof C2SLoginKeyPacket1_7) {
+            if (this.loginState != LoginState.SENT_HELLO) throw CloseAndReturn.INSTANCE;
+            this.loginState = LoginState.SENT_KEY;
+            final C2SLoginKeyPacket1_7 loginKeyPacket = (C2SLoginKeyPacket1_7) packet;
+
+            if (loginKeyPacket.encryptedNonce != null) {
+                if (!Arrays.equals(this.verifyToken, CryptUtil.decryptData(KEY_PAIR.getPrivate(), loginKeyPacket.encryptedNonce))) {
+                    Logger.u_err("auth", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Invalid verify token");
+                    this.proxyConnection.kickClient("§cInvalid verify token!");
+                }
+            } else {
+                final C2SLoginKeyPacket1_19 keyPacket = (C2SLoginKeyPacket1_19) packet;
+                final C2SLoginHelloPacket1_19 helloPacket = (C2SLoginHelloPacket1_19) this.proxyConnection.getLoginHelloPacket();
+                if (helloPacket.key == null || !CryptUtil.verifySignedNonce(helloPacket.key, this.verifyToken, keyPacket.salt, keyPacket.signature)) {
+                    Logger.u_err("auth", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Invalid verify token");
+                    this.proxyConnection.kickClient("§cInvalid verify token!");
+                }
+            }
+
+            final SecretKey secretKey = CryptUtil.decryptSecretKey(KEY_PAIR.getPrivate(), loginKeyPacket.encryptedSecretKey);
+            this.proxyConnection.getC2P().attr(MCPipeline.ENCRYPTION_ATTRIBUTE_KEY).set(new AESEncryption(secretKey));
+
+            final String userName = this.proxyConnection.getGameProfile().getName();
+
+            try {
+                final String serverHash = new BigInteger(CryptUtil.computeServerIdHash("", KEY_PAIR.getPublic(), secretKey)).toString(16);
+                final GameProfile mojangProfile = AuthLibServices.SESSION_SERVICE.hasJoinedServer(this.proxyConnection.getGameProfile(), serverHash, null);
+                if (mojangProfile == null) {
+                    Logger.u_err("auth", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Invalid session");
+                    this.proxyConnection.kickClient("§cInvalid session! Please restart minecraft (and the launcher) and try again.");
+                } else {
+                    this.proxyConnection.setGameProfile(mojangProfile);
+                }
+                Logger.u_info("auth", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Authenticated as " + this.proxyConnection.getGameProfile().getId().toString());
+            } catch (Throwable e) {
+                throw new RuntimeException("Failed to make session request for user '" + userName + "'!", e);
+            }
+
+            ExternalInterface.fillPlayerData(this.proxyConnection);
+            this.proxyConnection.getChannel().writeAndFlush(this.proxyConnection.getLoginHelloPacket()).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean handleP2S(IPacket packet, List<ChannelFutureListener> listeners) throws GeneralSecurityException, ExecutionException, InterruptedException {
+        if (packet instanceof S2CLoginKeyPacket1_7) {
+            final S2CLoginKeyPacket1_7 loginKeyPacket = (S2CLoginKeyPacket1_7) packet;
+
+            final PublicKey publicKey = CryptUtil.decodeRsaPublicKey(loginKeyPacket.publicKey);
+            final SecretKey secretKey = CryptUtil.generateSecretKey();
+            final String serverHash = new BigInteger(CryptUtil.computeServerIdHash(loginKeyPacket.serverId, publicKey, secretKey)).toString(16);
+
+            boolean auth = true;
+            if (this.proxyConnection.getServerVersion().isOlderThanOrEqualTo(VersionEnum.r1_6_4)) {
+                auth = this.proxyConnection.getUserConnection().get(ProtocolMetadataStorage.class).authenticate;
+            }
+            if (auth) {
+                ExternalInterface.joinServer(serverHash, this.proxyConnection);
+            }
+
+            final byte[] encryptedSecretKey = CryptUtil.encryptData(publicKey, secretKey.getEncoded());
+            final byte[] encryptedNonce = CryptUtil.encryptData(publicKey, loginKeyPacket.nonce);
+
+            final C2SLoginKeyPacket1_19_3 loginKey = new C2SLoginKeyPacket1_19_3(encryptedSecretKey, encryptedNonce);
+            if (this.proxyConnection.getServerVersion().isNewerThanOrEqualTo(VersionEnum.r1_19) && this.proxyConnection.getLoginHelloPacket() instanceof C2SLoginHelloPacket1_19 && ((C2SLoginHelloPacket1_19) this.proxyConnection.getLoginHelloPacket()).key != null) {
+                ExternalInterface.signNonce(loginKeyPacket.nonce, loginKey, this.proxyConnection);
+            }
+            this.proxyConnection.getChannel().writeAndFlush(loginKey).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+
+            if (this.proxyConnection.getServerVersion().isNewerThanOrEqualTo(VersionEnum.r1_7_2tor1_7_5)) {
+                this.proxyConnection.getChannel().attr(MCPipeline.ENCRYPTION_ATTRIBUTE_KEY).set(new AESEncryption(secretKey));
+            } else {
+                this.proxyConnection.setKeyForPreNettyEncryption(secretKey);
+            }
+
+            return false;
+        } else if (packet instanceof S2CLoginSuccessPacket1_7) {
+            final S2CLoginSuccessPacket1_7 loginSuccessPacket = (S2CLoginSuccessPacket1_7) packet;
+
+            if (this.proxyConnection.getClientVersion().isNewerThanOrEqualTo(VersionEnum.r1_8)) {
+                if (Options.COMPRESSION_THRESHOLD > -1 && this.proxyConnection.getC2P().attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).get() == -1) {
+                    this.proxyConnection.getChannel().config().setAutoRead(false);
+                    this.proxyConnection.getC2P().writeAndFlush(new S2CLoginCompressionPacket(Options.COMPRESSION_THRESHOLD)).addListeners(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE, (ChannelFutureListener) f -> {
+                        if (f.isSuccess()) {
+                            this.proxyConnection.getC2P().attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).set(Options.COMPRESSION_THRESHOLD);
+                            this.proxyConnection.getChannel().config().setAutoRead(true);
+                        }
+                    });
+                }
+            }
+
+            final ConnectionState nextState = this.proxyConnection.getClientVersion().isNewerThanOrEqualTo(VersionEnum.r1_20_2) ? ConnectionState.CONFIGURATION : ConnectionState.PLAY;
+
+            this.proxyConnection.setGameProfile(new GameProfile(loginSuccessPacket.uuid, loginSuccessPacket.name));
+            Logger.u_info("session", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Connected successfully! Switching to " + nextState + " state");
+
+            this.proxyConnection.getChannel().config().setAutoRead(false);
+            listeners.add(f -> {
+                if (f.isSuccess() && nextState != ConnectionState.CONFIGURATION) {
+                    this.proxyConnection.setConnectionState(nextState);
+                    this.proxyConnection.getChannel().config().setAutoRead(true);
+                }
+            });
+        } else if (packet instanceof S2CLoginCompressionPacket) {
+            final S2CLoginCompressionPacket loginCompressionPacket = (S2CLoginCompressionPacket) packet;
+
+            this.proxyConnection.getChannel().attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).set(loginCompressionPacket.compressionThreshold);
+            return false;
+        }
+
+        return true;
+    }
+
+}
diff --git a/src/main/java/net/raphimc/viaproxy/proxy/packethandler/PacketHandler.java b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/PacketHandler.java
new file mode 100644
index 0000000..a14f56a
--- /dev/null
+++ b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/PacketHandler.java
@@ -0,0 +1,42 @@
+/*
+ * This file is part of ViaProxy - https://github.com/RaphiMC/ViaProxy
+ * Copyright (C) 2023 RK_01/RaphiMC and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package net.raphimc.viaproxy.proxy.packethandler;
+
+import io.netty.channel.ChannelFutureListener;
+import net.raphimc.netminecraft.packet.IPacket;
+import net.raphimc.viaproxy.proxy.session.ProxyConnection;
+
+import java.util.List;
+
+public abstract class PacketHandler {
+
+    protected final ProxyConnection proxyConnection;
+
+    public PacketHandler(final ProxyConnection proxyConnection) {
+        this.proxyConnection = proxyConnection;
+    }
+
+    public boolean handleC2P(final IPacket packet, final List<ChannelFutureListener> listeners) throws Exception {
+        return true;
+    }
+
+    public boolean handleP2S(final IPacket packet, final List<ChannelFutureListener> listeners) throws Exception {
+        return true;
+    }
+
+}
diff --git a/src/main/java/net/raphimc/viaproxy/proxy/packethandler/ResourcePackPacketHandler.java b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/ResourcePackPacketHandler.java
new file mode 100644
index 0000000..0752745
--- /dev/null
+++ b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/ResourcePackPacketHandler.java
@@ -0,0 +1,96 @@
+/*
+ * This file is part of ViaProxy - https://github.com/RaphiMC/ViaProxy
+ * Copyright (C) 2023 RK_01/RaphiMC and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package net.raphimc.viaproxy.proxy.packethandler;
+
+import com.viaversion.viaversion.api.Via;
+import com.viaversion.viaversion.libs.gson.JsonElement;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFutureListener;
+import net.raphimc.netminecraft.constants.MCPackets;
+import net.raphimc.netminecraft.packet.IPacket;
+import net.raphimc.netminecraft.packet.PacketTypes;
+import net.raphimc.netminecraft.packet.UnknownPacket;
+import net.raphimc.vialoader.util.VersionEnum;
+import net.raphimc.viaproxy.cli.options.Options;
+import net.raphimc.viaproxy.proxy.session.ProxyConnection;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class ResourcePackPacketHandler extends PacketHandler {
+
+    private final int joinGameId;
+
+    public ResourcePackPacketHandler(ProxyConnection proxyConnection) {
+        super(proxyConnection);
+
+        this.joinGameId = MCPackets.S2C_JOIN_GAME.getId(this.proxyConnection.getClientVersion().getVersion());
+    }
+
+    @Override
+    public boolean handleP2S(IPacket packet, List<ChannelFutureListener> listeners) {
+        if (packet instanceof UnknownPacket) {
+            final UnknownPacket unknownPacket = (UnknownPacket) packet;
+            if (unknownPacket.packetId == this.joinGameId) {
+                listeners.add(f -> {
+                    if (f.isSuccess()) {
+                        this.sendResourcePack();
+                    }
+                });
+            }
+        }
+
+        return true;
+    }
+
+    private void sendResourcePack() {
+        if (Options.RESOURCE_PACK_URL != null) {
+            this.proxyConnection.getChannel().eventLoop().schedule(() -> {
+                if (this.proxyConnection.getClientVersion().isNewerThanOrEqualTo(VersionEnum.r1_8)) {
+                    final ByteBuf resourcePackPacket = Unpooled.buffer();
+                    PacketTypes.writeVarInt(resourcePackPacket, MCPackets.S2C_RESOURCE_PACK.getId(this.proxyConnection.getClientVersion().getVersion()));
+                    PacketTypes.writeString(resourcePackPacket, Options.RESOURCE_PACK_URL); // url
+                    PacketTypes.writeString(resourcePackPacket, ""); // hash
+                    if (this.proxyConnection.getClientVersion().isNewerThanOrEqualTo(VersionEnum.r1_17)) {
+                        resourcePackPacket.writeBoolean(Via.getConfig().isForcedUse1_17ResourcePack()); // required
+                        final JsonElement promptMessage = Via.getConfig().get1_17ResourcePackPrompt();
+                        if (promptMessage != null) {
+                            resourcePackPacket.writeBoolean(true); // has message
+                            PacketTypes.writeString(resourcePackPacket, promptMessage.toString()); // message
+                        } else {
+                            resourcePackPacket.writeBoolean(false); // has message
+                        }
+                    }
+                    this.proxyConnection.getC2P().writeAndFlush(resourcePackPacket).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+                } else if (this.proxyConnection.getClientVersion().isNewerThanOrEqualTo(VersionEnum.r1_7_2tor1_7_5)) {
+                    final byte[] data = Options.RESOURCE_PACK_URL.getBytes(StandardCharsets.UTF_8);
+
+                    final ByteBuf customPayloadPacket = Unpooled.buffer();
+                    PacketTypes.writeVarInt(customPayloadPacket, MCPackets.S2C_PLUGIN_MESSAGE.getId(this.proxyConnection.getClientVersion().getVersion()));
+                    PacketTypes.writeString(customPayloadPacket, "MC|RPack"); // channel
+                    customPayloadPacket.writeShort(data.length); // length
+                    customPayloadPacket.writeBytes(data); // data
+                    this.proxyConnection.getC2P().writeAndFlush(customPayloadPacket).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+                }
+            }, 250, TimeUnit.MILLISECONDS);
+        }
+    }
+
+}
diff --git a/src/main/java/net/raphimc/viaproxy/proxy/packethandler/StatusPacketHandler.java b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/StatusPacketHandler.java
new file mode 100644
index 0000000..7052312
--- /dev/null
+++ b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/StatusPacketHandler.java
@@ -0,0 +1,42 @@
+/*
+ * This file is part of ViaProxy - https://github.com/RaphiMC/ViaProxy
+ * Copyright (C) 2023 RK_01/RaphiMC and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package net.raphimc.viaproxy.proxy.packethandler;
+
+import io.netty.channel.ChannelFutureListener;
+import net.raphimc.netminecraft.packet.IPacket;
+import net.raphimc.netminecraft.packet.impl.status.S2CStatusPongPacket;
+import net.raphimc.viaproxy.proxy.session.ProxyConnection;
+
+import java.util.List;
+
+public class StatusPacketHandler extends PacketHandler {
+
+    public StatusPacketHandler(ProxyConnection proxyConnection) {
+        super(proxyConnection);
+    }
+
+    @Override
+    public boolean handleP2S(IPacket packet, List<ChannelFutureListener> listeners) {
+        if (packet instanceof S2CStatusPongPacket) {
+            listeners.add(ChannelFutureListener.CLOSE);
+        }
+
+        return true;
+    }
+
+}
diff --git a/src/main/java/net/raphimc/viaproxy/proxy/packethandler/UnexpectedPacketHandler.java b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/UnexpectedPacketHandler.java
new file mode 100644
index 0000000..13f5124
--- /dev/null
+++ b/src/main/java/net/raphimc/viaproxy/proxy/packethandler/UnexpectedPacketHandler.java
@@ -0,0 +1,43 @@
+/*
+ * This file is part of ViaProxy - https://github.com/RaphiMC/ViaProxy
+ * Copyright (C) 2023 RK_01/RaphiMC and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package net.raphimc.viaproxy.proxy.packethandler;
+
+import io.netty.channel.ChannelFutureListener;
+import net.raphimc.netminecraft.constants.ConnectionState;
+import net.raphimc.netminecraft.packet.IPacket;
+import net.raphimc.viaproxy.proxy.session.ProxyConnection;
+
+import java.util.List;
+
+public class UnexpectedPacketHandler extends PacketHandler {
+
+    public UnexpectedPacketHandler(ProxyConnection proxyConnection) {
+        super(proxyConnection);
+    }
+
+    @Override
+    public boolean handleC2P(IPacket packet, List<ChannelFutureListener> listeners) {
+        final ConnectionState connectionState = this.proxyConnection.getConnectionState();
+        if (connectionState.equals(ConnectionState.HANDSHAKING)) {
+            throw new IllegalStateException("Unexpected packet in " + connectionState + " state");
+        }
+
+        return true;
+    }
+
+}
diff --git a/src/main/java/net/raphimc/viaproxy/proxy/proxy2server/Proxy2ServerHandler.java b/src/main/java/net/raphimc/viaproxy/proxy/proxy2server/Proxy2ServerHandler.java
index aaf92a9..b5c3d15 100644
--- a/src/main/java/net/raphimc/viaproxy/proxy/proxy2server/Proxy2ServerHandler.java
+++ b/src/main/java/net/raphimc/viaproxy/proxy/proxy2server/Proxy2ServerHandler.java
@@ -17,53 +17,27 @@
  */
 package net.raphimc.viaproxy.proxy.proxy2server;
 
-import com.mojang.authlib.GameProfile;
-import com.viaversion.viaversion.api.Via;
-import com.viaversion.viaversion.libs.gson.JsonElement;
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
+import com.google.common.collect.Lists;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.SimpleChannelInboundHandler;
-import net.raphimc.netminecraft.constants.ConnectionState;
-import net.raphimc.netminecraft.constants.MCPackets;
-import net.raphimc.netminecraft.constants.MCPipeline;
-import net.raphimc.netminecraft.netty.crypto.AESEncryption;
-import net.raphimc.netminecraft.netty.crypto.CryptUtil;
 import net.raphimc.netminecraft.packet.IPacket;
-import net.raphimc.netminecraft.packet.PacketTypes;
-import net.raphimc.netminecraft.packet.UnknownPacket;
-import net.raphimc.netminecraft.packet.impl.login.*;
-import net.raphimc.netminecraft.packet.impl.status.S2CPingResponsePacket;
-import net.raphimc.vialegacy.protocols.release.protocol1_7_2_5to1_6_4.storage.ProtocolMetadataStorage;
-import net.raphimc.vialoader.util.VersionEnum;
-import net.raphimc.viaproxy.cli.options.Options;
-import net.raphimc.viaproxy.proxy.external_interface.ExternalInterface;
+import net.raphimc.viaproxy.proxy.packethandler.PacketHandler;
 import net.raphimc.viaproxy.proxy.session.ProxyConnection;
 import net.raphimc.viaproxy.proxy.util.ExceptionUtil;
 import net.raphimc.viaproxy.util.logging.Logger;
 
-import javax.crypto.SecretKey;
-import java.math.BigInteger;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.PublicKey;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
+import java.util.List;
 
 public class Proxy2ServerHandler extends SimpleChannelInboundHandler<IPacket> {
 
     private ProxyConnection proxyConnection;
 
-    private int joinGamePacketId = -1;
-
     @Override
     public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
         super.channelRegistered(ctx);
 
         this.proxyConnection = ProxyConnection.fromChannel(ctx.channel());
-
-        this.joinGamePacketId = MCPackets.S2C_JOIN_GAME.getId(this.proxyConnection.getClientVersion().getVersion());
     }
 
     @Override
@@ -81,31 +55,13 @@ public class Proxy2ServerHandler extends SimpleChannelInboundHandler<IPacket> {
     protected void channelRead0(ChannelHandlerContext ctx, IPacket packet) throws Exception {
         if (this.proxyConnection.isClosed()) return;
 
-        switch (this.proxyConnection.getConnectionState()) {
-            case LOGIN:
-                if (packet instanceof S2CLoginKeyPacket1_7) this.handleLoginKey((S2CLoginKeyPacket1_7) packet);
-                else if (packet instanceof S2CLoginSuccessPacket1_7) this.handleLoginSuccess((S2CLoginSuccessPacket1_7) packet);
-                else if (packet instanceof S2CLoginCompressionPacket) this.handleLoginCompression((S2CLoginCompressionPacket) packet);
-                else break;
-
+        final List<ChannelFutureListener> listeners = Lists.newArrayList(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+        for (PacketHandler packetHandler : this.proxyConnection.getPacketHandlers()) {
+            if (!packetHandler.handleP2S(packet, listeners)) {
                 return;
-            case STATUS:
-                if (packet instanceof S2CPingResponsePacket) {
-                    this.proxyConnection.getC2P().writeAndFlush(packet).addListeners(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE, ChannelFutureListener.CLOSE);
-                    return;
-                }
-                break;
-            case PLAY:
-                final UnknownPacket unknownPacket = (UnknownPacket) packet;
-                if (unknownPacket.packetId == this.joinGamePacketId) {
-                    this.proxyConnection.getC2P().writeAndFlush(packet).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
-                    this.sendResourcePack();
-                    return;
-                }
-                break;
+            }
         }
-
-        this.proxyConnection.getC2P().writeAndFlush(packet).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+        this.proxyConnection.getC2P().writeAndFlush(packet).addListeners(listeners.toArray(new ChannelFutureListener[0]));
     }
 
     @Override
@@ -113,95 +69,4 @@ public class Proxy2ServerHandler extends SimpleChannelInboundHandler<IPacket> {
         ExceptionUtil.handleNettyException(ctx, cause, this.proxyConnection);
     }
 
-    private void handleLoginKey(final S2CLoginKeyPacket1_7 packet) throws InterruptedException, GeneralSecurityException, ExecutionException {
-        final PublicKey publicKey = CryptUtil.decodeRsaPublicKey(packet.publicKey);
-        final SecretKey secretKey = CryptUtil.generateSecretKey();
-        final String serverHash = new BigInteger(CryptUtil.computeServerIdHash(packet.serverId, publicKey, secretKey)).toString(16);
-
-        boolean auth = true;
-        if (this.proxyConnection.getServerVersion().isOlderThanOrEqualTo(VersionEnum.r1_6_4)) {
-            auth = this.proxyConnection.getUserConnection().get(ProtocolMetadataStorage.class).authenticate;
-        }
-        if (auth) {
-            ExternalInterface.joinServer(serverHash, this.proxyConnection);
-        }
-
-        final byte[] encryptedSecretKey = CryptUtil.encryptData(publicKey, secretKey.getEncoded());
-        final byte[] encryptedNonce = CryptUtil.encryptData(publicKey, packet.nonce);
-
-        final C2SLoginKeyPacket1_19_3 loginKey = new C2SLoginKeyPacket1_19_3(encryptedSecretKey, encryptedNonce);
-        if (this.proxyConnection.getServerVersion().isNewerThanOrEqualTo(VersionEnum.r1_19) && this.proxyConnection.getLoginHelloPacket() instanceof C2SLoginHelloPacket1_19 && ((C2SLoginHelloPacket1_19) this.proxyConnection.getLoginHelloPacket()).key != null) {
-            ExternalInterface.signNonce(packet.nonce, loginKey, this.proxyConnection);
-        }
-        this.proxyConnection.getChannel().writeAndFlush(loginKey).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
-
-        if (this.proxyConnection.getServerVersion().isNewerThanOrEqualTo(VersionEnum.r1_7_2tor1_7_5)) {
-            this.proxyConnection.getChannel().attr(MCPipeline.ENCRYPTION_ATTRIBUTE_KEY).set(new AESEncryption(secretKey));
-        } else {
-            this.proxyConnection.setKeyForPreNettyEncryption(secretKey);
-        }
-    }
-
-    private void handleLoginSuccess(final S2CLoginSuccessPacket1_7 packet) {
-        if (this.proxyConnection.getClientVersion().isNewerThanOrEqualTo(VersionEnum.r1_8)) {
-            if (Options.COMPRESSION_THRESHOLD > -1 && this.proxyConnection.getC2P().attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).get() == -1) {
-                this.proxyConnection.getChannel().config().setAutoRead(false);
-                this.proxyConnection.getC2P().writeAndFlush(new S2CLoginCompressionPacket(Options.COMPRESSION_THRESHOLD)).addListeners(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE, (ChannelFutureListener) f -> {
-                    if (f.isSuccess()) {
-                        this.proxyConnection.getC2P().attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).set(Options.COMPRESSION_THRESHOLD);
-                        this.proxyConnection.getChannel().config().setAutoRead(true);
-                    }
-                });
-            }
-        }
-
-        this.proxyConnection.setGameProfile(new GameProfile(packet.uuid, packet.name));
-        Logger.u_info("connect", this.proxyConnection.getC2P().remoteAddress(), this.proxyConnection.getGameProfile(), "Connected successfully! Switching to PLAY state");
-
-        this.proxyConnection.getChannel().config().setAutoRead(false);
-        this.proxyConnection.getC2P().writeAndFlush(packet).addListeners(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE, (ChannelFutureListener) f -> {
-            if (f.isSuccess()) {
-                this.proxyConnection.setConnectionState(ConnectionState.PLAY);
-                this.proxyConnection.getChannel().config().setAutoRead(true);
-            }
-        });
-    }
-
-    private void handleLoginCompression(final S2CLoginCompressionPacket packet) {
-        this.proxyConnection.getChannel().attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).set(packet.compressionThreshold);
-    }
-
-    private void sendResourcePack() {
-        if (Options.RESOURCE_PACK_URL != null) {
-            this.proxyConnection.getChannel().eventLoop().schedule(() -> {
-                if (this.proxyConnection.getClientVersion().isNewerThanOrEqualTo(VersionEnum.r1_8)) {
-                    final ByteBuf resourcePackPacket = Unpooled.buffer();
-                    PacketTypes.writeVarInt(resourcePackPacket, MCPackets.S2C_RESOURCE_PACK.getId(this.proxyConnection.getClientVersion().getVersion()));
-                    PacketTypes.writeString(resourcePackPacket, Options.RESOURCE_PACK_URL); // url
-                    PacketTypes.writeString(resourcePackPacket, ""); // hash
-                    if (this.proxyConnection.getClientVersion().isNewerThanOrEqualTo(VersionEnum.r1_17)) {
-                        resourcePackPacket.writeBoolean(Via.getConfig().isForcedUse1_17ResourcePack()); // required
-                        final JsonElement promptMessage = Via.getConfig().get1_17ResourcePackPrompt();
-                        if (promptMessage != null) {
-                            resourcePackPacket.writeBoolean(true); // has message
-                            PacketTypes.writeString(resourcePackPacket, promptMessage.toString()); // message
-                        } else {
-                            resourcePackPacket.writeBoolean(false); // has message
-                        }
-                    }
-                    this.proxyConnection.getC2P().writeAndFlush(resourcePackPacket).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
-                } else if (this.proxyConnection.getClientVersion().isNewerThanOrEqualTo(VersionEnum.r1_7_2tor1_7_5)) {
-                    final byte[] data = Options.RESOURCE_PACK_URL.getBytes(StandardCharsets.UTF_8);
-
-                    final ByteBuf customPayloadPacket = Unpooled.buffer();
-                    PacketTypes.writeVarInt(customPayloadPacket, MCPackets.S2C_PLUGIN_MESSAGE.getId(this.proxyConnection.getClientVersion().getVersion()));
-                    PacketTypes.writeString(customPayloadPacket, "MC|RPack"); // channel
-                    customPayloadPacket.writeShort(data.length); // length
-                    customPayloadPacket.writeBytes(data); // data
-                    this.proxyConnection.getC2P().writeAndFlush(customPayloadPacket).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
-                }
-            }, 250, TimeUnit.MILLISECONDS);
-        }
-    }
-
 }
diff --git a/src/main/java/net/raphimc/viaproxy/proxy/session/ProxyConnection.java b/src/main/java/net/raphimc/viaproxy/proxy/session/ProxyConnection.java
index 56425f7..cac6858 100644
--- a/src/main/java/net/raphimc/viaproxy/proxy/session/ProxyConnection.java
+++ b/src/main/java/net/raphimc/viaproxy/proxy/session/ProxyConnection.java
@@ -41,12 +41,15 @@ import net.raphimc.netminecraft.packet.registry.PacketRegistryUtil;
 import net.raphimc.netminecraft.util.ServerAddress;
 import net.raphimc.vialoader.util.VersionEnum;
 import net.raphimc.viaproxy.proxy.external_interface.OpenAuthModConstants;
+import net.raphimc.viaproxy.proxy.packethandler.PacketHandler;
 import net.raphimc.viaproxy.proxy.util.CloseAndReturn;
 import net.raphimc.viaproxy.util.logging.Logger;
 
 import java.security.GeneralSecurityException;
 import java.security.Key;
+import java.util.ArrayList;
 import java.util.Base64;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
@@ -59,6 +62,8 @@ public class ProxyConnection extends NetClient {
     public static final AttributeKey<ProxyConnection> PROXY_CONNECTION_ATTRIBUTE_KEY = AttributeKey.valueOf("proxy_connection");
 
     private final Channel c2p;
+    private final List<PacketHandler> packetHandlers = new ArrayList<>();
+
     private final AtomicInteger customPayloadId = new AtomicInteger(0);
     private final Map<Integer, CompletableFuture<ByteBuf>> customPayloadListener = new ConcurrentHashMap<>();
 
@@ -110,6 +115,10 @@ public class ProxyConnection extends NetClient {
         return this.c2p;
     }
 
+    public List<PacketHandler> getPacketHandlers() {
+        return this.packetHandlers;
+    }
+
     public ServerAddress getServerAddress() {
         return this.serverAddress;
     }
@@ -176,6 +185,11 @@ public class ProxyConnection extends NetClient {
                     this.getChannel().attr(MCPipeline.PACKET_REGISTRY_ATTRIBUTE_KEY).set(PacketRegistryUtil.getLoginRegistry(true, this.clientVersion.getVersion()));
                 this.c2p.attr(MCPipeline.PACKET_REGISTRY_ATTRIBUTE_KEY).set(PacketRegistryUtil.getLoginRegistry(false, this.clientVersion.getVersion()));
                 break;
+            case CONFIGURATION:
+                if (this.getChannel() != null)
+                    this.getChannel().attr(MCPipeline.PACKET_REGISTRY_ATTRIBUTE_KEY).set(PacketRegistryUtil.getConfigurationRegistry(true, this.clientVersion.getVersion()));
+                this.c2p.attr(MCPipeline.PACKET_REGISTRY_ATTRIBUTE_KEY).set(PacketRegistryUtil.getConfigurationRegistry(false, this.clientVersion.getVersion()));
+                break;
             case PLAY:
                 if (this.getChannel() != null)
                     this.getChannel().attr(MCPipeline.PACKET_REGISTRY_ATTRIBUTE_KEY).set(PacketRegistryUtil.getPlayRegistry(true, this.clientVersion.getVersion()));
@@ -241,15 +255,20 @@ public class ProxyConnection extends NetClient {
         Logger.u_err("kick", this.c2p.remoteAddress(), this.getGameProfile(), message.replaceAll("§.", ""));
 
         final ChannelFuture future;
-        if (this.connectionState == ConnectionState.LOGIN) {
+        if (this.connectionState == ConnectionState.STATUS) {
+            future = this.c2p.writeAndFlush(new S2CStatusResponsePacket("{\"players\":{\"max\":0,\"online\":0},\"description\":" + new JsonPrimitive(message) + ",\"version\":{\"protocol\":-1,\"name\":\"ViaProxy\"}}"));
+        } else if (this.connectionState == ConnectionState.LOGIN) {
             future = this.c2p.writeAndFlush(new S2CLoginDisconnectPacket(messageToJson(message)));
+        } else if (this.connectionState == ConnectionState.CONFIGURATION) {
+            final ByteBuf disconnectPacket = Unpooled.buffer();
+            PacketTypes.writeVarInt(disconnectPacket, MCPackets.S2C_CONFIG_DISCONNECT.getId(this.clientVersion.getVersion()));
+            PacketTypes.writeString(disconnectPacket, messageToJson(message));
+            future = this.c2p.writeAndFlush(disconnectPacket);
         } else if (this.connectionState == ConnectionState.PLAY) {
             final ByteBuf disconnectPacket = Unpooled.buffer();
             PacketTypes.writeVarInt(disconnectPacket, MCPackets.S2C_DISCONNECT.getId(this.clientVersion.getVersion()));
             PacketTypes.writeString(disconnectPacket, messageToJson(message));
             future = this.c2p.writeAndFlush(disconnectPacket);
-        } else if (this.connectionState == ConnectionState.STATUS) {
-            future = this.c2p.writeAndFlush(new S2CStatusResponsePacket("{\"players\":{\"max\":0,\"online\":0},\"description\":" + new JsonPrimitive(message) + ",\"version\":{\"protocol\":-1,\"name\":\"ViaProxy\"}}"));
         } else {
             future = this.c2p.newSucceededFuture();
         }